Skip to content

S436 — Infra Code-Truth & Posture Verdicts

S436 — Infra Code-Truth & Posture Verdicts

Section titled “S436 — Infra Code-Truth & Posture Verdicts”

Task: read-only evidence pass answering Liam’s 6 pointed security/architecture questions against LIVE files. Every claim cites file:line. No repo/docs-site file was edited (this artefact is the only write). No network/SSH — where only a live probe could confirm, marked VERIFY-LIVE.

Baseline read: .user-scratch/client-deployment-architecture-s435.md (the explainer under review). Verdict vocabulary: INTENTIONAL-LONG-TERM / INTENTIONAL-INTERIM / OVERSIGHT / GAP.


Q1 — /walk + /extract public over HTTPS, bearer-gated: long-term or interim?

Section titled “Q1 — /walk + /extract public over HTTPS, bearer-gated: long-term or interim?”
FactSource
/walk gate = Authorization: Bearer ${CRON_SECRET}; unset→503 fail-closed, wrong→401, 2nd concurrent→409scripts/cocoindex_pipeline/server.py:487-498, 516-519
/extract gate = DEDICATED Bearer ${EXTRACT_API_TOKEN} (diff secret); unset→401 fail-closed; rate-limited (429); 20 MB body capserver.py:592-602, 606-609, 576-583
Container binds 0.0.0.0:8080 but publishes NO ports: mapping — reachable only via the coolify-proxy Traefik on the shared docker netserver.py:881-885
Traefik routes exactly /walk+/health+/extract on the public FQDN, HTTPS, LE cert (certresolver=letsencrypt) — all 4 appsdocker-compose.production.yaml:84-91; staging.yaml:107-114; platform.yaml:98-105; platform-staging.yaml:99-106
The disabled Coolify cron hits LOOPBACK, not the FQDN: both prod+staging scheduled tasks target http://127.0.0.1:8080/walk (runs INSIDE the container via python3 urllib / bash /dev/tcp; no curl in image)runbooks/onprem-b1-deploy.md:241-242, 256, 269, 244
App→pipeline calls run from Vercel over ${COCOINDEX_WORKER_URL} (serverless, no stable egress IP): walk-nudge on feed publish, folder-drop /walk, /extract cleanerlib/intelligence/pipeline.ts:526-529; lib/upload/folder-drop.ts:262-266; lib/extraction/clean-via-worker.ts:63; runbook :303-308

WHO must reach each route: (a) the cron — already loopback-internal, needs zero public reach; (b) the Vercel app — walk-nudge, /extract cleaner, folder-drop — runs on serverless with no stable egress IP, so it can only reach the pipeline via the public FQDN. That Vercel→VPS hop is the ONLY thing forcing public exposure. Alternatives, honest:

  • Traefik IP-allowlist — impractical: Vercel serverless egress IPs aren’t stable/enumerable without Vercel Enterprise “Secure Compute” dedicated egress.
  • mTLS at Traefik — feasible; app presents a client cert → 2nd independent factor beyond bearer.
  • Cloudflare Tunnel (cloudflared on VPS) + Access service-token — origin stops being directly public at all; strongest + simple to operate.
  • Keep public+bearer, harden — rate-limits, secret rotation, fail2ban/WAF.

Deliberate, documented, fail-closed, path-scoped, LE-certed — not an oversight. But public+bearer is not the ideal multi-client posture.

Fine for now (pre-launch, single tenant). Before multiple clients go live: (1) put the pipeline behind a Cloudflare Tunnel or require a client certificate (mTLS) so it isn’t directly on the open internet, keeping the password/token as a second lock; (2) add a rate-limit to /walk (only /extract has one today); (3) rotate the /walk and /extract secrets on a schedule. The scheduled auto-ingest cron already runs privately inside the box — good, leave it.


Q2 — /stage loopback-only, not Traefik-routed: should it be?

Section titled “Q2 — /stage loopback-only, not Traefik-routed: should it be?”
FactSource
/stage writes multipart fixture bytes straight into the corpus dir (COCOINDEX_SOURCE_PATH)server.py:265-350
Deliberately NOT routed (Inv-13) — Traefik has no router for it → 404 at edgeserver.py:881-884; compose docstring production.yaml:64-72; runbook :344-347
Reached only via docker exec loopback by the verify driverdeploy/onprem/verify/live-verify.sh:8, 194-195
Tension: live folder-drop upload UI → /api/ingest/folder-dropPOST {COCOINDEX_WORKER_URL}/stage from Vercelcomponents/create-content/upload-tab-content.tsx:118; lib/upload/folder-drop.ts:212

/stage accepts arbitrary bytes that then get walked + AI-extracted (token burn) + landed in the KB. Public exposure would be a corpus-injection vector. Keeping it loopback/docker exec-only is a correct, deliberate security decision — it’s a test-fixture staging seam for the verify driver, not a production ingest route. HOWEVER the folder-drop Path B upload (a live admin/editor UI path) POSTs /stage over the same public COCOINDEX_WORKER_URL — which Traefik 404s. So Path B’s stage-leg cannot complete from Vercel against the deployed split topology.

