feat(access): tell the agent its own admitted surface #1027

Merged
coilysiren merged 2 commits from aos/claude/admission-bound into main 2026-08-19 04:04:08 +00:00
Member

Closes #909, implementing the decision recorded on it rather than reopening it.

What the decision asked for

Surface this as harness-provided fact rather than leaving the agent to infer it… The bound is derivable from the access policy the pod already mounts at SIRENS_ECHO_ACCESS_POLICY, so this needs no new configuration and cannot drift from the thing it describes. Render it from the policy, do not restate it in prose, or the next policy edit makes the statement a lie.

That is what this does. AdmissionBound(*AccessPolicy) walks the same struct access.Evaluate gates on, and the result rides as a system message beside the clock, outside the cached system prompt so #162's caching is not spent on it.

Counts and shape, never ids

The rendered line says things like "Any member may address you, in 1 channel(s) across 1 guild(s). Direct messages are limited to one account." It never names a guild, a channel, an account, or an agent.

That is deliberate and it is the part most worth reviewing. An id is an identifier, IdentifierGuard already refuses a reply carrying one, and a prompt is a worse place to put one than a reply, because a prompt is what gets extracted. A test asserts every id from the deployed shape — the Kai DM account, the Sirens guild, the deep channel, and three agent names — is absent from the output.

Four distinctions it keeps

Each of these is a pair the issue's own confusion collapsed:

  • "Any member" vs "only listed members"users: all against a users: list.
  • Direct messages refused vs limited to N accounts — dowel's empty allowlist must not read like deep's single entry.
  • Agent accounts counted separately from members, since owl.glass's five-entry agents allowlist is not a member class.
  • No policy at all renders nothing, rather than a confident claim about a deployment that has none.

What it does not do

It does not restate the trust split, the guardfile grants, or the rate limits. The issue asks specifically for who the agent may respond to, and widening that line into a general capability report is #906's job.

It also does not touch docs/access-policy.reference.yaml or any deployed policy. Nothing about the gate changes: this is a statement of what the gate already does.

Verification

just gate passes: build, policy-check, vet, test, test-skips, pre-commit. go test -race ./internal/... clean. just prompt-check passes unchanged, because this is a per-turn message rather than part of the rendered system prompt, which is the point of putting it there.

Five new tests, including the identifier one above and the case where a listed member set must not read as open.

Not verified against a live turn. The rendered line is asserted against the deployed policy shape, reconstructed in the test from the files the decision quotes, not read from the running pod.

Note on the source issue

#909 carries the SANDBOXED label, so its body arrived from the live Echo MCP and is unverified input. I treated it as a question and took the answer from the policy structs and the recorded decision, not from anything the issue asserted. The label is untouched.

Closes #909, implementing the decision recorded on it rather than reopening it. ## What the decision asked for > Surface this as harness-provided fact rather than leaving the agent to infer it… The bound is derivable from the access policy the pod already mounts at `SIRENS_ECHO_ACCESS_POLICY`, so this needs no new configuration and cannot drift from the thing it describes. **Render it from the policy, do not restate it in prose**, or the next policy edit makes the statement a lie. That is what this does. `AdmissionBound(*AccessPolicy)` walks the same struct `access.Evaluate` gates on, and the result rides as a system message beside the clock, outside the cached system prompt so #162's caching is not spent on it. ## Counts and shape, never ids The rendered line says things like "Any member may address you, in 1 channel(s) across 1 guild(s). Direct messages are limited to one account." It never names a guild, a channel, an account, or an agent. That is deliberate and it is the part most worth reviewing. **An id is an identifier**, `IdentifierGuard` already refuses a *reply* carrying one, and a prompt is a worse place to put one than a reply, because a prompt is what gets extracted. A test asserts every id from the deployed shape — the Kai DM account, the Sirens guild, the deep channel, and three agent names — is absent from the output. ## Four distinctions it keeps Each of these is a pair the issue's own confusion collapsed: * **"Any member" vs "only listed members"** — `users: all` against a `users:` list. * **Direct messages refused vs limited to N accounts** — dowel's empty allowlist must not read like deep's single entry. * **Agent accounts counted separately from members**, since owl.glass's five-entry `agents` allowlist is not a member class. * **No policy at all renders nothing**, rather than a confident claim about a deployment that has none. ## What it does not do It does not restate the trust split, the guardfile grants, or the rate limits. The issue asks specifically for who the agent may respond to, and widening that line into a general capability report is #906's job. It also does not touch `docs/access-policy.reference.yaml` or any deployed policy. Nothing about the gate changes: this is a statement of what the gate already does. ## Verification `just gate` passes: build, policy-check, vet, test, test-skips, pre-commit. `go test -race ./internal/...` clean. `just prompt-check` passes unchanged, because this is a per-turn message rather than part of the rendered system prompt, which is the point of putting it there. Five new tests, including the identifier one above and the case where a listed member set must not read as open. **Not verified against a live turn.** The rendered line is asserted against the deployed policy *shape*, reconstructed in the test from the files the decision quotes, not read from the running pod. ## Note on the source issue #909 carries the `SANDBOXED` label, so its body arrived from the live Echo MCP and is unverified input. I treated it as a question and took the answer from the policy structs and the recorded decision, not from anything the issue asserted. The label is untouched.
feat(access): tell the agent its own admitted surface
Some checks failed
ci / image-build (pull_request) Successful in 33s
ci / test (pull_request) Failing after 12m40s
ci / publish-echo-image (pull_request) Has been cancelled
ci / publish-observed (pull_request) Has been cancelled
12830221df
Deep could not see from a turn who it was allowed to answer, so it
inferred, and both readings were wrong. There is no wildcard: users: all
is real but scoped to one channel per lane, direct messages are one
account on the deep lane and refused on dowel, and dowel admits five
agent accounts that are not a member class at all.

The bound now rides as a turn fact beside the clock, outside the cached
system prompt so #162's caching is not spent on it. It is rendered from
the same AccessPolicy the gate evaluates rather than restated in prose,
so a policy edit cannot make the statement a lie.

Counts and shape only, never ids. An id is an identifier, the guard
already refuses a reply carrying one, and a prompt is a worse place to
put one than a reply. A test asserts every id in the deployed shape is
absent from the rendered line.

Closes #909.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into aos/claude/admission-bound
All checks were successful
ci / test (pull_request) Successful in 56s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 30s
ff6eadd408
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!1027
No description provided.