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.
Declare the boundary, not the population
Section titled “Declare the boundary, not the population”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:
| Policy | Language | What it says |
|---|---|---|
ToolAccessFloor | Cedar | No agent reaches a destructive tool without an approval on the request. |
RuntimeHarnessFloor | Cedar | A runtime-created harness reaches Loom’s own API and nothing else. |
AssistantToolAllowlist | Cedar | Declared agents invoke Loom’s API and the code-interpreter sandbox. |
OwnSessionMemoryOnly | Cedar | An agent reads its own session’s memory and no other. |
SessionSpendBudget | dogwood | No session spends past 5000 cents in 12h (sum_within). |
ApprovalBeforeDestructiveTool | dogwood | A 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.
The tier dial
Section titled “The tier dial”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.
| Tier | Enforcement |
|---|---|
light | Pinned LOG_ONLY permanently. No parameter lifts it. |
production / production-ha | Enforce-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.
The rollout
Section titled “The rollout”chant run loom-policy-rollout --temporalFive phases, WAF count-mode’s shape:
- Apply — re-synthesize and deploy the policy set with
EnforcementModelog-only, through the existingloom-agentscomponent. - 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. - Replay — replay the emitted
.dwset 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 theWouldDenysearch attribute, which makes “show me the rollouts that would break something” one filter. - 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. - Enforce — flip
EnforcementModeand 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-rolloutWhat is real locally, and what is not
Section titled “What is real locally, and what is not”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-agentcoreentry — only abedrock-runtimestub. - 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
ListEventsreturns 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.
Editing the policy set
Section titled “Editing the policy set”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 dialEdit 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
.cedarschemawas 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
dogwoodbinary 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.