Verdict: INTENTIONAL-LONG-TERM (for /stage staying loopback-only) + GAP (folder-drop Path B)

Section titled “Verdict: INTENTIONAL-LONG-TERM (for /stage staying loopback-only) + GAP (folder-drop Path B)”

Keep /stage private — do not route it. Separately, VERIFY-LIVE whether the folder-drop upload tab actually works in production; by code-truth its /stage call 404s from Vercel. Either retire Path B in favour of the app-side Path A (/api/upload, which needs no /stage) or give the app a dedicated bearer-gated staging route. This is a real latent break, not cosmetic.


Q3 — pipeline-runs/record: two auth methods, or two layers?

Section titled “Q3 — pipeline-runs/record: two auth methods, or two layers?”
FactSource
Route enforces verifyCronAuth = Authorization: Bearer ${CRON_SECRET}, fail-closed if unset — SAME compiled route in every envapp/api/internal/pipeline-runs/record/route.ts:171; lib/cron-auth.ts:16-26
Staging pipeline → client’s staging Vercel preview; auth = Vercel protection-bypass token (preview deploys are SSO-walled; token gets THROUGH the platform wall)runbooks/client-app-deploy.md §3A.2 table + “Why a protection-bypass token (staging only)“
Prod pipeline → client’s real domain (no preview wall); auth = the route’s own bearersame §3A.2
S435 baseline compresses this to “Bearer/secret (prod) OR bypass token (staging)” — reads as either/or.user-scratch/client-deployment-architecture-s435.md:119

Two LAYERS, not two auth methods. The route’s CRON_SECRET bearer is in the code and enforced in both envs. The Vercel protection-bypass token is an additional platform-level gate that exists only on staging preview deployments (SSO wall); the token merely gets the caller THROUGH that wall to reach the route, which then STILL checks the bearer. On prod (real domain) there’s no wall, so only the bearer applies.

Verdict: INTENTIONAL-LONG-TERM (architecture correct); doc one-liner is imprecise

Section titled “Verdict: INTENTIONAL-LONG-TERM (architecture correct); doc one-liner is imprecise”

The runbook already states it correctly. Fix the S435 explainer’s one-liner (line 119) to say “two layers: the route’s bearer is always required in both envs; staging preview adds a Vercel platform SSO wall that the pipeline clears with a bypass token.” Docs SHOULD spell this out — it’s a common source of “is staging less secure?” confusion (it isn’t; it has an extra layer).


Q4 — goose on client deployments: per-client container?

Section titled “Q4 — goose on client deployments: per-client container?”
FactSource
goose = ONE service kh-goose-pilot on Server B (Platform), its own containerdeploy/coolify/goose-pilot/docker-compose.yaml:35-47
Reaches the app ONLY via the public remote-MCP endpoint (default prod platform MCP) — “just another MCP client on a timer”, no DB/pipeline accessdocker-compose.yaml:89 (GOOSE_MCP_URL), :19-21
Pilot-gated, digest-pin unresolved, NOT yet run E2E (G4 never completed, S424)compose G-notes :29-33, 54-59; baseline §(e); specs/id-71-ai-tooling/goose-pilot-deploy-plan.md
No per-client replication defined anywhere in compose or the deploy plan(absence)

Today goose is Platform-only and unproven. Because it touches the knowledge base ONLY through the public MCP surface (never client data-plane), a client does not need a goose container on its own VPS — one goose can serve many clients by pointing at each client’s MCP URL with that client’s service-actor bearer. The per-client topology is simply undecided and undocumented.

Verdict: GAP (doc coverage) — not a code oversight (goose is deliberately pilot-scoped)

Section titled “Verdict: GAP (doc coverage) — not a code oversight (goose is deliberately pilot-scoped)”

Don’t build per-client goose yet — the pilot hasn’t proven out. Once it does, document the model explicitly. Recommended: keep goose on Server B, one recipe + service-actor per client pointing at each client’s MCP URL; no goose on client VPSes needed.


Q5 — Raindrop (id-104 / B-INV-21): code state + per-client local deployment?

Section titled “Q5 — Raindrop (id-104 / B-INV-21): code state + per-client local deployment?”
FactSource
The three DEFERRED organs are pattern_detector / ab_runner / auto_rollbacknone is Raindroplib/eval/deferred-organs.ts:22-82
Graduation metric computed in-house from Supabase only — no Raindrop host, no Workshop write key (B-INV-18)lib/eval/graduation.ts:13-15, 76
Raindrop Workshop documented as local-only localhost:5899 + local SQLite, no account/key; hosted Raindrop EXCLUDED from client-data path (WS-14)specs/id-71-ai-tooling/headless-requirement-refinement.md:112, 114, 147; RESEARCH.md:475
Raindrop appears in docs-site ONLY inside id-71 specs — nowhere in runbooks/referencegrep of docs-site
No per-client local-Raindrop deployment documented anywhere(absence)

