Skip to content

perf(prd-262): worker cuts — 1,074 → 910 MB, toolchain kept - #1220

Merged
AutomatosAI merged 19 commits into
mainfrom
feat/prd-262-worker-cuts
Oct 20, 2026
Merged

AutomatosAI merged 19 commits into
mainfrom
feat/prd-262-worker-cuts

Conversation

@AutomatosAI

@AutomatosAI AutomatosAI commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

What and why

PRD-262 (Container diet), the workspace worker's code cuts from the 19 Oct size breakdown. W6 (#1166, squash f230dad) is already on main; this PR is the 16 commits the worker lane made after it (lane feat/prd-262-w6-worker, 4bc2514b0..dba924c95, local only; commit SHAs below are this branch's), replayed onto main with cherry-pick -x, plus one comment fix in the Helm chart.

The worker keeps its agent toolchain: gcc, g++, make, node, npm, corepack, pnpm, uv, git and Perl all stay (§3-D5, decided by Gerard on 19 Oct as trims only). What goes is what nothing in the image runs, two dependency swaps, and the aiohttp server.

Measured, local arm64 image, installed size: 1,340 MB before PRD-262 → 1,074 MB on main today (W0–W8) → 910.1 MB with this PR. gzip of the image: 374.7 MB.

The cuts

Row numbers are the 19 Oct ranked size breakdown. MB are installed bytes on arm64, as the breakdown measured them.

Row Cut MB Commit
16 gcc's lto-dump deleted in the build-essential RUN. gcc -flto runs lto1, lto-wrapper and the linker plugin, never lto-dump 29.4 0f5cfe9
19 The API's git trim (US-019): git-shell, scalar, git-daemon, git-http-backend, git-http-push, git-imap-send gone; git-core's second git becomes a link to /usr/bin/git. HTTPS and SSH clone, pull and push stay 23.2 0f5cfe9
47 gosu out; entrypoint.sh drops to the worker with setpriv (util-linux, already in python:3.12-slim) and sets HOME=/home/worker, which setpriv leaves as root's 2.9 a480864
21 Node's binary stripped (strip --strip-unneeded; .dynsym stays, so N-API addons resolve) in a node-stripped stage that media-render carries byte for byte. Copied first with COPY --link as a /node-layer/ tree, so both images get the same layer digest on every build 20.0 0761bc6, 3e6616e, 2f1419a
47 npm's docs and man pages (an npm stage deletes them), and the /tmp/node-compile-cache and /root/.npm logs the pnpm RUN left behind 4.1 ccb2d61
40 strip --strip-debug on site-packages *.so* (not *.libs/, which patchelf rewrote), in the stable lock's RUN 8.1 50ad695
47 site-packages tests/ directories (jsonschema, referencing, certifi) 0.62 50ad695
47 hiredis (redis-py uses its pure-Python parser) and python-dotenv (no worker module imports it) out of the lock 1.2 36fb598
27 psycopg2 built from source against Debian's libpq5 instead of psycopg2-binary (its wheel bundles libpq, OpenSSL, krb5, LDAP and SASL). See For Dave 15.7 f8821c1, cb9199e
36 The worker's HTTP server moves from aiohttp to the Starlette and uvicorn already in the image (mcp depends on them). aiohttp and six helper packages leave the lock. See For Dave 12.2 84f3742, 45c1bc5
12 §3-D5 trim: gcc's sanitizer runtimes (asan, hwasan, lsan, tsan, ubsan) and their archives, preinit objects and links, found from dpkg -L. 24 paths per arch. -fsanitize no longer links 42.2 (41.7 amd64) c885a01, 4907f8c
31 §3-D5 trim: libc.a and libstdc++.a. -static and -static-libstdc++ no longer link. libstdc++exp.a stays: GCC 14 ships it only as an archive and -lstdc++exp needs it for a dynamic link 12.2 c885a01

The breakdown's estimates add up to about 172 MB. Measured: 1,074 → 964.5 MB before the §3-D5 trims, then 910.1 MB with them (54.4 MB, the trims' estimate exactly).

Other changes that come with the cuts:

  • services/shared/automatos_metrics.py and its two vendored copies (byte-identical, as test_prd262_vendored_metrics.py requires) swap add_aiohttp_metrics for add_starlette_metrics. The worker was its only user. A 404 is now labelled 404, not 500.
  • docs/workspace-execution/file-operations.md describes the Starlette server and the one header that differs; _index.md says which builds no longer link.
  • orchestrator/core/workspace_client.py: a comment names the worker's body limit constant (no code change).
  • charts/automatos/templates/worker.yaml: a comment said the entrypoint drops privileges with gosu; it now says setpriv.

For Dave: the two dependency changes

1. psycopg2 from source instead of psycopg2-binary (row 27).

  • Same project, same release (2.9.13) and same API; only the packaging differs. task_executions_db.py is unchanged.
  • The binary wheel bundles its own libpq, OpenSSL 1.1 and 3, krb5, LDAP and SASL. Built from source, psycopg2 links Debian's libpq5, which the weekly OS rebuild keeps patched.
  • A separate psycopg2 build stage (same python:3.12-slim digest as production) installs gcc, libc6-dev, libpq-dev and binutils there only, builds the hash-checked sdist and strips the extension. Production installs libpq5 and copies the result in as its own layer.
  • Supply chain (security review of the first version, cb9199e): --require-hashes checked the sdist, but uv's isolated build fetched setuptools unpinned. Now requirements-build.in asks for setuptools>=70.1 (all psycopg2 2.9.13 needs), requirements-build.lock pins setuptools==84.0.0 with its sha256 hashes, the stage installs that lock first (--require-hashes --no-deps --only-binary :all:), and psycopg2 builds with --no-build-isolation. Nothing unhashed is installed.
  • Like the worker's other locks, Dependabot can't regenerate requirements-build.lock (its pip and uv updaters read only *.txt and *.in). test_prd262_dependabot.py lists it with that reason. How these locks are kept current is still open for you and Gerard.

