Skip to content

feat(conductor): op-shaped dashboard tools + durable inbox/dispatch record - #6237

Closed
iamwhatever wants to merge 1 commit into
mainfrom
feat/conductor-script-dispatch
Closed

feat(conductor): op-shaped dashboard tools + durable inbox/dispatch record#6237
iamwhatever wants to merge 1 commit into
mainfrom
feat/conductor-script-dispatch

Conversation

@iamwhatever

@iamwhatever iamwhatever commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What

Two changes that share one design note (docs/request-for-change/rfc-conductor-op-tool-and-script-boundary.md):

  1. kirocrew-dashboard advertises two op-shaped tools instead of eight verbs.
    chat_folder_ctl(op, args) and session_ctl(op, args), same shape as the
    existing browser tool. The eight handler bodies are untouched — each is the
    same endpoint call validated by the same per-op schema — so this is a surface
    change, not a rewrite.
  2. scripts/queue.py in the goal-conductor skill: the durable inbox and
    dispatch record.

Why the surface change is worth it

tools/list is read once per session, so every stub is a standing context cost
for that whole session whether or not the capability is ever used. Six of the
eight go away. The per-op detail moves into the skill, which is loaded on demand.

The alternative that motivated this — moving session control out of MCP into
bundled scripts — was rejected on a boundary that is easy to miss and is written
down in the RFC: every identity source the strict resolver accepts is injected by
the gateway into the MCP server process, and a child of execute_bash has
none of them (measured: KIROCREW_SESSION_KEY absent, KIROCREW_HOST_PID
present). A script could therefore only assert which session it is. The op
shape gets the stub reduction that idea was after without giving up a live
authorization check: under session_ctl the tool-call hook still sees a tool name
plus real args, so it can apply per-op rules; under a script it would see one
execute_bash python3 … and the arguments would be inside the process.

Why TWO tools and not one

Collapsing all eight into one name looked cleaner and is wrong. Channel
containment (channel.CHANNEL_AGENT_BLOCKED_TOOLS) matches on the tool name
against rendered permission-request text and never sees an op. One merged name
would make that block all-or-nothing: either a channel agent regains session
control — the exact regression the old hand-copied per-site list already caused
once, where session_create was identity-gated but reachable from a channel — or
it loses the folder organization it has today. Splitting on the capability class
keeps the block exact and still removes six of the eight stubs.

Two consequences are implemented rather than left to prose:

  • The caller-identity gate runs on the resolved op. Gating on the op-shaped
    name would gate folder work too; gating before translation would leave every
    session op ungated. Its list (SESSION_CONTROL_INNER) is derived from the
    op→handler map, so an op added later is identity-gated the moment it is mapped —
    that derivation replaces the hand-copied list whose per-site drift caused the
    earlier regression.
  • The advertised enum, the op→handler map, and the refusal text for an unknown op
    all read the same two tuples, so an op cannot be half-added.

queue.py — and what it does NOT do

It holds the two things that previously lived only in the skill's prose, and
therefore only in context, which is exactly what compaction takes away:

  • A mid-flight user message. The skill applies a goal change at the round
    boundary; between arrival and that boundary the message lived nowhere. Now
    enqueue parks it, claim drains it at the boundary, done drops what was
    applied. A claim does not delete — a turn that dies after claiming leaves
    the message recoverable, and a later claim re-serves anything claimed longer ago
    than stale_secs. A full inbox and an oversize message both refuse: silently
    dropping a message the user typed is the failure this exists to prevent, and half
    a steering instruction can invert its meaning.
  • Whether an item was already dispatched. dispatch_begin pre-assigns the id
    and returns the same id on a retry (a fresh id per attempt is what opens two
    sessions for one item), flagging the window where a session may exist with no
    seed — which otherwise looks exactly like a session that is merely quiet.

Stated plainly, in the script's own docstring and in the RFC: dispatch is
detectable-and-convergent, not atomic. The create/send pair are MCP calls
the model makes, so only moving dispatch into gateway-side code would make a
duplicate impossible. That is deliberately out of scope here.

It touches no identity by construction — no session key, no gateway
credential, no SEL trust root, no socket, no subprocess. That is the property that
makes it safe as a script rather than a tool, so it is pinned by a source ratchet
rather than trusted to review.

Docs

  • docs/system-specs/modules/session-control.md described the four session verbs
    as the advertised surface, and its table omitted send entirely — a gap that
    predates this change. It now documents the ops, their routes, and their internal
    handler names (unchanged, and still what the audit trail and error strings say).
  • docs/architecture/mcp.md listed only the four folder tools for this server.

Tests