Code CONFIRMS the ruling: hosted Raindrop is excluded (B-INV-18 in graduation.ts); Raindrop is not a deferred organ (the brief’s “check deferred-organs.ts” framing is imprecise — it isn’t there). Raindrop Workshop is a dev/CI eval-authoring tool, local to whoever writes evals — never a client-runtime component. So per-client deployment doesn’t apply and its absence from docs is correct, not a gap.

Verdict: INTENTIONAL-LONG-TERM (exclusion enforced in code); minor doc-clarity note

Section titled “Verdict: INTENTIONAL-LONG-TERM (exclusion enforced in code); minor doc-clarity note”

No deployment action. Optionally add one line to platform-context.md clarifying “Raindrop = dev/CI eval tooling, local-only, excluded from client runtime” so the local-only vs per-client-deploy confusion doesn’t recur. Do NOT describe it as a deferred organ.


Q6 — manual-upload forms: what does cataloguing + extraction TODAY? Is Liam right?

Section titled “Q6 — manual-upload forms: what does cataloguing + extraction TODAY? Is Liam right?”
FactSource
Form template upload = APP-SIDE: .docx → Supabase Storage + form_templates row (status uploaded); NO cocoindexapp/api/procurement/[id]/templates/route.ts:179-216
Form/tender question extraction = APP-SIDE: download from Storage → direct Anthropic + mammothapp/api/procurement/[id]/questions/extract/route.ts:86-88; lib/domains/procurement/ai/extract-questions.ts:1-7
Auto-map = APP-SIDE string similarity (not AI, not cocoindex).../templates/[templateId]/auto-map/route.ts:139
Path C catalogue = human-confirmed app-side script → form_template_requirements; pipeline ingest NEVER writes the catalogue (Inv-20).claude/skills/catalogue-form-requirements/SKILL.md; lib/domains/procurement/form-templating/catalogue/from-instance.ts:163
Whole procurement/forms subsystem is cocoindex-free (only ref: a comment noting a prompt is a TS replica of the Python one)grep app/api/procurement + lib/domains/procurementfrom-instance.ts:110 (comment only)
CONTRAST — URL/content path DOES use cocoindex: feed URL publish → nudgeCocoindexWalkPOST /walk → cocoindex extractslib/intelligence/pipeline.ts:676, 526-529
Vestigial corpus-forms machinery (workspace_resolver RouteKind “forms” + flow.py fork + manifest-gate test) still on main until BL-392baseline §(g) item 8; DR-014

Liam is partly right, but not for forms. For CORPUS/URL content — reference_items via feed publish, and Path B folder-drop generic files — yes, cocoindex still does the extraction, just triggered on-demand (/walk) instead of watching. But forms are app-side end-to-end (DR-014 decoupled them): upload to Storage, extract questions with a direct Anthropic call, auto-map + catalogue in the app. cocoindex is not in the forms path at all. The only “gap” vs DR-014 target state is dead-but-present corpus-forms code in the Python pipeline that BL-392 will delete — it’s not an active second path (nothing feeds forms to the corpus anymore).

Verdict: INTENTIONAL-LONG-TERM (DR-014 forms→app-side decoupling)

Section titled “Verdict: INTENTIONAL-LONG-TERM (DR-014 forms→app-side decoupling)”

Tell Liam: for web pages / feed URLs / dropped files, cocoindex does the extraction (fired by an on-demand walk, not a watcher). For forms specifically, the app does the extraction directly — cocoindex isn’t involved. That split is intentional (DR-014). The leftover forms code in the pipeline is scheduled for removal under BL-392.


  1. Rate-limit /walk — parity with /extract’s _rate_limit_allows; /walk has none today (server.py).
  2. Rotate CRON_SECRET + EXTRACT_API_TOKEN on a schedule — long-lived shared secrets.
  3. De-overload CRON_SECRET — it gates /walk inbound, pipeline-runs/record inbound, datapath-watch + intelligence crons, AND the outbound walk-nudge (cron-auth.ts, server.py:487, pipeline.ts:517). Split the pipeline-trigger secret from the Vercel-cron secret (as /extract already did) to shrink blast radius.
  4. Cloudflare Tunnel or mTLS in front of the pipeline for multi-client GA so the origin isn’t directly public; keep the bearer as defence-in-depth.
  5. fail2ban / CrowdSec on the VPS for auth-fail bursts against the pipeline FQDNs.
  6. Reconcile folder-drop Path B vs /stage-not-routed (Q2) — private/bearer-gated staging reach, or retire Path B for Path A. VERIFY-LIVE first.
  7. Doc fixes: S435 line 119 (Q3 two-layers); add goose per-client model (Q4) + Raindrop dev-tooling note (Q5).
  • Does folder-drop Path B upload actually succeed in the deployed split topology? (Q2 — code says its /stage leg 404s from Vercel.)
  • Confirm COCOINDEX_WORKER_URL in each Vercel env = the public pipeline FQDN (assumed from code; not probeable here).
  • Confirm the two /walk Coolify scheduled tasks remain enabled: false in the live Coolify UI (Q1/§d).