Skip to content

feat: implement group chat and chat store with MCP server integrations#7

Open
fannnzhang wants to merge 359 commits into
mainfrom
feature/mobile-auth-and-agent-session
Open

feat: implement group chat and chat store with MCP server integrations#7
fannnzhang wants to merge 359 commits into
mainfrom
feature/mobile-auth-and-agent-session

Conversation

@fannnzhang

@fannnzhang fannnzhang commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces room-first group chat plumbing and chat-store-backed MCP integration for agents.

  • Adds queued MCP room commands for request_agent_help and mention_user, including persistence, claiming, completion/failure state, and MCP tool permission policy.
  • Passes each agent's source identity and bounded-room MCP policy through the agent runtime when chat MCP is enabled.
  • Lets the TUI poll pending MCP room commands, dispatch agent mentions into the room flow, surface user mentions, and expose embedded chat MCP policy flags.

Impact

Agents can now read the bound room, ask another Minos agent for help through MCP, or mention the user without escaping the room-first chat model. The TUI remains the dispatcher for those queued commands, so room history and visible agent activity stay coordinated.

Validation

  • cargo fmt --all --check
  • git diff --check HEAD~3..HEAD
  • cargo test -p minos-chat-store -p minos-agent-runtime -p minos-tui

Known Validation Gap

  • cargo test -p minos-chat-store -p minos-agent-runtime -p minos-tui -p minos-daemon currently fails in existing minos-daemon integration tests because those tests still construct old BackendState fields and relay sender types.

These vars were used by the backend in plan 05's backend-assembled-QR
model. ADR 0016 superseded that — clients now have CF Access tokens
and backend URL baked at build time. The vars haven't been read by
backend source for a while.

- error.rs fixture: drop the MINOS_BACKEND_ prefix on the CF var name
  in the CfAccessMisconfigured display test.
- cloudflare-tunnel-setup.md §7a: replace the dead-var configuration
  block with a clear note that the backend doesn't need them.

Historical references in docs/adr/0014 and docs/superpowers/plans/05
get superseded-by banners in Phase 8.
Both crates' build.rs gain release-mode panic and debug-mode
cargo:warning when MINOS_BACKEND_URL is not in the build environment.
Combined with Phase 5's Pre-Build hook this is two layers of guard
against the localhost-baking bug:

- Layer 1 (Xcode): Pre-Build script fails the build if MINOS_BUILD_VIA_JUST=1
  is absent (catches IDE Build/Run).
- Layer 2 (cargo): build.rs panics in release if MINOS_BACKEND_URL is
  absent (catches CLI invocations that bypassed just but somehow set
  MINOS_BUILD_VIA_JUST=1).

Debug builds still emit a visible warning so the silent localhost
fallback is never invisible.

cargo auto-discovers build.rs at package root; no Cargo.toml edit
needed for the new daemon build.rs.
- README.md: rewrite Local setup around `cp .env.example .env.local`
  and just recipes; remove direct flutter run / Xcode IDE Build paths
  (now blocked by the Pre-Build guard).
- apps/mobile/README.md: same shape, points readers at workspace just.
- docs/adr/0018-just-config-pipeline.md: new ADR documenting the
  decision (single .env.local, justfile entry point, Pre-Build hook,
  release fail-fast, secret hygiene).
- docs/ops/secrets-rotation.md: runbook for CF Access service token
  rotation and JWT secret rotation.
- ADRs 0013, 0014, 0016: status banners pointing at 0018.
- justfile: add rotate-cf-access recipe (cats the runbook).
The new build.rs (Phase 7) emits a cargo:warning on debug builds when
MINOS_BACKEND_URL is unset. Pinning the dev-fallback URL at the job
level keeps the warning quiet without changing build behavior — both
linux and macos lanes do debug builds only today, and minos_domain's
DEV_BACKEND_URL constant is exactly this string.

