Repository navigation
sessions: ✨ Spend grants for owner-approved session stops - #278
Merged
Merged
Conversation
yasyf
force-pushed
the
feat/sessions-grants
branch
from
October 3, 2026 04:43
dba631d to
c8aaebf
Compare
Context: the unreleased owner-close annotation allowed unlimited reuse and accepted cc-notes answers written during the acting session, and failed launches left idle Orca terminals their creating session could not close. Summary: add sessions.close, sessions.launchctl, and sessions.task-stop grants minted from rulings that name the target and predate the acting session, plus self-created idle terminal evidence for closes; each ruling and scope is one use across all trees. Motivation: the owner asked for an owner-named lift for launchctl and TaskStop, and for sessions to close idle orphan terminals they created; every other session-guard block keeps its policy. Details: a lifted close or launchctl stop must run once at the head of the payload; Grants gains an optional action, explicit proposals, judge-less mints, live evidence rechecks, and a replay window the session kinds set to two minutes. Claude-Session-Id: 900424b6-7393-480c-a26a-f1bd21da6e57
Context: the hook lint forbids a catch-all in a pack hook, and the sessions lift carried one. Summary: Grants.decide runs check and turns any error into a Denied naming it; lifted and the sessions lift both use it. Motivation: test shard 6 failed on the swallows-failures lint at _sessions.py. Details: the system_message keeps the hook name and the error; would_allow follows it.
yasyf
force-pushed
the
feat/sessions-grants
branch
from
October 3, 2026 04:48
c8aaebf to
1c0a893
Compare
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.
Why
The owner chose the service and task policy:
After a failed launch left an orphan Orca terminal, the owner said:
The unreleased
# ccx:owner-authorized=<cc-notes answer id>annotation let an answernaming a terminal authorize unlimited closes, including when the answer was written
during the acting session. Use grants to count and record each approval's use, and
let a session clean up an idle terminal it created when no dispatch owns it.
This PR stacks on #277.
What
ccn answer showlookup with three declarationswithout a judge:
sessions.closescopes toterminal,sessions.launchctlto thelaunchd
servicelabel, andsessions.task-stoptotask(task_idor the retiredshell_id). The budget is one use per ruling and scope across all session trees,with the default one-day expiry.
capt-hook grant show <id>shows the spend.All three set
replay=timedelta(minutes=2): the identical scope and payload canreuse a committed spend for less than two minutes, covering
PermissionRequestafter
PreToolUsefor the same call or a prompt retry. A later identical commandneeds a new approval.
Rulings(search=<scope value>)for all three. The answer body must name the valueas a whole name, and
updated_atmust predate the acting session's first transcriptline. A preceding word character,
., or-prevents a match, as does a followingword character,
-, or.followed by a word character.com.x.helperdoes notname
com.x; a sentence-ending period counts. Evidence keys areccn:<answer id>@<updated_at>. Judge-less grants key on<key>#<scope json>andlook across every tree, so an answer naming two terminals mints two grants, but
another tree cannot mint a fresh grant for the same scope. Live evidence rechecks
the revision before a stored grant spends; editing the answer invalidates that
grant.
capt-hook grant importalso marks cc-notes evidencelive=True.CreatedHereevidence source for terminal closes. The session's own transcriptmust show
orca terminal createas the only command in its Bash call. It mustuse bare
orcawithout an environment prefix or substitution../orcaandother paths do not qualify. The output must return the handle in
result.terminal.handle. Anorca-launch.sh <lane> ...call instead needs itsown output to contain
terminal=<handle>and a matching~/.claude/scratch/orca-launch/*/<lane>.terminal.jsonreceipt with mtime betweencall start minus one second and result plus one second. The worker-list probe drops
ORCA_TERMINAL_HANDLEand checks every Run for a dispatch naming the terminal.tui-idlemust succeed within one second; the screen must show Claude's or Codex'sidle prompt among its last eight nonblank, non-border lines, with no busy marker.
The last spoken line before the prompt must not end in a question mark. The screen
markers come from monorepo's
.agents/skills/orca/scripts/orca-gc.--tabon a literal single-terminal close only when Orca's visual layoutproves that pane is alone; otherwise, say "Drop
--tab". Terminals whose processtrees host no agent still close directly. A batch stays blocked even if a ruling
names every terminal.
is first at top level, the Bash payload text starts with it, and every other
command is a pipe successor.
2>&1 | tail -3is allowed; loops,&∨chainssuch as
cd /tmp && launchctl ..., background&,sh -c, substitutions, andrespelled variables keep the block.
TaskStopruns once as a tool call.launchctl bootout,kickstart,disable,kill,stop, andremove, with no wrapper, substitution, or variable. Targets aresystem/<label>or<domain>/<id>/<label>forgui,user,pid, orlogin;killtakes a signal first, andstopandremovealso accept a bare label.Domain targets,
reboot,asuser,bsexec, andunloadstay blocked, as do allother session-guard denials, including process signals, worktree removal, and
worker-stop.the owner for a cc-notes answer naming the target for a later session. Spent grants,
stale rulings, and failed checks reach the user through
systemMessageprefixedwith
sessions:. Document the owner'scapt-hook grant addpath too.Grants.actionoptional for handlers that pass aProposaltocheck(evt, action).hook(..., grants=...)still requires an action function. A declaration without ajudge mints from its first collected evidence item and accepts a matching stored
grant after validity and rule checks;
Asked()andOwnerWords()without a judgeraise
ValueError.Evidence.livegeneralizes the old cc-notes revision check.Grants.grant(..., across_trees=True)passes that option tostore.mintfor thejudge-less mint. If another tree reaches for the same approval and scope without
adopting the grant, it is refused as a grant covering the scope in another tree.
Grants.replay: timedeltadefaults totimedelta.max;store.retried,store.unusable,store.matching, andstore.reservetake the replay window.Review
An astra finder pass raised nine findings. Fixed: identical retries have a
two-minute window, and loops cannot run a lifted command twice.
./orcacannotforge creation output. Imported grants keep the revision recheck.
com.xdoes notmatch
com.x.helper. A ruling cannot mint a fresh grant for the same scope in everysession tree.
Accepted: owner rulings and self-created idle terminals are separate lifts under
the owner's ruling, not cumulative requirements. A deliberately forged and
backdated launch receipt is outside the threat model: the guard catches agent
mistakes, and the proof also needs the launch call's own output and the receipt
mtime within the call's window.
launchctl removestays liftable as a service-labelunload equivalent to
bootout.Tests
sessions.pyandstops.pycases cover rulings, a stored grant,launchctland
TaskStopallows and denials, answers written during the session, creation andidle checks, dispatch ownership, and lone, crowded, or unreadable tabs. Added rows
allow a launchctl lift piped through
| tail -3and refuse a launchctlforloop,a
cd /tmp && launchctl ...chain, a ruling namingcom.example.orca-serve.helperfor
com.example.orca-serve,./orca terminal createas creation proof, and a loopof terminal closes.
tests/test_pack_sessions.pycovers recorded close andTaskStopspends, a secondclose changed to
--tabreportingwas spent, wrong or late rulings, unreadablecc-notes evidence, blocked batches,
created:<handle>spend evidence, creation andidle refusals, and launch receipts written during versus after the call.
tests/test_grants.pycovers whole-name rulings, minting and exhaustion without ajudge, a deny with an empty reason, stale live evidence,
OwnerWordsneedinga judge, and attachments needing an action. It also covers one judge-less approval
covering each named scope once across trees and the replay window bounding free
retries.
tests/test_guard_literal.pyassertsthat the fast path flags exactly the
TaskStoprows, including the allowed rows.capt-hook --hooks captain_hook/builtin_packs/general/hooks test: 967 passed.pytest -p no:randomly tests/test_grants.py tests/test_pack_sessions.py tests/test_guard_literal.py tests/test_dispatch_mandatory.py: 363 passed.