Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7081b58a4a | ||
|
|
717ebd1fb2 | ||
|
|
9032b05703 |
@@ -4,6 +4,14 @@ pnpm-lock.yaml
|
||||
**/node_modules
|
||||
**/drizzle
|
||||
**/.next
|
||||
# Python build/test artifacts — same category as node_modules/dist/.next above.
|
||||
# Prettier must never scan generated trees; without these a local venv poisons
|
||||
# `pnpm format:check` with thousands of third-party files.
|
||||
**/venv
|
||||
**/__pycache__
|
||||
**/.mypy_cache
|
||||
**/.pytest_cache
|
||||
**/htmlcov
|
||||
.claude/
|
||||
docs/tess/TASKS.md
|
||||
docs/scratchpads/
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# mos-remediation — LIVE BOARD (keep < 8 KB)
|
||||
|
||||
**Phase:** PLANNING — adversarial task decomposition IN FLIGHT.
|
||||
**Updated:** 2026-07-31 (mos-remediation orchestrator; seat active on `mosaic-fleet`).
|
||||
|
||||
## Head
|
||||
|
||||
- Mission charter + 15 decisions + 4-build plan: PERSISTED (`docs/remediation/MISSION.md`).
|
||||
- HOLD lifted for this workstream (Jason 2026-07-31). Nothing implemented yet — planning first.
|
||||
- Orchestrator seat `mos-remediation` is LIVE and owns the mission. Residency attestation: PASS.
|
||||
- TASK-0 (env + push mission package) IN PROGRESS — checkout deps repaired, gates being verified honestly.
|
||||
- TASK-1 (adversarial decomp) DISPATCHED for real, both planners on GUARANTEED-CLEAN context.
|
||||
|
||||
## In-flight
|
||||
|
||||
| Task | Owner | State |
|
||||
| ---------------------------------------------------- | --------------- | --------------------------------------------- |
|
||||
| TASK-0 env repair + push `remediation/mission-setup` | mos-remediation | IN PROGRESS — deps installed; gates verifying |
|
||||
| Decomp (robustness side) → `DECOMP-OPUS.md` | planner-opus | WORKING (clean session) |
|
||||
| Decomp (pragmatic side) → `DECOMP-SOL.md` | planner-sol | WORKING (reset to 0.0% ctx before dispatch) |
|
||||
| Reconcile both decomps → `docs/remediation/TASKS.md` | mos-remediation | BLOCKED on the two decomps |
|
||||
|
||||
## Fleet seats
|
||||
|
||||
- mos-remediation — project orchestrator (Claude, /src/mosaic-stack, socket `mosaic-fleet`) — ACTIVE
|
||||
- planner-opus — adversarial planner (robustness), Opus 5, socket `default` — WORKING
|
||||
- planner-sol — adversarial planner (pragmatic), gpt-5.6-sol, socket `default` — WORKING
|
||||
- rev-974 — mosaicstack reviewer identity (id 16, write:repository) — idle, on call
|
||||
- Mos (mos-claude) — lead coordinator, socket `default` — relay path to Jason
|
||||
|
||||
## Gate status
|
||||
|
||||
- Delivery gates active: author≠reviewer, diff-blind pre-registered checks, CI-green, merged-PR completion.
|
||||
- Freeze: LIFTED for this workstream only.
|
||||
- Git identity: `MOSAIC_GIT_IDENTITY=mos-dt-0` INTERIM. Mos ruled gate-16 HOLDS (author≠reviewer is what
|
||||
gate-16 protects; rev-974 reviews, mos-dt-0 never self-reviews). Dedicated identity TRACKED, Mos provisions.
|
||||
- Remote control: native `/remote-control` NOT wired in this runtime. Path is **Mos-relay**
|
||||
(Jason ↔ mos-claude via Discord ↔ mos-remediation via agent-send). Not a blocker.
|
||||
|
||||
## Sequencing (from MISSION.md)
|
||||
|
||||
1. Spine + choke-point service (MACP wiring @ mosaic_orchestrator.py::run_single_task) + PG/Redis
|
||||
2. Rotation daemon (finish Mission Control Plane, reuse packages/coord)
|
||||
3. Comms service (envelope→service→PG/Redis→adapters)
|
||||
4. Hygiene + conformance harness
|
||||
Cross-cutting retirements: flat-file tracking, 3 MACP islands, silent MOSAIC BYPASS.
|
||||
|
||||
## Dogfood evidence captured this session (live failure classes, not hypotheticals)
|
||||
|
||||
- **D-1 / P-ACTIVATION + hygiene — committed `.npmrc` hard-pins `store-dir=/root/.local/share/pnpm/store`.**
|
||||
Correct for the CI container (runs as root), fatal for EVERY non-root local checkout: `EACCES` on
|
||||
`/root/.local/share/pnpm/store/v10/server/server.json`. A committed config that only works on one
|
||||
runtime is exactly the activation-skew class. Fix candidate: make store-dir env-overridable, not hardcoded.
|
||||
- **D-2 / hygiene — husky `prepare` fails `EPERM` copying into root-owned `.husky/_/`.** Repo working
|
||||
tree has root-owned dirs (`.husky/`, repo root) under a non-root agent. Worked around with the
|
||||
intended `HUSKY=0` escape hatch (does NOT disable the existing pre-commit/pre-push hooks).
|
||||
- **D-3 / P-FLEET-001 — the seats running this mission are UNMANAGED.** `mos-remediation`, `rev-974`,
|
||||
`planner-opus`, `planner-sol` appear in NO roster (`~/.config/mosaic/fleet/roster.yaml`, `agents/`).
|
||||
Planners run on socket `default`; the roster declares `mosaic-fleet`. This is the exact
|
||||
"one roster-owned socket/host + quarantine unmanaged + stale GC" failure P-FLEET-001 indicts —
|
||||
observed on the remediation mission's own fleet. Prerequisite for INBOX identity-addressing.
|
||||
- **D-4 / P-LIFECYCLE + hygiene — a dispatched agent silently IGNORED an in-message context reset.**
|
||||
planner-sol was at 64.3%/372k; the brief asked it to reset first; it began work on dirty context anyway.
|
||||
Only an out-of-band `/new` driven by the orchestrator guaranteed clean state. Confirms the postmortem
|
||||
thesis: **instructions are not enforcement.** Reset must be a mechanical pre-dispatch step, not a request.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-07-31 — Mission set up by Mos post-postmortem (15/15 decided). Dogfood posture active.
|
||||
- 2026-07-31 — Mos: stale `.mosaic/orchestrator/mission.json` is RESIDUE of the disabled Python
|
||||
orchestrator rail that this plan RETIRES. Do NOT invest in it; do NOT build on that rail. The 0/0
|
||||
milestone banner is cosmetic. (Supersedes any plan to repair it.)
|
||||
- 2026-07-31 — Mos: planners must be dispatched with GUARANTEED clean context, not requested-clean.
|
||||
Prior default-socket planner sessions predate this mission; dirty context is the indicted hygiene.
|
||||
- 2026-07-31 — mos-remediation: worker briefs forbid all git ops and restrict each worker to a single
|
||||
named output file, so two planners can share one checkout without a branch race (M2-era incident doctrine).
|
||||
@@ -0,0 +1,44 @@
|
||||
# mos-remediation — Orchestrator Kickstart / Compaction-Survival Resume
|
||||
|
||||
**You are `mos-remediation`, the project orchestrator for the Mosaic Stack remediation, launched in `/src/mosaic-stack`.**
|
||||
This file is your fail-closed resume procedure. Read it on EVERY fresh/cleared session and on the FIRST turn
|
||||
after any compaction. This mission's whole point is that manual compaction-survival is fragile — so follow this
|
||||
mechanically until Build 3 (rotation) makes it automatic.
|
||||
|
||||
## On resume (do in order, before any orchestration action)
|
||||
|
||||
1. `cd /src/mosaic-stack` and confirm you are on the remediation working branch.
|
||||
2. Read `docs/remediation/MISSION.md` — the charter (goal, 4 builds, 15 decisions, sequencing, directives).
|
||||
3. Read `docs/remediation/BOARD.md` — the LIVE state: current phase, in-flight tasks, fleet seat assignments,
|
||||
gate status. This is your single source of in-flight truth (kept small).
|
||||
4. Read the discussion checkpoint for full rationale if needed:
|
||||
`../jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md` (or the jarvis-brain repo path).
|
||||
5. **Residency attestation (fail-closed):** restate from the reloaded files — (a) the goal in one line, (b) the
|
||||
current build/phase, (c) the BOARD head (in-flight tasks + who owns them). If you cannot, HALT and re-read.
|
||||
Do NOT act on memory alone; a compaction may have dropped context silently.
|
||||
|
||||
## Standing invariants (never violate)
|
||||
|
||||
- **North star:** deterministic-right-answer → code/gate; LLM only for judgment.
|
||||
- **Delivery gates:** author≠reviewer; PRE-REGISTERED diff-blind checks committed before reading the diff;
|
||||
CI terminal-green; completion = merged PR + closed issue. rev-974 = the mosaicstack reviewer identity.
|
||||
- **Dogfooding:** every fix validated against its live seed case (MISSION.md lists them).
|
||||
- **Tracking → DB** (hard cutover); do NOT re-invest in flat-file tracking. jarvis-brain PDA is off-limits.
|
||||
- **Git identity:** export `MOSAIC_GIT_IDENTITY=<your-seat>` so wrappers author correctly and survive respawn.
|
||||
|
||||
## After every significant event
|
||||
|
||||
Overwrite stale lines in `BOARD.md`, keep it < 8 KB, commit + push. The board IS your checkpoint until the
|
||||
DB-backed rotation daemon (Build 3) exists. Persist typed state (phase, tasks, owners, gates) — never the transcript.
|
||||
|
||||
## Fleet
|
||||
|
||||
- Adversarial planners: `planner-opus` (robustness), `planner-sol` (pragmatic) — dispatch for task decomposition; reconcile their oppositional decomps.
|
||||
- Coders/reviewers: dispatch per roster + delivery gates. Comms: `~/.config/mosaic/tools/tmux/agent-send.sh`
|
||||
(`-L <socket> -s <dst> -S <yourhost>:<yourseat> --class <class>`); always pass `-S`.
|
||||
- Lead coordinator: Mos (`mos-claude`). Escalate only on the Constitution's escalation triggers.
|
||||
|
||||
## Remote control
|
||||
|
||||
On first startup, activate remote control for this session (`/remote-control`) so Jason can reach/drive you while
|
||||
away. If the command is unavailable in this runtime, report it to Mos and continue — it is not a blocker.
|
||||
@@ -0,0 +1,98 @@
|
||||
# MACP wiring investigation
|
||||
|
||||
**Scope:** `/src/mosaic-stack` inspected at HEAD `b79336a8c11e2a4646a47ff8d295a226e0c71404`; read-only. Existing dirty/untracked state was not touched.
|
||||
|
||||
## Verdict
|
||||
|
||||
**(c) STRANDED.** `packages/macp` is exported, unit-tested, and registered as a CLI command group, but no production dispatch/execution code invokes its credential resolver, gate runner, or event emitter.
|
||||
A separate MACP-named OpenClaw/orchestrator rail exists, but it redefines task/result types and gate/event logic instead of importing `@mosaicstack/macp`; direct `mosaic yolo|claude|codex|opencode|pi` also bypasses it.
|
||||
|
||||
## 1. Production call sites vs tests
|
||||
|
||||
### Production references to `@mosaicstack/macp`
|
||||
|
||||
| Surface | Evidence | Actual use |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Unified CLI | `packages/mosaic/src/cli.ts:8,385` | Imports and registers `registerMacpCommand`; no task/gate/event execution. |
|
||||
| Forge | `packages/forge/src/types.ts:1,17,68,79` | **Type-only** imports of `GateEntry` and `TaskResult`. Pipeline calls an injected abstract executor at `packages/forge/src/pipeline-runner.ts:189-190,299-300`, not MACP. |
|
||||
| Mosaic package metadata | `packages/mosaic/package.json:36`; `packages/mosaic/src/runtime/update-checker.ts:172` | Dependency/update inventory only. |
|
||||
| Agent | No match under production `packages/agent/src/**` | No MACP import/call. |
|
||||
| Coord | No match under production `packages/coord/src/**`; dependency list is only `@mosaicstack/types` at `packages/coord/package.json:25-27` | No MACP import/call. |
|
||||
| Plugins | No `@mosaicstack/macp` import under `plugins/**` | No package use; the MACP-named plugin is an independent implementation (below). |
|
||||
|
||||
**Repository-wide production call-site search result:** excluding `packages/macp/**`, tests, worktrees, and build output, there are **zero** calls to `runGate`, `runGates`, `emitEvent`, `appendEvent`, or `resolveCredentials`.
|
||||
|
||||
### `packages/macp` implementation is internally connected only
|
||||
|
||||
- Public exports: `packages/macp/src/index.ts:1-48` exports Task/GateEntry/MACPEvent/TaskResult, credential resolution, `runGate(s)`, risk-floor, and event emission.
|
||||
- Gate runner calls its own event emitter: `packages/macp/src/gate-runner.ts:187-236`.
|
||||
- Event persistence implementation appends NDJSON to a caller-supplied path: `packages/macp/src/event-emitter.ts:11-27`.
|
||||
- There is **no exported programmatic `submit` implementation** in `packages/macp/src/index.ts:1-48`; only the CLI placeholder named `submit`.
|
||||
|
||||
### Test-only invocations
|
||||
|
||||
- Gate runner: `packages/macp/__tests__/gate-runner.test.ts:96-242` invokes `runGate/runGates`.
|
||||
- Event ledger: `packages/macp/__tests__/event-emitter.test.ts:46-133` invokes `appendEvent/emitEvent` against temporary `events.ndjson` files.
|
||||
- Credential resolver: `packages/macp/__tests__/credential-resolver.test.ts` exercises resolver behavior.
|
||||
- CLI tests only verify command registration: `packages/macp/src/cli.spec.ts:37-73`; `packages/mosaic/src/cli-smoke.spec.ts:8` imports registration.
|
||||
|
||||
## 2. Gate on the live dispatch path
|
||||
|
||||
### Direct Mosaic runtime launch bypasses MACP
|
||||
|
||||
- Runtime commands dispatch directly to harness launch: `packages/mosaic/src/commands/launch.ts:730-801`.
|
||||
- Claude/Pi go through the lease broker, then spawn the runtime: `packages/mosaic/src/commands/launch.ts:817-843`.
|
||||
- Commander wiring sends `mosaic yolo <runtime>` and direct runtime commands to `launchRuntime`: `packages/mosaic/src/commands/launch.ts:1102-1157,1165-1167`.
|
||||
- None of those ranges imports/calls `@mosaicstack/macp`, `runGates`, or `emitEvent`.
|
||||
|
||||
**Result:** a direct `mosaic yolo`, `mosaic claude/codex/opencode/pi`, or underlying exec does not create a typed MACP Task, run the package gate-runner, or append a package MACPEvent.
|
||||
|
||||
### Coord bypasses MACP
|
||||
|
||||
- Coord reads/updates `docs/TASKS.md`: `packages/coord/src/runner.ts:6,306-386`; parser/writer is `packages/coord/src/tasks-file.ts:326-377`.
|
||||
- Coord launches a child process directly: `packages/coord/src/runner.ts:397-427`.
|
||||
- Mission state is its own `.mosaic/orchestrator/mission.json`/`next-task.json`: `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:15-16,355-384`.
|
||||
|
||||
**Result:** Coord task execution has no MACP Task validation, package gate runner, or event append.
|
||||
|
||||
### Forge bypasses MACP execution
|
||||
|
||||
- Forge defines its own `ForgeTask` and abstract `TaskExecutor`: `packages/forge/src/types.ts:48-80`.
|
||||
- The production CLI injects a **stub executor** that immediately reports completion with empty gates: `packages/forge/src/cli.ts:13-31,167,185`.
|
||||
|
||||
**Result:** even `mosaic forge run` does not execute MACP gates or persist MACP events.
|
||||
|
||||
### Separate MACP-named rail is not `packages/macp`
|
||||
|
||||
- OpenClaw plugin registers an ACP backend named `macp`: `plugins/macp/src/index.ts:1-18,72-102`.
|
||||
- It locally redefines `OrchestratorTask`, `TaskResult`, and gate-result shapes instead of importing package types: `plugins/macp/src/macp-runtime.ts:43-77`.
|
||||
- It appends directly to `.mosaic/orchestrator/tasks.json`, triggers an external controller, and polls `results/<task>.json`: `plugins/macp/src/macp-runtime.ts:290-329,437-483`.
|
||||
- The controller independently implements `append_event`, `emit_event`, shell execution, gate execution, and results: `packages/mosaic/framework/tools/orchestrator-matrix/controller/mosaic_orchestrator.py:29-91,126-276`.
|
||||
- Its gate loop runs raw string gates after worker success: `mosaic_orchestrator.py:213-235`; it does not support the package's structured `GateEntry`/AI-review behavior.
|
||||
- Current checkout disables this controller: `.mosaic/orchestrator/config.json:2` (`"enabled": false`).
|
||||
- Plugin references `tools/macp/dispatcher/pi_runner.ts` at `plugins/macp/src/macp-runtime.ts:85-91`, but `tools/macp/` does not exist in this checkout.
|
||||
|
||||
**Result:** there is a parallel, optionally enabled MACP-shaped rail, not package integration. It cannot make `packages/macp` the enforced path.
|
||||
|
||||
## 3. Event ledger status
|
||||
|
||||
- Package persistence exists only as a library primitive: `packages/macp/src/event-emitter.ts:11-27` appends JSON lines to an arbitrary `eventsPath`.
|
||||
- Package event emission is reached only from package `runGates`: `packages/macp/src/gate-runner.ts:204-236`.
|
||||
- No production caller invokes package `runGates/emitEvent/appendEvent`; therefore no runtime destination path is configured for the package ledger.
|
||||
- Test-only ledgers use temp paths: `packages/macp/__tests__/event-emitter.test.ts:35-133`; gate tests use temp `events.ndjson`: `packages/macp/__tests__/gate-runner.test.ts:171-242`.
|
||||
- The separate Python controller writes `.mosaic/orchestrator/events.ndjson`: `mosaic_orchestrator.py:129-133,159-161,219-235`; the Mosaic Framework plugin only **reads** that file for context at `plugins/mosaic-framework/src/index.ts:279-316,430-438`.
|
||||
- In this checkout, `.mosaic/orchestrator/events.ndjson` is absent and the controller is disabled (`.mosaic/orchestrator/config.json:2`).
|
||||
|
||||
**Conclusion:** `MACPEvent` from `packages/macp` is defined/tested but not emitted or persisted by live production call sites. The similarly shaped Python ledger is a duplicate island.
|
||||
|
||||
## 4. Coord link
|
||||
|
||||
- `packages/coord` has no `@mosaicstack/macp` dependency/import: `packages/coord/package.json:25-27`; no matches in `packages/coord/src/**`.
|
||||
- Coord's task model is Markdown `docs/TASKS.md` plus mission/session JSON: `packages/coord/src/tasks-file.ts:1-10,257-377`; `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:306-427`.
|
||||
- It does not consume `.mosaic/orchestrator/events.ndjson`, MACP Task, MACPEvent, GateEntry, or TaskResult.
|
||||
|
||||
**Conclusion:** Coord and `packages/macp` are disconnected islands.
|
||||
|
||||
## Shortest wiring gap
|
||||
|
||||
**Single integration point:** replace the duplicated execution/gate/event block in `mosaic_orchestrator.py::run_single_task` (`:126-276`) with one production Node `TaskExecutor` backed by `@mosaicstack/macp` (typed Task validation + `resolveCredentials` + `runGates` + `emitEvent`), and make Coord/Forge/OpenClaw submit through that executor. This queue/controller choke point is where `yolo|acp|exec` worker outcomes can be gated and journaled before completion is recorded.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Mosaic Stack Remediation — Mission Charter
|
||||
|
||||
**Owner:** project orchestrator `mos-remediation` (Claude, launched in `/src/mosaic-stack`).
|
||||
**Origin:** 2026-07-16..31 fleet lifecycle postmortem. **Status:** PLANNING (task decomposition).
|
||||
**HOLD lifted** for this workstream by Jason, 2026-07-31 — "begin full mosaic fleet operation on this."
|
||||
|
||||
## Goal
|
||||
|
||||
Convert the 15 accepted postmortem remediation proposals into a working, **dogfooded** implementation.
|
||||
**North star:** anything with a deterministic right answer moves OUT of the LLM into a deterministic
|
||||
gate/program; the LLM handles only genuine judgment.
|
||||
|
||||
## Decision record (authoritative, immutable)
|
||||
|
||||
- **15/15 proposals decided: 13 accept, 2 modify (P-AUTHORITY-001, P-INBOX-001), 0 reject.**
|
||||
- Site + `annotations.json`: `jarvis-brain/docs/postmortem-spec/site/` (committed, origin/main).
|
||||
- Discussion checkpoint (rich rationale per proposal): `jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md`.
|
||||
- Postmortem report: mosaicstack/stack PR #107 (merged 88f4ee04).
|
||||
- MACP wiring scout (verdict c=STRANDED): `/tmp/macp-wiring-investigation.md` (copy into this dir — see TODO).
|
||||
|
||||
## The plan — 15 proposals collapse to 4 builds + hygiene
|
||||
|
||||
| Build | Absorbs | What it is |
|
||||
| ------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **1. One choke-point service** (mechanical enforcer) | MISSION, STATE, AUDIT, WRAPPER, QUEUE | Deterministic program every task/data mutation flows through. **Wire the stranded `@mosaicstack/macp`** in at `mosaic_orchestrator.py::run_single_task` — typed tasks, gate-runner, event ledger, credential binding, tri-state write outcomes. |
|
||||
| **2. One durable spine + hot path** | (storage under everything) | **PG system-of-record + Redis hot queue** (transactional-outbox). Mission/tasks/state-claims/audit-ledger/comms-inbox all land here. |
|
||||
| **3. Rotation lifecycle** (finish the Mission Control Plane) | LIFECYCLE, CONTRACT, GUIDE, RECOVERY | Coordinator daemon: contract-hash binding, compaction-detected → rotate-not-compact, checkpoint→fresh-session→rehydrate, broker-independent recovery. Deterministic, not an LLM. Reuse `packages/coord`; existing PRD at `docs/mission-control/`. |
|
||||
| **4. Comms service** | AUTHORITY, INBOX (+ versioning roadmap) | Envelope (comms/v1) → sole-path service → PG/Redis → pluggable adapters (tmux→Matrix/Discord/Slack/Telegram). Version the protocol, not participants. |
|
||||
| **+ Hygiene & proof** | FLEET, WORKFLOW, CONFORMANCE | One roster-owned socket/host + stale GC; allowlist auto-sync; the conformance harness that fault-injects the failure classes and proves builds 1–4 hold. |
|
||||
|
||||
## The finding that sets the cost
|
||||
|
||||
**Built-but-unwired disease.** `@mosaicstack/macp` is stranded (nothing calls it); `packages/coord` primitives
|
||||
exist; the Mission Control PRD exists; PG + Redis already run in-stack. Three duplicate MACP islands, an
|
||||
orphaned context loader, a fail-open bypass. **Work = wire + consolidate + retire, NOT greenfield. "Finish, don't re-spec."**
|
||||
|
||||
## Sequencing (skeleton — adversarial decomposition refines this)
|
||||
|
||||
1. **Spine + choke-point service** (builds 1+2) — foundation; unlocks MISSION/STATE/AUDIT/WRAPPER/QUEUE at one integration point.
|
||||
2. **Rotation daemon** (build 3) on that spine — the drift fix proper.
|
||||
3. **Comms service** (build 4) — envelope → service → PG/Redis → adapters; retire direct-tmux.
|
||||
4. **Hygiene + conformance** (build 5) — fleet convergence, allowlist sync, dogfood harness.
|
||||
|
||||
- **Cross-cutting retirements:** flat-file orchestration tracking (hard cutover to DB), the 3 duplicate MACP islands, the silent `MOSAIC BYPASS`.
|
||||
|
||||
## Standing directives (Jason, 2026-07-31)
|
||||
|
||||
- **Dogfooding:** validate EACH fix against the live fleet failure that motivated it. Seed acceptance tests:
|
||||
Pi brick (RECOVERY), scout-bounce (INBOX/FLEET), gate-6 inert + #1019 recursion (QUEUE), identity drift
|
||||
(WRAPPER), auto-sync sweep (WORKFLOW), #1018 stale-consumed (INBOX). The fleet is its own test bed.
|
||||
- **Orchestration tracking → DB**, hard cutover ("rip off the bandaid"), NO flat-file interim. jarvis-brain
|
||||
PDA flat-files untouched. Current flat-file tracking runs as-is/unhardened until DB tracking is real, then one clean replace.
|
||||
|
||||
## The 15 decisions (one-line; full rationale in the checkpoint)
|
||||
|
||||
1. **P-ACTIVATION-001** accept — transactional CLI+hooks+broker+version release; block launch on skew, fail-SAFE.
|
||||
2. **P-AUTHORITY-001** MODIFY — structured authenticated inbox; envelope carries comms-PROTOCOL version; version the protocol not participants; N-version window.
|
||||
3. **P-LIFECYCLE-001** accept — rotation not recursive compaction; pre-empt at token threshold; enforcer = deterministic coordinator; = finish Mission Control Plane.
|
||||
4. **P-MISSION-001** accept — bind lanes to mission+task ledger; convention exists, ENFORCEMENT is the gap; mission+tasks → DB spine (hard cutover).
|
||||
5. **P-QUEUE-001** accept — repair queue transport + exit-asserting non-null-case tests (gate-6 was INERT fleet-wide; #1019 fix recursed the same bug).
|
||||
6. **P-STATE-001** accept — typed claims (source/confidence/TTL) not prose blob; MACP typed record; integrity fail-closed HMAC; don't fork a 4th island.
|
||||
7. **P-AUDIT-001** accept — MACPEvent lifecycle ledger; EXTEND enum to lifecycle events; runtime-neutral (executor-emitted); retire duplicate Python ledger.
|
||||
8. **P-WRAPPER-001** accept — identity derives from seat name + survives respawn; tri-state write outcomes MANDATORY; name safe target metadata.
|
||||
9. **P-CONTRACT-001** accept — bind session to contract hash; re-anchor on policy-change OR compaction-detected; stale generation loses authority MECHANICALLY.
|
||||
10. **P-INBOX-001** MODIFY — sole-path comms SERVICE; PG durable SoR + Redis hot queue (outbox, reconciliation sweeper); pluggable adapters; protocol-first, PG-first-then-Redis.
|
||||
11. **P-RECOVERY-001** accept — broker-independent bootstrap recovery; honest capability labeling; break-glass LOUD+AUDITED+TEMPORARY not silent permanent bypass.
|
||||
12. **P-GUIDE-001** accept — delete `/compact and continue` from orchestrator path (keep for ephemeral); removal = substitution (wire rotation trigger).
|
||||
13. **P-FLEET-001** accept — one roster-owned socket/host; quarantine unmanaged; stale-session GC; prerequisite for INBOX identity-addressing.
|
||||
14. **P-WORKFLOW-001** accept — auto-sync ALLOWLIST not denylist; worktree/lease isolation for agent docs/source; DB-tracking obviates the flat-file-sweep criterion.
|
||||
15. **P-CONFORMANCE-001** accept — fleet lifecycle harness on REAL runtime artifacts + fault injection; the 100-rotations-lossless bar is a test; target the DB substrate.
|
||||
|
||||
## Fleet operating model
|
||||
|
||||
- **Project orchestrator** `mos-remediation` (this seat) owns the mission; coordinates under Mos (lead).
|
||||
- **Adversarial task decomposition:** `planner-opus` (robustness) + `planner-sol` (pragmatic) each decompose
|
||||
the plan independently; orchestrator reconciles into `TASKS.md`/DB tasks. Oppositional by design.
|
||||
- **Delivery gates (non-negotiable):** author≠reviewer, PRE-REGISTERED diff-blind acceptance checks committed
|
||||
before reading the diff, CI terminal-green, completion = merged PR + closed issue. rev-974 = mosaicstack reviewer.
|
||||
- **Compaction survival:** see `KICKSTART.md` in this dir — the resume procedure. Persist typed state, not transcript.
|
||||
@@ -1,22 +0,0 @@
|
||||
[Unit]
|
||||
Description=Mosaic lease broker daemon (framework tools/lease-broker/daemon.py)
|
||||
Documentation=https://git.mosaicstack.dev/mosaicstack/stack
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# The broker socket lives under the runtime directory so it disappears with
|
||||
# the user session instead of surviving as stale state across logins.
|
||||
# daemon.py's secure_parent() fails closed unless this directory is exactly
|
||||
# 0700, so RuntimeDirectoryMode is not cosmetic.
|
||||
RuntimeDirectory=mosaic-lease
|
||||
RuntimeDirectoryMode=0700
|
||||
# Remove loader and noninteractive-shell controls before ExecStart loads env,
|
||||
# matching the tmux fleet units in this same directory.
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
ExecStart=/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin XDG_RUNTIME_DIR=%t /bin/bash --noprofile --norc %h/.config/mosaic/tools/lease-broker/start-lease-broker.sh
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Supervisor entry point for the Mosaic lease broker daemon (issue #869, C3).
|
||||
#
|
||||
# Resolves the broker socket path with the SAME precedence as
|
||||
# `defaultLeaseBrokerSocket` in `packages/mosaic/src/commands/launch.ts`, so a
|
||||
# gated runtime launched through that client always finds the socket this
|
||||
# supervisor creates:
|
||||
# 1. an explicit MOSAIC_LEASE_BROKER_SOCKET
|
||||
# 2. "$XDG_RUNTIME_DIR/mosaic-lease/broker.sock"
|
||||
# 3. "/run/user/<uid>/mosaic-lease/broker.sock"
|
||||
#
|
||||
# The state file is colocated next to the socket (same directory,
|
||||
# "state.json"), mirroring how the broker already colocates its per-session
|
||||
# generation files beside the socket.
|
||||
#
|
||||
# This script never installs, enables, or starts the systemd unit that calls
|
||||
# it; it is only ever invoked BY that unit (or by a human/test harness that
|
||||
# passes its own HOME/XDG_RUNTIME_DIR).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
if [ -n "${MOSAIC_LEASE_BROKER_SOCKET:-}" ]; then
|
||||
SOCKET="$MOSAIC_LEASE_BROKER_SOCKET"
|
||||
else
|
||||
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
SOCKET="$RUNTIME_DIR/mosaic-lease/broker.sock"
|
||||
fi
|
||||
STATE="$(dirname -- "$SOCKET")/state.json"
|
||||
|
||||
exec python3 "$SCRIPT_DIR/daemon.py" --socket "$SOCKET" --state "$STATE"
|
||||
@@ -1,237 +0,0 @@
|
||||
import { createServer, type Server } from 'node:net';
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyBrokerSupervisor,
|
||||
checkBrokerSupervisorHealth,
|
||||
isBrokerSupervisorHealthy,
|
||||
resolveBrokerSupervisorPaths,
|
||||
resolveLeaseBrokerSocketPath,
|
||||
type BrokerSupervisorPaths,
|
||||
} from './broker-supervisor.js';
|
||||
|
||||
const REAL_FRAMEWORK_ROOT = new URL('../../framework/', import.meta.url).pathname;
|
||||
|
||||
const cleanupDirs: string[] = [];
|
||||
const cleanupServers: Server[] = [];
|
||||
|
||||
async function tempDir(prefix: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
cleanupDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const server of cleanupServers.splice(0)) {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
for (const dir of cleanupDirs.splice(0)) {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('resolveLeaseBrokerSocketPath', () => {
|
||||
it('honors an explicit MOSAIC_LEASE_BROKER_SOCKET override', () => {
|
||||
expect(resolveLeaseBrokerSocketPath({ MOSAIC_LEASE_BROKER_SOCKET: '/tmp/explicit.sock' })).toBe(
|
||||
'/tmp/explicit.sock',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to $XDG_RUNTIME_DIR/mosaic-lease/broker.sock', () => {
|
||||
expect(resolveLeaseBrokerSocketPath({ XDG_RUNTIME_DIR: '/run/user/1000' })).toBe(
|
||||
join('/run/user/1000', 'mosaic-lease', 'broker.sock'),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to /run/user/<uid>/mosaic-lease/broker.sock as a last resort', () => {
|
||||
expect(resolveLeaseBrokerSocketPath({}, 4242)).toBe(
|
||||
join('/run/user', '4242', 'mosaic-lease', 'broker.sock'),
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers the explicit override over XDG_RUNTIME_DIR', () => {
|
||||
expect(
|
||||
resolveLeaseBrokerSocketPath({
|
||||
MOSAIC_LEASE_BROKER_SOCKET: '/explicit.sock',
|
||||
XDG_RUNTIME_DIR: '/run/user/1000',
|
||||
}),
|
||||
).toBe('/explicit.sock');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveBrokerSupervisorPaths', () => {
|
||||
it('colocates the state file next to the resolved socket', () => {
|
||||
const paths = resolveBrokerSupervisorPaths({
|
||||
mosaicHome: '/home/x/.config/mosaic',
|
||||
frameworkRoot: '/repo/framework',
|
||||
env: { XDG_RUNTIME_DIR: '/run/user/1000' },
|
||||
});
|
||||
expect(paths.socketPath).toBe(join('/run/user/1000', 'mosaic-lease', 'broker.sock'));
|
||||
expect(paths.statePath).toBe(join('/run/user/1000', 'mosaic-lease', 'state.json'));
|
||||
});
|
||||
|
||||
it('targets the systemd --user dir under the given home, not mosaicHome', () => {
|
||||
const paths = resolveBrokerSupervisorPaths({
|
||||
mosaicHome: '/somewhere-else/.config/mosaic',
|
||||
frameworkRoot: '/repo/framework',
|
||||
homeDir: '/home/canary',
|
||||
env: {},
|
||||
uid: 0,
|
||||
});
|
||||
expect(paths.systemdUserDir).toBe(join('/home/canary', '.config', 'systemd', 'user'));
|
||||
expect(paths.unitTargetPath).toBe(
|
||||
join('/home/canary', '.config', 'systemd', 'user', 'mosaic-lease-broker.service'),
|
||||
);
|
||||
});
|
||||
|
||||
it('is a pure function: identical options resolve to identical paths', () => {
|
||||
const options = {
|
||||
mosaicHome: '/h/.config/mosaic',
|
||||
frameworkRoot: '/repo/framework',
|
||||
env: { XDG_RUNTIME_DIR: '/run/user/1000' },
|
||||
};
|
||||
expect(resolveBrokerSupervisorPaths(options)).toEqual(resolveBrokerSupervisorPaths(options));
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyBrokerSupervisor', () => {
|
||||
async function fakePaths(): Promise<BrokerSupervisorPaths> {
|
||||
const home = await tempDir('mosaic-broker-supervisor-home-');
|
||||
const mosaicHome = join(home, '.config', 'mosaic');
|
||||
const runtimeDir = await tempDir('mosaic-broker-supervisor-runtime-');
|
||||
return resolveBrokerSupervisorPaths({
|
||||
mosaicHome,
|
||||
frameworkRoot: REAL_FRAMEWORK_ROOT,
|
||||
homeDir: home,
|
||||
env: { XDG_RUNTIME_DIR: runtimeDir },
|
||||
});
|
||||
}
|
||||
|
||||
it('renders a unit that references the installed wrapper script and hardens the runtime dir', async () => {
|
||||
const paths = await fakePaths();
|
||||
const unitSource = await readFile(paths.unitSourcePath, 'utf8');
|
||||
expect(unitSource).toContain('ExecStart=');
|
||||
expect(unitSource).toContain('%h/.config/mosaic/tools/lease-broker/start-lease-broker.sh');
|
||||
expect(unitSource).toContain('RuntimeDirectory=mosaic-lease');
|
||||
expect(unitSource).toContain('RuntimeDirectoryMode=0700');
|
||||
expect(unitSource).toContain('Restart=on-failure');
|
||||
expect(unitSource).toContain('WantedBy=default.target');
|
||||
// No ambient environment file preload, matching the other fleet units'
|
||||
// strict-parsing convention.
|
||||
expect(unitSource).not.toMatch(/^Environment(File)?=/m);
|
||||
});
|
||||
|
||||
it('materializes the unit, wrapper script, and daemon sources on first apply', async () => {
|
||||
const paths = await fakePaths();
|
||||
|
||||
const result = await applyBrokerSupervisor(paths);
|
||||
|
||||
expect(result.installedFiles).toContain(paths.unitTargetPath);
|
||||
expect(result.installedFiles).toContain(paths.wrapperTargetPath);
|
||||
for (const target of paths.daemonTargetPaths) {
|
||||
expect(result.installedFiles).toContain(target);
|
||||
}
|
||||
|
||||
const unitTargetContent = await readFile(paths.unitTargetPath, 'utf8');
|
||||
const unitSourceContent = await readFile(paths.unitSourcePath, 'utf8');
|
||||
expect(unitTargetContent).toBe(unitSourceContent);
|
||||
|
||||
const wrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;
|
||||
expect(wrapperMode).toBe(0o755);
|
||||
|
||||
for (const target of paths.daemonTargetPaths) {
|
||||
await expect(stat(target)).resolves.toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('is idempotent: applying twice reproduces identical files with no error', async () => {
|
||||
const paths = await fakePaths();
|
||||
|
||||
await applyBrokerSupervisor(paths);
|
||||
const firstUnit = await readFile(paths.unitTargetPath, 'utf8');
|
||||
const firstWrapper = await readFile(paths.wrapperTargetPath, 'utf8');
|
||||
const firstWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;
|
||||
|
||||
await expect(applyBrokerSupervisor(paths)).resolves.toBeDefined();
|
||||
|
||||
const secondUnit = await readFile(paths.unitTargetPath, 'utf8');
|
||||
const secondWrapper = await readFile(paths.wrapperTargetPath, 'utf8');
|
||||
const secondWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;
|
||||
|
||||
expect(secondUnit).toBe(firstUnit);
|
||||
expect(secondWrapper).toBe(firstWrapper);
|
||||
expect(secondWrapperMode).toBe(firstWrapperMode);
|
||||
});
|
||||
|
||||
it('never touches the real host: only writes under the supplied temp dirs', async () => {
|
||||
const paths = await fakePaths();
|
||||
await applyBrokerSupervisor(paths);
|
||||
expect(paths.systemdUserDir.startsWith(tmpdir())).toBe(true);
|
||||
expect(paths.mosaicHome.startsWith(tmpdir())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkBrokerSupervisorHealth / isBrokerSupervisorHealthy', () => {
|
||||
async function fakeHealthPaths(): Promise<
|
||||
Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>
|
||||
> {
|
||||
const runtimeDir = await tempDir('mosaic-broker-supervisor-health-');
|
||||
await mkdir(join(runtimeDir, 'systemd-user'), { recursive: true });
|
||||
return {
|
||||
unitTargetPath: join(runtimeDir, 'systemd-user', 'mosaic-lease-broker.service'),
|
||||
socketPath: join(runtimeDir, 'broker.sock'),
|
||||
};
|
||||
}
|
||||
|
||||
it('reports unhealthy when neither the unit nor the socket exist', async () => {
|
||||
const paths = await fakeHealthPaths();
|
||||
|
||||
const health = await checkBrokerSupervisorHealth(paths);
|
||||
|
||||
expect(health).toEqual({ unitInstalled: false, socketPresent: false, healthy: false });
|
||||
expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
|
||||
});
|
||||
|
||||
it('reports unhealthy when the unit is installed but no socket is listening', async () => {
|
||||
const paths = await fakeHealthPaths();
|
||||
await writeFile(paths.unitTargetPath, '[Unit]\n');
|
||||
|
||||
const health = await checkBrokerSupervisorHealth(paths);
|
||||
|
||||
expect(health.unitInstalled).toBe(true);
|
||||
expect(health.socketPresent).toBe(false);
|
||||
expect(health.healthy).toBe(false);
|
||||
});
|
||||
|
||||
it('reports healthy=true once a real Unix socket exists at the resolved path, and false again once removed', async () => {
|
||||
const paths = await fakeHealthPaths();
|
||||
|
||||
const server = createServer();
|
||||
cleanupServers.push(server);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(paths.socketPath, resolve);
|
||||
});
|
||||
|
||||
expect(await isBrokerSupervisorHealthy(paths)).toBe(true);
|
||||
const health = await checkBrokerSupervisorHealth(paths);
|
||||
expect(health.socketPresent).toBe(true);
|
||||
expect(health.healthy).toBe(true);
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await rm(paths.socketPath, { force: true });
|
||||
|
||||
expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not confuse a stale regular file at the socket path with a live socket', async () => {
|
||||
const paths = await fakeHealthPaths();
|
||||
await writeFile(paths.socketPath, 'not actually a socket');
|
||||
|
||||
expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Activation-side supervisor for the Mosaic lease broker (issue #869, Point-1
|
||||
* C3). #828 shipped fail-closed enforcement hooks (`mutator-gate.py`,
|
||||
* `receipt-observer-client.py`) with nothing that guaranteed `daemon.py` was
|
||||
* running or that its socket existed before a gated runtime started. This
|
||||
* module:
|
||||
*
|
||||
* - resolves the broker socket/state paths and the on-disk locations of the
|
||||
* supervisor artifacts, deterministically and consistently with
|
||||
* `defaultLeaseBrokerSocket` in `../commands/launch.ts`;
|
||||
* - idempotently applies (materializes) a systemd `--user` unit plus the
|
||||
* wrapper script and daemon sources it execs, mirroring the tmux fleet
|
||||
* unit convention in `framework/systemd/user/`;
|
||||
* - exposes a health predicate other cards (e.g. the C1 activation probe)
|
||||
* can call to learn whether a broker supervisor is present and healthy.
|
||||
*
|
||||
* `applyBrokerSupervisor` only writes files under the paths it is given. It
|
||||
* never runs `systemctl`, never starts `daemon.py`, and never touches a real
|
||||
* host's `~/.config` unless the caller explicitly resolves paths there.
|
||||
* Enabling/starting the unit is a separate, later, out-of-scope step.
|
||||
*/
|
||||
import { chmod, copyFile, mkdir, stat } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const UNIT_NAME = 'mosaic-lease-broker.service';
|
||||
const WRAPPER_SCRIPT_NAME = 'start-lease-broker.sh';
|
||||
|
||||
/** Co-located modules `daemon.py` imports at runtime; kept alongside it. */
|
||||
const DAEMON_SOURCE_FILE_NAMES = [
|
||||
'daemon.py',
|
||||
'lease_generation.py',
|
||||
'normative_fragments.py',
|
||||
'receipt_challenge.py',
|
||||
'receipt_observer.py',
|
||||
] as const;
|
||||
|
||||
export interface ResolveBrokerSupervisorPathsOptions {
|
||||
/** `~/.config/mosaic` (or an override) — where installed tool copies live. */
|
||||
mosaicHome: string;
|
||||
/** Root of the checked-out `framework/` directory (canonical file source). */
|
||||
frameworkRoot: string;
|
||||
/** Defaults to `process.env`; pass a fake for tests. */
|
||||
env?: NodeJS.ProcessEnv;
|
||||
/** Defaults to `os.homedir()`; pass a temp dir in tests. */
|
||||
homeDir?: string;
|
||||
/** Defaults to `process.getuid()` (or 0); pass a fake for tests. */
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
export interface BrokerSupervisorPaths {
|
||||
readonly mosaicHome: string;
|
||||
readonly frameworkRoot: string;
|
||||
readonly systemdUserDir: string;
|
||||
readonly leaseBrokerToolsDir: string;
|
||||
readonly unitSourcePath: string;
|
||||
readonly unitTargetPath: string;
|
||||
readonly wrapperSourcePath: string;
|
||||
readonly wrapperTargetPath: string;
|
||||
readonly daemonSourcePaths: readonly string[];
|
||||
readonly daemonTargetPaths: readonly string[];
|
||||
/**
|
||||
* Resolved with the same precedence as `defaultLeaseBrokerSocket` in
|
||||
* `../commands/launch.ts`: an explicit `MOSAIC_LEASE_BROKER_SOCKET`, else
|
||||
* `$XDG_RUNTIME_DIR/mosaic-lease/broker.sock`, else
|
||||
* `/run/user/<uid>/mosaic-lease/broker.sock`.
|
||||
*/
|
||||
readonly socketPath: string;
|
||||
/** Colocated next to the socket, matching the broker's own generation-file convention. */
|
||||
readonly statePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the lease broker socket path alone, with the same precedence as
|
||||
* `defaultLeaseBrokerSocket` in `../commands/launch.ts`. Exported so callers
|
||||
* (and tests) can assert the two stay in agreement without importing the CLI
|
||||
* command module.
|
||||
*/
|
||||
export function resolveLeaseBrokerSocketPath(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
uid: number = typeof process.getuid === 'function' ? process.getuid() : 0,
|
||||
): string {
|
||||
const explicit = env['MOSAIC_LEASE_BROKER_SOCKET'];
|
||||
if (explicit) return explicit;
|
||||
const runtimeDir = env['XDG_RUNTIME_DIR'];
|
||||
if (runtimeDir) return join(runtimeDir, 'mosaic-lease', 'broker.sock');
|
||||
return join('/run/user', String(uid), 'mosaic-lease', 'broker.sock');
|
||||
}
|
||||
|
||||
/** Resolve every path the supervisor apply/health functions need, deterministically. */
|
||||
export function resolveBrokerSupervisorPaths(
|
||||
options: ResolveBrokerSupervisorPathsOptions,
|
||||
): BrokerSupervisorPaths {
|
||||
const { mosaicHome, frameworkRoot } = options;
|
||||
const env = options.env ?? process.env;
|
||||
const homeDir = options.homeDir ?? homedir();
|
||||
const systemdUserDir = join(homeDir, '.config', 'systemd', 'user');
|
||||
const leaseBrokerToolsDir = join(mosaicHome, 'tools', 'lease-broker');
|
||||
const frameworkLeaseBrokerDir = join(frameworkRoot, 'tools', 'lease-broker');
|
||||
const socketPath = resolveLeaseBrokerSocketPath(env, options.uid);
|
||||
const statePath = join(dirname(socketPath), 'state.json');
|
||||
|
||||
return {
|
||||
mosaicHome,
|
||||
frameworkRoot,
|
||||
systemdUserDir,
|
||||
leaseBrokerToolsDir,
|
||||
unitSourcePath: join(frameworkRoot, 'systemd', 'user', UNIT_NAME),
|
||||
unitTargetPath: join(systemdUserDir, UNIT_NAME),
|
||||
wrapperSourcePath: join(frameworkLeaseBrokerDir, WRAPPER_SCRIPT_NAME),
|
||||
wrapperTargetPath: join(leaseBrokerToolsDir, WRAPPER_SCRIPT_NAME),
|
||||
daemonSourcePaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(frameworkLeaseBrokerDir, name)),
|
||||
daemonTargetPaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(leaseBrokerToolsDir, name)),
|
||||
socketPath,
|
||||
statePath,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApplyBrokerSupervisorResult {
|
||||
readonly installedFiles: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotently materialize the supervisor unit, its wrapper script, and the
|
||||
* daemon sources it execs. Safe to call on every reseed: every write is a
|
||||
* deterministic overwrite of the same target path from the same source, so a
|
||||
* second call reproduces identical bytes/modes and never errors.
|
||||
*
|
||||
* Never runs `systemctl`; the caller decides separately whether/when to
|
||||
* `daemon-reload`/`enable`/`start` the installed unit.
|
||||
*/
|
||||
export async function applyBrokerSupervisor(
|
||||
paths: BrokerSupervisorPaths,
|
||||
): Promise<ApplyBrokerSupervisorResult> {
|
||||
await mkdir(paths.leaseBrokerToolsDir, { recursive: true });
|
||||
await mkdir(paths.systemdUserDir, { recursive: true });
|
||||
|
||||
const installedFiles: string[] = [];
|
||||
|
||||
for (let index = 0; index < paths.daemonSourcePaths.length; index += 1) {
|
||||
const source = paths.daemonSourcePaths[index];
|
||||
const target = paths.daemonTargetPaths[index];
|
||||
if (source === undefined || target === undefined) continue;
|
||||
await copyFile(source, target);
|
||||
await chmod(target, 0o644);
|
||||
installedFiles.push(target);
|
||||
}
|
||||
|
||||
await copyFile(paths.wrapperSourcePath, paths.wrapperTargetPath);
|
||||
await chmod(paths.wrapperTargetPath, 0o755);
|
||||
installedFiles.push(paths.wrapperTargetPath);
|
||||
|
||||
await copyFile(paths.unitSourcePath, paths.unitTargetPath);
|
||||
await chmod(paths.unitTargetPath, 0o644);
|
||||
installedFiles.push(paths.unitTargetPath);
|
||||
|
||||
return { installedFiles };
|
||||
}
|
||||
|
||||
export interface BrokerSupervisorHealth {
|
||||
/** Whether the systemd unit file has been materialized at its target path. */
|
||||
readonly unitInstalled: boolean;
|
||||
/** Whether a Unix domain socket currently exists at the resolved socket path. */
|
||||
readonly socketPresent: boolean;
|
||||
/**
|
||||
* The signal other cards (e.g. C1's activation probe) should treat as
|
||||
* "a broker supervisor is present and healthy". Presence of a live socket
|
||||
* is the authoritative signal: a gated runtime can only ever succeed by
|
||||
* connecting to it, so this is what fail-closed callers must check.
|
||||
*/
|
||||
readonly healthy: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the supervisor's on-disk/health signals. Never throws for an
|
||||
* absent unit or socket — both simply report `false`; unexpected filesystem
|
||||
* errors (permission issues, etc.) still propagate.
|
||||
*/
|
||||
export async function checkBrokerSupervisorHealth(
|
||||
paths: Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>,
|
||||
): Promise<BrokerSupervisorHealth> {
|
||||
const [unitInstalled, socketPresent] = await Promise.all([
|
||||
pathExists(paths.unitTargetPath),
|
||||
isUnixSocket(paths.socketPath),
|
||||
]);
|
||||
return { unitInstalled, socketPresent, healthy: socketPresent };
|
||||
}
|
||||
|
||||
/** Convenience boolean form of {@link checkBrokerSupervisorHealth} for simple call sites. */
|
||||
export async function isBrokerSupervisorHealthy(
|
||||
paths: Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>,
|
||||
): Promise<boolean> {
|
||||
return (await checkBrokerSupervisorHealth(paths)).healthy;
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isEnoent(error)) return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function isUnixSocket(path: string): Promise<boolean> {
|
||||
try {
|
||||
const info = await stat(path);
|
||||
return info.isSocket();
|
||||
} catch (error) {
|
||||
if (isEnoent(error)) return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isEnoent(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
(error as NodeJS.ErrnoException).code === 'ENOENT'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user