The trailing release-job TODO comment (previously plan-05-flavored)
is rewritten to use `just build-mobile-ios Release` per ADR 0018,
including the secret-injection shape and the PR-from-fork guard.
Bundled snapshot of feature/mobile-auth-and-agent-session local work:

- mobile (flutter): app_shell_page + pairing_page rewrite; drop the
  top-level `pairing` route in favour of an entry-point inside the
  shell; rename ConnectionState UI labels and the matching
  `threadListOffline` enum (formerly `threadListMacOffline`) so they
  describe the device↔server link rather than the Mac peer (mobile has
  no PeerState wiring — Event::PeerOnline/PeerOffline are dropped in
  recv_loop, so the old "Mac …" labels were misleading).
- mobile (rust core): client.rs surface tweaks, minos.rs additions,
  FRB bindings regenerated.
- daemon: relay_client + keychain_store adjustments.
- agent-runtime: exec_jsonl extensions.
- macos app: AppState + MenuBarView + DaemonBootstrap follow-ons.
- justfile + docs (ADR 0018, README, unified-config-pipeline-design):
  config pipeline polish.
Mirror the existing apps/mobile/.fvm/ rule for the workspace-root FVM
config, and exclude third_party/ which holds local checkouts of fire,
openwire, and remodex used as design references.
Pre-existing test that newer dart format wants collapsed onto one
test() call — applied so check-all stops failing on this file.
Stopping an exec session now snapshots a ResumableExec (agent + started_at
+ codex_session_id) keyed by thread id, so a subsequent send_user_message
into that thread re-mints the active session instead of returning
AgentNotRunning. Drop the synthetic thread/archived event so a stopped
thread can be revived seamlessly.

The backend's raw_events store grows insert_assigning_seq, which keeps
exact-payload retransmits idempotent but appends at a fresh backend seq
when a resumed daemon reuses a process-local seq with new payload —
otherwise restart-after-resume silently drops new Codex output.

Also satisfies clippy 1.95 warnings carried over from the prior
checkpoint (unnested_or_patterns + too_many_lines on
normalize_event_msg/normalize_response_item and the e2e resume test).
Migration 0010 drops the single-pair-per-device triggers; one Mac may
now be paired with multiple iOS clients and one iOS may pair multiple
Macs. Removes the spec §10.2 R4 "refuse if already paired" check —
the only remaining PairingStateMismatch is self-pairing. Forget paths
clear device secrets only when the side has no remaining pair AND is
not an AgentHost (one Mac stays online for the other clients).

Login no longer revokes other devices on the same account: introduces
refresh_tokens::revoke_all_for_device so login rotates only the caller's
own tokens, and stops force-closing other iOS WS sessions in the
registry. close_account_sessions remains for explicit admin/revocation
flows.

Pair-fanout follows: ingest::broadcast_to_peers_of iterates every peer
returned by pairings::get_peers; envelope::handle_forward routes Mac
JSON-RPC replies back to the originating iOS by tracking
(request_id -> requester) on the AgentHost handle, since paired_with no
longer uniquely identifies a peer.

Cargo.lock drops the now-orphaned rustls-pemfile transitive.
…are UI

ActiveSessionController grows sendToThread(threadId, agent, text) so the
thread page can send into a known thread id no matter the global state.
ThreadViewPage stops always reusing the live session: when the user
opens a historical thread, the page renders against a view-scoped
ActiveSession that's only "live" when the session is bound to that same
thread id. Combined with the daemon's resumable-exec change, follow-ups
into a stopped thread call send_user_message(session_id) and the daemon
re-mints the session instead of erroring out.

SecurePairingStore + MinosCoreProtocol gain peerDisplayName get/set so
the QR's host_display_name survives reboots; AppShell's runtime row and
profile header read it via a peerDisplayNameProvider, falling back to
"Agent Runtime" when missing. ForgetPeer wipes it.

