Policy → sandbox config builder
Policy → sandbox config builder
Section titled “Policy → sandbox config builder”What it does: Reads a policy and produces the per-layer config one sandbox runs against.
1. Vision context
Section titled “1. Vision context”When an operator authors policy via the intent-to-policy flow, the runtime today reads that policy directly at every call. That works for chat (one process, stateless). It does not work for hosted agents — agents that run for hours on customer machines, each needing its own derived config, all needing to stay current as policy changes.
This component is the bridge: policy in → multi-layer config out, deterministically, ready to be delivered to a host machine and applied at five enforcement layers. The output (a SandboxConfig, sometimes called "the keyring") is the artifact that connects "policy authored" to "policy applied at every chokepoint." See concepts/governed-agents-architecture.md §10 for full vision context.
2. Functional specification
Section titled “2. Functional specification”Inputs:
| Name | Type | Description |
|---|---|---|
pipeline_config | dict | The L4 effective pipeline output of the intent-to-policy resolver |
sandbox_spec | dict | The named template (e.g. customer-support-emailer) — selects which policy applies and which layers are enabled |
cascade_levels | list | Ordered: [org_policy_id, project_policy_id, agent_policy_id]. Any may be null. |
tool_universe | list | The set of tools known to the catalog (curated + per-tenant) at derivation time |
org_id | str | For per-org cache keying and AAD scoping |
Outputs: a SandboxConfig object:
@dataclassclass SandboxConfig: # identity sandbox_id: str sandbox_name: str policy_version: int # monotonic per sandbox sync_url: str
# 5 enforcement layers layer_5_gateway: GatewayLayerConfig # gateway_url, gateway_token, capabilities layer_4_mcp: McpLayerConfig # mcp_proxy_url, mcp_session_token, pipeline, capabilities layer_3_codemode: CodemodeLayerConfig # allowed_imports, denied_imports, executor_network layer_2_harness: HarnessLayerConfig # permissions, hooks layer_1_kernel: KernelLayerConfig # network_egress_allowlist, fs rules, landlock_rules
# provenance — every rule traces back to a cascade level + concern + category provenance: dict[str, list[ProvenanceEntry]]
# validity issued_at: datetime expires_at: datetimeError cases:
| Condition | Behavior |
|---|---|
| Cascade contradiction (project tries to relax org floor) | Raise CascadeViolationError at apply time; no SandboxConfig produced |
sandbox_spec references unknown layers | Raise UnknownLayerError; refuse to produce partial config |
pipeline_config references tools not in tool_universe | Emit warning; bind to unknown_tool capability bucket; flag for review |
| Forbidden capability appears in agent override | Raise ForbiddenCapabilityViolation; floor cannot be lifted |
| Cache stale (policy_store written since last derivation) | Invalidate; re-derive on next request |
Non-goals:
- Delivering the config to the host (that's live-policy-push-channel)
- Enforcing the config (that's the layer appliers in q09-rings-and-cross-cutting/)
- Authoring the policy (that's intent-to-policy, already shipped)
- Choosing the sandbox-spec (that's the operator's decision via wizard or
tappass sandbox-spec create)
3. Technical design
Section titled “3. Technical design”Where it lives
Section titled “Where it lives”tappass/policy/sandbox_config_builder/├── __init__.py├── derive.py # the core derivation function (~150 LOC)├── cascade.py # strictest-wins merge (~80 LOC, reuses intent-to-policy resolver helpers)├── layer_emitters/│ ├── gateway.py # pipeline_config → GatewayLayerConfig│ ├── mcp.py # pipeline_config + tool_universe → McpLayerConfig│ ├── codemode.py # pipeline_config → CodemodeLayerConfig│ ├── harness.py # pipeline_config → HarnessLayerConfig│ └── kernel.py # pipeline_config → KernelLayerConfig├── provenance.py # threads "because" trail through every emit├── cache.py # per-org, version-keyed; invalidated on policy_store write└── schema/ └── sandbox-config.schema.jsonState transitions
Section titled “State transitions”pipeline_config + sandbox_spec + cascade_levels │ ▼[cascade.merge() — strictest-wins, validates floors] │ ▼[per-layer emitters run in parallel] │ ▼[provenance map built from emit trails] │ ▼[issued: SandboxConfig with monotonic policy_version + provenance] │ ▼[cached per (org_id, sandbox_id, policy_version_set)]Integration points
Section titled “Integration points”- Reads from
policy_store(intent-to-policy substrate, live on main). - Reads from
tool_catalog(intent-to-policy + runtime-tool-discovery components). - Reads from
vault_llm_keys,vault_mcp_servers(existing + new in this concept). - Writes to
sandbox_keyring_store(new table in this concept). - Triggers
live-policy-push-channelto push the new config to active sandboxes. - Emits audit events:
policy_intent_applied,cascade_violation,sandbox_config_derived.
The cascade merge (~80 LOC)
Section titled “The cascade merge (~80 LOC)”Reuses the merge primitives from intent-to-policy.md §10b (strictest-wins). New here: cascade-level awareness, so a project rule that would relax an org rule is rejected rather than merged.
def merge_cascade( org: PipelineConfig | None, project: PipelineConfig | None, agent: PipelineConfig | None,) -> PipelineConfig: """Strictest-wins merge with floor enforcement.
Each level can ADD rules or make existing rules STRICTER. No level can RELAX rules from a level above it. """ floor = org or empty_pipeline() if project: validate_does_not_relax(floor, project) # raises CascadeViolationError floor = strictest_wins_merge(floor, project) if agent: validate_does_not_relax(floor, agent) floor = strictest_wins_merge(floor, agent) return floorThe provenance trail
Section titled “The provenance trail”Every rule in the output carries a ProvenanceEntry:
@dataclassclass ProvenanceEntry: rule_id: str # e.g. "step:detect_pii" introduced_by: Literal["org", "project", "agent", "compliance_pack", "manual"] via_cascade_level: str | None # e.g. "org" | "project:eu-team" | "agent:ag_8cAxWM4H" via_concern: str | None # e.g. "data_leak" via_category: str | None # e.g. "customer_pii" via_compliance_pack: str | None # e.g. "eu-ai-act" operator: str # who introduced it (SSO identity) at: datetimeThe dashboard's because-trail (architecture §5.2) reads from this.
4. Definition of done
Section titled “4. Definition of done”- All
acceptance_criteriapass. - Property test: 1000 random
(pipeline_config, sandbox_spec, cascade_levels)triples produce identical output across runs. - Property test: cascade order doesn't matter (
merge(merge(o,p), a)==merge(o, merge(p, a))). - Integration test: org-level
eu-ai-actpack applied → every derived sandbox in that org has the EU AI Act floor inprovenance. - Integration test: project tries to relax org floor →
CascadeViolationErrorraised at apply, no keyring written, audit event recorded. - Performance: derivation completes in <100ms for typical inputs (P50), <500ms (P99).
- Cache: per-org cache invalidated on
policy_storewrite; subsequent reads see fresh derivation. - Audit:
policy_intent_appliedemitted with full provenance for every derivation. - Spec section in
concepts/tappass-keyring-and-sync.mdwritten and reviewed. - JSON schema
sandbox-config.schema.jsonpublished; downstream layer appliers consume it.
5. Coordination notes
Section titled “5. Coordination notes”With trust-engine-async (foundational dependency)
Section titled “With trust-engine-async (foundational dependency)”We need the async trust engine for token issuance during derivation. This must close in the first two weeks of Q3 or this component blocks. Owner of trust-engine-async: existing platform team per memory project_async_deadlock.md.
With cascade-merge-engine (sibling component)
Section titled “With cascade-merge-engine (sibling component)”We co-own the cascade merge logic. Lean: cascade-merge-engine is the authoritative implementation of validate_does_not_relax and strictest_wins_merge; this builder calls into it. Avoid duplicating merge logic in two places.
With every layer applier in q09-rings-and-cross-cutting/
Section titled “With every layer applier in q09-rings-and-cross-cutting/”We emit; they apply. The contract is sandbox-config.schema.json. Every layer applier must conform to its slice's schema. Schema changes require coordination with all five appliers (and the upstream-tool-proxy that consumes layer-4).
We trigger pushes on derivation. Push channel reads from sandbox_keyring_store. We should never push directly — the channel owns delivery.
The evaluator derives temporary SandboxConfigs for evaluation runs (not persisted). We expose derive(...) as a pure function (no side effects, no DB write, no audit emit) when called with mode=evaluation. Production calls use mode=production which persists + emits.
Open questions
Section titled “Open questions”- (Q1) Provenance storage. Do we store the full provenance map alongside every keyring (storage cost), or recompute it on demand for the dashboard (compute cost)? Lean: store. Storage is cheap; the because-trail must be instantly readable.
- (Q2) Cascade re-derive triggers. When org policy changes, do we re-derive every sandbox in the org synchronously (could be thousands), or queue and process async? Lean: queue + process async with a strict TTL on the old config (5 min via
expires_at); incoming sync pushes happen in priority order (most-active sandboxes first). Owner: this component + push channel coordinate. - (Q3) Backward compatibility for existing pipeline_config. Today's runtime reads
pipeline_configdirectly; we add a new derivation step that producesSandboxConfig. Do we ship both code paths during transition, or rip-and-replace perfeedback_no_backcompat_saas? Lean: rip-and-replace; existing runtime starts readingSandboxConfigexclusively at GA.
6. Out of scope
Section titled “6. Out of scope”- Delivery. Pushing the config to a host is live-policy-push-channel.
- Application. Each layer's applier in q09-rings-and-cross-cutting/ takes the config and makes it real.
- Authoring. Operators write policy via intent-to-policy and compliance packs; we consume.
- Evaluation. pre-deployment-evaluator calls us in evaluation mode; we don't run probes ourselves.
- Provider routing. The gateway's
gateway_tokenis opaque to us — we issue it; the gateway interprets. - Capability token signing. ES256 happens in
tappass/gateway/— we just include the issued tokens in the layer-5 slice.