Deployment Architecture — client + platform topology
Deployment Architecture
Section titled “Deployment Architecture”Status: authored S436 (2026-07-02) from a verified evidence pass — live compose files,
server.py, proxy.ts, route code, live HTTPS probes, the OKF design lineage, and the
S436 owner decision board. Restated S441 (2026-07-03) to the ratified corpus reframe
(DR-025/DR-026/DR-027) — §3 layer definitions and §6 gate tiers carry the new model.
Where a runbook disagrees with this doc, this doc was checked against the live files more
recently; fix the runbook (see the S436 stale-flag sweep). Rulings referenced as DR-NNN
live in reference/decision-register.md — ids swept from the register (DR-012/013/015/020/024
here) resolve via its “Retired ids” note + git history; §11 carries their one-line substance. Refreshed S491 (22/07/2026) against the
ledger + live route code: id-131/133/134/135/136/138 closed; the first Platform OKF bundle
published; the {127.18} secret split live (PIPELINE_TRIGGER_SECRET); the S456/S458
platform-agents architecture (DR-042..DR-046) folded into §7/§9 — depth in
reference/platform-agents-architecture.md. Refreshed S507 (28/07/2026) against PR #141
(id-389 tier extension): the platform composes now carry ANTHROPIC_BASE_URL /
ANTHROPIC_AUTH_TOKEN passthrough so Platform prod AND staging can route the extraction
lane via the OpenRouter Anthropic skin (dogfooding-only until launch; no lane may depend on
the disabled direct Anthropic key). The mechanism + the empty-string guard that makes
${VAR:-} passthrough safe are documented in §2.2.
1. Plain-language overview
Section titled “1. Plain-language overview”A Canonical deployment is three moving parts per client, plus one shared control box:
- The ingestion pipeline (cocoindex) — a background worker in a container on a VPS. It reads the client’s connected evidence sources (files today; connectors later), uses AI to extract structured records (questions/answers, reference items, chunks, entities), and writes them into the client’s own database — where record classes that carry human curation change only via the promotion path (DR-025/DR-026).
- The web app (Next.js on Vercel) — the client-branded site users log into. It reads the records and serves search, browse, and AI tools (including the remote-MCP surface agents connect to).
- The knowledge bundle (OKF, L-concepts) — markdown “concept” files forming the map
agents use to traverse the client’s knowledge. Client-owned, produced by our producer
(id-132), versioned in a client-owned private git repository. The first Platform
bundle has published (11/07/2026 — repo
canonical-okf-showcase, first commit3f66483, 18 concepts); client bundles are still future.
The shared control box is Coolify: one control-plane on the Platform VPS that deploys and manages the pipeline containers on both the Platform box and each client box. Every tenant gets its own dedicated VPS and its own Supabase project — client data never shares a machine or database with another tenant.
Everything is pre-launch. Ingest, the scoped pipeline ingress, the two-server split, the client app, the goose scaffold, the records-model refactor (id-131, closed S451), the ontology linter (id-133, S452), the promotion gate (id-134, S452), the corpus durable home (id-138, S452), search/browse surfaces (id-135, S485), and the producer’s first Platform bundle ({132.15}/{132.16}) are built — as is the private pipeline ingress ({127.20}/{127.38}) for the two platform hosts (S558, 2026-08-12); id-132 close-out (client bundles), the same private-ingress cutover for the two client hosts, the goose pilot ({71.26}/{71.27}), and the first full client re-ingest (id-45, the cutover gate) are in flight.
2. Topology
Section titled “2. Topology”flowchart TB subgraph CTRL["ONE Coolify control-plane (v4.x) — lives on Server B"] COOLIFY["Coolify dashboard\n(deploys + manages BOTH servers)"] end
subgraph SB["Server B — PLATFORM VPS (87.106.72.59)"] direction TB PPROD["cocoindex: ca-pipeline-platform-production\n→ Platform PROD DB"] PSTG["cocoindex: ca-pipeline-platform-staging\n→ Platform STAGING DB"] GOOSE["goose pilot (kh-goose-pilot)\npilot only — target model is per-client (DR-017)"] COOLIFY end
subgraph SA["Server A — CLIENT VPS (88.208.255.178)"] direction TB CPROD["cocoindex: kh-onprem-pipeline-production\n→ Client PROD DB"] CSTG["cocoindex: kh-onprem-pipeline-staging\n→ Client STAGING DB"] CGOOSE["(target) client goose container\narrives at goose G6 adopt (DR-017)"] end
COOLIFY -. manages .-> PPROD & PSTG & GOOSE & CPROD & CSTG
subgraph RAW["L-raw — source bindings + evidence register (client-owned sources)"] FILES["Client file server / local-fs / feeds / uploads\n(.docx .pdf websites) — per-binding retention class (DR-025)"] end
subgraph DBS["Supabase — four databases (L-records)"] DBPP["Platform prod zjqbrdctesqvouboziae"] DBPS["Platform stg rbwqewalexrzgxtvcqrh (.env.local / CI target)"] DBCP["Client prod rovrymhhffssilaftdwd"] DBCS["Client stg turayklvaunphgbgscat"] end
subgraph OKF["L-concepts — CLIENT-OWNED private git repo (id-132)"] BUNDLE["OKF bundle: concept .md + index.md + log.md\nconcepts cite records via canonical://table/uuid"] end
subgraph VERCEL["Vercel — client-branded Next.js app (login-gated)"] APP["client domain (Platform's own: kh.aisolutionhub.co.uk)"] MCP["/api/mcp remote-MCP endpoint"] HOOK["/api/internal/pipeline-runs/record webhook"] end
FILES -->|"POST /walk (bearer, public HTTPS)"| CPROD CPROD -->|"AI extract + embed → UPSERT"| DBCP CPROD -->|"run finished → webhook"| HOOK DBCP -->|"2-pass producer (id-132)"| BUNDLE DBCP --> APP BUNDLE --> APP GOOSE -->|"cron → remote-MCP (bearer)"| MCP MCP --> APPOne-line reading: client files → bearer-gated POST /walk → cocoindex on the client VPS →
client Supabase DB → OKF bundle in the client’s git repo → the client’s Vercel app. Coolify
on Server B manages every container. goose is an MCP client on a timer — it never touches
the pipeline or databases directly.
Domains: the four pipeline hostnames currently sit on the platform-owned
*.aisolutionhub.co.uk (temporary); the client pipeline host flips to a client domain at
the id-45 handover (runbooks/per-client-release-pins.md).
2.1 Bid-worker services in the platform composes
Section titled “2.1 Bid-worker services in the platform composes”Both platform composes (docker-compose.platform.yaml prod,
docker-compose.platform-staging.yaml staging) declare a bid-worker sibling service
alongside the cocoindex sidecar — bid-worker-platform (container
ca-bid-worker-platform) and bid-worker-platform-staging
(ca-bid-worker-platform-staging). The bid-worker is the on-prem processing_queue
consumer for the procurement form-first lane (ID-145): it runs the analyse_form and
template_fill jobs from scripts/bid_worker.py (command: ["python3", "-m", "scripts.bid_worker"]). It is not part of the cocoindex pipeline surface and is omitted
from the mermaid diagram above; it carries no Traefik ingress because bid_worker.py is a
pure poller with no HTTP server (no healthcheck — restart: unless-stopped covers
crash-recovery).
It reuses the same kh-cocoindex-pipeline image as the cocoindex sidecar — scripts/**
is packaged whole by onprem-deploy.yml, so no second image build/CI pipeline is needed,
only a second container running the same artefact with a different entrypoint.
CNB launcher entrypoint (ID-145.31/S481, 16/07/2026). The Google Cloud Buildpacks
image’s default ENTRYPOINT is /cnb/process/web, which pins the container to the web
process type and silently ignores compose command:. Before the fix the bid-worker
containers reported Up while bid_worker.py never actually ran — the cocoindex HTTP
wrapper booted instead of the poller, and the processing_queue consumer had never run on
staging or prod. Both bid-worker services now set
entrypoint: ["/cnb/lifecycle/launcher"], which execs the custom command: under the
buildpack env. The fix shape was proven empirically on Server B
(docker run --entrypoint /cnb/lifecycle/launcher … python3 -m scripts.bid_worker boots
the poller).
LibreOffice (soffice) provisioning (DR-059, ID-145.31). The .doc/.xls
convert-on-upload lane in bid_worker.py shells out to soffice; LibreOffice is baked
into the shared kh-cocoindex-pipeline image by the “Bake git + LibreOffice” post-pack
layer in .github/workflows/onprem-deploy.yml — not by the compose or any
buildpack/Dockerfile. The cocoindex sidecar shares the same image but never calls
soffice itself (accepted bloat per the G8 folded-worker decision). bid_worker.py
itself is stateless (no LMDB/engine store); the LibreOffice convert step and
template-fill’s tempfile I/O both use ephemeral container-local temp paths.
2.2 LLM-tier substrate — env-driven model selection (id-389)
Section titled “2.2 LLM-tier substrate — env-driven model selection (id-389)”The cocoindex extraction lane selects its Anthropic backend purely from deploy env, with no code fork between mock / OpenRouter / real-Anthropic surfaces. The platform composes carry the three credential envs as passthroughs so Coolify is the single tier-selection knob:
| Compose | ANTHROPIC_BASE_URL | ANTHROPIC_AUTH_TOKEN | Default backend |
|---|---|---|---|
docker-compose.platform.yaml (prod) | ${ANTHROPIC_BASE_URL:-} (default unset → api.anthropic.com) | ${ANTHROPIC_AUTH_TOKEN:-} (default unset → unused) | Real Anthropic API via ANTHROPIC_API_KEY. No mockllm sibling in prod by design — mock is a staging/CI tier. |
docker-compose.platform-staging.yaml | ${ANTHROPIC_BASE_URL:-http://mockllm-platform-staging:8080} | ${ANTHROPIC_AUTH_TOKEN:-} (default blank → unused) | The Stacklok mockllm sibling (mockllm-platform-staging) — the existing staging default. |
Operator recipe for an OpenRouter tier-2 run on either surface: set BOTH
ANTHROPIC_BASE_URL=https://openrouter.ai/api and ANTHROPIC_AUTH_TOKEN=<OpenRouter key> in Coolify; extraction.py’s _extraction_async_client() then forces
ANTHROPIC_API_KEY out of the process env so the real Anthropic key never reaches a
third-party endpoint. Unsetting both vars reverts to the direct-API default. The app-side
client (lib/anthropic.ts) mirrors the same routing: a non-empty
ANTHROPIC_AUTH_TOKEN constructs the SDK client with authToken + apiKey: null (both
credentials are rejected by the API, and a non-empty Anthropic-shaped X-Api-Key pins
OpenRouter’s provider routing).
Why empty-string is treated as unset (the id-389 guard). Compose ${VAR:-} renders
the var as a literal empty string when unset, and anthropic==0.79.0 treats '' as SET for
all three credential envs — base_url="" beats the SDK default and breaks every request,
and auth_token="" emits a broken Authorization: Bearer header (verified S505 +
2026-07-28). extraction.py:_extraction_async_client() scrubs empty-string values from
os.environ before construction, so the SDK falls through to its normal default for that
field. This is what makes the ${VAR:-} passthrough above a safe deploy shape.
Dogfooding posture (S506, owner-directed). Until launch, Platform staging AND prod are
dogfooding-only surfaces — the direct Anthropic key is disabled (unattributed charges), so
no lane may depend on it. The eval-nightly.yml lane mirrors the same posture: its
llm_tier choice input defaults to openrouter (real Claude billed via OpenRouter
credits) with anthropic as a parity opt-in; see runbooks/ci.md §6.6.
3. The three-layer knowledge model (OKF)
Section titled “3. The three-layer knowledge model (OKF)”Two of the three layers are client-owned; only L-records is operated by Canonical.
| Layer | What it is | Owner | Where it lives |
|---|---|---|---|
| L-raw | Source bindings + the evidence register (DR-025): connected evidence sources, each with a retention class (keep-and-watch / ingest-once / live-connected / external-referenced), plus the permanent source_documents provenance rows (survive byte discard) | Client (sources); register rows in the client’s DB | Kept evidence: corpus bucket in the client’s OWN Supabase project (DR-023); the VPS mount (COCOINDEX_SOURCE_PATH) is a rebuildable working copy; never in git |
| L-records | Extracted, structured records: source_documents, q_a_pairs (+ raw q_a_extractions), reference_items, content_chunks, entity graph, citations, record_embeddings (EMB-STORE), record_lifecycle (per-axis governance). Promoted/curated records are CANONICAL — normal production data, backed up, never regenerated; engine-owned staging/derived tables are the rebuildable part | Canonical-operated, per-client Supabase project | The client’s DB. content_items is eliminated (id-131, closed S451) |
| L-concepts | The OKF bundle — the distilled map agents traverse; a Q&A pair is a record, never a concept. Durable via its own git history + log.md; citations degrade gracefully to the evidence register when source bytes are discarded (DR-025) | Client | Client-owned private git repo (id-132; DR-016), carrying the materialised effective ontology (DR-027) |
Load-bearing facts that keep getting re-derived:
- The canonical layer is the client DB + the bundle (DR-025) — sources are evidence, not the store of record; authority is earned at the knowledge-admission gate (promotion for records; linter + publish gate for concepts), never inherited from a folder. Full-replace is a pre-launch tool only (DR-023); the pipeline’s write boundary is the staging layer — walks never mutate promoted records; changes to promoted records arrive as proposals with auto-apply earned per progressive trust (DR-026).
- Record identity is minted at admission (DR-024 clause i) — stored, never re-derived;
rel_pathis a mutable attribute. The point-of-no-return is FIRST bundle publication: the SEED-CONTRACT (seed strings + namespace + thecanonical://<table>/<uuid>scheme) must be frozen before the first bundle ships or every citation orphans (id-132 publish gate BI-20/21). For the Platform bundle this point has passed — first publish 11/07/2026 ({132.15}), so the SEED-CONTRACT is live; each client’s own first publish is that client’s point-of-no-return. - Bundle citations target
{source_document, reference_item, concept}only — q_a_pair citation is DB-internal by design. - Concept granularity is a product decision (product/topic/cert/company/metric), not one-file-per-record mechanics.
- The producer is 2-pass: Pass 1 drafts purely from L-records (the extracted corpus — no web); Pass 2 enriches only from retained evidence + the register (host-allowlist, depth-limited — never the open web; restated from “the gated corpus” per DR-025). The ontology (id-133, delivered S452) gates both extraction and concept writes as a semantic linter.
- Producer trigger (DR-018, built {132.16}): a successful ingest walk with
source_documentsdeltas chains a producer run; regeneration is incremental (memoiseddeclare_file— only affected concepts rewrite). Manual operator invocation is the bearer-gatedPOST /producer-runforced-run surface (DR-055; loopback-only, not Traefik-routed — see §4). - Git is the storage medium, deliberately (DR-016): a git working tree IS a local-fs folder; git adds rollback, per-change authorship, atomic publish, and the diff substrate the human review bridge requires. Clients never touch git — they own the repo; the product consumption surface is the app/viewer/MCP, and the storage layer is never the security boundary. Approved human edits are captured as producer overrides re-applied on every regeneration (id-132 invariant) — never direct file mutations; the accept/edit/reject review UI is a follow-on human surface (DR-013 shape).
- RAG is reduced, not removed: the EMB-STORE vector index stays over bundle + long tail.
- Conformance posture (DR-019): the bundle is genuine OKF (Google spec v0.1) as a
documented deliberate superset — closed validator-enforced type taxonomy and the opaque
canonical://scheme are spec-legal producer-side strictness. House rules: ISO-8601 intimestampfrontmatter +log.md(UK date format in body prose only),okf_version: "0.1"stamped in the bundle index, the closed concept-typeset (topic/product/company/certification/case_study) withmetric/dataset/playbookas facet tags (DR-019 as amended 15/07/2026 — never free-form types), a bundleCONFORMANCE.mddocumenting the supersets.
4. Public vs private — every surface
Section titled “4. Public vs private — every surface”| Surface | Public URL? | Gate | Source |
|---|---|---|---|
Pipeline POST /walk (×4 apps) | Client hosts (×2): yes — public HTTPS. Platform hosts (×2): NO — private (Cloudflare Tunnel + Access, S558) | Platform hosts: Cloudflare Access service-token policy at the edge (CF-Access-Client-Id/-Secret) then the bearer. Both postures: dedicated Bearer ${PIPELINE_TRIGGER_SECRET} ({127.18} — the legacy CRON_SECRET no longer authenticates here; dual-accept window retired S457); rate-limited ({127.17}); unset→503 fail-closed; concurrent→409 | server.py _walk_handler (~:898); runbooks/private-ingress-cutover.md |
Pipeline POST /extract (×4) | Client hosts: yes — public HTTPS. Platform hosts: NO — private (as above) | Access service token (platform hosts) + dedicated Bearer ${EXTRACT_API_TOKEN}; rate-limited; 20 MB cap | server.py _extract_handler (~:1299) |
Pipeline GET /health (×4) | Client hosts: yes, intentionally. Platform hosts: no — behind Access; the external uptime monitor is retired (DR-046) | Client hosts: none — liveness only, returns no data. Platform hosts: Access service token | server.py _health_handler (~:304) |
Pipeline POST /stage (×4) | No — not Traefik-routed; loopback/docker exec only (a public probe gets a Coolify catch-all 503, never the app) | Test-fixture seam only — accepts raw bytes into the corpus dir | server.py _stage_handler (~:320); compose Inv-13 |
Pipeline POST /producer-run | No — not Traefik-routed; loopback only | Bearer ${PIPELINE_TRIGGER_SECRET}; rate-limited; single-flight 409 — the DR-055 manual forced-run surface for the OKF producer | server.py _producer_run_handler (~:1206) |
| The 4 pipeline FQDNs | 2 client: yes. 2 platform: no (S558, 2026-08-12) — proxied Cloudflare Tunnel CNAME; Server B’s inbound 443+80 CLOSED at the VPS firewall | Traefik path rule = exactly /walk|/health|/extract (unchanged in both postures — the tunnel routes through Traefik); Let’s Encrypt certs on the public pair, Cloudflare edge TLS on the private pair | all four deploy/coolify/docker-compose.*.yaml; runbooks/private-ingress-cutover.md |
| Client web app | Yes (client domain) | Supabase login; unauthenticated → /login except proxy.ts publicRoutes; sign-up fail-closed to the client’s allowed domain | proxy.ts |
/api/mcp (remote MCP) | Yes | Bearer ${MCP_BEARER_TOKEN} + X-MCP-Actor (service-actor grant) | app/api/mcp/[transport]/route.ts |
/api/internal/pipeline-runs/record | Yes | Two layers, not two methods: the route’s Bearer ${PIPELINE_TRIGGER_SECRET} ({127.18} — verifyPipelineTriggerAuth, lib/cron-auth.ts; Vercel-cron /api/cron/* routes stay on CRON_SECRET) is enforced in BOTH envs; staging additionally sits behind Vercel preview Deployment Protection (SSO), which the pipeline clears with a protection-bypass token. Staging has one extra lock, not a different one | record/route.ts:200; runbooks/client-app-deploy.md §3A.2 |
| L-raw / L-records / L-concepts | No | Client-side / private Supabase + RLS / private client repo | — |
| Coolify dashboard | Yes (own FQDN) | Coolify auth; one instance, Server B | runbooks/onprem-b1-deploy.md |
Cutover status (S558, 2026-08-12). The two platform pipeline hosts are now private:
ca-platform-pipeline-staging.aisolutionhub.co.uk(S555) andca-platform-pipeline.aisolutionhub.co.uk(S558) sit behind a Cloudflare Tunnel with an Access service-token policy, their DNS is a proxied tunnel CNAME, and Server B’s inbound 443+80 are closed at the VPS firewall. The bearer (PIPELINE_TRIGGER_SECRET/EXTRACT_API_TOKEN) is unchanged as the second factor, and app→pipeline calls now sendCF-Access-Client-Id/CF-Access-Client-Secret(CF_ACCESS_*in Vercel Production+Preview and the GHProduction/Stagingenvironments). The external/healthuptime monitor is retired (DR-046). The two CLIENT hosts on Server A remain public — their cutover is ordered 3rd/4th and is a separate task. Procedure, rollback and verification:runbooks/private-ingress-cutover.md.
Ingress posture (DR-015 → DR-046): public + bearer was deliberate but interim, and
is now the client-host-only posture. Only
the Vercel app forces public reach (serverless, no stable egress IP) — the scheduled ingest
cron already fires on loopback inside the container (http://127.0.0.1:8080/walk), and
needs no public path. The S436 hardening has landed: {127.17} /walk rate-limit done,
{127.18} CRON_SECRET split live in code (PIPELINE_TRIGGER_SECRET sole bearer;
redeploy-gated re-smoke outstanding), {127.19} rotation runbook done
(runbooks/secret-rotation.md). The private-ingress endgame is now ratified as DR-046
(S456): Cloudflare Tunnel default, Static-IP allowlist escape hatch, and
ingress-is-control-plane-only (push inlets terminate app/bucket-side; connectors are
outbound-pull). {127.20} is executed for both platform hosts (S555 staging, S558 prod;
{127.38} landed first as its hard gate); the two client hosts are still to move. Bearer
tokens stay as defence-in-depth in every posture.
5. End-to-end data flow
Section titled “5. End-to-end data flow”Legend: [A] automated · [M] operator · [BUILT] shipped · [PLAN] in flight.
- [A][BUILT] Client raw files (L-raw) are mounted read-only into the pipeline
container (
COCOINDEX_SOURCE_PATH). - [A][BUILT] A walk is triggered by bearer-gated
POST /walk(202 + requestId, single-flight). Boot never walks (burn-safety, ID-83). Since id-138 (closed S452) the walk first runs a pull-sync (under the {138.9} writer fence) that materialises the VPS working copy from the durablecorpusbucket in the client’s own Supabase project — the bucket is authoritative, the mount rebuildable. - [M today, A at cutover][BUILT, DISABLED] The recurring trigger is a Coolify
scheduled task curling
/walkon loopback inside the box — built on both envs, deliberatelyenabled: falseuntil cutover. Publishing a feed URL fires a one-off walk-nudge from the app. The Verceldatapath-watchcron (*/15) only monitors. - [A][BUILT] cocoindex is incremental: content-hash memoisation; unchanged files short-circuit.
- [A][BUILT] Changed files: binary → markdown (Docling) → AI extraction (Anthropic) → embeddings → UPSERT into the client DB (L-records) — engine-owned staging + derived tables; promoted records change only via the promotion path (DR-026).
- [A][BUILT] On completion the pipeline POSTs
/api/internal/pipeline-runs/recordsopipeline_runsis recorded and the datapath monitor can verify rows landed. - [A][BUILT for the Platform bundle — id-132 close-out in flight] The OKF producer
runs as a separate cocoindex flow — chained off a successful walk when
source_documentschanged (DR-018, {132.16} done), withPOST /producer-runas the DR-055 manual forced-run surface. Pass 1 from L-records; Pass 2 from retained evidence- the register (DR-025). Output goes to a staging state; the git knowledge-sync
writer commits one commit per producer run after the publish/review gate. The
first Platform bundle published 11/07/2026 ({132.15} —
canonical-okf-showcase, first commit3f66483, 18 concepts): the SEED-CONTRACT point-of-no-return §3 anticipates has occurred for the Platform bundle; client bundles remain future.
- the register (DR-025). Output goes to a staging state; the git knowledge-sync
writer commits one commit per producer run after the publish/review gate. The
first Platform bundle published 11/07/2026 ({132.15} —
- [A][BUILT] The client’s Vercel app serves L-records (login-gated UI + API, remote
MCP); with id-135 closed (S485), humans and agents browse/search the OKF bundle and
follow
canonical://citations back to records.
Forms are NOT in this flow (DR-014): forms enter app-side end-to-end — manual upload
to Storage, direct in-app AI question-extraction, human-confirmed catalogue
(form_template_requirements). cocoindex never touches forms; the vestigial corpus
forms-route is retired (BL-392 → id-136, PR #113 merged S448; the DR-014 app-side
manual form-upload UX is owned by ID-145).
6. Corpus admission — the gate has two tiers
Section titled “6. Corpus admission — the gate has two tiers”Restated S441 (DR-025): nothing becomes canonical unmediated — but the front door is deliberately light. The gate has two tiers:
- Binding admission (the light tier — connecting a source at all): privileged users (admin/editor) connect sources / contribute documents through the app-side upload path and assign each binding’s retention class (keep-and-watch / ingest-once / live-connected / external-referenced). Authoritative-source criteria inform the class choice; they do not bar messy evidence from entering — value-from-messy-data is the pitch, and authority is earned downstream. (DR-020 as restated by DR-025; implemented via {131.24} + ID-138 — both closed.)
- Knowledge admission (the authoritative tier — what becomes canon): record
promotion (dedup review + confidence gate + human sign-off, auto-apply earned per
progressive trust — DR-026), and for concepts the id-133 ontology register + semantic
linter (
allowed_types/allowed_relations, HARD-reject/SOFT-WARN) over extraction and concept writes. Per-valueprovenance(core/client) is what lets a client legitimately widen the allowed set without a code change. id-133 owns ONLY this tier — the binding gate lives with the app-side upload path (id-131 rework/ID-138), not the ontology register.
Folder-drop: the folder-drop UI’s /stage leg was a confirmed live break (it POSTed a
route Traefik deliberately doesn’t expose) — the ratified disposition (DR-020) has
landed with id-131 ({131.24}): the /stage transport is retired from the UI and
folder-drop routes through the gated app-side path. /stage itself stays loopback-only
test plumbing, never public.
Pipeline state that cannot be rebuilt: the cocoindex LMDB engine store on the pipeline box is the walk’s incremental-change memory — losing it silently turns the next walk into a full re-extraction (cost + duplicate-churn tail). Mutual exclusion + recovery: runbooks/corpus-writer-fence.md.
7. goose (id-71) — the outbound agent layer
Section titled “7. goose (id-71) — the outbound agent layer”- goose is a headless agent runtime: cron →
goose run --no-session --recipe→ the app’s public remote-MCP endpoint withBearer ${MCP_BEARER_TOKEN}+X-MCP-Actor. It is just another MCP client on a timer — no pipeline access, no DB access. - Today: one pilot container (
kh-goose-pilot) on Server B, staging-pointed; substrate files landed (G0, S421) but not yet proven end-to-end — the G1–G6 owner-gated deploy track remains open under {71.26}/{71.27} (bothin_progress), with zero AI spend so far (per the 09/07/2026 arch re-check). Pilot gates G0–G6:specs/id-71-ai-tooling/goose-pilot-deploy-plan.md. - Target topology (DR-017, ratified S436; generalised S456 by DR-045): per-client goose on each client’s own VPS at client rollout — the agent processing a client’s knowledge (and its API spend) lives on that client’s box, honouring the isolation promise. The pilot stays on Server B until G6 adopt; per-client rollout is a post-pilot task.
- The decided platform-agents architecture (S456/S458 — DR-042..DR-046):
reference/platform-agents-architecture.md. Agent posture is a per-client choice behind a thin swappable interface (DR-044): the cloud track runs agents in the client’s own Vercel project (the first client is cloud-native); strict on-prem runs the local-equivalent stack on the client VPS, with goose as its agent runtime. Data plane per-client, control plane central and telemetry-only (DR-045); the goose pilot completes on the DR-017 topology regardless — the two postures unify behind the one interface. One owner residue: OQ-71.26-COST (per-tenant Vercel cost ceiling), awaiting sign-off.
8. What is NOT a client-deployed component
Section titled “8. What is NOT a client-deployed component”- Raindrop Workshop (id-104/id-71): a local dev/CI eval-authoring tool
(
localhost:5899, zero egress — enforced inlib/eval/graduation.ts); hosted raindrop.ai is permanently excluded from the client-data path. There is no per-client Raindrop deployment. - The docs-site, ledgers, and this doc: private operator material — never deployed to clients.
9. Per-client rollout model (what “a new client” means)
Section titled “9. Per-client rollout model (what “a new client” means)”Each client standup comprises: a dedicated VPS (Coolify-managed from Server B) · two
cocoindex apps (prod + staging) with scoped Traefik ingress + LE certs · two Supabase
projects (prod + staging; schema + ACLs via the release-pinned migration chain, which lands
the DR-035/id-347 anon posture unaided — S493; seed data + storage buckets are the two
explicit extra steps) · a client-branded Vercel app (own domain,
sign-up domain-gated, tenant_config) · a client-owned private OKF repo (created by the
producer’s knowledge-sync at first publish) · the client’s agent posture choice
(DR-044 — cloud track: agents in the client’s own Vercel project, the first client’s
shape; strict on-prem: post-pilot goose container on the client VPS; see
reference/platform-agents-architecture.md) · client domain cutover for the pipeline
host at handover. One public GitHub repo
serves all tenants — clients get no GH environment (id-95). Depth:
runbooks/onprem-b1-deploy.md, runbooks/client-app-deploy.md,
runbooks/per-client-release-pins.md, runbooks/platform-to-client-promote.md.
10. Built vs in-flight (by task)
Section titled “10. Built vs in-flight (by task)”| State | What |
|---|---|
| BUILT | L-records ingest (walk/extract/embed/UPSERT) incl. pull-sync from the durable corpus bucket (id-138, S452); scoped ingress ×4 (client ×2 public; platform ×2 private behind Cloudflare Tunnel + Access — {127.20}, S555/S558); two-server split (id-127); pipeline-runs webhook; client app + branding + signup gating; goose scaffold ({71.24}) + G0 substrate files (S421); id-131 L-records refactor (S451 — content_items era over); id-133 ontology linter (S452); id-134 promotion-confidence gate (S452); id-135 search/browse (S485); id-136 forms-route retirement (S448); {127.17} /walk rate-limit + {127.19} rotation runbook + {127.18} secret split (code-live; redeploy-gated re-smoke pending); OKF producer + first Platform bundle publish ({132.15}/{132.16}, 11/07/2026) |
| IN FLIGHT | id-132 close-out (client bundles future; task in_progress); id-45 client cutover; private ingress for the two CLIENT hosts (DR-046 shape, ordered 3rd/4th — the platform pair is done, S555/S558); goose pilot G1–G6 ({71.26}/{71.27}); id-163 OKF system bundle (spec-tier); OQ-71.26-COST owner sign-off |
11. Decision index (rulings this doc encodes)
Section titled “11. Decision index (rulings this doc encodes)”Do not compress this section away. §11 (and §7) are DR-tombstone targets —
scripts/generate-dr-tombstones.tsrecords retired decisions’ substance as moved HERE. Deleting it orphans thosesubstance_moved_topointers.
DR-012 (source-doc route re-point owned by id-131) · DR-013 (id-135 net-new surfaces) ·
DR-014 (forms manual-upload; corpus forms-route retired) · DR-015 (ingress: public+bearer
interim; private ingress before multi-client GA) · DR-016 (OKF git medium + access model +
override invariant) · DR-017 (per-client goose topology) · DR-018 (producer post-walk
trigger) · DR-019 (OKF documented-deliberate-superset conformance posture) · DR-020
(corpus admission two-tier gate; folder-drop re-routed — as restated by DR-025) · DR-023
(source-binding model, as refined by DR-025) · DR-024 (admission-minted identity; clause ii
superseded by DR-026) · DR-025 (the corpus reframe: sources are evidence; authority earned
at promotion) · DR-026 (promotion boundary; walks never mutate promoted records) · DR-027
(effective ontology ships in the bundle; base CVs in the platform repo) · DR-042 (compute
posture: pipeline on-prem, app on Vercel) · DR-043 (zero-egress = no client data at rest
in shared multi-tenant infra) · DR-044 (per-client agent posture behind a thin interface;
first client cloud-native) · DR-045 (data-plane per-client / control-plane central,
telemetry-only; prove→pin→promote) · DR-046 (private pipeline ingress: Cloudflare Tunnel
default, Static-IP escape hatch; ingress is control-plane-only) · DR-055 (producer manual
forced-run surface, POST /producer-run).