Commit Graph
96 Commits
Author SHA1 Message Date
host f73c3f7a05 pr 超时:git/gh 子进程默认 120 秒超时,超时转 502 不挂 handler
此前 /pr 的子进程调用没有超时:远端通路卡死时 handler
线程永久挂住,客户端断开也收不回来。_run 真实路径一律
带 timeout(默认 120 秒,CLOUD_PR_SUBPROCESS_TIMEOUT
可改,非法值回落默认),超时杀子进程、返回 124,调用方
按原有失败路径处理(push/pr_create/commit 抛 PrStepError,
探针按跳过处理)。App 路径的 GitHub API 调用已有 30 秒
超时,本次不动。测试:真实 sleep 超时 2 个、环境变量
2 项、超时映射 push/commit 2 个
2026-10-26 17:01:47 +08:00
openqj 37d049d6b7 Merge pull request #39 from openqj/app-token
App-token:推分支建 PR 改走 GitHub App
2026-10-26 16:49:46 +08:00
host 25d9b3d938 App-token:推分支建 PR 改走 GitHub App installation token
- github_app:纯标准库 RS256 签 App JWT(PKCS#1/PKCS#8),
  换 installation token(现换现用、不缓存),按仓库名查安装
  记录,任一步失败回落 None
- pr:app_token 路径 push 走 credential helper(token 只进
  子进程环境),建 PR 走 REST API(读 default_branch 当 base);
  不传 token 时 gh 路径不变
- server:/pr 端点按项目 repo_url 解析仓库名、现换 token,
  响应加 auth_via=app|pat,换失败回落 PAT 不报错
- 测试:JWT/换 token/解析 9 个、pr App 路径 6 个、端点级
  App 与 PAT 回落 2 个
2026-10-26 16:47:55 +08:00
openqj ee4b01c8f6 Merge pull request #37 from openqj/multi-key
多 key:OAuth 登录不再轮换
2026-10-26 16:30:15 +08:00
host 7b85b4a4fd 多 key:OAuth 登录签发附加 key 不再轮换,keys 管理端点 2026-10-26 16:27:42 +08:00
openqj aa5ad41eba Merge pull request #36 from openqj/github-app-pg-dialect
修 GitHub App 安装在 Postgres 上崩
2026-10-26 16:13:58 +08:00
host 9fa80bef89 修 GitHub App 安装在 Postgres 上崩:适配层译 INSERT OR IGNORE,安装后增减仓库同步 2026-10-26 16:11:29 +08:00
openqj f07b3ccc7c Merge pull request #35 from openqj/github-app-install
GitHub App 一键安装
2026-10-26 16:05:08 +08:00
host 32059178bd GitHub App 一键安装:manifest 建 App、安装记录、双 secret 验签、/fix-review 内容去重 2026-10-26 16:02:45 +08:00
openqj ecf4b5c715 Merge pull request #34 from openqj/bind-external-identity
oauth: bind existing accounts to external identities
2026-10-26 15:50:12 +08:00
host f12b18412a oauth: bind existing accounts to external identities
New external_identities table (UNIQUE(provider, external_id),
UNIQUE(provider, user_id)) + bind/initiate, bindings list/unbind
endpoints. Callback branches on bind_user_id in state: bind success
shows no key and no user id; conflicts are 409
(identity_bound_to_other / user_already_bound); repeat bind to the
same user+identity is idempotent. Login resolution checks bindings
first: a bound identity lands in the bound account and rotates its
key like any OAuth login; unbound identities keep the deterministic
oauth:<provider>:<login> mapping. Unbind restores oauth mapping.
Tests: new cloud/tests/test_oauth_bind.py (11). Full suites: host
29 OK, cloud 389 OK (4 pre-existing skips).
2026-10-26 15:47:25 +08:00
openqj 5735ea16d6 Merge pull request #32 from openqj/pr-commit-before-push
pr: commit worktree changes before push
2026-10-26 15:22:57 +08:00
host 0b96b2929c pr: commit worktree changes before push; fix merge/pr order docs
POST /tasks/{id}/pr used to push the branch as-is, so uncommitted
worktree changes never reached the PR (empty diff). Now, after all
skip checks pass, the endpoint commits the worktree on host/<task>
(guarded: refuses when the worktree is on another branch), cleans
__pycache__/*.pyc like host.py merge does, and falls back to the
cloud identity when no git identity is configured. Nothing-to-commit
proceeds. Docs corrected: PR comes after status-merge and before
any local git merge (which deletes branch+worktree).
2026-10-26 15:20:02 +08:00
openqj b984171f27 Merge pull request #30 from openqj/monitor-autoscaler-check
monitor.sh: worker check accepts autoscaler (static worker retired)
2026-10-26 11:03:49 +08:00
host c6754d0233 monitor.sh: worker check accepts autoscaler (static worker retired)
Since the autoscaler takeover (static codex-cloud-worker stopped at
2026-10-26 07:02:55, disabled by design), the monitor's worker check
(systemctl is-active codex-cloud-worker) has failed every 5 minutes
while the real workers were healthy (health worker_ok kept passing,
GET /workers shows live autoscale-1 + host-1). A monitor that always
fails masks real failures. Fix: pass if either codex-autoscaler or
codex-cloud-worker is active, covering both deployment modes; the
health check still covers worker heartbeat freshness.
2026-10-26 11:01:52 +08:00
openqj 7c0e9366f8 Merge pull request #29 from openqj/docker-runtime-prod
Production docker tasks: --docker-runtime runsc in autoscaler unit
2026-10-26 10:38:36 +08:00
host a55d2bcf4b Production docker tasks: --docker-runtime runsc in autoscaler unit
One-off worker verification passed before this flip (2026-10-26):
production-shaped docker task (apparmor=docker-codex, pids-limit 512,
tenant isolation, egress proxy 8888, --docker-runtime runsc) completed
in 60s with gpt-6.1-sol, worktree quadruple + proof verified, template
and tenant repo copies clean. README now states the production state.
2026-10-26 10:36:42 +08:00
openqj 54834c3744 Merge pull request #28 from openqj/docker-runtime-runsc
Docker tasks: --docker-runtime option (runsc support, default off)
2026-10-26 10:29:14 +08:00
host 9f05178f2b Docker tasks: --docker-runtime option (runsc support, default off)
DockerExecutor gains a runtime parameter (passed as docker --runtime
in build_run_command, after --pids-limit); worker CLI gains
--docker-runtime, wired through _build_cli_executor. Unset or empty
means no --runtime flag, so existing tasks keep docker default (runc);
an unregistered runtime fails container start (fail closed), it never
silently falls back. Tests: runsc appears in the command, default/empty
omit it, CLI wiring passes through. README documents the flag and
corrects a stale multi-tenant note (host kernel does not ban userns;
bwrap fails inside Docker containers on nested-pidns proc mount).
2026-10-26 10:26:21 +08:00
openqj b31e9365de Merge pull request #27 from openqj/host-profile-fixes
Fix codex-host profile (capability + netlink for bwrap)
2026-10-26 08:41:53 +08:00
host 4a78ca1e95 Fix codex-host profile: capability allow + netlink (bwrap needs both)
Two bwrap failures found by live host tasks, both in the
AppArmor profile (not in codex or the worker):
1. deny capability sys_admin (etc.) breaks bwrap at userns
   creation: codex-host with the deny list failed, the same
   profile with capability, allowed it. codexsvc holds no host
   capabilities, so the allow only covers fake-root caps inside
   bwrap's own user namespace (scoped, harmless to the host);
   network/ptrace/file denies still hold independently.
2. bwrap brings up loopback in its new net namespace via a
   NETLINK_ROUTE socket: without network netlink raw, codex
   workspace-write fails with 'Failed to bind NETLINK_ROUTE
   socket' and no tool runs. And a bare 'deny network raw'
   overrides the netlink allow (deny wins over allow), so the
   raw deny must name inet/inet6 explicitly (verified by an
   isolated netlink bind test under each profile variant).
README correction in the same change: codex workspace-write
does NOT isolate /tmp -- an agent /tmp write lands on host
/tmp (verified). The verified boundary is: own worktree
writable, everything else read-only, /tmp shared.
2026-10-26 08:39:57 +08:00
openqj bfe3521ef7 Merge pull request #26 from openqj/host-sandbox
Host-level sandbox tasks (LocalExecutor + bwrap)
2026-10-26 08:26:53 +08:00
host 5d09af855f Host-level sandbox tasks: LocalExecutor + bwrap workspace-write
Docker outer layer and bwrap inner layer are mutually exclusive
on this host (fresh proc mounts fail with kernel EPERM inside
Docker's nested pidns, but work on the host under a userns
profile). So tasks that need an inner sandbox run on the host:
- Worker --claim-executor-only: strict executor claim (only
  executor == own type, never NULL). The host worker MUST use
  it, or it would steal NULL-executor tasks meant for Docker
  workers (those tasks would lose the Docker outer layer).
  Default behavior unchanged (NULL claimable by any worker).
- host-config-host.json: sandbox workspace-write, state
  work/host-state-host.json (distinct from Docker tasks'
  host-state.json), same relay home and test repo as Docker.
- deploy/apparmor/codex-host: docker-codex base + userns,
  mount, pivot_root for bwrap; still denies raw network,
  ptrace, dangerous capabilities, /proc/sys writes.
- deploy/host-worker/codex-host-worker.service: aa-exec -p
  codex-host, --executor local --claim-executor-only,
  capacity 2, proxy env 127.0.0.1:8888 so host tasks' relay/git
  egress also goes through the Tinyproxy allowlist (host direct
  egress would bypass it).
POST /tasks already accepts executor (local/docker/cloud), so
no API change was needed for routing.
Tests: 4 host-sandbox-routing tests (strict claim incl. CAS,
default still takes NULL, config sandbox/state, profile
allows bwrap without deny mount). Full suites pass.
2026-10-26 08:23:37 +08:00
openqj 67acddaf7c Merge pull request #25 from openqj/egress-fixes
Fix egress/AppArmor configs found during host install
2026-10-26 08:03:13 +08:00
host eae60cf3d7 Fix egress/AppArmor configs found during host install
- AppArmor network rules must use socket types (stream/dgram),
  not protocol names (tcp/udp/icmp): the parser rejected the
  profile (Invalid network entry) until fixed.
- Tinyproxy has no 'Upstream none' syntax; the line failed the
  service at startup. Direct connection is the default, so the
  line is removed.
- Tinyproxy Filter is POSIX BASIC regex, not extended: the
  extended-style anchored patterns denied even allowed domains
  (verified: xcmapi.org got 403). Rewritten in basic syntax
  (\(...\)* groups); the anchored form still blocks
  evilgithub.com and xcmapi.org.evil.com (verified on host).
- Filter file must be world-readable (was 640 root:root, so
  tinyproxy could not read it and exited on reload); install
  steps now chmod 644 (documented in the filter header via
  the install note in tinyproxy.conf).
Host already runs these exact files (proxy allow/block
verified: xcmapi/github/api.github 200, example/evilgithub/
xcmapi.org.evil.com 403).
2026-10-26 08:01:20 +08:00
openqj 7859d5dd8c Merge pull request #24 from openqj/outer-hardening
Outer hardening: AppArmor, pids limit, egress allowlist
2026-10-26 07:43:57 +08:00
host 1e4623c61c Outer hardening: AppArmor profile, pids limit, egress allowlist proxy
Inner codex sandbox stays danger-full-access by choice (bwrap
path is a separate decision); this hardens everything around it:
- DockerExecutor: --security-opt plumbing (repeatable, dedups
  no-new-privileges), --pids-limit (CLI default 512, fork-bomb
  guard), per-call extra_env (-e NAME=VALUE, non-secrets only,
  e.g. proxy URLs; the model key still goes by -e NAME only).
- Worker: --egress-proxy-port (0 = off). When on, Docker task
  containers get HTTP(S)_PROXY pointing at the host filtering
  proxy via their network gateway IP (tenant network gateway
  or default bridge gateway). Gateway unresolvable => the
  attempt fails instead of running unproxied (fail closed,
  same rule as tenant network creation failure).
- AppArmor profile deploy/apparmor/docker-codex: docker-default
  base + deny network raw/packet, deny ptrace, deny mount/
  umount/pivot_root, deny dangerous capabilities, deny writes
  to /proc/sys, /sys, /proc/keys. Deliberately NO userns rule
  (that would enable the bwrap path, not this one). File rules
  mirror docker-default (broad + sensitive denies); a strict
  file allowlist would need profiling codex's file access set
  and is not claimed here.
- Egress proxy deploy/egress/: Tinyproxy with
  FilterDefaultDeny + anchored domain allowlist (relay,
  github, pypi, npm). Anchoring matters: an unanchored
  'github.com' regex would also match 'evilgithub.com'.
- Seccomp honesty (README): Docker default seccomp already
  enforces on all task containers; no new stricter allowlist
  is claimed in this change (building one from memory risks
  silently breaking the toolchain; it needs syscall profiling
  first).
Tests: 3 outer-hardening tests (security-opt dedup, pids
limit, proxy env without leaking the key value, gateway
parse/fail, proxy fail-closed). Full suites pass.
2026-10-26 07:42:14 +08:00
openqj 4f7c7eb13e Merge pull request #23 from openqj/tenant-isolation
Multi-tenant isolation for Docker tasks
2026-10-26 07:26:33 +08:00
host 03a98124db Multi-tenant isolation for Docker tasks (per-project mounts + networks + drop caps)
Container-side isolation was project-blind: every task mounted
the shared /app/work (all worktrees + host-state), the shared
codex-home (all thread history), the shared repos dir and the
shared test-repo, and all containers shared the default bridge
network. With --tenant-isolation (Docker only; Local runs on
the host under one user and has no container boundary to
enforce), each project gets its own host dirs under
tenants_root/<project>/{work,codex-home,test-repo,<project>}:
- mounts: container paths stay identical (/app/work,
  codex-home, repos/<pid>, test-repo) so host.py config needs
  no change; only the host mount sources are per-project.
  Static --docker-extra-mounts are replaced per task (not
  appended), so a shared mount cannot leak back in.
- codex-home seeds ONLY the template's config.toml (relay
  endpoint/provider names, no secret values); thread_history
  is never copied (it holds other tasks' prompts/results).
  test-repo is a per-project copy of the fixture (worktree
  creation needs a writable repo; a shared writable fixture
  would let one tenant poison the next task's base code).
- repos: repo_url checkouts go to the tenant dir and task
  repo is set to the container path; no-repo_url tasks get
  the tenant test-repo copy instead of the shared fixture.
- network: one Docker network per project (codex-<pid>,
  created idempotently, already-exists is OK); network
  creation failure fails the task instead of silently
  falling back to the default network (a silent downgrade
  would make isolation fail quietly).
- hardening for all Docker runs: --cap-drop=ALL and
  --security-opt=no-new-privileges (non-root user was already
  the case); verified live by a good-model task, not assumed.
Honest limit, kept in README: the inner codex sandbox is
still danger-full-access (host kernel blocks user namespaces,
bwrap unusable), so the boundary is container + mounts +
network, not the inner sandbox.
Tests: 7 tenant tests (seeding incl. no-history, per-project
mounts, network sanitization, per-call override, ensure
network ok/fail). Full suites pass.
2026-10-26 07:24:54 +08:00
host b74deffb26 hotfix: autoscaler queue_depth NULL depends_on blinded scaler
queue_depth turned a NULL depends_on into [None], and
ready_tasks then required None to be in completed_ids, so
such tasks never counted as ready. Worker pick_next was
unaffected (_task_from_row maps NULL to []), which is why
tasks still ran while the scaler always saw depth 0 and
never scaled up (proven live: depth probe with 1 queued
task returned 0). NULL now means no dependencies, with a
regression test.
2026-10-26 07:13:45 +08:00
host 951730e224 autoscaler: fix double-prefixed worker log filename
Log file was autoscale-autoscale-1.log because worker_id already
carries the autoscale- prefix; now it is autoscale-1.log.
2026-10-26 07:05:04 +08:00
host 26daddf909 hotfix: autoscaler main() setup_logging arg
setup_logging() takes a log LEVEL, not a logger name; passing
'cloud.autoscaler' raised ValueError at startup, so the scaler
crash-looped under systemd and started zero workers (the host
had no worker at all until this fix). The '/opt/.../-m'
FileNotFoundError in the same journal lines is only Ubuntu's
apport hook misfiring on 'python3 -m', not a second bug.
Regression test: main() with mocked run_loop/signal must reach
the loop (this would have caught the crash before deploy).
2026-10-26 07:02:31 +08:00
host 1f4463209b deploy: fix host backup tar exclude patterns
The host backup tar runs with -C /opt codex-app-server, so its
members are named codex-app-server/..., and the old patterns
./work and ./worker-heartbeat.json never matched anything.
The live work/ dir (host-state, worktrees rewritten by running
workers) was therefore included, tar hit 'file changed as we
read it', exited 1, and set -e aborted the deploy. Patterns
are now codex-app-server/work and
codex-app-server/worker-heartbeat.json, which actually match.
2026-10-26 07:00:24 +08:00
host d1095853c1 deploy: exclude worker heartbeat from host backup tar
The worker rewrites ./worker-heartbeat.json every poll interval
(10s); the host backup tar reads the same directory, so it
could hit 'file changed as we read it', exit 1, and abort the
deploy under set -e (this killed the PR #22 deploy). The
heartbeat file is regenerable runtime state, not code, so
excluding it from the backup is correct, not a workaround.
2026-10-26 06:59:38 +08:00
openqj c5d245a7fb Merge pull request #22 from openqj/autoscaler
Autoscale workers by queue depth
2026-10-26 06:58:42 +08:00
host 7e147be9ed Autoscale workers by queue depth (adaptive threads + process scaler)
Layer 1, in-worker: run_all pool thread count is now adaptive
per round (min(capacity, ready_count)) instead of always
capacity threads; ready_count counts only dependency-satisfied
queued tasks via a shared _ready_tasks() helper (pick_next
refactored onto it), so blocked tasks never inflate the pool.

Layer 2, process scaler (cloud/autoscaler.py): a systemd-managed
daemon that starts/stops cloud.worker subprocesses by ready
queue depth. desired = ceil(depth / per_worker_capacity)
clamped to [min_workers, max_workers]; scale-up is immediate,
scale-down waits scale_down_after consecutive low checks
(default 3) to avoid flapping on bursty completions. Scale-down
is LIFO (youngest worker first), SIGTERM for graceful drain
(worker run_loop drains up to drain_timeout), SIGKILL of the
process group only after drain timeout. Depth read failure
keeps the current worker count (never scale on unknown state).
Worker command forces --loop/--worker-id/--worker-capacity
(appended last so argparse last-wins); --db comes from scaler
or worker args; workers inherit scaler env (Postgres etc.).
State file (depth/desired/actual/workers) written atomically
for observability. deploy.sh now restarts codex-autoscaler
instead of the static worker unit when the scaler is active,
so the two never poll the same queue at once.
Tests: 8 autoscaler tests (desired math, ready-only depth,
cycle=0, scale up/down, cooldown streak, forced worker flags).
2026-10-26 06:56:59 +08:00
openqj 67e1d3eb24 Merge pull request #21 from openqj/worker-concurrency-and-retry
Worker concurrency, survival (heartbeat/sweep/drain), retry endpoint
2026-10-26 06:48:48 +08:00
host d51c06a900 Worker concurrency (capacity enforced), survival (heartbeat/sweep/drain), retry endpoint
1. Concurrency: capacity is now a real limit, not display-only.
   run_all with capacity>1 runs a manual thread pool (threading,
   not concurrent.futures: a test in this repo puts cloud/ on
   sys.path, shadowing stdlib queue and breaking
   concurrent.futures' SimpleQueue import). Claim stays CAS, so
   two threads racing one task still yield one owner; max_tasks
   counts dispatched+running to avoid overshoot; stop signals
   stop new claims only, running tasks are drained by run_loop.
   Running state moved from single fields to a _running dict
   (proc/container per task) so cancel kills the right task.
   Docker timeout is now a per-call argument, never a mutated
   shared executor field. CostStore opens a fresh connection per
   record/report (shared sqlite connection is thread-bound and
   raised ProgrammingError under the pool; :memory: keeps the
   shared connection because a fresh one is an empty DB).
   Final status write is CAS on status='running' so a background
   thread cannot overwrite an interrupted task with completed.
2. Survival: run_loop starts a daemon heartbeat thread (every
   30s, refreshes running tasks' last_heartbeat) and sweeps stale
   tasks every round (threshold 300s, best-effort); live tasks
   never go stale now, dead workers' tasks are reclaimed without
   manual /admin/sweep. On exit, run_loop waits up to
   drain_timeout (CLI --drain-timeout, default 60s, inside
   systemd's 90s stop window) for running tasks before killing
   leftovers and marking interrupted.
3. Retry: POST /tasks/{id}/retry (maintainer+, 404/403/409):
   failed/interrupted/merge-failed go back to queued with
   result/error/model cleared, prompt/model/deps kept, CAS
   update, audit task_retried.
Tests: 5 concurrency tests (barrier overlap, serial capacity=1,
max_tasks, wait_for_running, CAS final write), 3 retry tests;
full suites pass.
2026-10-26 06:46:20 +08:00
openqj 341f4a83e1 Merge pull request #20 from openqj/worker-poll-perf
Perf: light worker poll query + tasks(status) index
2026-10-26 06:21:30 +08:00
host 26d9edac33 Perf: light worker poll query + tasks(status) index
Worker polls the DB every interval (production 30s) and every
poll ran SELECT * FROM tasks: full rows including result_text
and error_text (up to 4000 chars each) for every task ever,
growing with history. load_tasks now selects only the columns
pick_next actually needs (id/project/prompt/status/depends/
priority/timestamps/executor) and only queued/completed/merged
rows; running/failed rows are neither ready nor completed, so
excluding them cannot change pick_next. get_task still does
SELECT * for the single claimed task. Plus CREATE INDEX IF NOT
EXISTS idx_tasks_status on tasks(status) in both SQLite
init_db and the Postgres schema, so the poll filter stops being
a full table scan on old databases too. Test: load_tasks returns
only queued+completed out of queued/completed/running/failed.
2026-10-26 06:19:01 +08:00
openqj a1e492657a Merge pull request #19 from openqj/host-error-surfacing
Surface failed-turn reason from thread_history in dispatch output
2026-10-26 06:15:03 +08:00
host c114178463 Surface failed-turn reason: host.py recovers it from thread_history, dispatch prints it
Real bad-model turn: relay returns 404, codex retries sampling
5 times, turn fails, but app-server's turn/completed event
carries status=failed with NO error and NO text, so worker
never saw any fallbackable signal. The 404 actually lives in
codex's thread_history_1.sqlite (thread_turns.error_json).
host.py Server.run_turn now best-effort recovers that message
(read-only query by thread_id+turn_id, any failure -> None,
never breaks the turn itself), and dispatch CLI prints the
error field (run CLI already printed the full result). Worker
_attempt_error prefers the error field when text is empty, so
a double failure keeps the real reasons, not just status codes.
Tests: host TurnErrorFromHistoryTest (recovery + missing/corrupt
DB -> None), full host + cloud suites pass.
2026-10-26 06:13:27 +08:00
openqj 6a857e06bd Merge pull request #18 from openqj/fallback-real-turn-token-and-codex-home
Fallback: extract HTTP code from failed-turn JSON; Docker codex_home points at real relay home
2026-10-26 06:09:37 +08:00
host 5827716a3a Fallback: extract HTTP code from failed-turn JSON; Docker codex_home points at real relay home
1. Worker _fallback_token correction: real host.py never returns
status='404'; a failed turn is status='failed' with the real
error (e.g. '404 model not found') in text/error. The old code
returned host_status verbatim, so 'failed' blocked every real
fallback and the old event-only branch did too. Now: if
host_status itself is a fallbackable code it is used; otherwise
the code is extracted from turn JSON text/error, stdout, stderr
or runner error, and 'failed' alone never counts as a signal.
Tests: failed-turn JSON with 404 in text falls back and
succeeds; with 401 it never falls back.
2. Docker config codex_home: /var/lib/codex-app-server/codex-home,
mounted at the same path into the container, is the only home
that holds the real relay config (xcmapi provider, base_url,
env_key). Pointing codex_home at a fresh empty dir made codex
POST to api.openai.com with no auth header and loop on connect
timeouts until host.py's 600s turn timeout. The /app/work
codex-home from PR #17 was necessary to get past initialize
but insufficient for a real turn; this replaces it.
2026-10-26 06:07:07 +08:00
openqj edc187bb70 Merge pull request #17 from openqj/docker-config-codex-home
Docker config: writable codex_home under /app/work
2026-10-26 05:59:27 +08:00
host c1fba878d2 Docker config: writable codex_home under /app/work
Inside the container the default HOME is /root, which uid
1003 cannot write, so codex cannot create ~/.codex and the
app-server never answers initialize (host.py times out after
60s, task fails with queue.Empty). Setting codex_home to
/app/work/codex-home (mounted writable, owned by codexsvc uid
1003, same as the container user) lets the app-server start
and run turns. No secrets in this file.
2026-10-26 05:56:47 +08:00
openqj 0f2e427d93 Merge pull request #16 from openqj/docker-config-repo-and-stderr
Docker default repo + keep stderr in worker error_text
2026-10-26 05:48:17 +08:00
host 87a2ad0066 Docker default repo + keep stderr in worker error_text
Two live-test findings in one fix:
1. DockerExecutor has no repo fallback: when a project has no
repo_url, worker leaves task['repo'] unset and the container
host.py dispatch fails instantly with 'missing --repo' because
host-config-docker.json had no repo field either. Add the host
test-repo as the config default; projects with repo_url still
override it via worker's explicit --repo.
2. Worker discarded stderr from the final error_text, so every
container/host crash surfaced only as the generic 'host
execution failed' and was undebuggable from the task record.
When stdout is empty, error_text now keeps the stderr tail
(last 1000 chars, where the real error line lives).
2026-10-26 05:45:51 +08:00
openqj 3acf0d7b8c Merge pull request #15 from openqj/docker-config-state-path
Fix Docker config state path to work/host-state.json
2026-10-26 05:41:33 +08:00
host 619366aac8 Fix Docker config state path: work/host-state.json, not /work/host-state.json
/work is a bind mount whose host dir docker auto-creates as
root:root, so the container user (1003:1004) cannot write a
state lock there (Permission denied, task fails in ~1s).
work/host-state.json resolves relative to the config dir /app
to /app/work/host-state.json, which maps via the extra mount to
the host work dir owned by codexsvc (uid 1003, same as the
container user), so it is writable. /app/host-state.json itself
is still unusable (/app is mounted read-only).
2026-10-26 05:39:04 +08:00