Test fixtures updated for the new sendToThread path and for the peer
display name surface.
Hash bump for ActiveSessionController (sendToThread) and
PairingController source changes from the previous commit.
Output of `codex app-server generate-json-schema --out ./schemas`. Used
as the source of truth for the upcoming `minos-codex-protocol` typed
crate (see docs/superpowers/specs/codex-typed-protocol-design.md).
Spec for the upcoming `minos-codex-protocol` crate: vendored typify
codegen of the JSON schemas in `schemas/`, hand-written `ClientRequest`
/ `ClientNotification` traits, and post-processor-generated union enums
(`ServerRequest`, `ServerNotification`) for typed inbound dispatch.

Drives a follow-up refactor of `minos-agent-runtime`:
- `codex_client.rs` gains `call_typed` / `notify_typed`.
- `runtime.rs` migrates 6 hand-rolled JSON-RPC sites to typed; deletes
  the `thread_id_from_response` heuristic.
- `approvals.rs` is rewritten against the typed `ServerRequest` enum,
  fixing two latent bugs surfaced while authoring this spec: v2
  approval method-name drift and non-conformant `decision: "rejected"`
  reply payloads (each approval response has its own decision schema).

Supersedes §10.1 (wire format) and §6.4 (approval method names) of
`codex-app-server-integration-design.md`.
Detailed task-by-task plan for building `minos-codex-protocol` and
migrating `minos-agent-runtime` onto it. Phases A (standalone crate),
B (runtime migration), C (ADR + cross-references), D (verification),
each with TDD-style steps, exact file paths, and ready-to-paste code
snippets.

Spec: docs/superpowers/specs/codex-typed-protocol-design.md
New crate `minos-codex-protocol`: vendored typify codegen of the JSON
schemas in /schemas, hand-written `ClientRequest` / `ClientNotification`
traits, and post-processor-generated `ServerRequest` / `ServerNotification`
tagged enums for typed inbound dispatch. Driven by `cargo xtask
gen-codex-protocol`.

The crate is independent (depends only on serde/serde_json). No
`minos-agent-runtime` changes in this commit; the migration follows in
the next refactor commit.

Spec: docs/superpowers/specs/codex-typed-protocol-design.md
CodexClient gains `call_typed` / `notify_typed`. runtime.rs migrates 5
call sites + 1 notification site off `serde_json::json!` onto typed
params from minos-codex-protocol; deletes the `thread_id_from_response`
heuristic, the `initialize_params` helper, and `thread_id_request_params`
since typed responses make them unnecessary.

approvals.rs is rewritten against the typed `ServerRequest` enum. Two
latent bugs fixed in the rewrite:
- v2 approval method names are `item/commandExecution/requestApproval` /
  `item/fileChange/requestApproval` / `item/permissions/requestApproval`
  (namespaced); the old hand-maintained `APPROVAL_METHODS` string list
  used the v1 names, so v2 prompts silently fell through to "warn,
  don't reply".
- The auto-reject payload was `{"decision": "rejected"}` for all five
  approval shapes, but no schema accepts that string. The typed rewrite
  picks `Denied` (v1 ReviewDecision) / `Decline` (v2 CommandExecution +
  FileChange) / empty `GrantedPermissionProfile` (Permissions, which
  has no `decision` field at all) per variant.

Spec: docs/superpowers/specs/codex-typed-protocol-design.md
ADR records the typed-codegen choice and rejected alternatives. Old
`codex-app-server-integration-design.md` gains pointer notes in §4.5
and §6.4 directing the reader to `codex-typed-protocol-design.md` for
the current wire format and approval handler shapes; original prose
preserved as historical context.

(ADR numbered 0019 since 0011 was already taken by
`0011-broker-envelope-protocol.md`.)
Pre-deployment: replaces device-keyed pairings with account-keyed
account_mac_pairings (migration 0012, next commit) per ADR-0020.

