Skip to content

Policy rollout

Loom’s app creates agents you did not write. A user opens the Harnesses screen, fills in a form, and a new no-code agent exists — with tools, with memory, with a gateway. loomster declares exactly one agent, the Strands assistant. Every other agent in a running Loom is a population loomster cannot enumerate, name, or tag.

That is not a gap to close. It is the shape of the product, and the authorization layer is written for it.

The policies in src/loom-agents/policy/ name no agent. They are written against two attributes:

  • principal.origin"declared" for an agent this repo synthesizes, "runtime" for one Loom’s app created on demand.
  • resource.surface"loom-api", "code-interpreter", or "external".

So the harness floor reads:

forbid (principal is Loom::Agent, action in [InvokeTool, CallExternal], resource is Loom::Tool)
when { principal.origin == "runtime" }
unless { resource.surface == "loom-api" };

A harness created five minutes from now, by a user nobody has met, matches that policy on creation. Nothing regenerates, nothing redeploys, no list is updated. chant cannot declare what Loom’s users will create; it can declare the boundary that governs whatever appears — and a forbid beats every permit in the set unconditionally, so the boundary survives a wider grant somebody adds later, including one Loom’s own app writes.

The full set is six policies on one AWS::BedrockAgentCore::PolicyEngine, riding the existing loom-agents stack:

PolicyLanguageWhat it says
ToolAccessFloorCedarNo agent reaches a destructive tool without an approval on the request.
RuntimeHarnessFloorCedarA runtime-created harness reaches Loom’s own API and nothing else.
AssistantToolAllowlistCedarDeclared agents invoke Loom’s API and the code-interpreter sandbox.
OwnSessionMemoryOnlyCedarAn agent reads its own session’s memory and no other.
SessionSpendBudgetdogwoodNo session spends past 5000 cents in 12h (sum_within).
ApprovalBeforeDestructiveTooldogwoodA destructive call needs an approval event in the same session within 1h (formerly).

The two dogwood policies are the reason the rollout is staged rather than applied. A temporal policy is not decidable from its source: whether formerly within 1h … fires depends on traffic nobody has replayed. You cannot read it and know what it will do.

EnforcementMode has two values, and LOG_ONLY is not “off” — the policy is evaluated on every request and its decision is observed rather than returned. That is what makes a rollout watchable before it binds.

TierEnforcement
lightPinned LOG_ONLY permanently. No parameter lifts it.
production / production-haEnforce-eligible. Still log-only by default; promoted by the Op below.

light is the tier people run to look at Loom. A policy that denies a tool call in a demo deployment is a support ticket, not a guardrail.

chant run loom-policy-rollout --temporal

Five phases, WAF count-mode’s shape:

  1. Apply — re-synthesize and deploy the policy set with EnforcementMode log-only, through the existing loom-agents component.
  2. Observe — a durable multi-day window (default 72h). It is a gate that advances on its own when the timer elapses, so an operator who has seen enough traffic can end it early: chant run signal loom-policy-rollout end-policy-observation.
  3. Replay — replay the emitted .dw set against the window’s traffic and count what it would have denied. Every decision is expected to allow — that is current behaviour — so each divergence is a request the policy set would have blocked. The count rides out as the WouldDeny search attribute, which makes “show me the rollouts that would break something” one filter.
  4. Approve — the human gate. The approver reviews dist/policy/would-deny.json, the would-deny report, not a diff: chant run signal loom-policy-rollout approve-policy-rollout.
  5. Enforce — flip EnforcementMode and re-apply.

onFailure re-applies log-only. A policy rollback that left the set enforcing would lock every agent out of every tool, which is the one outcome worse than the policy not being enforced.

On light the Op is a one-shot log-only apply — no window, no gate, no promotion, because there is nothing to promote to — and it runs on the local executor with no Temporal:

chant run loom-policy-rollout

The Observe/Replay phases run against a checked-in trace fixture (test/fixtures/policy-observation.log), not live gateway traffic. That is not a temporary shortcut:

  • Floci’s published service index carries no bedrock-agentcore entry — only a bedrock-runtime stub.
  • More decisively, a control-plane emulator could not serve this data even if it existed. AgentCore exposes no decision-history read API. Per-call policy decisions go to CloudWatch, and Memory ListEvents returns what the agent wrote, not what the service decided.

Both verified in intentius/chant#1691 and recorded on loomster#171.

So: policy evaluation and live trace capture are real-AWS-only verifies, the same class as the runtime Verify checks the local caveats already document. What is real locally is everything up to the decision — the policy set builds, validates, synthesizes at the right EnforcementMode per tier, embeds into the right arm of AgentCore’s Definition union, and replays end to end through the same dogwoodReplay activity the real Op runs.

Point the Op at a trace captured from a real deployment with LOOM_POLICY_TRACE_PATH=/path/to/trace.log.

The policies are typed TypeScript, generated from a Cedar schema:

src/loom-agents/policy/
loom-agents.cedarschema # the authorization model — entity types, actions, context
generated/ # checked in; `npm run generate:cedar` regenerates it
policies.ts # the Cedar half
temporal.ts # the dogwood half, plus the event schema and macros
stage.ts # the tier dial

Edit the schema, run npm run generate:cedar, and a typo’d entity type becomes a compile error rather than a validation error at deploy time. npm run synth:policy emits dist/policy/{policies.cedar,policies.cedar.json,policies.dw,events.dwschema} — the artifacts any Cedar evaluator reads, chant nowhere in them.

Two build warnings are expected and honest:

  • CEDE010 reports that no .cedarschema was emitted beside the policies, so the set was checked for parseability rather than validated against the schema. The schema here is an authored source file, and chant ships no declarable that emits one as a build artifact.
  • DWDE010 reports that no dogwood binary was found, so macro expansion and the temporal type check did not run. Upstream ships no published build; nothing in gating CI runs it, by design. A check that quietly succeeds when it could not run is claiming a guarantee it did not make.

Dogwood is pre-release. Upstream says on its own README that it is not intended for production use, has no releases and no versioning, and chant pins a git SHA.