Repository navigation
Refuse a blocked agent policy decision at the agent output ingest route - #680
Merged
Merged
Conversation
POST /v0/vnext/agents/ingest-output asked the agent policy for a decision and passed it to the connector service, which recorded it in metadata and wrote a source and a needs_review artifact whatever it said. A request the policy blocked (a read_only_agent key, a key whose domains exclude the requested domain, a call with no key that declares no profile) answered HTTP 201 and stored its rows while the body said blocked. The route now returns the same refusal as POST /v0/vnext/sources from inside the connection, so the policy events commit and nothing else is written. Credential material in the title, the content or the source refs raised CaptureCredentialRefused, which the route did not catch, and answered HTTP 500. It answers HTTP 400 with the route's invalid request text, repeats none of the request and writes nothing. VNextConnectorService.ingest_agent_output also refuses a blocked decision before it writes, so a caller that forgets the check gets the refusal and no rows. A unit test reads every function under routers/ and fails when a call of a helper that returns a decision does not act on a blocked one.
A refused request writes no source, chunk, artifact, provenance link, memory or embedding job. It stores the policy events of the refusal and records the agent identity, as any request does. The CHANGELOG pins of the mutation manifest are refreshed for the new wording.
samrusani
marked this pull request as ready for review
October 26, 2026 07:40
This was referenced Oct 26, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
POST /v0/vnext/agents/ingest-outputasked the agent policy for a decision, recorded it in metadata, and wrote a source and aneeds_reviewartifact (and a candidate memory whenpropose_memorywas set) whatever the decision said. A request the policy blocked answered HTTP 201 while its body saidblocked.POST /v0/vnext/sourcesanswers HTTP 403 for the same key, andalice_vnext_ingest_agent_outputandalicebot vnext agents ingest-outputalready refused.POST /v0/vnext/sourcesreturns (HTTP 403,agent policy blocked this action, the policy decision) from inside the connection, so the policy events of the refusal commit. It writes no source, chunk, artifact, provenance link or memory.CaptureCredentialRefused, which the route did not catch, and answered HTTP 500. It now answers HTTP 400 with the route's invalid-request text, repeats none of the request and writes nothing.VNextConnectorService.ingest_agent_outputrefuses a recorded decision that isblockedbefore it writes anything (it raisesAgentPolicyBlockedError), so a later caller that forgets the check gets the refusal and no rows. A call with no decision, and the decisions of the command line, stay as they were.routers/and fails when a call of a helper that returns a decision (_vnext_policy_checked,_vnext_exact_resource_policy,_vnext_operator_gate,evaluate_agent_policy) does not act on a blocked one.tests/integration/operator_route_vault.py: the owner vault's setup call to this route now declares a profile. It relied on the route writing for a keyless call that names anagent_idand no profile, which is aread_only_agent.A keyless call that names an
agent_idother thanhermesandopenclawand declares no profile is aread_only_agent, so this route now refuses it, asPOST /v0/vnext/sources, the MCP tool and the command line do.Call sites of helpers that return a decision
Every other call under
routers/already acts on a blocked decision, and the new test keeps it so:vnext_memories.py(sources, memory review twice, memory proposal throughpropose_memory, memory audit, the source operator gate),vnext_projects.py(open loop, scheduler configure, run now, run due, pause and resume),vnext_review.py(five report and candidate generators, queue task),vnext_retrieval.py(context pack, artifact trace),vnext_changes.py, and_vnext_authorized_artifact, which raises.Tests
tests/integration/test_ingest_output_policy_postgres.py(PostgreSQL): aread_only_agentkey, a key whose domains exclude the requested domain, a key whose sensitivity labels exclude the request, and a read-only key bound to a project each get 403 and no new row in sources, chunks, artifacts, provenance links or memories, with the policy events stored; a key bound to one project that asks for another is refused; atrusted_local_agentkey and a keyless call that declares a profile which may write still get 201 and their rows; credential material in the source refs, the content or the title is 400, writes nothing and is not echoed.tests/unit/test_agent_output_blocked_decision.py: the service refusal, including a decision taken from the policy engine and compared field for field.tests/unit/test_router_policy_decisions_are_acted_on.py: the scan, and its checks on source text that does and does not act.scripts/derived_label_mutations.json: seven new cases (the route check, the service refusal, the credential catch, and the refusal moved below validation or made to lose its reasons), the cases of the three changed files re-pinned, and one stale case repaired (its test had been renamed, so it never ran).Upgrade Overview
Protected Areas
Compatibility Impact
POST /v0/vnext/agents/ingest-outputnow answers HTTP 403 for a request its agent policy blocks and writes no source, chunk, artifact, provenance link, memory or embedding job for it (the refusal's policy events and the agent identity are still recorded, as on every request), asPOST /v0/vnext/sources, the MCP tool and the command line already do. A keyless call that names anagent_idother thanhermesandopenclawwithout declaring a profile is aread_only_agentand is refused; a caller that relied on that call writing must declare a profile that may write or use a key with one. Credential material in the request answers HTTP 400 instead of HTTP 500. Responses for allowed requests are unchanged.Migration / Rollout
No schema change, no migration and no backfill. The change applies as soon as the server runs this code.
Operator Action
Check that agents which send agent output through this route either use a key whose profile may write or, on a keyless install, declare such a profile. Rows written before the upgrade for requests the policy blocked carry
"decision": "blocked"in their storedpolicy_decisionmetadata, so an operator who wants to review them can find them there.Validation
tests/integration/test_ingest_output_policy_postgres.py(22 tests on PostgreSQL),tests/unit/test_agent_output_blocked_decision.py(9),tests/unit/test_router_policy_decisions_are_acted_on.py(14); 21 of the 45 fail on main and all pass here. The full unit suite and the route's existing integration tests pass, and the mutation cases of the changed files are killed.Rollback
Revert this commit. The route then writes blocked requests again and answers HTTP 500 for credential material; no data needs to be changed back.