New and updated:

  • Op routing for all eight verbs against an explicit expected map (a test that
    read _OP_TO_INNER to check _OP_TO_INNER would pass on any mapping).
  • Unknown op refused by name, listing the valid ops, without touching the
    gateway
    ; missing op the same; non-object args refused.
  • Identity-gate ordering in both directions: a session op with an unverifiable
    caller is refused and never posts; a folder op is not caught by that gate.
  • Per-op schema still validates (op="send" with no message raises).
  • Containment: matcher cases for every rendered form of session_ctl, coverage of
    all four ops by that one name, and the deliberate non-block of chat_folder_ctl.
  • 26 cases for queue.py, including the two properties that matter — a claimed
    message survives a turn that never calls done, and a retried
    dispatch_begin converges on one id — plus goal-id traversal refusals and the
    no-identity source ratchet.

Local: 9,091 passed in the affected area (mcp / channel / conductor / validation /
session_control / skill). flake8, isort, mypy clean. New files are black-clean; the
six pre-existing files this touches are already in .github/black-baseline.txt and
fail identically on a clean main, so no new offender is introduced and no
unrelated reformatting is carried.

Post-open review: measured, and two defects fixed

The claims above were asserted, so they were checked. Head moved 541c0d40178a211e90.

1. The advantage is real but smaller than "8 → 2" sounds. Measured on the real
_tool_definitions() output at both commits:

tools tools/list payload ≈ tokens
before 8 6,956 B ~1,930
after 2 4,523 B ~1,260
delta −6 −2,433 B (−35%) ~−676

The stub count fell 75%; the cost fell 35%. The gap is deliberate — the two
descriptions keep enough per-op detail to be driven by an agent carrying no
conductor skill — but "six of eight stubs go away" must not be read as a 75%
saving. The RFC now carries this table instead of the earlier unquantified claim.

2. The authorization claim holds, with one honest limit. hooks.py's
on_tool_call(tool_name, *, raw_params: dict | None = None, …) does receive real
arguments, so a per-op rule is expressible under one tool name. Nothing today keys
a rule on op; the value is that the op shape KEEPS that possible where a script
would foreclose it. Also verified: no governance, security, config or frontend
surface keyed on the eight retired tool names, so nothing became dead config.

3. Two real defects in queue.py, found by reviewing it adversarially rather
than re-reading the design.
Both are mutation-verified — the fix reverted, the
new test observed to fail, the fix restored:

  • Silent data loss. _read returned a BLANK record for any OSError, and
    every mode then wrote that blank back — so a state file that merely could not be
    read this time was overwritten, destroying exactly the parked messages the
    script exists to keep. Now only absent reads as blank; unreadable raises and
    is surfaced as state_unreadable with the bytes left untouched. Mutation: with
    the old behaviour restored the new test fails assert True is False, i.e. the
    enqueue succeeded in overwriting.
  • Unbounded busy loop. _Lock.__enter__ had except OSError: continue around
    the staleness stat, skipping both the deadline check and the sleep — a stat
    that keeps failing spins a core forever instead of answering locked. It now
    falls through to the deadline and, deliberately, does not steal a lock whose
    staleness it cannot judge. Mutation: with the old code restored the new test is
    killed by a 25s timeout; with the fix it completes in under a second.

Five new cases cover both, plus the stale-lock steal that keeps a crashed holder
from wedging a goal forever. test_conductor_queue.py is now 31 cases; the
affected-area suites are 229 passed; flake8, isort, mypy and black clean on both
new files.

@iamwhatever
iamwhatever requested a review from a team as a code owner August 27, 2026 06:43
@iamwhatever
iamwhatever requested a review from Zedmor August 27, 2026 06:43
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 27, 2026
…ecord

The kirocrew-dashboard server advertised eight tools -- four folder verbs and
four session verbs -- and `tools/list` is read once per session, so every stub is
a standing context cost for that whole session whether or not the capability is
used. Collapse them into two op-shaped tools, `chat_folder_ctl(op, args)` and
`session_ctl(op, args)`, the same shape the `browser` tool already uses. The
eight handler bodies are untouched: each is still the same endpoint call
validated by the same per-op schema, so this is a surface change.

TWO tools rather than one, deliberately. Channel containment
(`CHANNEL_AGENT_BLOCKED_TOOLS`) matches on the tool NAME against rendered
permission text and never sees an `op`, so one merged name would make that block
all-or-nothing: either a channel agent regains session control -- the exact
regression the old hand-copied per-site list already caused once -- or it loses
the folder organization it has today.

Two consequences are implemented rather than left to prose:

* The caller-identity gate runs on the RESOLVED op. Gating on the op-shaped name
  would gate folder work too; gating before translation would leave every session
  op ungated. Its list (`SESSION_CONTROL_INNER`) is derived from the op->handler
  map, so an op added later is identity-gated the moment it is mapped.
