Skip to content

Feat/secure room - #295

Open
zonblade wants to merge 95 commits into
mainfrom
feat/secure-room
Open

zonblade wants to merge 95 commits into
mainfrom
feat/secure-room

Conversation

@zonblade

Copy link
Copy Markdown
Member

No description provided.

The secure assistant replaces /security with an Orpheus-style room: a Chief
that delegates to specialist avatars, who share findings through a shelf.
This lands the parts that do not touch the coding chat yet.

- registry: prompts, tool definitions, roles, avatars and skills, embedded
  from src-misc/room with a hot-reloaded per-session overlay that can narrow
  permissions but never widen them
- shelf: SQLite store of claims, evidence, verdicts, findings, reports, assets
  and trajectories, with revision checks, merging writes and the report-pack
  rule
- coordinator: group_delegate, group_status, group_nudge, group_finish and
  group_task_complete, with the finish gates and the confirmation queue's
  completion half
- avatars: several avatars may play one specialist role (two Reds), each with
  its own lanes and models

Nothing is wired into the daemon yet; the module is unused at runtime.
The pure-logic half of the lane layer, ahead of the runner that uses it:

- inbox: the Chief's priority inbox. A turn is drained highest-first, so an
  unread user message always leads and a coincident wake rides in the same
  turn instead of racing ahead. This is the fix for the Orpheus steering
  problem, where a background wake could answer before the user's message.
- control: pause, resume, stop and redirect a work lane, mapped to the action
  the actor carries out. Pause is cooperative; stop leaves the assignment to be
  read as uncertain, never silently done.
- digest: a compact per-lane picture folded from lane events, which each chat
  lane reads at turn start so it can report progress without reading the work
  lanes' chatter.

Still unused at runtime; the runner and actor land next.
A lane streams a model reply, runs the tools it asks for, feeds the results
back, and loops until the model stops or the step budget runs out. The
structure is cloned from app/subagent/engine.rs rather than reused, so the
room does not bend into the sub-agent module's shape or break when it changes.

The loop reaches the model and koma's built-in tools only through the
ModelClient and ToolRouter seams, so it runs under a scripted model and a fake
router in tests, with no live provider. A chat lane answers the user in short
turns; a work lane runs one assignment and forwards the router's events (a
shelf publish, a submit). Steering, pause, resume and stop arrive on the input
channel and are applied at a turn boundary.

Tested: a chat answer, a work tool round, router events forwarded, a model
error, the step budget, steering folded into history, stop, and pause blocking
until resume.
The Chief ends its turn after delegating; this brings it back. Each pass reads
the lane state behind every pending assignment, moves the assignment to match
(a submitted result becomes completed once its lane ends, an unprovable lane
becomes uncertain, a cancelled lane cancels the work), then wakes the Chief
when the batch settles or needs attention. Waking is idempotent: a content
nonce means one distinct batch state wakes the Chief at most once, and a
healthily running batch earns only a slow check-in.

The automatic review-queue and reporter dispatch is a later step; this syncs
and wakes only.
…e reporter

The confirmation loop is now automatic, so the Chief no longer delegates every
verification by hand.

- a probe's published claim is queued for review the moment it and its
  evidence exist, and the lane is told why when a claim cannot be queued
- the next idle reviewer takes up to eight queued claims in one verification,
  covering at most 32 references
- a returned claim goes back to the Red that made it, exactly once: steered
  into its running lane, or a new probe if it is idle and provably so
- a review that fails, is cancelled or is replaced takes its items with it
- once a review passes and findings exist, an idle reporter is handed the
  report pack until it is complete (a detailed technical report per finding
  plus one manager summary)

The reconciler runs this between syncing assignments and deciding to wake the
Chief, so a probe that just finished has its review started before the Chief is
told the batch settled.
…channel

The clerk watches the work lanes and speaks only to the Chief. It cannot pause
or stop anything and never edits anyone's work; the Chief decides what a flag
is worth.

Orpheus's clerk is a room-chatter summariser, so only its rhythm is ported: it
runs only while work is active, never twice at once, and a quiet room costs
nothing because a model pass starts only when something changed.

What a program can establish, a program establishes. A deterministic audit runs
on every tick and hands its findings to the clerk's model pass to judge:

- a claim or asset targeting a host no scope record covers (a stop finding)
- claims published with no scope record at all
- a running lane that has gone silent, unless the user paused it
- a lane deep into its work that has shared nothing on the shelf
- the same assignment given three times

