Skip to content

fix(session-control): let a dispatched session inherit its creator's trust - #8571

Merged
bolichen97 merged 1 commit into
mainfrom
feat/session-create-inherit-trust
Sep 5, 2026
Merged

fix(session-control): let a dispatched session inherit its creator's trust#8571
bolichen97 merged 1 commit into
mainfrom
feat/session-create-inherit-trust

Conversation

@iamwhatever

@iamwhatever iamwhatever commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

A conductor session running with "trust this session" dispatches worker sessions via session_create, and every one of them is born interactive. The worker then blocks on its first tool-approval prompt with nobody watching it — which is precisely the case trust was enabled for. The dashboard's global Trust does not cover it either: that path iterates the slots that exist at click time, so a session dispatched a minute later is untouched.

spawn_run subagents already do the right thing here. SubagentManager's admission gate reads sessions.get_approval_policy(parent) (parent_trusted) and starts the subagent auto-approved. A session_create child took its trust state from _ChatSlot.__init__'s empty defaults instead, so the same delegation behaved differently depending only on whether it got a sidebar tab.

Why it matters

It breaks unattended dispatch. Conductor patrol is autonudge-driven: it wakes itself, finds a worker parked on an approval nobody answered, and the goal makes no progress until a human opens the tab and clicks. The operator already granted the authority; the grant simply did not reach the session doing the work.

What changed (motivation → approach → change)

Root cause: create_session inherits workspace, agent, project dir and folder, and nothing about approval posture — trust is per-slot state with no transfer path.

create_session now carries the creator's session posture to the child, and only that:

  • _trust — the "trust this session" grant.
  • _trust_reads — the same posture narrowed to read-only bash. It has to carry too, or the setting a cautious operator picks is the one whose own workers still stall. It is bounded by construction: what it admits has no side effects.

The session-store half needs no write here. The child has no ACP session yet, where set_approval_policy silently no-ops on a missing session, and chat_runner already assigns the persistable policy from _trust on every session create/resume — so the subagent spawn gate sees it from the child's first turn, through the existing seam rather than a second one.

Two exclusions carry the safety, and they are why this is not just "copy the trust fields":

_trusted_patterns is not inherited. These are per-command grants ("npm test is fine"), not a posture, and that distinction decides it: a pattern is judged against the session the operator was looking at, while a dispatched worker runs model-authored work they have not seen, so the same glob can admit a command the grant was never asked about. Inheriting them could not pay for itself in either direction — with _trust set the child already auto-approves through _slot_is_trusted, so the list was dead weight; and because chat_runner matches patterns independently of _trust, it changed an outcome only when the operator had withheld session trust and approved single commands instead, which is exactly the case that must keep asking. Raised as BLOCKING by the GPT 5.6 lane on 2fd95999 and fixed in 277f98eb; dispositioned in-thread.

_trust_scope is not inherited. It names a TTL-bounded, SEL-audited SafetyOverride scope whose entire value is being re-checked on every approval. Forking the key would hand a second session a credential whose revocation this path cannot observe, so the child would keep auto-approving after the scope that justified it is gone. An unattended worker that needs one gets its own, from whatever owns its lifecycle.

The value transferred is read at allocation, not at entry. create_session suspends three times before the slot exists (project dir, config load, folder confirmation). Reading the entry-time slot would let an operator pick normal mid-call and still have the revoked posture resurrected by a create already in flight. It reads live_caller in the synchronous window after the last re-gate, so revoking mid-call yields an untrusted child — the direction that fails safe.

Nothing is persisted at birth: trust is in-memory by construction, so a restart returns the child to interactive along with its creator. The blast radius is bounded as before — allow_create's per-caller rate window and MAX_SLOTS_PER_CREATOR (50) already exist because session_create is auto-approved for the conductor (#6109).

The create audit records what the child was born with (inherited_trust, inherited_trust_reads) on both outcomes, so an auto-approved tool call in a dispatched session is traceable to the creator's posture instead of appearing unexplained, and "false" is positive evidence it did not transfer.

Tests

8 tests in test/test_session_control.py — five that fail without the change, three negative controls that make those five mean something.

