Skip to content

feat(session-control): add agent.member_dispatch operator ceiling - #9166

Merged
bolichen97 merged 1 commit into
mainfrom
fix/members-dispatch-8073
Sep 8, 2026
Merged

feat(session-control): add agent.member_dispatch operator ceiling#9166
bolichen97 merged 1 commit into
mainfrom
fix/members-dispatch-8073

Conversation

@chenmingwei23

Copy link
Copy Markdown
Contributor

What is the problem?

A crew member's DM session is a conductor by design: it dispatches work into
worker sessions it creates and patrols, with zero configuration. To make that
work, a member caller bypasses the global agent.session_control switch at both
_member_caller switch-bypass gates in src/kiro_crew/dashboard/session_control.py.
The consequence, flagged by the Design and First Principles lanes on the original
work: an operator who explicitly turned session control off has no lever short of
disabling the member entirely to keep that member's thread chat-only. There was
no separate switch for the member bypass.

Why it matters to the user

An operator who withdraws session control is making a deliberate statement that
agents may not open and drive each other's sessions. Today that statement is
silently not honoured for crew members, and the only workaround is a blunt one
(disable the member), which also removes the member's legitimate chat use. The
operator needs a narrow ceiling: keep the member as a chat participant while
denying it the dispatch bypass.

How the fix solves it

Add agent.member_dispatch (bool, default true), a sibling of
agent.session_control and agent.member_acp_backend under AgentConfig.

  • Symptom: an operator cannot restrict member dispatch without disabling the
    member.
  • Root cause: the member switch-bypass at the two gates is unconditional -- it
    keys only on the immutable member slot-key prefix (_member_caller), with no
    operator input.
  • Fix: a shared _member_bypass(caller_key) helper gates the bypass on
    _member_caller(caller_key) AND member_dispatch_enabled(). Both bypass gates
    (create_session and authorize_target) call the one helper, so the condition
    cannot drift between them. member_dispatch_enabled() reads
    agent.member_dispatch at the gate, synchronously, right beside the existing
    session_control_enabled() read.

Default true reproduces today's behaviour exactly, so installing this change
alters nothing until an operator opts in. Set false, a member caller stops
bypassing and falls back under agent.session_control like any ordinary caller.
The knob reads through _safe_bool (so a quoted "false" disables the bypass
rather than loading as enabled), and fails closed in the same direction
session_control_enabled() does: an unreadable config withdraws the member
bypass rather than granting it. The creator-ownership fence
(_caller_is_ownership_fenced, and the fence-reason selector) is deliberately
untouched -- it binds a member to sessions it created itself either way, even
when the global switch is on.

On the two questions a permission-gate change must answer:

  • The property the gate keys on is the one that decides the answer. The
    bypass is decided by two things read together at the gate: whether the caller
    is a member (immutable slot-key prefix) and whether the operator ceiling is on
    (config, read synchronously right before the act). Neither is a proxy for the
    bypass; both are inputs to it, evaluated in the same synchronous window as the
    existing switch read beside them.
  • What this makes newly reachable: nothing. The default is today's behaviour,
    and the only new state (member_dispatch: false) removes a bypass -- it makes
    a member more restricted, never less. This is not an opt-in that unlocks a path;
    it is an opt-in that closes one.