* The advertised enum, the op->handler map, and the refusal text for an unknown
  op all read the same two tuples, so an op cannot be half-added.

Also adds `scripts/queue.py` to the goal-conductor skill: the durable inbox and
dispatch record. It holds the two things that previously lived only in the
skill's prose, and therefore only in context, which is what compaction takes
away -- a mid-flight user message parked until the round boundary, and whether an
item was already dispatched. `dispatch_begin` returns the SAME id on a retry and
names the unseeded window; a `claim` does not delete, so a turn that dies after
claiming leaves the message recoverable. Dispatch is detectable-and-convergent,
NOT atomic: the create/send pair are MCP calls the model makes, so only
gateway-side dispatch would make a duplicate impossible, and that is left out of
scope. The script reads no identity, opens no socket and runs no subprocess --
pinned by a source ratchet, since that is the property that makes it safe as a
script rather than a tool.

Docs: `docs/system-specs/modules/session-control.md` described the four session
verbs as the advertised surface and its table omitted `send` entirely (a gap that
predates this change); it now documents the ops, their routes and their internal
handler names. `docs/architecture/mcp.md` listed only the four folder tools for
this server. Design of record:
`docs/request-for-change/rfc-conductor-op-tool-and-script-boundary.md`.

Tests: op routing for all eight verbs against an explicit expected map, unknown
op refused without touching the gateway, non-object `args` refused, identity gate
ordering in both directions, per-op schema validation, containment coverage by
tool name plus the deliberate non-block of `chat_folder_ctl`, and 26 cases for
queue.py including the message-survives-a-dead-turn and
retry-converges-on-one-id properties.
@iamwhatever
iamwhatever force-pushed the feat/conductor-script-dispatch branch from 541c0d4 to 78a211e Compare August 27, 2026 06:48
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 27, 2026
@bolichen97
bolichen97 enabled auto-merge August 29, 2026 23:59
@bolichen97 bolichen97 added needs-pr-triage PR scanner: awaiting automated triage needs-author-decision PR blocked on author input and removed needs-pr-triage PR scanner: awaiting automated triage labels Aug 30, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: bolichen97#bb3ad1ca]: This PR was flagged for triage with a merge conflict. I reviewed the blockers but they require your input:

  • Conflict in src/kiro_crew/builtin_skills/goal-conductor/SKILL.md + the conductor prompt in src/kiro_crew/agent.py: main's fix(conductor): one plan gate, required goal folder, rate-bounded dashboard trust #6109 (merged shortly after this branch was cut) added a per-verb-NAME auto-approval policy — chat_folder_tree, chat_folder_create, session_create, session_read_message auto-approved; chat_folder_move_session, session_send, session_stop deliberately NOT auto-approved. This PR collapses all eight verbs into session_ctl/chat_folder_ctl, so name-based approval can no longer draw that line. How the approve-some/prompt-some split maps onto the op-shaped surface (per-op gating in the hook? which ops stay prompted?) is a security-semantics design decision that only you can make — it also likely belongs in the RFC this PR adds.
  • Conflict in docs/request-for-change/README.md: both sides added RFC index rows; your row's status text ("shipped") also needs restating post-rebase.
  • Main additionally rewrote the same conductor prompt/SKILL passages in verb-tool vocabulary (fix(conductor): guidance that dispatches work and names tools, not internals #6377, fix(conductor): reach the verbosity setting, patrol with monitor_start not wait #6238, plus the new session_ledger_read/session_ledger_record tools) — re-expressing that guidance in the op-shaped vocabulary is re-authoring your design text, which I won't do on your behalf.

When you've rebased and settled the approval-mapping question, the pipeline will re-assess on its next cycle.

@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.

Description / code mismatch

The Description presents the new queue.py test module as complete, passing evidence for the script, but the module is POSIX-only and fails at import time on Windows, where the backend test job collects it.

1. New queue.py test module is POSIX-only and errors the Windows backend-test job at collection

The Description says

26 cases for queue.py, including the two properties that matter — a claimed message survives a turn that never calls done, and a retried dispatch_begin converges on one id — plus goal-id traversal refusals and the no-identity source ratchet.

and, for the evidence behind it:

Local: 9,091 passed in the affected area (mcp / channel / conductor / validation / session_control / skill). flake8, isort, mypy clean.

The code does — the module reaches for POSIX-only behaviour on the import and collection path rather than inside a guarded test. test/test_conductor_queue.py:269 opens the unreadable-state case, state.chmod(0o000) at test/test_conductor_queue.py:274 relies on POSIX mode bits meaning "unreadable" (a 0o000 file remains readable by its owner on Windows, so the case cannot express what it asserts), and the root check that would otherwise skip it calls os.geteuid() at test/test_conductor_queue.py:52 — an attribute that does not exist in os on Windows, so it raises AttributeError where it is evaluated. test/test_conductor_queue.py:89 compares state_path against a /-separated suffix via endswith, which never matches a Windows path built with \.

Nothing excludes the module from the Windows run. test/conftest.py:142 builds collect_ignore from test/windows-collect-ignore.txt — the repository's documented mechanism for exactly this case, whose own header records that a skip marker cannot prevent an import-time failure — and test_conductor_queue.py is not listed in it.

Risk — module import happens before pytest-split deselects by --group, so every Windows shard errors, not only the one that would own this file. A collection error takes the rest of that shard's work with it. No backend or lint check-run exists on this head SHA, so nothing in CI has contradicted the Description's local-only pass claim yet; the breakage stays invisible until the PR becomes mergeable and CI actually runs. AGENTS.md states the backend supports Windows natively, and ci.yml documents the Windows job as the line that keeps a POSIX-only regression from landing silently.

Required change — either make the module run on Windows or make the Description's claim conditional, and in code:

  • guard the module (or the three affected tests) with pytest.mark.skipif(sys.platform == "win32", …), following the existing _POSIX_ONLY precedent;
  • replace os.geteuid() with getattr(os, "geteuid", lambda: 1)(), the pattern already used at test/test_tailnet_peer.py:557;
  • compare state_path using Path(...) parts rather than a /-separated endswith.

Adding the file to test/windows-collect-ignore.txt is the alternative if the cases are meant to stay POSIX-only, but the skip guard is the narrower fix because it keeps the platform-independent cases running there.

@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • This PR is OVERLAPPING with PR #5819. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. Two independent cross-session delivery surfaces with different gates; whoever lands second should say why both exist and whether channel containment covers the new one. Files: src/kiro_crew/validation.py.
  • This PR is OVERLAPPING with PR #6109. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. Main's per-verb-name auto-approval landed after this branch was cut and cannot be expressed under one tool name; the mapping is an author-owned security decision, which is why the PR carries the needs-author-decision label. Files: src/kiro_crew/agent.py.
  • This PR is OVERLAPPING with PR #6146. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. The op-shaped create must carry PR #6146's folder argument and the skill must keep the single-call filing, or a shipped atomic-filing fix is reverted. Files: src/kiro_crew/builtin_skills/goal-conductor/SKILL.md.
  • This PR is OVERLAPPING with PR #7160. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. A fifth session verb shipped after the branch was cut and must become a fifth op; as written the collapse silently removes it. Files: src/kiro_crew/mcp_dashboard.py.
  • This PR is OVERLAPPING with PR #7238. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. A third per-verb grant list must be mapped onto the op shape at the same time as the conductor's. Files: src/kiro_crew/agent.py.
  • This PR is OVERLAPPING with PR #8153. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. A second per-verb-name grant list now depends on the names this PR retires, and it rewrote a doc the PR also rewrites. Files: src/kiro_crew/agent.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Closing this in favour of #8489, which lands the half of it that is still valid.

What is being kept. scripts/queue.py — the durable inbox and dispatch
record — is rebased onto current main and opened on its own as #8489. It is
independent of the tool-shape change and nothing on main supersedes it: the
skill still ships only accept_eval.py and ledger_entry.py, so a mid-flight
user message and a half-finished dispatch still live nowhere but the conductor's
context. The review comments on the test file are addressed there (the Windows
portability defects in particular — the suite now runs on Windows rather than
being excluded, since O_EXCL over flock was the point).

What is being abandoned, and why. The other half collapsed the eight
session-control verb tools into op-shaped session_ctl(op, args) and
chat_folder_ctl(op, args). That was a reasonable trade when this branch was
cut. It is not now: #6109 built per-verb auto-approval that matches on tool
NAME, and channel containment matches on NAME too. Collapsing the names does not
just rename a surface — it re-opens the approve-some/prompt-some decision those
mechanisms encode, across ~16 files plus two conductor prompts and two
SKILL.md files. That is a security decision worth its own RFC and its own
review, not a rider on a queue script.

The RFC document from this branch is not carried over. If the op-shaped surface
is worth revisiting, it should start from main's current auto-approval model
rather than from a pre-#6109 premise.

@iamwhatever iamwhatever closed this Sep 4, 2026
auto-merge was automatically disabled September 4, 2026 16:29

Pull request was closed

@github-actions github-actions Bot removed the readiness: checking Automated validation is still running label Sep 4, 2026
@bolichen97
bolichen97 deleted the feat/conductor-script-dispatch branch September 6, 2026 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Branch has merge conflicts with its base — author must resolve before merge needs-author-decision PR blocked on author input

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants