Commit Graph
55 Commits
Author SHA1 Message Date
Miguel Palhas 88b6bba9f2 feat(maestro): show only the program on session cards
ci / web (push) Successful in 34s
ci / rust (push) Successful in 3m56s
ci / flake (push) Successful in 7m8s
The command fragment named the full command line, so a session spawned
with `claude --dangerously-skip-permissions` spent the whole line on a
flag. Cards now name the program only — its icon in the web client, its
bare name in the TUI — and keep the full command line in the tooltip.

Adds `maestro_protocol::display::command_program`, the Rust counterpart
of the web's `splitCommand`.
2026-09-01 11:11:39 +01:00
Miguel PalhasandClaude Opus 5 c9cfdb5668 feat(maestro): one-word handles, quieter project boxes
Session handles are one word from a single 218-entry list — instruments,
forms, notation and theory, tempo and dynamics — replacing the
adjective-noun pair. A batch is shuffled, so it repeats only once the
wordlist runs out.

Project boxes show the parent directory instead of the full path: the
last segment is already the name line above it. The last command is gone
from the box, and picking a project no longer pre-selects it — the
directory and what runs in it are independent choices. Both changes land
in the TUI picker too, which mirrors the web form's rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 22:40:49 +01:00
Miguel Palhas 1589d58ca0 feat(maestro): sidebar cards show diff stats and tool icons
Line 2 of a session card spelled out its blocked work ("1 uncommitted
file") and its command ("claude"). Both now render as marks: the
breakdown as diff notation, and the command's leading program as its
tool's icon in the web client.

The probe carries the line delta of the tracked changes — staged and
unstaged together, against HEAD — so `git add` never makes the counter
drop. Untracked files stay a file count; counting their lines means
reading every one off disk. A tracked change with no line delta falls
back to `~N` rather than claiming `+0 -0`.

The TUI keeps its command as text: a terminal cannot be assumed to have
a font carrying tool glyphs.
2026-08-31 14:54:54 +01:00
Miguel PalhasandClaude Opus 5 d39fbdbbfc feat(maestro): drop retained worktrees and header debug toggle
Removes the retained-worktree store end to end: the daemon's state-dir
store, GET/DELETE /api/worktrees, `maestro worktrees list|reclaim`, the
TUI overlay behind `w`, and the web `#/worktrees` view and header link.

The kill guard stays. A forced kill and a dirty natural exit now leave
the worktree on disk untouched instead of parking it in a store, so
removing one is an ordinary `git worktree remove`. A pristine tree is
still removed on exit. DELETE /api/sessions/:id always answers 204.

The header's debug button also goes; Ctrl+Shift+D and `?hud` still
toggle the HUD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 09:00:52 +01:00
Miguel PalhasandClaude Opus 5 345650172b feat(maestro): rename a session over PATCH /api/sessions/:id
The daemon trims and caps the name, persists it to the sidecar under the
sessions lock, and broadcasts SessionUpdated. A blank name clears
metadata.name so the card falls back to the command, matching the rule the
spawn form already uses.

Refs #125

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 11:26:12 +01:00
Miguel Palhas 975f3ef0c5 feat(maestro): move path-abbreviation helpers into maestro-protocol
Port abbreviateCwd, abbreviateFolderName, mergeLinear, and
sessionTreePath from web/src/{types,tree}.ts into a maestro-protocol
display module, so the TUI sidebar can group sessions with the same
labels the web UI uses (#117). abbreviate_folder_name returns the
collapsed stem and last segment separately instead of escaped HTML,
so a ratatui caller can style the stem without parsing markup.

maestro-cli's own abbreviate_dir moves into the new module; the CLI
and the worktrees list now call the shared implementation.

Closes #117
2026-08-25 12:31:43 +01:00
Miguel Palhas 4f0e887a50 fix(maestro): parse reported activity states strictly 2026-08-24 22:05:40 +01:00
Miguel Palhas 81312aa0c5 merge: sidebar-cards/110-lenient-activity 2026-08-24 22:02:04 +01:00
Miguel Palhas 5c4e083c2d feat(maestro): add activity_detail to Session and the activity-report wire types 2026-08-24 21:32:27 +01:00
Miguel Palhas 71add0215a fix(maestro): deserialize unknown ActivityState variants as Idle
A newer daemon's unknown activity variant failed the whole Session
deserialization on older clients. Hand-written Deserialize maps any
unknown string to Idle; non-string values still error. (#110)
2026-08-24 21:28:30 +01:00
Miguel Palhas fc6f3469b0 feat(maestro): add Error activity state to the wire 2026-08-24 19:21:44 +01:00
Miguel Palhas b95b5c04c1 feat(maestro): add activity_since to the session wire 2026-08-24 19:19:33 +01:00
Miguel Palhas 003e3839bc feat(maestro): carry a blocked-work indicator on Session (#83)
Session.blocked_work is Option<InTheWay>, refreshed by the daemon's
watcher; None for worktree-less sessions and until first probed.
Defaults on the wire so older payloads stay forward-compatible.
2026-08-24 12:58:16 +01:00
Miguel Palhas 31702e03e3 refactor(maestro): drop cleanup_on_kill from the wire and the CLI
Nothing has honoured WorktreeSpec.cleanup_on_kill since #56, and after
#79 and #81 survival is decided by what is in a worktree, not by what
the spawn asked for. A field that lies is worse than a missing one.

Removes the field from the protocol, the --cleanup-on-kill flag from
the CLI, and the tests asserting its default.
2026-08-24 12:08:41 +01:00
Miguel Palhas f67803acf5 Merge remote-tracking branch 'origin/blitz/worktree-lifecycle' into wl/81-force-retains
# Conflicts:
#	crates/maestro-protocol/src/client.rs
2026-08-24 11:35:08 +01:00
Miguel Palhas bb2f439302 feat(maestro): kill outcome payload and force query in protocol
DELETE /api/sessions/:id gains a force query param (#81). A forced kill
answers 200 with KillOutcome, naming what was retained and why; the
unforced kill keeps its 204. DaemonClient::kill_session takes the flag.
2026-08-24 11:20:00 +01:00
Miguel Palhas 86443d195e merge: base update from blitz/worktree-lifecycle 2026-08-24 11:14:12 +01:00
Miguel Palhas 62581d9f85 merge: refuse a kill when the worktree holds unsaved work
Collapses the two probes #79 and #80 grew in parallel into one:
worktree_probe::in_the_way is the single predicate behind the kill
refusal, the retained list, and the natural-exit classifier to come.
Keeps #79's in-worktree resolution (detached HEAD, missing directory)
and #80's InTheWay payload, and drops the duplicate UnsavedWork type.

Closes #79
2026-08-24 11:09:24 +01:00
Miguel Palhas 267f8a6712 feat(maestro): DaemonClient calls for retained worktrees 2026-08-24 11:06:30 +01:00
Miguel Palhas 898c3cc763 feat(maestro): shared unsaved-work probe for worktrees (#79)
One function, unsaved_work(), returns the structured breakdown —
uncommitted tracked changes, untracked non-ignored files, unpushed
commits — so the kill guard and the natural-exit classifier (#82)
answer from the same place. Ignored files never count: every Rust
checkout has a target/, and counting those would leave no session
killable.

WorktreeManager::remove drops its own dirty-tree guard; git itself
still refuses a dirty tree, and the real guard moves onto the kill.
2026-08-24 10:59:08 +01:00
Miguel Palhas cc8af94254 feat(maestro): worktree routes and retained-worktree payload in protocol 2026-08-24 10:48:12 +01:00
Miguel Palhas e9368bb242 fix(maestro): fourth-review findings across daemon, tui and web
The existing-branch picker could never succeed in either client: both
treated the main checkout's HEAD as available, and since that is usually
`main` it became the default pick — git then refuses the second checkout.
A TUI test had baked the bug in by asserting the selection lands on the
`is_head` entry. Both pickers now treat "checked out anywhere" as held.

Worktree paths were still not injective. Flattening `/` and `.` to `-`
made `feat/x`, `feat.x` and `feat-x` share one directory, and the
override root keyed on the repo basename alone, so `~/a/maestro` and
`~/b/maestro` collided. A branch name is already a valid relative path —
git stores it as refs/heads/feat/example — so it is used verbatim and
nests under `worktrees/feat/`; the override root gains a digest of the
full repo path. Git forbids holding both `feat` and `feat/x`, so the
nesting cannot collide for valid refs.

Also:

- Recovered sessions dropped their worktree association, so after a
  restart a kill cleaned up nothing and the leftover directory blocked
  that branch forever. It is persisted on Session now.
- The nesting guard compared git's resolved paths against an unresolved
  one, so it silently passed on any symlinked repo path — and that guard
  is what stands between a kill and an inner tree's uncommitted work.
- Draft loads ran on the TUI event loop: /api/projects on a cold cache
  walks all of $HOME, freezing keys, frames and attached panes for up to
  the request timeout. They are detached tasks posting to a channel now.
- Fast-forward in the web client created the session and never mounted
  it, so `N` looked like a dead key while the TUI attached.
- The fast-forward fallback seed forced the worktree toggle off, against
  both a normal draft and the TUI.
- The projects background refresh honoured the TTL, so two of every three
  ticks were no-ops and requests paid for the rescan instead.
2026-08-24 00:10:35 +01:00
Miguel Palhas 8ed44e785d test(maestro): add branch/project clients and config seeding to e2e harness 2026-08-23 22:55:52 +01:00
Miguel Palhas cc1b3d45b8 merge: maestro-names crate and GET /api/names (#53)
# Conflicts:
#	crates/maestro-daemon/src/local_api.rs
#	crates/maestro-protocol/src/routes.rs
2026-08-23 21:20:08 +01:00
Miguel Palhas 2a84e07f7d feat(maestro): GET /api/branches — local branches and worktree occupancy (#55) 2026-08-23 21:14:16 +01:00
Miguel Palhas a970ba89c2 feat(maestro): GET /api/names serves name batches (#53) 2026-08-23 21:13:46 +01:00
Miguel Palhas 33fb50be6e feat(maestro): Project wire type and PROJECTS route (#54) 2026-08-23 20:32:29 +01:00
Miguel Palhas d8acc3adde feat(maestro): always clean up worktrees on kill behind a dirty-work guard 2026-08-23 20:17:08 +01:00
Miguel Palhas 85e7749e5a feat(maestro): worktree specs can check out an existing branch 2026-08-23 20:05:26 +01:00
Miguel PalhasandClaude Opus 5 b20651c33e fix(maestro): close three holes in the wire contract
ci / web (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 1m40s
ci / web (push) Successful in 35s
ci / rust (push) Successful in 2m23s
Each was a typed field that type-checked on both sides and carried
nothing across.

SessionStatus::Dead was never constructed by the daemon, yet the TUI, CLI
and SPA all render it and DESIGN.md specifies the exited badge. Sessions
were removed on death, never marked. They are now marked Dead and kept
until the operator dismisses them, with the PtyBridge sealed rather than
dropped: attached clients still get a Close frame (NAP-226), and the
replay buffer survives so a later attach can read what a crashed agent
printed. Previously that output was destroyed. Dead sessions are
read-only and do not survive a daemon restart.

CreateSessionRequest.env was accepted, stored, echoed back and dropped —
new_session_piped had no env parameter. It now reaches tmux as sorted
`-e KEY=VAL` flags. A key containing `=` or NUL is rejected, since tmux
would parse it as a different assignment than the caller wrote.

PROTOCOL_VERSION was written by the daemon and read by nobody. Adds
`maestro_protocol::is_compatible`, compared on the major component only;
`maestro list` shows the daemon version and the TUI warns on mismatch.

Also stops mapping every non-404 session error to 502: an invalid env key
is a client mistake and now answers 400.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:27:51 +01:00
Miguel PalhasandClaude Opus 5 148f97fa71 chore(maestro): drop the unused tokio dep from the client feature
ci / rust (pull_request) Successful in 5m29s
reqwest brings its own runtime requirement; maestro-protocol never names
tokio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:17:46 +01:00
Miguel PalhasandClaude Opus 5 01a1073943 feat(maestro): own the /api surface in maestro-protocol
The crate held the wire types but not the wire surface, so every consumer
re-derived routes, URL construction and the error envelope.

Adds `routes` (path constants, axum patterns, and a single fallible
`ws_base`), the `ApiError` envelope, `DaemonInfo`, and a `DaemonClient`
behind an optional `client` feature. `SpawnPreset` moves here because it
rides `GET /api/info`; `maestro-config` re-exports it so the on-disk
schema and the payload stay one type.

`ws_base` now rejects a base with no recognised scheme. The five previous
copies disagreed — the CLI errored, the web proxy trusted it, the TUI
passed it through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:11:37 +01:00
Miguel Palhas c856333e4a docs(maestro): describe the single-host architecture
ci / rust (push) Successful in 2m40s
Rewrites CLAUDE.md as the current-architecture reference, drops the
registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the
registry Dockerfile, and marks the dated design specs superseded where
they describe the registry, JWT auth, or cross-host attach.

The installer loses its configure step — every config field now has a
default, so a daemon starts with no config file at all.
2026-08-22 10:26:49 +01:00
Miguel Palhas d006f4a7bd refactor(maestro): drop registry, p2p, and auth from daemon core
Maestro is now single-host: one daemon owns the machine's sessions and
clients talk to it directly. Removes the registry crate, the daemon's
registry client/broker/catalog mirror, JWT attach auth, cookie auth, and
the perimeter (T2) transport config.

The daemon API is now unauthenticated on its bind address, same trust
model as tmux. Session and CreateSessionRequest lose daemon_id;
RegistryEvent becomes CatalogEvent with no daemon variants; attach moves
to WS /api/attach/:id with no token.
2026-08-22 10:00:24 +01:00
naps62-yoloandClaude Opus 4.8 458c81094b feat(maestro): nest sessions by live working directory, drop labels
Replace the freeform `metadata.label` session grouping with nesting derived
from each session's working directory. The daemon's activity watcher already
polls each tmux pane every tick, so it now also captures
`#{pane_current_path}` and surfaces it as a new `Session.current_cwd`
(serde-default, back-compat); `cwd` stays the spawn dir for worktree
inference, spawn-similar, and recents.

Web catalog builds the tree from `current_cwd ?? cwd`, collapses the home
dir to `~`, and folds single-child folder chains so `~/git/maestro` reads as
one node. CLI swaps its LABEL column for an abbreviated DIR; the spawn forms
(web + TUI) and `--label` flag are removed.

The pane format stays space-delimited (parser splits the first three tokens
and keeps the remainder as the path) so a cwd containing spaces survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:11:23 +00:00
naps62-yoloandClaude Opus 4.8 fd80ae152a feat(maestro): infer session activity state + foreground (Active/Idle/AwaitingInput) (#18)
ci / rust (push) Failing after 2m15s
Add an orthogonal activity dimension to sessions, inferred entirely from
structured tmux/`/proc` state — no PTY byte-stream parsing, no work on the
attach hot path.

Protocol: `ActivityState` enum (Active/Idle/AwaitingInput) + three additive,
serde-defaulted `Session` fields (`foreground`, `activity`, `last_activity_at`)
so the snapshot/diff flow stays forward-compatible with older daemons.

Detection (piggybacks the existing ~2s liveness tick, no new timer):
- foreground command via `tmux list-panes -F '#{pane_pid} #{pane_current_command}
  #{pane_tty}'`;
- quiescence from a per-`PtyBridge` last-read `Instant`, bumped in `run_reader`;
- AwaitingInput via best-effort `/proc` walk to the deepest descendant: state
  `S` + a non-timer `wchan` + an stdio fd on the pane tty. (`/proc/pid/syscall`
  is ptrace-gated and unreadable for own sessions, so `wchan` distinguishes a
  blocking read from a plain `sleep`.)
- combine: recent output → Active; else input-blocked → AwaitingInput; else
  Idle. Emit `SessionUpdated` only on (activity, foreground) change — no diff
  storms at steady state.

`maestro list` gains an ACTIVITY column and shows the live foreground. Pure
combine/parse logic is unit-tested; an e2e asserts `cat` → AwaitingInput and
`sleep` → Idle with correct foreground against real tmux.

Closes #18
Closes #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:51:08 +00:00
naps62-yoloandClaude Opus 4.8 cc8412ca59 feat(maestro): spawn protocol types + relay + daemon HTTP for recents/gitroot/presets (#26)
Expose the daemon-side spawn data (#24 recents, #25 gitroot, #22 presets) to
the web, mirroring the NAP-329 ListDir broker plumbing end to end so it works
across federated hosts.

Wire (maestro-protocol): `RecentDir`/`RecentDirs`/`GitRoot` payloads;
`DaemonInbound::RecentDirs`/`GitRoot` requests + `ForwardRecentDirs`/
`ForwardGitRoot` relay pairs + their `*Result` replies on both enums. All
forward-compatible via the existing `RpcResult` + `#[serde(other)] Unknown`.

Relay (broker + registry + registry_client): home daemon → registry → owning
daemon → back, with the same fail-fast-while-disconnected + RAII pending-entry
cleanup discipline as ListDir. The owning daemon answers RecentDirs from its
spawn history and GitRoot by resolving the path against its home dir
(shared `dirlist::resolve`) then walking up via `gitroot::find_root`.

Daemon HTTP (maestro-daemon):
- GET /api/daemons/:id/recent-dirs → RecentDirs
- GET /api/daemons/:id/gitroot?path= → GitRoot
- Presets ride GET /api/web/info (the documented DaemonInfo sibling) so the
  form needs no extra round-trip; resolved once at startup from [spawn].presets
  (a bad preset list now refuses startup).

Tests: protocol round-trip + forward-compat for the new variants; broker
forward round-trips + fail-fast; HTTP handler tests (self recents/gitroot,
presets advertisement, peer 503 without a registry link).

Closes #26

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:09:18 +00:00
naps62-yolo eb2162889e Merge branch 'updates'
ci / rust (push) Successful in 4m3s
2026-06-19 07:07:59 +00:00
naps62-yoloandClaude Opus 4.8 b6833ede43 refactor(maestro): drop allow_inbound policy gate, keep JWT session binding (NAP-339)
The peer (T2) attach path had a per-pair access-control layer
(`[daemon.policy].allow_inbound`): a target-side allow-list of daemon ids
permitted to broker-attach, enforced inside JWT verification, hot-reloadable via
SIGHUP / `POST /api/policy/reload`, with a `maestro policy` CLI. On a flat-trust
perimeter (the operator's VPN/firewall is the boundary for which hosts connect)
that granularity is unused, and deny-by-default made it config friction —
cross-daemon attach silently failed until every peer was listed.

Remove the allow-list; keep the registry-minted pair-attach JWT and ALL its
binding checks (iss=registry, aud=target, sid=session, exp). The firewall gates
host->port reachability; it does not stop a reachable host from attaching to a
session it wasn't handed or replaying a token elsewhere — that's the JWT's job
and it stays. Only the `sub in allow_inbound` check is dropped.

Removed: DaemonPolicySection / [daemon.policy].allow_inbound (config + daemon),
policy.rs (SIGHUP listener + GET/POST /api/policy), the `maestro policy` CLI,
PairAttachVerifier's allow-list field/reload/snapshot + JwtError::NotAllowed,
and the e2e broker_rejects_when_target_doesnt_allow_inbound test. The NAP-265
pre-upgrade-502 path stays covered by broker_attach_returns_502_on_perimeter_mismatch.

Also dropped the unit's ExecReload=kill -HUP: with no SIGHUP handler left,
default SIGHUP disposition is terminate, so wiring `systemctl reload` to it would
kill the daemon. Re-introducing a per-pair gate later is a verify-time change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:07:55 +00:00
naps62-yoloandClaude Opus 4.8 75d162dea3 refactor(maestro): share cookie name + extraction across CLI and registry (NAP-273 F)
ci / rust (push) Successful in 6m45s
Promote `COOKIE_NAME` and a robust `extract_cookie` (iterates every `;`-pair,
so it works on both `Cookie:` and `Set-Cookie:` headers regardless of position)
into `maestro-protocol`. The registry re-exports them; the CLI drops its weaker
`extract_cookie_value` (which only inspected the first pair) and its duplicate
`COOKIE_NAME`. Both ends of the wire now agree on one definition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:35:30 +00:00
naps62-yolo 473aa948a8 feat(maestro): exclude hidden dirs from spawn-form autocomplete (NAP-329)
ci / rust (push) Successful in 5m6s
2026-06-16 06:41:39 +00:00
naps62-yolo 1f20d70a2e feat(maestro): add ListDir/ForwardListDir wire protocol + DirListing (NAP-329) 2026-06-15 21:11:43 +00:00
naps62-yoloandClaude Opus 4.8 6d7cd0b4c8 feat(maestro): spawn form defaults command to claude + prefills cwd to daemon home dir (NAP-328)
Two web spawn QoL improvements plus the wire support they need:

- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
  daemon change unless the user has typed their own value.

The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.

Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:07:53 +00:00
naps62-yoloandClaude Opus 4.8 daeacc750f feat(maestro): registry relays Forward{Create,Kill} RPC for cross-daemon ops (NAP-327)
Adds the home-daemon → registry control-link RPC that brokers a cross-daemon
session create/kill without putting any session bytes on the registry: the
owning daemon still does the tmux spawn, the registry just relays request and
reply.

- protocol: `DaemonOutbound::{ForwardCreate,ForwardKill}` +
  `DaemonInbound::{ForwardCreateResult,ForwardKillResult}` (+ round-trip tests).
- registry: `Registry::{forward_create,forward_kill}` reuse the existing
  `create_session`/`kill_session` R→D machinery and route the reply back to the
  home daemon; transport faults map to stable `ErrorInfo` codes. Wired into the
  `WS /daemon` dispatch.

Next: daemon-side broker handle + local_api wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:21:11 +00:00
naps62-yoloandClaude Opus 4.8 eb6aeb4241 refactor(maestro): typed perimeter advertisement replaces Option pair (NAP-272)
ci / rust (push) Failing after 5m58s
`(perimeter_id, private_addr)` was carried as two `Option<String>`s with an
unwritten "both Some or both None" invariant, enforced ad-hoc at every reader
and silently violable (`with_perimeter(Some, None)`).

Collapse it to `Option<PerimeterCandidate>` (reusing the existing protocol
type, so it flows unconverted from Heartbeat → registry → AttachCandidates):

- protocol: `Heartbeat { perimeter: Option<PerimeterCandidate> }` (empty still
  serializes to the legacy `{"type":"Heartbeat"}` shape).
- registry: `DaemonEntry.perimeter`, `update_perimeter`, `perimeter_for` lose
  the defensive AND-check; `build_attach_candidates` uses it directly.
- daemon: `RegistryClient.with_perimeter(Option<PerimeterCandidate>)`; the
  both-or-neither rule now lives once, in `DaemonConfig::perimeter()`, which
  warns and drops a half-configured pair instead of advertising it.

Unblocked by NAP-270's coverage; broker T2 + perimeter-mismatch e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:32:31 +00:00
naps62-yoloandClaude Opus 4.8 939d67615a fix(maestro): bind registry JWTs to issuer/audience to stop token confusion (NAP-266)
ci / rust (push) Failing after 6m0s
Cookie and pair-attach JWTs are signed with the same shared secret. Neither
verifier bound the token's purpose, so a token minted for one could in principle
validate as the other.

- Add a canonical `REGISTRY_ISSUER` to maestro-protocol; registry re-exports it.
- Auth cookies now carry `iss = maestro-registry` + a dedicated `aud =
  maestro-cookie`; `verify_cookie` binds both, so a pair-attach JWT (daemon-id
  audience) is rejected.
- `PairAttachVerifier::verify` now binds `iss = REGISTRY_ISSUER`, rejecting any
  token not minted by the registry.

Tests: pair-verifier rejects a foreign issuer; verify_cookie rejects a
pair-attach JWT. Existing cookie + broker e2e flows unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:44:25 +00:00
naps62-yoloandClaude Opus 4.8 8b4cdfd650 fix(maestro): tolerate unknown WS frame types instead of dropping the connection (NAP-269)
ci / rust (push) Successful in 14m9s
`dispatch_text` (registry) and `handle_inbound` (daemon) treated any
frame that didn't deserialize into a known `DaemonOutbound`/`DaemonInbound`
variant as fatal: the registry returned `false` (breaking the handler
loop and deregistering the daemon, wiping its catalog), and the daemon
propagated the serde error out of the read loop. A newer peer emitting a
not-yet-merged variant would therefore get stuck in a tight
disconnect/reconnect loop.

- Add a `#[serde(other)] Unknown` catch-all to both enums so unknown
  `type` tags decode instead of erroring.
- Log-and-skip the `Unknown` variant on both sides.
- Also log-and-skip genuinely malformed frames rather than tearing down
  the connection, so one bad frame can't wipe a daemon's catalog.

Tested: unknown/malformed frames keep the connection alive (registry
handler), and unknown `type` decodes to `Unknown` (protocol round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:30:36 +00:00
naps62-yoloandClaude Opus 4.7 c4c9c8a24f feat(maestro): DaemonInbound::CatalogEvent + RegistryEvent::for_peer (NAP-246)
New control-link message wrapping a registry event for daemons that want a
local catalog mirror. `RegistryEvent::for_peer(self_id)` strips events that
concern the recipient itself — daemons mirror peer state, not self.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:18:08 +00:00
naps62-yoloandClaude Opus 4.7 45795a144a feat(maestro): add PairAttachClaims JWT shape (NAP-242)
ci / rust (push) Failing after 10m43s
iss/sub/aud/sid/exp claims for the broker-mediated attach flow. "Pair-scoped"
means the credential identifies which home daemon (sub) may attach to which
target daemon (aud) for which session (sid) — a stolen token only works
between the two daemons it was issued for.

Existing JwtClaims stays as a compat path; NAP-243 wires the new claims into
the daemon's verifier and the registry's issuer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:46:43 +00:00
naps62-yoloandClaude Opus 4.7 9a8109559b feat(maestro): extend Heartbeat with perimeter_id + private_addr (NAP-242)
Convert DaemonOutbound::Heartbeat from unit to struct variant with two
optional fields advertising the daemon's T2 reachability. Old payloads
({"type":"Heartbeat"} with no fields) still parse — both fields default
to None — and a new daemon that doesn't populate them serializes back to
the same minimal shape.

Update the daemon's heartbeat ticker and the registry's match arm to the
new shape. Population from daemon config + registry persistence lands
with the broker logic in NAP-243.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:46:05 +00:00