Compare commits
4 Commits
feat/758-r
...
docs/753-k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed203b2eb | ||
|
|
3f6a3387b4 | ||
|
|
3e23158843 | ||
|
|
aca0d9022b |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
415
docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md
Normal file
415
docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# KBN-010 — Threat, Authorization, and Constraint-Impact Gate
|
||||
|
||||
- **Issue:** [#753](https://git.mosaicstack.dev/mosaicstack/stack/issues/753)
|
||||
- **Gate status:** **PASS / GO**
|
||||
- **Reviewed baseline:** `origin/main` at `49e8a54` (2026-07-14)
|
||||
- **Frozen target:** `SHARED-CONTRACT.md` v1.0.0-rc.4 and `contracts/*.v1.ts`
|
||||
- **Disposition input:** contract commit `3f6a3387b419eb99453ee10dd25ba888faaab0b5`, tree `7ebab8fa530a7180036928cea9527f808548aa14`
|
||||
- **Scope:** documentation and future-test planning only; no runtime, schema, migration, API, configuration, dependency, CI, or deployment change
|
||||
|
||||
## 1. Decision
|
||||
|
||||
KBN-010 is **PASS / GO** against frozen contract rc.4. The original rc.3 finding remains historical detection evidence:
|
||||
|
||||
- **KBN010-SI-001 — rc.3 invalid mission composite-FK candidate key.** At rc.3, `missionsV1` declared a primary key on `id` and a unique key on `(workspace_id, project_id, id)`, but not a candidate key on `(workspace_id, id)`. Both `artifacts_workspace_mission_fk` and `approval_decisions_workspace_mission_fk` referenced exactly `(missions.workspace_id, missions.id)`. PostgreSQL requires the referenced column list of a foreign key to match a non-partial unique/primary candidate key; uniqueness of `id` alone did not satisfy that two-column reference. The rc.3 DDL was therefore invalid, and KBN-010 correctly blocked it.
|
||||
|
||||
Contract rc.4 resolves SI-001 by adding the non-partial `missions_workspace_id_uidx` candidate key on `(workspace_id, id)` while retaining the global `id` primary key and the project-congruent `(workspace_id, project_id, id)` key. Both polymorphic child FKs retain their exact workspace-safe ordered columns and `ON DELETE RESTRICT`; no target, tenancy, project-congruence, exactly-one-target, N-1, rollback, no-cascade, identity, approval, or fencing authority is weakened.
|
||||
|
||||
Independent Homelab non-author schema/security review returned **APPROVE** for the exact rc.4 commit/tree/content and found no collision with #757 connector fencing. SI-001 has no unresolved contract/schema-design impact.
|
||||
|
||||
This GO completes the KBN-010 analysis/review prerequisite only. It does **not** claim that runtime schema or migration DDL exists. KBN-100 remains held and may be released only after this PR squash-merges, the merged change reaches terminal-green CI on `main`, and issue #753 closes.
|
||||
|
||||
### 1.1 Independent rc.4 evidence identity
|
||||
|
||||
- **Commit:** `3f6a3387b419eb99453ee10dd25ba888faaab0b5`
|
||||
- **Tree:** `7ebab8fa530a7180036928cea9527f808548aa14`
|
||||
- **Stable full-index SHA-256:** `6b40a76265c4f3e6d1d30a7f262a2dd16e0d51997e99c146b59f527e6524cd42`
|
||||
- **Stable patch-id:** `058cf98026fcd1043703c866aee047c8bb144740`
|
||||
- **Verdict:** Homelab independent non-author schema/security review **APPROVE**.
|
||||
- **Reviewed conclusions:** the candidate key repairs both dependent FKs; tenant safety, polymorphic exactly-one-target semantics, RESTRICT/no-cascade behavior, and N-1/rollback semantics remain valid; #757 uses separate tables/indexes/FKs/identity/fence authority and has no collision.
|
||||
|
||||
A command-rendered patch SHA may differ when Git rendering options, headers, or command form differ. That rendering digest is non-authoritative. Canonical review identity is the Git commit object plus tree and exact file content; the stable full-index digest and stable patch-id above are corroborating identities.
|
||||
|
||||
## 2. Method and trust boundaries
|
||||
|
||||
### 2.1 Inputs inspected
|
||||
|
||||
- Canonical requirements: `docs/requirements/native-kanban-sot.md`.
|
||||
- Workstream manifest and read-only task plan.
|
||||
- Frozen health, schema, Mechanical Coordinator, and recovery contracts in full.
|
||||
- Actual current-main schema, Better Auth guard/scope helpers, project/task/mission/team controllers and repositories, fleet backlog, and `TASKS.md` parser/writer.
|
||||
- Issue #753 through the Mosaic provider wrapper.
|
||||
|
||||
### 2.2 Current-main exposure that the target must replace, not inherit
|
||||
|
||||
| Current-main fact | Constraint on future implementation |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Teams are global; projects, missions, tasks, agents, and fleet backlog have no `workspace_id`. | KBN-100 must add the workspace boundary and KBN-110 must query by server-derived workspace in every repository operation. |
|
||||
| `AuthGuard` authenticates a Better Auth user, while `scopeFromUser` falls back through optional tenant/team/org claims and finally user ID. | Kanban tenancy must derive from an authenticated **active workspace membership**, not this compatibility fallback or caller data. |
|
||||
| Team list/get/member endpoints return global team data to any authenticated user. | New Kanban endpoints must use a uniform no-oracle denial and must not reuse global team lookup as authorization. |
|
||||
| Project/task repositories load and mutate by bare IDs; controller checks are separate and sometimes distinguish not-found from forbidden. | Workspace predicates and authorization must be inside the authoritative transaction/repository command path. |
|
||||
| Tasks can have nullable project/mission links, free-text assignee, JSON tags, no aggregate version, and no fence. | Expand/backfill/quarantine must precede NOT NULL/composite constraints; new commands cannot trust legacy fields. |
|
||||
| `mission_tasks.status` is a second status writer. | Pre-expand must prohibit it as a write source and later retire it only after N-1 evidence. |
|
||||
| Fleet `backlog` has global JSON dependencies and TTL claims without workspace, assignment, approval, session, or fencing. | It must be frozen and imported as non-dispatching shadow data; it cannot be adapted into the canonical lease path. |
|
||||
| `packages/coord/src/tasks-file.ts` parses and mutates `TASKS.md`. | KBN-120 must replace production use with generated, read-only projection code and prove there is no import/mutation path. |
|
||||
| No Kanban transaction-local health proof, semantic audit/event chain, change proposals, canonical outbox, approval binding, or fenced lease model exists. | These are new frozen invariants, not behaviors that may be inferred from current endpoints. |
|
||||
|
||||
## 3. Authorization matrix
|
||||
|
||||
The exact route/DTO freeze belongs to KBN-105. This matrix fixes the minimum authorization behavior that freeze and later implementation must preserve.
|
||||
|
||||
| Principal/state | Permitted authority | Required authoritative checks | Explicit denials |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| Unauthenticated caller | Public health observation only, if deployment exposes it | Health DTO validation; no proof field accepted | All canonical reads/mutations; health observation never authorizes a write |
|
||||
| Active workspace `owner`/`admin` user | Policy-allowed workspace administration and domain commands | Better Auth session; active membership; server-derived workspace; command-family role; expected version/idempotency | Foreign workspace, suspended workspace, revoked membership, caller workspace override |
|
||||
| Active workspace `member` user | Policy-allowed project/task/proposal commands | Active membership plus project/team capability and target checks in the same transaction | Admin, approval, purge, service-only Coordinator, and unrelated project commands |
|
||||
| Active workspace `auditor` user | Workspace-scoped reads and audit/evidence inspection | Active membership and read capability | Every mutation, approval, lease, token issuance, purge |
|
||||
| Active workspace `service` identity | Only explicitly issued command families | Credential maps to workspace+agent+session; agent enabled; session live; role/capability allowlist; token expiry/audience; DB recheck per command | Raw DB credentials, user/admin fallback, cross-workspace scope, command families absent from token and registry |
|
||||
| Enabled agent with live session | Agent commands matching its declared and policy-approved specialist role/capabilities | Exact workspace+agent+session binding, heartbeat/state, assignment target, lease, current decimal-string fence | Ended/offline/degraded session where policy disallows; disabled agent; another assignment/session/fence |
|
||||
| Mechanical Coordinator engine | Pure eligibility/order/expiry decisions from immutable snapshots | Complete workspace-local snapshot and policy revision | Authentication, ID loading, SQL, proof minting, scope invention, approval, certification, merge |
|
||||
| Coordinator persistence service | Service-only assignment/lease/checkpoint/recovery commands | Fresh transaction-local proof; locks; current assignment/approval/task/session/policy/fence | Public/user proof-by-value, stale approval/policy, direct completion/certification/merge |
|
||||
| Reviewer/SecReview/Certifier | Attributable evidence decisions allowed by gate policy | Active authority, author differs from reviewer, mandatory SecReview classification, immutable artifacts | Self-review; missing evidence; Certifier merge/issue-close/release |
|
||||
| Break-glass retention operator | Narrow, time-bounded purge procedure only | Separate break-glass authority, reason, scope, approvals, immutable pre-purge evidence, semantic audit, post-action reconciliation | Normal application role DELETE/UPDATE, bulk unscoped purge, unaudited hard delete |
|
||||
| Revoked/expired/disabled identity or ended session | None beyond policy-permitted public observation | Revocation/lifecycle checked from PostgreSQL on every command | Cached token/Valkey state cannot preserve authority |
|
||||
|
||||
**No-oracle rule:** authentication may return 401, but once authenticated, a foreign-workspace, nonexistent, inaccessible, or wrong-project identifier must follow the one KBN-105-frozen 404/403 policy with the same response shape and no foreign metadata, timing-derived detail, or WebSocket/MCP discrepancy.
|
||||
|
||||
## 4. Threat matrix
|
||||
|
||||
Every disposition is against the frozen target, not a claim about current-main behavior.
|
||||
|
||||
| ID | Attacker or failure | Asset | Precondition and abuse path | Frozen preventive/detective control | Required schema/API/negative-test evidence | Future owner | Residual risk | Disposition |
|
||||
| --- | --------------------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| T01 | Authenticated user supplies a foreign workspace/resource ID | Tenant confidentiality and integrity | Caller knows or guesses project/task/mission/team IDs and probes REST, MCP, WebSocket, repository, or Coordinator paths | `workspace_id` on every canonical row; composite relations; server-derived tenant; uniform no-oracle denial | Composite FK/unique DDL; every repository predicate includes workspace; N100-01/02, N110-01..05, N130-01 | KBN-100, 105, 110, 130 | Timing/volume side channels require operational review | Controlled after evidence |
|
||||
| T02 | Revoked or inactive member retains an old session | Ownership and mutation authority | Authentication remains valid after workspace membership revocation | Active membership rechecked in the authoritative transaction for owners, principals, proposers, and decision actors | Active/inactive membership fixtures; N100-03, N110-06/07; no cached membership authority | KBN-100, 110 | Better Auth session may remain valid for unrelated features | Controlled after evidence |
|
||||
| T03 | User joins/forges a team relation outside its workspace | Team-owned projects and tasks | Global-current-main team behavior or a stale membership is reused | Team is intra-workspace only; workspace/team composites; active workspace membership precedes team authorization | Cross-workspace team/member/owner insert and command denials; N100-04/05, N110-08 | KBN-100, 110 | Team-role policy mistakes remain possible | Controlled after evidence |
|
||||
| T04 | Same-workspace IDs from a different project are combined | Planning hierarchy integrity | Valid mission/milestone/parent/current-milestone UUIDs are substituted | Project-congruent composite relations and serialized hierarchy validation | Mission/milestone/parent/current milestone mismatch and parent-cycle tests; N100-06..10, N110-09 | KBN-100, 110 | Deep hierarchy checks can be expensive | Controlled after evidence |
|
||||
| T05 | Foreign or unrelated evidence/link/artifact IDs are attached | Review and audit truth | Caller has a valid same-workspace or foreign artifact UUID | Workspace-aware joins; immutable artifact digest/revision; semantic same-target validation in authoritative transaction | Mixed-workspace and same-workspace wrong-task/mission checkpoint/approval evidence tests; N100-11..14, N210-15/16 | KBN-100, 110, 210 | Same-workspace semantic validation is application-enforced | Controlled after evidence |
|
||||
| T06 | Stolen, over-scoped, or replayed service token | Coordinator and task mutation authority | Service credential is accepted as admin/user or claims are trusted without DB state | Command-family least privilege; agent/session workspace binding; no raw DB credentials; enabled/live state checked per command | Auth registry fixtures prove audience/expiry/role/capability; revoked agent and ended session denials; N105-01, N110-10..13, N210-01/02 | KBN-105, 110, 210 | Credential theft until expiry/revocation check | Controlled after evidence |
|
||||
| T07 | Caller forges public `healthy` or replays a stale health response | Sole-writer/fail-closed invariant | Public health body or caller field reaches mutation context | Public DTO is observation only; public DTOs reject proof/health fields; Gateway mints internal proof after live PG transaction probe | Contradictory union and forbidden-field tests; N105-02, N110-14..17 | KBN-105, 110, 140 | Health endpoint can still be used for reconnaissance | Controlled after evidence |
|
||||
| T08 | Internal stale, wrong-policy, or wrong-transaction proof is reused | Transaction integrity | A branded value leaks or an adapter fails to revalidate it | Non-exported brand; transaction identity, `checkedAt <= now < validUntil`, and policy revision revalidated immediately before mutation | Wrong transaction, expiry boundary, future timestamp, policy mismatch, commit-after-expiry tests; N110-18..22 | KBN-110, 140 | In-process code can bypass TypeScript; runtime checks are mandatory | Controlled after evidence |
|
||||
| T09 | DB/transport uncertainty is mislabeled as deliberate denial or conflict | Safe retry and exactly-once result | Timeout occurs before/after commit and client changes key or retries 503 | Exact 503/502/504/timeout/409 union; unknown outcome retries only with same idempotency key | Exhaustive fixture mapping and commit-before-timeout replay; N105-03, N110-23..27, N120-01/02 | KBN-105, 110, 120, 140 | External client may ignore retry rules | Controlled after evidence |
|
||||
| T10 | Assignment payload forges task version, target agent/session, role, expiry, or proposer | Work routing authority | Lease service trusts command DTO rather than persisted assignment | Persisted assignment identity; exactly-one principal/proposer; exact agent/session composite; acquire accepts IDs then reloads+locks | Cross-workspace and same-workspace target substitutions, stale task version, invalid role, expired assignment; N100-15..18, N210-03..08 | KBN-100, 200, 210 | Compromised authorized proposer can make harmful proposals | Controlled by approval/audit |
|
||||
| T11 | Approval proof is forged by value or borrowed from another assignment | Gate integrity | Caller submits `approved=true`, unrelated decision ID, stale policy, or self-approval | Relational approval bound to assignment; lock/reload; policy revision; author≠reviewer and mandatory SecReview | No proof-by-value DTO; wrong assignment/task/workspace/policy/actor/decision tests; N105-04, N210-09..14, N230-01 | KBN-105, 210, 230 | Colluding principals remain an organizational risk | Controlled after evidence |
|
||||
| T12 | Revoked policy or expired proposal/assignment is raced against lease acquisition | Routing policy | Approval and lease transactions do not lock/revalidate current rows | Lock assignment, approval, task, target session; compare current policy and expiry inside fresh-proof transaction | Concurrent revoke/expire/acquire tests with one valid terminal result; N210-17..19 | KBN-210, 230 | Clock skew if DB time is not canonical | Controlled after evidence |
|
||||
| T13 | Stale worker sends ack/heartbeat/checkpoint/review after reassignment | Canonical task and evidence state | Old process retains task/session IDs | Task-row-locked atomic monotonic bigint fence; every worker command carries exact lease/session/fence | Lower, expired, future, and other-task fences denied; old worker loses after new lease; N100-19/20, N210-20..24 | KBN-100, 210, 230 | Signed bigint exhaustion is theoretical | Controlled after evidence |
|
||||
| T14 | JavaScript precision truncates a fence | Stale-worker exclusion | bigint token is serialized as number above `2^53-1` | Drizzle bigint and decimal-string wire type only | `9007199254740993` and near-`int8` boundary round trips; numeric JSON rejected; N105-05, N210-25 | KBN-105, 210 | Nonconforming external clients | Controlled after evidence |
|
||||
| T15 | Checkpoint/evidence from another lease/task/session is submitted | Recovery and certification evidence | Same-workspace valid IDs are mixed | Exact lease composite binds workspace+task+assignment/session+fence; checkpoint composite binds lease+fence; evidence join plus semantic artifact-owner check | Same-workspace mismatched task/assignment/lease/session/checkpoint/artifact tests; N100-21..23, N210-26..31 | KBN-100, 210 | Artifact URI target may disappear outside DB | Controlled with digest/retention |
|
||||
| T16 | Outage note or pending/rejected proposal mutates/orders work | Sole SOT and gate integrity | Importer/UI treats note/proposal as task state | Proposals are inert; only explicit accept invokes normal typed command after recovery | Row/outbox/task counts unchanged for pending/rejected; no readiness/dependency/lease effect; N110-28..31 | KBN-110, 140 | Humans may act outside Mosaic operationally | Accepted as attributable residual |
|
||||
| T17 | Submission event is missing, foreign, or for another proposal | Proposal audit chain | Caller supplies an existing event UUID | Preallocated proposal ID; event-first same transaction; workspace composite FK; exact event type/aggregate/version semantic check | Missing/foreign/wrong-type/wrong-proposal event rolls back event+proposal; N100-24/25, N110-32..36 | KBN-100, 110 | Semantic checks are transaction code, not only FK | Controlled after evidence |
|
||||
| T18 | Acceptance borrows an unrelated command event | Proposal and target integrity | Same-workspace event exists for another target/command/proposal | Accept locks proposal+target, executes normal command, requires workspace/target match, causation=submission event, payload proposal ID | Foreign, wrong target/type/command/causation/payload event aborts target/event/proposal atomically; N100-26, N110-37..43 | KBN-100, 110 | Event payload schema drift | Controlled by KBN-105 fixtures |
|
||||
| T19 | Application role updates/deletes audit, approval evidence, checkpoint, or artifact | Nonrepudiation | Broad DB grants or parent cascade exists | INSERT/SELECT-only application roles; RESTRICT parent deletes; archive/cancel normal lifecycle | Role-level UPDATE/DELETE denied; parent delete RESTRICT; digest unchanged; N100-27..31 | KBN-100 | DB superuser can alter state | Break-glass/infra audit residual |
|
||||
| T20 | Break-glass purge is used as routine deletion or erases its own evidence | Retention and incident forensics | Elevated credential available | Separate audited retention procedure, bounded scope, reason, pre/post evidence, authority separation | Normal role denied; expired/missing approval denied; purge cannot delete its authorizing audit package; N115-01, N230-02/03 | KBN-115, 230 | Privileged DBA compromise | Accepted operational residual |
|
||||
| T21 | PostgreSQL unavailable or partitioned | Canonical state | Public health/Valkey remains live while transaction probe fails | Fail closed; no alternate writer/hidden queue; 503 only for proven not-applied; transport uncertainty remains unknown | Fault injection proves DB rows/outbox/files/Valkey unchanged on deliberate denial; commit-unknown replay; N110-44..48, N140-01 | KBN-110, 140, 230 | Availability loss is intentional | Accepted by Option A |
|
||||
| T22 | Valkey unavailable, duplicated, stale, or partitioned | Scheduling notifications | Queue wake is treated as truth or publication fails | Valkey derived/expendable; transactional outbox in PG; idempotent publisher; recovery from PG | Commit with Valkey down leaves pending outbox; replay publishes once logically; stale wake reloads PG; N110-49, N140-02, N230-04..06 | KBN-110, 210, 230 | Duplicate at-least-once delivery | Consumers must be idempotent |
|
||||
| T23 | Coordinator restarts between assignment, lease, checkpoint, or outbox steps | Durable orchestration truth | Process-local cache is treated as authority | PostgreSQL stores assignments, execution state, leases, fences, checkpoints, events, outbox; `recoverFromPostgres` | Restart at every transaction boundary reconstructs identical active/expired/pending sets without Valkey/files; N210-32..36, N230-07 | KBN-210, 230 | Recovery latency | Controlled after evidence |
|
||||
| T24 | Dependency cycle or concurrent reciprocal edge | Readiness and dispatch safety | Two transactions each see an acyclic graph before inserting | Unique directed edge; no self-edge; serialized recursive cycle check; readiness evaluates all blockers | Self/duplicate/cycle and concurrent A→B/B→A tests; all predecessor property test; N100-32..35, N200-01/02 | KBN-100, 200, 230 | Very large DAG performance | Bounded operational residual |
|
||||
| T25 | Parent-task cycle or project-incongruent relation | Planning hierarchy | Valid same-workspace IDs are arranged into an invalid tree | Project-congruent composites; serialized parent-cycle/orphan validation required by REQ-PLAN-001 | Self/indirect parent cycle, orphan, and cross-project mission/milestone/parent tests; N100-06..10 | KBN-100, 110 | Cycle validation is service/transaction enforced | Controlled after evidence |
|
||||
| T26 | Concurrent update, duplicate retry, or idempotency payload drift | Aggregate consistency | Two clients use same version/key with different payloads | Expected-version check; semantic event and outbox in same transaction; key returns prior immutable result only for identical command | One update wins; stale gets 409; duplicate identical returns prior; payload drift rejected; N110-50..54, N140-03 | KBN-105, 110, 140 | Long-lived clients face visible conflicts | Intentional user-visible residual |
|
||||
| T27 | State/event/outbox partial commit | Audit and notification consistency | Separate transactions or exception after state write | One PostgreSQL transaction for state+semantic event+outbox | Failure injected after each insert rolls all three back; success revisions align; N110-55..58 | KBN-110, 140 | Outbox publication remains asynchronous | Controlled after evidence |
|
||||
| T28 | Malicious/incorrect importer injects foreign workspace data or dispatchable work | Migration integrity | Source keys collide, lineage is absent, or importer has direct DB authority | Immutable source snapshots/checksums; one-way Gateway/migration-only port; workspace-safe idempotent modes; shadow records cannot dispatch | Foreign/malformed/duplicate/partial-resume/lineage checksum and no-dispatch tests; N300-01..08 | KBN-300, 330 | Source data may be semantically ambiguous | Quarantine and owner sign-off |
|
||||
| T29 | Cutover leaves legacy writer or forward/reverse sync active | Sole-writer invariant | Credentials/processes survive switch or rollback is improvised | Writer inventory, freeze, final delta, Gateway switch, credential shutdown, no dual write; rollback authority changes after first DB mutation | Process/credential inventory; concurrent-writer assertion; before/after-mutation rollback rehearsal; N320-01..06, N330-01 | KBN-320, 330, 340 | Missed external automation | Owner-gated residual |
|
||||
| T30 | Generated `TASKS.md`/`mission.json` is edited or parsed into DB | Canonical state | Current-main parser/writer remains reachable or file watcher imports changes | Generated non-authoritative header/IDs/time/revision; no production importer; regenerate/overwrite only | Static import search, tamper/regeneration, read-only permission, source-revision parity; N120-03..07, N140-04 | KBN-120, 140 | Humans may mistake snapshots for live data | Header and docs mitigate |
|
||||
| T31 | N-1 compatibility copies legacy ambiguity into canonical authority | Data integrity | Nullable/global/current-main fields are guessed during backfill | Nullable-first expand; deterministic mapping or quarantine; checksums; no new-only status before switch; legacy fields retained | Production-shape, ambiguous owner/assignee, status shadow, JSON/config/digest, rollback tests; N100-36..44 | KBN-100 | Quarantined records require human decision | Controlled by signed reconciliation |
|
||||
| T32 | Recovery posture claims durability not provided by mechanisms | Availability and audit retention | Shape-only validation or optimistic RPO is accepted | Normative validator; WAL/PITR/RPO/storage/high-assurance constraints; mechanism and restore evidence | Unknown/impossible/weakened configuration plus actual mechanism/restore tests; N115-02..08 | KBN-115 | Backup operator or storage compromise | Separate failure domain residual |
|
||||
| T33 | rc.3 frozen DDL could not create mission-scoped evidence/approval FKs | Tenant/evidence relational integrity | KBN-100 generated DDL from the rc.3 contract without an exact composite candidate key | rc.4 adds non-partial `missions_workspace_id_uidx(workspace_id,id)` before both dependent FKs while retaining global and project-congruent keys | KBN-100 must execute N100-45..50: exact-key reconciliation, candidate-before-FKs, duplicate feasibility, empty/prod/N-1/rollback, and both-child foreign-workspace negatives | KBN-100 after PR/CI/#753 release | Runtime DDL remains unimplemented and must prove the frozen order | **Resolved by rc.4 + independent APPROVE; implementation evidence remains required** |
|
||||
|
||||
## 5. Constraint-impact matrix
|
||||
|
||||
| Impact ID | Required invariant | Frozen schema impact | API/transaction impact | Required evidence | Owner | Status |
|
||||
| --------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| CI-01 | Hard workspace tenancy and no oracle | `workspace_id`, workspace-aware unique/FKs on all canonical rows | Server-derived workspace; uniform denial on all surfaces | N100-01..14; N110-01..09; N130-01 | KBN-100/105/110/130 | Resolved by frozen controls |
|
||||
| CI-02 | Active user membership | Membership row plus unique `(workspace_id,user_id)`; active state retained | Recheck active membership in same authoritative transaction | N100-03; N110-06/07 | KBN-100/110 | Resolved; not FK-only |
|
||||
| CI-03 | Service identity least privilege/revocation | Agent/session workspace, lifecycle, state, roles, capabilities | Token maps to exact agent/session; command-family allowlist; DB recheck; no admin/raw DB fallback | N105-01; N110-10..13; N210-01/02 | KBN-105/110/210 | Resolved at auth/API layer |
|
||||
| CI-04 | Project-congruent hierarchy | Composite project/mission/milestone/parent/current-milestone relations | Lock/serialized parent-cycle and orphan validation | N100-06..10 | KBN-100/110 | Resolved; cycle behavior required |
|
||||
| CI-05 | Health-proof authority | Internal branded proof has transaction/time/policy fields | Probe and revalidate on same PG transaction; no public field | N105-02/03; N110-14..27 | KBN-105/110 | Resolved by frozen controls |
|
||||
| CI-06 | Assignment/approval identity | Exactly-one principal/proposer, exact agent/session assignment, relational approval | Reload+lock all IDs; compare version/target/state/expiry/policy/decision | N100-15..18; N210-03..19 | KBN-100/210 | Resolved by frozen controls |
|
||||
| CI-07 | Monotonic bigint fencing | Durable bigint counter, exact lease/fence keys, one active lease | Atomic increment/RETURNING; decimal-string DTO; reject every stale worker command | N100-19..23; N210-20..31 | KBN-100/105/210 | Resolved by frozen controls |
|
||||
| CI-08 | Proposal event chain | Both workspace-aware event FKs; event table created first | Exact submission/acceptance semantic checks in one transaction | N100-24..26; N110-28..43 | KBN-100/110 | Resolved; semantic checks not FK-only |
|
||||
| CI-09 | Immutable audit/evidence retention | RESTRICT parents; INSERT/SELECT-only immutable tables | Archive/cancel normal flow; separately authorized purge | N100-27..31; N115-01; N230-02/03 | KBN-100/115/230 | Resolved by frozen controls |
|
||||
| CI-10 | DB/Valkey/outbox/restart semantics | PG outbox and durable orchestration rows | Fail closed; same-key uncertainty retry; Valkey reloads PG; restart from PG | N110-44..49; N140-01/02; N230-04..07 | KBN-110/210/230 | Resolved by frozen controls |
|
||||
| CI-11 | DAG/race/idempotency/version | Unique edge; self check; event idempotency; aggregate versions | Serialized recursive cycle check; payload binding; expected-version conflict | N100-32..35; N110-50..58; N200-01/02 | KBN-100/110/200 | Resolved by frozen controls |
|
||||
| CI-12 | Import/cutover trust boundary | Lineage/artifact/event fields; shadow state cannot dispatch | One-way scoped importer, freeze, no direct DB/file authority, no dual writer | N300-01..08; N320-01..06 | KBN-300/320/330 | Resolved by frozen controls |
|
||||
| CI-13 | Generated-file no-import | No canonical file schema/import contract | Projection-only package; static reachability check removes current parser from production Kanban paths | N120-03..07; N140-04 | KBN-120/140 | Resolved by frozen controls |
|
||||
| CI-14 | Mission-scoped artifact and approval FKs | rc.4 adds non-partial `missions_workspace_id_uidx(workspace_id,id)` and retains global/project-congruent keys | KBN-100 must emit the candidate before both exact RESTRICT FKs and preserve N-1/rollback order | N100-45..50: exact reconciliation, duplicate feasibility, empty/prod/N-1/rollback, and separate artifact/approval foreign-workspace negatives | KBN-100 after PR/CI/#753 release | **Resolved by rc.4 and independent APPROVE; future executable evidence required** |
|
||||
|
||||
## 6. Exact future negative-test catalog
|
||||
|
||||
These names are normative evidence identifiers for future slices. Equivalent test-file names are acceptable only if traceability retains these IDs and expected outcomes.
|
||||
|
||||
### KBN-100 — schema and migration
|
||||
|
||||
- **N100-01** reject every canonical child row whose `workspace_id` differs from its parent.
|
||||
- **N100-02** reject foreign-workspace link, artifact, proposal target, dependency, assignment, lease, checkpoint, approval, and event relationships.
|
||||
- **N100-03** reject an inactive/revoked member as accountable owner, proposer, decision actor, archive actor, or user principal in the authoritative command transaction.
|
||||
- **N100-04** reject a team/project relation crossing workspaces.
|
||||
- **N100-05** reject a team authorization path when the user lacks active membership in the team's workspace.
|
||||
- **N100-06** reject task→mission project mismatch.
|
||||
- **N100-07** reject task→milestone and project→current-milestone project mismatch.
|
||||
- **N100-08** reject task→parent project mismatch and self-parent.
|
||||
- **N100-09** reject indirect parent cycles under concurrent transactions.
|
||||
- **N100-10** reject mission→milestone project mismatch/orphan.
|
||||
- **N100-11** reject checkpoint artifact from another workspace.
|
||||
- **N100-12** reject checkpoint artifact owned by another same-workspace task/mission unless an explicitly frozen evidence rule permits it.
|
||||
- **N100-13** reject approval evidence from another workspace.
|
||||
- **N100-14** reject same-workspace approval evidence unrelated to the approval target.
|
||||
- **N100-15** reject zero/multiple assignment principals and zero/multiple proposers.
|
||||
- **N100-16** reject target session without its exact target agent.
|
||||
- **N100-17** reject assignment task/agent/session crossing workspaces.
|
||||
- **N100-18** reject non-positive task version and expired assignment acquisition.
|
||||
- **N100-19** concurrent lease insert permits one active lease and returns one winner.
|
||||
- **N100-20** successive leases return strictly increasing bigint fences.
|
||||
- **N100-21** reject checkpoint with another task, lease, or fence.
|
||||
- **N100-22** reject duplicate/non-monotonic checkpoint sequence.
|
||||
- **N100-23** reject evidence join for a mismatched checkpoint/task.
|
||||
- **N100-24** proposal insert without exact submission event fails atomically.
|
||||
- **N100-25** foreign/wrong-type/wrong-proposal submission event fails atomically.
|
||||
- **N100-26** foreign/wrong-target/unrelated acceptance event fails atomically.
|
||||
- **N100-27** application role cannot UPDATE/DELETE `task_events`.
|
||||
- **N100-28** application role cannot UPDATE/DELETE checkpoints/artifacts/evidence joins.
|
||||
- **N100-29** parent hard delete is RESTRICTed while audit/evidence children exist.
|
||||
- **N100-30** archive does not alter canonical lifecycle status.
|
||||
- **N100-31** purge without break-glass authority/evidence is denied.
|
||||
- **N100-32** reject dependency self-edge and duplicate directed pair regardless of type.
|
||||
- **N100-33** reject direct and indirect dependency cycles.
|
||||
- **N100-34** concurrent reciprocal dependency inserts cannot both commit.
|
||||
- **N100-35** readiness remains false until every blocking predecessor and completion condition passes.
|
||||
- **N100-36** empty DB migration succeeds after the contract amendment.
|
||||
- **N100-37** production-shape expand retains all legacy declarations.
|
||||
- **N100-38** crash/resume backfill is idempotent and checksum-stable.
|
||||
- **N100-39** ambiguous workspace/owner/assignee is quarantined, never guessed.
|
||||
- **N100-40** no `ready`/`in_review` status is emitted to N-1 readers before switch.
|
||||
- **N100-41** `mission_tasks.status` cannot remain a write source.
|
||||
- **N100-42** tags/assignee/date/mission JSON/config/description/agent fields reconcile without loss.
|
||||
- **N100-43** claimed fleet backlog rows are quarantined and imported rows cannot dispatch.
|
||||
- **N100-44** pre-switch rollback works while post-first-mutation rollback requires freeze/reconciliation.
|
||||
- **N100-45** reconcile both exact child FK column lists to the rc.4 `(workspace_id,id)` mission candidate while retaining the global `id` primary key and `(workspace_id,project_id,id)` key.
|
||||
- **N100-46** empty-DB migration creates `missions_workspace_id_uidx` before `artifacts_workspace_mission_fk` and `approval_decisions_workspace_mission_fk`.
|
||||
- **N100-47** production-shape preflight finds no duplicate `(workspace_id,id)` groups, preserves global `id` uniqueness, and applies the candidate before both dependent FKs.
|
||||
- **N100-48** N-1 startup/read/write remains unchanged; pre-switch rollback drops both dependents before the candidate and preserves the global/project-congruent keys.
|
||||
- **N100-49** artifact insert using a valid mission ID paired with a foreign workspace fails before commit.
|
||||
- **N100-50** approval-decision insert using a valid mission ID paired with a foreign workspace fails before commit.
|
||||
|
||||
### KBN-105/KBN-110/KBN-120/KBN-130/KBN-140 — API and P1
|
||||
|
||||
- **N105-01** every route has an explicit user/service command-family policy; user/admin tokens cannot call service-only Coordinator mutations.
|
||||
- **N105-02** public DTO validation rejects `writeProof`, internal context, body `workspaceId`, and caller-asserted health.
|
||||
- **N105-03** fixture exhaustiveness prevents 503, 502/504/timeout, and 409 cross-mapping.
|
||||
- **N105-04** approval DTO accepts an ID and decision command only, never approval proof-by-value.
|
||||
- **N105-05** all fence fields accept/emit decimal strings and reject JSON numbers.
|
||||
- **N110-01** listing with a foreign `workspaceId` or foreign filter ID follows the frozen no-oracle denial and returns no rows/counts/cursors.
|
||||
- **N110-02** get by foreign or nonexistent aggregate ID has the same frozen denial shape and no foreign metadata.
|
||||
- **N110-03** create/update/archive with a foreign owner, parent, project, mission, milestone, tag, or target ID is denied before mutation.
|
||||
- **N110-04** dependency/proposal commands with foreign target IDs are denied with unchanged state/event/outbox counts.
|
||||
- **N110-05** REST, MCP, WebSocket, and internal Coordinator paths produce equivalent no-oracle behavior for the same foreign ID.
|
||||
- **N110-06** a revoked/inactive owner is denied even with a still-valid Better Auth session.
|
||||
- **N110-07** stale membership/team cache cannot authorize a proposer, decision actor, archive actor, or principal after revocation.
|
||||
- **N110-08** a team ID from another workspace cannot authorize or own the command target.
|
||||
- **N110-09** same-workspace but wrong-project mission/milestone/parent IDs are denied inside the transaction.
|
||||
- **N110-10** an expired service token is denied before repository access.
|
||||
- **N110-11** an audience- or workspace-mismatched service token is denied without an existence oracle.
|
||||
- **N110-12** an over-scoped service token cannot call a command family absent from its role/capability allowlist.
|
||||
- **N110-13** disabled agent or ended session revokes service-token command authority immediately on PostgreSQL recheck.
|
||||
- **N110-14** contradictory public health state/boolean combinations fail validation.
|
||||
- **N110-15** Valkey-only liveness cannot mint or substitute a PostgreSQL write proof.
|
||||
- **N110-16** caller-forged public `healthy` cannot enter internal mutation context.
|
||||
- **N110-17** public REST/MCP/CLI bodies containing health/proof fields are rejected.
|
||||
- **N110-18** an expired internal proof produces no state/event/outbox write.
|
||||
- **N110-19** a future-dated or not-yet-valid proof produces no write.
|
||||
- **N110-20** a policy-revision-mismatched proof produces no write.
|
||||
- **N110-21** a proof minted on another transaction/connection produces no write.
|
||||
- **N110-22** a proof that expires before the final pre-mutation check produces no write.
|
||||
- **N110-23** deliberate read-only/write-unavailable denial maps only to authoritative 503/not-applied/non-retryable.
|
||||
- **N110-24** timeout before commit maps to transport-unknown and permits only same-key retry.
|
||||
- **N110-25** timeout after commit maps to transport-unknown and same-key retry returns the committed canonical result once.
|
||||
- **N110-26** expected-version mismatch maps only to 409/not-applied/non-retryable.
|
||||
- **N110-27** recovery replay with a changed idempotency key cannot masquerade as the original uncertain request.
|
||||
- **N110-28** pending proposal cannot alter target fields/status/rank/version.
|
||||
- **N110-29** rejected proposal cannot affect readiness, dependencies, or gates.
|
||||
- **N110-30** pending/rejected proposal cannot create an assignment or lease.
|
||||
- **N110-31** direct proposal-row state manipulation cannot bypass normal command execution.
|
||||
- **N110-32** proposal submission without a submission event rolls back fully.
|
||||
- **N110-33** foreign-workspace submission event rolls back fully.
|
||||
- **N110-34** wrong aggregate/event type submission event rolls back fully.
|
||||
- **N110-35** same-workspace event for another proposal rolls back fully.
|
||||
- **N110-36** submission event with wrong previous/new version semantics rolls back fully.
|
||||
- **N110-37** foreign-workspace acceptance event rolls back proposal, target, event, and outbox.
|
||||
- **N110-38** same-workspace event for another target aggregate rolls back acceptance.
|
||||
- **N110-39** event from an unrelated normal command rolls back acceptance.
|
||||
- **N110-40** event caused by a different submission event rolls back acceptance.
|
||||
- **N110-41** event whose payload lacks or changes `changeProposalId` rolls back acceptance.
|
||||
- **N110-42** event for another proposal with the same target/command rolls back acceptance.
|
||||
- **N110-43** missing accepted-command event after target handling rolls back the entire transaction.
|
||||
- **N110-44** read-only-degraded denial changes no DB row/outbox/file/Valkey/provider state.
|
||||
- **N110-45** write-unavailable denial changes no DB row/outbox/file/Valkey/provider state.
|
||||
- **N110-46** PostgreSQL disconnect cannot redirect a command to any fallback writer.
|
||||
- **N110-47** commit uncertainty remains `unknown` and never becomes a fabricated 503/not-applied result.
|
||||
- **N110-48** same-key replay after recovery returns one canonical result with no duplicate event/outbox row.
|
||||
- **N110-49** Valkey publication failure leaves committed PG outbox pending and replayable.
|
||||
- **N110-50** two same-version updates produce one winner and one visible 409 loser.
|
||||
- **N110-51** identical duplicate key+payload returns the prior immutable result without another event/outbox row.
|
||||
- **N110-52** same key with payload/command drift is rejected as an idempotency conflict.
|
||||
- **N110-53** the same key in another workspace cannot reveal or reuse the first workspace's result.
|
||||
- **N110-54** stale reconnect/update cannot silently overwrite a newer aggregate revision.
|
||||
- **N110-55** failure after state write but before semantic event rolls back state.
|
||||
- **N110-56** failure after semantic event but before outbox rolls back state and event.
|
||||
- **N110-57** failure after outbox insert but before commit rolls back state, event, and outbox.
|
||||
- **N110-58** success commits matching aggregate/event/outbox revisions and correlation/causation.
|
||||
- **N120-01** CLI never retries an authoritative 503 deliberate denial.
|
||||
- **N120-02** CLI retries only transport-unknown outcomes and preserves the exact idempotency key.
|
||||
- **N120-03** generated projection header contains non-authoritative warning, workspace/project IDs, generated time, and source revision.
|
||||
- **N120-04** projection revision and records match the API snapshot revision exactly.
|
||||
- **N120-05** hand-tampering is overwritten or rejected by regeneration and never mutates PostgreSQL.
|
||||
- **N120-06** static/runtime reachability finds no parser/import path from `TASKS.md`, `mission.json`, or another export.
|
||||
- **N120-07** projection writer has no domain mutation/raw SQL/Valkey authority.
|
||||
- **N130-01** UI foreign/no-access/not-found state follows the frozen no-oracle response and renders no stale foreign data.
|
||||
- **N140-01** real-Gateway DB fault journey proves fail-closed no-fallback behavior.
|
||||
- **N140-02** real-Gateway Valkey-loss journey proves pending outbox replay.
|
||||
- **N140-03** real-Gateway concurrent update/retry journey proves version and idempotency semantics.
|
||||
- **N140-04** generated-file tamper journey proves projection parity and no import.
|
||||
|
||||
### KBN-115/KBN-200/KBN-210/KBN-230 — recovery and coordination
|
||||
|
||||
- **N115-01** retention purge without current break-glass authority, reason, immutable evidence, or bounded scope is denied and audited.
|
||||
- **N115-02** recovery posture with an unknown top-level or storage field is rejected.
|
||||
- **N115-03** PITR retention without WAL archival is rejected.
|
||||
- **N115-04** WAL archival with zero PITR retention is rejected.
|
||||
- **N115-05** claimed RPO better than the configured backup/WAL mechanism is rejected.
|
||||
- **N115-06** unencrypted, optional, or same-failure-domain storage is rejected.
|
||||
- **N115-07** weakened high-assurance values are rejected.
|
||||
- **N115-08** shape-only validation cannot pass without normative mechanism and restore evidence.
|
||||
- **N200-01** cyclic/incomplete dependency snapshots never become eligible.
|
||||
- **N200-02** identical immutable snapshot+policy+time returns identical ordering and explanation with no I/O/model import.
|
||||
- **N210-01** disabled agent cannot claim, ack, heartbeat, checkpoint, or submit review.
|
||||
- **N210-02** ended/offline/mismatched session cannot claim, ack, heartbeat, checkpoint, or submit review.
|
||||
- **N210-03** foreign-workspace task is rejected after lock/reload without an oracle.
|
||||
- **N210-04** stale task version is rejected before fence increment.
|
||||
- **N210-05** assignment target agent mismatch is rejected.
|
||||
- **N210-06** target session mismatch is rejected.
|
||||
- **N210-07** expired assignment is rejected.
|
||||
- **N210-08** assignment in rejected/released/expired/superseded/leased-invalid state is rejected.
|
||||
- **N210-09** missing approval is rejected.
|
||||
- **N210-10** rejected/escalated/requested approval is rejected as approval authority.
|
||||
- **N210-11** stale policy-revision approval is rejected.
|
||||
- **N210-12** foreign-workspace approval is rejected without an oracle.
|
||||
- **N210-13** approval for another assignment is rejected.
|
||||
- **N210-14** author self-approval/review is rejected when independence is required.
|
||||
- **N210-15** foreign-workspace artifact evidence is rejected.
|
||||
- **N210-16** same-workspace artifact unrelated to the assignment/task/gate is rejected.
|
||||
- **N210-17** concurrent policy revocation versus acquire cannot produce a lease under the revoked revision.
|
||||
- **N210-18** concurrent assignment expiry versus acquire cannot produce a lease after expiry.
|
||||
- **N210-19** concurrent session end versus acquire cannot produce a lease for the ended session.
|
||||
- **N210-20** lower fencing token is rejected without writes.
|
||||
- **N210-21** token from an older lease is rejected without writes.
|
||||
- **N210-22** token paired with another task is rejected without writes.
|
||||
- **N210-23** token paired with another session is rejected without writes.
|
||||
- **N210-24** token on an expired/revoked/released lease is rejected without writes.
|
||||
- **N210-25** fences above JavaScript safe integer round-trip exactly as decimal strings.
|
||||
- **N210-26** lease task does not match assignment task and is rejected.
|
||||
- **N210-27** lease agent/session does not match assignment target and is rejected.
|
||||
- **N210-28** checkpoint task does not match lease task and is rejected.
|
||||
- **N210-29** checkpoint fence does not match exact lease fence and is rejected.
|
||||
- **N210-30** checkpoint sequence duplicate/regression is rejected.
|
||||
- **N210-31** checkpoint artifact does not match workspace/task/evidence semantics and is rejected.
|
||||
- **N210-32** restart after assignment persistence reconstructs the pending assignment.
|
||||
- **N210-33** restart after lease commit reconstructs exact active lease and fence.
|
||||
- **N210-34** restart after checkpoint commit reconstructs checkpoint/recovery state.
|
||||
- **N210-35** restart during expiry/retry/quarantine reconstructs durable disposition and eligibility.
|
||||
- **N210-36** restart with pending outbox reconstructs publication work without Valkey/files.
|
||||
- **N230-01** author=self-review and missing mandatory SecReview cannot certify or complete.
|
||||
- **N230-02** normal application role cannot execute retention purge.
|
||||
- **N230-03** break-glass purge cannot delete or alter its own authorization/evidence chain.
|
||||
- **N230-04** Valkey down leaves canonical work in PostgreSQL/outbox.
|
||||
- **N230-05** duplicate wake produces one logical effect after PostgreSQL reload/idempotency.
|
||||
- **N230-06** stale wake cannot revive an expired/revoked assignment or lease.
|
||||
- **N230-07** restart with no Valkey/files reconstructs leases/retry/quarantine/outbox exactly.
|
||||
|
||||
### KBN-300/KBN-320/KBN-330/KBN-340 — migration and cutover
|
||||
|
||||
- **N300-01** source record targeting another workspace is denied/quarantined without an oracle.
|
||||
- **N300-02** malformed source record is rejected with attributable reject evidence.
|
||||
- **N300-03** duplicate source system/key/batch replay is idempotent.
|
||||
- **N300-04** source snapshot/checksum drift aborts apply/verify.
|
||||
- **N300-05** partial import resumes from durable lineage without duplicating state/events.
|
||||
- **N300-06** imported shadow record cannot become ready, assigned, or leased automatically.
|
||||
- **N300-07** missing source key/file/checksum/batch lineage prevents apply/sign-off.
|
||||
- **N300-08** importer cannot use direct DB, generated file, Valkey, or provider issue as canonical write authority.
|
||||
- **N320-01** cutover without a verified write freeze fails safe.
|
||||
- **N320-02** active legacy writer process or credential blocks cutover.
|
||||
- **N320-03** reverse and forward synchronization cannot run concurrently.
|
||||
- **N320-04** failed final delta/reconciliation blocks client switch.
|
||||
- **N320-05** rollback before first canonical DB mutation may switch authority back only after freeze assertion.
|
||||
- **N320-06** rollback after first canonical mutation requires freeze, DB-delta export/reconciliation, and owner decision.
|
||||
- **N330-01** rehearsal cannot sign off while counts/checksums/exceptions/writer inventory differ.
|
||||
- **N340-01** cutover cannot proceed without owner authorization, terminal evidence, scoped identities, and zero active legacy writers.
|
||||
|
||||
## 7. Requirements traceability
|
||||
|
||||
| Requirement | Threats/impacts | Planned evidence |
|
||||
| ---------------- | ---------------------------- | --------------------------------------------------------------- |
|
||||
| REQ-SOT-001 | T16, T21, T22, T27, T29, T30 | N110-28..31, N110-44..49, N110-55..58, N120-03..07, N320-01..06 |
|
||||
| REQ-SOT-002 | T07, T08, T09, T21 | N105-02/03, N110-14..27, N110-44..48 |
|
||||
| REQ-SOT-003 | T30 | N120-03..07, N140-04 |
|
||||
| REQ-SOT-004 | T16..18 | N100-24..26, N110-28..43 |
|
||||
| REQ-TEN-001 | T01..05, T15, T33 | N100-01..14, N100-45..50, N110-01..09, N210-15/16 |
|
||||
| REQ-ID-001 | T02, T03, T06, T10..12 | N105-01, N110-06..13, N210-01..19 |
|
||||
| REQ-PLAN-001 | T04, T25 | N100-06..10 |
|
||||
| REQ-TASK-001 | T13, T26, T31 | N100-20, N100-37..42, N110-50..54 |
|
||||
| REQ-TASK-002 | T16, T24 | N110-28..31, N100-35, N200-01 |
|
||||
| REQ-DEP-001 | T24 | N100-32..35, N200-01 |
|
||||
| REQ-ASN-001 | T10..12 | N100-15..18, N210-03..19 |
|
||||
| REQ-AUD-001 | T17..20, T22, T27 | N100-24..31, N110-32..43, N110-49, N110-55..58 |
|
||||
| REQ-API-001 | T01, T06..18, T26 | N105-01..05 plus KBN-110 catalog |
|
||||
| REQ-UI-002/003 | T01, T15, T26 | N130-01 and real-Gateway KBN-140 journeys |
|
||||
| REQ-COORD-001 | T22..24 | N200-01/02, N210-32..36 |
|
||||
| REQ-COORD-002 | T10..12, T16 | N210-03..19, N110-28..31 |
|
||||
| REQ-COORD-003 | T13..15, T23 | N100-19..23, N210-20..36 |
|
||||
| REQ-COORD-004 | T23, T26 | N210-32..36, N230-07 |
|
||||
| REQ-GATE-001/002 | T11, T19, T20 | N210-09..14, N230-01..03 |
|
||||
| REQ-REC-001 | T20, T32 | N115-01..08 |
|
||||
| REQ-MIG-001/002 | T28, T29, T31 | N100-37..44, N300-01..08, N320-01..06, N330-01, N340-01 |
|
||||
|
||||
REQ-UI-001 and REQ-UI-004 are downstream functional/accessibility requirements rather than schema-threat controls; they remain owned by KBN-130/KBN-140. Their security-relevant tenancy, conflict, and stale-reconnect portions are covered above.
|
||||
|
||||
## 8. Issue #753 acceptance mapping
|
||||
|
||||
| Issue requirement/criterion | Evidence in this document | Result |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------- |
|
||||
| Cross-workspace owners, principals, evidence, project hierarchy | T01–T05, T15, T25; CI-01–04 | Mapped |
|
||||
| Active membership and service-token boundaries | Authorization matrix; T02, T03, T06; CI-02/03 | Mapped |
|
||||
| Stale/forged health and transaction-local proof | T07–T09, T21; CI-05 | Mapped |
|
||||
| Assignment/approval forgery and monotonic fencing | T10–T15; CI-06/07 | Mapped |
|
||||
| Change-proposal abuse and event binding | T16–T18; CI-08 | Mapped |
|
||||
| Immutable audit and break-glass | T19/T20; CI-09 | Mapped |
|
||||
| PostgreSQL/Valkey failures | T21–T23; CI-10 | Mapped |
|
||||
| Dependency/idempotency/version races | T24–T27; CI-11 | Mapped |
|
||||
| Import/cutover and generated-file boundary | T28–T31; CI-12/13 | Mapped |
|
||||
| Every schema/API/test impact explicit | Constraint matrix and negative-test catalog | Mapped |
|
||||
| No unresolved schema impact | CI-14; rc.4 resolved-impact record | **PASS — none unresolved** |
|
||||
| Independent SecReview | Homelab non-author exact commit/tree/content review | **PASS / APPROVE** |
|
||||
| PR merge, terminal-green main CI, and #753 closure | Orchestrator-owned post-worker gates | Pending; KBN-100 remains held until completion |
|
||||
|
||||
## 9. UNRESOLVED SCHEMA IMPACTS
|
||||
|
||||
none
|
||||
|
||||
### Resolved-impact record — KBN010-SI-001
|
||||
|
||||
- **Historical detection:** rc.3 lacked an exact `(workspace_id,id)` candidate key for the artifact and approval-decision mission FKs. This document's original BLOCKED verdict was correct and remains preserved in §1 and T33.
|
||||
- **Resolution:** rc.4 adds non-partial `missions_workspace_id_uidx(workspace_id,id)` before both exact dependent FKs while retaining the global primary key and project-congruent key.
|
||||
- **Reviewed object:** commit `3f6a3387b419eb99453ee10dd25ba888faaab0b5`, tree `7ebab8fa530a7180036928cea9527f808548aa14`.
|
||||
- **Corroborating identities:** full-index SHA-256 `6b40a76265c4f3e6d1d30a7f262a2dd16e0d51997e99c146b59f527e6524cd42`; stable patch-id `058cf98026fcd1043703c866aee047c8bb144740`.
|
||||
- **Independent verdict:** Homelab non-author schema/security review **APPROVE**. It confirmed PostgreSQL candidate/FK validity, unchanged tenant and polymorphic exactly-one-target safety, RESTRICT/no-cascade semantics, N-1/rollback validity, and no shared table/index/FK/identity/fence authority collision with #757.
|
||||
- **Digest interpretation:** a command-rendered patch digest varied with rendering command/options and is non-authoritative. Git commit + tree + exact file content are canonical; stable full-index SHA-256 and stable patch-id corroborate that identity.
|
||||
- **Residual implementation obligations:** KBN-100 must create the candidate before both dependent FKs; prove production-shape duplicate feasibility without weakening global uniqueness; pass empty/prod/N-1/rollback tests; reconcile both exact FK targets; and separately reject foreign-workspace mission references for artifacts and approval decisions (N100-45..50).
|
||||
- **Implementation status:** no runtime schema, migration, API, or deployment implementation is claimed by this gate disposition.
|
||||
|
||||
## 10. Residual risk and handoff
|
||||
|
||||
- Active membership, polymorphic targets, same-task evidence semantics, parent/DAG cycle checks, token scope, and no-oracle behavior depend on authoritative transaction code and must not be treated as FK-only guarantees.
|
||||
- DB superuser and break-glass compromise cannot be eliminated by application constraints; separation of duties, immutable external backup/audit evidence, drills, and monitoring remain required.
|
||||
- PostgreSQL unavailability intentionally sacrifices writes for integrity. Transport-unknown outcomes remain safe only when clients preserve the exact idempotency key.
|
||||
- Imported ambiguous records remain quarantined until owner sign-off; no automated mapping may convert ambiguity into authority.
|
||||
- SI-001 is resolved at frozen contract/design-review level only. KBN-100 still owes N100-45..50 executable migration evidence.
|
||||
|
||||
**Handoff status:** KBN-010 **PASS / GO** at rc.4. KBN-100 remains held until this PR squash-merges, terminal-green CI completes on `main`, and issue #753 closes; the orchestrator owns those remaining gates.
|
||||
@@ -1,9 +1,21 @@
|
||||
# Native Kanban/SOT — Remediated Shared Contract v1
|
||||
|
||||
**Status:** INDEPENDENT REVIEW GO; freezes as v1 when issue #751 canon merges to `main`
|
||||
**Version:** 1.0.0-rc.3
|
||||
**Status:** CONTROL-PLANE SI-001 AMENDMENT AUTHORIZED; prior KCR-001–016 independent-review GO retained; rc.4 requires independent schema/SecReview before KBN-100
|
||||
**Version:** 1.0.0-rc.4
|
||||
**Date:** 2026-07-14
|
||||
**Change authority:** Mosaic control plane/Jason only
|
||||
**SI-001 amendment authority:** `web1:mosaic-100` control-plane decision under issue #753
|
||||
|
||||
## Amendment record
|
||||
|
||||
### 1.0.0-rc.4 — KBN010-SI-001
|
||||
|
||||
- **Choice:** add the explicitly named, non-partial unique candidate key `missions_workspace_id_uidx` on `missions(workspace_id, id)` and retain `missions_workspace_project_id_uidx` on `(workspace_id, project_id, id)`.
|
||||
- **Rationale:** mission `id` remains globally unique, while the composite candidate key makes the frozen tenant-safe generic mission relations valid. `artifacts` and `approval_decisions` are polymorphic exactly-one-target records and do not consistently carry `project_id`; widening both children would unnecessarily broaden v1 and its target semantics.
|
||||
- **Exact effect:** `artifacts_workspace_mission_fk` and `approval_decisions_workspace_mission_fk` continue to reference the exact ordered columns `missions(workspace_id, id)` with RESTRICT deletion, now backed by a matching candidate key.
|
||||
- **Non-effect:** no SOT, tenancy, project-congruence, proposal-audit, approval, fencing, immutability, no-cascade, API, or wire-version invariant changes. The `SuccessEnvelopeV1.contractVersion` remains `1.0.0`.
|
||||
- **Historical evidence boundary:** `KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md` intentionally remains the immutable rc.3 blocker verdict that detected SI-001; this rc.4 record and the #753 scratchpad append are the authorized disposition. Rewriting the gate verdict is outside this amendment's exclusive scope.
|
||||
- **Gate:** this amendment resolves the DDL defect identified by KBN010-SI-001 but does not itself lift KBN-100; independent schema/SecReview remains required.
|
||||
|
||||
## 1. Authority
|
||||
|
||||
@@ -43,6 +55,7 @@ Complete declaration: `contracts/kanban-schema.v1.ts`.
|
||||
- Specialist roles everywhere: `planning | enhance | coder | review | security-review | pr-monitor | certifier`.
|
||||
- Owner uses exactly-one user/team; assignment principal exactly-one user/team/agent; users require active membership; agent/session and all evidence are workspace-bound.
|
||||
- Task→mission/milestone/parent, mission→milestone, and project→current-milestone are project-congruent composite relations.
|
||||
- Mission `id` remains globally unique. The additional non-partial `missions_workspace_id_uidx` candidate key on `(workspace_id, id)` exists only to support the frozen workspace-safe polymorphic artifact and approval-decision mission relations; the project-congruent `(workspace_id, project_id, id)` key remains authoritative wherever `project_id` is present.
|
||||
- Dependency identity is workspace+predecessor+successor independent of type.
|
||||
- Approval evidence and checkpoint evidence are workspace-scoped joins to immutable artifacts, never JSON ID arrays.
|
||||
- Proposal audit links are composite relations: `(workspace_id, submitted_audit_event_id)` and `(workspace_id, accepted_command_audit_event_id)` reference `task_events(workspace_id, id)` with RESTRICT deletion.
|
||||
@@ -76,7 +89,20 @@ Legacy columns remain declared in unified `schema.ts` for expand + full N-1/roll
|
||||
6. **Switch:** stop N-1 writers; Gateway sole command boundary; enable canonical statuses.
|
||||
7. **Contract release:** later release after rollback/N-1; remove compatibility/global uniques/legacy fields.
|
||||
|
||||
### 5.2 New audit/proposal DDL order
|
||||
### 5.2 Mission candidate-key and dependent-FK DDL order
|
||||
|
||||
KBN-100 migration DDL must execute the SI-001 portion in this order:
|
||||
|
||||
1. expand/backfill `missions.workspace_id` and `missions.project_id` while preserving the global `missions.id` primary key and the project-congruent `missions_workspace_project_id_uidx` key;
|
||||
2. prove duplicate-key feasibility on the production-shape dataset: `(workspace_id, id)` has no duplicate groups and global `id` uniqueness remains intact;
|
||||
3. create the non-partial unique index `missions_workspace_id_uidx` on exact ordered columns `(workspace_id, id)`;
|
||||
4. only after step 3, create/alter `artifacts` and add `artifacts_workspace_mission_fk` from `(workspace_id, mission_id)` to exact `missions(workspace_id, id)` with `ON DELETE RESTRICT`;
|
||||
5. only after step 3, create/alter `approval_decisions` and add `approval_decisions_workspace_mission_fk` from `(workspace_id, mission_id)` to exact `missions(workspace_id, id)` with `ON DELETE RESTRICT`;
|
||||
6. validate both constraints and prove a mission ID paired with a foreign workspace is rejected for each child.
|
||||
|
||||
The candidate key is intentionally redundant with globally unique `missions.id`, but PostgreSQL requires a matching unique candidate key for the exact two-column FK target. It is additive and N-1-safe. Pre-switch rollback drops the two dependent FKs/tables before dropping this candidate key, preserves the global primary key and project-congruent key, and follows the existing freeze/reconciliation rule after the first canonical mutation.
|
||||
|
||||
### 5.3 New audit/proposal DDL order
|
||||
|
||||
KBN-100 migration DDL must execute in this order:
|
||||
|
||||
@@ -88,36 +114,39 @@ KBN-100 migration DDL must execute in this order:
|
||||
|
||||
The submission transaction inserts the event first using a preallocated proposal UUID, then the proposal. Acceptance inserts the normal command event before updating the locked proposal. Neither FK is omitted or replaced by a bare UUID/index check.
|
||||
|
||||
### 5.3 Field map
|
||||
### 5.4 Field map
|
||||
|
||||
| Current | Expand/backfill | N-1 compatibility | Switch/contract |
|
||||
| ---------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |
|
||||
| global `teams`, `team_members` | add workspace nullable; bootstrap; validate active owners | retain global slug/FKs | workspace composites; global unique contracts later |
|
||||
| `projects.status` | add `canonical_status`; map active/paused/completed/archived | mirror representable values; no `planning` | canonical authority; legacy contracts later |
|
||||
| project `owner_id/team_id/owner_type` | add exact accountable user/team; deterministic map or quarantine | preserve old reads and compare drift | canonical exact-one; remove legacy after parity |
|
||||
| current milestone | create milestones then join table (no circular DDL) | absent to N-1 | join is authority |
|
||||
| nullable `missions.project_id` | derive workspace/project; null/orphan exception, never guess | keep nullable legacy read | canonical required; validate/set NOT NULL later |
|
||||
| mission `description` | add objective; preserve description; reviewed nonblank mapping | N-1 description | objective authority; retain until signed review |
|
||||
| `missions.status` | add canonical; planning→draft, active/paused/completed/failed same | no new-only statuses emitted | canonical authority |
|
||||
| mission `milestones` JSON | normalize with source digest; preserve malformed/original | N-1 reads JSON; no reverse sync | normalized authority; JSON removed after checksum sign-off |
|
||||
| mission config/metadata/phase/user | retain all; map known typed policy only | all remain declared | remove only by signed consumer inventory |
|
||||
| nullable `tasks.project_id` | derive explicit/mission project; orphan quarantine | retain nullable read/write during compatibility | canonical required; NOT NULL later |
|
||||
| `tasks.mission_id` | add project-congruent composite | old relation readable | composite authority |
|
||||
| `tasks.status` | canonical: not-started→backlog, in-progress→in_progress, others same | no ready/in_review emission | canonical authority |
|
||||
| `tasks.assignee` | deterministic active user/team/agent assignment; raw value preserved if ambiguous | mirror text only if unambiguous | canonical owner/assignment; remove after no-loss sign-off |
|
||||
| `tasks.tags` JSON | normalize trim/case/dedupe with original digest | transactionally mirror normalized rows | normalized authority; JSON later removed |
|
||||
| `tasks.due_date` | copy exactly to `due_at` | mirror | due_at authority; legacy later |
|
||||
| task common fields | preserve metadata byte-for-byte; add criteria/rank/retry/archive/version/fence | old reads valid | new fields canonical |
|
||||
| `mission_tasks.status` | keep; prohibit as write source; linked status ignored; unlinked becomes task or reject | read-only compatibility value | membership uses task mission; status dropped after no readers |
|
||||
| mission-task notes/PR/user | map to metadata/artifact/event/link/attribution; preserve | read-only | remove after parity |
|
||||
| `agents.status` | add workspace/lifecycle/runtime/roles; status remains presence | retain all legacy fields | lifecycle/roles authority; status may remain telemetry |
|
||||
| agent project/owner/prompt/tools/skills/config | preserve; validate tenant; derive typed capabilities without loss | N-1 reads | removal only by separate inventory |
|
||||
| fleet `backlog` | map to designated-project tasks; edges; claimed rows quarantine | freeze claims before switch; read-only compare | task/lease authority; retire after stabilization |
|
||||
| Current | Expand/backfill | N-1 compatibility | Switch/contract |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| global `teams`, `team_members` | add workspace nullable; bootstrap; validate active owners | retain global slug/FKs | workspace composites; global unique contracts later |
|
||||
| `projects.status` | add `canonical_status`; map active/paused/completed/archived | mirror representable values; no `planning` | canonical authority; legacy contracts later |
|
||||
| project `owner_id/team_id/owner_type` | add exact accountable user/team; deterministic map or quarantine | preserve old reads and compare drift | canonical exact-one; remove legacy after parity |
|
||||
| current milestone | create milestones then join table (no circular DDL) | absent to N-1 | join is authority |
|
||||
| nullable `missions.project_id` | derive workspace/project; null/orphan exception, never guess | keep nullable legacy read | canonical required; validate/set NOT NULL later |
|
||||
| mission relational candidate keys | retain global `id` PK and project-congruent key; add non-partial `(workspace_id,id)` key before artifact/approval FKs | additive key is ignored safely by N-1 readers/writers | retain both composite keys; generic mission children use exact workspace+ID target |
|
||||
| mission `description` | add objective; preserve description; reviewed nonblank mapping | N-1 description | objective authority; retain until signed review |
|
||||
| `missions.status` | add canonical; planning→draft, active/paused/completed/failed same | no new-only statuses emitted | canonical authority |
|
||||
| mission `milestones` JSON | normalize with source digest; preserve malformed/original | N-1 reads JSON; no reverse sync | normalized authority; JSON removed after checksum sign-off |
|
||||
| mission config/metadata/phase/user | retain all; map known typed policy only | all remain declared | remove only by signed consumer inventory |
|
||||
| nullable `tasks.project_id` | derive explicit/mission project; orphan quarantine | retain nullable read/write during compatibility | canonical required; NOT NULL later |
|
||||
| `tasks.mission_id` | add project-congruent composite | old relation readable | composite authority |
|
||||
| `tasks.status` | canonical: not-started→backlog, in-progress→in_progress, others same | no ready/in_review emission | canonical authority |
|
||||
| `tasks.assignee` | deterministic active user/team/agent assignment; raw value preserved if ambiguous | mirror text only if unambiguous | canonical owner/assignment; remove after no-loss sign-off |
|
||||
| `tasks.tags` JSON | normalize trim/case/dedupe with original digest | transactionally mirror normalized rows | normalized authority; JSON later removed |
|
||||
| `tasks.due_date` | copy exactly to `due_at` | mirror | due_at authority; legacy later |
|
||||
| task common fields | preserve metadata byte-for-byte; add criteria/rank/retry/archive/version/fence | old reads valid | new fields canonical |
|
||||
| `mission_tasks.status` | keep; prohibit as write source; linked status ignored; unlinked becomes task or reject | read-only compatibility value | membership uses task mission; status dropped after no readers |
|
||||
| mission-task notes/PR/user | map to metadata/artifact/event/link/attribution; preserve | read-only | remove after parity |
|
||||
| `agents.status` | add workspace/lifecycle/runtime/roles; status remains presence | retain all legacy fields | lifecycle/roles authority; status may remain telemetry |
|
||||
| agent project/owner/prompt/tools/skills/config | preserve; validate tenant; derive typed capabilities without loss | N-1 reads | removal only by separate inventory |
|
||||
| fleet `backlog` | map to designated-project tasks; edges; claimed rows quarantine | freeze claims before switch; read-only compare | task/lease authority; retire after stabilization |
|
||||
|
||||
### 5.4 Required migration tests
|
||||
### 5.5 Required migration tests
|
||||
|
||||
Empty DB; exact production-shape snapshot; crash/resume; rollback before switch; N-1 startup/read/write; workspace/member negatives; status-shadow/no premature new status; `mission_tasks.status` write prohibition; tags/assignee/date/mission JSON/config/description/agent checksum; project congruence/current-milestone order; backlog freeze/no dispatch; and proof legacy declarations persist until contract release.
|
||||
|
||||
SI-001 adds frozen future executable evidence: empty and production-shape migrations create `missions_workspace_id_uidx` before either dependent FK; duplicate-key feasibility preflight returns no `(workspace_id,id)` duplicate groups without weakening global `id` uniqueness; N-1 startup/read/write behavior is unchanged; pre-switch rollback removes dependents before the candidate key; both exact FK column lists reconcile to the candidate key; and foreign-workspace mission references fail for both artifacts and approval decisions. TDD is not applicable to this design-only amendment; KBN-100 must implement these negative migration tests before runtime schema release.
|
||||
|
||||
Proposal-specific negatives must attempt: missing submission event, foreign-workspace submission event, foreign-workspace acceptance event, same-workspace event for another proposal, event for another target aggregate, and unrelated normal-command event. Every attempt must fail atomically with no accepted proposal and no target mutation.
|
||||
|
||||
## 6. Ownership and Coordinator split
|
||||
@@ -216,4 +245,10 @@ KBN-115/coder2 owns `packages/config/src/recovery-posture.ts`, tests, and recove
|
||||
|
||||
Required release evidence includes empty/prod/partial/rollback/N-1 migration tests; cross-workspace and same-workspace wrong-project negatives; active-membership owners/principals; proposal inertness/normal acceptance; exact failure mapping; concurrent monotonic bigint fences; relational lease/checkpoint/evidence mismatch; immutability privileges/RESTRICT; recovery validation/mechanism evidence; endpoint registry alignment; accessible web journeys; author≠reviewer; mandatory SecReview; final Certifier pass/no merge authority.
|
||||
|
||||
The build hold remains active until independent re-review reports GO for KCR-001–016. Mos alone releases waves and serializes integration roots.
|
||||
### 9.1 SI-001 amendment gate and #757 boundary
|
||||
|
||||
- KBN-100 must provide the §5.2 candidate-key ordering, duplicate-feasibility, exact-FK reconciliation, empty/prod/N-1/rollback, and two-child foreign-workspace evidence before SI-001 can be certified closed.
|
||||
- All prior KCR-001–016 decisions and fixed SOT/tenant/authority, proposal-audit, approval, task-fencing, immutability, and no-cascade invariants remain unchanged.
|
||||
- Read-only PR #757 cross-check: its logical-agent connector lease/CAS fencing uses separate runtime tables/contracts and `lease_epoch`; rc.4 changes only the frozen `missions` candidate key. There is no shared table, index, FK, identity, fence, or authority semantic to consume or reconcile, and #757 remains owned by its existing lane.
|
||||
|
||||
The build hold remains active until independent re-review reports GO for KCR-001–016 and the rc.4 SI-001 amendment. Mos alone releases waves and serializes integration roots.
|
||||
|
||||
@@ -70,19 +70,21 @@ No consumer implementation begins before KBN-105. No schema work begins before K
|
||||
|
||||
### KBN-000 — Remediate and publish canon
|
||||
|
||||
- **Owner:** Mos / publication control plane.
|
||||
- **Mode:** SERIAL; publication gate in progress.
|
||||
- **Status:** COMPLETE — PR #752 squash-merged as `49e8a54`; issue #751 closed; post-merge pipeline #1798 terminal success.
|
||||
- **Owner:** Mosaic publication control plane.
|
||||
- **Mode:** SERIAL; completed.
|
||||
- **IN:** Resolve KCR-001–016 in requirements, schema, health, Coordinator, recovery, migration map, and slices; independent re-review.
|
||||
- **OUT:** Feature implementation.
|
||||
- **Depends on:** none.
|
||||
- **Contract surfaces:** all canon.
|
||||
- **Evidence:** strict TS; Prettier; per-finding traceability; independent author≠reviewer GO.
|
||||
- **Evidence:** strict TS; Prettier; per-finding traceability; independent author≠reviewer GO; Ultron GO; terminal-green CI.
|
||||
|
||||
### KBN-010 — Threat, authorization, and constraint-impact gate
|
||||
|
||||
- **Owner:** coder3; independent `secrev`.
|
||||
- **Status:** IN PROGRESS — issue [#753](https://git.mosaicstack.dev/mosaicstack/stack/issues/753).
|
||||
- **Owner:** `kbn-coder3`; independent `secrev`.
|
||||
- **Mode:** SERIAL prerequisite of KBN-100.
|
||||
- **Exclusive files:** Mos-selected threat/auth docs only.
|
||||
- **Exclusive files:** `docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md` and task scratchpad only.
|
||||
- **IN:** Cross-workspace owners/principals/evidence; active membership; stale/forged health; approval forgery; fence monotonicity; audit retention; proposal target/audit-event forgery; service tokens; DB/Valkey outage.
|
||||
- **OUT:** Runtime/schema edits.
|
||||
- **Depends on:** KBN-000 independent re-review GO.
|
||||
|
||||
@@ -485,6 +485,7 @@ export const missionsV1 = pgTable(
|
||||
foreignColumns: [projectsV1.workspaceId, projectsV1.id],
|
||||
}).onDelete('restrict'),
|
||||
uniqueIndex('missions_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id),
|
||||
uniqueIndex('missions_workspace_id_uidx').on(t.workspaceId, t.id),
|
||||
index('missions_workspace_project_status_idx').on(
|
||||
t.workspaceId,
|
||||
t.projectId,
|
||||
|
||||
101
docs/scratchpads/753-kbn010-threat-gate.md
Normal file
101
docs/scratchpads/753-kbn010-threat-gate.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Issue #753 — KBN-010 threat, authorization, and constraint-impact gate
|
||||
|
||||
## Objective
|
||||
|
||||
Complete the mandatory threat/auth/constraint-impact analysis that gates KBN-100 schema implementation.
|
||||
|
||||
## Scope
|
||||
|
||||
- In: threat matrix, frozen-control mapping, schema/API/test impact inventory, security evidence plan.
|
||||
- Out: runtime, schema, migration, API, dependency, CI, deployment, and secret changes.
|
||||
- Canonical requirements: `docs/requirements/native-kanban-sot.md`.
|
||||
- Frozen contract: `docs/native-kanban-sot/SHARED-CONTRACT.md` and `contracts/*.v1.ts`.
|
||||
- Tracking: Mosaic Stack issue #753.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Independently inspect current-main implementation and frozen canon.
|
||||
2. Enumerate tenant, identity, health-proof, approval, fencing, proposal, audit, token, and outage threats.
|
||||
3. Map every threat to required constraints, command behavior, negative tests, and owning future slice.
|
||||
4. Surface any unresolved schema impact as a blocker; do not silently amend the frozen contract.
|
||||
5. Run documentation/static validation and submit for independent SecReview.
|
||||
|
||||
## Execution log
|
||||
|
||||
- 2026-07-14: KBN-000 completed through PR #752 and post-merge pipeline #1798.
|
||||
- 2026-07-14: KBN-010 issue #753 created; task marked in progress; fresh GPT worker pending dispatch.
|
||||
|
||||
## Verification evidence
|
||||
|
||||
Pending worker validation, independent SecReview, Ultron gate, PR merge, post-merge CI, and issue closure.
|
||||
|
||||
## 2026-07-14 worker analysis checkpoint
|
||||
|
||||
- Inspected issue #753, canonical requirements, all frozen v1 contracts, and actual `origin/main` at `49e8a54` across DB schema, Better Auth scope, project/task/mission/team repositories/controllers, fleet backlog, and `TASKS.md` parsing/writing.
|
||||
- Confirmed the worker branch has no source/runtime/schema delta from `origin/main`; orchestrator-owned `.mosaic` state remains dirty and untouched.
|
||||
- Authored the threat, authorization, constraint-impact, negative-test, and requirements-traceability analysis in `docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md`.
|
||||
- Gate decision: **BLOCKED** by `KBN010-SI-001`. `missionsV1` lacks a unique candidate key on `(workspace_id, id)`, while `artifacts_workspace_mission_fk` and `approval_decisions_workspace_mission_fk` both reference that exact pair. PostgreSQL cannot create the frozen composite foreign keys as declared.
|
||||
- Decision: do not select or apply a schema fix. Contract authority must version either a `(workspace_id, id)` mission candidate key or project-congruent child keys, then obtain independent re-review before KBN-100.
|
||||
- Additional risks are controlled by frozen transaction/API behavior but require the exact future negative tests cataloged in the deliverable, especially active membership, service-token revocation, same-workspace semantic evidence checks, no-oracle behavior, and serialized parent/DAG cycle checks.
|
||||
- OpenBrain startup recall was attempted but unavailable because `/home/hermes/.config/mosaic/credentials.json` is absent; no project state was written to an alternate memory silo.
|
||||
- TDD: not applicable; this slice changes documentation/test-plan analysis only and implements no runtime behavior.
|
||||
|
||||
## Validation log
|
||||
|
||||
- `pnpm exec prettier --check docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md docs/scratchpads/753-kbn010-threat-gate.md` — PASS.
|
||||
- Changed-doc link validator — PASS (`relative_links=0`, one external issue link); `curl -fsSIL https://git.mosaicstack.dev/mosaicstack/stack/issues/753` — PASS.
|
||||
- The first inline link-validator invocation had a Python f-string syntax error; corrected once and rerun successfully without changing the deliverable.
|
||||
- `pnpm format:check` — PASS.
|
||||
- `pnpm lint` — PASS (23 tasks successful).
|
||||
- `pnpm typecheck` — PASS (42 tasks successful).
|
||||
- `pnpm exec tsc -p docs/native-kanban-sot/tsconfig.json --noEmit` — PASS.
|
||||
- Scoped diff review — PASS: authored delta is limited to the exclusive deliverable and this scratchpad; no runtime/schema/config/dependency/CI/deployment file is changed, and `docs/native-kanban-sot/TASKS.md` has no worker worktree delta from tracking commit `9b55de0`.
|
||||
- Independent SecReview remains pending and cannot return PASS until contract authority resolves `KBN010-SI-001`.
|
||||
- Final formatting/diff/test-ID completeness review — PASS (all catalog prefixes contiguous with no duplicate IDs).
|
||||
- Remaining: scoped commit, queue guard, and push.
|
||||
|
||||
## 2026-07-14 KBN010-SI-001 contract-authority amendment
|
||||
|
||||
- Authority: `web1:mosaic-100` directed the minimal rc.4 amendment under issue #753: add a non-partial unique candidate key on `missions(workspace_id, id)` while retaining global `missions.id` uniqueness and the project-congruent `(workspace_id, project_id, id)` key.
|
||||
- Rationale: artifacts and approval decisions are polymorphic exactly-one-target records and do not consistently carry `project_id`; widening both children would broaden frozen v1 semantics without improving tenant safety.
|
||||
- Plan: amend only the frozen schema contract and shared contract, freeze exact DDL ordering and future migration negatives, run scoped/full validation and independent review, then commit and push without opening/merging a PR or closing #753.
|
||||
- TDD: not applicable because this is a design-contract amendment with no runtime schema or migration implementation; rc.4 freezes future executable empty/prod/N-1/rollback/foreign-workspace and duplicate-key-feasibility tests.
|
||||
- Budget: no explicit cap supplied; use a 20K-equivalent soft working cap and one bounded worker lane.
|
||||
- Read-only #757 boundary: PR #757 adds separate logical-agent connector lease/CAS fencing (`logical_agent_connector_leases.lease_epoch`) in runtime schema and connector contracts. SI-001 changes only the frozen mission candidate key; it does not consume, alter, or reinterpret connector fencing, task fencing, lease authority, or #757 ownership.
|
||||
|
||||
### Amendment verification evidence
|
||||
|
||||
- Frozen schema: added exactly one non-partial `missions_workspace_id_uidx` on `(workspace_id, id)`; retained the global `id` primary key and `missions_workspace_project_id_uidx` on `(workspace_id, project_id, id)`.
|
||||
- FK reconciliation: targeted static checks prove the candidate key precedes both `artifacts_workspace_mission_fk` and `approval_decisions_workspace_mission_fk`; each continues to reference exact ordered columns `(workspace_id, mission_id)` → `missions(workspace_id, id)` with RESTRICT deletion.
|
||||
- Shared contract: candidate version is `1.0.0-rc.4`; authority, rationale, exact effect/non-effect, candidate-before-FK DDL order, duplicate feasibility, empty/prod/N-1/rollback/foreign-workspace future tests, and unchanged KCR/SOT/tenant/proposal/fencing/no-cascade invariants are explicit.
|
||||
- Changed-file Prettier, contract ESLint, `pnpm exec tsc -p docs/native-kanban-sot/tsconfig.json`, and targeted SI-001 static checks — PASS.
|
||||
- Full `pnpm format:check && pnpm lint && pnpm typecheck` — PASS (23 lint tasks and 42 typecheck/build tasks).
|
||||
- Independent Codex security review — PASS, zero critical/high/medium/low findings; tenant isolation is preserved.
|
||||
- Independent Codex code review confirmed the candidate key repairs both FK targets and reported no blocker on the authorized delta. Its initial request to rewrite the historical KBN-010 verdict conflicts with the exclusive scope and was resolved by documenting the immutable-evidence boundary in rc.4; its remaining finding concerns pre-existing live `.mosaic` session files, which are untouched and excluded from this commit.
|
||||
- Scoped diff: only the two frozen contract files and this append-only scratchpad amendment are staged for delivery; no runtime schema, migration, task plan, gate verdict, provider artifact, or #757-owned file is included.
|
||||
|
||||
## 2026-07-14 final rc.4 KBN-010 disposition
|
||||
|
||||
- Control-plane direction authorized final disposition edits only to `docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md` and this append-only scratchpad; contract author `web1:kbn-contract` remained idle and undisturbed.
|
||||
- Exact reviewed contract object: commit `3f6a3387b419eb99453ee10dd25ba888faaab0b5`, tree `7ebab8fa530a7180036928cea9527f808548aa14`.
|
||||
- Corroborating review identities: full-index SHA-256 `6b40a76265c4f3e6d1d30a7f262a2dd16e0d51997e99c146b59f527e6524cd42`; stable patch-id `058cf98026fcd1043703c866aee047c8bb144740`. A command-rendered patch digest varied by Git rendering command/options and is non-authoritative; commit+tree+exact file content are canonical.
|
||||
- Independent Homelab non-author schema/security review verdict: **APPROVE**. It confirmed the rc.4 `missions_workspace_id_uidx(workspace_id,id)` repairs both dependent FKs while retaining the global PK and project-congruent key; tenant, polymorphic exactly-one-target, RESTRICT/no-cascade, N-1/rollback semantics remain valid.
|
||||
- Read-only #757 cross-check: no shared table, index, FK, identity, fence, or authority collision with connector lease/CAS fencing.
|
||||
- Final KBN-010 gate decision: **PASS / GO** at rc.4 with `UNRESOLVED SCHEMA IMPACTS` equal to exact `none`. Historical SI-001 detection remains in the gate document as evidence that rc.3 was invalid.
|
||||
- No runtime schema/migration/API/config/dependency/CI/deployment implementation is claimed. KBN-100 must still implement candidate-before-dependent-FK ordering, duplicate feasibility, empty/prod/N-1/rollback evidence, exact FK reconciliation, and separate artifact/approval foreign-workspace negatives (N100-45..50).
|
||||
- TDD remains not applicable because this continuation changes only documentation/evidence disposition and no runtime behavior.
|
||||
- Remaining orchestrator-owned sequence: worker validation/commit/push → PR open/update → Ultron review → squash merge → terminal-green post-main CI → close #753 → release KBN-100. KBN-100 is not released earlier.
|
||||
|
||||
### Final worker validation
|
||||
|
||||
- Changed-doc Prettier and link checks — PASS; issue #753 external link returned successfully.
|
||||
- Static future-test catalog check — PASS: all prefixes are individually enumerated, contiguous, and duplicate-free; N100 now spans N100-01..50.
|
||||
- rc.4 disposition assertions — PASS: gate status PASS/GO, frozen target rc.4, exact `none` unresolved section, independent APPROVE, review identities, and N100-50 are present.
|
||||
- Review identity reproduction — PASS: commit tree `7ebab8fa530a7180036928cea9527f808548aa14`, full-index SHA-256 `6b40a76265c4f3e6d1d30a7f262a2dd16e0d51997e99c146b59f527e6524cd42`, and stable patch-id `058cf98026fcd1043703c866aee047c8bb144740` match.
|
||||
- rc.4 frozen-contract static check — PASS: the candidate key is unique in the declaration and precedes both dependent FKs; frozen contract files remain byte-identical to commit `3f6a3387b419eb99453ee10dd25ba888faaab0b5`.
|
||||
- `pnpm exec tsc -p docs/native-kanban-sot/tsconfig.json --noEmit` — PASS.
|
||||
- `pnpm format:check` — PASS.
|
||||
- `pnpm lint` — PASS (23 tasks successful).
|
||||
- `pnpm typecheck` — PASS (42 tasks successful).
|
||||
- Scoped diff — PASS: only the gate document and this scratchpad are authored changes; contracts, TASKS, requirements, runtime/schema/migration/config/dependency/CI/deployment and #757-owned files are unchanged. Live `.mosaic` session state remains untouched and excluded.
|
||||
- Remaining worker steps: final formatting/scoped staging, commit `docs(#753): clear KBN-010 schema gate`, queue guard, push, and control-plane notification.
|
||||
@@ -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