feat(automation): let a routine keep a standing grant for a tool with no target - #645
Open
djpentz wants to merge 1 commit into
Open
feat(automation): let a routine keep a standing grant for a tool with no target#645djpentz wants to merge 1 commit into
djpentz wants to merge 1 commit into
Conversation
…rget
"Allow every time" on a run's approval card mints a standing rule on the task, but
only when the call is external-risk AND names a target. A routine whose work is web
search therefore asks the same question on every single run: the session-scoped
"always" dies with the run, and each scheduled run is a fresh session.
The refusal is upside down against the engine's own strictness table. Egress ranks
1, external ranks 2 — so a routine can be trusted forever to send a message off the
machine, but never to read a web page. The target binding is what makes an external
grant safe; a tool that takes no destination simply fell outside the shape and got
downgraded to a one-off, silently, while the API reported success.
Egress calls with no bindable target now mint a name-only rule on the task —
`ScheduledTask.add_tool_rule`, the same list, read by _seed_task_permissions on
every run and revocable through the existing PATCH …/{id} {"revoke": entry}.
Write-local and exec are unchanged (shell asks forever), and external stays
target-bound: "message this chat" is a grant, "send anything anywhere" is not.
Found running a daily monitoring routine on a headless box: it asked permission to
search the web every morning for four days, and "Always" could not stop it.
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.
The problem
"Allow every time" on a scheduled run's approval card calls
mint_task_rule, whichrequires
standing_rule_candidate— external risk and a declared target argumentand a non-empty target. A routine whose work is
web_searchsatisfies none of that,so the grant is refused and quietly downgraded to a one-off (
ApprovalOutcome.ONCE),while
POST /v1/inbox/{id}/resolvestill returns{"ok": true}.The practical effect: the routine asks the same question on every run, forever. The
session-scoped
ALWAYS_TOOLdoesn't help either — each scheduled run is a fresh session,so "always" lasts one run. There is no other path:
PATCH /v1/automations/{id}canrevokea rule but cannot add one.We hit this with a daily monitoring routine on a headless deployment. It asked to search
the web at 07:00 every morning for four days running; answering "Always" changed nothing,
because the refusal is silent.
Why the refusal looks upside down
From
risk.py:Egress ranks below external. Yet an external call earns a durable, unattended
standing rule the moment it names a target, while an egress call can never earn one.
A routine can be trusted forever to send a message off the machine, but not to read a
web page. The target binding is what makes the external grant safe — and a tool that
takes no destination (a search takes a query) simply fell outside that shape.
The change
standing_tool_candidate(tool)— egress only — gates a new target-less grant.ScheduledTask.add_tool_rule(tool)appends a name-only entry to the samealways_allowed_toolslist the engine already reads on every run via_seed_task_permissions, so nothing new has to be honoured anywhere.anything anywhere" is not.
public()["always_allowed"]withtarget: nullandPATCH {"revoke": entry}removes it.Scope is one task. A grant on the daily sweep says nothing about any other routine or
any interactive session.
Tests
tests/test_task_tool_grants.py— the gate admits egress and refuses exec/write; aweb_searchgrant persists on the task, reads back with a null target, and revokes; arun_shellcall still earns nothing. The persistence test fails againstmain.Full suite on this branch: 1955 passed, 1 skipped. (Three deselected locally — they bind
the fixed OAuth callback port, which a container on this machine holds; unrelated to
this change.)
🤖 Generated with Claude Code
https://claude.ai/code/session_01PgGyV9GDu4oLhonom37BU5