2. Starlette and uvicorn listed directly; aiohttp removed (row 36).

  • requirements.in now lists starlette>=1.0.1,<2.0.0 (GHSA-86qp-5c8j-p5mr, as the orchestrator) and uvicorn>=0.50.1,<1.0.0, because the worker imports them. The lock already had them through mcp (starlette 1.9.0, uvicorn 0.54.0) and keeps those pins. aiohttp, aiohappyeyeballs, aiosignal, frozenlist, multidict, propcache and yarl leave the lock. aiohttp was the server only; no worker module used it as a client.
  • Behaviour kept from aiohttp 3.13.5, checked against its source: every route, method (GET answers HEAD), status and JSON answer; the X-Internal-Token check (now constant time); the 1 MiB body limit (a body of exactly 1 MiB is the 400, as aiohttp's >=); the 3630 s keep-alive (aiohttp's RequestHandler default), well past the API's 5 s httpx pool expiry; a 60 s graceful shutdown; no trailing-slash redirect. A port that can't be bound logs and ends the health server only, as aiohttp did; the worker keeps consuming tasks.
  • One header differs: a download whose file name a header cannot carry as it is (non-ASCII, a quote) is named with RFC 5987 filename*=, where aiohttp sent raw UTF-8 or broke on a quote.
  • Tracing (your feat(worker): the workspace worker continues the API's trace (PRD-256 O2b, #847) #1044, PRD-256 O2b) is now an ASGI middleware, outermost as before. It continues the caller's traceparent, labels by route template (or "unmatched"), never traces /health or /metrics, and flushes on shutdown.

§3-D5 and what waits until after WebSummit

  • §3-D5 (worker toolchain): decided 19 Oct as trims only, rows Update memory_knowledge_system.py #12 and Chat rebrand #31 above. No toolchain-free variant.
  • §9-Q14, the bundled Claude CLI (267 MB, the biggest single item left): not in this PR. After WebSummit.
  • Also after WebSummit, not in this PR: §9-Q20 (git/Perl), §3-D4 (fp16 voice, media-render), W9 and W10.

How it was proven

Local image build and run (arm64, 20 Oct, from the local integration branch before the replay onto main). services/workspace-worker/, services/shared/automatos_metrics.py and the two in-image scripts on this branch are byte-identical to what was built (integration commit b8b7e004a).

  • scripts/ci/image_worker_toolchain.py: 12 of 12 checks passed: no npm docs, compile cache or logs; no lto-dump and gcc -flto links; no sanitizer runtime, archive or link left; no libc.a or libstdc++.a and everything a dynamic link needs kept; make, gcc and g++ build hello world linked dynamically; git's server-side binaries gone; git init, commit, clone, branch, checkout and pull; an HTTPS clone and the credential protocol; node stripped, keeps .dynsym and runs; a native N-API addon loads; npm, npx, corepack and pnpm run; no gosu and setpriv drops to the worker.
  • scripts/ci/image_worker_site_packages.py: 5 of 5: 8 stripped libraries load with no debug sections; no tests directories; psycopg2 links Debian's libpq; psycopg2 connects to Postgres and runs a query; the removed distributions are gone and their replacements import.
  • 38 of 38 route checks against the running worker, including the body-limit and keep-alive parity with aiohttp 3.13.
  • OTel: a request carrying a traceparent continues the caller's trace in the worker's server span.
  • The API's 10 of 10 worker client calls answered against the new server.
  • The INSTALL_BROWSER=true build (Railway's default) passes the same 12 toolchain checks and renders a PNG through the worker's workspace_html_to_png.

Static checks on this branch (no Docker, no database), all on HEAD against origin/main:

  • ruff 0.16.9 through diff-quality (CI's code-standards lane, changed lines only): 100%, 0 violations. Plain ruff --select F821,F401,F811,F841 on the changed files reports 6 F401s, all in the three automatos_metrics.py copies (typing.Optional, CollectorRegistry), on lines this PR doesn't touch; main has the same 6.
  • py_compile on all 24 changed .py files (3.11 and 3.12), bash -n entrypoint.sh, YAML load of images.yml and test.yml: all pass.
  • scripts/ci/check_changed_code_shape.py --base origin/main: 0 errors.
  • pytest (3.11, the orchestrator's pins for fastapi, starlette, uvicorn, psycopg2-binary, OpenTelemetry, aiohttp): every test_prd262_*.py file except the futureagi worker client one, plus test_prd256_o2b_worker_routes_moved.py, test_prd256_o2b_worker_span.py (including the API-to-worker one-trace test) and test_socials_worker_binary_pieces.py: 717 passed, 1 skipped (Linux /proc). Three need what isn't on this machine: the two test_prd262_web_stack.py route tests (the full API app) and the real-Postgres lifecycle test in test_prd262_worker_task_executions.py.
  • The other tests that load or read the worker (F173, F178, F220, the PRD-170 canvas suites, PRD-203 CS7/CS8, PRD-209 entrypoint, PRD-233, the line-ending and prod-parity tests, 19 files): 187 passed, 2 skipped (playwright and httpx2 not installed here). In test_prd256_o5_logs_metrics.py, 9 passed and 3 need a database. test_f333_a_second_workspaces_session_files_open.py needs the full API app and did not run.

Replay onto main

git cherry-pick -x 4bc2514b0..dba924c95 (the lane's head, local only), all 16 commits, each keeping its sign-off. Two picks conflicted, both in orchestrator/tests/test_prd262_node24_images.py, which W8 (#1182) had changed on main: main's NODE_COPIES map and assert copied == NODE_COPIES[path] are kept, and the lane's counting of copies from every stage built FROM node (with the /node-layer/ tree read as the binary) goes under it. Its docstring and the NODE_LAYER comment name both lanes' tests. images.yml, test.yml and test_prd262_dependabot.py merged on their own; every line the lane added is present, the only lines that differ are the docstring and comment just described.

Merging with the other PRD-262 PRs

Planned order: frontend (#1168), media (#1219), API, then this PR (after Dave's review). Trial merges (git merge-tree) of this branch:

  • With the media PR (perf(prd-262): media-render cuts — 1,344 → 1,212 MB #1219, feat/prd-262-media-cuts): one file, test_prd262_node24_images.py, two hunks, comments only (the docstring line and the NODE_LAYER comment). Take this PR's side in both: it names both lanes' tests. Every code line is identical on both sides, and media's comment on its own NODE_COPIES entry merges cleanly. After that resolution the node24 suite, media's test_prd262_media_render_node_strip.py and this PR's test_prd262_worker_diet.py pass together (760 passed with the rest of test_prd262_*). The node-stripped stage text, from the FROM node:24-trixie-slim@sha256:7f486cab… AS node line to its last RUN, is byte-identical in both Dockerfiles, on the same python:3.12-slim digest.
  • With the frontend PR: images.yml. The frontend's "The frontend image serves with no network…" step and this PR's two worker steps (toolchain, site-packages) land at the same spot after the scan policy step. Keep all three; each has its own if: matrix.name == ….
  • With the API PR (feat/prd-262-api): images.yml, where the API's in-image steps (stdlib bytecode layer, site-packages, botocore, scipy, system tools, left-out packages, pip layers) land at the same spot: keep all of them and this PR's two. And test_prd262_dependabot.py, where both add entries to the same map: keep this PR's requirements-build.lock entry and the API's lock entries.

Test plan

  • CI on this PR: the required orchestrator job, code standards, and images.yml for workspace-worker on amd64 and arm64, including the two new in-image steps (toolchain 12 checks; site-packages 5 checks, with psycopg2 against the compose Postgres image).
  • test.yml prod-image-parity: the worker's entrypoint check (uid 1000 and HOME=/home/worker), the locked-set step (pip check, SDK and uv versions) on the Railway build.
  • images.yml's size report for workspace-worker against edge.
  • After merge, the first Railway worker build: the psycopg2 stage compiles from the hashed sdist and the build passes.
  • After deploy: one canvas session and one workspace_html_to_png call in production; /metrics answers 200; a worker trace joins the API's.
  • Local stack rebuilt from main: the worker's and media-render's node layers have the same digest.

How it was verified

  • Tests added or updated, and they fail without this change (each commit body says how; e.g. 4907f8c: four tests in test_prd262_worker_d5_trims.py fail without its Dockerfile change)
  • CI green (a lane marked non-required counts too)
  • Both editions considered: local (AUTH_EDITION=local) and hosted. Railway builds INSTALL_BROWSER=true, proven locally above; compose and GHCR build false.
  • UI change: screenshots below. No UI change.

AI assistance

  • None
  • Assisted: tool(s): Claude Code (PRD-262 lane and replay); it wrote or changed: every change in this PR
  • I have read and understood every line of this change, and can explain it in review

Checklist (AGENTS.md)

  • Every commit signed off (git commit -s)
  • No secrets, customer data or credentials anywhere in the change. The CI Postgres probe uses trust auth on a throwaway network, no password.
  • Settings through orchestrator/config.py; no hardcoded values. The worker is a separate service with its own worker_config.py.
  • Replaced paths deleted in this pull request (no shims or V2 copies). aiohttp, add_aiohttp_metrics, gosu and psycopg2-binary are gone, not kept alongside.
  • Migration (if any): no migration.
  • New routes the frontend calls are in orchestrator/reports/route-manifest.json: no new routes; the worker's routes are unchanged.
  • Docs updated where behaviour changed (docs/workspace-execution/file-operations.md, _index.md, the chart comment)
  • New dependency: none new. psycopg2 replaces psycopg2-binary (same project); starlette and uvicorn were already installed through mcp and are now listed directly; setuptools is a build-stage-only pin. Dave reviews.

Summary by CodeRabbit

  • New Features

    • Workspace workers now use a Starlette/uvicorn server while preserving existing routes, authentication, responses, file limits, keep-alive, and graceful shutdown behavior.
    • Metrics and tracing now support Starlette routes, including unmatched requests and server errors.
    • Worker startup now runs with the worker identity and sets the correct home directory.
  • Improvements

    • Worker images are slimmer by removing unused development files and artifacts while retaining required tooling and libraries.
    • Downloads support standards-compliant filenames for broader compatibility.
  • Documentation

    • Updated workspace execution and file-operation documentation to reflect the current server, build, and authentication behavior.

…ries

Breakdown rows 16 and 19 (19 Oct size breakdown), about 52.6 MB installed on arm64.

- Row 16 (29.4 MB): /usr/bin/*lto-dump* goes in the apt layer that installs
  build-essential. gcc -flto runs lto1, lto-wrapper and the linker plugin, never
  lto-dump, which only prints LTO objects.
- Row 19 (23.2 MB): the API's git trim (PRD-262 US-019) applied to the worker:
  git-shell, scalar, git-daemon, git-http-backend, git-http-push and git-imap-send,
  both copies, and git-core's second copy of git becomes a link to /usr/bin/git.
  git-remote-http(s) and ssh stay, so clone, pull and push over HTTPS and SSH work.

scripts/ci/image_worker_toolchain.py runs in the worker image images.yml built, on
both arches: no lto-dump, two units compiled and linked with -flto run; the trimmed
binaries are gone, git-core's git links to /usr/bin/git, and git init, commit,
branch, checkout, clone, pull, an HTTPS clone and git credential fill all work.
orchestrator/tests/test_prd262_worker_diet.py pins the Dockerfile lines against
what the in-image check looks for, and the images.yml step.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit a94b76d65941486bd20648b0a3cd5a940d193824)
…, not gosu

Breakdown row 47 (worker smalls): gosu, 2.9 MB installed, leaves the apt list.
entrypoint.sh now ends
  exec setpriv --reuid=worker --regid=worker --init-groups env HOME=/home/worker python -m main "$@"
setpriv comes with util-linux in the python:3.12-slim base. It sets the real,
effective and saved uid and gid and the worker's supplementary groups, as gosu did,
but leaves HOME as root's, so the exec sets HOME=/home/worker: the canvas's Claude
CLI and ~/.cache read it.

- test_prd262_worker_boot.py runs the entrypoint with a stand-in setpriv and pins
  the new exec line.
- test.yml's parity job (Railway build) now also checks the worker starts with
  HOME=/home/worker, besides uid 1000.
- scripts/ci/image_worker_toolchain.py checks in the GHCR image that gosu is gone and
  that the same setpriv drop runs a command as 1000:1000 with HOME=/home/worker.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 97a496b95dfeb8a76b8cf5ef9698f6de13249b0d)
…shared with media-render

Breakdown row 21: 20.0 MB installed (123.3 MB node binary, of which .symtab,
.strtab and debug sections are 20 MB).

A node-stripped stage follows the node stage, byte for byte the text the
media-render lane adds to services/media-render/Dockerfile: it installs binutils in
the build stage only and runs strip --strip-unneeded --preserve-dates on
/usr/local/bin/node. strip keeps .dynsym, so native addons still resolve Node's
N-API symbols. The production stage copies the binary with
  COPY --link --from=node-stripped /usr/local/bin/node /usr/local/bin/node
as its first instruction, so on the same python:3.12-slim digest the worker's and
media-render's node layers are one blob; npm, corepack and pnpm are copied and
installed as before.

- scripts/ci/image_worker_toolchain.py (images.yml, both arches): node has no
  .symtab/.strtab/.debug_*, keeps .dynsym, prints v24 and a correct sha256; an N-API
  addon compiled in the image (ABI only, loaded by napi_register_module_v1) loads
  and answers; npm, npx, corepack and pnpm --version run on it.
- test_prd262_worker_diet.py pins the stage text, the COPY line and its position,
  and that any Dockerfile with a node-stripped stage carries the same text.
- test_prd262_node24_images.py now accepts Node's files from any stage built FROM
  node (and only Node's /usr/local paths), so the media lane's same change passes;
  test_prd262_worker_browser.py reads the new COPY line.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 1242473d5a625db02381f1c51eda20062209c1ed)
…stall's caches

Breakdown row 47 (worker smalls), about 4.1 MB installed:

- npm docs and man pages (2.67 MB): a new `npm` stage, FROM node, deletes
  node_modules/npm/docs and /man, and the production stage copies npm from it.
  The image has no man, so `npm help` never showed them; `npm <cmd> --help`
  prints from npm's own code.
- The compile-cache leftover (1.4 MB): Dockerfile:53-54 ran npm cache clean but
  left Node 24's /tmp/node-compile-cache, which npm writes, and npm's logs in
  /root/.npm. The same RUN now deletes both, last.

scripts/ci/image_worker_toolchain.py checks first, before any of its checks runs
npm, that none of those paths is in the image; npm, npx, corepack and pnpm still
run (row 21's check). test_prd262_worker_diet.py pins the stage, the copy and the
cleanup's place in the RUN.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit d485934d701bdc5fdb81ed58ef80e7a9e3bc895d)
…info and drops test suites

Breakdown rows 40 (8.1 MB) and 47's tests directories (0.62 MB).

The stable lock's RUN now ends, in the same layer, with the API's US-020 rule:
- find site-packages -type f -name '*.so*' -not -path '*.libs/*'
  -exec strip --strip-debug {} +: the .debug_* sections go (31.7 MB of .so become
  23.7, mostly cryptography's), .dynsym stays, and the auditwheel *.libs libraries,
  rewritten by patchelf, are left alone. binutils is already there with
  build-essential. The SDK's bundled CLI is installed later and matches no *.so*.
- find site-packages -type d -name tests -prune -exec rm -rf {} +: jsonschema's,
  referencing's and certifi's test suites.

scripts/ci/image_worker_site_packages.py, run by a new images.yml step in the worker
image on both arches as uid 1000: every *.so* outside *.libs has no .debug_* section
and loads in a fresh interpreter (an extension exporting PyInit_<name> is imported by
its dotted name, anything else opened with ctypes), cffi, cryptography, pydantic-core
and rpds among them; no tests directory is left. test_prd262_worker_diet.py pins the
two find commands and the step.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 602bcb5ac781428c4b6609bba11ec2c30eccc708)
Breakdown row 47 (worker smalls): hiredis 1.1 MB, python-dotenv 0.12 MB.

- requirements.in asks for redis instead of redis[hiredis]. redis-py falls back to
  its pure-Python reply parser, which the worker's queue rate (a BRPOP poll, task
  status writes and canvas publishes) does not notice.
- python-dotenv goes: no worker module imports dotenv, and no locked package
  depends on it.

Relocked with the lock's own command (uv pip compile ... --universal
--python-version 3.12 --generate-hashes): the only change is the two entries
gone; every other pin and hash is as it was. requirements-browser.lock, compiled
against it, is unchanged.

scripts/ci/image_worker_site_packages.py checks in the image that neither
distribution is installed and that redis.asyncio imports with
HIREDIS_AVAILABLE false; test_prd262_worker_diet.py pins requirements.in, both
locks and that no worker module imports either.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 6bef38a97ec9931601886bde2a0dca768c157097)
…ead of psycopg2-binary

Breakdown row 27: 15.7 MB installed (psycopg2_binary.libs, 16.13 MB, out; libpq5,
0.41 MB, in).

Dependency change, justified: psycopg2-binary and psycopg2 are the same project,
release (2.9.13, the lock's psycopg2-binary pin and PyPI's latest) and API; only the
packaging differs. The binary wheel bundles its own libpq, OpenSSL 1.1 and 3, krb5,
LDAP and SASL, libraries Debian already installs in this image; built from source,
psycopg2 links Debian's libpq5, which the weekly OS rebuild keeps patched. No code
change: task_executions_db.py's one UPDATE imports psycopg2 as before.

- requirements.in: psycopg2>=2.9.0,<3.0.0 replaces psycopg2-binary. Relocked with the
  lock's own command; the only change is that entry (its hashes are the sdist's and
  the Windows wheels'). The browser lock is unchanged.
- Dockerfile: the locks stage cuts psycopg2's entry out as psycopg2.lock (the build
  fails unless it holds psycopg2 alone). A psycopg2 stage, on the production stage's
  python:3.12-slim digest, installs gcc, libc6-dev, libpq-dev and binutils there
  only, builds the hash-checked sdist with uv (--require-hashes --no-binary psycopg2,
  isolated build), installs it into /install with bytecode, strips the extension's
  debug info and drops uv's lock file. The production stage installs libpq5 with
  the toolchain and copies /install into /usr/local as its own layer.

Proof: scripts/ci/image_worker_site_packages.py (images.yml, both arches, as uid
1000): no psycopg2-binary and no psycopg2_binary.libs; import psycopg2 reports
libpq 17 or later and _psycopg links Debian's libpq.so.5; and psycopg2 connects to
the compose stack's Postgres image, which the step starts on a throwaway network
with trust auth, and runs a parameterised query. test.yml's parity job runs pip
check on the Railway build. test_prd262_worker_locks.py now expects the
three-way split and the build stage's --prefix install; test_prd262_worker_diet.py
pins the swap, the stage, libpq5 and the step.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit dffab7a6105b1502f0a23dc11de683445c43cc84)
…ptools, without build isolation

Security review of dffab7a61 (MEDIUM, supply chain), breakdown row 27.

uv pip install --require-hashes checked psycopg2's sdist, but its build ran in uv's
isolated build environment, which fetches the build backend (setuptools) unpinned and
unhashed, past --require-hashes.

- services/workspace-worker/requirements-build.in asks for setuptools>=70.1, all
  psycopg2 2.9.13's pyproject.toml requires (that release carries its own
  bdist_wheel, so no wheel package). requirements-build.lock is compiled the way the
  other locks are (uv pip compile --universal --python-version 3.12
  --generate-hashes): setuptools==84.0.0 with its wheel's and sdist's sha256.
- The psycopg2 stage copies that lock, installs it first with --system --no-cache
  --compile-bytecode --require-hashes --no-deps --only-binary :all: (python:3.12-slim
  carries no setuptools), then builds psycopg2 with --no-build-isolation on it. The
  build now runs only on hash-checked files; nothing else is installed in the stage.

Checked locally in the scratchpad with uv 0.11.26 and Homebrew's pg_config: without
the build lock, the --no-build-isolation install fails (uv asks for setuptools; it
fetches nothing); with it, psycopg2 2.9.13 builds from the hashed sdist and imports.
setuptools 84.0.0's wheel still ships the distutils shim that psycopg2's setup.py
imports (build_ext, ccompiler.get_default_compiler, errors.CompileError) and
bdist_wheel. The Docker build itself is the build stage's to prove.

Guards:
- test_prd262_worker_diet.py: the stage copies requirements-build.lock, installs it
  wheels-only before the build, builds psycopg2 with --no-build-isolation, makes no
  other install, and every install in it is uv with --require-hashes --no-deps
  from a .lock; the build lock pins setuptools alone, with hashes.
- test_prd262_worker_locks.py: the build lock's compile command and flags, every line
  an exact pin or its hash, it satisfies requirements-build.in, and the install
  order now starts with it.
- test_prd262_dependabot.py: requirements-build.lock is listed with the same reason
  as the worker's other locks (no Dependabot updater regenerates a .lock).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit f054ad1df2e490f648692646d9883efb2f1c2dd8)
…d of aiohttp

Breakdown row 36: 12.2 MB installed. aiohttp, aiohappyeyeballs, aiosignal,
frozenlist, multidict, propcache and yarl leave the worker's lock. Starlette 1.9.0
and uvicorn 0.54.0 were already in it as mcp's dependencies; requirements.in now
lists them (starlette>=1.0.1 for GHSA-86qp-5c8j-p5mr, uvicorn>=0.50.1, the release
the platform's tests run) because the worker imports them. Relocked with the lock's
own command: the seven entries go, starlette and uvicorn keep their pins, click and
h11 lose an emscripten marker. aiohttp was the server only; no worker module used
it as a client, so nothing moves to httpx.

Every route keeps its path, its methods (GET answers HEAD), the X-Internal-Token
check (constant time) and its answers. What aiohttp did implicitly is explicit:
- read_json reads a body up to aiohttp's 1 MiB client_max_size; one byte more is
  the handler's 400 "Invalid JSON body", as before;
- json_response is aiohttp's json_response byte for byte (json.dumps,
  application/json; charset=utf-8);
- no trailing-slash redirect; a download is an attachment named after the file
  (RFC 5987 filename* for a name a header cannot carry, where aiohttp sent raw
  UTF-8 or broke on a quote);
- uvicorn holds idle connections 75 s (aiohttp's keep-alive, longer than httpx's
  5 s pool expiry, so the API's pooled connections never race a close), waits up
  to 60 s on shutdown (aiohttp's runner), logs through the worker's handlers, and
  never installs signal handlers: main.py's SIGTERM/SIGINT stop the worker, which
  stops the server, as before.

Tracing (Dave's #1044, PRD-256 O2b) is an ASGI middleware, outermost as before:
it matches the route the router will (route template or "unmatched" for 404 and
405), continues the caller's traceparent, records the status, marks a raised error
with its type only, never traces /health or /metrics, and flushes on lifespan end.
services/shared/automatos_metrics.py swaps add_aiohttp_metrics for
add_starlette_metrics (the worker was its only user; the vendored copies stay
byte-identical); /metrics keeps prometheus_client's exact content type and the
token check, and a 404 is now labelled 404, not 500.

Tests: test_prd256_o2b_worker_routes_moved.py, test_prd256_o2b_worker_span.py and
test_prd262_vendored_metrics.py are ported from aiohttp's TestServer to Starlette's
TestClient with the same assertions (the metrics one now also checks the status
label and a raised request's 500). test_prd262_worker_starlette.py pins the body
limit, the JSON bytes, HEAD, no redirect, the download header, metrics outside the
token check, keep-alive, the signals and serve() stopping with the worker.
scripts/ci/image_worker_site_packages.py checks in the image that the seven
distributions are gone and that the worker's modules import and build the app
with its 15 routes and /metrics.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit fedb6cc4ab54d650eb3f1f96202e9197f35145ab)
… limit and keep-alive, and survives a taken port

Breakdown row 36, corrections to fedb6cc4a, checked against aiohttp 3.13.5's source:

- Body limit: aiohttp's read() refused a body at len(body) >= client_max_size
  (web_request.py:675), so a body of exactly 1 MiB was already a 400 "Invalid JSON
  body". read_json now compares with >= too; it compared with >.
- Keep-alive: aiohttp 3.13's RequestHandler default keepalive_timeout is 3630 s
  (web_protocol.py:186), not 75 s, and the worker's AppRunner set none. uvicorn now
  holds idle connections 3630 s, well past the API's httpx pool expiry (5 s).
- Bind failure: uvicorn calls sys.exit when it cannot bind its port, and asyncio
  re-raises a SystemExit out of the event loop, which would have killed the whole
  worker. aiohttp's failed site.start() only ended the health task. serve() now runs
  uvicorn through _serve_or_log, which logs the failure and returns, so the worker
  keeps consuming tasks as before (Docker's healthcheck still reports it unhealthy).

test_prd262_worker_starlette.py: a body of 1 MiB - 1 is parsed and one of 1 MiB is
the 400; keep-alive is 3630; a new test binds the port first and checks serve()
returns and logs (it fails with the guard removed: checked by mutation). The helper's
docstring is rewrapped.

Ran in a throwaway uv env (pytest-asyncio, no orchestrator app):
test_prd262_worker_starlette, test_prd256_o2b_worker_routes_moved,
test_prd256_o2b_worker_span (minus the API one-trace test, which needs core),
test_prd262_vendored_metrics, F173 and F178: 43 passed on starlette 1.8.0 /
uvicorn 0.50.1 (CI's orchestrator pins) and 42 passed on 1.9.0 / 0.54.0 (the
worker lock's, FastAPI test excluded).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 0074914692b5c24aabfb7952d42b0f666e2adfdd)
… of the Starlette move

Breakdown row 36. fedb6cc4a's line said the move changed no answer; the JSON answers
and statuses are the same, but a download's Content-Disposition uses RFC 5987's
filename*= for a name a header cannot carry as it is, and Starlette's FileResponse
adds ETag, Last-Modified and Accept-Ranges. The line also names the kept 1 MiB body
limit and 3630 s keep-alive (007491469).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit fe4aa04f0b015f0d5690f67b1b16f5f0aeb70cdd)
…the Starlette move

Breakdown row 36. fe4aa04f0 said downloads newly carry ETag, Last-Modified and
Accept-Ranges; aiohttp 3.13.5's FileResponse sent all three as well
(web_fileresponse.py). Only the RFC 5987 filename*= for a name a header cannot carry
as it is differs.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit bffc96a39c49a1d623ffef4ab2fca196b6c04206)
…every build

Two local builds of the shared node-stripped stage gave media-render and the worker different node layers (diff_ids 78528fcd… and 0e337ed7…) although the binary was the same file (sha256 8f01756d…, mtime 1791963666): docker save showed only usr/local/bin differing, its mtime 1792448532 against 1792442969, each build's own time. strip renames its output into /usr/local/bin, and COPY --link keeps the parent directories' times in the layer. The stage now sets the directory's time back to the binary's upstream one, so the layer depends only on the node digest. Breakdown row #22 (shared stripped node).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 85951e703e532657848eae5096093572f7d1fefd)
…red node layer is the same on every build

The touch fix (previous commit) did not hold: rebuilt, media-render's node layer still carried usr/local/bin at the build's time (mtime 1792449603, 22:40:03Z) although the stage's own /usr/local/bin was back at the binary's 1791963666. A COPY of one file into /usr/local/bin stamps that directory when the layer is made, whatever the source directory says. Copying a tree instead keeps the tree's directory times: the stage writes the stripped binary to /node-layer/usr/local/bin/node (strip -o, dates preserved) and sets every directory of the tree to the binary's upstream time, and the images copy /node-layer/ to /. Checked with a two-stage probe built cached and with --no-cache: the file COPY gave two diff_ids (44df0b6a…, b854ab01…), the tree COPY the same one twice (63b8f90e…). Breakdown row #22 (shared stripped node).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit d848627cf7d095bdb7eb7d57578af24c27ce2ed1)
… and libstdc++ static archives

Rows #12 and #31 of the 19 Oct size breakdown. PRD-262 §3-D5 was decided on 19 Oct 2026 by Gerard: trims only, toolchain kept. gcc, g++, make, node, npm, pnpm, uv, git and Perl stay in the worker.

Row #12: the sanitizer runtimes (asan, hwasan, lsan, tsan, ubsan) go, with gcc's archives, preinit objects and links for them, so -fsanitize no longer links. The list comes from dpkg -L of the installed packages that match libgcc-[0-9]*-dev and lib<name>[0-9]*, filtered to /usr/lib/.../lib<name>.* and lib<name>_*. A package one arch lacks is skipped. On Debian trixie (gcc-14 14.2.0-19) both arches have all five, and the RUN deletes 23 paths on each: the ten runtime .so files and links in /usr/lib/<triplet>, and thirteen .a, _preinit.o and .so links in /usr/lib/gcc/<triplet>/14. That is 42.2 MB on arm64 and 41.7 MB on amd64 by package file sizes.

Row #31: libc.a (libc6-dev) and libstdc++.a (libstdc++-[0-9]*-dev) go, found the same way, 12.2 MB on each arch, so gcc -static and -static-libstdc++ no longer link. libstdc++exp.a stays, although the breakdown's 'how' listed it (0.9 MB): GCC 14 ships it only as a static archive, so an ordinary dynamic link of std::stacktrace (-lstdc++exp) needs it, which is more than -static. libgcc*.a, libc_nonshared.a, libssp_nonshared.a, crt*.o and the libc.so, libgcc_s.so and libstdc++.so links are never touched.

Each list is a name=$(... | grep ...) assignment in the && chain, the pattern the API lane uses for ICU, so a list that comes back empty fails the RUN. ldconfig runs after both.

Where: in the RUN that installs build-essential, so no layer keeps the files. The files are deleted and the packages stay registered, instead of the breakdown's dpkg --purge --force-depends: the browser step runs apt-get install later, and a purge would leave libgcc-14-dev's dependencies unmet, which would make that install fail.

Tests: orchestrator/tests/test_prd262_worker_d5_trims.py pins the RUN (order, no purge, dpkg-derived lists, nothing a dynamic link needs named) and runs its trim commands under sh with stand-in dpkg-query, dpkg, rm and ldconfig, fed the trixie packages' real file lists for amd64 and arm64: exactly the 23 sanitizer paths and the two archives go on each arch, a missing libhwasan0 still builds, and no sanitizer file or a missing archive fails the RUN. scripts/ci/image_worker_toolchain.py gains three in-image checks: no sanitizer file under /usr/lib or in ld.so's cache; gcc finds neither archive but finds every file a dynamic link needs; make builds a C and a C++ hello world with gcc and g++ that run and name libc.so.6 and libstdc++.so.6 as NEEDED. test_prd262_worker_diet.py's git-trim guard and row list follow the new RUN and checks. docs/workspace-execution/_index.md says what no longer links.

Not built here: no docker was run. The image build has to prove it.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 83f574e7088da7db0ab52366cb15d891868bd9c7)
…t.o, which the compiler package ships

The first local build of 83f574e70 (arm64) kept /usr/lib/gcc/aarch64-linux-gnu/14/libhwasan_preinit.o, and image_worker_toolchain.py's row 12 check failed on it (11 of 12). dpkg -S names gcc-14-aarch64-linux-gnu as its owner, not libgcc-14-dev, and Debian trixie's file lists show the same on amd64 (gcc-14-x86-64-linux-gnu). The RUN's dpkg-query now also matches 'gcc-[0-9]*-*-linux-gnu'; its dpkg -L passes the same grep, which picks only libhwasan_preinit.o from that package (libsanitizer.spec, cc1's plugins and the LTO files do not match).

test_prd262_worker_d5_trims.py's fixtures gain the compiler package's real file list for both arches, and the removed set gains the preinit object, 24 paths per arch. Four of its tests fail without the Dockerfile change.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit dba924c950d66307877d3dcc6458fa37ead53d25)
…ed gosu

The worker entrypoint drops from root to the worker user with setpriv since
row 47 of the 19 Oct size breakdown took gosu out of the image. The Helm
chart's comment on why the pod has no runAsNonRoot still named gosu.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
@AutomatosAI
AutomatosAI requested a review from daarthur October 20, 2026 00:25

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0a7bbb40-6598-40b1-b0a0-9e89f4ac4e9e
📥 Commits

Reviewing files that changed from the base of the PR and between 8f19ace and 54ac73a.

⛔ Files ignored due to path filters: 2
  • services/workspace-worker/requirements-build.lock is excluded by !**/*.lock
  • services/workspace-worker/requirements.lock is excluded by !**/*.lock
📒 Files selected for processing: 33
  • .github/workflows/images.yml
  • .github/workflows/test.yml
  • charts/automatos/templates/worker.yaml
  • docs/workspace-execution/_index.md
  • docs/workspace-execution/file-operations.md
  • orchestrator/core/workspace_client.py
  • orchestrator/tests/helpers_workspace_worker.py
  • orchestrator/tests/test_prd256_o2b_worker_routes_moved.py
  • orchestrator/tests/test_prd256_o2b_worker_span.py
  • orchestrator/tests/test_prd262_dependabot.py
  • orchestrator/tests/test_prd262_node24_images.py
  • orchestrator/tests/test_prd262_vendored_metrics.py
  • orchestrator/tests/test_prd262_worker_boot.py
  • orchestrator/tests/test_prd262_worker_browser.py
  • orchestrator/tests/test_prd262_worker_d5_trims.py
  • orchestrator/tests/test_prd262_worker_diet.py
  • orchestrator/tests/test_prd262_worker_locks.py
  • orchestrator/tests/test_prd262_worker_starlette.py
  • orchestrator/tests/test_socials_worker_binary_pieces.py
  • scripts/ci/image_worker_site_packages.py
  • scripts/ci/image_worker_toolchain.py
  • services/agent-opt-worker/automatos_metrics.py
  • services/shared/automatos_metrics.py
  • services/workspace-worker/Dockerfile
  • services/workspace-worker/automatos_metrics.py
  • services/workspace-worker/entrypoint.sh
  • services/workspace-worker/requirements-build.in
  • services/workspace-worker/requirements.in
  • services/workspace-worker/worker_http.py
  • services/workspace-worker/worker_otel.py
  • services/workspace-worker/worker_routes_canvas.py
  • services/workspace-worker/worker_routes_files.py
  • services/workspace-worker/worker_routes_tools.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The workspace-worker image now uses a trimmed toolchain, source-built psycopg2, and setpriv for worker execution. Its HTTP server, metrics, and tracing use Starlette and ASGI. CI and tests check image contents, runtime behavior, and database connectivity.

Changes: Workspace Worker Runtime

Layer / File(s) Summary
Image dependencies and package composition
services/workspace-worker/Dockerfile, services/workspace-worker/requirements*.in, orchestrator/tests/test_prd262_worker_locks.py, orchestrator/tests/test_prd262_worker_diet.py, orchestrator/tests/test_prd262_worker_d5_trims.py, orchestrator/tests/test_prd262_node24_images.py, orchestrator/tests/test_prd262_worker_browser.py, orchestrator/tests/test_prd262_dependabot.py, docs/workspace-execution/_index.md
The image adds a stripped Node layer and source-built psycopg2, separates dependency locks, and removes selected packages, caches, debug sections, tests, and toolchain files. Tests check the locks, image composition, and retained tools.
Starlette server and worker routes
services/workspace-worker/worker_http.py, services/workspace-worker/worker_routes_*.py, orchestrator/tests/test_prd262_worker_starlette.py, orchestrator/tests/test_prd256_o2b_worker_routes_moved.py, orchestrator/tests/helpers_workspace_worker.py, docs/workspace-execution/file-operations.md, orchestrator/core/workspace_client.py, orchestrator/tests/test_socials_worker_binary_pieces.py
The server and route handlers now use Starlette and uvicorn with shared request and response helpers. Tests cover authentication, body limits, response behavior, downloads, and server lifecycle.
Starlette metrics and tracing
services/*/automatos_metrics.py, services/workspace-worker/worker_otel.py, orchestrator/tests/test_prd262_vendored_metrics.py, orchestrator/tests/test_prd256_o2b_worker_span.py
Metrics integrations and worker tracing now use Starlette or ASGI middleware. Tests cover route and status labels, metrics responses, spans, and handler failures.
Image checks and worker startup
services/workspace-worker/entrypoint.sh, scripts/ci/image_worker_*.py, .github/workflows/images.yml, .github/workflows/test.yml, orchestrator/tests/test_prd262_worker_boot.py, orchestrator/tests/test_prd262_worker_diet.py, charts/automatos/templates/worker.yaml
The entrypoint uses setpriv and sets HOME=/home/worker. CI runs toolchain and site-packages checks against the built image, including compiler, Git, library-loading, and psycopg2 database checks.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant WorkerClient
  participant UvicornServer
  participant InternalAuthMiddleware
  participant StarletteRoute
  participant WorkerRouteHandler
  WorkerClient->>UvicornServer: Send HTTP request
  UvicornServer->>InternalAuthMiddleware: Dispatch ASGI request
  InternalAuthMiddleware->>StarletteRoute: Forward permitted request
  StarletteRoute->>WorkerRouteHandler: Call matched route handler
  WorkerRouteHandler-->>WorkerClient: Return HTTP response
Loading
Merge Risk: ⚪ Minimal · up to 54ac7

The worker image is smaller, builds psycopg2 from source, and now serves HTTP through Starlette and uvicorn. Routes, authentication, body limits, and tooling are retained, and tests and CI image checks cover them. No concrete defect was found that should block merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 218 functions across 25 files. (8 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: reducing the worker image size while retaining the required toolchain.
Description check Passed The description follows the required template and provides detailed scope, rationale, verification results, AI assistance disclosure, checklist status, and follow-up items. It also explains unchecked …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description follows the required template and provides detailed scope, rationale, verification results, AI assistance disclosure, checklist status, and follow-up items. It also explains unchecked items such as pending CI at the time of writing and the absence of UI or migrations.

Full details: Docstring Coverage

Explanation

Docstring coverage is 47.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 218 functions across 25 files. (8 skipped: 8 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

AutomatosAI pushed a commit that referenced this pull request Oct 20, 2026
…while the step runs

test_a_playbook_step_keeps_the_loop_running_while_it_looks_up_composio failed twice on this PR (the loop stood still 0.58 s, then 0.52 s) and passes on main, #1219 and #1220. The same 44 test files ran before it in each shard. What differed is the set of test files the shard collected: three of this PR's frontend guards instead of one. With a few thousand tests collected, a full garbage collection takes about half a second, and where it lands depends on what the shard collected, not on the code under test. _watched now runs gc.collect() before it starts and pauses the collector until the step returns. The 0.3 s bar and every assertion are unchanged.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Brings in #1219 media-render cuts, #1168 W7 frontend, #1222, #1226 (PRD-262
API W2-W5 and cuts), the PRD-261 theme waves and #1232. Three conflicts:

- .github/workflows/images.yml: main's in-image steps after "Scan policy"
  (the API's stdlib bytecode layer, site-packages, botocore, scipy, system
  packages, left-out packages and pip layers, then the frontend's "serves
  with no network") and this branch's two worker steps (agent toolchain,
  site-packages and psycopg2) all kept, main's first, each under its own
  `if: matrix.name == ...`. The worker's site-packages step still follows its
  toolchain step, the one its "Same conditions as the step above" names.
- orchestrator/tests/test_prd262_dependabot.py: both sides' lock entries kept,
  this branch's services/workspace-worker/requirements-build.lock and main's
  orchestrator/requirements*.lock and scripts/ci/reference/*.lock.
- orchestrator/tests/test_prd262_node24_images.py: the code lines were the
  same on both sides (NODE_COPIES, NODE_LAYER, _node_stages). The module
  docstring and the NODE_LAYER comment take this branch's wording, which
  names both lanes' tests (test_prd262_worker_diet.py for the worker's #21,
  test_prd262_media_render_node_strip.py for media-render's #22); main's
  frontend #24 assertions and its NODE_COPIES comment are kept.

test.yml, charts/automatos/templates/worker.yaml and
orchestrator/core/workspace_client.py merged without conflict.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
@AutomatosAI

Copy link
Copy Markdown
Owner Author

Updated against main (ebedc7e, which includes #1219, #1168, #1222 and #1226): ba242b0.

  • The merge's 3 conflicts are resolved keeping both sides, as listed in the merge commit's body:
    • images.yml: every side's in-image steps;
    • test_prd262_dependabot.py: both sides' lock entries;
    • test_prd262_node24_images.py: comment wording naming both lanes' tests.
  • git diff origin/main...HEAD is exactly the worker lane's 35 files.
  • The node-stripped stage is still byte-identical to media-render's on main.

CI on ba242b0. Green: test, images (workspace-worker amd64 and arm64 with the toolchain 12-check and site-packages steps), smoke-fresh-clone, chart, CodeQL and the rest.

One non-required check is red: smoke-images' core-stack step. It's the same failure #1226 merged with, not this PR: make up CORE=1 on a fresh checkout, then start the worker, then PermissionError … /workspaces/<c1>/repos.

@daarthur: the two dependency changes for your review are under For Dave in the description. psycopg2 is built from source against Debian's libpq, with hash-checked setuptools and --no-build-isolation. starlette and uvicorn are now listed directly, and aiohttp is gone.

Clean merge. main adds #1242 (media-render's esbuild built on Go 1.26.10, litellm 1.91.5 in agent-opt-worker), PRD-263 PR-3A..3E-a and PRD-264 work since ebedc7e.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
@AutomatosAI
AutomatosAI merged commit 14b56d8 into main Oct 20, 2026
56 of 60 checks passed
@AutomatosAI
AutomatosAI deleted the feat/prd-262-worker-cuts branch October 20, 2026 12:17
AutomatosAI pushed a commit that referenced this pull request Oct 20, 2026
…) into the nights 16–17 + socials fix wave — no conflicts; no new undefined names

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
@daarthur

Copy link
Copy Markdown
Collaborator

Post-merge review, as agreed in the PRD-262 close-out, against main at ab10979. Since this merged, only #1268 (the uv image pin and certifi hashes) has touched the worker.

Verdict: good change, nothing to revert. No blocker or major defect. The one real item, F452, isn't a regression from this PR; it's tracked in #1309 with the measured details.

What I checked

A local build of services/workspace-worker (INSTALL_BROWSER=false, arm64, Docker 29.9):

  • it built in 37 s, at 910 MB installed (du -xsm /), matching the PR's 910.1;
  • image_worker_toolchain.py 12/12 and image_worker_site_packages.py 5/5, including psycopg2 connecting to the pinned pgvector image as uid 1000.

CI on main: run 40116620165 (24 Oct) has the worker build on amd64 and arm64, both in-image steps (12/12, 5/5) and the publish, all green. smoke-images on main is green, and promote-edge succeeded. The core-stack step is still red on PRs only, waiting on P256-FIX-WRK-1.

Tests on main's locked stack (fastapi 0.146, starlette 1.8, uvicorn 0.50.1): test_prd256_o2b_worker_span.py, test_prd256_o2b_worker_routes_moved.py and test_prd262_worker_starlette.py, 19/19.

The two "For Dave" changes

1. psycopg2 from source. Happy with it.

  • Supply chain:
    • setuptools 84.0.0 is installed from the hashed requirements-build.lock (--require-hashes --no-deps --only-binary :all:);
    • the psycopg2 sdist is hash-locked and built with --no-build-isolation;
    • uv comes from a digest-pinned image, and both stages share one python base digest.
  • The build stage's apt packages (gcc, libc6-dev, libpq-dev, binutils) are unpinned but from signed Debian repos, which is acceptable for a build-only stage.
  • /install holds psycopg2 and its dist-info only, no setuptools.
  • libpq: production has libpq5 17.11-0+deb13u1 and psycopg2 reports 170011. The base is digest-pinned, so a major-version skew can't happen, and .so.5 stays ABI-compatible within the release.
  • Lock upkeep: Dependabot can't regenerate requirements-build.lock, the same as the worker's other locks. That's still to settle with Gerard. My vote is a scheduled uv pip compile --generate-hashes job that opens a PR, run with the same --overrides so fi stays out.

2. aiohttp → Starlette/uvicorn, and the tracing middleware (O2b). It holds up:

Smaller notes

  • Toolchain trims: as worker, an sdist with a C extension (markupsafe, --no-binary) builds and loads, and apt-get install still works (gdb installed).
    • The trims are file-only, so apt-get install --reinstall libc6-dev quietly brings libc.a back. That's fine, just worth knowing.
    • -static and -fsanitize no longer link, as §3-D5 intends.
    • node-gyp and maturin builds weren't tried. Their dynamic links don't touch the removed archives.
  • Entrypoint: exec setpriv … env HOME=/home/worker python -m main is correct, and the exec makes signals reach python. USER and LOGNAME stay root's (nit).
  • Stripped RECORDs: strip --strip-debug leaves the stripped .sos' dist-info RECORD hashes stale. Only a pip-style RECORD check would notice (nit).
  • uvicorn access lines: the access log carries each request line with its query string, as aiohttp's did, and Logging: one-off scripts and SDK retry lines still bypass the request-URL guard #1178's request-log guard doesn't cover uvicorn.access. Worker paths can name a workspace file, so it may be worth holding uvicorn.access at WARNING in the worker, or redacting it. I can raise that separately.
  • No token, no check: when no X-Internal-Token is configured, every request is accepted. That's documented and unchanged from aiohttp.

Follow-ups

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants