Repository navigation
Add BioRouter Crew with shared daemon services and native CLI - #366
Merged
Merged
Conversation
…me handoff and the unlisted reports
…vice code, and documents revoke's exit status
…ion and device code, and says which responses are unauthenticated
…n, and list SCOPE-BIND among the work packages
…record the fail-closed revoke in P09 and G12
…resolved, and put the UI-DIALOGS row back in the progress table
The hello v2 verification bullet claimed a relay that strips signature_v2 cannot alter the capabilities and that they are display-only. BrokerHello::unsigned keeps them as unauthenticated hints and join_status acts on join_by_name_v1, so a bridge-path relay can add or strip them. State that, and why it fails closed, matching the naming design's compatibility table.
… unsigned capability can be stripped The contract said a client sends a new parameter only after the capability that introduced it is present. Neither client does that for expected_username, and following it would let the M2 relay strip signature_v2 and human_names_v1 and so switch off target_mismatch. State that capabilities gate methods and screens, never a defense, and fix the naming design's matching sentence.
A journal record's checksum is verified by re-serializing the parsed record, so parsing must keep the writer's key order. The shipped broker only had preserve_order through workspace feature unification with biorouter-server; a broker built on its own (cargo build -p biorouter-crew, and the crate's own tests) refused every existing journal with journal_corrupt: checksum mismatch. Measured on the live QA fixture: the standalone build failed to open both workspaces, the same commit with the feature replayed them byte-identically. The new test fails without the feature and passes with it.
…shared drop set
displayText.ts restated the \p{Cc}/\p{Cf} drop set inside its own rejected
class, a second definition the annotationChannel guard forbids. utils/untrustedText
now exports stripHiddenCharacters as the one definition (sanitizeUntrustedLabel
calls it: strip, trim, slice, as before), and sanitizeDisplayText calls it after
removing only the classes it refuses beyond that set.
The extra classes run FIRST. The obvious order (strip, then the rest) is not
equivalent: deleting a format character between a lone high and a lone low
surrogate joins them into U+10000, which no later pass removes. Measured equal
to the single-class implementation over every code point (three wrappings) and
800k random strings; the new tests were run against the old implementation and
pass there too, and the surrogate test fails on the reversed order.
…s v flag INSTITUTION_ID_PATTERN had a bare '-' in a character class. A browser compiles a pattern attribute with the 'v' flag, where that is a syntax error, and a pattern that fails to compile is dropped silently: the privacy popover and the onboarding field accepted 'UCSF' and left the refusal to the daemon. The '-' is now escaped; the flagless RegExp isInstitutionId builds reads '\-' as the same literal '-'. nameRules.ts carried a corrected copy under INSTITUTION_FIELD_PATTERN because of this defect. It is now a re-export of the one pattern, under the same name the dialogs import. Tests compile the pattern under 'v' and with no flag and check the same accept/refuse table under both; they fail on the old pattern.
…p offering attachment as a resolvable kind
The chat named its Crew channel only from the Crew view's in-memory labels, so after a
restart it fell back to 'a channel in {workspace}' although the daemon records the
destination's name at grant time. grants.ts now parses labels (every field checked as
text, a label for a different channel ignored), and the chat prefers that name, then the
remembered one, then the recorded workspace. Access rows use it only when the snapshot no
longer shows the channel. CrewUnknownName carries the daemon's case-only did_you_mean, and
'attachment' is gone from the resolvable kinds, since the daemon refuses it with
crew_invalid_selector.
… the staged helper Formatting only: prettier --write on the seven files format:check failed on (line wrapping, and the trailing commas and parentheses wrapping implies); three are unchanged with all whitespace removed, the rest differ only in that. Their tests and ESLint pass unchanged. src/computer-use/ is the native helper scripts/computer-use-resources.js stages. The repo .gitignore ignores it, but Prettier run from ui/desktop reads only that directory's .gitignore and .prettierignore, so format:check failed on its manifest.json and pin.json on any machine that had built the helper.
…rewFiles The /crew route renders only CrewApp, so crew/legacy/LegacyCrewLayout.tsx, crew/crew.css, CrewFiles.tsx and its regression test are deleted rather than left compiled (D16). CrewView stays as the controller root the test harnesses mount, with layout now required. The stylesheet guards lose their legacy exemptions, and integration/legacyStylesheet.test.ts now fails if a deleted path exists again or if any source file or build config imports, mocks or @imports one. The upload privacy handoff CrewFiles.regression covered is held by files/useCrewUpload.test.tsx.
…le in the 360px pane An access row put the icon, the title and destination, the status badge and the actions on one unwrapping line, so in the 360px details pane the title and destination were squeezed to zero width and the More actions button was clipped (D12). The line now wraps, in authored crew-app.css rules rather than new utilities: the text block keeps at least 10rem, and the badge and the actions share one end group that moves onto its own line, never shrinks, and wraps inside itself before it would overflow. Names and order are unchanged. AccessTab.test asserts the structure the wrap depends on; measured in headless Chromium, the text keeps 308px at 360px and the last button stays inside the row down to 240px. crew-app.css's header also drops its legacy exemption note.
…odes in the dialogs
A broker refusal reached the dialogs as an older daemon's raw
'Crew broker refused request: {json}' envelope, and the broker's new
codes (rate_limited, target_mismatch, not_invited, identity_unavailable,
unknown_account, quota_exceeded, identity_conflict, device_conflict,
identity_mismatch, already_approved) had no words of their own.
parseRefusal now removes the legacy envelope before reading the code, so
both daemons read the same. A person-written broker sentence is shown
without its code; the broker's technical texts (the journal and
state-size quota, the challenge rate limit, the member identity_conflict,
device_conflict and the two enrollment identity_mismatch texts) get copy
deck sentences. quota_exceeded is matched on its words because the join
quota means something else. The canonical-spelling hint no longer
captures the sentence's full stop, and Let in answers already_approved
with Replace code, which re-sends the same code with replace: true.
Fixtures are now the broker's literal texts, with legacy-envelope cases.
…w only the privacy an invitation states
D9. Re-pasting an invitation for a workspace this computer already has returns the
existing connection. JoinDialog then PATCHed it with the Advanced choices (which
disconnects it) and, when that failed, removed it together with its device key. The
dialog now reads existing_connection_id from the preview and offers 'Open {connection}'
instead of saving; a submit snapshots the saved ids first, never updates a connection
that was already here, and rolls back only one it certainly created (when the list
can't be read, nothing is removed).
D15. The summary read the planned save values (mode, institution_id), which default to
Private for a legacy paste, as the workspace's stated privacy. It now reads
workspace_mode and workspace_institution_id; when the invitation doesn't say, it says
so and leaves Private/Public unchosen, and Join stays disabled until the person picks.
The preview also keeps fingerprint and missing (a paste that names no server asks for
the login up front), and a 409 crew_connection_exists or crew_invitation_conflict
offers to open the connection it names. CrewJoinClaim carries JoinClaimed's inviter,
workspace_name and add_device instead of a never-sent principal. The JoinDialog pattern
assertion now reads INSTITUTION_ID_PATTERN, which 8473c84 changed.
…, then offer Reconnect GET and POST .../join answer 409 crew_not_connected while the connection is down, and the join card showed that as a poll error on every tick, forever. It now stops polling, connects once by itself, and asks again; if the route still can't reach the workspace it shows a Reconnect button (a user-initiated connect, so Sign in opens when the server asks). A claim refused the same way reconnects and claims again. The card also uses what the daemon's JoinClaimed and join status carry: the inviter and workspace name are remembered for the screens after joining, and an add-device invitation says it adds this computer to the person's account.
…ames, run start times and broker capabilities to clients - CrewRouteError: a broker refusal envelope now answers broker_code and the broker's own sentence instead of the transport's JSON text (D1). - Observer messages frames carry remaining and page_size, so a client can tell when the opening backlog ends and how large a page really is (D5). - Each messages frame carries the people and channel_names its page gave that message, so a departed author keeps a name (D6). - RunView records started_at; owned runs sort newest first and a state frame lists live runs plus the 20 newest finished runs per channel (D10). - State frames carry the connected broker's capabilities (D13).
…ply stays under its stack check_provider_dispatch (binding + tier + standing + a context.manifest worker request) was awaited inline from Agent::provider, which Agent::reply reaches through the tool-surface preparation. Its future was laid out in reply's own frame and overflowed the default 2 MiB test-thread stack in privacy_toggle's the_master_toggle_governs_every_gate_in_both_directions. Both it and standing() now run their bodies behind Box::pin; every call site and pub signature is unchanged, and no stack size is raised anywhere. Measured: privacy_toggle passes three times with RUST_MIN_STACK unset, and the stack-margin binaries (soft_interrupt_agent_loop, turn_abort_tests, subagent_delegation, steer_always_lands) and privacy_guard_wiring pass.
The CLI printed a forwarded broker refusal as 'Daemon returned 400: …',
and from an older daemon as the raw 'Crew broker refused request: {json}'
envelope.
DaemonRefusal now carries the daemon's broker_code (validated as a code
and capped at 128 characters), falling back to the code inside an older
daemon's envelope, whose message becomes the refusal's text. The text
stays terminal-safe and apostrophes stay unescaped.
biorouter crew prints a broker refusal as the sentence the desktop shows
for the same code, byte for byte (a test reads the desktop's copy deck),
followed by the command that acts on it: enroll approve --replace for
already_approved, enroll invite @<canonical> for identity_ambiguous,
enroll invite --add-device for already_member, and 'Choose another
name.' for a name_taken that does not already say so. JSON output keeps
code and adds broker_code. Exit codes and the revocation handling are
unchanged.
…'s count, and name departed authors - crewApi: CrewHttpError carries the broker's code (brokerCode, from broker_code) and api/errors exports crewBrokerCode (D1). - Messages frames validate remaining, page_size, people and channel_names; people and channel_names are built on a null prototype and never keep a __proto__/constructor/prototype key (D5, D6). - The controller exposes optional backlogComplete, pageSize, people and capabilities. useOpening ends the opening on backlogComplete and keeps its quiet/stall timers only for a daemon that sends no remaining; the timeline measures full pages by pageSize (D5). - An older page asks at the observer's page size, halves it on response_too_large down to 1, and records the size it loaded with. A 5xx or a size/shape refusal returns to the live tail with the error in the connection bar; access, privacy, stale-cursor and unclassified failures still clear the verified view (D5). - Every message page's people merge into a per-channel map that the timeline, pane and channel presentations pass to usePeopleDirectory, so a former author reads 'Name · former member' (D6). - The timeline scrolls to each new newest message while following, since a full tail does not grow, and auto mark-read measures that the newest row is on screen when the dwell ends (D4). - ObservedRun gains started_at; unanchored task rows appear only for live runs or runs that started within the loaded messages, oldest first, and newestTaskIn prefers the latest start (D10). - uniqueNamesSupported prefers the broker's unique_names_v1 capability, so a new S2 workspace with no teams offers Rename (D13). - An expired pending join reads 'Invitation expired · Invite again…' with no Let in, in the sidebar and in workspace settings (D15). - Drop the deprecated Invitation.status?: never (D16).
Live shell lines were lost, leaked between concurrent calls, or never shown, for four reasons that compounded: - The developer shell sent each line as an MCP logging notification with no link to its tool call. It now echoes the call's progress token in `data.progress_token` (additive; absent when the call had none), on output lines and on the foreground heartbeat, which names the command. - The client subscribed every unpooled dispatch to a broadcast, so two concurrent shell calls on one developer client each received the other's lines. Every dispatch, pooled or not, now registers a route under its own token; a tokened logging line goes to that route only. A token no route holds is dropped, never broadcast. An untokened line keeps the old fallback: broadcast on an unpooled client, dropped on a pooled one. - A pooled (routed_only) client dropped every logging line, and its per-dispatch token never reached the server at all: rmcp 0.14's send_request_with_option overwrites _meta.progressToken with its own counter before merging PeerRequestOptions::meta. The token now travels through options.meta, which rmcp applies last. Tokens are per-client decimal numbers minted for every request, so they cannot collide with a listing's token and the recorded MCP cassettes still match. - Per-dispatch channels held 16 notifications and try_send dropped the rest silently. Capacity is now 256; try_send stays so a slow consumer never parks a task per line, and drops are counted per route and logged once when the route is removed (RAII, also on cancellation). Tests: tokened routing on unpooled and pooled clients, unknown and malformed tokens dropped rather than broadcast, a 200-line burst arriving complete, overflow counting, two concurrent dispatches over a real MCP connection each receiving only their own lines with the token the server actually received, and the shell tool echoing the token. This is MCP protocol and async-concurrency code; per HOWTOAI.md it needs human review before merge.
…very write, and revoke a run whose setup failed NEEDS HUMAN REVIEW: both changes are grant and revocation state. D8. Every process sharing a profile (the daemon, a CLI run) loads connections.json once, and every save wrote its whole in-memory copy back: a connection another process saved vanished, and a grant another process revoked came back live. All registry writes now go through one helper, update_registry(_keeping): take the in-process registry lock, take an advisory exclusive lock on <root>/connections.lock (std File::try_lock with a bounded 5 s backoff, never blocking the runtime), re-read connections.json, apply the edit to that copy, write it atomically only when it changed, and make it this process's copy. When the file is exactly what this process last read or wrote (tracked by digest), its own copy is the base, so single-process behaviour is unchanged. When another process wrote, what the file never holds for this process is carried over: each connection's live status and last error, an in-memory binding of a legacy grant, and a stop (a grant expired here stays expired for the same run, even if its save failed). prepare_device, save/update, remove, adopt_verified_hello, the grant insert, revoke, prune_stale_grant and retire_deleted_sessions all use it; edit_saved_scopes is gone. Edits that always held in memory when the save failed (revoke above all) still do. A chat delete in a profile that never used Crew still creates nothing, and a new-connection refusal that needs no saved state is made before the registry is touched. D7. After run.create returned a run, a failed protected-context check, missing credential, credential or registry save, or the first messages.history left the run live at the workspace, and, once recorded, the grant active here. Every failure after the run ID is known now abandons it: a recorded grant is revoked as the person would (expired and saved here, then run.revoke), falling back to a direct run.revoke if that could not ask the workspace; an unrecorded one gets run.revoke and its freshly written run credential deleted (new CredentialVault::delete, keyring/file/vault). The original error is returned. grant_session shares the path, so a failed grant is left expired, not active. routes/crew.rs is unchanged. Tests: registry_lock_tests.rs (two managers on one root: save + revoke both survive; a revoke is never written back live through remove/prepare/save/ revoke/retire; unsaved stop survives; process state carried; newer grant not overridden; 24 concurrent writes all land; lock wait and bounded refusal; no directory for a Crew-less profile; refused/no-op edits write nothing), and in mod.rs a failed messages.history revokes the run and leaves the grant expired, a pre-record refusal revokes and leaves no credential, and credential deletion for file and vault backends. Mutation-checked: disabling the re-read or the abandon fails 8 of them. scope_binding_tests: the retire test now also expects the other process's grant in memory, since writes read it back.
…he stricter pass runs first
…eason (F-1) A removed member's download through the renderer or the raw route was accepted (200 starting) and then ended needs_file_selection, "Transfer stopped. Reselect the original local file or destination to resume", which no reselection could ever fix. A transfer the workspace itself refused now ends in the terminal failed state with the workspace's reason as a sentence (crew::refusal_sentence): for a removed member, "That channel isn't available to you. It may be archived, or you may not be in it.", the words the CLI already used. A stop the workspace did not answer (a pause, a dropped connection, a locked vault) is still needs_file_selection, a download stopped while publishing is still publication_unconfirmed, and a failed receipt stays failed across a restart.
The text form of `biorouter crew grants list` printed a grant whose expires_at had passed as Active, printed task grants as "Chat", and ignored the conversation's title. It now names a task a task (kind), names the chat by its session_name, and reads the state from expired, expires_at and the daemon's new revocation word together, in the words the desktop's access list uses: Active (with when it ends), Expired, Revoked, Ended for a task, "Stopped on this device; the workspace hasn't confirmed yet" for an unconfirmed revocation, and "Ended: Crew settings changed" for a run the workspace ended itself.
…ed by itself (F3) The daemon now asks the workspace again for an unconfirmed revocation with no Retry pressed, so a task the ledger left waiting on that confirmation would have kept reading "Stop not confirmed" (or a finished task its "didn't confirm" note) after the workspace confirmed. Listing a connection's runs now settles each such task whose run the registry records as confirmed (CrewManager::remote_revocation_confirmed): a stop reads cancelled, a failure its own failure without the unconfirmed suffix, and a finished task loses the note, as a confirmed Retry would have left them. Nothing else is touched.
…ver the transport record (NEW-1) The workspace menu's header showed the last failure as the daemon wrote it — a failed Connect's message, else the saved record's last_error, which for an SSH drop is 'Crew SSH failure [ssh_eof; child_before_cleanup=exit_255]: ...'. It now reads both through connectErrorText, the saved one by its typed last_error_code: 'Can't reach lab-server.', 'Can't connect to …', and a code prefix never reaches the person.
…e the allowlist refuses them (B-1)
G12 (b) passed live only because GPT-5.5 declined, in the model, to send a
method outside crew__request's schema enum, so the daemon's worker allowlist
was never exercised. This integration binary drives it deterministically with
no model: a real Agent::reply loop on a chat granted through the real grant
path (workspace.snapshot, run.create, the run credential), a Private model
capability affiliated with the workspace's institution, and a scripted provider
that sends crew__request for enrollment.approve, team.add_member,
channel.add_member, run.revoke and policy.set. Every call must come back as the
allowlist's refusal ("Operation unavailable to a scoped Crew worker"), and the
fake broker, which logs every frame, must receive none of those methods: after
the grant it sees only the turn's scoped context.manifest reads, all under the
run credential. The broker is this test binary itself, started by a fake ssh,
so the binary is offline and runs in CI's loopback-only integration step.
Checked against a disabled allowlist, which it fails.
…nnect (F3) The 503 crew_revocation_unconfirmed answer said "reconnect and retry" and the CLI said "reconnect and run this command again", which was the only way it ever got confirmed. The daemon now asks the workspace again by itself whenever the connection is back, so both say that, and the CLI names grants list as where to see it confirmed. The revoke route test now also pins the grants list's revocation: unconfirmed and remote_revocation_confirmed: false after the 503, and null for an untouched grant.
… a transfer of it (F-1) A transfer the workspace refused because the file itself is gone or out of reach (forbidden: attachment unavailable) now ends with "That file isn't available to you. It may have been removed, or you may not be in its channel." rather than the bare "Attachment unavailable."
…try, not this process's copy (P-1) The no-op check compared the PATCH body with this process's in-memory connection, so a change another process had saved since (D8) could be mistaken for the setting it replaced and the person's save silently skipped. It now compares under the registry's locks against connections.json as it is now, and writes nothing; the unchanged-save test pins the cross-process case.
…ain or its id reissued (F3) A chat's grant is stored under its session id, one per id, and the daemon's own retries found unconfirmed stops only there. Two paths dropped such a stop for good, with no run.revoke ever sent again: granting the same chat again (begin_run_with_policy replaced the grant), and pruning a deleted chat's grant once another chat held its id (prune_stale_grant). The old run stayed live at the workspace until it lapsed, and grants list stopped showing it. Both paths now move an unconfirmed stop to the registry's `replaced` list (Registry::keep_replaced) and arm a retry pass for its connection. The list is: - asked about by the same retries (unconfirmed_revocations reads it), and a confirmation is recorded on it (confirm_replaced), which the task ledger's remote_revocation_confirmed also reads; - listed by session_grants as `replaced_grants`, apart from `grants`, so a reader that looks a chat up by id (the desktop's findSessionGrant) never takes an earlier grant for the chat's access; the route adds kind and a null session_name (the id may name another chat now), and the CLI text lists them under their own heading; - merged across processes like any stop (carry_process_state): what the workspace said is never forgotten either way round, and an unconfirmed one the file never heard of is carried in while its connection is saved; - dropped when its connection is removed, and otherwise only once settled: a week past its run's own end, or at confirmation when no end is recorded. Only an unconfirmed stop is kept; a live grant, a confirmed stop and one the workspace ended itself are replaced or pruned as before. Nothing reads the list to decide what a chat may do. Tests: the reviewer's scenario end to end against the scripted broker (revoke offline, the workspace refuses the reconnect retry, grant again: run.revoke for the old run is sent again at once, it is listed and saved as replaced and unconfirmed, and the next reconnect confirms it while the new grant is never touched); the reissued-id prune; which stops are kept; retention; the merge; the route's replaced_grants row; the CLI text. The first two fail with keep_replaced disabled.
…ping its bridge (P-1) update() asked only validate_connection's refusals before disconnect_locked, so an edit that save_inner refused later still dropped a working bridge: an institution ID that does not normalize, and "Choose this private SSH connection's institution before saving". Every refusal the save makes (those two, aliases whose institutions differ, a prepared identity on an edit) is now asked first, by running the save's own edit on a copy of the saved registry that is then thrown away (refusal_before_saving). The save checks again, so a change another process saves in between is still judged by the save. The P-1 test now also edits with both refusals and pins that the bridge, the status, the mode and the policy epoch are unchanged and no bridge was dialled again; it fails with the early check removed.
…penAPI gates 07c2a10 left three gates red: - clippy::too_many_lines: begin_run_with_policy grew to 109 lines. The F3 recording (insert the new grant, keep an unconfirmed earlier stop, arm its retries) moves unchanged into CrewManager::record_grant in revocation.rs, beside keep_replaced. Same order: the edit holds in memory when the save fails, the retries are armed either way, and the file's error is returned. - the id-reuse seam audit: keepalive_tests.rs now calls forget_minted_session_ids_for_test to reissue a deleted chat's id (F3's prune path). It is a #[cfg(test)] module; it gets an allowed row with its reason. - OpenAPI drift: regenerated for the crew_profile_grants replaced_grants paragraph (07c2a10) and the crew_profile_revoke 503 sentence (cf02f2d). Doc comments only.
…dc27465 The final lanes on 461f789 left D-1 (the G12 (d) wording failure), NEW-1, revoke F1-F5, F-1, P-1 and the B-1 coverage gap open. A final polish fixed them in 15 commits ending at dc27465, passed the local gate there, and re-checked nine items live on a new two-account fixture that was then torn down and independently verified. This records it where each reader looks: - the acceptance evidence gains a Final polish section: what was fixed, by which commits and tests, the reviewers' non-blocking notes, the gate, the live verdicts, the three new findings (NEW-2 P2, NEW-3 and NEW-4 P3) and the teardown receipts, and says which lanes were not re-run on dc27465; - the status ledger updates P02, P09, G02, G08, G09 (now pass for every fixture of the campaign), G12 ((d) passes; (b) has a deterministic test, not a live model) and G15, and its close-out blockers; - the handoff's close-out names the final code head, the new gate, and five final-polish rows for human review: the automatic revocation retry, the policy-changed mapping, the worker-allowlist test, the connection-save no-op and the re-send hold; - plan section 16's final status says the same; - the CLI guide's revoke section stops telling people to reconnect and run the command again, which the daemon now does by itself, and names the grant states. Hosted CI on the pushed head is recorded in the PR, since a commit cannot record its own.
…n that dropped elsewhere (NEW-2) Crew armed its read-only offline follow only for a loss it was on screen for, or after a failed Connect. A bridge that dropped while the person was in a chat left Crew saying Offline after the daemon had re-dialled (11 min 34 s live), until a Connect replaced the working bridge. The follow now starts whenever Crew mounts on, or selects, a connection the daemon already calls disconnected. It still only reads GET /connections and never connects. It is not started while a loss is being decided, for an ended membership, or for a connection the person disconnected in this window (the daemon never re-dials a Disconnect); seeing it connected again forgets that Disconnect.
…ess, and keep its row (NEW-3) Only a 200 was recorded for Show past access, so a revoke stopped on this device (503) and confirmed later by the daemon itself (F3's path) read plain Revoked, and vanished from the list once the chat was granted again. Every run this window sees waiting (its own 503, or a grant list that says revocation: unconfirmed) is now remembered, and the first list read that says confirmed records it, dated then. The grant list's replaced_grants are read for this too, so a stop confirmed after its chat was granted again is dated as well. A run confirmed before this window saw it waiting gets no invented time, a workspace-ended run is never recorded as a revoke, and a 200's own time is not moved. Display only.
…anged, as the CLI does Past access labelled a grant the workspace ended itself (revocation: ended_by_workspace, D-1) "Expired", while the CLI's grants list says "Ended: Crew settings changed" and the chat says the settings changed. Nobody revoked it and its time did not run out. Access rows now read "Ended: Crew settings changed" for such a grant, chat or task, and the chat-connect note and the Chat access pane say "Crew settings changed since ... was given access." A grant whose time ran out still reads Expired, and a finished task still reads Ended.
…or leaves the pane (NEW-4) The note that the workspace confirmed a revoke this view saw waiting lived in the pane body's component state. When Crew reconnected, the offline screen gave way to the channel, the pane body mounted again and forgot it had waited: live, "Confirmed." was visible for about seven seconds. useConfirmedAfterWait now keeps what it saw per scope. The Chat access pane passes its pane intent, which lives across that remount and ends when the person closes the pane or opens another, so the note survives Crew reconnecting and goes when the person leaves the pane. The note carries a Dismiss control in the pane, the Access list and the chat's bar.
While the Crew connection was down, the chat's own access bar offered only Connect in Crew, so stopping the chat meant going to Crew first. A revoke needs no connection: the daemon stops the grant on this device at once (503, "Stopped on this device") and confirms it with the workspace by itself once the connection is back (F3). The offline bar now offers Revoke access beside Connect, asks inline as the connected bar does, and then shows the held chat with the unconfirmed note. Revoking never navigates to Crew or connects anything.
…s ended (F1, daemon) Edit in place truncated a chat, and /reply's conversation_so_far replaced its history, whatever the chat's Crew grant said; the turn that followed was then refused at dispatch. The renderer holds those paths first, but a window that had not re-read the grant since the CLI or another window revoked it could still cut the stored transcript for a turn that could never run. CrewManager::history_rewrite_refusal answers, locally and without asking the workspace, the sentence the chat's next turn would be refused with: removed, ended by the workspace or a settings change (D-1), or past the run's recorded end. The standing checks it shares with every turn now live in one function, grant_stands, so the two can never word one grant differently. The daemon asks it before anything changes: edit_message's in-place arm (after the reach gate, before the turn lock and the snapshot), /reply's write-back (before apply_client_writeback), and, for a caller holding the person's proof, both diverge doors, which refused a Crew chat's copy as a bare 500. Each refusal is 403 with the sentence as a plain-text body. A caller without the proof is still refused by the reach gate first and never learns where the grant stands. Tests: crew_history_rewrite_hold (new integration binary; 3 of its tests failed before the fix: 200 + truncated, 500, 200 + written back) and two crew unit tests pinning that the turn and the rewrite say the same sentence.
…daemon history-rewrite hold NEW-2, NEW-3, NEW-4, two re-check observations and the daemon half of F1 landed in six commits ending at 715c59b. They are recorded as verified by regression tests and an ALL_GREEN local gate only: no live fixture ran. The handoff's close-out names the new final code head and adds the daemon's history-rewrite hold to the human-review list.
…landing code block overflow
… use noun phrase headings
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.
Summary
BioRouter Crew adds SSH workspaces with teams, channels, invitations, persistent messages, attachments and individually owned agents. Its Linux broker runs under an ordinary user without sudo or an exposed network listener. Native OpenSSH handles host verification, jump hosts and interactive authentication.
The desktop,
biorouter crew, and explicit shared-daemon conversations use common daemon services for authentication, credentials, connections, transfers, observation, grants and tasks. Human approval remains separate from SSH login and provider credentials. Transfers verify hashes and retain restart receipts; uncertain submissions are not retried automatically.Private SSH connections now require an institution ID. The workspace host confirms an immutable shared institution label. Local models can serve any institution; institutional models must match the private workspace. Admission and ongoing worker checks use the resolved provider affiliation, selected-source protection and policy epochs. A context change invalidates an earlier unprotected grant before output. Required institution provenance survives optional privacy settings being disabled. Crew conversation copying and divergence are refused until safe scope inheritance is supported.
Sending a message retains the verified room instead of flashing the join view. Enter sends; Shift-Enter inserts a newline, with composition and repeated-key guards. A development-only stdin path permits unattended synthetic QA approval input while preserving daemon identity/proof checks; installed applications retain their native prompts. Saved CLI conversations restore their provider through the trusted session store, and slow observer state admission no longer continually takes priority over queued messages.
Crew UI redesign campaign (2026-09-23 to 2026-09-25)
Evidence: redesign acceptance evidence, close-out handoff. Designs: UI redesign spec, naming design, plan §16. All 28 plan §16 work packages landed, plus the integrate-verify pass and four rounds of live-QA fixes.
What changed
CrewView.tsxform is replaced by a layered interface built only from the design system: a sidebar of workspaces, teams, channels and members; channel header and menu; timeline; a composer that takes drop and paste; a details pane; "Ask my agent"; one dialog inventory; each error rendered once; no machine IDs on Crew screens.join_by_name_v1, on by default from461f7899). The host or a team owner can add an admitted member directly. The broker enforces unique names and refuses system accounts at invite, including on the legacy token path. S4 (discovery, 8-digit codes) is deferred.Fixes found by live QA
sessions.incarnation, so a reissued session ID never inherits it, and a deleted chat's grant restricts but never authorizes (0dcbc6fb,9de11e76).serde_json/preserve_orderitself. Without it, a standalone broker build refused every existing journal withchecksum mismatch. The live upgrade, downgrade and re-upgrade then passed byte-identically.tests/journal_key_order.rspins this (0cae537e).6f2b0b233).3c81c7095,3b2130ab1,334cffc76,bd7cd6033).4cd2152ba,dcdffab8d,0ee194605,b5e955bea,29c78640d).webUtils.getPathForFile, followed by a native confirmation that the renderer cannot answer (5edc20f6d,284c54b48).POST /crew/filesregistration in both directions, with links, case variants and hard links resolved. It adds the netrc, pgpass, git-credentials, docker, kube and gh stores, and refuses settings, login and autostart download destinations and executable overwrites (9708e78dc,b7585ba1a,6aca6c18a,7339e6d9d).an_include_resolves_like_opensshpinned the absoluteIncludearm with/etc/ssh/extra, which Windows does not treat as absolute. It now uses a path that is absolute on every OS (d1aaae4f). On Linux,SecretGuard::is_deniedaskedis_inside_rootof a path's lowercase spelling. Under a root with a capital letter that spelling names nothing on a case-sensitive filesystem, so a project's own negation (!.netrc) always lost to the floor. Containment is now decided from the path as given, and the test roots its project at a directory with a capital letter, so it is deterministic (ec305ddf; the old code was reproduced failing on a case-sensitive APFS volume).Live QA trend. Four rounds ran on a disposable AWS fixture with synthetic data only. Fresh novice, security and accessibility critics drove the real development apps on private Versa
gpt-5.5-2026-04-24.5959ebdc5b079379ac02e57ab1c87edcThree brand-new host and joiner pairs completed their journeys: Alice with Bob, Carol, Dave and Erin; Henry with Gina; and Iris with Jack (21 of 22 tasks unaided). Every broker boundary the security critics tested held in every round, checked against the broker journal. The round-4 P1s (Q4-01 re-dial, Q4-02 stale "Can't connect", Q4-03 file-name truncation) were fixed before the final build and re-checked there.
Final acceptance on
461f7899(seven lanes, 2026-09-25 14:26–15:11Z)grant_expiredenvelope (D-1).grants listlabels) and F5 (duplicate rows).Hosted CI on
ec305ddf(the head before the final polish; see Final polish below)All green: Rust 36160256955 (guards, serve, macOS/Ubuntu/Windows tests, both cross-checks), Frontend 36160256954, Apps smoke 36160256951, Computer Use native payloads 36160257045, Check Commit Messages 36160256969 (pull_request) and 36160251513 (push). Before this, only Rust failed. Windows failed at
ac02e57aandb1c87edcon theIncludetest; the61e30f55run was cancelled when its fix was pushed. Ubuntu then failed atd1aaae4f(36156654158) on the secret-guard test. No flaky re-run was needed. The localjust check-everythingverdict at461f7899is not on file; re-run it before merging.Needs human security review before merge
CLAUDE.md requires a person to review authorization, credential, MCP-protocol and async code. Every change below had independent agent review only.
sessions.incarnation(0dcbc6fb,9de11e76). Check that a reissued session ID never inherits a grant, that a deleted chat's grant restricts but never authorizes, and the--no-sessionIDs.67082118). Check that a failed task setup revokes its run, and thatconnections.jsonwrites are read-modify-write under a cross-process lock.ec6a154e). Check progress-token routing inmcp_client.rs, the 256-slot channel, and that an unknown token is never broadcast.500bf8ada,5c1c5cdfe,456337eb8,3b577b111,5e8760460). Check that only the host or a team owner can add, theexpected_username, recycled-UID and former-member refusals, and the legacy token path.6f2b0b233).3c81c7095,3b2130ab1,334cffc76,bd7cd6033). Check that the heartbeat is authenticated, that re-dials use non-interactive credentials only, and that a refused heartbeat or revoked device never re-dials.4cd2152ba,dcdffab8d,0ee194605,b5e955bea,29c78640d). Check the fixed commands, the user-action proof, and the refusal of extra fields and metacharacters.5edc20f6d,284c54b48). Check the path source, and that the development auto-confirm gate fails closed outside validated dev profiles.9708e78dc,b7585ba1a,6aca6c18a,7339e6d9d), including the~/Librarycloud-drive exception.ec305ddf). Check that containment is decided from the path as given, not its lowercase form. This makes a project negation work on case-sensitive filesystems, and it can also refuse more: an uppercase path outside the project no longer borrows the project's negations.join-by-nameon by default (461f7899). Check that every shipping build carriesjoin_by_name_v1and that--no-default-featuresstill builds a broker without S3a.cd94f12fb,b88ea1476,cf02f2d03,07c2a104f,dc2746551). Check that the retry is armed at every connect and backs off 5 s to 5 min while connected. Check thatrun.revokeis signed with the device key, thatrevocationis merged across processes without ever losing a confirmation, and thatreplaced_grantskeeps an unconfirmed stop when its chat is granted again or its ID reissued without authorizing anything.cd94f12fb,b6226ac59,9248dcbb0). Check that a brokergrant_expiredends the grant locally asended_by_workspacewithout arun.revoke, that every workspace refusal at turn start goes throughagent_error_textso no raw envelope is shown, and that a timed-out run is not marked revoked.9f84b4f08). Check that it drives the realAgent::replyunder a Private capability through the real grant path, that the fake broker receives none of the five person-only methods, and that it fails with the allowlist disabled.cd94f12fb,fb2c0f4a8,f3dd33761). Check that an unchanged PATCH is judged under the registry's cross-process locks againstconnections.jsonand writes nothing, and that every save refusal is asked before the bridge is dropped.9248dcbb0). Check that every path inBaseChat.tsxthat sends outside the composer asks the Crew hold first. The daemon's own hold is the next item.715c59b5d, last fixes).CrewManager::history_rewrite_refusalis asked before anything stored changes in three places:edit_message's in-place arm (after the reach gate, before the turn lock and snapshot),/reply'sconversation_so_farwrite-back (beforeapply_client_writeback), and both diverge doors for a proven caller. Check that each refusal is a 403 with the turn's own sentence, and that an unproven caller meets the reach gate first and learns nothing about the grant. Check that moving the standing checks into the sharedgrant_standsdid not loosen the turn'scheck_tier. The check is local only, so a grant the workspace ended that this daemon has not heard about still edits, and its turn is refused.POST /crew/filesin both directions andPOST /agent/call_tool(the structural half of Q3-01/Q4-55). The credential floor and destination rules are defense in depth. A registration proof held only by the main process needs a security design decision.Validation
The implementation status, validation report, protocol contract and CLI guide retain exact artifact scopes and limitations.
source bin/activate-hermit && CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 just check-everythingcompleted with exit 0 at 00:41:39 UTC on September 24, 2026. Independent Astra review found no actionable issues. This checkpoint ise11fe795; its hosted checks and rebuilt runtime replay are pending.foreign-syntheticrefused a UCSF-affiliated Versa task. Its refusing CONNECT recorder remained empty before and after the attempt. No task/context receipt was visible; an additional native CLI corroboration was blocked by automatic approval review and is not counted.Remaining draft gates and limits
Complete the matching artifact replay, ordinary-chat MCP/selected-context scenarios, CLI model-resume self-test, remaining privacy/recovery cases and hosted checks. The new CLI and observer fixes are independently reviewed; this PR remains draft until applicable acceptance is complete.
Shared native CLI IPC is Unix-only. Windows desktop acceptance remains open. Canonical broker storage requires a supported local Linux filesystem; NFS/SMB hosting, arbitrary shells, automatic failover and retention/compaction/backup tooling remain unsupported. Narrows NFS home hosting is refused; Leo basic chat/files are not runtime-qualified and its recorded Landlock ABI cannot support the execution helper. Crew copy/diverge requires starting a fresh conversation and explicitly granting context instead. Synthetic testing and SSH encryption do not establish HIPAA compliance.
Source and curated text evidence are public. Credentials, screenshots and raw visual/transcript evidence remain local. No merge is requested.
Final polish (2026-09-25)
The final acceptance lanes on
461f7899left the G12 (d) wording failure (D-1) and some P2/P3 findings open: NEW-1, revoke F1–F5, F-1 and P-1. The worker allowlist had also never been exercised (B-1).Fixes. 15 commits ending at
dc2746551fix them, in two groups: daemon, core and CLI; and the renderer. Each group had an adversarial agent review; the daemon group's first review failed F3 and P-1, and round 2 fixed both.ended_by_workspace.failedwith a plain reason.crew_worker_allowlist) sends the five person-only methods through a real agent loop, and each is refused.Local gate. ALL_GREEN on
dc274655:--workspace --lib --bins8540 passed, 0 failed; 13 Crew and privacy integration binaries; no OpenAPI drift; 9213 frontend tests.Live re-check. Run on
dc274655on a new disposable AWS fixture: two accounts, private Versa GPT-5.5, synthetic data. It passed all nine items it covered: D-1, P-1, F3 (three times), NEW-1, F1, F2, F5, F-1, and no unrequested navigation to Crew. The fixture was torn down and independently verified.New findings from the re-check, none leaking data:
Not re-run on
dc274655: the three-user, institution, mixed GUI and CLI, fairness and self-test lanes.Hosted CI on
55a345a8(dc274655plus the docs commit that records the above) is all green on first attempt, with no re-runs:Needs human security review. The five final-polish items are added to the checklist above.
Records. Details are in the Final polish section of
docs/research/biorouter-crew/evidence/ui-redesign-acceptance-2026-09.mdand in the handoff's close-out. This PR stays draft.Last fixes (2026-09-25)
After the live re-check, six commits (
b5a0caaa0…715c59b5d) fix NEW-2 (Crew now follows the daemon's re-dial when it opens on a connection that dropped elsewhere), NEW-3 (a revoke the daemon confirmed after a 503 is dated in Past access and keeps its row), NEW-4 ("Confirmed." stays until dismissed or the pane is left) and two re-check observations: the chat's Crew bar offers Revoke while offline, and a workspace-ended grant reads "Ended: Crew settings changed". The daemon now also holds F1 itself: Edit in place,/reply's history write-back and diverge on a chat whose Crew access has ended are refused with a 403 carrying the turn's sentence, before anything stored changes (new offline binarycrew_history_rewrite_hold). These fixes are verified by regression tests that fail on each parent and by a local gate that was ALL_GREEN on715c59b5d(--workspace --lib --bins8542 passed, 0 failed; 18 integration binaries; no OpenAPI drift; 9229 frontend tests). No live fixture ran, so none of them has been checked in a running app. Still open: the offline consent form's "a channel you can't see",replaced_grantsnot listed in the desktop, and human review, whose checklist above now includes the daemon hold. Details are in the evidence record's "Last fixes (2026-09-25)" section.Hosted CI on
7b5482f7(715c59b5dplus its docs commit): all green on first attempt, with no re-runs. Rust 36200867724 (guards, serve, macOS, Ubuntu and Windows tests, both cross-checks); Frontend 36200867727; Apps smoke 36200867730; Computer Use native payloads 36200867747; Check Commit Messages 36200867751 (pull_request) and 36200864129 (push).Type of Change
AI Assistance
GPT-6 Astra implements and reviews product code. GPT-5.6 Luna writes tests, runs validation and drives the app.
Project lineage
The README, NOTICE, governance, the landing about page, the research reports, the release notes and the history docs now describe Biorouter as an independent project inspired by Goose and other agents (Claude Code, Codex CLI, Gemini CLI, OpenHands, Aider, Cline, OpenCode and ForgeCode), not as a fork (
45da68c6b). NOTICE keeps the Block, Inc. attribution lines ("This product includes software developed by Block, Inc." and "Copyright 2024 Block, Inc.") verbatim, as a legal notice. The functionalGOOSE_environment prefixes in code are unchanged. The published v1.91.0 GitHub release body still contains the old "Biorouter is a fork of Block's Goose" sentence: only the repository copy,docs/releases/notes/v1.91.0.md, changed.User manual
The Crew user manual is in
docs/crew/: 11 pages for members, hosts and IT staff, covering joining and hosting a workspace, teams and channels, messages and files, agents and chat access, privacy, troubleshooting, thebiorouter crewcommands and server administration. The landing site's documentation has a matching Crew page (landing/docs.html,#crew). Both are written from the UI copy, the CLI help and the Crew source, and were made consistent across pages at3811a9b8f, which also stops command blocks on the landing docs page from pushing a step past the right edge. Final word counts (wc -w): 26,878 words in total. README 611, getting-started 1,955, hosting-a-workspace 3,298, joining-a-workspace 2,687, teams-channels-and-people 2,012, messages-and-files 2,080, agents-and-chat-access 2,742, privacy-and-security 1,882, connections-and-troubleshooting 2,791, command-line 4,156, administration 2,664. The landing Crew page has about 4,081 words of text.🤖 Generated with Claude Code