On the enterprise scope question the issue raised ("consider whether an enterprise
SCOPE_CATALOG scope should cover it"): a lookup, not a new decision.
session_control itself is a plain agent.* bool with no SCOPE_CATALOG
entry, and no existing catalog scope reaches the session-control surface. A scope
would need a new governance enforcement seam rather than a data-only append, which
is out of scope for a minimal ceiling. Left to a follow-up, and stated as such in
the module doc.

Note on the issue's second half (schema-parity test)

The issue asks for a second thing: a WORKER_*_SCHEMA subset-of SESSION_*_SCHEMA
parity test. That half's premise is gone on main. src/kiro_crew/mcp_tools/workers.py
and the worker_* tools were part of PR #8021's shape; #8021 was closed by
maintainer decision, and the member-dispatch feature landed instead via #8153 with
a per-session session_* mount. There is no second schema to hold parity with, so
this PR does not implement it. This is called out rather than silently omitted.

Note on the knob's spelling

The issue names the knob members.dispatch, written against #8021's shape. On the
landed #8153 design there is no members: config namespace -- member configuration
lives on agent.* (agent.member_acp_backend, agent.session_control). The
in-tree-consistent spelling is therefore agent.member_dispatch, a sibling of the
switch it ceilings. Today's behaviour is expressible as its default (true), so
the knob's shape is right.

What tests we did

Scoped pytest -n0 on the touched files and their session-control consumers:

  • test/test_member_session_control.py (15 tests, 6 new in
    TestMemberDispatchCeiling): the _member_bypass truth table (member AND
    ceiling-on required; non-member never bypasses), member_dispatch_enabled()
    reading the field and failing closed on a raising config read, a member falling
    back to session_control_disabled at authorize_target when the ceiling is off
    and the switch is off, the default behaviour preserved (ceiling on + switch off
    still passes the config gate), and the config default being true. The existing
    member-bypass tests were made robust to the new ceiling by pinning
    member_dispatch_enabled=True in their shared helper (they assert the default).
  • test/test_session_control.py, test/test_session_control_boundaries.py,
    test/test_cron_session_control.py (211 tests): the gate change touches shared
    code; these consumers still pass.
  • test/test_config_baseline.py, test/test_config_loader.py (503 tests): the new
    field round-trips through load and the committed config-baseline.json snapshot
    was regenerated (scripts/generate_config_baseline.py); the diff is exactly the
    one new agent.member_dispatch entry (+15 lines), nothing else.

black --target-version py310, isort, flake8 clean on all four changed Python
files; mypy clean on the three changed source files. On the security posture:
this is a permission-ceiling change, so I attempted the organizational
security-guidance search; it is unreachable from this session. The query I would
have run is for least-privilege operator config ceilings on an automatic grant. The
change only ever tightens (default = current behaviour, opt-in removes a bypass),
and the ceiling fails closed.

Why no screenshot: backend config + gate wiring, a docs spec update, and a unit
test. No website/ surface is touched and there is no rendered delta.

Any other suggestions on the work

  • The enterprise SCOPE_CATALOG scope for member dispatch is a real follow-up, but
    it needs a governance enforcement seam (a chokepoint that reads the scope), not a
    catalog append -- the same reason session_control is not a catalog scope today.
  • If a future change adds a members: config namespace, agent.member_dispatch
    would be the natural thing to alias/move there; until then the agent.* sibling
    is the consistent home.

Pattern harvest

Defect class avoided: a permission-gate knob whose default silently shifts an
existing deployment's behaviour, or whose gate keys on a mutable proxy. Here the
default is pinned to today's behaviour (verified by a test asserting
AgentConfig().member_dispatch is True and by the config-baseline diff being the
single new entry), the two gates share one extracted predicate so they cannot
drift, and the new state only ever tightens so nothing is made newly reachable.

Rule candidate: when adding an operator ceiling on an existing automatic grant,
prove three things in the PR and in a test -- (1) the default equals current
behaviour so install is a no-op, (2) the two-or-more enforcement sites share one
extracted predicate rather than copy-pasted conditions, and (3) the new
configurable state removes reach rather than adding it. If the current behaviour
cannot be expressed as a default value of the knob, the knob's shape is wrong.

Refs #8073

@chenmingwei23
chenmingwei23 requested a review from a team as a code owner September 7, 2026 00:38
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

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

Design-Verdict: CONCERNS

Sound ceiling, but its fail-closed machinery is bespoke to the new knob, leaving the sibling session_control with the exact fail-open modes this PR names.

Watch

  • The docstring claims parity ("fails closed … the same direction session_control_enabled does"), but only member_dispatch_enabled() checks degraded_sections and only member_dispatch gets the pre-validation coercion. By the PR's own mechanism ("load() … falls back to the field default … a degraded agent overlay carrying member_dispatch: false would silently revert"), a degraded agent section or a validation-popped malformed value silently reverts an explicit session_control: false to enabled — the identical governance-ceiling fail-open, one line away in session_control_enabled() (src/kiro_crew/dashboard/session_control.py:346). Two operator switches read side by side at the same gate now have different corruption semantics; either extend the same treatment to the sibling or record the gap where the follow-up scope note already lives.
  • The fail-closed treatment is three hand-coordinated pieces (inline coercion before _validate_config_data, _safe_bool, degraded-section check) that every future fail-closed bool must replicate by hand; the inline special case in load() is where the next one drifts.