Workspace tests will fail until Phase E lands (existing pairings.rs
still queries dropped table). check-all skipped intentionally; full
gate restored after Phase E commit "mint Mac secret only; insert
account_mac_pairings".
New pair model is (mac_device_id, mobile_account_id); paired_via_device_id
records the mobile device that performed the scan for audit only.

check-all still skipped at this commit — see B1 commit message.
…ptional

Per ADR-0020 server-centric routing. Workspace consumers (backend,
mobile, daemon) intentionally broken until Phase D-K migrate them;
check-all skipped for this transitional commit.
Per ADR-0020 server-centric routing. iOS no longer receives a device
secret from POST /v1/pairings — it's bearer-only post-pair. New
MeMacsResponse + MacSummary cover GET /v1/me/macs.

Workspace consumers (backend, mobile, daemon) intentionally broken
until Phase D-K migrate them; check-all skipped for this transitional
commit.
Phase D of plan 11. Adds the new (mac_device_id, mobile_account_id)
keyed store module mandated by ADR-0020 and removes the obsolete
device-keyed `pairings` module. Also seeds two test_support helpers
(`insert_account`, `insert_ios_device`) so the new module's tests —
and the upcoming Phase E handler tests — can build a populated pool
without duplicating boilerplate across files.

Adapted from the plan's literal code in three places:
  * `mobile_account_id` rides as `&str` / `String` (no `AccountId`
    newtype exists in this codebase yet — accounts are opaque UUID
    strings everywhere).
  * Errors use the existing `BackendError::StoreQuery` /
    `StoreDecode` variants (the plan's `BackendError::Storage`
    variant doesn't exist).
  * `DeviceId` round-trips via `Uuid::parse_str(...).map(DeviceId)`
    rather than `String::parse::<DeviceId>()` since `DeviceId` is a
    plain `pub struct DeviceId(pub Uuid)` without `FromStr`.

The `exists` query uses `SELECT pair_id` rather than `SELECT 1 AS hit`
because sqlite reports integer literals as untyped, which makes the
`sqlx::query!` macro infer `Option<()>` and fail to compile.

Workspace is intentionally non-building at this commit. Five handlers
(`http/v1/pairing.rs`, `http/v1/threads.rs`, `http/ws_devices.rs`,
`ingest/mod.rs`) still reference the now-deleted
`crate::store::pairings` module. Phase E rewrites those handlers
against `account_mac_pairings::*` and restores the build. Do not push
this commit alone to a shared branch — pair it with Phase E.

Sqlx offline cache: 6 stale `pairings`-table query JSONs deleted, 5
new `account_mac_pairings` query JSONs added. The remaining 11 cache
entries (devices, accounts, tokens, threads, raw_events, refresh_tokens)
are untouched.
iOS row stays at secret_hash NULL per ADR-0020. Pair-table insert moves
from the consume transaction to the handler (post-commit) since the
handler has bearer's account_id; idempotent via ON CONFLICT DO NOTHING.

PairingOutcome drops consumer_secret. Compensation paths now call
account_mac_pairings::delete_pair instead of forget_pair. PairingService
no longer carries forget_pair / clear_secret_if_unpaired_non_host /
normalize_pairing_insert_error — the (mac, account) pair model owns
state and has no per-device idempotency rule to normalise.

Legacy DELETE /v1/pairing route stubbed to 410 Gone (forget_pair was
removed); bearer-authenticated DELETE /v1/pairings/{mac_device_id}
follows in Task E2.

tests/v1_pairing.rs reworked: PairResponse no longer carries
your_device_secret; happy-path test asserts EventKind::Paired.your_device_secret
is Some on the Mac side and that account_mac_pairings::exists is true.
delete_pairing_* tests collapsed to the 410-Gone smoke check (legacy
forget semantics are gone; full e2e returns in Phase M).

Workspace remains in transition (threads, me, ws, ingest, envelope still
use legacy pairings — Phases F-H restore the build); check-all skipped.
Bearer-authenticated; legacy DELETE /v1/pairing stays 410 Gone with
"Use DELETE /v1/pairings/{mac_device_id}" message.

Handler resolves the bearer's account, deletes the (mac, account) row
from account_mac_pairings, and best-effort pushes Event::Unpaired to
the Mac's live session if registered. 404 when the pair does not
exist; 400 on malformed mac_device_id.

E2E test deferred to Phase M (test infrastructure landing there).
check-all still skipped — workspace in transition (threads, me, ws,
ingest, envelope).
Adds DeviceRole as a 3rd arg to classify() to document the
ADR-0020 split: iOS rows authenticate via bearer alone (secret_hash
stays NULL → UnpairedExisting); Mac rows still use the secret-hash
verify branch.

Workspace still has 4 errors in threads/me/ws/ingest/envelope —
remaining Phase F-H work; check-all skipped.
Threads handlers no longer assert authenticated_with_secret; bearer
JWT is the trust root after ADR-0020. require_paired_session is
renamed require_authed_session and drops the device-keyed
pairings::get_pair lookup — list/read are scoped by the bearer's
account_id (one account may be paired to multiple Macs).
ingest::history::read_thread now scopes by account_id (not owner
device_id) so iOS callers see all paired Macs' threads via single API.

v1_threads test helpers switch to account_mac_pairings::insert_pair;
the legacy "unpaired returns 401" test is reframed as
"missing-bearer returns 401" since pairing is no longer device-keyed.

http/v1/auth.rs needed no edits: grep for authenticated_with_secret
shows no occurrences in that file. check-all skipped — me.rs,
ingest, envelope still pending Phase G/H.
Pre-upgrade auth already routed iOS through UnpairedExisting (NULL
secret_hash); the only remaining wire-up was killing the
pairings::get_pair call in revalidate_live_session_auth — replaced
with Ok(None). Multi-mac presence reconstruction lands in Phase G.

The reconnect test that asserted Some(peer) is reframed as
returns_none_for_authenticated_mac_reconnect since paired_with is
no longer single-valued; module doc updated to match.

Workspace still has 3 errors in me.rs, ingest/mod.rs, envelope/mod.rs
— remaining Phase G/H work; check-all skipped.
…d_with slot

iOS Forward must stamp target_device_id; backend validates against
account_mac_pairings::exists for the caller's account. SessionHandle no
longer carries a single-peer paired_with slot — that field is removed.

Heartbeat collapses to a single PAIRED_TIMEOUT (90s); the legacy
notify_live_peer_disconnect / notify_live_peer_connected paths are
dropped, and activate_live_session emits Event::Unpaired as the initial
frame for every fresh socket. Comprehensive multi-mac presence
broadcasts on connect/disconnect are deferred to Phase M.

Existing envelope and ws_devices unit tests rewritten against the new
contract — no #[ignore]; the rebuild happens against memory_pool +
account_mac_pairings::insert_pair fixtures so they still cover happy
path, peer-offline-via-no-pair-row, peer-backpressure, and Mac-reply
correlation by jsonrpc id. Session registry's Arc strong-count test
re-pointed at account_id (same Arc shape) since paired_with is gone.

check-all skipped — me.rs (Phase H) still pending.
- render_chat now builds only visible window of lines
- RenderCache stored on UiState for the active chat
- apply_selection_with_offset handles absolute row mapping
- selected_text accepts cache (full-rebuild preserved for now)
- tracing spans added to render_chat and render_group_chat

Co-Developed-By: opencode <noreply@opencode.ai>
…ments

The separator before each item (idx > 0) is part of that item's rendered
segment in render_chat. item_starts[idx] must point to the separator line,
not the content line. This fixes an off-by-one in line_offset_within_first_segment
that caused wrong lines to be displayed when scrolled mid-item.
…aries

Adds PromptHistory to InputState so Up/Down at the first/last visual row
(after soft-wrapping) browse previously submitted prompts instead of moving
the cursor. Esc or Down-past-end restores the in-progress draft. History is
recorded on submit for both room and agent inputs.

- New PromptHistory struct with previous/next/cancel/record/is_browsing
- New pub helpers visual_cursor_row and visual_row_count in input_bar
- New InputState::load_history_entry to swap content and reset cursor
- Room and agent input Up/Down/Esc handlers updated; submit records history
- 21 new unit tests covering PromptHistory and the visual-row helpers
- InputPicker enum replaces agent_picker: None | Agent | Path
- PathCandidate and InputPathPickerState types
- active_path_range detects path-like tokens (/ or ~/)
- list_path_candidates reads directory entries with prefix filter
- Tab priority chain: path picker accept → agent picker noop →
  path token opens picker → cycle focus
- Path completion supports ~/ (dirs crate), relative, absolute paths
- Directory candidates auto-append / and re-trigger completion
The function returns the index of the last visual row, not a count.
Renaming prevents future off-by-one misuse. Also adds a clarifying
doc comment on selected_text's cache parameter.
Add workspace_path column (migration 0006), thread it through ProjectRow
and store.create_project, and have list_projects return the stored value
with a synthetic fallback only when NULL.
Adds list_projects, create_project, list_project_threads, and
start_agent_in_project to AgentBackend. start_agent_in_project takes no
prompt (the prompt flows via the AgentStartedForPrompt effect later).
Implements them on DaemonBackend (RPC), EmbeddedBackend (in-memory), and
TestBackend (with with_projects / with_project_threads builders).
…Effect/AppEvent/UiState

Introduces the nav type layer (Projects/Sessions/Session/AgentDetail)
and extends Action, EffectResult, Effect, AppEvent, and UiState with
project-related variants and fields. Handlers, executors, and renderers
are added in subsequent phases; temporary no-op match arms keep the
crate compiling in the meantime.
… forwarding

Replaces Phase 2's temporary no-op arms with real handlers: pure nav
handlers (projects/sessions/session levels, dialogs, startup prompt),
EffectResult arms that mutate ui state and emit follow-up effects
(AgentStartedForPrompt for project sessions), effect executors that
spawn backend calls, and ensure_project_session_visible bridging
project sessions to the legacy thread list for rendering.
Adds per-nav-level key intercepts at the top of key_to_mapping
(projects/sessions/session/dialogs/startup-prompt) that take
precedence over the legacy focus-based mappings. Adds
resolve_startup_project which loads projects on init, matches the
current workspace to a project, and either enters Sessions level or
offers to create a new project. Existing legacy-behavior tests are
pinned to NavLevel::Session so they keep exercising the chat path.
render_ui now dispatches by nav_level: Projects and Sessions render the
new project shell (list + sidebar + optional input bar), Session and
AgentDetail fall through to the legacy chat render. Adds Renderable
widgets for project list, session list, and the create-project dialog.
Also reconciles agent_detail_visible with nav Uplevel so Esc cleanly
closes the detail panel when leaving Session level.
…ture-tui

Adds nav_integration tests covering projects navigation, dialog flow,
Esc behavior per level, and project-bound session creation. Updates
architecture-tui.md with the NavLevel/NavAction layer, the new
AgentBackend project methods, project AppEvents, and the nav-level
render dispatch.
…tegration tests

Final review found that AppEvent::ProjectsLoaded / EffectResult::ProjectsLoaded
were fully wired but never emitted (resolve_startup_project loads inline).
Removes the dead pathway per latest-only policy. Adds two integration tests
covering the cross-phase session-entry flows that were the main coverage gaps:
start_new_session_via_input (SubmitSessionInput -> StartAgentInProject ->
ProjectSessionStarted -> AgentStartedForPrompt) and
open_existing_session_bridges_into_legacy_thread_list (OpenProjectSession ->
ensure_project_session_visible -> ui.threads).
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