Scope matching is strict on purpose, because a false "in scope" hides a real
violation: an exact host, an explicit *.domain wildcard (subdomains only), or an
IPv4 range, with deny always winning.

The clerk lane raises findings with clerk_flag, which can name a specialist
lane only. Each flag is also recorded on the shelf as an audit note.

Digest events now carry timestamps so stalls can be told from slow work, and the
shelf has a cheap fingerprint to tell "nothing new" from "something changed".
The room's map, drawn from the shelf. A lane may publish its own graph of what
it is exploring; this merges it with what the shelf proves, so a claim reads as
verified only when a finding exists for it. A reviewer's "confirmed" verdict is
not enough, and a lane stamping its own node "confirmed" is ignored until the
shelf backs it. The lanes' exploration nodes (intent, fact, goal, ...) are kept
on top of the shelf's spine, never dropped because a claim exists.

Ported from Orpheus shelf-status, trajectory-graph and trajectory-layout, with
their unit tests as the spec. The layout keeps the same rank and lane rules,
in terminal rows instead of pixels.

Views: an outline tree (the default, readable at any width), a layered graph
for wide terminals that falls back to the outline when it does not fit, and
dot, mermaid and json for reports and tools. Glyphs carry meaning by shape, so
the output reads without colour.

`koma room trajectory [--goal K] [--format F] [--view V] [--session ID]` is the
first change outside the room module: one verb in the parser and one
short-circuit in main, the same pattern as `lsp`. It opens the session's
database read-only, so it is safe against a live session: no tables, data or
schema change. Unknown verbs are still rejected as before.

The swim-lane view needs lane activity history rather than the shelf, so it
comes with the UI.
Two views the UI needs, both pure logic ahead of the actor and UI.

- timeline: who did what, in what order. The digest shows each lane's current
  state and the shelf shows what was proved; neither keeps a history. This
  records one milestone per lane as events arrive (opened, published,
  submitted, failed, flagged) and renders a swim lane per avatar. It lives in
  the actor's memory, so a restart starts the history fresh.
- classify_shelf_items: the per-row gate classification for a status menu
  (open / verified / returned / report), ported from Orpheus. A verified claim
  shows through its finding, not twice; a returned verdict surfaces only when
  its claim is not already shown.
The actor is one state machine that owns the room and connects its parts.
Everything arrives on one channel — the user and the control channel, a
periodic tick, events from the lanes, and the results of the tools those lanes
call — so the actor is a single-threaded loop with no locks of its own.

It keeps the Chief's chat lane and the specialist and clerk work lanes; starts
a lane for each coordinator StartLane effect and steers or shows the others;
folds each lane's events into the digest and the swim-lane timeline; on every
tick runs the reconciler and the clerk; and drains the Chief's inbox into one
chat turn when the Chief is idle, so an unread user message always leads. A
user message while the Chief is busy is injected into the running turn, not
dropped.

The tool router is the seam between a lane and the room: the room's own tools
go to the coordinator and shelf, message_user and clerk_flag become actor
messages, and everything else is a koma built-in run through a BuiltinHost. A
tool the lane was not offered is refused before it runs.

Tested end to end with a scripted model and the real coordinator, shelf and
clerk, so the whole room runs with no live provider: the Chief delegates, a
work lane publishes to the shelf and submits, the reconciler syncs the probe to
completed, message_user reaches the user, and a forged tool name is refused.

The two real seams — a model client over stream_complete and a built-in host
over execute_tool — and the daemon wiring land next.
Two thin adapters that connect the room to koma's live provider and tools, so
the actor runs against them in the daemon exactly as it runs against the
scripted model and stub in tests.

- ProviderModelClient streams a reply from OpenRouterClient::stream_complete and
  folds the events into a ModelReply, the way the sub-agent engine does. Every
  room tool is presented to the model as a function definition. A lane's
  per-avatar model, provider and effort override a base route; the credentials
  stay the session's (cross-provider per avatar is a later refinement).
- CtxBuiltins runs a lane's built-in tools through execute_tool against a
  per-avatar ToolCtx, so a red lane's cyber lane and scope differ from a
  reviewer's (red -> Red, reviewer -> Blue, others -> Research). The daemon
  builds the contexts and the advertised specs and hands them in.

BuiltinHost.run now takes the calling lane's avatar and kind so one host can
pick the right context. These compile against the real types; the live path is
exercised once the daemon hosts the actor.
RoomSession::open reads a session's room.sqlite (the shelf and work tables
share it), loads the registry with the session overlay, spawns the actor, and
drives it with a one-second ticker. It takes the model and tool seams as
arguments, so it stays free of the daemon's AppState: the daemon builds the
real seams, tests pass fakes.