Suggestions

  • Fold the three pieces into one shared "fail-closed bool" path (e.g. field metadata driving coercion + a degraded-aware read helper) and point both switches at it — a small follow-up that deletes the special case.

[DESIGN-REVIEWED] ebd2794

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

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

Review details

No findings.

[OPUS-REVIEWED] ebd2794

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

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

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of ebd2794c873f1af923a3fdece9203ebea6266a59 — 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 checks are done — the change is well-derived, with one counted sibling gap worth flagging. Final review:

First-Principles-Verdict: CONCERNS

The ceiling is derived and tightens-only, but it fixes a fail-open class for the new knob while leaving the same two fail-opens on agent.session_control itself.

What this change ships

Intent: let an operator who turned session control off keep a crew member's DM thread chat-only without disabling the member — an ADDITION, derived from a review-lane defect on the landed member-dispatch work.

  1. New config key agent.member_dispatch, default true (today's behaviour) — justified
  2. Set false, a member falls under the Session Control switch like any caller — justified (the fix itself; operator-above-agent boundary)
  3. A malformed value for the new key withdraws the bypass instead of riding the permissive default — justified
  4. An unreadable or degraded agent config withdraws the bypass — justified, but see Watch
  5. Both switch gates now share one extracted _member_bypass predicate — mechanism-level, justified
  6. Spec section + regenerated config-baseline.json entry — mandated (same-commit spec rule; generated snapshot)

Watch

  • Point patch, 1 counted unfixed sibling: agent.session_control — the very switch this knob ceilings — has both fail-opens the PR closes for member_dispatch. A quoted "false" is a schema type violation, _apply_field_default pops it (agent.session_control is not in _FAIL_CLOSED_PATHS, validation.py:178), and _safe_bool(agent_data.get("session_control", True), True) (loader.py:2850, the only hit, no pre-validation coercion) reloads it as enabled against an explicit opt-out; and session_control_enabled() (session_control.py:346-352) never checks degraded_sections, so a discarded agent section carrying session_control: false silently re-enables the surface. The description's "fails closed in the same direction session_control_enabled() does" overstates the parity — the new gate goes strictly further than the sibling it cites.

[FIRST-PRINCIPLES-REVIEWED] ebd2794

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

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

This comment is updated in place on each push.

Review details

FINDING -- src/kiro_crew/dashboard/session_control.py:411 -- "right before the act" contradicts create_session, which awaits before allocation -> Fix: describe this as an entry-time switch check.
[GPT-REVIEWED] ebd2794

False positive or not applicable? A repository writer can comment:
/ai-review override gpt ebd2794c873f1af923a3fdece9203ebea6266a59: <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 7, 2026
@chenmingwei23
chenmingwei23 force-pushed the fix/members-dispatch-8073 branch from a2e7ce5 to 827d7b0 Compare September 7, 2026 00:56
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 7, 2026
Member DM sessions bypass the agent.session_control switch by design (the
zero-configuration crew-member operating model). An operator who turned
session control off had no lever short of disabling the member to keep it
chat-only. Add agent.member_dispatch (bool, default true) consumed at both
_member_caller switch-bypass gates via a shared _member_bypass helper: at
its default the bypass is unchanged (today's behaviour), set false the
member falls back under agent.session_control. Reads at the gate through
_safe_bool and fails closed, the same direction session_control does. The
creator-ownership fence is untouched and still binds a member either way.

Refs #8073
@chenmingwei23
chenmingwei23 force-pushed the fix/members-dispatch-8073 branch from 827d7b0 to ebd2794 Compare September 7, 2026 01:16
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 7, 2026
@bolichen97
bolichen97 merged commit 262c51b into main Sep 8, 2026
91 of 92 checks passed
@bolichen97
bolichen97 deleted the fix/members-dispatch-8073 branch September 8, 2026 06:39
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 8, 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.

3 participants