Skip to content

perf(prd-262): API — W2–W5, pull-first self-host, boot, and the 19 Oct cuts (1,920 → 789 MB) - #1226

Merged
AutomatosAI merged 224 commits into
mainfrom
feat/prd-262-api
Oct 20, 2026
Merged

AutomatosAI merged 224 commits into
mainfrom
feat/prd-262-api

Conversation

@AutomatosAI

@AutomatosAI AutomatosAI commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

What and why

PRD-262 (Container diet), the API side. W0 (#1145), W1 and W6 (#1166), W7 (#1168), W8 (#1182) and the media cuts (#1219) are already on main. This PR is what's left for the API:

  • W2–W5 (US-014 to US-042): the API clean-up, the code swaps, the boot changes and the pull-first default stack.
  • The 19 Oct API image cuts: 8 rows of the ranked size breakdown, plus one fix to an in-image check.

Branch feat/prd-262-api. The waves and the cuts were built on feat/prd-262-api-integration and come in through its merge commits. The branch has been merged with main four times (see Merges with main).

Measured on a local arm64 build, installed size: the API image was 1,920 MB before PRD-262, 973 MB after W0–W5 (972.6 MB measured) and 789.2 MB with the cuts (docker image inspect .Size). That is 266.1 MB gzip (docker save | gzip -1). The cuts saved 183.3 MB. The breakdown estimated about 186 MB, and #17 and #25 overlap by a few MB.

W2: API clean-up, no behaviour change (US-014 to US-022)

  • Test tools leave the production image. requirements-dev.txt is installed only by CI.
  • Unused Python packages go: nltk, passlib, python-jose, scikit-learn, pgvector, gotenberg-client and others.
  • The app layer is written once, last, owned by uid 1000. There is no chown -R layer.
  • Local builds no longer bake uploads and caches into the image (orchestrator/.dockerignore).
  • Dead files are deleted (D6). Dockerfile.backend and orchestrator/requirements.txt are gone.
  • The system packages shrink to what the code calls. Ghostscript, poppler, osd traineddata and git's server tools are gone.
  • site-packages ships without debug info or test suites.
  • botocore keeps only the services we call: s3, s3vectors, bedrock-runtime, sts, sso and sso-oidc.
  • The dependency set is locked. requirements.in compiles to requirements.lock with hashes, and there is a requirements-dev.lock. scripts/split_lock.py installs the lock as two layers, the stable set and the rest.

W3: API code swaps (US-023 to US-028)

  • Tables come from pdfplumber, not camelot and opencv.
  • Gemini goes through the OpenAI-compatible endpoint, so google-generativeai and google-api-python-client are gone.
  • pandas leaves the image.
  • The text splitters we use are vendored, so langchain-text-splitters and langchain-core are gone.
  • Leiden runs through graspologic-native in one API image. The [leiden] extra and graspologic's heavy closure are gone.
  • Code-graph parsing uses each language's own tree-sitter package and never downloads grammars at runtime. tree-sitter-language-pack is gone.

W4: API boot (US-029 to US-034)

  • Migrations run once per boot, and the production CMD execs uvicorn.
  • A warm boot at head runs no alembic at all.
  • The two ALTER TABLE … ADD COLUMN IF NOT EXISTS statements main.py ran on every boot become the revision prd262_boot_columns.
  • Seeds run when their fingerprint changes, not on every boot.
  • Heavy modules load when used, not at import (FR-6).
  • The API boots with no internet: tiktoken's encodings are fetched at build time into TIKTOKEN_CACHE_DIR.
  • The server is ready after the core stages. The rest runs in the background, off the event loop.

W5: pull-first default stack (US-035 to US-042)

  • Compose runs the production backend. Hot reload moves to make dev.
  • make up pulls the published images. make build builds from the checkout.
  • make up writes the three secrets into the user's .env and refuses placeholders.
  • Health gates answer in seconds: start_interval: 1s, on a stated floor of Docker Engine 25.
  • Source builds keep their dependency cache: make clean keeps a week of build cache.
  • The core stack is five containers.
  • MinIO runs as its own user.
  • The self-host docs and the chart describe the pull-first flow.
  • A new large-files check fails on any newly tracked file over 5 MB.

The 19 Oct cuts

Row numbers come from the 19 Oct ranked size breakdown. The MB are the breakdown's arm64 estimates.

Row Cut MB Commit
11 graphify's 12 unused tree-sitter grammars are left out of the image through split_lock.py's LEFT_OUT. The lock still pins them, because CI's pip install --require-hashes needs every graphifyy requirement pinned. split_lock.py refuses a LEFT_OUT name the lock no longer pins 46.9 be69b12
13 ICU (libicu76) and tesseract's 15 training tools are deleted. /usr/bin/tesseract doesn't link ICU. The build fails if ldd /usr/bin/tesseract finds anything missing or tesseract --list-langs fails 41.1 a4029f6
17 The scipy subpackages no code path loads are pruned: stats, io, signal, interpolate, ndimage, cluster, odr, fftpack, differentiate, datasets and misc. The build fails if a kept one is missing. core/math/probability_theory.py now takes the t quantile from scipy.special.stdtrit, which is exactly what stats.t.ppf returns 29.0 3d1276a
20 qdrant-client is replaced by core/qdrant, an httpx REST client covering the 15 methods the code calls, with qdrant-client's names, defaults, models and error classes. grpcio, portalocker, h2, hpack and hyperframe leave the lock. qdrant-client stays in requirements-dev.txt for the PRD-108 tests' in-memory engine 22.9 9c3de87
25 strip --strip-unneeded replaces --strip-debug on site-packages' compiled libraries. .dynsym stays 17.8 8d9d2d7, 52b107b
35 The Type1 copies of urw-base35 are deleted and fontconfig's cache is rebuilt. Pango ≥ 1.44 can't use Type1 12.5 a4029f6
37 C, C++ and Cython sources and headers are deleted from site-packages 11.6 0d9c64b
47 The stdlib compileall skips idlelib, tkinter, turtledemo, lib2to3 and pydoc_data (2.65 MB). watchfiles is development-only: make dev keeps it, production has none (1.3 MB) ~4 ef72406, be69b12

There is also one fix, 74d0899. scripts/ci/image_site_packages.py now loads scipy's _matfuncs_sqrtm_triu through _matfuncs_sqrtm, as scipy does. Imported on its own, that extension always fails with a circular ImportError, and it did so in the pre-cut 973 MB image too. Without this fix, the site-packages step fails on every API image since the scipy 1.17.1 pin.

Proven on the built image

These checks ran on a local arm64 build on 19 Oct (image e12bfdd3fd1f), from the local integration branch at ac83f96b7. Since that build, this branch has merged main. The following are byte-identical to what was built: orchestrator/Dockerfile, requirements.in, requirements.lock, scripts/split_lock.py, orchestrator/.dockerignore, core/qdrant/ and the in-image check scripts image_site_packages.py, image_scipy.py and image_system_tools.py. Other orchestrator code has main's changes since (PRD-261, PRD-263 and PRD-264, and the Haiku default).

  • Health and imports.

    • /health and /health/ready both answered 200.
    • The container log had no ImportError or ModuleNotFoundError lines.
  • FR-6. import main took 4.3 s. Afterwards none of camelot, cv2, matplotlib, pandas, nltk, google.generativeai or googleapiclient was loaded. Nor was any pruned scipy subpackage, graphify.extract, qdrant_client, grpc, watchfiles or a left-out grammar.

  • Tesseract OCR (Testing the PR with Automatos workflows #13).

    • A line rendered with PIL was read back exactly by the tesseract CLI and by pytesseract.
    • image_system_tools.py reported ok for each of its checks: packages, tesseract, OCR fixture, git, ICU and fonts.
    • No ICU library or training tool is left, and ldd tesseract finds nothing missing.
  • Fonts (Pushing updates to load into railway #35).

    • fc-list lists 10 URW families from 35 OTF files, and no Type1 file is left.
    • fc-match resolves Helvetica, Times, Courier, Palatino and Bookman to the OTF faces.
    • The Helvetica brand-kit PDF fixture prints 2 pages and 76 lines in NimbusSans, the same as the pre-cut 973 MB image.
  • scipy (Credential management #17). image_scipy.py reported ok for its 7 checks: subpackages, datasketch, graphify dedup, the code graph's pagerank, the Knowledge Graph (Leiden), Student's t, and that no pruned subpackage was ever loaded.

  • Qdrant over httpx (Merge pull request #18 from AutomatosAI/credential-management #20). All 15 client methods ran against the local Qdrant, on a scratch collection called prd262_probe:

    • create the collection and a payload index;
    • upsert two points, then query, a filtered query, retrieve, scroll and count;
    • create, list and delete a snapshot;
    • a filtered delete, then delete the collection.

    Afterwards the collection list equalled the one before. A count on the deleted collection raises UnexpectedResponse 404. The existing collections were only read. The orphan sweep in --dry-run reported 0 orphans.

  • site-packages (PRD-30 modular architecture complete - 11 modules, 3 layers, 263 files #25, Claude/tool router registry z85 pz #37). image_site_packages.py, after 74d0899, checked 173 files: 172 extensions imported and 1 library opened with ctypes. No file has debug sections or .symtab, no tests/ directory or C or Cython source is left, and import main finds no stale bytecode.

  • Other in-image checks.

    • image_botocore.py, image_app_bytecode.py (951 .pyc) and image_dists_absent.py (the left-out grammars, watchfiles, qdrant-client and its transport, the FR-6 distributions, the dev tools) all exited 0.
    • The image runs as uid 1000.
  • Warm recreate. The backend was healthy 10.6 s after docker compose up --force-recreate started, 6.2 s after the container started. Before this wave it took 19 s.

Alembic

  • prd262_boot_columns chains onto auto_haiku_default_20261019, main's head. Main has added no revision since, so there is exactly one head.
  • The head pins follow it: test_prd209_alembic_single_head.py (EXPECTED_HEAD and its dated history comment), test_prd236_w1_routes.py (the chain asserts) and test_prd262_boot_columns.py (PARENT).
  • The upgrade is ADD COLUMN IF NOT EXISTS, so it does nothing on a database the old main.py booted on. The downgrade leaves both columns.
  • A local database already at prd262_boot_columns (from a local build where the revision chained onto prd263_harness_runs) needs: alembic stamp prd263_harness_runs && alembic upgrade heads. That runs main's auto-Haiku data migration, then the idempotent boot-columns migration. Production never had prd262_boot_columns, so it needs nothing.

Fixes made while preparing this PR

These three tests failed on CI before this PR, and none of the failures came from a merge. Each failed locally before its fix and passes after it.

  • e7605b9 test_prd262_offline_boot.py::test_the_base_stage_fetches_the_encodings_into_the_cache_dir. It looked for COPY --from=pybuild /install /usr/local, which US-022 split into /install/stable and /install/rest. It now requires both layers to be copied into base before ENV TIKTOKEN_CACHE_DIR, which must come before the fetch RUN.
  • 356d234 test_prd262_smoke_fresh_clone.py::test_script_times_boots_from_the_container_start_not_its_own_sleeps. US-034 (25c1db5) added one sleep, sleep "$left" in check_ready_after_core. That sleep waits until the last boot's first 60 s, counted from the container's StartedAt, are in its log. It times no boot. The test now allows exactly that line, requires its length to come from smoke_ready.py --window-left, and still forbids any other sleep.
  • eb0006d test_prd262_large_files.py::test_a_renamed_large_file_counts_as_newly_tracked_at_its_new_path. git mv into a directory that doesn't exist fails on every platform. The test now creates moved/ first.

The action pins and the secret scan:

  • a6acba3: seven action references this branch added still used the SHAs from before main's ci(deps): Bump the actions-major group across 1 directory with 6 updates #1201. They now use main's pins: checkout 3d3c42e5 (v7.0.1), setup-python 5fda3b95 (v7.0.0) and upload-artifact cf430e03 (v7.0.2). The changed lines are in test.yml (3), bytecode-experiment.yml, smoke-fresh-clone.yml (2) and smoke-images.yml. No other uses: under .github/ differs from main's pin for its action. scripts/ci/check_action_pins.py passes (127 references).
  • f1fc78d: a local gitleaks range scan, in the form the "Scan history for secrets" lane uses, found one generic-api-key match. It is a made-up placeholder PANDASAI_API_KEY in test_prd262_pandas_leaves.py (US-025), not a credential. The test now uses a short fake key, and .gitleaksignore fingerprints the old blob. The scan now finds no leaks.

Merges with main

After each merge, a script compared line counts 3-way (ours + theirs − base) for every file either side changed. No line main added is missing unless a conflict resolution above changed it, and no line this branch added is missing. A whole-file check, which also covers binary files, found every file one side left alone equal to the other side's version.

Left for after WebSummit (Gerard's decision, 19 Oct)

None of these is in this PR:

Merging with the other open PRD-262 PR

A trial merge (git merge-tree) of #1220 (worker, feat/prd-262-worker-cuts d121874) with this branch shows three conflicts:

Test plan

  • CI on this PR:
    • the required orchestrator job;
    • code standards;
    • DCO: every non-merge commit is signed off; one old merge commit (8ed2299) is not, and the DCO lane checks with --no-merges;
    • gitleaks;
    • action pins.
  • images.yml for api on amd64 and arm64, including the in-image steps: stdlib bytecode layer, site-packages, botocore, scipy, system packages, left-out packages and pip layers. Also the size report against edge.
  • test.yml prod-image-parity: the Railway build's backend steps (Leiden, build context, no test tools, removed packages, left-out packages, site-packages, botocore, scipy, boot bytecode, ownership, system packages, the Helvetica render, and the clone and code-graph tests in the test image), with the frontend and worker steps beside them.
  • smoke-fresh-clone and smoke-images:
    • make up pull-first to a working UI;
    • warm restarts skip alembic and seeds;
    • the core stages finish within 3 s and no APScheduler job is missed in the first 60 s;
    • the production backend runs with no bind mount;
    • the API boots with egress cut.
  • After merge: the first Railway API build passes; /health and /health/ready answer 200; one document is stored and fetched back through the pruned botocore.
  • After merge, local: rebuild the stack from main; run alembic stamp prd263_harness_runs && alembic upgrade heads on the local DB; confirm alembic current = prd262_boot_columns.

How it was verified

  • Tests added or updated, and they fail without this change. Each cut commit's body names its guard tests. The three test fixes above failed before their fix and pass after it.
  • CI green (a lane marked non-required counts too)
  • Both editions considered: local (AUTH_EDITION=local) and hosted. The local edition gets the pull-first stack. Railway builds the production stage with no build args, which prod-image-parity builds and checks.
  • UI change: screenshots below. There is no UI change.

Local checks on this branch's HEAD (no Docker; nothing ran against the local stack):

  • uvx ruff@0.16.9 check --select F821,F401,F811,F841 on the 115 changed .py files found 13 findings. All 13 are in files main changed (api/sites.py, api/webhooks.py, api/widgets/chat.py, channels/, consumers/chatbot/service.py, three tests), and main has the same 13. Neither the merges nor the fixes add one.
  • Every changed .py file compiles. bash -n passes on the changed shell scripts. yaml.safe_load loads the 6 changed workflows. jq reads the 34 changed JSON files. There are no conflict markers.
  • scripts/ci/check_changed_code_shape.py --base origin/main: 0 errors.
  • pytest, Python 3.11 with requirements.lock and requirements-dev.lock, ran on every test_prd262_*.py, the two head-pin tests and every orchestrator test file the merges brought in (127 files). Result: 1 failed, 2,826 passed, 41 skipped, 28 errors. None of these comes from the change; each needs something this machine doesn't have:
    • the 28 errors are test_f149_… and test_f232_…, which need Postgres (CI has it), and they error the same way on main;
    • the 1 failure is test_prd262_system_packages.py::test_the_fixture_names_helvetica_and_fills_two_pages_here, which needs WeasyPrint's pango, not installed here; it also fails at the commit before the fixes, and the built image runs the same render;
    • the skips are for Postgres, Linux /proc, the docker CLI and node.
  • Alembic: exactly one head, prd262_boot_columns, whose down_revision is auto_haiku_default_20261019.

AI assistance

  • None
  • Assisted: tool(s): Claude Code (PRD-262 lanes, merges and fixes); 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). Every non-merge commit is; the one old merge commit 8ed2299 is not, and the DCO lane checks with --no-merges.
  • No secrets, customer data or credentials anywhere in the change. A local gitleaks range scan finds none (see f1fc78d). make up generates the local secrets into the user's .env at run time; none is committed.
  • Settings through orchestrator/config.py; no hardcoded values. TIKTOKEN_CACHE_DIR is read by tiktoken itself (FR-12).
  • Replaced paths deleted in this pull request (no shims or V2 copies). camelot, pandas, langchain's splitters, qdrant-client, Dockerfile.backend and requirements.txt are gone, not kept alongside.
  • Migration (if any): one head (prd262_boot_columns), chained onto the current head, IF NOT EXISTS so it is safe on a create_all-built database, and the head pins moved.
  • New routes the frontend calls are in orchestrator/reports/route-manifest.json. There are no new routes.
  • Docs updated where behaviour changed: self-hosting, QUICKSTART, SETUP, README, CONTRIBUTING, docker-containerization and the Helm chart (README, values and templates).
  • New dependencies, for review:
    • graspologic-native is now listed directly (US-027). It was installed before through graspologic.
    • 10 tree-sitter-<language> packages replace tree-sitter-language-pack (US-028): c, c-sharp, cpp, go, java, kotlin, php, ruby, rust and swift. python, javascript and typescript were already listed.
    • openpyxl and pytz are now listed directly. Before, they came only through camelot and pandas.
    • Nothing else is new. The removed ones are listed above.

… first run that pulls

Grounded on main @ 73f18f1 and registry/CI measurements of 16 Oct 2026. Reviewed by 18 adversarial agents (213 confirmed or partly-confirmed findings applied). §3 and §9 Q1-Q13 taken at the recommendations on 16 Oct; Q14-Q21 open.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
The frontend (3000), Adminer and Gotenberg published on all interfaces: their ports: lines had no host IP, while every other service used ${BIND_ADDRESS:-127.0.0.1}. In the local edition the web app's chat proxy forwards an anonymous session the backend serves as the owner, so anyone on the network could chat with the backend as the owner. The three lines now carry the ${BIND_ADDRESS:-127.0.0.1}: prefix; Gotenberg's line goes with the service in US-018.

test_prd234_s1a_compose_bind.py checked five named variables, which is how these three slipped through. It now reads every ports: entry of every service in every tracked compose file (short and long syntax, compose's !reset/!override tags), requires the BIND_ADDRESS host IP, and proves itself non-vacuous: the canonical file is found, its core services publish, and parametrised cases show the checker refuses the old frontend line, bare ports, 0.0.0.0, a hard-coded 127.0.0.1, ${BIND_ADDRESS} with no default and a long-syntax entry without host_ip. The five-variable test is replaced, not kept.

The compose header, self-hosting.md's Network exposure section and the threat model now say every published port, the web app's included, is on loopback by default.

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

Once the backend answers /health/ready, smoke-fresh-clone.sh runs scripts/ci/smoke-loopback-ports.sh before tearing down. The check starts the frontend from the checkout when it is not already running (--no-deps, so the ready backend is left alone), requires 'docker compose port frontend 3000' to print 127.0.0.1:3000 (0.0.0.0:3000 before US-005), then reads every running service's published ports from 'docker compose ps --format json' and fails on any address other than 127.0.0.1. It writes the port table to the step summary. The smoke's success exit carries the check's result; teardown still runs either way.

The helper is its own file so the W0 smoke rewrite (US-002), which also starts the frontend, can call it in one line. The workflow's push and pull_request path filters add the helper. test_prd234_s1a_compose_bind.py guards the wiring: the call and exit in the smoke script, the expected address and the port command in the helper, and the path filters.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
docker compose prints interpolation warnings on stderr. Capturing 2>&1 would put a warning line into the address and fail the 127.0.0.1:3000 comparison for the wrong reason; stderr now goes to the log.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
scripts/ci/image_report.py reads a pushed image back from its registry and
reports what it weighs and what it costs an existing user to update:

- measure: picks the build's arch out of the index BuildKit pushes (skipping the
  unknown/unknown attestation manifest), sums the layer sizes, pairs each layer
  with the Dockerfile instruction that made it (non-empty history entries) and a
  kind (system / dependencies / code), lists each layer's paths (every prefix to
  depth 5, the 25 heaviest at depth 6, so the pip layers and site-packages show),
  and diffs the layer digests against the published edge. The delta is split by
  kind, so a requirements change is reported apart from a code-only change; an
  image with no edge reports its size only. JSON to an artifact, Markdown to
  the step summary.
- budget: checks the size against .github/image-budgets.json. The file is in
  warn mode (report only); US-004 sets the five-run baselines and flips it to
  fail. Budgets start at edge 0d9f0ac's sizes rounded up to 0.1 MB, each with
  a reason.
- report: per arch, the layers the API and the worker share (today only the
  29.8 MB Debian layer; layers under 1 KiB are left out of the count) and the
  app images' first pull, using edge for any image the run did not build.

image_registry.py reads registries with the standard library only: the arm64
runner image has no crane or skopeo, and imagetools cannot fetch layer blobs.
Bearer tokens are anonymous unless REGISTRY_USER/REGISTRY_TOKEN are set, and are
never forwarded on a blob redirect. No new dependency.

Tests feed manifests and configs recorded from the published edge images
(tests/fixtures/prd262_edge_images.json): edge plus a 10 MB public/ layer is a
+10 MB code-only update and the committed budgets warn on it; a pip-layer change
is filed as dependencies; the platform pick skips the attestation manifest; the
API and worker share one real layer; path listing, zstd refusal, and the three
CLI commands against a fake registry.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…anged images, measure every build

images.yml:

- PR builds, and branch builds started by workflow_dispatch, push by digest to
  a registry:2 service in the job (localhost:5000, buildx driver-opts
  network=host) instead of building without pushing, so they can be measured
  (and, from US-013, scanned) without writing to GHCR. Main and v* builds push
  to GHCR as before; their service image is empty, so no registry starts.
- A changes job (scripts/ci/image_matrix.py) picks the build matrix: a PR
  builds only the images whose inputs it touches, on both arches; a change to
  the workflow, the budgets, .grype.yaml or the report scripts builds them all;
  a push or a manual run builds every image. The image definitions moved into
  its IMAGES (with a publish flag, for the scan-only images US-006 and US-054
  add).
- pull_request paths add frontend/next.config.js, csp-sources.js, .npmrc and
  public/**, .github/image-budgets.json and .grype.yaml (D1), and the report
  scripts. A test holds the paths equal to the images' inputs.
- Every build runs image_report.py measure and budget and uploads its layer
  report; a report job adds the API/worker shared layers and the first pull
  and exposes them as job outputs (US-058 reads them).
- §9-Q13: cache-to is written only by publishing runs (PR and branch builds
  read the GHA cache but never write it), with mode=min for the API and max
  for the frontend and worker.

test_compose_published_images.py reads the published image names from the
changes job's IMAGES now, and also checks the merge job publishes the same set.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Every route but /health and /metrics now sits on one APIRouter whose dependency requires X-Internal-Token equal to AGENT_OPT_WORKER_TOKEN (hmac.compare_digest, worker_auth.py). It is a router-level dependency, not a path comparison, because starlette 0.41.3 lets a forged Host header rewrite request.url.path (GHSA-86qp-5c8j-p5mr), and it never skips the check for an empty token: worker_config.internal_token() stops the import, and so the start-up, when the token is unset or blank.

FastAPI(docs_url=None, redoc_url=None, openapi_url=None). GET /test, which ran three FutureAGI evaluations for anyone, is deleted.

Inputs are capped at what the platform sends: metrics are known template names, at most one per template (11; /assess and /score start a thread and an evaluation per entry, and an unknown name still cost a turing_flash call); scoring_template is a known template; teacher_model is allow-listed (gpt-4o-mini); dataset holds at most 10 items; and a BoundedSemaphore (AGENT_OPT_MAX_CONCURRENT_JOBS, default 2) bounds concurrent /optimize jobs, answering 429 beyond it. The job thread frees its slot however the job ends.

The service gets its own suite (tests/, pytest.ini, requirements-test.txt with pytest==9.0.3 as in media-render, installed only in the image's test stage). It runs inside the image with TestClient: 401 for every protected route with no, empty or wrong token and with forged Host headers; start-up fails with the token unset, empty or blank; /docs, /redoc and /openapi.json are 404; more than 11 or unknown metrics, a non-allow-listed teacher_model, more than 10 dataset items and an unknown scoring_template are 422; the job bound answers 429 and frees its slots.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…e PyPI package fi

requirements.txt becomes requirements.in, and requirements.lock is generated from it with uv pip compile --generate-hashes (Python 3.11, x86_64 Linux; the command is in requirements.in and the lock header). The Dockerfile installs the lock with pip --require-hashes --no-deps, so every Railway build installs exactly the 94 pinned, hashed packages instead of whatever the floating ranges resolved to at the last cache miss (24 of 99 pins matched between March and October).

overrides.txt removes fi from the resolution with a never-true marker. agent-opt 0.0.1 requires fi>=0.0.2, an unrelated 2019 sdist that runs setup.py at build and would install any future upload of that name. The fi.evals and fi.opt modules the worker imports come from ai-evaluation and agent-opt themselves; the CI lane imports both inside the image. ai-evaluation's floor moves to 1.1.0 because 0.1.x still required fi.

Dependency changes, all inside this one service and none new to it:
- uvicorn[standard]==0.34.0 becomes uvicorn==0.34.0: uvloop, httptools, watchfiles and websockets (17.8 MB installed) go; this service uses none of them.
- fastapi 0.115.6 -> 0.144.0 and starlette 0.41.3 -> 1.8.0 (a starlette>=1.0.1 floor in requirements.in), past GHSA-86qp-5c8j-p5mr. The resolver allows it: fastapi 0.144.0 requires starlette>=0.46.0 with no upper bound.
- litellm stays at 1.80.0: ai-evaluation 1.1.0 requires openai<2 and litellm 1.80.5+ requires openai>=2.8.0. Its Criticals go to the US-013 allowlist with that reason.

Dockerfile: useradd runs first and the code is copied with COPY --chown=1000:1000, so the chown -R layer goes (taken over from US-016). Stages are runtime, test (pytest on top, for CI) and production, which stays last because Railway builds no target. No BuildKit cache mount: Railway fails one without a hard-coded service id, and that id is unknown (PRD-262 section 10).
Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…endored metrics copy

services/shared/automatos_metrics.py labelled requests_total and request_duration with the raw request path. Every /optimize/{job_id} poll, every scanner path that reached the public domain and, under starlette 0.41.3, every forged Host header (request.url.path) added a counter series per status and a histogram family, kept until the next restart.

The FastAPI middleware now reads the matched route from the ASGI scope after call_next (scope['route'].path, set by FastAPI's APIRoute.matches in 0.115 and 0.144 alike) and the aiohttp middleware reads request.match_info.route.resource.canonical, which aiohttp resolves before any middleware runs. A path no route matches (404, and aiohttp's 405) is labelled 'unmatched'. A method outside GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS is labelled 'OTHER', since the client chooses it too. The /metrics skip reads scope['path'], not request.url.

The fix is made in services/shared and copied byte for byte to agent-opt-worker and workspace-worker. test_prd262_vendored_metrics.py (required orchestrator suite) fails when any services/*/automatos_metrics.py differs from services/shared, and drives both middlewares with job ids, scanner paths, a forged Host and PROPFIND, asserting one label per route template plus 'unmatched'. The worker's in-image suite checks the same on the real app. US-045's later /metrics content-type fix goes to the same copies.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…ion while it waits

futureagi_service sends X-Internal-Token on every worker call (POST and the optimisation poll) from the new setting AGENT_OPT_WORKER_TOKEN (config.py, listed in reports/config-surface.json by hand, as the generator would sort it; the generator needs the app importable). The Railway manifest and infrastructure/.env.example name it for both services.

The per-turn live-eval hook (consumers/chatbot/service.py _post_response) now calls futureagi_service.schedule_live_eval, which reads a cached 'any prompt has scoring on' flag. While the flag is fresh and off, nothing is scheduled and the database is untouched; before, every chat turn opened a SessionLocal and ran a SELECT on the event loop. A stale flag (older than SCORING_FLAG_TTL_SECONDS, 60 s) is re-read in a thread by the scheduled task; the admin toggle drops this process's copy at once. eval_live_traffic awaits the worker with no session checked out and writes the live runs in a new short session in a thread, so no pooled connection sits idle in a transaction for up to 120 s. Scheduled tasks are held in a set until they finish.

run_assessment reads and marks the run running in one short session that commits and closes (a thread), awaits the worker (assess, safety, or optimize_prompt, which polls for up to 1,500 s) with no session checked out, and writes the result in a new session. Before, one pooled connection (pool 10 + overflow 20) and one threadpool token were held for up to 25 minutes. Failures are logged with logger.exception and recorded on the run in their own session.

test_prd262_futureagi_worker_client.py: the header on both calls (and none when the token is empty); a fresh-off flag schedules nothing and touches no database; a stale flag is re-read off the event-loop thread; no session is checked out while live scoring awaits the worker or while optimize_prompt starts, sleeps and polls; results land in a new session; a worker failure and a missing version fail the run. Data scope is unchanged: live scoring and _collect_optimization_dataset send what they sent before (the PRD's open data question).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…suite inside the image

Nothing built, linted or tested services/agent-opt-worker before; only Railway built it. test.yml's changes job gains an agent_opt lane (^services/agent-opt-worker/ and test.yml; every lane runs on main and by hand). The agent-opt-worker job:
- fails on a '^fi==' line in requirements.lock and on any pin without a hash, and on uvicorn extras;
- builds the image with no target, as Railway does;
- imports fi.evals and fi.opt.optimizers inside it, and fails if pip shows the PyPI package fi;
- runs the image with no token and requires a non-zero exit naming AGENT_OPT_WORKER_TOKEN;
- builds --target test and runs the service's own TestClient suite inside it;
- runs the container with a token and checks /health and /metrics answer, every protected route 401s without the token (and with a wrong one or a forged Host), and /docs, /openapi.json and /test are 404.

test_prd262_agent_opt_worker_image.py (required orchestrator suite) pins the files a later edit could quietly loosen: requirements.in plus a fully hashed lock, fi kept out by overrides.txt, plain uvicorn, starlette >= 1.0.1, production last, no cache mount, --require-hashes --no-deps, useradd before COPY --chown with no chown -R, docs off, only /health on the app, and the lane itself.

The images.yml build-and-scan (local registry, US-013 policy, size budget, litellm allowlist) is US-013's, after W0 merges into this lane.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
agent-opt-worker-service.md gains an Access section (the token on every route but /health and /metrics, no API docs, the input caps, why /metrics stays open), AGENT_OPT_WORKER_TOKEN and AGENT_OPT_MAX_CONCURRENT_JOBS in its variable table, and the locked install. live-traffic-scoring.md lists the token for both services. The prompt-optimization pages point at requirements.in instead of the deleted requirements.txt.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…fails to start

Once optimize() took a job slot, an exception before the worker thread started (the stale-job sweep, the job-store insert, Thread()) leaked the slot. The route is a plain def, so concurrent requests run in FastAPI's threadpool, and the sweep iterating the store while another request inserted could raise. After MAX_CONCURRENT_JOBS leaks every /optimize answered 429 until restart. Everything after acquire now sits in one try that releases on any exception, and the job store is guarded by a lock (sweep over a snapshot, pop instead of del). Found by the automated commit security review.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
scripts/ci/smoke-fresh-clone.sh is now the measurement behind PRD-262 §8's boot bars:

- Every boot is timed from the container's own start (docker inspect
  State.StartedAt) to the response that first carried 200, polled every second
  (was 5 s and the script's own sleeps). /health and /health/ready are timed
  separately.
- After the fresh boot it starts the frontend, asserts :3000 answers 200
  (following / -> /chat) and records what `docker compose port frontend 3000`
  reports (0.0.0.0:3000 today; US-005 makes it the 127.0.0.1 assertion).
- Two warm phases run `docker compose restart backend` with the database kept
  and are timed the same way; the report gives their median.
- The step summary is written on every exit, pass or fail (it used to print logs
  only on failure): the boots, Started -> Healthy per service and per start from
  Docker's start/health_status events (streamed to a file, since the daemon
  replays only a short buffer), the /health/bootstrap stage table of each boot,
  and the alembic runs per boot counted the §8 way (Context impl PostgresqlImpl
  lines after 'init_fresh_db: done', or all of them when init_fresh_db did not
  run; review addenda S2/S9). The raw Context impl count is shown beside it.
- The images are built first and the build is timed for the record only (§7: no
  CI build-time bar). The records, the report JSON and the stack's logs are
  uploaded as the smoke-fresh-clone-boot artifact, which US-004 takes its
  five-run medians from. No time is a bar yet: the lane fails only when a boot or
  the frontend never answers 200.

scripts/ci/smoke_boot.py polls and records one boot; scripts/ci/smoke_report.py
writes the summary and the JSON. Standard library only, run by the runner's
python3: no new dependency.

smoke-fresh-clone.yml:
- paths add services/workspace-worker/**, docker-compose.dev.yml, Makefile,
  frontend/Dockerfile, orchestrator/requirements*.txt, frontend/package-lock.json,
  orchestrator/main.py, core/seeds/**, core/database/load_seed_data.py,
  alembic/**, envs/*.defaults (replacing envs/api.defaults), and from the review
  addendum orchestrator/requirements.in and requirements*.lock (US-022). Also
  orchestrator/docker-healthcheck.sh and alembic.ini (they decide Started ->
  Healthy and the alembic log lines) and the two new tools.
- a nightly schedule (02:41 UTC), since most main pushes skip the filter;
- actions/checkout and upload-artifact pinned by SHA (v7.0.1, as images.yml);
  persist-credentials false; permissions contents: read;
- runs-on ubuntu-24.04, so baselines compare like with like;
- only pull requests cancel an older run; main, nightly and manual runs are
  measurements and finish.

test_prd262_smoke_fresh_clone.py: the poller times from the container start on a
1 s tick (fake clock), gives up at the timeout and records liveness and readiness
apart; the report's alembic count, Started -> Healthy, warm median, bootstrap
table and frontend address from recorded records/events/log, and a summary on a
run that failed before any boot; the script's frontend, warm-restart and
report-on-exit wiring; the workflow's paths, schedule, SHA pins and artifact.
test_prd209_boot_lanes_hard.py's guards still hold (readiness probe, exported
DEFAULT_WORKSPACE_ID, no continue-on-error).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…e with only the node binary

The base stage (development and builder) moves from node:20-alpine, which reached end of life on 30 April 2026, to node:24-alpine3.24, pinned as tag@digest (24.22.0, index digest looked up on Docker Hub on 18 Oct). It drops apk python3, make, g++ and curl: no package in the lock compiles on Linux (sharp, esbuild, unrs-resolver and @next/swc ship prebuilt binaries; Prisma and Clerk only download or print).

The production stage is alpine:3.24 (tag@digest) plus libstdc++, with /usr/local/bin/node copied from the base stage and nothing else from it: no npm, yarn, corepack, C headers or curl. The musl binary is built for Alpine 3.24, so the base tag's alpine3.24 suffix and the runtime's alpine:3.24 name the same release; the comment says to bump them together. Streaming the node:24-alpine3.24 amd64 layer shows the binary needs only libstdc++.so.6 and libgcc_s.so.1 beyond musl.

The HEALTHCHECK keeps its options and uses busybox wget on /favicon.ico at 127.0.0.1:${PORT:-3000}: the middleware matcher skips paths with an extension, so a probe never renders a page or calls Clerk, and the server listens on IPv4 only (HOSTNAME=0.0.0.0).

FROM ... as becomes FROM ... AS on all four stages (BuildKit's FromAsCasing warning). production stays the last stage. The docs' frontend stage table and line references follow.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…e official image, not NodeSource

workspace-worker (Node 20, end of life) and media-render (Node 22) installed Node from deb.nodesource.com. That brought two extra apt updates, Debian's python3.13, gnupg and 49-59 MB of C headers, and on 15 Oct a 7.5-minute download stall in CI. Both now take Node 24 (24.22.0) from a named stage, FROM node:24-trixie-slim@sha256:... AS node, so the tag@digest pin sits on a FROM line, the only place an updater reads (a bare COPY --from=<image> is never read). They copy /usr/local/bin/node, npm and corepack, and link npm, npx and corepack into /usr/local/bin as the official image does. /usr/local/include is not copied: node-gyp downloads the headers on its first native build.

Libraries: the glibc node binary needs only libstdc++6 and libgcc-s1 beyond glibc (DT_NEEDED read from the amd64 and arm64 node:24-trixie-slim layers). The NodeSource deb used to declare them; the worker gets them from build-essential, and media-render now names libstdc++6 on its apt line.

Worker: pnpm is pinned exactly to 12.14.0, the current 12.x on npm on 18 Oct (PRD-262 §9-Q9), and npm cache clean --force runs in the same RUN, so the 31 MB /root/.npm cache is not in the layer. Global installs move from /usr to /usr/local; the agent sandbox PATH already lists /usr/local/bin first. Adding the node stage makes the worker Dockerfile multi-stage, so its own stage is named production and stays last (FR-8); Railway, images.yml, test.yml and compose all build it with no target, so what ships is unchanged. This also gives US-011 the stage name it needs.

media-render: npm stays for the global Hyperframes install and versions.py's npm root -g (unchanged until US-053).

Tests that pinned the old Node move with it: test_hyperframes_is_pinned_exactly_on_node_22 becomes ..._on_node_24 and asserts the named stage instead of the NodeSource script; the in-image test_hyperframes_is_pinned_exactly_and_runs_on_node_22 becomes ..._node_24 and expects v24. The PRD-251 W1 acceptance script's Node check accepts the official image (22 or later). The OSS Scanner image, which mirrors the frontend CI lane, moves to node:24-bookworm through a named stage too. Docs follow, including line references the new stage shifted.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… built images

frontend-ci (test.yml) and browser-e2e set up Node 24 instead of 20. browser-e2e is newer than the PRD's count of one node-version line; it moves too, so no CI lane runs an end-of-life Node.

prod-image-parity (test.yml), which already builds the frontend and worker with Railway's no-args defaults, gains two steps:
- frontend: node -v prints v24., and command -v npm, curl, yarn and corepack each fail, one name per check;
- worker: node -v prints v24.; npm, npx and corepack answer; pnpm --version equals the Dockerfile's exact pin (read from the Dockerfile, not repeated); dpkg -s python3.13, nodejs and gnupg each fail, one package per check; /root/.npm/_cacache and /usr/local/include/node are absent.
media-render.yml checks node -v and the same three dpkg packages right after its build. That workflow runs on dispatch only, so the ship agent dispatches it on the wave branch.

New orchestrator/tests/test_prd262_node24_images.py (pure file reads, run by the required orchestrator job):
- frontend base on node:24-alpine3.24 with tag@digest and no apk build tools; runtime is alpine:<same release>@digest, the last stage, copying only /usr/local/bin/node from base, apk-adding only libstdc++, naming no npm, yarn, corepack, curl or headers; HEALTHCHECK is wget on /favicon.ico;
- worker and media-render: a node stage on node:24-trixie-slim@digest, production last, exactly node, npm and corepack copied from it, no NodeSource, headers, gnupg or python3.13;
- the worker's pnpm pinned exactly to a 12.x and the npm cache cleaned in the same RUN;
- every Dockerfile that runs Node (including .oss-scanner) copies only from named stages, and every node image is node:24-*@sha256;
- every actions/setup-node step in .github/workflows runs 24;
- the parity and media-render jobs keep the checks above.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…checks what ran

scripts/ci/smoke-fresh-clone.sh gains SMOKE_IMAGES, so smoke-images.yml runs the
same boot steps as the fresh-clone lane on images instead of a source build:

- pull: docker-compose.images.yml with AUTOMATOS_IMAGE_TAG (today's published
  path). The Docker image cache is emptied first, then `docker compose pull` is
  timed and the bytes the runner's default-route interface received are counted.
- prebuilt: the PR variant. The workflow built the three amd64 images under the
  GHCR names, so compose neither pulls nor builds.
Both start the stack with `up --no-build`; unset, the lane builds as before.

scripts/ci/smoke_images.py does what is not the boot itself:
- target: sha-<7> after a main publish, pr-<7> on a PR, a manual run's tag
  (validated; a sha-<7> tag names its commit's prefix).
- workers: the first CI boot of the production stage. From `docker top -eo
  pid,ppid,args -ww`, the backend must run uvicorn without --reload and with the
  production CMD's 4 workers (the spawn_main children of the uvicorn process).
- images: each app container runs the ghcr.io/automatosai/automatos-* image
  compose names, carrying the expected org.opencontainers.image.revision. When
  pulled, its RepoDigests must hold the index digest the tag resolves to on GHCR
  (a local build has none). No image may carry a local-* tag or compose's
  <project>-<service> name. It also sums the app images' unique compressed layers.
- report (smoke_images_report.py): pull seconds and bytes, the first run from the
  pull's start (or compose up's) to /health/ready 200 and :3000 200, the
  provenance table and the process check, on every exit, plus
  smoke-images-report.json beside US-002's smoke-report.json for US-004.

image_registry.resolve(ref) reads the digest a tag points at (HEAD and
Docker-Content-Digest, else the hash of the served manifest). smoke_report.py
takes a --title, so each lane names its summary.

Docker Hub 429 recording (§9-Q17) is not added: the question is open.
Standard library only; no new dependency.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…s only after it passes

New .github/workflows/smoke-images.yml:

- smoke-images: runs after images.yml publishes on main (workflow_run: a
  successful push or manual run from this repository's main) and on
  workflow_dispatch with a tag. It runs on ubuntu-24.04 and ubuntu-24.04-arm,
  checked out at the commit the images were built from. It pulls sha-<7> into an
  empty Docker cache and boots it through scripts/ci/smoke-fresh-clone.sh
  (SMOKE_IMAGES=pull), uploading the records as smoke-images-<arch>.
- promote-edge: once both arches pass for a main publish, scripts/ci/promote_edge.py
  plans every image, then runs `docker buildx imagetools create -t …:edge
  …:sha-<7>`. With one source and no annotations, edge gets the same signed
  digest, and the tool reads it back to check. It moves edge only when this commit
  is ahead of, or the same as, the commit edge was built from (its revision label
  against GitHub's compare API). So a re-run of an old smoke never moves edge back,
  and one unreadable image moves none. A manual run never promotes. The job is
  serialised (concurrency promote-edge) and is the only one with packages: write.
- smoke-images-pr: on PRs that touch compose, the Makefile, envs/ or an image input
  (images.yml's PR paths, plus the backend's entrypoint and healthcheck and this
  lane's files). It builds the three amd64 images in the job with images.yml's
  context, target, build args and metadata labels, reading its GHA cache and never
  writing it (§9-Q13). It loads them as ghcr.io/automatosai/automatos-*:pr-<7> and
  boots them with SMOKE_IMAGES=prebuilt. It measures the boot only; the pull
  numbers come from main runs.

images.yml's merge job pushes sha-<7> (and semver tags) but no longer edge.

Report only until US-004: no time or size fails a job. A boot that never answers
200, a built image, a missing or wrong revision label, or --reload or the wrong
worker count fails it.

test_prd262_smoke_images.py holds the workflow to images.yml (PR paths are a
superset; the PR builds repeat IMAGES' context, target, args, cache scope and
labels; the tags are docker-compose.images.yml's names; promote-edge moves exactly
the merge job's images). It also holds the script's modes, and the expected
worker count to orchestrator/Dockerfile's production CMD.
test_prd262_promote_edge.py covers ahead, identical, behind, diverged, no label,
no edge yet, a partial failure and a move that lands elsewhere.

CONTRIBUTING lists the lane; QUICKSTART says edge is the latest main build whose
published images booted in CI. No new dependency.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Seam review of W0. The image tools caught registry read failures with (URLError, LookupError, ValueError, OSError), copied in image_report.py, smoke_images.py and promote_edge.py. A blob or manifest body that breaks off mid-read raises http.client.IncompleteRead, an HTTPException that is neither a URLError nor an OSError, so it escaped every handler:

- image_report.py _listing (the per-layer path listing, report only) let it fail the build job; on main that skips the merge job, so nothing publishes and edge never moves;
- read_edge did the same instead of reporting 'no update delta';
- smoke_images.py app_layer_bytes (report only) crashed the images check;
- promote_edge.py exited with a traceback instead of its 'edge stays where it is' error.

image_registry.py now defines READ_ERRORS once, with http.client.HTTPException added, and the three tools use it instead of their own copies. Each consumer gets a test that feeds it an IncompleteRead. No new dependency.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… with every route walk reading FastAPI's route tree

starlette 0.41.3 carried GHSA-86qp-5c8j-p5mr (on CISA's KEV list). The web stack moves to fastapi 0.146.0 (the current release), starlette 1.8.0 and uvicorn[standard] 0.50.1, the newest uvicorn whose [standard] extra still accepts websockets 12.0 (0.50.2 needs >= 13). Its --workers manager restarts a worker that dies (since 0.30). All four are pinned exactly. uv pip compile resolves the file for linux amd64 and arm64, with and without the [leiden] extra; the only other change in the resolution is annotated-doc 0.0.5, a dependency of fastapi.

F092 is why this is not a version edit. Since FastAPI 0.137, include_router keeps each included router as one node of app.routes, with no path and no endpoint, instead of copying its routes in. A plain walk over app.routes then sees about a dozen routes, which is F092's symptom. Every walk now goes through fastapi.routing.iter_route_contexts, FastAPI's public API for this, which gives each served route with its full path and inherited dependencies:
- scripts/dump_routes.py, so reports/route-manifest.json is unchanged by the bump;
- scripts/scaffold_platform_tool.py's route lookup;
- tests/authz_sweep_probe.py. Without the change, the sweep would silently have covered only the top-level routes.
- The socials tests that look up routes defined in the routers api/socials.py includes. They do not change what they assert.

slowapi is pinned to 0.1.10, which the old range already resolved to. The next commit subclasses its middleware.

New test_prd262_web_stack.py:
- checks the pins, and that the suite runs on the pinned versions;
- runs tests/web_stack_probe.py in a fresh process with the database unreachable. The probe asks the app's routing about every HTTP path of every router in router_manifest.py, and every HTTP route in the committed manifest. It uses a method no route takes, so 405 means the path is served and 404 means it is lost. No middleware, lifespan or endpoint runs.
- requires every committed manifest route to still be collected by dump_routes.

The entrypoint's watchdog comment no longer says the --workers supervisor outlives a dead worker. That was true only before 0.30.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…s of included routers

FastAPI 0.137+ changed two more readers of the route list besides the route walks.

1. slowapi 0.1.10's middleware looks a request's endpoint up by walking app.routes for a full match that has an endpoint. An included router is now one node of that list, with no endpoint. So the lookup finds nothing for a route of an included router, which is nearly every route of this API, and slowapi exempts the request. Under the policy plane (F040), main.py's 60/minute default limit would therefore have applied to almost nothing.
   - RouteTreeSlowAPIMiddleware (core/security/rate_limit_middleware.py) keeps slowapi's dispatch line for line. It looks the endpoint up with core.route_tree.matching_endpoint, which walks iter_route_contexts and takes the last full match, as slowapi did.
   - slowapi is pinned to 0.1.10 for that reason.
   - test_prd174_flag_gating now requires the subclass, registered under the flag.
2. A routed request's scope['route'] is now the route as its router declared it, without the prefixes it was included under. main.py's API stats keyed requests by scope['route'].path, so routes under an include prefix would have lost it: /api/alerts would have become /alerts, and the routes api/socials.py includes would have lost /api/socials. core.route_tree.route_template gives the full template again.
   - It finds the route's served contexts once per app (cached).
   - When a route is included under two prefixes, the one that matches the request's path wins.
   - It uses only FastAPI's public route API.

test_prd262_route_tree.py checks:
- the template for a route of a nested included router, for a route included twice, for a plain route and for a 404;
- the endpoint lookup;
- that the subclass returns 429 on the second request to a route of an included router;
- that slowapi's own middleware still exempts that route. When that last check fails, slowapi reads the tree itself and the subclass can go.

main.py's line count is unchanged.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…corn to replace it

scripts/ci/smoke_worker_restart.py is the kill-a-worker step for smoke-images' pull-request variant (US-003). That variant boots the production stage through compose, with no /app bind mount, and the CMD runs uvicorn --workers 4. The step runs after the variant's worker-count check.

1. It reads the backend's processes from /proc inside the container, using the container's own python, because the slim image has no ps. From them it finds the uvicorn process and its --workers children, which run multiprocessing's spawn_main. It never takes the sh -c 'alembic ... && uvicorn ...' process for uvicorn.
2. It kills the newest worker with SIGKILL.
3. Within 30 s it requires all of these:
   - one new worker under the same uvicorn process, with the worker count back where it was;
   - 'Started server process [<new pid>]' in the container's log;
   - --health-url answering 200 after that;
   - an unchanged restart count and start time, so the container itself never restarted.

uvicorn 0.24 never replaced a dead worker. The step refuses a single uvicorn process, because a single process has no manager to restart it. Once US-029 takes the worker count from WEB_CONCURRENCY, the job sets WEB_CONCURRENCY=2. The step writes a JSON report and a step-summary table. It uses the standard library only, like the other smoke tools.

smoke-images.yml and its image-mode boot are W0's (US-003) and are not on this lane's base, so the step is wired into W0's smoke-fresh-clone.sh when W1 is rebased onto W0. That script marks the spot: 'PRD-262 US-008's kill-a-worker step goes after this check.'

test_prd262_worker_restart.py covers:
- finding the processes;
- the replacement rule (new, logged, count restored, killed worker gone);
- the three failure cases and the one-worker refusal;
- the report and the exit code;
- the /proc listing, run on the CI runner.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…ile over 800 lines

check_changed_code_shape.py warns when a file over 800 lines grows. test_prd251_api.py had grown by one comment line in the previous commit, so the comment moves onto the loop it describes.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
FastAPI 0.132 added strict_content_type, on by default. A body with no Content-Type header is not parsed as JSON, so a typed body parameter answers 422. Under 0.115 such a body was parsed. A browser sends a Blob body with no Content-Type without a CORS preflight, and the local edition has no login, so the old behaviour let any web page the owner opened call a JSON endpoint. The API keeps FastAPI's default:
- the frontend's apiClient sends application/json;
- a grep of the Python callers found none that post JSON without the header;
- the public webhooks (api/webhooks.py, the recipe webhook) read the raw request, which this setting does not touch.

The test pins the behaviour on the pinned FastAPI and requires that main.py never sets strict_content_type.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…e way only

CodeQL (py/redos, high) flagged RUN_PREFIX in scripts/ci/image_layers.py: in (?:\S+=\S* )* a token such as '!==' splits several ways, so a history line starting 'RUN |0 ' followed by many '!== ' and no shell backtracks exponentially. A build-arg name now stops at its first '=' ([^\s=]+), so each NAME=value token has exactly one match and the scan is linear. Values may still contain '='. Tests pin the stripping (including a value with '=') and feed the CodeQL input, which pytest-timeout would kill if it ever backtracked again.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…tive builds by C library, no Prisma or unused dependencies

sharp resolves to 0.35.5, inside next@15.5.27's optional range '^0.34.3 || ^0.35.4' (#1023 moved next but left sharp at 0.34.5, which carries three High findings). sharp 0.35.5 itself needs semver ^7.8.5, @img/colour ^1.1.0 and, through its wasm build, @emnapi/runtime ^1.11.3, so those three move too (7.7.4 to 7.8.5, 1.0.0 to 1.1.0, 1.8.1 to 1.11.3). No other version in the lock changes, and no dependency is added.

The lock is written with npm 11.19.1, the npm Node 24.22.0 ships and so the one the Docker builder and frontend-ci now install with (US-007). npm 10 never persisted the libc field, so the @img, @rollup and @unrs native entries had os and cpu only, and npm installed both the glibc and the musl build everywhere: on Alpine, Next traced the glibc libvips (16.65 MB, unloadable there) into the standalone image. Every native entry whose published manifest declares libc now records it. The @rollup and @unrs entries (dev only) were re-resolved at their locked versions to pick the field up; only license and libc changed. The two @unrs arm 32-bit bindings publish no libc, so they stay as they were. A dry run of npm 11.19.1 against the new lock for linux x64 musl adds only the linuxmusl sharp and libvips builds (and the musl swc, rollup and unrs builds); against the old lock it added both libcs.

Removed, because nothing imports, requires or configures them (git grep for each as a quoted module name, including import() and require(); tailwind.config.ts loads only tailwindcss-animate; .scrollbar-hide is hand-written CSS in globals.css; react-grid-layout appears only in a comment and CSS):
- prisma and @prisma/client, with frontend/lib/db.ts (its only user, imported by nothing), frontend/prisma/ and the package.json prisma block, which seeded from scripts/seed.ts, a file that does not exist. That ends the Prisma engine download (about 14.8 MB) on every cold install;
- @anthropic-ai/sdk, @floating-ui/react, @headlessui/react, @hookform/resolvers, @tiptap/react, @tiptap/starter-kit, dotenv, lodash, react-grid-layout, @types/react-grid-layout and tailwind-scrollbar-hide.
npm pruned exactly 85 packages from the lock, the number the PRD's reachability walk gives. lodash stays in the lock as a dependency of recharts.

The deepwiki line references into package.json follow the new line numbers; the Classic Shell reference pointed at @headlessui/react and now points at @radix-ui/react-dialog, which the Sheet uses.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…nside the image

prod-image-parity (test.yml), which already builds the production frontend image, gains a step that lists node_modules/@img in the image, fails on any glibc build (sharp-linux-*, sharp-libvips-linux-*) and, when sharp is in the standalone trace, fails unless its version is the lock's (read with jq, not repeated). The WebAssembly fallback, @img/sharp-wasm32, prints a warning if it ever appears; npm 11 leaves it out on Linux.

New orchestrator/tests/test_prd262_frontend_lockfile.py (pure file reads, run by the required orchestrator job):
- sharp is 0.35.5 or later, and every @img/sharp-* build is at sharp's version;
- the x64 and arm64 sharp and libvips builds record musl or glibc, and so does every other Linux build whose name targets one C library (the two @unrs arm 32-bit bindings publish no libc and are listed with that reason). The failure message says to write the lock with Node 24's npm, because npm 10 drops the field;
- Prisma is gone: no package.json prisma block, no frontend/prisma/, no frontend/lib/db.ts, no prisma or @prisma/* in the lock;
- none of the 13 removed dependencies is declared again (the test says to drop a name from its list once code imports it);
- the lock's root entry equals package.json's dependency lists, and every lock entry is reachable from them through Node's lookup, so a lock that still carries a removed package's closure fails. Against the old lock and the new package.json this walk finds the 85 packages npm pruned; against the new lock it finds none;
- the parity job keeps the image check above.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…the unread watch file goes

Railway's config file has no target key: its build keys are builder, dockerfilePath, buildCommand, watchPatterns and the Nixpacks/Railpack ones. So railway.json's "dockerfileTarget": "production" was never read, and test_dockerfile_prod_parity.py asserted a value that proved nothing. Production ships only because it is the last stage of each Dockerfile; the next commit adds the test that checks that order.

- railway.json: dockerfileTarget removed.
- test_dockerfile_prod_parity.py: the assertion of it removed; the builder and manifest checks stay.
- orchestrator/.railway-watch.json deleted. It is not a Railway file (Railway's watch paths are build.watchPatterns) and nothing in the repo reads it.
- Comments that said Railway builds 'target production' (orchestrator/Dockerfile, test.yml's prod-image-parity note, the manifest's builder note) now say it builds the last stage. Comment lines are replaced one for one, so no line number moves.
- Docs: production-deployment.md no longer says railway.json targets the production stage, and the railway.json line references in it and in system-health-monitoring.md follow the removed line. docker-containerization.md names the new rule and its test.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… Dockerfile Railway builds

Railway builds the last stage of each Dockerfile, because its config has no target key. A stage added after production (a debug or test stage) would ship to production with every check green. test_prd262_production_last_stage.py parses the FROM lines and checks the order:

- orchestrator/Dockerfile, frontend/Dockerfile and services/media-render/Dockerfile, which the PRD names, end with the stage named production. So do the workspace worker and agent-opt-worker, which Railway also builds and which became multi-stage in US-006 and US-007.
- That list must equal the in-repo services of infrastructure/railway-manifest.json (root_dir plus railway.json's dockerfilePath), so a new Railway service has to join it.
- Any other tracked Dockerfile with a production stage keeps it last (FR-8). Today that adds the root Dockerfile.backend until US-018 deletes it; .oss-scanner/Dockerfile has no production stage and is not built by Railway.
- railway.json carries no dockerfileTarget, and orchestrator/.railway-watch.json stays gone.
- The checker fails on fixtures with a named stage after production, an unnamed stage after it, and no production stage. It refuses a FROM line it cannot read rather than skip it. It reads --platform flags, lower-case 'as', continuation lines and comments the way BuildKit does.

The fixtures are inline strings, not files, so no fixture Dockerfile is picked up by an image scanner or a dependency updater. No new dependency.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… watchfiles out of production

Breakdown rows #11 (~46.9 MB) and #47 (watchfiles, ~1.3 MB).

#11: graphifyy hard-requires all 25 tree-sitter grammars; only graphify/extract.py imports them, and the app never imports graphify.extract (it parses code with the 13 grammars requirements.in names). Taking the 12 extra grammars out of the lock itself would break every CI install: test.yml, nl2sql-eval-scheduled.yml, .oss-scanner and start.sh run pip install --require-hashes without --no-deps, and pip refuses a requirement of graphifyy the lock does not pin. So the lock keeps them and orchestrator/scripts/split_lock.py (the breakdown's first option) leaves its new LEFT_OUT set out of both halves pybuild installs with --no-deps; it refuses a LEFT_OUT name the lock no longer pins.

#47 watchfiles: only uvicorn --reload uses it. Dropping it from the lock would leave the development stage on StatReload, which rglobs and stats every .py under the mounted /app four times a second, a real cost on Docker Desktop bind mounts. Instead split_lock.py's DEVELOPMENT_ONLY goes to a third part, pybuild installs it into /install/development (stripped and compiled like the halves), and only the development stage copies it, after the RUN it shares with production. make dev keeps WatchFilesReload; the production image has no watchfiles.

Proof: images.yml (both arches) and test.yml's prod-image-parity run image_dists_absent.py inside the built image with LEFT_OUT and DEVELOPMENT_ONLY. test_prd262_left_out_packages holds LEFT_OUT equal to the grammars the lock pins for graphifyy alone, scans orchestrator/ for any import of graphify.extract (graphify's lazy attribute included), a left-out grammar or watchfiles, and follows every import of the installed graphify from the modules the app imports. Guards that read the old lines were updated: the install loop is now 'for part in stable rest development', the compile loop covers /install/development, split_lock.main takes four paths, and the development stage's COPY list includes the part.
Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… data in the bytecode layer

Breakdown row #47, API part (~2.65 MB). The production stage's stdlib compileall now excludes idlelib, tkinter, turtledemo, lib2to3 and pydoc_data as well as site-packages. tkinter cannot import in python:slim (no Tk libraries), nothing in the API runs IDLE, turtledemo or 2to3, and pydoc's help() compiles pydoc_data in memory if anyone ever calls it. Only .pyc files go; every module stays importable.

test_prd262_test_tools_leave_the_image reads the new line and checks the -x pattern against sample paths the way compileall searches it: the five directories and site-packages are skipped, json, asyncio and pydoc itself are still compiled. images.yml's stdlib bytecode layer check is unchanged (stdlib .pyc only, nothing under site-packages).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…RW fonts

Breakdown rows #13 (~41.1 MB) and #35 (~12.5 MB), both in the base stage's apt RUN.

#13: tesseract-ocr depends on libicu76 for its 15 training tools; the breakdown's scan of every ELF file found only four of them linking ICU, and neither /usr/bin/tesseract (which pytesseract runs) nor libtesseract.so.5 does. The RUN deletes the files dpkg -L libicu76 lists as libicu*.so.* (an empty match fails the RUN) and the 15 tools, the way it already deletes osd.traineddata and git's server binaries, so dpkg keeps its records and apt sees no broken Depends. Then ldconfig, and the build itself fails if ldd /usr/bin/tesseract finds anything missing or tesseract --list-langs fails. Debian builds tesseract the same way on amd64 and arm64.

#35: WeasyPrint draws through Pango >= 1.44, which can't use Type1, and fontconfig's metric aliases give Helvetica, Times, Courier, Palatino and Bookman the package's OpenType faces. The RUN deletes /usr/share/fonts/type1/urw-base35 and /usr/share/fonts/X11/Type1 and rebuilds fontconfig's cache with fc-cache -f (fontconfig comes with libpango).

Proof: scripts/ci/image_system_tools.py gains an ICU check (libraries and tools gone, ldd of tesseract complete, and ldd of every ELF file in the image that names ICU finds it) and a fonts check (no Type1 under /usr/share/fonts, fc-list lists every OpenType URW face, fc-match puts all five families on one). It still OCRs its fixture through pytesseract. test.yml's prod-image-parity already runs it; images.yml now runs it on every API build, on both arches. test_prd262_system_packages holds the Dockerfile's tool list equal to the script's and checks the order: deletions, ldconfig, ldd, list-langs; fonts, then fc-cache.
Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… leaves the image

Breakdown row #20 (19 Oct, ~22.9 MB on arm64: grpcio, portalocker, plus h2/hpack/hyperframe and qdrant-client itself).

The call sites are live, not legacy: the PRD-108 mission field (modules/context/adapters/vector_field.py), PRD-187 durable memory (modules/memory/durable_store.py), the PRD-197 snapshot job (services/qdrant_snapshots.py) and the orphan sweep script all talk to Qdrant whenever QDRANT_URL is set (SaaS). qdrant-client served them over REST and brought grpcio for a gRPC transport the code never used.

core/qdrant is a thin async client on the httpx the image already ships: the 15 methods the code calls (collection_exists, get_collections, create/delete_collection, create_payload_index, upsert, delete, set_payload, retrieve, scroll, count, query_points, create/list/delete_snapshot), with qdrant-client's argument names, defaults (wait=true on writes, with_payload=True, limit 10) and REST bodies, the api-key header, and the same model names and fields (Filter.must/should, FieldCondition.key/match, MatchValue.value, MatchAny.any, PointStruct, VectorParams, ...), so the callers change only their imports. Errors keep qdrant-client's shape (UnexpectedResponse with status_code; ResponseHandlingException with source), so core/qdrant_writes.py's F103 timeout retry reads them unchanged. Answers are parsed into frozen dataclasses and a shape Qdrant never sends is refused at the boundary. This replaces a library client for our own datastore; it adds no third-party integration.

Lock: qdrant-client leaves requirements.in; requirements.lock is recompiled with its header's flags (only removals: qdrant-client, grpcio, portalocker, pywin32, h2, hpack, hyperframe). grpcio leaves split_lock.py's stable set. qdrant-client moves to requirements-dev.txt (pinned at the 1.19.1 the lock had) because tests/test_prd108_proof.py and test_prd108_scenarios.py search with its in-memory engine in CI; requirements-dev.lock is recompiled against requirements.lock. test.yml's 'no test tools in the production image' step already checks every requirements-dev.txt name is absent from the built image.

The orphan sweep's 86-line main was split into its two passes (same behaviour; failures now logged with logger.exception) so the touched code meets the shape limits.

Tests: test_prd262_qdrant_rest_client drives every method through httpx.MockTransport (method, path, wait, body, api-key, parsed answer), the 408/400/ReadTimeout/ConnectError mapping through is_timeout, an F103 retry over the client, payload encoding, malformed answers, every method the callers call, and the lock and dev-lock contents. test_prd262_orphan_field_sweep covers the refactored script. Existing memory tests now patch QdrantRestClient and import core.qdrant's errors.
Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Comment-only follow-up to be69b12 (rows #11 and #47): pybuild's /install now holds three parts, and the strip and test-directory prune cover the development stage's part too. No instruction changes.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…ion through _matfuncs_sqrtm, as scipy does

scipy 1.17's linalg/_matfuncs_sqrtm_triu.so takes SqrtmError from _matfuncs_sqrtm at init, and _matfuncs_sqrtm is the module that imports it (for logm). Imported first on its own it always fails with a circular ImportError, in the pre-cut 973 MB image as in the cut one, so scripts/ci/image_site_packages.py failed on every API image since the lock pinned scipy 1.17.1 (US-022). The probe now imports, before an extension, its parent and the module LOADED_THROUGH names for it; the extension must still load. Found by the first local build of the api lane (breakdown row #25's .symtab check runs in the same script).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Brings the PRD-262 API work (W2-W5 and the 19 Oct API image cuts) up to
origin/main dd82d53: W1+W6 as one squash (#1166), W8 (#1182), PRD-263
harness PRs, PRD-261 W-A2, Auto defaults to Haiku, the vendored fonts and
dependabot bumps #1199/#1201/#1202.

Method: W1 reaches both sides as different commits (74ed2ff here, inside
the #1166 squash on main), so each of the 51 conflicted files was re-merged
3-way with W1's last commit 74ed2ff as the base (git merge-file --diff3:
ours = this branch, theirs = origin/main). W1 then cancels out and only
real differences remain. Every non-conflicted file git merged was checked
against the same W1-based merge: identical in all 708.

Clean through the W1 base, main's side taken (this branch has not touched
them since W1): action-pins, browser-e2e, check-shopify-isolation,
checkout-line-endings, codeql, dco, gitleaks, import-linter, malware-scan,
media-render, scan-allowlist workflows; docs/workspace-execution/_index.md;
test_prd262_node24_images.py; test_prd262_vendored_metrics.py;
services/media-render/{Dockerfile,README.md,tests/test_image.py};
services/workspace-worker/Dockerfile. The worker and media lanes are main's
W6/W8 exactly; this branch adds nothing there.

Clean through the W1 base, this branch's side taken (main's copy is W1's):
docs/getting-started/self-hosting.md, frontend/Dockerfile,
orchestrator/Dockerfile, test_prd234_s1a_compose_bind.py,
test_prd262_web_stack.py, test_prd262_weekly_rebuild.py,
test_prd262_worker_restart.py, scripts/ci/smoke_images.py,
scripts/ci/smoke_worker_restart.py. Dockerfile.backend (US-018) and
orchestrator/requirements.txt (US-022) stay deleted: main's copies are W1's.

Clean through the W1 base, both sides combined: QUICKSTART.md,
docker-compose.yml (main's MinIO digest and media-render urllib health
check), docs/deployment-infrastructure/docker-containerization.md,
nl2sql-eval-scheduled.yml, test_prd251w1_media_render_image.py.

Resolved by hand, keeping both sides:
- .github/dependabot.yml: one lock-files comment naming the worker's locks
  (US-044) and the API's locks (US-022).
- .github/image-budgets.json: `about` describes on_disk_max_mb and the
  `updates` bars; media-render's budgets and the api code-only bar both kept.
- images.yml: both IMAGES comments; main's On-disk size step, then the API's
  renamed "Size and update budgets" step; the tree-<12> tag step ends the
  merge job and main's media-assets job follows it.
- smoke-fresh-clone.yml, smoke-images.yml: path filters are the union; the
  smoke-images header lists US-035, US-038 and US-045's bars.
- test.yml prod-image-parity: the API's backend steps, then main's W6 worker
  step ("the headless shell only, every library resolved, no Mesa").
- .oss-scanner/Dockerfile: the API's hashed lock install, and the worker's
  requirements.in (main deleted its requirements.txt).
- CONTRIBUTING.md: the API's smoke rows, with main's images row.
- test_prd143_full_surface_positive.py: neither import (the API dropped the
  camelot shim's importlib, main dropped the harness half's json).
- test_prd262_dependabot.py: both sets of lock reasons.
- test_prd262_image_matrix.py: both docstring bullets.
- scripts/ci/image_matrix.py: entry() carries the API's SOURCE_DATE_EPOCH and
  rewrite_timestamp and main's cache_from/cache_to (cache_mode goes, as on main).
- scripts/ci/image_layers.py: docstring lists on_disk_verdict, update_verdict
  and layer_verdict.
- scripts/ci/image_report.py: five commands; budget judges the size budget,
  the on-disk bar and the update bar, with each side's titles and labels.
- scripts/ci/smoke-fresh-clone.sh: main's worker health check (US-045) runs
  after check_images in the API's flow; variables, header and exit lines merged.

Alembic: main's head auto_haiku_default_20261019 and this branch's
prd262_boot_columns both chained onto prd263_harness_runs. prd262_boot_columns
now chains onto auto_haiku_default_20261019 (down_revision and docstring), and
the pins follow: test_prd209_alembic_single_head.py (history comment,
EXPECTED_HEAD = prd262_boot_columns), test_prd236_w1_routes.py (both chain
asserts and the guard) and test_prd262_boot_columns.py (PARENT).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…edia-render step order names the renamed budget step

Two failures the merge of origin/main brought together:

- main's Auto-defaults-to-Haiku change (683e61e) has seed_auto_agent import
  core.llm.auto_model_sync and core.llm.new_agent_model, which US-031's seed
  fingerprint did not hash, so a change to either would not rerun the seeds.
  test_prd262_seed_fingerprint.py's import walk failed on both; they join
  INPUT_FILES.
- images.yml's budget step is "Size and update budgets" since US-016 (it judges
  the update bar too). W8's test_prd262_media_render_images.py checked the step
  order by the old name "Size budget"; it reads the new one.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
test_the_base_stage_fetches_the_encodings_into_the_cache_dir looked for
`COPY --from=pybuild /install /usr/local`, a line the base stage lost in US-022,
when pybuild began installing into two layers, /install/stable and /install/rest,
and base copies both. The test failed on every run since.

It now finds every `COPY --from=pybuild /install...` line in the base stage,
requires the stable and the rest layer both to be there, and keeps the order it
guarded: every install layer is copied before ENV TIKTOKEN_CACHE_DIR, which comes
before the RUN that fetches the encodings.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
test_script_times_boots_from_the_container_start_not_its_own_sleeps forbade every
`sleep` in scripts/ci/smoke-fresh-clone.sh. US-034 (25c1db5) added one, in
check_ready_after_core: `sleep "$left"`, where $left comes from
`smoke_ready.py --window-left`, the seconds until the last backend boot's first
60 s (counted from the container's StartedAt) have passed. It waits for those log
lines to exist before the APScheduler check reads them; it times no boot, which
smoke_boot.py still does from the container start. The test failed on every run
since.

The test now allows exactly that sleep line, requires it to sit in
check_ready_after_core with its length read from --window-left, and still fails on
any other sleep in the script.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
test_a_renamed_large_file_counts_as_newly_tracked_at_its_new_path ran
`git mv moves-large.bin moved/large.bin` in a throwaway repository with no moved/
directory. git mv does not create the destination's parent, so the call failed
("renaming 'moves-large.bin' failed: No such file or directory") on every
platform and the rename case was never checked.

The test now makes moved/ before the git mv, so it checks what it names: a large
file renamed to a new path counts as newly tracked there.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
#1201 bumped actions/checkout, actions/setup-python and actions/upload-artifact
across main's workflows. Seven references this branch added were written before
that bump and still carried the old SHAs, so one workflow file mixed two versions
of the same action:

- test.yml reference-goldens: actions/checkout 11d5960a (v4.4.0) and
  actions/setup-python a26af69b (v5.6.0);
- actions/upload-artifact 043fb46d (v7.0.1) in test.yml (reference-goldens),
  bytecode-experiment.yml, smoke-fresh-clone.yml (twice) and smoke-images.yml.

Each now uses main's pin for the same action: checkout 3d3c42e5 # v7.0.1,
setup-python 5fda3b95 # v7.0.0, upload-artifact cf430e03 # v7.0.2. No other
`uses:` under .github/ differs from main's pin for its action.
scripts/ci/check_action_pins.py passes (127 references pinned).

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Brings main from dd82d53 up to 111b430: PRD-261 W-A4P (#1215), W-A4 (#1217)
and W-C2 (#1218); PRD-264 EUAI-05/06a (#1209), EUAI-11a/11b (#1207) and
EUAI-02a/03/04 (#1210); the PRD-262 media-render cuts (#1219, e7b7d88).

No conflicts. Four files changed on both sides and merged by hunk, each in a
different place: orchestrator/config.py (main adds AI_DISCLOSURE_IDLE_HOURS,
VOICE_AI_GREETING and VOICE_AI_REMINDER_MINUTES; this branch drops
GOTENBERG_URL), orchestrator/reports/config-surface.json (the same names),
docs/frontend-architecture/play-theme.md and
orchestrator/tests/test_prd251w1_media_render_image.py (main's node-stripped
and Chrome .so assertions; this branch's lock-file scan).

Main added no Alembic revision, so prd262_boot_columns stays the one head, on
auto_haiku_default_20261019. services/media-render/**, media-render.yml,
test_prd262_node24_images.py and the media-render cut tests are main's
versions unchanged. #1168 (frontend) is not merged on main yet, so it is not
in this merge.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Brings main from 111b430 to 469ca59: the PRD-262 W7 frontend diet and its
19 Oct cuts (#1168, squash), which merged after the previous merge commit.

Three conflicts, resolved by hand keeping both sides:

- .github/workflows/images.yml, the build job after "Scan policy": both sides
  added steps at the same spot. The API's in-image steps (stdlib bytecode layer,
  site-packages, botocore, scipy, system packages, left-out packages, pip layers)
  come first, then the frontend's "serves with no network" step; each keeps its
  own `if: matrix.name == ...`.
- .github/workflows/test.yml:
  - PROD_IMAGE_PATHS is the union: the frontend's .npmrc and
    scripts/check-(standalone-image|layer-reuse).sh with the API's
    requirements*.(txt|in|lock), split_lock.py, scripts/ci image checks, boot
    modules, code-graph tests and fixtures, and the Leiden adapter. Its comment
    names both sides' inputs.
  - The prod-image-parity header names the API's Leiden clustering (US-027)
    and the frontend's dummy Clerk key, local build and booted frontends (US-046).
  - In prod-image-parity, main's four new frontend steps (local build with no
    network, npm ci layer reuse, layer reuse, build context) go in after the
    builder's sharp step, where main put them, and the API's backend steps follow
    them. Main kept the "Backend — graph extras present with no args" step there;
    this branch replaced that step with the Leiden step in US-027, so it stays
    replaced.
- docs/getting-started/self-hosting.md, first boot: the API's pull-first text,
  with the frontend's install named `npm ci` as main now has it.

Main added no Alembic revision; prd262_boot_columns stays the one head.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…is fingerprinted

A range scan of this branch (gitleaks 8.30.1, `--no-merges --first-parent`, as
the "Scan history for secrets" lane runs on a pull request) found one
generic-api-key match: test_prd262_pandas_leaves.py (US-025, 49448b5) set
config.PANDASAI_API_KEY to a made-up placeholder string, only so the PandasAI
service counts as enabled. It is not a credential, but the lane would fail on it.

The test now sets the short fake key the repo's other fixtures use, so no new
blob matches (including the squash commit a merge to main would scan), and
.gitleaksignore fingerprints the historical blob, described without quoting it.
test_prd262_pandas_leaves.py: 20 passed.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Brings main from 469ca59 to 9ec43ac: #1222, a fix to
frontend/scripts/check-public-assets.js's SVG prolog pattern. No conflicts;
this branch never touched the file.

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

@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

Important

Review skipped

Too many files!

This PR contains 350 files, which is 250 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 452aa0cb-00ec-465b-81ae-39548743c37c
📥 Commits

Reviewing files that changed from the base of the PR and between 9ec43ac and ef68909.

⛔ Files ignored due to path filters: 6
  • frontend/tsconfig.tsbuildinfo is excluded by !**/*.tsbuildinfo
  • orchestrator/requirements-dev.lock is excluded by !**/*.lock
  • orchestrator/requirements.lock is excluded by !**/*.lock
  • scripts/ci/reference/requirements-leiden-partitions.lock is excluded by !**/*.lock
  • scripts/ci/reference/requirements-pdf-tables.lock is excluded by !**/*.lock
  • scripts/ci/reference/requirements-text-splitters.lock is excluded by !**/*.lock
📒 Files selected for processing: 350
  • .env.example
  • .github/dependabot.yml
  • .github/image-budgets.json
  • .github/workflows/bytecode-experiment.yml
  • .github/workflows/images.yml
  • .github/workflows/large-files.yml
  • .github/workflows/nl2sql-eval-scheduled.yml
  • .github/workflows/smoke-fresh-clone.yml
  • .github/workflows/smoke-images.yml
  • .github/workflows/test.yml
  • .gitleaksignore
  • .oss-scanner/Dockerfile
  • AGENTS.md
  • CONTRIBUTING.md
  • Dockerfile.backend
  • Makefile
  • QUICKSTART.md
  • README.md
  • SETUP.md
  • charts/automatos/Chart.yaml
  • charts/automatos/README.md
  • charts/automatos/templates/_helpers.tpl
  • charts/automatos/templates/api-deployment.yaml
  • charts/automatos/templates/frontend.yaml
  • charts/automatos/templates/migrate-job.yaml
  • charts/automatos/templates/worker.yaml
  • charts/automatos/tests/api_test.yaml
  • charts/automatos/tests/image_test.yaml
  • charts/automatos/values.yaml
  • deploy/kind/datastores.yaml
  • deploy/kind/e2e.sh
  • deploy/kind/values.yaml
  • docker-compose.core.yml
  • docker-compose.dev.yml
  • docker-compose.images.yml
  • docker-compose.yml
  • docs/agents/llm-provider-management.md
  • docs/backend-architecture/background-services.md
  • docs/backend-architecture/database-models-migrations.md
  • docs/backend-architecture/fastapi-application-boot-sequence.md
  • docs/backend-architecture/fastapi-application.md
  • docs/backend-architecture/real-time-updates.md
  • docs/deployment-infrastructure/_index.md
  • docs/deployment-infrastructure/database-setup.md
  • docs/deployment-infrastructure/docker-compose-setup.md
  • docs/deployment-infrastructure/docker-containerization.md
  • docs/deployment-infrastructure/production-deployment-cicd.md
  • docs/deployment-infrastructure/redis-configuration.md
  • docs/deployment-infrastructure/redis-vector-store-configuration.md
  • docs/documentation-prd-corpus/runbooks-operational-docs.md
  • docs/frontend-architecture/play-theme.md
  • docs/frontend-architecture/settings-ui.md
  • docs/getting-started/_index.md
  • docs/getting-started/installation-setup.md
  • docs/getting-started/self-hosting.md
  • docs/knowledge-base-rag/_index.md
  • docs/knowledge-base-rag/document-generation-deliverables-rendering.md
  • docs/knowledge-base-rag/semantic-chunking-strategies.md
  • docs/memory-system/shared-field-memory-vector-field.md
  • docs/overview/system-architecture.md
  • docs/testing/PRD-260-PLAY-RUN-CHECKLIST.md
  • envs/api.defaults
  • envs/frontend.defaults
  • envs/observability.defaults
  • frontend/Dockerfile
  • frontend/components/play/edges/__tests__/edges.test.tsx
  • frontend/components/play/edges/start-command.tsx
  • orchestrator/.dockerignore
  • orchestrator/Dockerfile
  • orchestrator/README.md
  • orchestrator/alembic/versions/prd262_boot_columns.py
  • orchestrator/api/workspace_files.py
  • orchestrator/config.py
  • orchestrator/core/boot/startup_tasks.py
  • orchestrator/core/composio/client.py
  • orchestrator/core/context_guard.py
  • orchestrator/core/database/load_seed_data.py
  • orchestrator/core/llm/__init__.py
  • orchestrator/core/math/probability_theory.py
  • orchestrator/core/models/bootstrap.py
  • orchestrator/core/qdrant/__init__.py
  • orchestrator/core/qdrant/client.py
  • orchestrator/core/qdrant/errors.py
  • orchestrator/core/qdrant/models.py
  • orchestrator/core/qdrant_writes.py
  • orchestrator/core/routing/semantic_indexer.py
  • orchestrator/core/seeds/credential_type_overrides.py
  • orchestrator/core/seeds/fingerprint.py
  • orchestrator/core/seeds/leader_stage.py
  • orchestrator/core/seeds/seed_local_first_run.py
  • orchestrator/core/workspace_client.py
  • orchestrator/docker-entrypoint.sh
  • orchestrator/docker-secrets-guard.sh
  • orchestrator/integrations/shopify/tests/fixtures/regenerate.py
  • orchestrator/main.py
  • orchestrator/modules/codegraph/parsers/treesitter_parser.py
  • orchestrator/modules/context/adapters/vector_field.py
  • orchestrator/modules/documents/conversion_service.py
  • orchestrator/modules/documents/seed_templates.py
  • orchestrator/modules/documents/xlsx_render.py
  • orchestrator/modules/knowledge/graph_service.py
  • orchestrator/modules/knowledge/leiden_partition.py
  • orchestrator/modules/memory/durable_store.py
  • orchestrator/modules/rag/chunking/text_splitters.py
  • orchestrator/modules/rag/ingestion/manager.py
  • orchestrator/modules/rag/ingestion/multimodal/processors.py
  • orchestrator/modules/rag/service.py
  • orchestrator/modules/search/optimization/context_optimizer.py
  • orchestrator/modules/tools/discovery/action_semantic_index.py
  • orchestrator/modules/tools/execution/exec_document.py
  • orchestrator/modules/tools/execution/xlsx_from_table_file.py
  • orchestrator/modules/tools/services/pandas_ai_service.py
  • orchestrator/reports/config-surface.json
  • orchestrator/requirements-dev.txt
  • orchestrator/requirements.in
  • orchestrator/scripts/alembic_heads.py
  • orchestrator/scripts/boot_modules.py
  • orchestrator/scripts/nuke_orphan_field_collections.py
  • orchestrator/scripts/split_lock.py
  • orchestrator/services/qdrant_snapshots.py
  • orchestrator/start.sh
  • orchestrator/tests/conftest.py
  • orchestrator/tests/fixtures/codegraph_languages/sample.c
  • orchestrator/tests/fixtures/codegraph_languages/sample.cpp
  • orchestrator/tests/fixtures/codegraph_languages/sample.cs
  • orchestrator/tests/fixtures/codegraph_languages/sample.go
  • orchestrator/tests/fixtures/codegraph_languages/sample.java
  • orchestrator/tests/fixtures/codegraph_languages/sample.js
  • orchestrator/tests/fixtures/codegraph_languages/sample.jsx
  • orchestrator/tests/fixtures/codegraph_languages/sample.kt
  • orchestrator/tests/fixtures/codegraph_languages/sample.php
  • orchestrator/tests/fixtures/codegraph_languages/sample.py
  • orchestrator/tests/fixtures/codegraph_languages/sample.rb
  • orchestrator/tests/fixtures/codegraph_languages/sample.rs
  • orchestrator/tests/fixtures/codegraph_languages/sample.swift
  • orchestrator/tests/fixtures/codegraph_languages/sample.ts
  • orchestrator/tests/fixtures/codegraph_languages/sample.tsx
  • orchestrator/tests/fixtures/leiden_graphs.py
  • orchestrator/tests/fixtures/leiden_partitions.json
  • orchestrator/tests/fixtures/pdf_table_grids.json
  • orchestrator/tests/fixtures/pdf_tables.py
  • orchestrator/tests/fixtures/reference_locks.py
  • orchestrator/tests/fixtures/text_splitter_cases.py
  • orchestrator/tests/fixtures/text_splitter_chunks.json
  • orchestrator/tests/fixtures/text_splitters/guide.md
  • orchestrator/tests/fixtures/text_splitters/inventory.py.txt
  • orchestrator/tests/fixtures/text_splitters/label-brazil.txt
  • orchestrator/tests/fixtures/text_splitters/label-colombia.txt
  • orchestrator/tests/fixtures/text_splitters/notes.txt
  • orchestrator/tests/security/test_nl2sql_tenancy.py
  • orchestrator/tests/security/test_template_security.py
  • orchestrator/tests/security/test_tenancy_matrix.py
  • orchestrator/tests/security/test_w2s7_fail_closed_authz_gate.py
  • orchestrator/tests/security/test_w3_full_autonomy_gate.py
  • orchestrator/tests/test_1100_cpu_readings_never_block_the_loop.py
  • orchestrator/tests/test_818_every_make_target_has_its_commands_in_the_guide.py
  • orchestrator/tests/test_818_runtime_files_stay_lf_on_a_windows_checkout.py
  • orchestrator/tests/test_837_mission_dispatch_never_freezes_the_loop.py
  • orchestrator/tests/test_996_hosted_workspaces_get_the_current_starters.py
  • orchestrator/tests/test_board_task_handlers.py
  • orchestrator/tests/test_checkout_line_endings.py
  • orchestrator/tests/test_ci_backend_test_plan.py
  • orchestrator/tests/test_compose_published_images.py
  • orchestrator/tests/test_dockerfile_prod_parity.py
  • orchestrator/tests/test_documents_team_filter.py
  • orchestrator/tests/test_entrypoint_modes.py
  • orchestrator/tests/test_f081_search_multimodal_extracted_only.py
  • orchestrator/tests/test_f103_field_write_retried_once.py
  • orchestrator/tests/test_f103_memory_write_not_lost_silently.py
  • orchestrator/tests/test_f105_a_frozen_loop_names_what_froze_it.py
  • orchestrator/tests/test_f112_telegram_polling_dependency.py
  • orchestrator/tests/test_f139_the_intent_gate_checks_the_named_composio_action.py
  • orchestrator/tests/test_f220_workspaces_survive_a_rebuild.py
  • orchestrator/tests/test_f228_a_slow_backend_boot_still_starts_the_frontend.py
  • orchestrator/tests/test_f343_a_pdf_with_an_image_renders.py
  • orchestrator/tests/test_golden_journeys.py
  • orchestrator/tests/test_l3_distill_input.py
  • orchestrator/tests/test_memory_degrades_when_l3_down.py
  • orchestrator/tests/test_memory_single_write_path.py
  • orchestrator/tests/test_memory_stored_sse.py
  • orchestrator/tests/test_mission_retry_feeds_critique.py
  • orchestrator/tests/test_p2w1_durable_store.py
  • orchestrator/tests/test_p2w2_ask_notification.py
  • orchestrator/tests/test_p2w2_gdpr_subject_tags.py
  • orchestrator/tests/test_p2w2_grant_resume.py
  • orchestrator/tests/test_p2w2_tool_approval_card.py
  • orchestrator/tests/test_p2w2_tool_grant_consume.py
  • orchestrator/tests/test_p2w2_tool_grant_issue.py
  • orchestrator/tests/test_prd143_boundary_sweep.py
  • orchestrator/tests/test_prd143_concierge_journey.py
  • orchestrator/tests/test_prd143_full_surface_positive.py
  • orchestrator/tests/test_prd143_manifest_parity.py
  • orchestrator/tests/test_prd143_selection_at_scale.py
  • orchestrator/tests/test_prd143_selection_metric.py
  • orchestrator/tests/test_prd143_su_executor_gate.py
  • orchestrator/tests/test_prd143_su_surface.py
  • orchestrator/tests/test_prd154_s2_team_access.py
  • orchestrator/tests/test_prd154_s5_missions.py
  • orchestrator/tests/test_prd154_s7_field.py
  • orchestrator/tests/test_prd163_approval_card.py
  • orchestrator/tests/test_prd163_approval_policy.py
  • orchestrator/tests/test_prd163_mission_lifecycle.py
  • orchestrator/tests/test_prd163_plan_mode.py
  • orchestrator/tests/test_prd164_agent_match.py
  • orchestrator/tests/test_prd164_dispatch_digest.py
  • orchestrator/tests/test_prd164_flywheel.py
  • orchestrator/tests/test_prd164_match_reasons_surface.py
  • orchestrator/tests/test_prd164_planning_pack.py
  • orchestrator/tests/test_prd164_progress_ledger.py
  • orchestrator/tests/test_prd181_approval_card_oversight.py
  • orchestrator/tests/test_prd200_approval_renotify.py
  • orchestrator/tests/test_prd206_recall_ranking.py
  • orchestrator/tests/test_prd206_resume_context.py
  • orchestrator/tests/test_prd206_write_contract.py
  • orchestrator/tests/test_prd209_alembic_single_head.py
  • orchestrator/tests/test_prd209_single_compose.py
  • orchestrator/tests/test_prd226_doctrine.py
  • orchestrator/tests/test_prd230_packages_seed.py
  • orchestrator/tests/test_prd233_s2_composio_degrade.py
  • orchestrator/tests/test_prd234_s1a_compose_bind.py
  • orchestrator/tests/test_prd236_w1_routes.py
  • orchestrator/tests/test_prd238_w1_honest_window.py
  • orchestrator/tests/test_prd251w1_builtin_skills.py
  • orchestrator/tests/test_prd251w1_media_render_image.py
  • orchestrator/tests/test_prd251w1_socials_package.py
  • orchestrator/tests/test_prd256_fix_dead_backend_restarts.py
  • orchestrator/tests/test_prd262_alembic_head_check.py
  • orchestrator/tests/test_prd262_alembic_runs.py
  • orchestrator/tests/test_prd262_api_locks.py
  • orchestrator/tests/test_prd262_app_bytecode.py
  • orchestrator/tests/test_prd262_app_layer_last.py
  • orchestrator/tests/test_prd262_boot_columns.py
  • orchestrator/tests/test_prd262_botocore_services.py
  • orchestrator/tests/test_prd262_build_context.py
  • orchestrator/tests/test_prd262_bytecode_experiment.py
  • orchestrator/tests/test_prd262_codegraph_clone.py
  • orchestrator/tests/test_prd262_codegraph_grammars.py
  • orchestrator/tests/test_prd262_codegraph_languages.py
  • orchestrator/tests/test_prd262_compose_production_backend.py
  • orchestrator/tests/test_prd262_core_stack.py
  • orchestrator/tests/test_prd262_dead_weight_retired.py
  • orchestrator/tests/test_prd262_dependabot.py
  • orchestrator/tests/test_prd262_health_gates.py
  • orchestrator/tests/test_prd262_image_matrix.py
  • orchestrator/tests/test_prd262_image_reports.py
  • orchestrator/tests/test_prd262_import_main_report.py
  • orchestrator/tests/test_prd262_kind_pins.py
  • orchestrator/tests/test_prd262_large_files.py
  • orchestrator/tests/test_prd262_lazy_imports.py
  • orchestrator/tests/test_prd262_left_out_packages.py
  • orchestrator/tests/test_prd262_leiden_adapter.py
  • orchestrator/tests/test_prd262_locked_install.py
  • orchestrator/tests/test_prd262_make_env.py
  • orchestrator/tests/test_prd262_make_up_tag_rule.py
  • orchestrator/tests/test_prd262_media_render_images.py
  • orchestrator/tests/test_prd262_minio_own_user.py
  • orchestrator/tests/test_prd262_offline_boot.py
  • orchestrator/tests/test_prd262_orphan_field_sweep.py
  • orchestrator/tests/test_prd262_pandas_leaves.py
  • orchestrator/tests/test_prd262_pdf_tables.py
  • orchestrator/tests/test_prd262_pip_layers.py
  • orchestrator/tests/test_prd262_placeholder_secrets.py
  • orchestrator/tests/test_prd262_qdrant_rest_client.py
  • orchestrator/tests/test_prd262_ready_after_core.py
  • orchestrator/tests/test_prd262_scipy_prune.py
  • orchestrator/tests/test_prd262_seed_fingerprint.py
  • orchestrator/tests/test_prd262_seed_stages.py
  • orchestrator/tests/test_prd262_self_host_docs.py
  • orchestrator/tests/test_prd262_site_packages_pruned.py
  • orchestrator/tests/test_prd262_smoke_document_presign.py
  • orchestrator/tests/test_prd262_smoke_egress.py
  • orchestrator/tests/test_prd262_smoke_fresh_clone.py
  • orchestrator/tests/test_prd262_smoke_images.py
  • orchestrator/tests/test_prd262_smoke_images_tools.py
  • orchestrator/tests/test_prd262_smoke_minio.py
  • orchestrator/tests/test_prd262_smoke_production_backend.py
  • orchestrator/tests/test_prd262_smoke_ready.py
  • orchestrator/tests/test_prd262_smoke_seeds.py
  • orchestrator/tests/test_prd262_source_build_cache.py
  • orchestrator/tests/test_prd262_system_packages.py
  • orchestrator/tests/test_prd262_test_tools_leave_the_image.py
  • orchestrator/tests/test_prd262_text_splitters.py
  • orchestrator/tests/test_prd262_unused_packages_go.py
  • orchestrator/tests/test_prd262_warmups_off_the_loop.py
  • orchestrator/tests/test_prd262_web_stack.py
  • orchestrator/tests/test_prd262_weekly_rebuild.py
  • orchestrator/tests/test_prd262_worker_not_running.py
  • orchestrator/tests/test_prd262_worker_restart.py
  • orchestrator/tests/test_rag_hydration.py
  • orchestrator/tests/test_rag_ingest_atomicity.py
  • orchestrator/tests/test_smart_orchestrator_store_exchange.py
  • orchestrator/tests/test_socials_photo_starters.py
  • orchestrator/tests/test_teams_api.py
  • orchestrator/tests/test_tool_reachability.py
  • orchestrator/tests/test_us009_limit_reporting.py
  • orchestrator/tests/test_vector_field.py
  • orchestrator/tests/test_w1s9_idle_in_tx.py
  • orchestrator/tests/test_w2s10_mission_verification.py
  • orchestrator/tests/test_w2s10_rag_primitives.py
  • orchestrator/tests/test_w2s9_reasoning_entry.py
  • orchestrator/tests/test_widget_docs_schema.py
  • promtail-config.yml
  • scripts/ci/build-cache-experiment.sh
  • scripts/ci/build_cache.py
  • scripts/ci/build_context.py
  • scripts/ci/bytecode-experiment.sh
  • scripts/ci/bytecode_experiment.py
  • scripts/ci/check_large_files.py
  • scripts/ci/image_app_bytecode.py
  • scripts/ci/image_botocore.py
  • scripts/ci/image_dists_absent.py
  • scripts/ci/image_layers.py
  • scripts/ci/image_matrix.py
  • scripts/ci/image_pdf_fixture.py
  • scripts/ci/image_pip_layers.py
  • scripts/ci/image_report.py
  • scripts/ci/image_scipy.py
  • scripts/ci/image_site_packages.py
  • scripts/ci/image_system_tools.py
  • scripts/ci/image_test_image.py
  • scripts/ci/import_main_report.py
  • scripts/ci/lock_check.py
  • scripts/ci/reference/leiden_partitions.py
  • scripts/ci/reference/pdf_tables.py
  • scripts/ci/reference/requirements-leiden-partitions.in
  • scripts/ci/reference/requirements-pdf-tables.in
  • scripts/ci/reference/requirements-text-splitters.in
  • scripts/ci/reference/text_splitters.py
  • scripts/ci/smoke-core-stack.sh
  • scripts/ci/smoke-egress.sh
  • scripts/ci/smoke-fresh-clone.sh
  • scripts/ci/smoke-make-env.sh
  • scripts/ci/smoke-make-up.sh
  • scripts/ci/smoke-minio-upgrade.sh
  • scripts/ci/smoke_alembic.py
  • scripts/ci/smoke_alembic_behind.py
  • scripts/ci/smoke_boot.py
  • scripts/ci/smoke_build_overlap.py
  • scripts/ci/smoke_document_presign.py
  • scripts/ci/smoke_health_gates.py
  • scripts/ci/smoke_images.py
  • scripts/ci/smoke_images_report.py
  • scripts/ci/smoke_minio.py
  • scripts/ci/smoke_production_backend.py
  • scripts/ci/smoke_ready.py
  • scripts/ci/smoke_seeds.py
  • scripts/ci/smoke_worker_restart.py
  • scripts/stack_docker_floor.sh
  • scripts/stack_env.sh
  • scripts/stack_images.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 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.

def test_make_devs_stack_names_no_published_image():
services = _compose_config("docker-compose.yml", "docker-compose.dev.yml")
assert {name: spec.get("image") for name, spec in services.items()
if str(spec.get("image", "")).startswith("ghcr.io/")} == {}
def test_this_repositorys_own_compose_images_are_its_published_and_dev_names():
"""docker-compose.yml names the three published images, docker-compose.dev.yml their dev builds."""
own = {where: image for where, image in _compose_images().items() if FIRST_PARTY.match(image)}
published = {where for where, image in own.items() if image.startswith("ghcr.io/")}
…in site-packages

images.yml's stdlib bytecode layer check failed on #1226: the compileall layer held usr/local/lib/python3.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc. python:3.11-slim ships setuptools with distutils-precedence.pth; at interpreter start that .pth imports _distutils_hack, and the root compileall process wrote its bytecode into site-packages, inside the stdlib layer. (The pip uninstall RUN US-014 removed used to be the first interpreter start and took that write.) python -S skips site, so no .pth runs; compileall needs only the stdlib. Proved in a container of the built API image: after removing that .pyc and the json stdlib .pyc, python -S -m compileall wrote 0 files under site-packages and put the 5 json .pyc back; the same command without -S recreated _distutils_hack/__pycache__. The two tests that read the RUN line follow it.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…e US-031 moved the seeds

test_prd230_packages_seed::test_boot_lane_runs_the_packages_seed_create_only and test_prd251w1_socials_package::test_boot_seeds_the_socials_marketplace_after_the_builtin_skills_and_before_the_packages read main.py, but since PRD-262 US-031 the boot leader's seeds run as core/seeds/leader_stage.py's SECTIONS (built-in skills, then the Socials package, then the marketplace packages, create-only), which main.py runs. Both failed on #1226's CI. They now read leader_stage.py: the same imports and calls, the same order (by the SECTIONS entries), and that main.py imports run_leader_seeds. Nothing they guarded is dropped.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
… mount point first

The egress-boot lanes (refused, blackholed) and the §9-Q7 bytecode experiment
start the backend alone (--no-deps) on a fresh stack. Docker refused every
backend container there:

  error mounting ".../workspaces" to rootfs at
  "/workspaces/00000000-0000-0000-0000-0000000000c1": ... mkdirat
  .../merged/workspaces/00000000-...-c1: read-only file system

docker-compose.yml's backend mounts workspaces_data at /workspaces read-only
(F220, on main) and nests the deliverables folder in it at
/workspaces/<DEFAULT_WORKSPACE_ID>. runc must create that mount point inside
the volume and cannot on a read-only mount. Reproduced with throwaway volumes
and redis:7-alpine: both mounts read-only on a fresh volume exit 125 with the
same error; after a container with the worker's shape (volume writable, the
same nested bind) has run once, the read-only pair starts.

Self-hosters do not hit it. make up / docker compose up start the worker
first (backend depends_on workspace-worker: service_started; the fresh-clone
lane's log shows the worker Started before the backend Starting), and the
worker's writable mount creates the mount point. The core stack
(docker-compose.core.yml) mounts the volume writable on the backend itself.
Only a backend started with --no-deps on a never-used volume meets it, and
--no-deps skips every depends_on, so a one-shot init service in compose would
not reach these lanes either.

So the lanes create it themselves: before any backend container, a one-off
backend under docker-compose.core.yml (`compose -f docker-compose.core.yml run
--rm -T --no-deps --entrypoint true backend`) creates the mount point with a
writable mount and exits. The boots keep docker-compose.yml's read-only
mounts, and the volume stays compose's, so `down -v` still removes it.

Tests: the egress smoke's order (datastores, mount point, probes, boots) on the
fake docker in both ways, the base and core mounts the step relies on, and the
bytecode script's step before its first boot. The five new or changed tests
fail on the scripts as they were.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
…need no embedding provider

The fresh-clone and smoke-images lanes failed the US-021 step:

  POST /api/missions/upload answered 500: {"detail":"File upload failed"}

with, in the backend's log, "Uploaded document to S3: s3://automatos-ai/
workspaces/<c1>/documents/1_prd262-us021-smoke.md", then "No embedding provider
configured/reachable; refusing to return random vectors." and "Error uploading
document prd262-us021-smoke.md".

The mission upload runs DocumentManager.upload_document: _record_upload stores
the source in object storage first, then _process_document chunks and embeds
it. Since ef92387 (PRD-185 S3, 4 Jul, on main) the disabled embedding
provider raises EmbeddingUnavailableError instead of returning random
vectors; the document is marked failed, the error is re-raised and the route
answers 500. CI has no embedding provider and must call no paid API, so that
route can never pass there, and US-021 is about object storage through the
pruned botocore, not retrieval.

The smoke now goes through the chat's attachments, which store a file and
nothing else (modules/attachments imports nothing of modules.rag or core.llm):

1. POST /api/attachments: AttachmentStore.put creates the bucket on first use
   and puts the bytes (ensure_bucket + put_object); the answer's size_bytes
   must be the size sent;
2. GET /api/attachments/<id>: the backend lists and HEADs the object and
   answers the same size;
3. docker exec in the backend's container runs AttachmentStore.sign_url, the
   app's presign for an attachment against S3_PUBLIC_ENDPOINT_URL (no route
   answers that link to a client), printed after a marker so no log line is
   read as the link;
4. unchanged: the link must be SigV4 on the origin compose publishes MinIO on,
   and the host's GET must return the bytes uploaded.

smoke-fresh-clone.sh passes --container "$BACKEND_CONTAINER" and --workspace
"$DEFAULT_WORKSPACE_ID". Product behaviour is unchanged.

Checked against a throwaway MinIO (cgr.dev/chainguard/minio, port 19000) from
the API venv: the real attachments router (permission dependencies
overridden) answered POST 200 and GET 200 with size_bytes 275, and
AttachmentStore.put/get plus SIGN_SNIPPET run as `python -c` in orchestrator/
gave a SigV4 link on the published origin whose GET returned the 276 bytes
put. That venv has the full botocore; the pruned one is what CI exercises.

Tests: the stand-in round trip and each way it breaks (refused upload, wrong
size, object not found, other origin, SigV2, other bytes, nothing
listening), the exec command and each way the sign fails, the snippet against
what the store defines, the routes in the manifest, the no-RAG imports, and
the smoke's wiring to the backend's container and the local workspace. The
suite does not collect against the old tool.

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

Copy link
Copy Markdown
Owner Author

CI state at ef68909, before merge. Every required check passes. All of these are green: test, images (api amd64 and arm64, with every in-image check), smoke-fresh-clone, bytecode-experiment, chart, browser-e2e and large-files.

Two non-required checks are red, both understood:

  1. smoke-images: the core-stack step. The scenario: make up CORE=1 on a fresh checkout, then start the worker, then GET /api/workspaces/<c1>/files. It answers 500, with PermissionError … /workspaces/<c1>/repos in the worker.
    • Cause: the deliverables folder bound at /workspaces/<c1> is created by the host user (uid 1001 on the runner), and the worker runs as uid 1000. On Linux, uid 1000 can't write a folder another user owns.
    • Not new: this is old behaviour on main, also with plain make up. This PR's core-stack step is the first CI step that lists the local workspace through the worker on Linux. Docker Desktop on macOS isn't affected.
    • Fix: a follow-up PR, fix/prd-262-worker-deliverables-owner. The worker entrypoint adopts the folder owner's (non-root) uid when uid 1000 can't write it. It gets a security review first, because it changes how the worker drops privileges.
  2. CodeQL: two py/incomplete-url-substring-sanitization alerts in test files (test_compose_published_images.py:214, test_prd262_dependabot.py:252: image.startswith("ghcr.io/")). They're fixed in the same follow-up by comparing the parsed host.

Fixed on this PR since it opened:

  • 07f180751: the stdlib compile runs with python -S, so setuptools' .pth no longer writes _distutils_hack bytecode into site-packages inside that layer.
  • 349a67d44: the boot seed wiring tests read core/seeds/leader_stage.py, where US-031 moved the seeds.
  • b7b110e16: the lanes that boot the backend alone make its workspaces mount point first.
  • ef6890999: the US-021 storage smoke goes through attachments, which need no embedding provider.

@AutomatosAI
AutomatosAI merged commit 0fc14da into main Oct 20, 2026
60 of 62 checks passed
@AutomatosAI
AutomatosAI deleted the feat/prd-262-api branch October 20, 2026 03:42
AutomatosAI pushed a commit that referenced this pull request Oct 20, 2026
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 added a commit that referenced this pull request Oct 24, 2026
… when uid 1000 cannot (#1314)

* fix(prd-262): the worker writes your deliverables folder as its owner when uid 1000 cannot

smoke-images' core-stack step (#1226, run of 20 Oct 03:04) got 500 from
GET /api/workspaces/<c1>/files once the worker started on the core stack. The worker
logged PermissionError: [Errno 13] Permission denied:
'/workspaces/00000000-0000-0000-0000-0000000000c1/repos' from
open_workspace -> ensure_workspace_exists.

Root cause: not the core stack's start order. /workspaces/<c1> is the bind-mounted
deliverables folder, created by the smoke (and by make up's mkdir) as the runner's
user, uid 1001, mode 755. The worker runs as uid 1000 and cannot create repos/ there.
The entrypoint's ownership block fires only when the volume root is not uid 1000, and
a fresh workspaces_data volume is uid 1000 whichever service mounts it first (both
images' /workspaces), so nothing makes the folder writable, by design: US-040/US-045
forbid re-owning the host folder. Reproduced with docker run --rm on the worker image
(edge, the same entrypoint as this branch) and a 1001:1001 folder nested in a fresh
volume: PermissionError on repos/ in BOTH orders, worker first (the default make up)
and backend first (CORE=1). The default stack never showed it because no CI step
listed the local workspace through the worker on Linux; Docker Desktop on macOS
lets uid 1000 write a host bind (checked: mkdir as uid 1000 succeeds, files land as
the Mac user), so nights never hit it. A self-hoster hits it on any Linux host whose
user is not uid 1000 (a second user, a CI runner), with make up or make up CORE=1.

Fix, in the worker's entrypoint, which already decides who the worker runs as and
fixes ownership for it:
- compose passes LOCAL_WORKSPACE_ROOT, the deliverables bind mount's target;
- when uid 1000 cannot write it (asked with setpriv as the worker, so Docker
  Desktop's binds, which show root:root but are writable, change nothing), the worker
  takes the folder owner's uid and gid (groupmod -o, usermod -o, which also re-owns
  /home/worker). What agents write there is the user's, as on macOS;
- a root-owned folder keeps uid 1000 and logs what to do: the worker never runs as root;
- the volume walk compares with the worker's uid and prunes LOCAL_WORKSPACE_ROOT as it
  prunes */projects, so it never re-owns host files (before, a root-owned volume
  re-owned the whole deliverables folder).
Railway and the chart set no LOCAL_WORKSPACE_ROOT: their walk is unchanged.

Checked in containers (edge worker image + this entrypoint): host folder 1001:1001,
worker first and backend first: the worker runs as 1001, ensure_workspace_exists
succeeds for the local and another workspace, a restart and a recreate keep it, the
probe stays 1001:1001 and a uid 1000 file in the folder stays 1000; folder 1000: no
change; root-owned: uid 1000 and the log line; no LOCAL_WORKSPACE_ROOT with a
root-owned volume: re-owned to 1000 as before; owners 1001:1000, 1001:100, 33:33,
65534:65534 all switch. test.yml's parity step, run the same way, passes all checks,
the new uid 1001 case included.

Tests: test_prd262_worker_boot.py drives the entrypoint with stand-ins (writable
folder, another user's, root's; the walk's new prune) and checks compose's
LOCAL_WORKSPACE_ROOT against the bind mount's target; 6 of its tests fail on the old
entrypoint and compose. prod-image-parity gains the uid 1001 case in the real image.
smoke-core-stack.sh also asserts that the reports/ folder the worker makes belongs to
the deliverables folder's owner. self-hosting.md's Linux ownership note and the
QUICKSTART line say what happens now.

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

* test(prd-262): read an image's registry from its parsed host, not a prefix

CodeQL flagged two test checks as py/incomplete-url-substring-sanitization
(alerts 307 and 308 on refs/pull/1226/merge): image.startswith("ghcr.io/") in
test_compose_published_images.py:214 and test_prd262_dependabot.py:252. They are
assertions, not sanitizers, but the check now compares the registry exactly:
urlparse(f"//{image}").netloc == "ghcr.io", the host before the reference's first
slash.

Not weakened: on eleven references (the published ghcr.io names with a tag, a
${AUTOMATOS_IMAGE_TAG:-edge} or a digest; the :dev names; redis, pgvector, Chainguard
MinIO; ghcr.io.evil/x and evil.com/ghcr.io/x; the empty string) the new check and
the old prefix agree. test_prd262_dependabot.py passes locally; the
docker-compose-config test in test_compose_published_images.py skips without the
docker CLI and runs in CI.

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
(cherry picked from commit 18b9105)

---------

Signed-off-by: Gerard Kavanagh <gerard161@gmail.com>
Co-authored-by: Gerard Kavanagh <gerard161@gmail.com>
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