Tested from disk: a room opens from a temp session dir and answers a user
message, creating its database in the session dir.
koma --daemon --kind secure takes the session and runs the room until a signal. Specialist work lanes use the host tools (bash, files, and for Red the web and browser). Chat lanes, the Chief, and the clerk stay on room tools. A sec_* name is refused.
A restart loads fired nonces from room.sqlite. The same batch state does not wake the Chief again, and the same dispatch line is not posted again.
The secure daemon binds the session socket. A hub status probe still gets a session listing. `koma room attach` prints the room and sends each typed line as the user.
In the main terminal, /security quits the coding daemon and attaches the secure room. /security again returns to the coding chat. koma legacy stays in the coding chat.
A long room added a check-in nonce every few minutes and never dropped one. The actor now prunes at start and on tick once the set passes nonce_cap, keeping the newest nonce_keep.
The room keeps speaking while no terminal is attached, for example during a handoff. The serve loop now keeps the latest status and the last 500 spoken lines and replays them on attach. Hello reports how many frames are history, and an attach asks the actor for a fresh status at once.
SessionScreen returned the room screen above its last useState, usePaste and useKeyboard, so turning the room on rendered fewer hooks and React threw. The return now sits below every hook, and the key and paste handlers do nothing while the room is open. A test mounts the screen and enters the room.
Orpheus keeps a nonce as a unique column on the record it guards and drops it with that record. The room now does the same. The unique key in room.sqlite is the only memory of what fired, so the two in-memory sets and the newest-N eviction are gone. Each nonce carries its work id, and a work that completes or is cancelled takes its nonces with it in the same transaction. An older database gets the column added in place.
User messages are panel cards with an accent rail, other authors sit behind the
dotted rail with an inked name, and the composer is the rounded [$] box. Lanes,
Swim, Shelf and Trajectory are tabs beside the chat at 100+ columns and one pane
at a time below that.
An overlay above the composer lists the matches as you type. Up and Down move,
Tab or Enter fills the name, and Enter on a finished word sends.
The daemon lists the models registered in the coding agent and writes a choice
into the session overlay, which the registry reloads for the lane's next turn.
The terminal shows minion, then lane, then a searchable model list full screen.
The chat is now one full-width column with no side pane.
zonblade and others added 30 commits November 13, 2026 11:01
…olours; usage model rows no longer repeat their name when selected
…t list a project's secure sessions, browse the shelf, read an item, goals and reports
…e; /leave stop ends it; the chat shows a secure room marker; Ctrl+C no longer asks; /shelf and /trajectory scroll with page keys and the wheel
…under the other with each child a step to the right
… boxes with a kind and severity line, and a wide layer wraps to the screen width instead of running sideways
…ee-line boxes, scrolls both ways (shift+wheel goes across) and no longer wraps a layer
… one depth stacked in its column; the wheel scrolls anywhere on the browse and model screens, and the model list pages
…pup: switch keeps the room running, stop ends it
…ure-room daemon self-test to the test matrix
…s are dotted, and the CI-only failures are fixed (container overlay owner, graph_query without the linker, Windows pipe and path tests, slow-runner waits)
…s the file, and the test says why a model was refused
…ctory, since a Windows named pipe has no parent folder
…robe, a reverse, a hardening pass and a build each have a procedure instead of only a shelf habit

red enumerates the surface, names the attack classes per component, tests the refused
path as well as the working one, and reproduces before it claims. re starts from
strings/imports/xrefs and confirms a decompiler reading against behavior. blue works
from verified findings and writes the check that fails first. researcher prefers
primary sources and states what it could not establish. builder reads, changes
narrowly, and runs it. The chief is told to name the goal and the surface and leave
the method to the lane.

Prompts stay affirmative and calm: the added strength is method and duty, not
absolutes, so it does not read as a conflict signal to the model's safety training.
… tags, with a per-role scope and pinned skills
…kill_modify replace the skill tool, and the prompt carries a stub plus pinned skills instead of every skill
…nd falls back to the word match when it cannot
… scoped by its role's skills_allow, and a loaded skill stays in that lane's prompt until unloaded
…ill_find is boxed, and the GUI gives them a skill icon
… the wire tests no longer share a name when they start in the same clock tick
skills: find them by asking, not by listing (chat and secure room)
…r team, and the daemon labels each line for the user or the team
…ping a fixed 30 ms, which a slow runner misses
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.

1 participant