Test Locks in Red against
test_created_session_inherits_the_callers_trust _trust transfers pristine main
test_created_session_inherits_trust_reads the narrower read-only posture transfers, and does not widen to full trust pristine main
test_the_create_audit_records_what_the_child_was_born_with both audit fields present and correct pristine main
test_command_grants_never_transfer_even_under_full_trust _trusted_patterns stays empty on the child even with _trust=True, and the caller keeps its own rev 2fd95999
test_a_pattern_only_creator_produces_a_child_that_still_asks the _trust=False + patterns case from the GPT finding: child born with nothing rev 2fd95999
test_an_untrusted_creator_makes_an_untrusted_child nothing is granted the creator did not hold (without it, a constant True passes the first three)
test_the_scoped_safety_override_grant_is_never_inherited _trust_scope does not fork, and is not laundered into _trust
test_trust_revoked_mid_create_is_not_inherited a posture revoked inside the project-dir resolution is not inherited (the entry-time-read bug)

Red-before verified by reverting session_control.py alone with the tests in place: 3 failed against pristine main, and 2 failed against this branch's first revision.

Manual verification

N/A — unit coverage sufficient. The transfer is two synchronous assignments inside create_session; both consumers of the flags (_slot_is_trusted per approval, _persistable_session_policy for the stored policy) are already covered, and the mid-call revoke window is exercised deterministically by monkeypatching the suspension rather than by timing.

Related Issues

no linked issue: reported directly from a conductor run, so there is no tracked issue for this to close.

Docs

docs/system-specs/modules/session-control.md gains a "What a created child inherits" section — the identity-versus-posture split, the two exclusions and why each is excluded, the allocation-time read, the birth metadata (showing no trust field is persisted), the two audit detail fields, and the transitive-grant gap recorded as a Known gap pointing at #8589. Required in the same commit by AGENTS.md, and raised by the Design Review lane.

Pattern harvest

Rule candidate: review-prompt
Pattern: a control-plane verb that creates a child copies identity-shaped state (workspace, agent, project) and must decide separately about authority-shaped state. Split that authority by whether it is a posture ("auto-approve while I supervise this") or a per-object grant ("this specific command is safe"): a posture describes the supervision relationship and can follow the delegation, while a grant was judged against content the child does not share and must not. Anything revocable or TTL-bounded is excluded from the copy rather than forked. The tell that a copy is wrong: it is redundant in the case where it would be safe, and load-bearing only in the case where it is not.

@iamwhatever
iamwhatever requested a review from a team as a code owner September 4, 2026 22:13
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 8c25d43d60dea778097a7c680c66e164f5db784d — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Aligns dispatched sessions with the parent_trusted precedent; the exclusions (_trusted_patterns, _trust_scope), allocation-time read, and SEL birth record make the widening deliberate, bounded, and traceable.

