The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.
This document is for people changing XORCISE. It records the constraints the codebase depends on and the reasoning behind them — the things you would otherwise have to rediscover by breaking something.It is deliberately not a user guide. How to install XORCISE, run an agent against a mission, read a trace or interpret a score is documented for users across the rest of this site. Nothing here repeats that; everything here is a rule, an invariant, or the reason an obvious-looking simplification is wrong.Rules are stated in the imperative and are testable. Where a rule is machine-enforced, the enforcing file is named — if you disagree with a rule, change the enforcement and this document together, in the same change.
XORCISE is one pip distribution. There is no server tier, no second package, and no fork per deployment: xorcise up boots one long-lived local process that serves the REST API, the web UI and the OTLP receiver, and drives Docker over the local socket.
All internal code lives under xorcise.core.*. The bare xorcise.* namespace is reserved for future products, and scripts/check_single_distribution.py fails CI if a second distribution appears. Do not add top-level modules.
Imports must have no side effects. Importing any package must not read config, open a database, touch the network, or start a thread. Settings load only when get_settings() is called.
Bring Your Own Model. XORCISE bundles no LLM and no key. Every feature that needs a model — the judge, terrain attribution — uses the operator’s own OpenAI-compatible endpoint. Never add a bundled or default model, and never ship a credential.
Vendor-neutral about the agent under test. XORCISE never launches the agent and ships no agent templates. The agent is given a connect prompt and reaches the run over the network on its own terms.
The data model is agent-centric. The agent is the first-class record: it is registered before anything else can happen, and a run belongs to exactly one agent, one mission and one budget. Run creation resolves the named agent and refuses with a 409 if none is registered — there is no anonymous run. Results accumulate into that agent’s track record, which is what makes scores comparable across runs.XORCISE judges an agent by what it did — its recorded trace and what it submitted — never by inspecting its source. Do not add a code-analysis path to the grading flow.xorcise.core.eval and xorcise.core.code are swap seams for a future premium tier. core.code is an intentionally empty stub. Do not delete it and do not fill it in the open-source tree; only core.seams may import a premium implementation.
Everything may depend inward; nothing may depend outward. contracts is the innermost layer and imports nothing of ours.
Delivery surfaces sit above domain modules.rest and frontend may import a domain module; a domain module must never import a delivery surface.
Part-islands are mutually non-importing. The runner, headscale and otel parts must not import each other. They communicate through typed clients in orchestration/clients/. When a part needs a fact owned by another module, it receives it as an injected callback, not an import — the headscale part learns which runs are active this way, and stays pure.
The kernel splits: config sits below db and observability.config is shared-kernel and must not import an application-layer module — this is why it re-derives the home path locally instead of importing xorcise.core.home.
Only the runner part touches Docker. Ingestion drives image builds through a builder port that it stubs in tests.
The kernel owns SQLAlchemy 2.0 and Alembic; models are owned by the module they belong to.
Migrations are hand-written. Autogenerate stays off. The Alembic environment must never import an application-layer model, so there is no target_metadata to diff against. Write the migration by hand.
Never add a model-level ForeignKey across modules. The foreign key lives in the migration DDL only. SQLite does not enforce foreign keys by default, and a model-level FK across modules would break part-island independence.
Cascades are application-layer and orchestrated by the delivery surface. Deleting a run, or an agent with its runs, deletes everything recorded for those runs. rest/run_purge.py holds the one ordered list of run-scoped stores. Both deletes walk it, and so does the boot sweep that clears records whose run row is gone. Each owning module lists and deletes its own tables. tests/unit/test_run_purge.py seeds every table with a run_id column, so a run-keyed table added later fails it until its module deletes it too. The result goes only after the run row, because a run row without its result is graded again at boot from whatever evidence is left. Do not “simplify” this to ON DELETE CASCADE — it would not fire on SQLite and would reintroduce the cross-module FK. This is the canonical case of an obvious simplification being wrong.
Never migrate populated data on boot. A database is classified fresh (scaffold it to head), ready (boot), or stale (refuse loudly and defer to xorcise db upgrade). Bootstrap happens on up; migration is always an explicit operator action.
A run’s state is authoritative in the database, not in a process-local map.
Persist within the operation that changes the state. In-memory maps are caches and must always have a durable fallback. A fact that outlives a request, or crosses a process or crash boundary, is written through the runs domain layer at the moment it changes.
Address external resources by run-derived identity — the router node is {run_id}-router, the container is named for the run — so a reconciler that never saw the creating process can still find and reap them.
Make create, teardown, terminal transition and result-recording idempotent and replayable. Reconcile-on-startup re-runs them to convergence after a crash; anything that is not replayable will corrupt state instead of healing it.
Release a run’s environment before deleting its row. The run row is the release sweeper’s only handle on the run: deleting it while the container or tailnet nodes are still up leaves the container until the next boot and the nodes forever. rest/run_teardown.pycheck_deletable refuses a delete for a run that is unfinished, still grading, or unreleased, and releases a finished one synchronously first.
The server never calls its own REST API to persist its own state. Go through the domain layer.
Terminal is first-wins and grading is asynchronous. Sealing happens synchronously so the agent gets an immediate answer; grading runs on a background task. The safety comes from a pair of guards — result-recording is idempotent first-write-wins on run_id, and the grade-and-record path is a guarded no-op if it has already run. Do not remove either guard, and do not move grading back onto the request thread. The budget watchdog runs on its own thread.
Versions bump only on an explicit update. Registering a duplicate name is a 409, never a silent overwrite. A mission version is stamped only through the shared atomic-install helper; a new install path that bypasses it silently breaks cross-run comparability.
Each run gets its own fenced network. The guarantee is that the mission is unreachable from the host and from other runs, and the tailnet is the only path between an agent and its mission. It is still not a hardened egress firewall: confinement is Docker’s internal flag enforced by the same nested daemon the mission runs on.
Mission containers run on a Docker network nested inside a fused container, so no route exists from the host to a mission address.
Every network the mission declares is created internal — not only the carved entry networks — so a multi-homed service cannot keep a route off box through its second attachment. The run’s own Tailscale router sits on the single non-internal network and is the run’s only egress. environment.allow_egress in the manifest opts a mission out of all of it.
Refuse rather than deploy a run that cannot be confined. A mission declaring the reserved network name xorcise-egress, or an external: true network, is rejected at build time with the collision named. Both would have reported themselves confined while one network silently was not: the reserved name gets overwritten, and internal is a no-op on a network Compose did not create. A compose file that cannot be read at all is the same refusal — fail closed, never narrow confinement silently.
A private tailnet carries the agent in. The access policy is fail-closed: exactly one rule per run, no default-allow. Refuse to apply a policy that does not match that shape.
Each run’s router carries a per-run ACL tag, not just the shared base tag. The inbound rule that lets a target open connections back to the agent pins that tag as its only source, so it names exactly one router. With a shared tag every run’s router matched every run’s inbound rule, and a mission that pivoted through its router had a compiled path to a concurrent run’s agent on any port. The policy gate pairs each router tag with its single permitted agent and rejects any policy that does not.
Render subnet allocation and the access policy from persisted non-terminal runs, unioned with the in-process in-flight set, and reconcile the policy after the row persists. Deriving them from in-process state alone lets concurrent runs collide and lets one process clobber another’s policy.
Delete the run’s router node by name once the run is graded. A completed run that leaves an online node behind leaks into the next run’s allocation.
Agents reach targets by IP; there is no DNS inside the fence. The tailnet login server is never host.docker.internal.
The per-run join key is read through its narrow getter only and is never surfaced on the general run entity.
A mission is mission.json plus a pullable OCI image — never an archive format. The server-to-runner deploy verb carries a thin reference only; mission bytes never ride the wire between them.
The mission and its runner are fused into one self-contained image per mission, with the inner stack baked in and loaded on boot so a deploy needs no inner pull. The cost is that a change to runner control logic requires rebuilding every mission image. That is an accepted trade, not a bug.
The runner is DinD-only. A mission’s containers always run on the nested daemon inside the run’s own container, never as siblings on the operator’s daemon. The sibling topology is gone, not optional: it put every mission’s containers on one daemon, where parallel runs collided on fixed container_name values and published ports. nested_container_check=skip bypasses the precondition check for hosts where the probe cannot run; it cannot restore the sibling topology.
A fused image records the base generation it was built on, and this build refuses an artifact older than the generation it runs, in both the CLI and the console, with a named refusal rather than a mid-run failure. One verdict serves both the run-create gate and the catalog browse surface, so a card can never say runnable for something a run would refuse.
The two acquisition paths never cross. A library mission is pulled as a prebuilt fused image; a local mission is ingested and built locally. Both write their installed record through the same shared helper.
Attachment bytes never ride run-control. The agent asks for a companion file by name and receives a short-lived HMAC-signed URL it fetches separately. Signing secrets are generated per process unless explicitly pinned.
Raw OTLP is canonical and immutable. Everything else is derived.
The live view and the grader read natively off the persisted raw store. The OTLP receiver is ingress-only — it never becomes a routing, correlation or persistence layer.
The agent-event projection is a rebuildable cache, never a source of truth. It is versioned by adapter name and version so a changed adapter invalidates and re-derives it.
The grader must never read the agent-event projection. It consumes the sealed raw record only. This one is enforced by name in .importlinter.
Adapters must be total. An unrecognised span becomes a generic event, an adapter exception becomes exactly one error event, and neither ever produces a 5xx. Supporting a new agent must require no frontend change and no core change.
Adapter selection is authoritative from the run’s recorded agent, not from agent-emitted resource attributes, which are forgeable.
Admit a batch and seal a run under the run’s admission lock. Both start their transaction by updating the run’s row in otel_admissions, so whichever comes second waits for the first to commit. In that one transaction the receiver checks the seal, numbers the batch MAX(seq) + 1 and inserts it (TraceStore.append_next), so a batch either commits before the seal and is hashed into the evidence digest, or sees the seal and is refused. Do not split the seal check from the append, and do not number a batch by counting the run’s rows. Ingest runs on the receiver’s own worker threads and the seal on another thread, so nothing else orders them: a split check lets a batch land while the run is being hashed, and an untouched run then reads mismatch; a count lets two batches share a seq. A unique (run_id, seq) index on traces and logs backs this up. On SQLite the seal’s half of the lock looks like dead code, because the database’s single write lock already orders the two; on an engine that locks rows, only this lock keeps a batch out of the hash. tests/unit/test_otel_admission.py races real threads against a real database, and test_seal_takes_the_admission_lock in tests/adapters/test_otel_seal_store_sqlite.py pins the seal’s half.
A run is sealed on terminal; spans arriving afterwards are rejected. Replay never affects a score.
The evidence digest covers every evidence stream grading reads: raw traces, raw logs, run-control submissions and observed facts. A new evidence stream goes inside it, and any change to what it covers or how it is framed bumps the scheme tag, so an old digest reads as unverifiable rather than as tampered. A recorded digest is never overwritten, and a digest XORCISE cannot check is never reported as a mismatch.
Telemetry is optional. A run with no traces is valid: deterministic checks read artifacts and observed facts, and criteria the judge cannot evidence are left unscored: they count as zero, and the result carries an upper bound that grants their weight. Never make a trace a precondition for grading.
A score is half deterministic checks, half model judgement, over the sealed evidence only.
Deterministic checks assert a named operation over a value resolved from a named source. Fetching is decoupled from asserting so a new check op needs no new resolver.
Agent-controlled evidence rides the user role, fenced as untrusted and stripped of fence markers; instructions ride the system role. Never interpolate agent output into the system prompt.
The transcript budget is counted with an explicitly configured tokenizer. Tokenizers are not interchangeable, so the choice is recorded rather than inferred. Going over budget must fail loudly; never silently truncate evidence.
A degraded judge produces a recorded, re-gradeable failure — never a silent zero.
REST is the only agent control surface. There is no MCP plane and no reserved port for one. If an MCP surface is ever wanted, it is a fresh decision starting from the REST contract, not a resumed stub.
The agent’s verbs are /mission, /intel, /artifacts, /complete, plus the connect and join helpers. Authentication is a per-run bearer token.
There is one submission verb. The flag is not special: it is the artifact named flag, credited by a deterministic check. Never reintroduce a dedicated flag endpoint.
Post-terminal run-control is uniformly 409 — no per-verb special cases, no “late submission” path.
Note that the agent-event vocabulary retains MCP call and result kinds. Those describe the agent under test invoking its own MCP tools, which is a different thing that happens to share a name.
Local configuration, including the BYOM judge key, is settable through the REST API so the UI and CLI share one path.
The config API is a fixed, judge-shaped contract, never a generic settings passthrough — a generic one could brick a running server.
Secrets are write-only. A stored key reads back only as its last few characters.
All writers go through the shared upsert helper and then clear the settings cache. Writing the environment file by any other path leaves the running server serving stale configuration.
Security note. In the default local topology the REST and OTLP planes bind loopback plus, on native Linux, the Docker bridge gateway — the narrowest set that still lets an agent in a container reach them via host.docker.internal. They widen to 0.0.0.0 only on an explicit XORCISE_HOST=0.0.0.0, or when that gateway cannot be determined at boot. Neither plane authenticates the operator surface. A loopback bind is therefore not a security boundary here: every local process and every container on that bridge can read runs, create runs, and write configuration. Do not add secret-bearing endpoints on the assumption that the surface is loopback-only, and do not assume the wildcard case never happens — it is a live fallback path.
Remote and cloud-hosted mission planes are not implemented. XORCISE runs on one machine. No Terraform ships in this repo, and the remote and cloud auth commands are placeholders.
There is no streaming trace endpoint. The live view polls with an incremental cursor. This is a deliberate simplification.
Telemetry mirroring to an external service is a reserved, default-off seam. Enabling it fails fast rather than silently exporting: nothing leaves the operator’s machine by default.
Two short rule sets travel with the codebase. They are stated here so a contributor finds every load-bearing rule on one page.The contribution process lives in the engine’s CONTRIBUTING.md, not here: setup with uv sync --locked --extra dev, the test lanes and which of them run on a pull request (the Docker-heavy integration and e2e lanes run on any pull request that changes more than documentation), the title and release-note label the pr-contract check reads, and the rule that a label is declared in .github/labels.yml before any configuration uses it, which tests/topology/test_label_contract.py enforces.
The application layer never holds the Docker socket (only runner). Nothing leaves by default (egress is opt-in).
Secrets live in ~/.xorcise/.env (0600), never in the repo. BYOM keys are the user’s.
Was this page helpful?
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"XORCISE","description":"Evaluate cyber AI agents against realistic mission environments — locally on your machine.","colors":{"primary":"#8a6710","light":"#ffcf6a","dark":"#8a6710"},"logo":{"light":"https://mintcdn.com/xorciseai/73cVaorGzBMhO39b/images/logo-light.svg?fit=max\u0026auto=format\u0026n=73cVaorGzBMhO39b\u0026q=85\u0026s=d57ed1fcfcf482cef1a8deb150224de5","dark":"https://mintcdn.com/xorciseai/73cVaorGzBMhO39b/images/logo-dark.svg?fit=max\u0026auto=format\u0026n=73cVaorGzBMhO39b\u0026q=85\u0026s=e446f488ec6b995d7946fb40121049dd"},"favicon":"https://mintcdn.com/xorciseai/73cVaorGzBMhO39b/images/favicon.svg?fit=max\u0026auto=format\u0026n=73cVaorGzBMhO39b\u0026q=85\u0026s=07aa7960f49c1540f37f9ebfdd757c8b","appearance":{"default":"dark","strict":true},"background":{"color":{"light":"#faf8f2","dark":"#0a0805"}},"navbar":{"links":[{"href":"https://github.com/xorcise-ai/xorcise/releases","label":"Changelog"}],"primary":{"type":"github","href":"https://github.com/xorcise-ai/xorcise"}},"footer":{"socials":{"github":"https://github.com/xorcise-ai/xorcise"}},"seo":{"metatags":{"og:site_name":"XORCISE Documentation","og:type":"website","og:image":"https://docs.xorcise.ai/images/og.png","og:image:width":"1200","og:image:height":"630","og:image:alt":"XORCISE.AI documentation — evaluate cyber AI agents against realistic mission environments, locally on your own machine","og:locale":"en_US","twitter:card":"summary_large_image","twitter:image":"https://docs.xorcise.ai/images/og.png"},"indexing":"navigable"},"fonts":{"heading":{"family":"JetBrains Mono"},"body":{"family":"Inter"}},"icons":{"library":"lucide"},"styling":{"eyebrows":"section","codeblocks":{"theme":"css-variables"},"latex":false},"contextual":{"options":["copy","view","claude","chatgpt","perplexity"],"display":"header"},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"xorciseai","hasAudioNarration":false,"actualSubdomain":"xorciseai","customDomains":["docs.xorcise.ai"],"gitSource":{"type":"github","owner":"xorcise-ai","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":true,"mountPath":"","label":""},"chroma":{"liveDeploymentHistoryId":"6ae2d030cffdd78d47a2ba35","collectionId":"b98492c0-ef81-41ad-8f94-dfcc5adc9904","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6ae2d030cffdd78d47a2ba35"},"feedback":{"thumbs":true,"edits":false},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6ae2d030cffdd78d47a2ba35","clientVersion":"0.0.4045","searchFilterCounts":[{"language":"default","tag":"Guides","count":22},{"language":"default","tag":"Reference","count":8},{"language":"default","tag":"Core concepts","count":6},{"language":"default","tag":"Start here","count":3},{"language":"default","tag":"Operate","count":3},{"language":"default","tag":"Contributing","count":2},{"language":"default","tag":"Authoring","count":2},{"language":"default","tag":"Experimental","count":1},{"language":"default","tag":"API","count":1},{"language":"default","tag":"Command line","count":1}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":true},"customCss":[{"filePath":"custom.css","content":":root{--xor-amber:#e8b84b;--xor-amber-bright:#ffcf6a;--xor-amber-hot:#ffe6a6;--xor-amber-dim:#9a8a5e;--xor-amber-faint:#6e6144;--xor-head:#f2ead6;--xor-body:#c7bb9f;--xor-phosphor:#0a0805;--xor-ui-bg:#111111;--xor-ui-chrome:#141414;--xor-ui-card:#1a1a1a;--xor-ui-raised:#232323;--xor-ui-border:rgba(255, 255, 255, 0.09);--xor-ui-glow:rgba(232, 184, 75, 0.06);--xor-ui-body:#d4d4d4;--xor-ui-secondary:#999999;--xor-data:#6ee7a8;--xor-warning:#f0d890;--xor-failure:#ff5f57;--xor-info:#38bdf8;--xor-mono:\"JetBrains Mono\", ui-monospace, \"SF Mono\", Consolas, monospace;--xor-sans:\"Inter\", system-ui, -apple-system, \"Segoe UI\", sans-serif;--xor-radius:6px;--xor-radius-panel:8px;--xor-motion:200ms;--xor-ease:cubic-bezier(0.4, 0, 0.2, 1);--t-display:2.5rem;--t-section:1.75rem;--t-sub:1.25rem;--t-label:1rem;--t-lede:1.1875rem;--t-body:1.0625rem;--t-dense:0.9375rem;--t-chrome:0.9375rem;--t-code:0.9375rem;--t-meta:0.8125rem;--t-micro:0.75rem;--lh-display:1.1;--lh-heading:1.25;--lh-body:1.75;--lh-dense:1.6;--lh-code:1.7}:root,html.dark{--gray-50:250 248 242;--gray-100:245 240 226;--gray-200:242 234 214;--gray-300:216 205 178;--gray-400:199 187 159;--gray-500:154 138 94;--gray-600:122 122 122;--gray-700:58 58 58;--gray-800:35 35 35;--gray-900:26 26 26;--gray-950:10 8 5;--default-border-color:var(--xor-ui-border);--scrollbar-thumb:#6e6144;--scrollbar-thumb-hover:#9a8a5e;--scrollbar-thumb-active:#9a8a5e}body{font-family:var(--xor-sans);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html.dark #sidebar,html.dark #sidebar>div{background-color:var(--xor-phosphor);border-right-color:var(--xor-ui-border)}html.dark #header,html.dark #navbar,html.dark #navbar>div{background-color:color-mix(in srgb,var(--xor-phosphor) 88%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-bottom-color:var(--xor-ui-border)}::selection{background:rgba(232,184,75,.12);color:var(--xor-head)}:focus-visible{outline:2px solid var(--xor-amber);outline-offset:2px;border-radius:var(--xor-radius)}.lucide,svg[class*=lucide]{stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}.eyebrow{font-family:var(--xor-mono);font-size:var(--t-meta);font-weight:500;letter-spacing:.18em;text-transform:uppercase;color:var(--xor-amber-dim);height:auto;margin-bottom:1rem}.eyebrow::before{content:\"// \";color:var(--xor-amber-faint)}#page-title,h1#page-title{font-family:var(--xor-sans);font-size:clamp(2rem,3.6vw,var(--t-display));line-height:var(--lh-display);letter-spacing:-.02em}#content-area .mdx-content,#content-area .prose{--tw-prose-body:var(--xor-body);--tw-prose-invert-body:var(--xor-body);--tw-prose-headings:var(--xor-head);--tw-prose-invert-headings:var(--xor-head);--tw-prose-bold:var(--xor-head);--tw-prose-invert-bold:var(--xor-head);--tw-prose-links:var(--xor-amber);--tw-prose-invert-links:var(--xor-amber);--tw-prose-bullets:var(--xor-amber-faint);--tw-prose-invert-bullets:var(--xor-amber-faint);--tw-prose-counters:var(--xor-amber-dim);--tw-prose-invert-counters:var(--xor-amber-dim);--tw-prose-hr:var(--xor-ui-border);--tw-prose-invert-hr:var(--xor-ui-border);--tw-prose-quotes:var(--xor-head);--tw-prose-invert-quotes:var(--xor-head);--tw-prose-quote-borders:var(--xor-amber);--tw-prose-invert-quote-borders:var(--xor-amber);--tw-prose-captions:var(--xor-amber-dim);--tw-prose-invert-captions:var(--xor-amber-dim);--tw-prose-th-borders:var(--xor-ui-border);--tw-prose-invert-th-borders:var(--xor-ui-border);--tw-prose-td-borders:var(--xor-ui-border);--tw-prose-invert-td-borders:var(--xor-ui-border);font-size:.95rem;line-height:1.6}#content-area .prose.text-lg>p,#content-area div[class*=text-lg].prose>p{font-size:var(--t-lede);line-height:1.65;color:var(--xor-body)}#content-area li,#content-area p{font-family:var(--xor-sans);font-size:var(--t-body);line-height:var(--lh-body);color:var(--xor-body)}#content-area strong,#page-title,h1#page-title{color:var(--xor-head);font-weight:600}#content-area h2:not(.not-prose){font-family:var(--xor-sans);font-weight:600;font-size:clamp(1.5rem,2.4vw,var(--t-section));line-height:var(--lh-heading);letter-spacing:-.015em;color:var(--xor-head);border-top:1px solid var(--xor-ui-border)}#content-area .mdx-content>h2:first-child{margin-top:.5rem;padding-top:0;border-top:none}#content-area h3:not(.not-prose){font-family:var(--xor-mono);font-weight:600;font-size:var(--t-sub);line-height:var(--lh-heading);letter-spacing:-.005em;text-transform:none;color:var(--xor-head);margin:2.5rem 0 .85rem}#content-area h4:not(.not-prose){font-family:var(--xor-mono);font-weight:600;font-size:var(--t-label);letter-spacing:.06em;text-transform:uppercase;color:var(--xor-amber-dim);margin:2rem 0 .7rem}#content-area a.link,#content-area li a,#content-area p a{text-decoration:underline}#content-area a.link,#content-area li a,#content-area p a,#content-area td a{color:var(--xor-amber);font-weight:400;text-decoration-color:color-mix(in srgb,var(--xor-amber) 40%,transparent);text-decoration-thickness:1px;text-underline-offset:2px;transition:color var(--xor-motion) var(--xor-ease)}#content-area a.link:hover,#content-area li a:hover,#content-area p a:hover{color:var(--xor-head);text-decoration-color:currentColor}#content-area td a:hover{color:var(--xor-head)}#content-area :not(pre)>code{font-family:var(--xor-mono);font-size:.9em;font-weight:400;color:var(--xor-head);background:0 0;border:0;padding:0}#content-area :not(pre)>code::after,#content-area :not(pre)>code::before{content:none}#content-area a>code{color:inherit}#content-area h1 code,#content-area h2 code,#content-area h3 code,#content-area h4 code{color:inherit;font-size:.92em;font-weight:inherit}#content-area blockquote{border-left:3px solid var(--xor-amber);padding-left:1.75rem;margin:1.75rem 0;font-family:var(--xor-sans);font-weight:400;font-size:var(--t-lede);line-height:1.6;color:var(--xor-head);font-style:normal;background:0 0;quotes:none}#content-area blockquote p{font-size:inherit;line-height:inherit;color:inherit;max-width:46rem}#content-area blockquote p::after,#content-area blockquote p::before{content:none}#content-area hr{border:0;border-top:1px solid var(--xor-ui-border);margin:3rem 0}#content-area table{border-collapse:collapse;width:100%;margin:1.25rem 0 2rem;font-size:var(--t-dense);border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius);overflow:hidden}#content-area .table-wrapper,#content-area div:has(>table){overflow-x:auto;max-width:100%}#content-area td:first-child a,#content-area td:first-child code{white-space:nowrap}#content-area td a,#content-area td a.link,#content-area th a,#content-area th a.link{text-decoration:none}#content-area td a.link:hover,#content-area td a:hover{text-decoration:underline;text-decoration-color:currentColor}#content-area thead{background:var(--xor-ui-chrome)}#content-area td,#content-area th{border-bottom:1px solid var(--xor-ui-border)}#content-area th{font-family:var(--xor-mono);font-weight:600;font-size:var(--t-micro);letter-spacing:.08em;text-transform:uppercase;color:var(--xor-ui-secondary);text-align:left;padding:.8rem 1rem;white-space:nowrap}#content-area td{font-family:var(--xor-sans);font-size:var(--t-dense);line-height:var(--lh-dense);color:var(--xor-body);padding:.85rem 1rem;vertical-align:baseline;font-variant-numeric:tabular-nums}#content-area tbody tr:last-child td{border-bottom:none}#content-area tbody tr{transition:background var(--xor-motion) var(--xor-ease)}#content-area tbody tr:hover{background:rgba(255,255,255,.02)}.nav-tabs-item{font-weight:500;letter-spacing:.02em}#sidebar .sidebar-group-header,#sidebar .sidebar-title,#sidebar .sidebar-title span{font-family:var(--xor-mono);font-size:var(--t-micro);font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--xor-ui-secondary)}#sidebar .sidebar-group-header{margin-bottom:.75rem}#search-bar-entry,#sidebar a,.nav-tabs-item{font-family:var(--xor-mono);font-size:var(--t-chrome)}#sidebar a{line-height:1.55;letter-spacing:.01em;transition:color var(--xor-motion) var(--xor-ease),border-color var(--xor-motion) var(--xor-ease)}#sidebar a[class*=border-l],#table-of-contents a{border-left-color:var(--xor-ui-border)}#sidebar a[class*=border-l]:hover,#table-of-contents a:hover{border-left-color:color-mix(in srgb,var(--xor-amber) 45%,transparent)}#search-bar-entry{letter-spacing:.02em;border-radius:var(--xor-radius);border-color:var(--xor-ui-border);background:var(--xor-ui-card)}#page-context-menu,#page-context-menu button,#page-context-menu-button{font-family:var(--xor-mono);font-size:var(--t-meta);font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--xor-body);border-color:var(--xor-ui-border);background:0 0;transition:color var(--xor-motion) var(--xor-ease),border-color var(--xor-motion) var(--xor-ease)}#content-area .code-block button:hover,#page-context-menu button:hover,#page-context-menu-button:hover{color:var(--xor-amber);border-color:color-mix(in srgb,var(--xor-amber) 45%,transparent)}#table-of-contents{font-family:var(--xor-mono);font-size:var(--t-meta);letter-spacing:.02em}#table-of-contents a{line-height:1.5}#footer{border-top:1px solid var(--xor-ui-border)}#footer a,#navbar a{font-family:var(--xor-mono);font-size:var(--t-chrome);letter-spacing:.04em}.breadcrumb-item,.breadcrumb-list{font-family:var(--xor-mono);font-size:.72rem;letter-spacing:.08em;color:var(--xor-amber-dim)}.breadcrumb-item:last-child{color:var(--xor-head)}#content-area .callout{--callout:var(--xor-amber);background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);border-left:3px solid var(--callout);border-radius:0 var(--xor-radius-panel) var(--xor-radius-panel) 0;padding:1rem 1.2rem;margin:1.5rem 0;gap:.75rem}#content-area .callout[class*=blue-]{--callout:var(--xor-info)}#content-area .callout[class*=green-]{--callout:var(--xor-data)}#content-area .callout[class*=yellow-]{--callout:var(--xor-warning)}#content-area .callout[class*=red-]{--callout:var(--xor-failure)}#content-area .callout [data-component-part=callout-icon],#content-area .callout [data-component-part=callout-icon] svg,#content-area .callout>svg{color:var(--callout)!important;flex:none}#content-area .callout p,#content-area .callout>div:last-child,#content-area .callout>div:last-child>:not(code):not(pre){font-size:var(--t-dense);line-height:var(--lh-dense);color:var(--xor-ui-body);max-width:none}#content-area .callout p:last-child,#content-area .callout>div:last-child>:last-child,#content-area .mdx-content p:last-child{margin-bottom:0}#content-area .callout strong{color:var(--xor-head)}#content-area .callout code{color:var(--xor-head)!important;background:0 0!important}#content-area .callout [class*=bg-grid]{display:none}#content-area .card,#content-area .not-prose a[href]>div[class*=border],#content-area .not-prose>a[href]>div{background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius);box-shadow:none;transition:border-color var(--xor-motion) var(--xor-ease)}#content-area .card:hover,#content-area .not-prose a[href]:hover>div[class*=border],#content-area .not-prose>a[href]:hover>div{border-color:color-mix(in srgb,var(--xor-amber) 45%,transparent);background:var(--xor-ui-card)}#content-area .card h2,#content-area .card h3,#content-area .card-title,#content-area a[href] h2.not-prose,#content-area a[href] h3.not-prose{font-family:var(--xor-mono);font-size:var(--t-dense);font-weight:600;line-height:1.4;letter-spacing:.02em;text-transform:none;color:var(--xor-amber);border-top:none;border-bottom:none;padding-top:0;padding-bottom:0;margin:0 0 .5rem}#content-area .card p{font-size:var(--t-dense);line-height:var(--lh-dense);color:var(--xor-body);max-width:none}#content-area div[class*=w-6][class*=h-6][class*=rounded-md]{background:var(--xor-ui-raised)!important;border:1px solid var(--xor-ui-border);color:var(--xor-amber)!important;filter:none!important;box-shadow:none!important;--tw-ring-color:transparent}:root{--mint-foreground:#d4d4d4;--mint-token-function:#dcdcaa;--mint-token-string:#d7ba7d;--mint-token-string-expression:#ce9178;--mint-token-constant:#b5cea8;--mint-token-keyword:#9cdcfe;--mint-token-parameter:#9cdcfe;--mint-token-punctuation:#999999;--mint-token-comment:#9a8a5e;--mint-token-link:#6ee7a8}#content-area .code-block,#content-area .code-block-background,#content-area [class*=bg-codeblock]{background-color:var(--xor-ui-card)!important;border-radius:var(--xor-radius-panel)}#content-area .code-block{border:1px solid var(--xor-ui-border);--tw-ring-color:transparent;margin:1.25rem 0 1.5rem;overflow:hidden}#content-area .code-block .font-mono,#content-area pre.shiki,#content-area pre.shiki code{font-family:var(--xor-mono);font-size:var(--t-code);line-height:var(--lh-code);color:var(--mint-foreground)}#content-area .code-block [class*=rounded-t]{font-family:var(--xor-mono);font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--xor-ui-secondary);background:var(--xor-ui-chrome);border-bottom:1px solid var(--xor-ui-border)}#content-area .code-block button{color:var(--xor-ui-secondary);background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius);position:relative;opacity:0;transition:opacity var(--xor-motion) var(--xor-ease),color var(--xor-motion) var(--xor-ease)}#content-area .code-block [class*=rounded-t] button{background:0 0;border:0}#content-area .step [data-component-part=step-title],#content-area .steps .step h3{font-family:var(--xor-mono);font-size:var(--t-label);font-weight:600;letter-spacing:0;text-transform:none;color:var(--xor-head);margin:0}#content-area .step [class*=rounded-full],#content-area .step [class*=rounded-lg]:first-child{font-family:var(--xor-mono);font-weight:700;font-size:var(--t-meta);background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);color:var(--xor-amber)}#content-area .steps [class*=bg-gradient-to-b]{background:var(--xor-ui-border)!important}#content-area .accordion,#content-area .accordion-group{background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius);transition:border-color var(--xor-motion) var(--xor-ease)}#content-area .accordion:hover{border-color:color-mix(in srgb,var(--xor-amber) 45%,transparent)}#content-area .accordion summary,#content-area .accordion summary p{font-family:var(--xor-mono);font-size:var(--t-dense);font-weight:500;line-height:1.5;color:var(--xor-head);max-width:none}#content-area .accordion-group .accordion{border-radius:0;border-left:none;border-right:none}#content-area [role=tablist]{border-bottom:1px solid var(--xor-ui-border)}#content-area [role=tab]{font-family:var(--xor-mono);font-size:var(--t-dense);letter-spacing:.04em}#content-area [class*=frame]>div:first-child,#content-area figure>div{border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius-panel);overflow:hidden}#content-area figcaption,#content-area figure p{font-family:var(--xor-mono);font-size:var(--t-meta);line-height:1.55;letter-spacing:.02em;color:var(--xor-ui-secondary);margin-top:.75rem;max-width:none}.mermaid{overscroll-behavior-x:contain}.mermaid,div[data-language=mermaid],pre.mermaid{overflow-x:auto;max-width:100%;background:var(--xor-ui-card);border:1px solid var(--xor-ui-border);border-radius:var(--xor-radius-panel);padding:1.5rem;margin:1.25rem 0 1.5rem}.mermaid svg{margin:0 auto;overflow:visible}.mermaid .edgeLabel,.mermaid .edgeLabel *,.mermaid .nodeLabel,.mermaid .nodeLabel *,.mermaid foreignObject div,.mermaid foreignObject p,.mermaid foreignObject span{font-family:var(--xor-mono)!important;font-size:12px!important;line-height:1.35!important;letter-spacing:normal!important;margin:0!important;max-width:none!important}.mermaid .label,.mermaid .nodeLabel,.mermaid foreignObject{overflow:visible!important}@media (prefers-reduced-motion:reduce){*,::after,::before{animation-duration:.001ms!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}.mermaid svg,div[data-language=mermaid] svg{width:100%;max-width:100%;height:auto}#content-area div:has(>.mermaid)>.absolute.z-10.grid{opacity:0;transition:opacity .12s ease-out}#content-area div:has(>.mermaid):focus-within>.absolute.z-10.grid,#content-area div:has(>.mermaid):hover>.absolute.z-10.grid{opacity:1}@media (prefers-reduced-motion:reduce){#content-area div:has(>.mermaid)>.absolute.z-10.grid{transition:none}}#content-area .code-block [class*=overflow-x-auto],#content-area .code-block-background{padding-right:3rem}#content-area .code-block button::after{content:\"\";position:absolute;inset:50% auto auto 50%;width:44px;height:44px;transform:translate(-50%,-50%)}@media (max-width:767px){#content-area table [class*=min-w-],#content-area td,#content-area th{min-width:0!important}#content-area td,#content-area th{padding:.7rem .75rem}}#sidebar a:focus-visible,#table-of-contents a:focus-visible{outline-offset:-2px}#table-of-contents a:hover{background:rgba(242,234,214,.05);color:var(--xor-head)}#content-area [role=tab]:hover,#xor-skip-link{color:var(--xor-head)}@media (max-width:1023px){#header button,#navbar button,#page-context-menu button{min-width:44px;min-height:44px}}#xor-skip-link{position:fixed;top:0;left:0;z-index:200;margin:.5rem;padding:.65rem 1rem;font-family:var(--xor-mono);font-size:var(--t-chrome);background:var(--xor-ui-card);border:1px solid var(--xor-amber);border-radius:var(--xor-radius);transform:translateY(-160%);transition:transform var(--xor-motion) var(--xor-ease)}#xor-skip-link:focus{transform:translateY(0)}.sr-only:focus-within{position:static!important;width:auto!important;height:auto!important;clip:auto!important;clip-path:none!important;margin:.5rem!important;padding:.4rem .7rem!important;background:var(--xor-ui-card);border:1px solid var(--xor-amber);border-radius:var(--xor-radius)}#content-area .code-block button:focus-visible,#content-area .code-block:focus-within button,#content-area .code-block:hover button{opacity:1}@media (hover:none){#content-area .code-block button{opacity:1}}:root{--t-body:1.125rem;--t-lede:1.25rem;--t-dense:1rem;--t-chrome:1rem;--t-code:1rem;--t-meta:0.875rem;--t-micro:0.8125rem;--t-sub:1.375rem;--t-section:1.875rem}#content-area .mdx-content p{margin-top:0;margin-bottom:1.35em}#content-area .mdx-content ol,#content-area .mdx-content ul{margin-top:1.1em;margin-bottom:1.6em}#content-area .mdx-content li{margin-top:.55em;margin-bottom:.55em;padding-left:.15em}#content-area .mdx-content li>p{margin-bottom:.5em}#content-area h2:not(.not-prose){margin-top:clamp(2.75rem,5vw,3.75rem);padding-top:clamp(2.75rem,5vw,3.75rem);margin-bottom:1.25rem}#content-area h3:not(.not-prose){margin-top:2.75rem;margin-bottom:1rem}#content-area .callout,#content-area .code-block,#content-area .mermaid,#content-area figure,#content-area table{margin-top:1.75rem;margin-bottom:2rem}#content-area td,#content-area th{padding-top:.95rem;padding-bottom:.95rem}#sidebar a{padding-top:.42rem;padding-bottom:.42rem}#table-of-contents a{padding-top:.45rem;padding-bottom:.45rem}#content-area .callout>div:last-child,#content-area blockquote,#content-area li,#content-area p,#content-area td{font-weight:450}#content-area li,#content-area p{letter-spacing:.005em}#footer a,#navbar a,#sidebar a,#table-of-contents a{font-weight:500}"}],"page":{"pagePath":"contribute/architecture","slug":"/contribute/architecture","pageMetadata":{"title":"Architecture","description":"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.","icon":"layers","href":"/contribute/architecture"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,em:`em`,li:`li`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsxs(_components.p,{children:[`This document is for people `,_jsx(_components.strong,{children:`changing`}),` XORCISE. It records the constraints the codebase depends on and the reasoning behind them — the things you would otherwise have to rediscover by breaking something.`]}),`\n`,_jsxs(_components.p,{children:[`It is deliberately not a user guide. How to install XORCISE, run an agent against a mission, read a trace or interpret a score is documented for `,_jsx(_components.strong,{children:`users`}),` across the rest of this site. Nothing here repeats that; everything here is a rule, an invariant, or the reason an obvious-looking simplification is wrong.`]}),`\n`,_jsx(_components.p,{children:`Rules are stated in the imperative and are testable. Where a rule is machine-enforced, the enforcing file is named — if you disagree with a rule, change the enforcement and this document together, in the same change.`}),`\n`,_jsx(Heading,{level:`2`,id:`1-shape-of-the-product`,children:`1. Shape of the product`}),`\n`,_jsxs(_components.p,{children:[`XORCISE is `,_jsx(_components.strong,{children:`one pip distribution`}),`. There is no server tier, no second package, and no fork per deployment: `,_jsx(_components.code,{children:`xorcise up`}),` boots one long-lived local process that serves the REST API, the web UI and the OTLP receiver, and drives Docker over the local socket.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`All internal code lives under `,_jsx(_components.code,{children:`xorcise.core.*`}),`.`]}),` The bare `,_jsx(_components.code,{children:`xorcise.*`}),` namespace is reserved for future products, and `,_jsx(_components.code,{children:`scripts/check_single_distribution.py`}),` fails CI if a second distribution appears. Do not add top-level modules.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Imports must have no side effects.`}),` Importing any package must not read config, open a database, touch the network, or start a thread. Settings load only when `,_jsx(_components.code,{children:`get_settings()`}),` is called.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Bring Your Own Model.`}),` XORCISE bundles no LLM and no key. Every feature that needs a model — the judge, terrain attribution — uses the operator’s own OpenAI-compatible endpoint. Never add a bundled or default model, and never ship a credential.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Vendor-neutral about the agent under test.`}),` XORCISE never launches the agent and ships no agent templates. The agent is given a connect prompt and reaches the run over the network on its own terms.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The data model is agent-centric.`}),` The agent is the first-class record: it is registered before anything else can happen, and a run belongs to exactly one agent, one mission and one budget. Run creation resolves the named agent and refuses with a 409 if none is registered — there is no anonymous run. Results accumulate into that agent’s track record, which is what makes scores comparable across runs.`]}),`\n`,_jsxs(_components.p,{children:[`XORCISE judges an agent by `,_jsx(_components.strong,{children:`what it did`}),` — its recorded trace and what it submitted — never by inspecting its source. Do not add a code-analysis path to the grading flow.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`xorcise.core.eval`}),` and `,_jsx(_components.code,{children:`xorcise.core.code`}),` are `,_jsx(_components.strong,{children:`swap seams`}),` for a future premium tier. `,_jsx(_components.code,{children:`core.code`}),` is an intentionally empty stub. Do not delete it and do not fill it in the open-source tree; only `,_jsx(_components.code,{children:`core.seams`}),` may import a premium implementation.`]}),`\n`,_jsx(Heading,{level:`2`,id:`2-layering-and-imports`,children:`2. Layering and imports`}),`\n`,_jsxs(_components.p,{children:[`The dependency rule is one-directional and machine-enforced by `,_jsx(_components.code,{children:`.importlinter`}),` (`,_jsx(_components.code,{children:`uv run lint-imports`}),`, wired into CI and pre-commit):`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`2`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes css-variables css-variables`,style:{backgroundColor:`var(--mint-background)`,\"--shiki-dark-bg\":`var(--mint-background)`,color:`var(--mint-foreground)`,\"--shiki-dark\":`var(--mint-foreground)`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`2`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`cli > roles > application > parts > kernel > contracts`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsxs(_components.p,{children:[`Everything may depend inward; nothing may depend outward. `,_jsx(_components.code,{children:`contracts`}),` is the innermost layer and imports nothing of ours.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Delivery surfaces sit above domain modules.`}),` `,_jsx(_components.code,{children:`rest`}),` and `,_jsx(_components.code,{children:`frontend`}),` may import a domain module; a domain module must never import a delivery surface.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Part-islands are mutually non-importing.`}),` The runner, headscale and otel parts must not import each other. They communicate through typed clients in `,_jsx(_components.code,{children:`orchestration/clients/`}),`. When a part needs a fact owned by another module, it receives it as an `,_jsx(_components.strong,{children:`injected callback`}),`, not an import — the headscale part learns which runs are active this way, and stays pure.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`The kernel splits: `,_jsx(_components.code,{children:`config`}),` sits below `,_jsx(_components.code,{children:`db`}),` and `,_jsx(_components.code,{children:`observability`}),`.`]}),` `,_jsx(_components.code,{children:`config`}),` is shared-kernel and must not import an application-layer module — this is why it re-derives the home path locally instead of importing `,_jsx(_components.code,{children:`xorcise.core.home`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Only the runner part touches Docker.`}),` Ingestion drives image builds through a builder port that it stubs in tests.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`3-persistence`,children:`3. Persistence`}),`\n`,_jsx(_components.p,{children:`The kernel owns SQLAlchemy 2.0 and Alembic; models are owned by the module they belong to.`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Migrations are hand-written. Autogenerate stays off.`}),` The Alembic environment must never import an application-layer model, so there is no `,_jsx(_components.code,{children:`target_metadata`}),` to diff against. Write the migration by hand.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`Never add a model-level `,_jsx(_components.code,{children:`ForeignKey`}),` across modules.`]}),` The foreign key lives in the migration DDL only. SQLite does not enforce foreign keys by default, and a model-level FK across modules would break part-island independence.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Cascades are application-layer and orchestrated by the delivery surface.`}),` Deleting a run, or an agent with its runs, deletes everything recorded for those runs. `,_jsx(_components.code,{children:`rest/run_purge.py`}),` holds the one ordered list of run-scoped stores. Both deletes walk it, and so does the boot sweep that clears records whose run row is gone. Each owning module lists and deletes its own tables. `,_jsx(_components.code,{children:`tests/unit/test_run_purge.py`}),` seeds every table with a `,_jsx(_components.code,{children:`run_id`}),` column, so a run-keyed table added later fails it until its module deletes it too. The result goes only after the run row, because a run row without its result is graded again at boot from whatever evidence is left. Do not “simplify” this to `,_jsx(_components.code,{children:`ON DELETE CASCADE`}),` — it would not fire on SQLite and would reintroduce the cross-module FK. This is the canonical case of an obvious simplification being wrong.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Never migrate populated data on boot.`}),` A database is classified `,_jsx(_components.code,{children:`fresh`}),` (scaffold it to head), `,_jsx(_components.code,{children:`ready`}),` (boot), or `,_jsx(_components.code,{children:`stale`}),` (refuse loudly and defer to `,_jsx(_components.code,{children:`xorcise db upgrade`}),`). Bootstrap happens on `,_jsx(_components.code,{children:`up`}),`; migration is always an explicit operator action.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`4-run-lifecycle-and-durable-state`,children:`4. Run lifecycle and durable state`}),`\n`,_jsxs(_components.p,{children:[`A run’s state is authoritative `,_jsx(_components.strong,{children:`in the database`}),`, not in a process-local map.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Persist within the operation that changes the state.`}),` In-memory maps are caches and must always have a durable fallback. A fact that outlives a request, or crosses a process or crash boundary, is written through the `,_jsx(_components.code,{children:`runs`}),` domain layer at the moment it changes.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Address external resources by run-derived identity`}),` — the router node is `,_jsx(_components.code,{children:`{run_id}-router`}),`, the container is named for the run — so a reconciler that never saw the creating process can still find and reap them.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Make create, teardown, terminal transition and result-recording idempotent and replayable.`}),` Reconcile-on-startup re-runs them to convergence after a crash; anything that is not replayable will corrupt state instead of healing it.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Release a run’s environment before deleting its row.`}),` The run row is the release sweeper’s only handle on the run: deleting it while the container or tailnet nodes are still up leaves the container until the next boot and the nodes forever. `,_jsx(_components.code,{children:`rest/run_teardown.py`}),` `,_jsx(_components.code,{children:`check_deletable`}),` refuses a delete for a run that is unfinished, still grading, or unreleased, and releases a finished one synchronously first.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The server never calls its own REST API to persist its own state.`}),` Go through the domain layer.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Terminal is first-wins and grading is asynchronous.`}),` Sealing happens synchronously so the agent gets an immediate answer; grading runs on a background task. The safety comes from a pair of guards — result-recording is idempotent first-write-wins on `,_jsx(_components.code,{children:`run_id`}),`, and the grade-and-record path is a guarded no-op if it has already run. Do not remove either guard, and do not move grading back onto the request thread. The budget watchdog runs on its own thread.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Versions bump only on an explicit update.`}),` Registering a duplicate name is a 409, never a silent overwrite. A mission version is stamped only through the shared atomic-install helper; a new install path that bypasses it silently breaks cross-run comparability.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`5-network-isolation`,children:`5. Network isolation`}),`\n`,_jsxs(_components.p,{children:[`Each run gets its own fenced network. The guarantee is that `,_jsx(_components.strong,{children:`the mission is unreachable from the host and from other runs, and the tailnet is the only path between an agent and its mission`}),`. It is still not a hardened egress firewall: confinement is Docker’s `,_jsx(_components.code,{children:`internal`}),` flag enforced by the same nested daemon the mission runs on.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Mission containers run on a Docker network nested inside a fused container, so no route exists from the host to a mission address.`}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`Every network the mission declares is created `,_jsx(_components.code,{children:`internal`})]}),` — not only the carved entry networks — so a multi-homed service cannot keep a route off box through its second attachment. The run’s own Tailscale router sits on the single non-internal network and is the run’s only egress. `,_jsx(_components.code,{children:`environment.allow_egress`}),` in the manifest opts a mission out of all of it.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Refuse rather than deploy a run that cannot be confined.`}),` A mission declaring the reserved network name `,_jsx(_components.code,{children:`xorcise-egress`}),`, or an `,_jsx(_components.code,{children:`external: true`}),` network, is rejected at build time with the collision named. Both would have reported themselves confined while one network silently was not: the reserved name gets overwritten, and `,_jsx(_components.code,{children:`internal`}),` is a no-op on a network Compose did not create. A compose file that cannot be read at all is the same refusal — fail closed, never narrow confinement silently.`]}),`\n`,_jsxs(_components.li,{children:[`A private tailnet carries the agent in. The access policy is `,_jsx(_components.strong,{children:`fail-closed`}),`: exactly one rule per run, no default-allow. Refuse to apply a policy that does not match that shape.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Each run’s router carries a per-run ACL tag`}),`, not just the shared base tag. The inbound rule that lets a target open connections back to the agent pins that tag as its only source, so it names exactly one router. With a shared tag every run’s router matched every run’s inbound rule, and a mission that pivoted through its router had a compiled path to a concurrent run’s agent on any port. The policy gate pairs each router tag with its single permitted agent and rejects any policy that does not.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Render subnet allocation and the access policy from persisted non-terminal runs`}),`, unioned with the in-process in-flight set, and reconcile the policy `,_jsx(_components.em,{children:`after`}),` the row persists. Deriving them from in-process state alone lets concurrent runs collide and lets one process clobber another’s policy.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Delete the run’s router node by name once the run is graded.`}),` A completed run that leaves an online node behind leaks into the next run’s allocation.`]}),`\n`,_jsxs(_components.li,{children:[`Agents reach targets `,_jsx(_components.strong,{children:`by IP`}),`; there is no DNS inside the fence. The tailnet login server is never `,_jsx(_components.code,{children:`host.docker.internal`}),`.`]}),`\n`,_jsx(_components.li,{children:`The per-run join key is read through its narrow getter only and is never surfaced on the general run entity.`}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`6-missions`,children:`6. Missions`}),`\n`,_jsxs(_components.p,{children:[`A mission is `,_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`mission.json`}),` plus a pullable OCI image`]}),` — never an archive format. The server-to-runner deploy verb carries a thin reference only; mission bytes never ride the wire between them.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`The mission and its runner are `,_jsx(_components.strong,{children:`fused into one self-contained image per mission`}),`, with the inner stack baked in and loaded on boot so a deploy needs no inner pull. The cost is that a change to runner control logic requires rebuilding every mission image. That is an accepted trade, not a bug.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The runner is DinD-only.`}),` A mission’s containers always run on the nested daemon inside the run’s own container, never as siblings on the operator’s daemon. The sibling topology is gone, not optional: it put every mission’s containers on one daemon, where parallel runs collided on fixed `,_jsx(_components.code,{children:`container_name`}),` values and published ports. `,_jsx(_components.code,{children:`nested_container_check=skip`}),` bypasses the precondition `,_jsx(_components.em,{children:`check`}),` for hosts where the probe cannot run; it cannot restore the sibling topology.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`A fused image records the base generation it was built on`}),`, and this build refuses an artifact older than the generation it runs, in both the CLI and the console, with a named refusal rather than a mid-run failure. One verdict serves both the run-create gate and the catalog browse surface, so a card can never say runnable for something a run would refuse.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The two acquisition paths never cross.`}),` A library mission is pulled as a prebuilt fused image; a local mission is ingested and built locally. Both write their installed record through the same shared helper.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Attachment bytes never ride run-control.`}),` The agent asks for a companion file by name and receives a short-lived HMAC-signed URL it fetches separately. Signing secrets are generated per process unless explicitly pinned.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`7-telemetry-and-the-evidence-pipeline`,children:`7. Telemetry and the evidence pipeline`}),`\n`,_jsxs(_components.p,{children:[`Raw OTLP is `,_jsx(_components.strong,{children:`canonical and immutable`}),`. Everything else is derived.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`The live view and the grader read natively off the persisted raw store. The OTLP receiver is ingress-only — it never becomes a routing, correlation or persistence layer.`}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The agent-event projection is a rebuildable cache, never a source of truth.`}),` It is versioned by adapter name and version so a changed adapter invalidates and re-derives it.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The grader must never read the agent-event projection.`}),` It consumes the sealed raw record only. This one is enforced by name in `,_jsx(_components.code,{children:`.importlinter`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Adapters must be total.`}),` An unrecognised span becomes a generic event, an adapter exception becomes exactly one error event, and neither ever produces a 5xx. Supporting a new agent must require no frontend change and no core change.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Adapter selection is authoritative from the run’s recorded agent`}),`, not from agent-emitted resource attributes, which are forgeable.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Admit a batch and seal a run under the run’s admission lock.`}),` Both start their transaction by updating the run’s row in `,_jsx(_components.code,{children:`otel_admissions`}),`, so whichever comes second waits for the first to commit. In that one transaction the receiver checks the seal, numbers the batch `,_jsx(_components.code,{children:`MAX(seq) + 1`}),` and inserts it (`,_jsx(_components.code,{children:`TraceStore.append_next`}),`), so a batch either commits before the seal and is hashed into the evidence digest, or sees the seal and is refused. Do not split the seal check from the append, and do not number a batch by counting the run’s rows. Ingest runs on the receiver’s own worker threads and the seal on another thread, so nothing else orders them: a split check lets a batch land while the run is being hashed, and an untouched run then reads `,_jsx(_components.code,{children:`mismatch`}),`; a count lets two batches share a seq. A unique `,_jsx(_components.code,{children:`(run_id, seq)`}),` index on `,_jsx(_components.code,{children:`traces`}),` and `,_jsx(_components.code,{children:`logs`}),` backs this up. On SQLite the seal’s half of the lock looks like dead code, because the database’s single write lock already orders the two; on an engine that locks rows, only this lock keeps a batch out of the hash. `,_jsx(_components.code,{children:`tests/unit/test_otel_admission.py`}),` races real threads against a real database, and `,_jsx(_components.code,{children:`test_seal_takes_the_admission_lock`}),` in `,_jsx(_components.code,{children:`tests/adapters/test_otel_seal_store_sqlite.py`}),` pins the seal’s half.`]}),`\n`,_jsxs(_components.li,{children:[`A run is `,_jsx(_components.strong,{children:`sealed`}),` on terminal; spans arriving afterwards are rejected. Replay never affects a score.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The evidence digest covers every evidence stream grading reads`}),`: raw traces, raw logs, run-control submissions and observed facts. A new evidence stream goes inside it, and any change to what it covers or how it is framed bumps the scheme tag, so an old digest reads as unverifiable rather than as tampered. A recorded digest is never overwritten, and a digest XORCISE cannot check is never reported as a mismatch.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Telemetry is optional.`}),` A run with no traces is valid: deterministic checks read artifacts and observed facts, and criteria the judge cannot evidence are left unscored: they count as zero, and the result carries an upper bound that grants their weight. Never make a trace a precondition for grading.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`8-grading`,children:`8. Grading`}),`\n`,_jsxs(_components.p,{children:[`A score is `,_jsx(_components.strong,{children:`half deterministic checks, half model judgement`}),`, over the sealed evidence only.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Deterministic checks assert a named operation over a value resolved from a named source. Fetching is decoupled from asserting so a new check op needs no new resolver.`}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Agent-controlled evidence rides the user role, fenced as untrusted and stripped of fence markers; instructions ride the system role.`}),` Never interpolate agent output into the system prompt.`]}),`\n`,_jsxs(_components.li,{children:[`The transcript budget is counted with an `,_jsx(_components.strong,{children:`explicitly configured tokenizer`}),`. Tokenizers are not interchangeable, so the choice is recorded rather than inferred. Going over budget must fail loudly; never silently truncate evidence.`]}),`\n`,_jsx(_components.li,{children:`A degraded judge produces a recorded, re-gradeable failure — never a silent zero.`}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`9-agent-control-surface`,children:`9. Agent control surface`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`REST is the only agent control surface.`}),` There is no MCP plane and no reserved port for one. If an MCP surface is ever wanted, it is a fresh decision starting from the REST contract, not a resumed stub.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`The agent’s verbs are `,_jsx(_components.code,{children:`/mission`}),`, `,_jsx(_components.code,{children:`/intel`}),`, `,_jsx(_components.code,{children:`/artifacts`}),`, `,_jsx(_components.code,{children:`/complete`}),`, plus the connect and join helpers. Authentication is a per-run bearer token.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`There is one submission verb.`}),` The flag is not special: it is the artifact named `,_jsx(_components.code,{children:`flag`}),`, credited by a deterministic check. Never reintroduce a dedicated flag endpoint.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Post-terminal run-control is uniformly 409`}),` — no per-verb special cases, no “late submission” path.`]}),`\n`,_jsxs(_components.li,{children:[`Note that the agent-event vocabulary retains MCP call and result kinds. Those describe the `,_jsx(_components.em,{children:`agent under test`}),` invoking its own MCP tools, which is a different thing that happens to share a name.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`10-configuration`,children:`10. Configuration`}),`\n`,_jsx(_components.p,{children:`Local configuration, including the BYOM judge key, is settable through the REST API so the UI and CLI share one path.`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`The config API is a `,_jsx(_components.strong,{children:`fixed, judge-shaped contract`}),`, never a generic settings passthrough — a generic one could brick a running server.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Secrets are write-only.`}),` A stored key reads back only as its last few characters.`]}),`\n`,_jsx(_components.li,{children:`All writers go through the shared upsert helper and then clear the settings cache. Writing the environment file by any other path leaves the running server serving stale configuration.`}),`\n`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Security note.`}),` In the default local topology the REST and OTLP planes bind loopback plus, on native Linux, the Docker bridge gateway — the narrowest set that still lets an agent in a container reach them via `,_jsx(_components.code,{children:`host.docker.internal`}),`. They widen to `,_jsx(_components.code,{children:`0.0.0.0`}),` only on an explicit `,_jsx(_components.code,{children:`XORCISE_HOST=0.0.0.0`}),`, or when that gateway cannot be determined at boot. Neither plane authenticates the operator surface. A loopback bind is therefore `,_jsx(_components.strong,{children:`not`}),` a security boundary here: every local process and every container on that bridge can read runs, create runs, and write configuration. Do not add secret-bearing endpoints on the assumption that the surface is loopback-only, and do not assume the wildcard case never happens — it is a live fallback path.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`11-deferred-and-deliberately-absent`,children:`11. Deferred, and deliberately absent`}),`\n`,_jsx(_components.p,{children:`Stated so nobody re-derives them as gaps:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Remote and cloud-hosted mission planes are not implemented.`}),` XORCISE runs on one machine. No Terraform ships in this repo, and the remote and cloud auth commands are placeholders.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`There is no streaming trace endpoint.`}),` The live view polls with an incremental cursor. This is a deliberate simplification.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Telemetry mirroring to an external service is a reserved, default-off seam.`}),` Enabling it fails fast rather than silently exporting: nothing leaves the operator’s machine by default.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`12-contributor-ground-rules`,children:`12. Contributor ground rules`}),`\n`,_jsx(_components.p,{children:`Two short rule sets travel with the codebase. They are stated here so a contributor finds every load-bearing rule on one page.`}),`\n`,_jsxs(_components.p,{children:[`The contribution process lives in the engine’s `,_jsx(_components.a,{href:`https://github.com/xorcise-ai/xorcise/blob/main/CONTRIBUTING.md`,children:`CONTRIBUTING.md`}),`, not here: setup with `,_jsx(_components.code,{children:`uv sync --locked --extra dev`}),`, the test lanes and which of them run on a pull request (the Docker-heavy `,_jsx(_components.code,{children:`integration`}),` and `,_jsx(_components.code,{children:`e2e`}),` lanes run on any pull request that changes more than documentation), the title and release-note label the `,_jsx(_components.code,{children:`pr-contract`}),` check reads, and the rule that a label is declared in `,_jsx(_components.code,{children:`.github/labels.yml`}),` before any configuration uses it, which `,_jsx(_components.code,{children:`tests/topology/test_label_contract.py`}),` enforces.`]}),`\n`,_jsx(Heading,{level:`3`,id:`coding-standards`,children:`Coding standards`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`ruff (lint+format), mypy `,_jsx(_components.code,{children:`--strict`}),`. No `,_jsx(_components.code,{children:`# type: ignore`}),` without a reason comment.`]}),`\n`,_jsxs(_components.li,{children:[`Small, focused files (one responsibility). Typed DTOs (`,_jsx(_components.code,{children:`xorcise.core.contracts`}),`) over loose dicts at boundaries.`]}),`\n`,_jsx(_components.li,{children:`TDD: failing test → minimal impl → green → commit.`}),`\n`,_jsxs(_components.li,{children:[`CI is GitHub Actions (`,_jsx(_components.code,{children:`.github/workflows/ci.yml`}),`); any CI lane, dependency setup, guard, or test-marker change must be reflected there.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`3`,id:`security`,children:`Security`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`The application layer never holds the Docker socket (only `,_jsx(_components.code,{children:`runner`}),`). Nothing leaves by default (egress is opt-in).`]}),`\n`,_jsxs(_components.li,{children:[`Secrets live in `,_jsx(_components.code,{children:`~/.xorcise/.env`}),` (0600), never in the repo. BYOM keys are the user’s.`]}),`\n`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"codeStyling":{"theme":"css-variables"},"config":{},"pageMetadata":{"title":"Architecture","description":"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.","icon":"layers","href":"/contribute/architecture"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"1. Shape of the product","slug":"1-shape-of-the-product","depth":2,"children":[]},{"title":"2. Layering and imports","slug":"2-layering-and-imports","depth":2,"children":[]},{"title":"3. Persistence","slug":"3-persistence","depth":2,"children":[]},{"title":"4. Run lifecycle and durable state","slug":"4-run-lifecycle-and-durable-state","depth":2,"children":[]},{"title":"5. Network isolation","slug":"5-network-isolation","depth":2,"children":[]},{"title":"6. Missions","slug":"6-missions","depth":2,"children":[]},{"title":"7. Telemetry and the evidence pipeline","slug":"7-telemetry-and-the-evidence-pipeline","depth":2,"children":[]},{"title":"8. Grading","slug":"8-grading","depth":2,"children":[]},{"title":"9. Agent control surface","slug":"9-agent-control-surface","depth":2,"children":[]},{"title":"10. Configuration","slug":"10-configuration","depth":2,"children":[]},{"title":"11. Deferred, and deliberately absent","slug":"11-deferred-and-deliberately-absent","depth":2,"children":[]},{"title":"12. Contributor ground rules","slug":"12-contributor-ground-rules","depth":2,"children":[{"title":"Coding standards","slug":"coding-standards","depth":3,"children":[]},{"title":"Security","slug":"security","depth":3,"children":[]}]}],"codeExamples":{}},"jsLinks":["/_mintlify/custom/xorciseai/3185289ed42cb0fc/mermaid-scale.js","/_mintlify/custom/xorciseai/7c0d3224d517b622/skip-link.js"],"subdomain":"xorciseai","siteName":"XORCISE","title":"Architecture - XORCISE","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-10-29T04:44:13.307Z","originalFileLocation":"contribute/architecture.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=\"dark\";if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"dark\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\">\u003cmeta data-mint-head name=\"application-name\" content=\"XORCISE\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Architecture - XORCISE\">\u003cmeta data-mint-head property=\"og:description\" content=\"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"XORCISE Documentation\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.xorcise.ai/contribute/architecture\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://docs.xorcise.ai/images/og.png\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Architecture - XORCISE\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://docs.xorcise.ai/images/og.png\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/xorciseai/YQeBW6ayu4BmBW5L/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"XORCISE\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#8a6710\">\u003cmeta data-mint-head name=\"og:locale\" content=\"en_US\">\u003cmeta data-mint-head name=\"og:image:alt\" content=\"XORCISE.AI documentation — evaluate cyber AI agents against realistic mission environments, locally on your own machine\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.xorcise.ai/contribute/architecture\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/contribute/architecture.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.xorcise.ai/#organization\",\"name\":\"XORCISE\",\"url\":\"https://docs.xorcise.ai\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/xorciseai/73cVaorGzBMhO39b/images/logo-light.svg?fit=max\u0026auto=format\u0026n=73cVaorGzBMhO39b\u0026q=85\u0026s=d57ed1fcfcf482cef1a8deb150224de5\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.xorcise.ai#website\",\"name\":\"XORCISE\",\"url\":\"https://docs.xorcise.ai\",\"publisher\":{\"@id\":\"https://docs.xorcise.ai/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.xorcise.ai/contribute/architecture#webpage\",\"url\":\"https://docs.xorcise.ai/contribute/architecture\",\"name\":\"Architecture\",\"description\":\"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\",\"dateModified\":\"2026-10-29T04:44:13.307Z\",\"isPartOf\":{\"@id\":\"https://docs.xorcise.ai#website\"},\"breadcrumb\":{\"@id\":\"https://docs.xorcise.ai/contribute/architecture#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.xorcise.ai/contribute/architecture#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"XORCISE\",\"item\":\"https://docs.xorcise.ai\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Architecture\",\"item\":\"https://docs.xorcise.ai/contribute/architecture\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.xorcise.ai/contribute/architecture#article\",\"headline\":\"Architecture\",\"name\":\"Architecture\",\"description\":\"The constraints and invariants the XORCISE codebase depends on — the rules to hold, and the reasons obvious-looking simplifications are wrong.\",\"url\":\"https://docs.xorcise.ai/contribute/architecture\",\"mainEntityOfPage\":{\"@id\":\"https://docs.xorcise.ai/contribute/architecture#webpage\"},\"image\":\"https://docs.xorcise.ai/images/og.png\",\"dateModified\":\"2026-10-29T04:44:13.307Z\",\"publisher\":{\"@id\":\"https://docs.xorcise.ai/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.xorcise.ai#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"XORCISE\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"XORCISE","nav":{"global":null,"tabs":[{"tab":"Guides","pages":[{"title":"Guides","href":"/introduction"}]},{"tab":"Reference","groups":[{"group":"Command line","pages":[{"title":"CLI reference","icon":"terminal","href":"/reference/cli"}]},{"group":"Configuration","pages":[{"title":"Configuration keys","icon":"sliders-horizontal","href":"/reference/configuration"}]},{"group":"Authoring","pages":[{"title":"Mission manifest","icon":"file-json","href":"/reference/mission-manifest"},{"title":"Checks and ops","icon":"list-checks","href":"/reference/checks"}]},{"group":"API","pages":[{"title":"REST API","icon":"webhook","href":"/reference/api"}]},{"group":"Lookup","pages":[{"title":"Glossary","description":"Every XORCISE term in one line, with a link to the page that defines it properly.","icon":"book-open-text","href":"/reference/glossary"}]},{"group":"Contributing","pages":[{"title":"Architecture","icon":"layers","href":"/contribute/architecture"},{"title":"Run a mission from a source checkout","description":"The extra prerequisites, stub mode, and failure modes that apply when you drive the full run loop from a development checkout rather than an installed package.","icon":"terminal","href":"/contribute/run-from-source"}]}]}]},"basePath":"","title":"Architecture - XORCISE","buildId":"dpl_36EDyZCDfRuoUD9af5CH4M1bqY2h","deploymentId":"dpl_36EDyZCDfRuoUD9af5CH4M1bqY2h","dynamics":[],"chunks":[]}