Compare commits
1 Commits
feat/758-r
...
docs/mos-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66afa325fe |
46
docs/PRD.md
46
docs/PRD.md
@@ -79,52 +79,6 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
|
||||||
|
|
||||||
### Problem and objective
|
|
||||||
|
|
||||||
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
|
|
||||||
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
|
|
||||||
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
|
|
||||||
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
|
|
||||||
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
|
|
||||||
gateway-backed agent catalog.
|
|
||||||
|
|
||||||
### Normative requirements
|
|
||||||
|
|
||||||
| ID | Requirement |
|
|
||||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
|
|
||||||
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
|
|
||||||
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
|
|
||||||
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
|
|
||||||
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
|
|
||||||
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
|
|
||||||
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
|
|
||||||
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
|
|
||||||
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
|
|
||||||
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
|
|
||||||
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
|
|
||||||
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
|
|
||||||
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
|
|
||||||
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
|
|
||||||
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
|
|
||||||
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
|
|
||||||
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
|
|
||||||
|
|
||||||
### M0 implementation gate
|
|
||||||
|
|
||||||
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
|
|
||||||
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
|
|
||||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
|
||||||
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tess Interaction Agent Workstream (TESS)
|
## Tess Interaction Agent Workstream (TESS)
|
||||||
|
|
||||||
### Problem and Objective
|
### Problem and Objective
|
||||||
|
|||||||
@@ -14,12 +14,11 @@
|
|||||||
|
|
||||||
## Workstream Rollup
|
## Workstream Rollup
|
||||||
|
|
||||||
| id | status | workstream | progress | tasks file | notes |
|
| id | status | workstream | progress | tasks file | notes |
|
||||||
| --- | ----------------- | ------------------------------ | ---------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
| --- | ----------------- | ---------------------- | ---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
||||||
| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready |
|
| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready |
|
||||||
| W3 | planning-complete | Native Kanban/SOT | 0 / 4 phases | [docs/native-kanban-sot/TASKS.md](./native-kanban-sot/TASKS.md) | Issue #751; canon independently approved; implementation held until canon merges |
|
| W3 | planning-complete | Native Kanban/SOT | 0 / 4 phases | [docs/native-kanban-sot/TASKS.md](./native-kanban-sot/TASKS.md) | Issue #751; canon independently approved; implementation held until canon merges |
|
||||||
| W4 | planning-complete | Fleet configuration management | 0 / 12 cards | This file (§ Fleet configuration management #758) | Issue #758; M0 docs gate defines the implementation DAG before any fleet mutation |
|
|
||||||
|
|
||||||
## Cross-Cutting Tracking
|
## Cross-Cutting Tracking
|
||||||
|
|
||||||
@@ -43,30 +42,6 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
|||||||
2. Read [docs/federation/TASKS.md](./federation/TASKS.md) for the next pending task
|
2. Read [docs/federation/TASKS.md](./federation/TASKS.md) for the next pending task
|
||||||
3. Follow per-task agent + tier guidance from the workstream manifest
|
3. Follow per-task agent + tier guidance from the workstream manifest
|
||||||
|
|
||||||
## Fleet configuration management (#758) — M0–M5 implementation DAG
|
|
||||||
|
|
||||||
> **PRD:** [Fleet declarative configuration management](./PRD.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
|
|
||||||
>
|
|
||||||
> Every row below is one independently reviewable card and **one PR**. `depends_on` is a
|
|
||||||
> hard DAG edge; no card may silently absorb another card's scope. All source cards require
|
|
||||||
> the repository quality gates, independent code and security review, terminal-green CI, and
|
|
||||||
> the applicable acceptance evidence before merge. Issue #758 remains open until M5 closes.
|
|
||||||
|
|
||||||
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
|
||||||
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| FCM-M0-001 | in-progress | Publish normative PRD requirements/acceptance criteria, this M0–M5 DAG, docs-IA checklist, and legacy example/profile disposition inventory; no implementation changes | #758 | sonnet | mosaicstack/stack | `docs/758-fleet-config-management` | — | 18K | M0 exit: approved docs; every shipped example/profile/service preset classified; docs-only PR |
|
|
||||||
| FCM-M1-001 | not-started | Implement narrow local-tmux v2 roster structural contract/compiler with YAML/JSON canonicalization and schema/parser parity tests | #758 | codex | mosaicstack/stack | `feat/758-roster-v2-compiler` | FCM-M0-001 | 30K | No lifecycle, remote, connector, secret, channel, or gateway work |
|
|
||||||
| FCM-M1-002 | not-started | Reuse existing profile/persona/provision resolver for roster semantics; add canonical class/authority validation and approved aliases | #758 | codex | mosaicstack/stack | `feat/758-shared-role-resolution` | FCM-M0-001 | 25K | Validator is certificate-only; merge-gate remains sole merge authority |
|
|
||||||
| FCM-M1-003 | not-started | Convert the M0 legacy inventory into executable example/profile/service-preset validation and explicit v1-version/retirement checks | #758 | codex | mosaicstack/stack | `test/758-example-profile-dispositions` | FCM-M1-001, FCM-M1-002 | 20K | Every shipped artifact must validate, be versioned v1, or be retired with replacement |
|
|
||||||
| FCM-M2-001 | not-started | Migrate generic launch chain to deterministic `.env.generated` plus strict data-only `.env.local`; quarantine forbidden legacy keys | #758 | codex | mosaicstack/stack | `feat/758-generated-env-boundary` | FCM-M1-001, FCM-M1-002 | 30K | No arbitrary command compatibility path; diagnostics expose key names/hashes only |
|
|
||||||
| FCM-M2-002 | not-started | Add generation-guarded local fleet agent create/get/update/delete mutations with plan/dry-run, atomic roster writes, and recovery output | #758 | codex | mosaicstack/stack | `feat/758-fleet-agent-crud` | FCM-M1-001, FCM-M2-001 | 30K | Fresh create persists stopped unless explicit persisted start |
|
|
||||||
| FCM-M3-001 | not-started | Implement local roster-owned reconcile/apply plus lifecycle/status/verify/doctor contracts and stable JSON/exit codes | #758 | codex | mosaicstack/stack | `feat/758-local-reconciler` | FCM-M2-001, FCM-M2-002 | 35K | Exact systemd/tmux ownership; remote/schema-only entries are inventory only |
|
|
||||||
| FCM-M3-002 | not-started | Add isolated systemd/tmux lifecycle, drift, socket, unmanaged-session, crash, and rollback acceptance coverage | #758 | sonnet | mosaicstack/stack | `test/758-reconciler-lifecycle-gates` | FCM-M3-001 | 25K | Proves stopped-state preservation and zero fuzzy destructive targeting |
|
|
||||||
| FCM-M4-001 | not-started | Implement field-complete v1-to-v2 inventory/preview/migrator with alias, lifecycle, env-quarantine, and remote/connector disposition evidence | #758 | codex | mosaicstack/stack | `feat/758-v1-v2-migrator` | FCM-M1-003, FCM-M3-001 | 35K | Preview first; no unreviewed lifecycle inference |
|
|
||||||
| FCM-M4-002 | not-started | Add reversible canary migration, rollback, stale-projection/orphan classification, and current-host 9-managed/3-unmanaged fixture coverage | #758 | sonnet | mosaicstack/stack | `test/758-migration-rollback-gates` | FCM-M4-001, FCM-M3-002 | 25K | Never starts a previously stopped agent or kills an unproven unmanaged session |
|
|
||||||
| FCM-M5-001 | not-started | Deliver the accepted fleet documentation IA, how-to/operations/migration references, and link/example validation | #758 | haiku | mosaicstack/stack | `docs/758-fleet-config-operator-docs` | FCM-M1-003, FCM-M2-002, FCM-M3-001, FCM-M4-001 | 24K | Must close every checklist item or record an approved deferral |
|
|
||||||
| FCM-M5-002 | not-started | Package/update asset-drift checks, rolling local canary, independent validation certificate, and release evidence | #758 | sonnet | mosaicstack/stack | `feat/758-fleet-config-release-gate` | FCM-M3-002, FCM-M4-002, FCM-M5-001 | 30K | Final #758 gate: quality, independent code/security review, validator certificate, merge-gate approval, green CI |
|
|
||||||
|
|
||||||
## Thin-core prompt diet (#528) — feat/contract-thin-core
|
## Thin-core prompt diet (#528) — feat/contract-thin-core
|
||||||
|
|
||||||
- Status: PR open, awaiting maintainer merge ratification (fleet-governing change).
|
- Status: PR open, awaiting maintainer merge ratification (fleet-governing change).
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# Fleet Configuration Management — Documentation IA Acceptance Checklist
|
|
||||||
|
|
||||||
**Issue:** #758 · **Scope:** M0 documentation gate for the local fleet declarative-configuration program.
|
|
||||||
|
|
||||||
This checklist is an acceptance contract for documentation and examples. It does not authorize
|
|
||||||
schema, runtime, systemd, role, profile, or live-fleet changes. An item is complete only when its
|
|
||||||
named artifact exists, is linked from the fleet documentation entry point, and its evidence is
|
|
||||||
recorded in the M0 task/PR.
|
|
||||||
|
|
||||||
## M0 baseline acceptance
|
|
||||||
|
|
||||||
- [ ] `docs/PRD.md` states the roster as desired-state SSOT; generated environment, systemd,
|
|
||||||
tmux, and heartbeat artifacts as non-authoritative projections; and fail-closed handling of
|
|
||||||
unsupported or quarantined legacy input.
|
|
||||||
- [ ] `docs/PRD.md` defines the required classes and authority boundary: `validator` certifies but
|
|
||||||
does not merge; `merge-gate` remains sole approve-to-land/merge authority; `team-leader`
|
|
||||||
capacity is lease-bounded; `interaction` is request/status only; instance names such as Tess
|
|
||||||
and Ultron remain configurable.
|
|
||||||
- [ ] `docs/PRD.md` defines local lifecycle semantics for `enabled`, persisted desired state, and
|
|
||||||
observed state, including stopped-state preservation through migration, apply, and reboot.
|
|
||||||
- [ ] `docs/PRD.md` defines the generated-env/local-override boundary, explicitly denies arbitrary
|
|
||||||
command overrides in M1–M5, and requires key-name/hash-only quarantine diagnostics.
|
|
||||||
- [ ] `docs/PRD.md` identifies the M1–M5 local-tmux scope and excludes remote reconciliation,
|
|
||||||
connector mutation, secret references, arbitrary commands/channels, gateway convergence, and
|
|
||||||
UI configuration storage.
|
|
||||||
- [ ] `docs/TASKS.md` contains the complete M0–M5 one-card/one-PR dependency DAG for #758 with
|
|
||||||
agent tier, branch, dependency, estimate, and evidence expectations.
|
|
||||||
- [ ] `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` classifies every current shipped
|
|
||||||
fleet example, profile, and service preset before M1 implementation starts.
|
|
||||||
|
|
||||||
## Required documentation IA for M1–M5
|
|
||||||
|
|
||||||
| Path | Minimum content | Delivery gate |
|
|
||||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
|
||||||
| `docs/fleet/README.md` | Fleet configuration entry point, desired-vs-observed decision tree, link map | M5 |
|
|
||||||
| `docs/fleet/concepts/desired-vs-observed-state.md` | SSOT/projection model, drift, generation and ownership | M5 |
|
|
||||||
| `docs/fleet/concepts/identity-class-runtime.md` | Stable name, display alias, class, runtime/provider/model separation | M5 |
|
|
||||||
| `docs/fleet/concepts/role-authority-and-leases.md` | Required roles, validator/merge-gate separation, lease limits | M5 |
|
|
||||||
| `docs/fleet/concepts/generated-env-launch-chain.md` | Generated/local files, precedence, quarantine and non-shell parsing | M5 |
|
|
||||||
| `docs/fleet/reference/roster-v2.schema.json` | Executable v2 structural contract | M1 |
|
|
||||||
| `docs/fleet/reference/roster-v2-fields.md` | Every field, default, constraint, compatibility behavior and examples | M1 |
|
|
||||||
| `docs/fleet/reference/cli.md` | `config`, `agent`, lifecycle, plan/apply, JSON and exit-code contracts | M2–M3 |
|
|
||||||
| `docs/fleet/reference/role-classes.md` | Canonical classes, aliases, authority matrix and instance-name rule | M1 |
|
|
||||||
| `docs/fleet/reference/lifecycle-transitions.md` | Create/start/stop/restart/apply/reboot/rollback transition table | M3 |
|
|
||||||
| `docs/fleet/reference/status-and-drift.md` | Desired/observed/managed state, orphans, revision mismatch, doctor output | M3 |
|
|
||||||
| `docs/fleet/how-to/create-update-delete-agent.md` | Safe CRUD, expected generation, dry-run and rollback | M2 |
|
|
||||||
| `docs/fleet/how-to/start-stop-restart.md` | Persisted versus one-shot lifecycle actions | M3 |
|
|
||||||
| `docs/fleet/how-to/configure-tess-interaction.md` | Configurable interaction instance; no hardcoded identity | M5 |
|
|
||||||
| `docs/fleet/how-to/configure-ultron-validator.md` | Configurable validator instance; no merge authority | M5 |
|
|
||||||
| `docs/fleet/how-to/customize-roles.md` | Existing baseline + `roles.local` resolution and validation | M1 |
|
|
||||||
| `docs/fleet/operations/reconcile-and-recover.md` | Plan/apply failure recovery, generation lock and canary rollout | M3 |
|
|
||||||
| `docs/fleet/operations/env-quarantine.md` | Legacy-key inventory, private quarantine and redaction behavior | M2 |
|
|
||||||
| `docs/fleet/operations/systemd-tmux-troubleshooting.md` | Socket ambiguity, ownership proof, systemd/tmux drift | M3 |
|
|
||||||
| `docs/fleet/operations/backup-restore.md` | Roster/projection backup and rollback boundaries | M4 |
|
|
||||||
| `docs/fleet/operations/upgrade-assets.md` | Source-vs-installed asset revision detection and safe refresh | M5 |
|
|
||||||
| `docs/fleet/migration/v1-to-v2.md` | Normative field map, observed-state preservation and rollback | M4 |
|
|
||||||
| `docs/fleet/migration/example-profile-disposition.md` | Final disposition of every shipped example/profile | M1–M4 |
|
|
||||||
| `docs/fleet/migration/legacy-class-aliases.md` | Alias, unresolved-class, and retirement rules | M1 |
|
|
||||||
|
|
||||||
## PRD acceptance-criteria mapping
|
|
||||||
|
|
||||||
| PRD acceptance criterion | Owning card(s) | Required evidence |
|
|
||||||
| ------------------------------------------------------------ | ---------------------------------- | --------------------------------------------------------------------------------------- |
|
|
||||||
| `AC-FCM-01` schema, semantic validation, canonical rendering | FCM-M1-001, FCM-M1-002 | YAML/JSON positive/negative and schema/parser/resolver parity tests |
|
|
||||||
| `AC-FCM-02` deterministic plan and no-mutation check | FCM-M3-001 | Stable JSON/exit-code and desired-versus-observed fixture tests |
|
|
||||||
| `AC-FCM-03` safe generation-guarded CRUD | FCM-M2-002 | Create/update/delete idempotency, expected-generation, dry-run, and recovery tests |
|
|
||||||
| `AC-FCM-04` generated/local boundary and quarantine | FCM-M2-001 | Launch-chain, shadow, injection, redaction, and forbidden-key tests |
|
|
||||||
| `AC-FCM-05` lifecycle/reconcile/socket/drift safety | FCM-M3-001, FCM-M3-002 | Isolated systemd/tmux, stopped-state, orphan, socket, and rollback evidence |
|
|
||||||
| `AC-FCM-06` v1 migration and example/profile disposition | FCM-M4-001, FCM-M4-002, FCM-M1-003 | Preview/canary/rollback fixture plus executable disposition inventory |
|
|
||||||
| `AC-FCM-07` authority and lease boundaries | FCM-M1-002 | Role/authority/lease denial tests and resolved role contracts |
|
|
||||||
| `AC-FCM-08` documentation and final release gate | FCM-M5-001, FCM-M5-002 | Checklist closure, link/example validation, reviews, certificate, and terminal-green CI |
|
|
||||||
|
|
||||||
## Cross-cutting evidence gates
|
|
||||||
|
|
||||||
- [ ] Every retained or migrated YAML/JSON example, profile, and service preset validates through the
|
|
||||||
same executable schema and shared baseline-plus-`roles.local` resolver used by the CLI.
|
|
||||||
- [ ] Every retired example/profile/service preset has a replacement link and deprecation note; no
|
|
||||||
unresolved legacy class or tool-policy alias remains silently shipped.
|
|
||||||
- [ ] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded
|
|
||||||
Tess/Ultron identity.
|
|
||||||
- [ ] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed
|
|
||||||
`mosaic agent` catalog.
|
|
||||||
- [ ] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of
|
|
||||||
legacy sensitive keys are never printed.
|
|
||||||
- [ ] M5 release review verifies links, schema/example validation, and that all checklist rows have
|
|
||||||
owner/evidence or an explicit approved deferral.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Fleet Configuration Management — Legacy Example, Profile, and Service Disposition Inventory
|
|
||||||
|
|
||||||
**Issue:** #758 · **Baseline:** `origin/main` `49e8a541` · **Status:** M0 inventory; no source
|
|
||||||
examples or profiles are changed by this document.
|
|
||||||
|
|
||||||
The v2 compiler may not silently accept an unresolved class. Before M1 exits, every shipped file
|
|
||||||
below must be either migrated and executable, retained as an explicitly versioned v1 fixture, or
|
|
||||||
retired with a replacement/deprecation note. Class resolution must use the existing
|
|
||||||
profile/persona/provision baseline-plus-`roles.local` resolver; this inventory does not create a
|
|
||||||
parallel resolver.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
| Shipped file | Current class evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| ---------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/examples/coding.yaml` | `orchestrator`, `enhancer`, `implementer`, `reviewer` | Migrate: `implementer → code`, `reviewer → review`; retain orchestration/enhancer intent | v2 fixture validates; role aliases and authority matrix tested |
|
|
||||||
| `framework/fleet/examples/general.yaml` | `orchestrator`, `enhancer`, `worker` | Migrate only after operator chooses a concrete canonical role for `worker`; no implicit conversion | Explicit replacement class, or versioned v1 fixture/retirement note |
|
|
||||||
| `framework/fleet/examples/hybrid.yaml` | `orchestrator`, `enhancer`, `implementer`, `researcher`, `reviewer` | Migrate aliases; resolve `researcher` through existing role resolver or retain/version | Shared resolver validation; no ad-hoc class scanner |
|
|
||||||
| `framework/fleet/examples/local-canary.yaml` | `orchestrator`, `implementer`, `reviewer` | Migrate aliases; preserve its local-tmux canary purpose | v2 fixture validates and preserves safe stopped/running behavior |
|
|
||||||
| `framework/fleet/examples/minimal.yaml` | `canary` | Retire or version as v1 unless an existing canonical role contract is selected deliberately | Replacement link/deprecation note or CI-valid v1 fixture |
|
|
||||||
| `framework/fleet/examples/operator-interaction.yaml` | `operator-interaction` | Migrate alias to `interaction`; preserve instance/display name as configuration, not schema identity | v2 interaction fixture validates; no Tess literal is required |
|
|
||||||
| `framework/fleet/examples/research.yaml` | `orchestrator`, `enhancer`, `researcher`, `analyst` | Resolve `researcher`/`analyst` through baseline + `roles.local`, or version/retire | Resolver evidence and explicit disposition for each unresolved class |
|
|
||||||
|
|
||||||
## Profiles
|
|
||||||
|
|
||||||
| Shipped file | Current class evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/profiles/business.yaml` | `ceo`, `coo`, `cfo`, `product-manager`, `marketing-lead`, `sales-lead`, `operations-manager`, `customer-success-manager`, `code`, `review` | Retain only if every class resolves through the existing role library/`roles.local`; otherwise version/retire rather than weakening validation | Shared resolver CI result for every class; documented role source or replacement |
|
|
||||||
| `framework/fleet/profiles/marketing.yaml` | `marketing-lead`, `content-strategist`, `copywriter`, `seo-specialist`, `social-media-manager`, `brand-strategist`, `growth-marketer`, `ux-designer` | Same resolver-or-version/retire rule | Per-class resolver CI result and replacement/deprecation record if unresolved |
|
|
||||||
| `framework/fleet/profiles/personal-assistant.yaml` | `personal-assistant`, `executive-assistant`, `scheduler`, `inbox-manager`, `researcher` | Same resolver-or-version/retire rule | Per-class resolver CI result; do not infer `interaction` equivalence |
|
|
||||||
| `framework/fleet/profiles/research.yaml` | `lead-researcher`, `researcher`, `data-analyst`, `data-scientist`, `market-analyst`, `documentation`, `review` | Same resolver-or-version/retire rule | Per-class resolver CI result and explicit compatibility posture |
|
|
||||||
| `framework/fleet/profiles/software-delivery.yaml` | `orchestrator`, `board`, `planner`, `decomposition`, `code`, `review`, `security-review`, `site-tester`, `documentation`, `merge-gate`, `rebase`, `operator`, `session-review`, `enhancer` | Retain as the governance reference; add `validator`, `team-leader`, and `interaction` only through approved role/profile work, not silent substitution | CI validates all current classes; separate fixture proves required M1 authority seats |
|
|
||||||
|
|
||||||
## Service presets
|
|
||||||
|
|
||||||
| Shipped file | Current policy evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/services/operator-interaction.yaml` | Generic policy only: `runtime: pi`, `model: openai/gpt-5.6-sol`, `reasoning: high`, `tool_policy: operator-interaction`; provisioning supplies the agent name as data | Retain as a generic service policy, not a Tess identity. Migrate `tool_policy: operator-interaction` only through the approved interaction tool-policy alias/semantic resolver; do not infer a class or machine name from this file. | Service-policy fixture validates runtime/model/reasoning and alias behavior; generic provisioning proves a configured interaction instance is supplied without a hardcoded Tess name. |
|
|
||||||
|
|
||||||
## Required disposition controls
|
|
||||||
|
|
||||||
1. **No silent aliasing:** only `implementer → code`, `reviewer → review`, and
|
|
||||||
`operator-interaction → interaction` are approved deterministic aliases in this M0 baseline.
|
|
||||||
`worker`, `analyst`, `canary`, and domain-specific classes require resolver evidence or an
|
|
||||||
explicit version/retirement decision.
|
|
||||||
2. **No identity hardcoding:** Tess and Ultron are optional instance/display names. An example/profile
|
|
||||||
may demonstrate the capability but must not make a product name a required class or machine ID.
|
|
||||||
3. **No lifecycle inference from an example:** examples describe desired configuration only; migration
|
|
||||||
of an installed v1 roster separately preserves observed stopped/running state.
|
|
||||||
4. **No secret or command migration:** examples/profiles must not introduce credential values or
|
|
||||||
`MOSAIC_AGENT_COMMAND`; those legacy keys are M2 quarantine inputs, never v2 authoring fields.
|
|
||||||
5. **Service presets are included:** service policies are inventoried alongside examples/profiles.
|
|
||||||
They may express launch/tool policy, but do not create a class, a canonical agent identity, or a
|
|
||||||
second validation path.
|
|
||||||
6. **Evidence is executable:** M1/M4 CI must enumerate these exact files, validate retained/migrated
|
|
||||||
inputs through the shared resolver, and fail if a file lacks its documented disposition.
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Fleet Roster v2 Structural Contract
|
|
||||||
|
|
||||||
**Status:** FCM-M1-001 local-tmux structural compiler contract. This document describes parsing,
|
|
||||||
strict structural validation, normalized in-memory representation, and deterministic rendering only.
|
|
||||||
It does not authorize role resolution, lifecycle reconciliation, mutation, migration, remote
|
|
||||||
placement, connector configuration, secret references, arbitrary commands, channels, gateway
|
|
||||||
mapping, or any live-fleet change.
|
|
||||||
|
|
||||||
The executable schema is [`roster-v2.schema.json`](./roster-v2.schema.json). The compiler exports
|
|
||||||
the same schema and its test parses this file and compares it structurally with the executable contract.
|
|
||||||
|
|
||||||
## Format and canonical shape
|
|
||||||
|
|
||||||
The compiler accepts YAML or JSON. It reads only snake_case source fields and renders canonical,
|
|
||||||
snake_case YAML. Rendering sorts runtime keys and agents by stable name. Agent names, class names,
|
|
||||||
and tool-policy names are structural identifiers; whether a class or policy resolves is a later
|
|
||||||
shared-resolver concern.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: 2
|
|
||||||
generation: 1
|
|
||||||
transport: tmux
|
|
||||||
tmux:
|
|
||||||
socket_name: mosaic-fleet
|
|
||||||
holder_session: _holder
|
|
||||||
defaults:
|
|
||||||
working_directory: ~/src
|
|
||||||
runtime: pi
|
|
||||||
runtimes:
|
|
||||||
pi:
|
|
||||||
reset_command: /new
|
|
||||||
agents:
|
|
||||||
- name: coder0
|
|
||||||
alias: Coder 0
|
|
||||||
class: code
|
|
||||||
runtime: pi
|
|
||||||
provider: openai
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
reasoning: high
|
|
||||||
tool_policy: code
|
|
||||||
working_directory: ~/src
|
|
||||||
persistent_persona: false
|
|
||||||
reset_between_tasks: true
|
|
||||||
lifecycle:
|
|
||||||
enabled: true
|
|
||||||
desired_state: stopped
|
|
||||||
launch:
|
|
||||||
yolo: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root fields
|
|
||||||
|
|
||||||
| Field | Required | Constraint | Meaning |
|
|
||||||
| ------------ | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `version` | yes | integer constant `2` | Identifies this contract. Version `1` is explicitly rejected by this compiler and remains on the existing v1 path until M4 migration. |
|
|
||||||
| `generation` | yes | positive safe integer | Desired-state generation. M2 uses it for mutation guards; M1 does not mutate it. |
|
|
||||||
| `transport` | yes | constant `tmux` | M1–M5 support local tmux only. |
|
|
||||||
| `tmux` | yes | strict object | Explicit local socket and holder-session configuration. |
|
|
||||||
| `defaults` | yes | strict object | Default work directory and one supported local runtime. |
|
|
||||||
| `runtimes` | yes | non-empty object | Declared local runtime reset policy map. |
|
|
||||||
| `agents` | yes | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
|
|
||||||
|
|
||||||
## Nested fields
|
|
||||||
|
|
||||||
| Path | Required | Constraint |
|
|
||||||
| ---------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
|
||||||
| `tmux.socket_name` | yes | non-empty `[A-Za-z0-9_.-]+`; an explicit named socket prevents default-versus-named socket ambiguity |
|
|
||||||
| `tmux.holder_session` | yes | non-empty `[A-Za-z0-9_.-]+` |
|
|
||||||
| `defaults.working_directory` | yes | non-empty string |
|
|
||||||
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
|
||||||
| `runtimes.<runtime>.reset_command` | yes | non-empty string; runtime key must be a supported local runtime |
|
|
||||||
| `agents[].name` | yes | unique `[A-Za-z0-9][A-Za-z0-9_.-]*` stable machine identity |
|
|
||||||
| `agents[].alias` | yes | non-empty display string |
|
|
||||||
| `agents[].class` | yes | `[a-z][a-z0-9-]*`; structural only in M1, semantic role resolution is FCM-M1-002 |
|
|
||||||
| `agents[].runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
|
||||||
| `agents[].provider`, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
|
|
||||||
| `agents[].reasoning` | yes | `low`, `medium`, or `high` |
|
|
||||||
| `agents[].tool_policy` | yes | `[a-z][a-z0-9-]*`; structural only in M1 |
|
|
||||||
| `agents[].persistent_persona`, `reset_between_tasks` | yes | booleans |
|
|
||||||
| `agents[].lifecycle.enabled` | yes | boolean; stored now, reconciled in FCM-M3-001 |
|
|
||||||
| `agents[].lifecycle.desired_state` | yes | `running` or `stopped` |
|
|
||||||
| `agents[].launch.yolo` | yes | boolean; structured data only, not an arbitrary command escape hatch |
|
|
||||||
|
|
||||||
## Fail-closed boundary
|
|
||||||
|
|
||||||
Every object is `additionalProperties: false`. The compiler rejects unknown, missing, malformed,
|
|
||||||
and wrong-type fields before producing a model. It specifically rejects remote/SSH/host/socket
|
|
||||||
per-agent fields, connector blocks, secret references, channel fields, arbitrary command fields,
|
|
||||||
and gateway fields because they are unsupported in the local-tmux M1 contract. It does not silently
|
|
||||||
ignore v1 camelCase input, version `1`, or a source that does not parse to an object.
|
|
||||||
|
|
||||||
The v2 compiler is intentionally isolated from the existing v1 loader. Existing v1 rosters and
|
|
||||||
current examples/profiles continue on their current path; FCM-M4 owns explicit inventory, preview,
|
|
||||||
migration, and rollback. FCM-M2 owns generated-file/local-override quarantine, and FCM-M3 owns
|
|
||||||
runtime lifecycle and reconciliation.
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"$id": "https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json",
|
|
||||||
"title": "Mosaic local tmux fleet roster v2",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["version", "generation", "transport", "tmux", "defaults", "runtimes", "agents"],
|
|
||||||
"properties": {
|
|
||||||
"version": {
|
|
||||||
"const": 2
|
|
||||||
},
|
|
||||||
"generation": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 9007199254740991
|
|
||||||
},
|
|
||||||
"transport": {
|
|
||||||
"const": "tmux"
|
|
||||||
},
|
|
||||||
"tmux": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["socket_name", "holder_session"],
|
|
||||||
"properties": {
|
|
||||||
"socket_name": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9_.-]+$"
|
|
||||||
},
|
|
||||||
"holder_session": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9_.-]+$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["working_directory", "runtime"],
|
|
||||||
"properties": {
|
|
||||||
"working_directory": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"type": "object",
|
|
||||||
"minProperties": 1,
|
|
||||||
"propertyNames": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
},
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["reset_command"],
|
|
||||||
"properties": {
|
|
||||||
"reset_command": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"alias",
|
|
||||||
"class",
|
|
||||||
"runtime",
|
|
||||||
"provider",
|
|
||||||
"model",
|
|
||||||
"reasoning",
|
|
||||||
"tool_policy",
|
|
||||||
"working_directory",
|
|
||||||
"persistent_persona",
|
|
||||||
"reset_between_tasks",
|
|
||||||
"lifecycle",
|
|
||||||
"launch"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*$"
|
|
||||||
},
|
|
||||||
"alias": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"class": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[a-z][a-z0-9-]*$"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"model": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"reasoning": {
|
|
||||||
"enum": ["low", "medium", "high"]
|
|
||||||
},
|
|
||||||
"tool_policy": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[a-z][a-z0-9-]*$"
|
|
||||||
},
|
|
||||||
"working_directory": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"persistent_persona": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"reset_between_tasks": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"lifecycle": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["enabled", "desired_state"],
|
|
||||||
"properties": {
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"desired_state": {
|
|
||||||
"enum": ["running", "stopped"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"launch": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["yolo"],
|
|
||||||
"properties": {
|
|
||||||
"yolo": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,19 +38,19 @@ Primary blockers:
|
|||||||
|
|
||||||
### AC disposition
|
### AC disposition
|
||||||
|
|
||||||
| AC | Result | Evidence |
|
| AC | Result | Evidence |
|
||||||
| --- | ----------------- | ------------------------------------------------------------------------------- |
|
|---|---|---|
|
||||||
| 01 | **Fail** | No `mosaic tess`; no bidirectional same-session chat/stream E2E |
|
| 01 | **Fail** | No `mosaic tess`; no bidirectional same-session chat/stream E2E |
|
||||||
| 02 | **Fail** | Generic CLI exists, but fleet/Matrix providers are unreachable in production |
|
| 02 | **Fail** | Generic CLI exists, but fleet/Matrix providers are unreachable in production |
|
||||||
| 03 | Pass | Pi profile/model/reasoning/effective-policy tests passed |
|
| 03 | Pass | Pi profile/model/reasoning/effective-policy tests passed |
|
||||||
| 04 | **Fail** | No registered fleet provider or real Mos consumer |
|
| 04 | **Fail** | No registered fleet provider or real Mos consumer |
|
||||||
| 05 | Partial | Hermes normalization/fail-closed matrix passes; live capability path is limited |
|
| 05 | Partial | Hermes normalization/fail-closed matrix passes; live capability path is limited |
|
||||||
| 06 | Partial | PGlite restart/idempotency passes; no actual harness failover |
|
| 06 | Partial | PGlite restart/idempotency passes; no actual harness failover |
|
||||||
| 07 | Partial | Focused denial/replay tests pass; full M5 abuse qualification absent |
|
| 07 | Partial | Focused denial/replay tests pass; full M5 abuse qualification absent |
|
||||||
| 08 | Partial | Mocked shared-intersection parity passes; Matrix not operationally wired |
|
| 08 | Partial | Mocked shared-intersection parity passes; Matrix not operationally wired |
|
||||||
| 09 | **Fail** | Baselines/CI green, but required E2E/security/rollback qualification absent |
|
| 09 | **Fail** | Baselines/CI green, but required E2E/security/rollback qualification absent |
|
||||||
| 10 | **Fail** | Inventory incomplete/inconsistent; rollback not exercised |
|
| 10 | **Fail** | Inventory incomplete/inconsistent; rollback not exercised |
|
||||||
| 11 | Pass/ledger stale | Documentation and sitemap exist; plugin/catalog ledger remains unresolved |
|
| 11 | Pass/ledger stale | Documentation and sitemap exist; plugin/catalog ledger remains unresolved |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -207,14 +207,14 @@ Missing pieces:
|
|||||||
|
|
||||||
# 5. Minimal follow-up issue decomposition
|
# 5. Minimal follow-up issue decomposition
|
||||||
|
|
||||||
| Order | Issue | Minimum acceptance criteria |
|
| Order | Issue | Minimum acceptance criteria |
|
||||||
| ----- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|---|---|---|
|
||||||
| 1 | **Logical identity and security fencing** | Server-derived `{tenant, logicalAgentId, connectorId, harness, leaseEpoch, scopes, expiry}`; signed/fenced execution grant; stale/forged/cross-tenant grants denied and audited; no connector credential in handoffs |
|
| 1 | **Logical identity and security fencing** | Server-derived `{tenant, logicalAgentId, connectorId, harness, leaseEpoch, scopes, expiry}`; signed/fenced execution grant; stale/forged/cross-tenant grants denied and audited; no connector credential in handoffs |
|
||||||
| 2 | **Durable connector lease** | PostgreSQL-backed exclusive lease with CAS, monotonic epoch, TTL/heartbeat, explicit takeover, and gateway rejection of stale holders; connectors for Claude Code, Pi, and Codex |
|
| 2 | **Durable connector lease** | PostgreSQL-backed exclusive lease with CAS, monotonic epoch, TTL/heartbeat, explicit takeover, and gateway rejection of stale holders; connectors for Claude Code, Pi, and Codex |
|
||||||
| 3 | **Canonical handoff/checkpoint** | Versioned, sealed schema containing canonical mission/task/git references, checkpoint digest, causal sequence, required capabilities, pending/ambiguous operation references, and source/destination acknowledgement; no raw secrets or mandatory harness transcript |
|
| 3 | **Canonical handoff/checkpoint** | Versioned, sealed schema containing canonical mission/task/git references, checkpoint digest, causal sequence, required capabilities, pending/ambiguous operation references, and source/destination acknowledgement; no raw secrets or mandatory harness transcript |
|
||||||
| 4 | **Exactly-once connector journal** | Durable operation IDs and receipts; idempotency propagated through every adapter; Matrix transaction mapping; tmux replaced or wrapped with receiver-side durable dedupe; ambiguous effects remain held for authorized reconciliation |
|
| 4 | **Exactly-once connector journal** | Durable operation IDs and receipts; idempotency propagated through every adapter; Matrix transaction mapping; tmux replaced or wrapped with receiver-side durable dedupe; ambiguous effects remain held for authorized reconciliation |
|
||||||
| 5 | **Cross-harness failover and rollback E2E** | Real Mos identity moves Claude Code → Pi → Codex and back; inject crashes before/after lease transfer, handoff persistence, send, and acknowledgement; stale connector fenced; no duplicate side effects; canonical state preserved; rollback evidence published |
|
| 5 | **Cross-harness failover and rollback E2E** | Real Mos identity moves Claude Code → Pi → Codex and back; inject crashes before/after lease transfer, handoff persistence, send, and acknowledgement; stale connector fenced; no duplicate side effects; canonical state preserved; rollback evidence published |
|
||||||
| 6 | **Generic gateway research ADR** | Evaluate LiteLLM subscription OAuth and Bifrost concepts without adding either to core; include terms/security review, credential lifecycle, tenant mapping, budgets, failover semantics, and adapter-only prototype |
|
| 6 | **Generic gateway research ADR** | Evaluate LiteLLM subscription OAuth and Bifrost concepts without adding either to core; include terms/security review, credential lifecycle, tenant mapping, budgets, failover semantics, and adapter-only prototype |
|
||||||
|
|
||||||
## Generic gateway placement
|
## Generic gateway placement
|
||||||
|
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import {
|
|
||||||
ROSTER_V2_JSON_SCHEMA,
|
|
||||||
RosterV2ValidationError,
|
|
||||||
parseRosterV2,
|
|
||||||
renderRosterV2Yaml,
|
|
||||||
} from './roster-v2.js';
|
|
||||||
|
|
||||||
const validRoster = `
|
|
||||||
version: 2
|
|
||||||
generation: 7
|
|
||||||
transport: tmux
|
|
||||||
tmux:
|
|
||||||
socket_name: mosaic-fleet
|
|
||||||
holder_session: _holder
|
|
||||||
defaults:
|
|
||||||
working_directory: ~/src
|
|
||||||
runtime: pi
|
|
||||||
runtimes:
|
|
||||||
pi:
|
|
||||||
reset_command: /new
|
|
||||||
agents:
|
|
||||||
- name: coder0
|
|
||||||
alias: Coder 0
|
|
||||||
class: code
|
|
||||||
runtime: pi
|
|
||||||
provider: openai
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
reasoning: high
|
|
||||||
tool_policy: code
|
|
||||||
working_directory: ~/src
|
|
||||||
persistent_persona: false
|
|
||||||
reset_between_tasks: true
|
|
||||||
lifecycle:
|
|
||||||
enabled: true
|
|
||||||
desired_state: stopped
|
|
||||||
launch:
|
|
||||||
yolo: true
|
|
||||||
`;
|
|
||||||
|
|
||||||
describe('roster v2 structural compiler', (): void => {
|
|
||||||
it('parses YAML into a normalized typed model and renders canonical YAML', (): void => {
|
|
||||||
const roster = parseRosterV2(validRoster, 'yaml');
|
|
||||||
|
|
||||||
expect(roster).toEqual({
|
|
||||||
version: 2,
|
|
||||||
generation: 7,
|
|
||||||
transport: 'tmux',
|
|
||||||
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
|
|
||||||
defaults: { workingDirectory: '~/src', runtime: 'pi' },
|
|
||||||
runtimes: { pi: { resetCommand: '/new' } },
|
|
||||||
agents: [
|
|
||||||
{
|
|
||||||
name: 'coder0',
|
|
||||||
alias: 'Coder 0',
|
|
||||||
className: 'code',
|
|
||||||
runtime: 'pi',
|
|
||||||
provider: 'openai',
|
|
||||||
model: 'gpt-5.6-sol',
|
|
||||||
reasoning: 'high',
|
|
||||||
toolPolicy: 'code',
|
|
||||||
workingDirectory: '~/src',
|
|
||||||
persistentPersona: false,
|
|
||||||
resetBetweenTasks: true,
|
|
||||||
lifecycle: { enabled: true, desiredState: 'stopped' },
|
|
||||||
launch: { yolo: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(renderRosterV2Yaml(roster)).toBe(validRoster.trimStart());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses JSON and produces the same normalized model', (): void => {
|
|
||||||
const yaml = parseRosterV2(validRoster, 'yaml');
|
|
||||||
const json = JSON.stringify({
|
|
||||||
version: 2,
|
|
||||||
generation: 7,
|
|
||||||
transport: 'tmux',
|
|
||||||
tmux: { socket_name: 'mosaic-fleet', holder_session: '_holder' },
|
|
||||||
defaults: { working_directory: '~/src', runtime: 'pi' },
|
|
||||||
runtimes: { pi: { reset_command: '/new' } },
|
|
||||||
agents: [
|
|
||||||
{
|
|
||||||
name: 'coder0',
|
|
||||||
alias: 'Coder 0',
|
|
||||||
class: 'code',
|
|
||||||
runtime: 'pi',
|
|
||||||
provider: 'openai',
|
|
||||||
model: 'gpt-5.6-sol',
|
|
||||||
reasoning: 'high',
|
|
||||||
tool_policy: 'code',
|
|
||||||
working_directory: '~/src',
|
|
||||||
persistent_persona: false,
|
|
||||||
reset_between_tasks: true,
|
|
||||||
lifecycle: { enabled: true, desired_state: 'stopped' },
|
|
||||||
launch: { yolo: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(parseRosterV2(json, 'json')).toEqual(yaml);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sorts runtime and agent maps in the deterministic renderer', (): void => {
|
|
||||||
const roster = parseRosterV2(
|
|
||||||
validRoster
|
|
||||||
.replace(
|
|
||||||
'runtimes:\n pi:\n reset_command: /new',
|
|
||||||
'runtimes:\n pi:\n reset_command: /new\n codex:\n reset_command: /clear',
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
'agents:\n - name: coder0',
|
|
||||||
'agents:\n - name: reviewer\n alias: Reviewer\n class: review\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: medium\n tool_policy: review\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
|
||||||
),
|
|
||||||
'yaml',
|
|
||||||
);
|
|
||||||
|
|
||||||
const rendered = renderRosterV2Yaml(roster);
|
|
||||||
expect(rendered.indexOf(' codex:')).toBeLessThan(rendered.indexOf(' pi:'));
|
|
||||||
expect(rendered.indexOf(' - name: coder0')).toBeLessThan(
|
|
||||||
rendered.indexOf(' - name: reviewer'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['v1 document', validRoster.replace('version: 2', 'version: 1'), /v1.*existing v1 path/i],
|
|
||||||
[
|
|
||||||
'unknown connector',
|
|
||||||
`${validRoster}\nconnector:\n kind: matrix\n`,
|
|
||||||
/unsupported field.*connector/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'remote host',
|
|
||||||
validRoster.replace(
|
|
||||||
'runtime: pi\n provider',
|
|
||||||
'runtime: pi\n host: remote\n provider',
|
|
||||||
),
|
|
||||||
/unsupported field.*host/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'secret reference',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n secret_ref: vault://x'),
|
|
||||||
/unsupported field.*secret_ref/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'channel override',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n channels: discord'),
|
|
||||||
/unsupported field.*channels/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'arbitrary command',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n command: whoami'),
|
|
||||||
/unsupported field.*command/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'gateway field',
|
|
||||||
`${validRoster}\ngateway:\n url: https://gateway.example\n`,
|
|
||||||
/unsupported field.*gateway/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'missing required field',
|
|
||||||
validRoster.replace(' model: gpt-5.6-sol\n', ''),
|
|
||||||
/model.*required/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid type',
|
|
||||||
validRoster.replace('generation: 7', 'generation: seven'),
|
|
||||||
/generation.*integer/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'unsafe generation',
|
|
||||||
validRoster.replace('generation: 7', 'generation: 9007199254740992'),
|
|
||||||
/generation.*integer/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'duplicate names',
|
|
||||||
validRoster.replace(
|
|
||||||
' - name: coder0',
|
|
||||||
' - name: coder0\n alias: Duplicate\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
|
||||||
),
|
|
||||||
/duplicate agent name/i,
|
|
||||||
],
|
|
||||||
['invalid name', validRoster.replace('name: coder0', 'name: ../coder0'), /invalid agent name/i],
|
|
||||||
[
|
|
||||||
'invalid transport',
|
|
||||||
validRoster.replace('transport: tmux', 'transport: matrix'),
|
|
||||||
/transport.*tmux/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid runtime',
|
|
||||||
validRoster.replace('runtime: pi', 'runtime: matrix'),
|
|
||||||
/runtime.*supported/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid reasoning',
|
|
||||||
validRoster.replace('reasoning: high', 'reasoning: extreme'),
|
|
||||||
/reasoning.*low.*medium.*high/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ambiguous socket',
|
|
||||||
validRoster.replace('socket_name: mosaic-fleet', 'socket_name: default/socket'),
|
|
||||||
/socket_name/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'agent socket override',
|
|
||||||
validRoster.replace(
|
|
||||||
'runtime: pi\n provider',
|
|
||||||
'runtime: pi\n socket: another\n provider',
|
|
||||||
),
|
|
||||||
/unsupported field.*socket/i,
|
|
||||||
],
|
|
||||||
])('rejects %s', (_name: string, source: string, expected: RegExp): void => {
|
|
||||||
expect((): void => {
|
|
||||||
parseRosterV2(source, 'yaml');
|
|
||||||
}).toThrow(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects malformed JSON as a validation error', (): void => {
|
|
||||||
expect((): void => {
|
|
||||||
parseRosterV2('{', 'json');
|
|
||||||
}).toThrow(RosterV2ValidationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('declares supported runtime map keys in the executable schema', (): void => {
|
|
||||||
expect(ROSTER_V2_JSON_SCHEMA).toMatchObject({
|
|
||||||
properties: {
|
|
||||||
runtimes: { propertyNames: { enum: ['claude', 'codex', 'opencode', 'pi'] } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the checked-in documentation schema structurally identical to the executable schema', async (): Promise<void> => {
|
|
||||||
const schemaPath = fileURLToPath(
|
|
||||||
new URL('../../../../docs/fleet/reference/roster-v2.schema.json', import.meta.url),
|
|
||||||
);
|
|
||||||
const documented = await readFile(schemaPath, 'utf8');
|
|
||||||
|
|
||||||
expect(JSON.parse(documented) as unknown).toEqual(ROSTER_V2_JSON_SCHEMA);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
import YAML from 'yaml';
|
|
||||||
|
|
||||||
export const ROSTER_V2_SUPPORTED_RUNTIMES = ['claude', 'codex', 'opencode', 'pi'] as const;
|
|
||||||
export const ROSTER_V2_REASONING_LEVELS = ['low', 'medium', 'high'] as const;
|
|
||||||
export const ROSTER_V2_DESIRED_STATES = ['running', 'stopped'] as const;
|
|
||||||
|
|
||||||
export type RosterV2RuntimeName = (typeof ROSTER_V2_SUPPORTED_RUNTIMES)[number];
|
|
||||||
export type RosterV2ReasoningLevel = (typeof ROSTER_V2_REASONING_LEVELS)[number];
|
|
||||||
export type RosterV2DesiredState = (typeof ROSTER_V2_DESIRED_STATES)[number];
|
|
||||||
export type RosterV2InputFormat = 'json' | 'yaml';
|
|
||||||
|
|
||||||
export interface FleetRosterV2Tmux {
|
|
||||||
readonly socketName: string;
|
|
||||||
readonly holderSession: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Defaults {
|
|
||||||
readonly workingDirectory: string;
|
|
||||||
readonly runtime: RosterV2RuntimeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Runtime {
|
|
||||||
readonly resetCommand: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Lifecycle {
|
|
||||||
readonly enabled: boolean;
|
|
||||||
readonly desiredState: RosterV2DesiredState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Launch {
|
|
||||||
readonly yolo: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Agent {
|
|
||||||
readonly name: string;
|
|
||||||
readonly alias: string;
|
|
||||||
readonly className: string;
|
|
||||||
readonly runtime: RosterV2RuntimeName;
|
|
||||||
readonly provider: string;
|
|
||||||
readonly model: string;
|
|
||||||
readonly reasoning: RosterV2ReasoningLevel;
|
|
||||||
readonly toolPolicy: string;
|
|
||||||
readonly workingDirectory: string;
|
|
||||||
readonly persistentPersona: boolean;
|
|
||||||
readonly resetBetweenTasks: boolean;
|
|
||||||
readonly lifecycle: FleetRosterV2Lifecycle;
|
|
||||||
readonly launch: FleetRosterV2Launch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2 {
|
|
||||||
readonly version: 2;
|
|
||||||
readonly generation: number;
|
|
||||||
readonly transport: 'tmux';
|
|
||||||
readonly tmux: FleetRosterV2Tmux;
|
|
||||||
readonly defaults: FleetRosterV2Defaults;
|
|
||||||
readonly runtimes: Readonly<Record<string, FleetRosterV2Runtime>>;
|
|
||||||
readonly agents: readonly FleetRosterV2Agent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RosterV2ValidationError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'RosterV2ValidationError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type JsonSchema = string | number | boolean | null | JsonSchema[] | { [key: string]: JsonSchema };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executable v2 structural contract. The checked-in documentation schema is
|
|
||||||
* structurally compared to this value in roster-v2.spec.ts.
|
|
||||||
*/
|
|
||||||
export const ROSTER_V2_JSON_SCHEMA: JsonSchema = {
|
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
||||||
$id: 'https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json',
|
|
||||||
title: 'Mosaic local tmux fleet roster v2',
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'],
|
|
||||||
properties: {
|
|
||||||
version: { const: 2 },
|
|
||||||
generation: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER },
|
|
||||||
transport: { const: 'tmux' },
|
|
||||||
tmux: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['socket_name', 'holder_session'],
|
|
||||||
properties: {
|
|
||||||
socket_name: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
|
|
||||||
holder_session: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['working_directory', 'runtime'],
|
|
||||||
properties: {
|
|
||||||
working_directory: { type: 'string', minLength: 1 },
|
|
||||||
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtimes: {
|
|
||||||
type: 'object',
|
|
||||||
minProperties: 1,
|
|
||||||
propertyNames: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
additionalProperties: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['reset_command'],
|
|
||||||
properties: { reset_command: { type: 'string', minLength: 1 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
type: 'array',
|
|
||||||
minItems: 1,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: [
|
|
||||||
'name',
|
|
||||||
'alias',
|
|
||||||
'class',
|
|
||||||
'runtime',
|
|
||||||
'provider',
|
|
||||||
'model',
|
|
||||||
'reasoning',
|
|
||||||
'tool_policy',
|
|
||||||
'working_directory',
|
|
||||||
'persistent_persona',
|
|
||||||
'reset_between_tasks',
|
|
||||||
'lifecycle',
|
|
||||||
'launch',
|
|
||||||
],
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]*$' },
|
|
||||||
alias: { type: 'string', minLength: 1 },
|
|
||||||
class: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
||||||
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
provider: { type: 'string', minLength: 1 },
|
|
||||||
model: { type: 'string', minLength: 1 },
|
|
||||||
reasoning: { enum: [...ROSTER_V2_REASONING_LEVELS] },
|
|
||||||
tool_policy: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
||||||
working_directory: { type: 'string', minLength: 1 },
|
|
||||||
persistent_persona: { type: 'boolean' },
|
|
||||||
reset_between_tasks: { type: 'boolean' },
|
|
||||||
lifecycle: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['enabled', 'desired_state'],
|
|
||||||
properties: {
|
|
||||||
enabled: { type: 'boolean' },
|
|
||||||
desired_state: { enum: [...ROSTER_V2_DESIRED_STATES] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
launch: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['yolo'],
|
|
||||||
properties: { yolo: { type: 'boolean' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROOT_KEYS = ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'];
|
|
||||||
const TMUX_KEYS = ['socket_name', 'holder_session'];
|
|
||||||
const DEFAULT_KEYS = ['working_directory', 'runtime'];
|
|
||||||
const RUNTIME_KEYS = ['reset_command'];
|
|
||||||
const AGENT_KEYS = [
|
|
||||||
'name',
|
|
||||||
'alias',
|
|
||||||
'class',
|
|
||||||
'runtime',
|
|
||||||
'provider',
|
|
||||||
'model',
|
|
||||||
'reasoning',
|
|
||||||
'tool_policy',
|
|
||||||
'working_directory',
|
|
||||||
'persistent_persona',
|
|
||||||
'reset_between_tasks',
|
|
||||||
'lifecycle',
|
|
||||||
'launch',
|
|
||||||
];
|
|
||||||
const LIFECYCLE_KEYS = ['enabled', 'desired_state'];
|
|
||||||
const LAUNCH_KEYS = ['yolo'];
|
|
||||||
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
||||||
const TMUX_IDENTIFIER = /^[A-Za-z0-9_.-]+$/;
|
|
||||||
const POLICY_IDENTIFIER = /^[a-z][a-z0-9-]*$/;
|
|
||||||
|
|
||||||
/** Parses YAML or JSON and compiles only the local-tmux roster v2 contract. */
|
|
||||||
export function parseRosterV2(source: string, format?: RosterV2InputFormat): FleetRosterV2 {
|
|
||||||
const parsed = parseSource(source, format);
|
|
||||||
return normalizeRosterV2(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Renders canonical snake_case YAML with sorted runtime and agent entries. */
|
|
||||||
export function renderRosterV2Yaml(roster: FleetRosterV2): string {
|
|
||||||
const normalized = normalizeRosterV2(toSourceShape(roster));
|
|
||||||
return YAML.stringify(toSourceShape(normalized));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSource(source: string, format?: RosterV2InputFormat): unknown {
|
|
||||||
const resolvedFormat = format ?? (source.trimStart().startsWith('{') ? 'json' : 'yaml');
|
|
||||||
try {
|
|
||||||
if (resolvedFormat === 'json') return JSON.parse(source) as unknown;
|
|
||||||
return YAML.parse(source) as unknown;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const detail = error instanceof Error ? error.message : String(error);
|
|
||||||
throw new RosterV2ValidationError(`Roster v2 ${resolvedFormat} parse failed: ${detail}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeRosterV2(raw: unknown): FleetRosterV2 {
|
|
||||||
const root = requiredObject(raw, 'Roster v2');
|
|
||||||
assertKnownKeys(root, 'Roster v2', ROOT_KEYS);
|
|
||||||
if (root.version === 1) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
'Roster v2 compiler rejects v1 input; use the existing v1 path until migration.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (root.version !== 2) throw new RosterV2ValidationError('Roster v2 version must be 2.');
|
|
||||||
|
|
||||||
const generation = requiredPositiveInteger(root.generation, 'Roster v2 generation');
|
|
||||||
const transport = requiredEnum(root.transport, 'Roster v2 transport', ['tmux'] as const);
|
|
||||||
const tmux = normalizeTmux(root.tmux);
|
|
||||||
const defaults = normalizeDefaults(root.defaults);
|
|
||||||
const runtimes = normalizeRuntimes(root.runtimes);
|
|
||||||
const agents = normalizeAgents(root.agents, runtimes);
|
|
||||||
|
|
||||||
if (!runtimes[defaults.runtime]) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`Roster v2 defaults runtime "${defaults.runtime}" must be declared in runtimes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { version: 2, generation, transport, tmux, defaults, runtimes, agents };
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTmux(value: unknown): FleetRosterV2Tmux {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 tmux');
|
|
||||||
assertKnownKeys(raw, 'Roster v2 tmux', TMUX_KEYS);
|
|
||||||
return {
|
|
||||||
socketName: requiredTmuxIdentifier(raw.socket_name, 'Roster v2 tmux socket_name'),
|
|
||||||
holderSession: requiredTmuxIdentifier(raw.holder_session, 'Roster v2 tmux holder_session'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDefaults(value: unknown): FleetRosterV2Defaults {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 defaults');
|
|
||||||
assertKnownKeys(raw, 'Roster v2 defaults', DEFAULT_KEYS);
|
|
||||||
return {
|
|
||||||
workingDirectory: requiredString(raw.working_directory, 'Roster v2 defaults working_directory'),
|
|
||||||
runtime: requiredRuntime(raw.runtime, 'Roster v2 defaults runtime'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRuntimes(value: unknown): Readonly<Record<string, FleetRosterV2Runtime>> {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 runtimes');
|
|
||||||
const names = Object.keys(raw);
|
|
||||||
if (names.length === 0)
|
|
||||||
throw new RosterV2ValidationError('Roster v2 runtimes must not be empty.');
|
|
||||||
|
|
||||||
const result: Record<string, FleetRosterV2Runtime> = {};
|
|
||||||
for (const name of names.sort()) {
|
|
||||||
const runtime = requiredRuntime(name, 'Roster v2 runtime name');
|
|
||||||
const config = requiredObject(raw[name], `Roster v2 runtime "${runtime}"`);
|
|
||||||
assertKnownKeys(config, `Roster v2 runtime "${runtime}"`, RUNTIME_KEYS);
|
|
||||||
result[runtime] = {
|
|
||||||
resetCommand: requiredString(
|
|
||||||
config.reset_command,
|
|
||||||
`Roster v2 runtime "${runtime}" reset_command`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAgents(
|
|
||||||
value: unknown,
|
|
||||||
runtimes: Readonly<Record<string, FleetRosterV2Runtime>>,
|
|
||||||
): readonly FleetRosterV2Agent[] {
|
|
||||||
if (!Array.isArray(value) || value.length === 0) {
|
|
||||||
throw new RosterV2ValidationError('Roster v2 agents must be a non-empty array.');
|
|
||||||
}
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const agents = value.map((candidate: unknown, index: number): FleetRosterV2Agent => {
|
|
||||||
const raw = requiredObject(candidate, `Roster v2 agents[${index}]`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agents[${index}]`, AGENT_KEYS);
|
|
||||||
const name = requiredIdentifier(raw.name, `Roster v2 agents[${index}] name`);
|
|
||||||
if (seen.has(name))
|
|
||||||
throw new RosterV2ValidationError(`Roster v2 has duplicate agent name: ${name}.`);
|
|
||||||
seen.add(name);
|
|
||||||
const runtime = requiredRuntime(raw.runtime, `Roster v2 agent "${name}" runtime`);
|
|
||||||
if (!runtimes[runtime]) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`Roster v2 agent "${name}" runtime "${runtime}" must be declared in runtimes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
alias: requiredString(raw.alias, `Roster v2 agent "${name}" alias`),
|
|
||||||
className: requiredPolicyIdentifier(raw.class, `Roster v2 agent "${name}" class`),
|
|
||||||
runtime,
|
|
||||||
provider: requiredString(raw.provider, `Roster v2 agent "${name}" provider`),
|
|
||||||
model: requiredString(raw.model, `Roster v2 agent "${name}" model`),
|
|
||||||
reasoning: requiredEnum(
|
|
||||||
raw.reasoning,
|
|
||||||
`Roster v2 agent "${name}" reasoning`,
|
|
||||||
ROSTER_V2_REASONING_LEVELS,
|
|
||||||
),
|
|
||||||
toolPolicy: requiredPolicyIdentifier(
|
|
||||||
raw.tool_policy,
|
|
||||||
`Roster v2 agent "${name}" tool_policy`,
|
|
||||||
),
|
|
||||||
workingDirectory: requiredString(
|
|
||||||
raw.working_directory,
|
|
||||||
`Roster v2 agent "${name}" working_directory`,
|
|
||||||
),
|
|
||||||
persistentPersona: requiredBoolean(
|
|
||||||
raw.persistent_persona,
|
|
||||||
`Roster v2 agent "${name}" persistent_persona`,
|
|
||||||
),
|
|
||||||
resetBetweenTasks: requiredBoolean(
|
|
||||||
raw.reset_between_tasks,
|
|
||||||
`Roster v2 agent "${name}" reset_between_tasks`,
|
|
||||||
),
|
|
||||||
lifecycle: normalizeLifecycle(raw.lifecycle, name),
|
|
||||||
launch: normalizeLaunch(raw.launch, name),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return agents.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
|
|
||||||
left.name.localeCompare(right.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLifecycle(value: unknown, agentName: string): FleetRosterV2Lifecycle {
|
|
||||||
const raw = requiredObject(value, `Roster v2 agent "${agentName}" lifecycle`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agent "${agentName}" lifecycle`, LIFECYCLE_KEYS);
|
|
||||||
return {
|
|
||||||
enabled: requiredBoolean(raw.enabled, `Roster v2 agent "${agentName}" lifecycle enabled`),
|
|
||||||
desiredState: requiredEnum(
|
|
||||||
raw.desired_state,
|
|
||||||
`Roster v2 agent "${agentName}" lifecycle desired_state`,
|
|
||||||
ROSTER_V2_DESIRED_STATES,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLaunch(value: unknown, agentName: string): FleetRosterV2Launch {
|
|
||||||
const raw = requiredObject(value, `Roster v2 agent "${agentName}" launch`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agent "${agentName}" launch`, LAUNCH_KEYS);
|
|
||||||
return { yolo: requiredBoolean(raw.yolo, `Roster v2 agent "${agentName}" launch yolo`) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredObject(value: unknown, label: string): Record<string, unknown> {
|
|
||||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
||||||
throw new RosterV2ValidationError(`${label} must be an object.`);
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertKnownKeys(
|
|
||||||
value: Record<string, unknown>,
|
|
||||||
label: string,
|
|
||||||
allowedKeys: readonly string[],
|
|
||||||
): void {
|
|
||||||
const allowed = new Set(allowedKeys);
|
|
||||||
const unknown = Object.keys(value).filter((key: string): boolean => !allowed.has(key));
|
|
||||||
if (unknown.length > 0) {
|
|
||||||
throw new RosterV2ValidationError(`${label} has unsupported field(s): ${unknown.join(', ')}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredString(value: unknown, label: string): string {
|
|
||||||
if (typeof value !== 'string' || value.trim() === '') {
|
|
||||||
throw new RosterV2ValidationError(`${label} is required and must be a non-empty string.`);
|
|
||||||
}
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!IDENTIFIER.test(result)) {
|
|
||||||
throw new RosterV2ValidationError(`Invalid agent name (${label}): ${result}.`);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredTmuxIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!TMUX_IDENTIFIER.test(result))
|
|
||||||
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredPolicyIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!POLICY_IDENTIFIER.test(result))
|
|
||||||
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredPositiveInteger(value: unknown, label: string): number {
|
|
||||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
||||||
throw new RosterV2ValidationError(`${label} must be a positive integer.`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredBoolean(value: unknown, label: string): boolean {
|
|
||||||
if (typeof value !== 'boolean') throw new RosterV2ValidationError(`${label} must be a boolean.`);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredRuntime(value: unknown, label: string): RosterV2RuntimeName {
|
|
||||||
return requiredEnum(value, label, ROSTER_V2_SUPPORTED_RUNTIMES);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredEnum<T extends string>(value: unknown, label: string, allowed: readonly T[]): T {
|
|
||||||
if (typeof value !== 'string' || !allowed.includes(value as T)) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`${label} must be one of the supported values: ${allowed.join(', ')}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSourceShape(roster: FleetRosterV2): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
version: roster.version,
|
|
||||||
generation: roster.generation,
|
|
||||||
transport: roster.transport,
|
|
||||||
tmux: { socket_name: roster.tmux.socketName, holder_session: roster.tmux.holderSession },
|
|
||||||
defaults: {
|
|
||||||
working_directory: roster.defaults.workingDirectory,
|
|
||||||
runtime: roster.defaults.runtime,
|
|
||||||
},
|
|
||||||
runtimes: Object.fromEntries(
|
|
||||||
Object.entries(roster.runtimes)
|
|
||||||
.sort(([left], [right]): number => left.localeCompare(right))
|
|
||||||
.map(([name, runtime]): [string, unknown] => [
|
|
||||||
name,
|
|
||||||
{ reset_command: runtime.resetCommand },
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
agents: [...roster.agents]
|
|
||||||
.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
|
|
||||||
left.name.localeCompare(right.name),
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(agent: FleetRosterV2Agent): Record<string, unknown> => ({
|
|
||||||
name: agent.name,
|
|
||||||
alias: agent.alias,
|
|
||||||
class: agent.className,
|
|
||||||
runtime: agent.runtime,
|
|
||||||
provider: agent.provider,
|
|
||||||
model: agent.model,
|
|
||||||
reasoning: agent.reasoning,
|
|
||||||
tool_policy: agent.toolPolicy,
|
|
||||||
working_directory: agent.workingDirectory,
|
|
||||||
persistent_persona: agent.persistentPersona,
|
|
||||||
reset_between_tasks: agent.resetBetweenTasks,
|
|
||||||
lifecycle: {
|
|
||||||
enabled: agent.lifecycle.enabled,
|
|
||||||
desired_state: agent.lifecycle.desiredState,
|
|
||||||
},
|
|
||||||
launch: { yolo: agent.launch.yolo },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user