The one real risk — transitive inheritance with no cascade revoke — is disclosed in the spec, bounded by in-memory trust plus the global picker, and already tracked (#8589), so it is a follow-up, not a defect in this shape. The child-with-a-named-agent case adds nothing new: existing code (chat_handlers.py:513, chat_handlers.py:5386) already reassigns slot.agent without resetting _trust, so trust is per-session posture today and this PR is consistent with that model.

[DESIGN-REVIEWED] 8c25d43

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 8c25d43d60dea778097a7c680c66e164f5db784d — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims in the description verify against the repository: parent_trusted at subagent_manager/admission.py:475-501 reads the stored "auto" policy; _slot_is_trusted and _persistable_session_policy in chat_runner.py are the two consumers exactly as described; set_approval_policy silently no-ops on a missing session (session_allocation.py:983-996), confirming the store-write alternative was correctly rejected; the assignment sits in the synchronous window after the live_caller re-gate. The one other creation path with a posture-holding creator is the fork (chat_fork.py:834), which copies no trust — but a fork is human-driven from the dashboard, so the unattended-stall harm doesn't apply there.

First-Principles-Verdict: PASS

A trusted operator's dispatched worker no longer stalls unattended; every item is derived from that defect or a documented invariant, exclusions included.

What this change ships

Intent: make a dispatched worker session inherit the supervision posture its creator already held, so unattended dispatch stops stalling on approval prompts — a FIX.

  1. A trusted creator's worker now starts trusted — justified (reported conductor defect; parity with spawn_run's existing parent_trusted)
  2. A read-only-trusted creator's worker starts read-only-trusted — justified (same defect, cautious variant; does not widen)
  3. Revoking trust mid-create yields an untrusted child (allocation-time read) — justified, fails safe
  4. Per-command grants explicitly do not transfer — declared exclusion, justified
  5. Scoped SafetyOverride grants explicitly do not transfer — declared exclusion, justified
  6. Create audit records the born-with posture, both outcomes — justified (SEL traceability of auto-approvals, a named boundary)
  7. Trust now flows transitively down a dispatch tree; revoke does not cascade — declared known gap, tracked (Trust inheritance: cascade revocation to dispatched sessions, and say so in the toggle's copy #8589)
  8. Spec section on inheritance — mandated by AGENTS.md same-commit rule

Verification notes: the two audit detail fields are boundary-derived (traceability at the kept SEL control), not speculative surface. Grepped get_or_create_slot( (20 call sites): the only unfixed creator-with-posture path is the fork, whose creator is a present human, so the named harm does not reach it — no sibling demands a general fix. The alternative mechanism (writing the session store at create) is genuinely unavailable: no ACP session exists yet and the write would silently no-op; the change instead feeds the existing chat_runner_persistable_session_policy seam, so nothing is a second spelling.

[FIRST-PRINCIPLES-REVIEWED] 8c25d43

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 8c25d43d60dea778097a7c680c66e164f5db784d and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 8c25d43

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 8c25d43d60dea778097a7c680c66e164f5db784d: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 8c25d43d60dea778097a7c680c66e164f5db784d — this comment is updated in place on each push.

Review details

I've verified the code at session_control.py:1071-1205. The mechanics match the candidate's description exactly: the transfer reads live_caller._trust/_trust_reads (the re-resolved, identity-checked caller after the last gate), copies only those two posture fields, and deliberately excludes _trusted_patterns and _trust_scope.

Falsifying CANDIDATE 1: the "widening" it describes is the deliberate, documented delegation posture — trust following dispatched work, mirroring the existing parent_trusted behavior for spawn_run subagents. The transitivity/revocation behavior is a disclosed "Known gap" (bounded by slot caps, in-memory-only trust, and the global Trust picker), not a mechanical bug. The candidate's own analysis found no bug in the transfer, and its "observable wrong outcome" is the intended behavior, not a defect. It fails (c). The remaining hunks (audit detail, spec) are internally consistent. No grounded new finding survives Step 2.

No findings.

[OPUS-REVIEWED] 8c25d43

Verdict parsed from the review's SHA-scoped output markers for commit 8c25d43d60dea778097a7c680c66e164f5db784d.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 8c25d43d60dea778097a7c680c66e164f5db784d: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@iamwhatever
iamwhatever force-pushed the feat/session-create-inherit-trust branch from 2fd9599 to 277f98e Compare September 4, 2026 23:05
@iamwhatever

iamwhatever commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

span=36bec42fc986 — Session-scoped command grants are delegated to another slot — fixed in 277f98eb374f.

BLOCKING -- src/kiro_crew/dashboard/session_control.py:1189 -- Session-scoped command grants are delegated to another slot
User trusts npm test in a conductor -> auto-approved session_create copies it into a child -> the child executes that command without its own approval.
Fix: Do not inherit _trusted_patterns; initialize the child with an empty set.

Accepted as written. The child is now born with _ChatSlot.__init__'s empty set; the two lines that copied the pattern set are gone, and only _trust / _trust_reads transfer.

What convinced me this was right rather than merely arguable is that inheriting the patterns could not pay for itself in either direction. Where it would have been safe it was dead weight: with _trust set the child already auto-approves every tool call through _slot_is_trusted, so the inherited list changed no outcome at all. Where it did change an outcome, the outcome was wrong: chat_runner matches _trusted_patterns independently of _trust, so the list mattered only when the operator had withheld session trust and approved single commands instead — an operator who has said "ask me", which is precisely the case that must keep asking.

The underlying distinction, now recorded in the code comment: _trust / _trust_reads are a posture ("auto-approve while I supervise this"), and a posture can follow a delegation. _trusted_patterns are per-command grants judged against the session the operator was looking at, and a dispatched worker runs model-authored work they have not seen — so the same glob can admit a command the grant was never asked about.

Two tests pin the exclusion, both red against the previous revision 2fd95999: test_command_grants_never_transfer_even_under_full_trust (a caller with _trust=True and {"npm test", "git status"} produces a child whose _trusted_patterns == set(), and the caller keeps its own) and test_a_pattern_only_creator_produces_a_child_that_still_asks (the _trust=False plus patterns case from the finding's own scenario). The inherited_trusted_patterns audit field was dropped with the behaviour it described.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Design Review 🟡 CONCERNS — "the grant now outlives and outspreads its session" — accepted-and-deferred to #8589.

Revocation doesn't cascade, and inheritance is transitive: a dispatched child is an ordinary slot, so it re-inherits into grandchildren, and revoking the conductor's trust post-create leaves the whole already-born tree auto-approving. … _created_by lineage already exists — a per-slot revoke that offers/performs descendant revocation would close it, and is a fine follow-up rather than a blocker.

The UI click's meaning changed from "trust this session" to "trust this session and its dispatch tree" … make sure the trust toggle's dashboard copy/disclosure catches up so consent matches behavior.

Both watch items are legitimate, and the verdict is right that neither blocks: they are tracked together in #8589 (deferred-finding, assigned, Due: 2026-10-06), because they have to be decided together — if revocation cascades, the toggle's copy has to say so.

Taking the transitive-grant item honestly rather than shrugging at it: this PR does introduce the gap. Before it a child was never trusted, so there was nothing to fail to revoke. What keeps it out of this PR is not that it is small but that the fix needs a ruling I should not make for you. slot._created_by already records the creating session key on exactly this path, so the lineage to walk a subtree exists; the open question is what revoking on a session with descendants should mean — cascade silently, prompt with a count, or stay slot-local and surface the tree. Picking one here would ship a semantics decision inside a stall fix. Two recovery paths bound it meanwhile: the global Trust picker with no slot selected sets normal across every live slot and does clear the whole tree, and trust is in-memory and absent from the birth metadata, so a gateway restart returns the entire tree to interactive.

On the copy item: the scope change is real and the fix is a string, but it lands in the English catalog plus ten locale bundles and turns a two-file backend diff into a UI change carrying a screenshot obligation. It also cannot be written correctly until Task 1 decides whether the click implies a cascading revoke, so writing it now risks shipping copy that the follow-up immediately contradicts. Same issue, stated as its own task with its own acceptance criteria.

One point of agreement worth recording, since it was the part most likely to be wrong: the verdict confirms _trust and _trust_scope are disjoint representations with no laundering path between them, which is the invariant the scoped-credential exclusion depends on. test_the_scoped_safety_override_grant_is_never_inherited asserts both halves — the scope does not fork, and it is not laundered into _trust.

Separately, the pattern-inheritance concern that the GPT 5.6 lane raised as BLOCKING on this same head is fixed in 277f98eb374f, not deferred: _trusted_patterns no longer transfers at all. That narrows this verdict's surface, since the tree can now only inherit a posture, never a per-command grant.

…trust

A conductor running with "trust this session" dispatched workers that were
born interactive. Every one of them then blocked on its first tool call with
nobody watching -- which is the same failure `parent_trusted` already closes
for `spawn_run` subagents, one layer up: a subagent reads the parent's stored
policy and starts auto-approved, while a `session_create` child started from
`_ChatSlot.__init__`'s empty defaults. The dashboard's global Trust only ever
writes the slots that exist when it is clicked, so a session dispatched
afterwards was never covered by it either.

`create_session` now carries the creator's session POSTURE to the child, and
only that: `_trust`, plus `_trust_reads` so that the setting a cautious
operator picks is not the one whose own workers still stall. The session-store
half needs no write here -- the child has no ACP session yet, where
`set_approval_policy` silently no-ops on a missing session, and `chat_runner`
already assigns the persistable policy from `_trust` on every session
create/resume, so the subagent spawn gate sees it from the child's first turn.

Two exclusions carry the safety, and they are the reason this is not simply
"copy the trust fields":

`_trusted_patterns` is NOT inherited. These are per-command grants ("`npm
test` is fine"), not a posture, and the distinction decides it: a pattern is
judged against the session the operator was LOOKING at, while a dispatched
worker runs model-authored work they have not seen, so the same glob can admit
a command the grant was never asked about. Inheriting them also buys nothing
where it would be safe -- with `_trust` set the child already auto-approves via
`_slot_is_trusted`, so the pattern list is dead weight -- and changes the
outcome ONLY when the operator withheld session trust and approved single
commands instead, which is exactly the case that must keep asking, because
`chat_runner` matches patterns independently of `_trust`.

`_trust_scope` is NOT inherited. It names a TTL-bounded, SEL-audited
`SafetyOverride` scope whose whole value is being re-checked on every approval;
forking the key would hand a second session a credential whose revocation this
path cannot observe, so the child would keep auto-approving after the scope
that justified it is gone. An unattended worker that needs one gets its own,
from whatever owns its lifecycle.

The value that transfers is read off `live_caller`, in the synchronous window
after the last gate -- not off the entry-time slot. `create_session` suspends
three times before the slot exists (project dir, config load, folder
confirmation), and an operator picking `normal` in any of those windows would
otherwise have a revoked grant resurrected by a create already in flight.
Revoking mid-call now yields an untrusted child, which is the direction that
fails safe.

Nothing is persisted at birth: trust is in-memory by construction, so a
restart returns the child to interactive along with its creator. The blast
radius stays bounded as before by `allow_create`'s per-caller rate window and
`MAX_SLOTS_PER_CREATOR`.

The create audit records what the child was born with (`inherited_trust`,
`inherited_trust_reads`) on both outcomes, so an auto-approved tool call in a
dispatched session is traceable to the creator's posture instead of appearing
unexplained, and "false" is positive evidence it did not transfer.

Tests: 8 in test_session_control.py. Three fail against pristine main (trust
inherited, trust_reads inherited, audit fields present); two more fail against
the first revision of this branch, pinning the pattern exclusion (no command
grant transfers even under full trust, and a pattern-only creator's child still
asks); three are negative controls -- an untrusted creator produces an
untrusted child, a scoped grant never forks, and a posture revoked inside the
project-dir resolution is not inherited.

Pattern harvest
Rule candidate: review-prompt
Pattern: a control-plane verb that creates a child copies identity-shaped state
(workspace, agent, project) and must decide separately about authority-shaped
state. Split that authority by whether it is a POSTURE ("auto-approve while I
supervise") or a per-object GRANT ("this command is safe"): a posture describes
the supervision relationship and can follow the delegation, while a grant was
judged against content the child does not share and must not. Anything
revocable or TTL-bounded is excluded from the copy rather than forked.
@iamwhatever
iamwhatever force-pushed the feat/session-create-inherit-trust branch from 277f98e to 8c25d43 Compare September 4, 2026 23:27
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Design Review 🟡 CONCERNS — "silently widens what the click grants, with no spec or UI update" — fixed (spec) + accepted-and-deferred (UI copy) in 8c25d43d60de.

the dashboard's "trust this session" affordance and docs/system-specs/modules/session-control.md (which enumerates exactly what a created child inherits, and the create audit's detail fields) both still describe the old semantics. AGENTS.md requires the spec update in the same commit; land it here, and disclose the propagation where the trust toggle lives.

Correct on both counts, and the spec half was a genuine hole in this diff rather than a follow-up: AGENTS.md line 30 requires the module doc to be updated in the same commit as what it documents, and session-control.md described creation as copying workspace, agent, project dir and folder with no mention of approval posture at all. Landed here.

docs/system-specs/modules/session-control.md now carries a "What a created child inherits" section that splits the two kinds of copied state (identity versus approval posture), states the parent_trusted parity, gives the two exclusions and why each one is excluded in a table, states that the posture read happens at allocation rather than entry and what that buys, enumerates the birth metadata to show no trust field is persisted, and documents the create record's inherited_trust / inherited_trust_reads fields including that they are written on both outcomes. Verified with docs-lint (261 files, all checks passed) and the brand gate.

It also records the transitive-grant gap as a Known gap in the spec rather than leaving it only in a tracker, with the three things that bound it (slot caps, in-memory-only trust, and the global Trust picker clearing every live slot) and a pointer to #8589. A gap that is real but accepted should be legible to the next reader of the spec, not just to whoever finds the issue.

The UI disclosure half stays deferred to #8589 (deferred-finding, assigned, Due: 2026-10-06), and the reason is sequencing rather than effort: the string cannot be written correctly until that issue's first task rules on whether revoking cascades, so writing copy now risks shipping a promise the follow-up contradicts. It also lands in the English catalog plus ten locale bundles, which turns a backend diff into a UI change carrying a screenshot obligation. #8589 names it as its own task with its own acceptance criteria, and requires the two to agree.

On the suggestion — a follow-up "untrust this session and its workers" that walks _created_by, rather than switching to live re-check inheritance — agreed, and that is the shape #8589's Task 1 proposes. Live re-check would mean the child consulting its creator's grant on every approval, which reintroduces the lifetime coupling the allocation-time read exists to avoid and would make a closed creator tab ambiguous.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 5, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@bolichen97
bolichen97 merged commit 6424dc2 into main Sep 5, 2026
66 of 72 checks passed
@bolichen97
bolichen97 deleted the feat/session-create-inherit-trust branch September 5, 2026 00:26
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants