Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e56f5947ac | ||
|
|
d52a3ecf25 | ||
|
|
5fde2da592 | ||
|
|
821eb80b91 |
+30
@@ -146,6 +146,36 @@ lands. M0 consists only of these normative requirements, the complete task DAG,
|
||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
||||
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
|
||||
|
||||
### Fleet git identity launch propagation (#1043)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
A fleet seat can have a registered per-agent Git credential while its launched runtime process lacks
|
||||
`MOSAIC_GIT_IDENTITY`. The credential resolver then cannot select the seat identity reliably, which
|
||||
blocks repository operations on fail-closed estates and can fall through to an unrelated identity on
|
||||
estates where that refusal is not active. The objective is to make Git identity a deterministic,
|
||||
roster-derived part of the generated launch projection and prove it reaches the launched process.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FGI-REQ-01`: Every generated fleet agent projection SHALL declare
|
||||
`MOSAIC_GIT_IDENTITY=<MOSAIC_AGENT_NAME>`; a differing or unsafe identity SHALL fail closed before
|
||||
tmux launch.
|
||||
2. `FGI-REQ-02`: The clean `/usr/bin/env -i` pane boundary SHALL pass every variable declared by the
|
||||
generated projection, including `MOSAIC_GIT_IDENTITY`, to the launched runtime process.
|
||||
3. `FGI-REQ-03`: A behavioral integration test SHALL set-compare the complete generated projection
|
||||
against the launched process environment. Source-text/string-presence assertions are insufficient.
|
||||
4. `FGI-REQ-04`: Verification SHALL include RED-first evidence and a delete-the-subject mutation that
|
||||
removes Git-identity pane propagation and makes the behavioral test fail.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FGI-01`: A launched seat process contains every key/value pair declared by its generated
|
||||
environment projection, including the roster-derived Git identity.
|
||||
2. `AC-FGI-02`: Missing, unsafe, or split Git identity is rejected before a tmux session is created.
|
||||
3. `AC-FGI-03`: Focused launcher and generated-environment tests, repository quality gates,
|
||||
independent review, and the required RED/green/R7 evidence are recorded before push.
|
||||
|
||||
---
|
||||
|
||||
## Exact Cross-Harness Fleet Communications Contract (#766)
|
||||
|
||||
+13
-10
@@ -5,14 +5,14 @@ Generated environment files are rebuildable projections, not an operator-editabl
|
||||
|
||||
## Launch chain
|
||||
|
||||
| Layer | Responsibility |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket. |
|
||||
| Projection writer | Renders deterministic fleet/agents/<name>.env.generated from the roster. |
|
||||
| Optional local data | Reads a strict, data-only fleet/agents/<name>.env.local; it cannot shadow generated keys. |
|
||||
| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. |
|
||||
| session launcher | Validates generated and local data before it queries, creates, or stops an exact tmux session. |
|
||||
| runtime launch | Derives the fixed mosaic yolo <runtime> argument array from validated roster data, then seeds the runtime contract. |
|
||||
| Layer | Responsibility |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket; Git identity is derived from the exact agent name. |
|
||||
| Projection writer | Renders deterministic fleet/agents/<name>.env.generated from the roster. |
|
||||
| Optional local data | Reads a strict, data-only fleet/agents/<name>.env.local; it cannot shadow generated keys. |
|
||||
| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. |
|
||||
| session launcher | Validates generated and local data before it queries, creates, or stops an exact tmux session. |
|
||||
| runtime launch | Derives the fixed mosaic yolo <runtime> argument array from validated roster data, then seeds the runtime contract. |
|
||||
|
||||
The launcher never `source`s or `eval`s an environment file and never accepts an environment-supplied
|
||||
command. `MOSAIC_AGENT_COMMAND`, command/channel overrides, unknown keys, generated-key shadowing,
|
||||
@@ -24,6 +24,7 @@ secret-like key names, duplicate keys, comments, quoted/export syntax, and unsaf
|
||||
|
||||
```dotenv
|
||||
MOSAIC_AGENT_NAME=<roster name>
|
||||
MOSAIC_GIT_IDENTITY=<roster name>
|
||||
MOSAIC_AGENT_CLASS=<roster class>
|
||||
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
||||
MOSAIC_AGENT_MODEL=<roster model hint>
|
||||
@@ -33,8 +34,10 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
|
||||
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
||||
```
|
||||
|
||||
The generated launch contract supports `claude`, `codex`, `opencode`, and `pi`. mosaic fleet add
|
||||
rejects another runtime before it writes the roster or modifies generated, local, or quarantine state.
|
||||
`MOSAIC_GIT_IDENTITY` is not independently configurable: it must equal `MOSAIC_AGENT_NAME`, preventing
|
||||
split runtime and repository identity authority. The generated launch contract supports `claude`,
|
||||
`codex`, `opencode`, and `pi`. mosaic fleet add rejects another runtime before it writes the roster or
|
||||
modifies generated, local, or quarantine state.
|
||||
The legacy dogfood stub remains an observability-only canary on its separate `mosaic-factory` socket;
|
||||
it has no generated-launch adapter and cannot be added through this path.
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
The launcher consumes validated data, not shell configuration.
|
||||
|
||||
1. Read and validate the canonical roster.
|
||||
2. Render deterministic <name>.env.generated data from that roster.
|
||||
2. Render deterministic <name>.env.generated data from that roster, including `MOSAIC_GIT_IDENTITY` derived exactly from the roster agent name.
|
||||
3. Parse optional <name>.env.local through a strict allowlist.
|
||||
4. Reject generated-key shadowing, unknown or sensitive-looking keys, unsafe paths/values, duplicates, malformed lines, shell syntax, and command overrides.
|
||||
5. Derive the runtime command from validated runtime/model/reasoning data.
|
||||
6. Target only the exact configured tmux socket and roster session after ownership checks.
|
||||
5. Reject a Git identity that is unsafe or differs from the generated agent name.
|
||||
6. Derive the runtime command from validated runtime/model/reasoning data and pass every generated projection entry through the clean process environment boundary.
|
||||
7. Target only the exact configured tmux socket and roster session after ownership checks.
|
||||
|
||||
## File precedence and ownership
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ values, credential material, or command text.
|
||||
|
||||
```dotenv
|
||||
MOSAIC_AGENT_NAME=<roster name>
|
||||
MOSAIC_GIT_IDENTITY=<roster name>
|
||||
MOSAIC_AGENT_CLASS=<roster class>
|
||||
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
||||
MOSAIC_AGENT_MODEL=<roster model hint>
|
||||
@@ -44,8 +45,9 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
|
||||
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
||||
```
|
||||
|
||||
The generated launch contract supports only `claude`, `codex`, `opencode`, and `pi`. fleet add
|
||||
uses that same runtime authority and rejects any other runtime before it writes the roster or changes
|
||||
`MOSAIC_GIT_IDENTITY` is derived from and must equal `MOSAIC_AGENT_NAME`; it is not a separate
|
||||
operator-controlled identity authority. The generated launch contract supports only `claude`, `codex`,
|
||||
`opencode`, and `pi`. fleet add uses that same runtime authority and rejects any other runtime before it writes the roster or changes
|
||||
projection, local, or quarantine files. The legacy dogfood stub on its separate `mosaic-factory`
|
||||
socket remains an observability canary; it has no generated-launch adapter and cannot be added through
|
||||
this projection path.
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
# #1043 — Fleet pane git-identity propagation
|
||||
|
||||
## Objective
|
||||
|
||||
Ensure a fleet seat's launched runtime process receives its roster-derived `MOSAIC_GIT_IDENTITY`, and lock the complete generated-environment propagation boundary with an enumerated set comparison.
|
||||
|
||||
## Tracking
|
||||
|
||||
- External issue: `mosaicstack/stack#1043`
|
||||
- Branch: `fix/1043-pane-git-identity`
|
||||
- Coordinator: `tl-mosaic`
|
||||
- `docs/TASKS.md`: read-only by project worker contract; not modified.
|
||||
|
||||
## Constraints
|
||||
|
||||
- RED-first bug reproducer is mandatory.
|
||||
- R7 delete-the-subject mutation must turn the behavioral test red.
|
||||
- Assert launched-process environment, not source text.
|
||||
- One push only; do not poll CI after push.
|
||||
- Run the CI queue guard immediately before push and report its `state=` line as state, not evidence.
|
||||
- Do not modify a live host launcher or obtain/copy another credential.
|
||||
- Self-post the PR, verify provider attribution, then stop.
|
||||
- Final status wording: `believed-fixed, pending jarvis validation`.
|
||||
|
||||
## Scope inventory
|
||||
|
||||
Re-derived against `origin/main` at `85d2108e`:
|
||||
|
||||
- Launch consumer: `packages/mosaic/framework/tools/fleet/start-agent-session.sh`
|
||||
- Behavioral launch test: `packages/mosaic/framework/tools/fleet/test-start-agent-session.sh`
|
||||
- Generated-environment contract/parser: `packages/mosaic/src/fleet/generated-env-boundary.ts`
|
||||
- Roster projection producers:
|
||||
- `packages/mosaic/src/commands/fleet.ts`
|
||||
- `packages/mosaic/src/fleet/fleet-reconciler.ts`
|
||||
- `packages/mosaic/src/fleet/fleet-agent-crud.ts`
|
||||
- `packages/mosaic/src/fleet/v1-v2-migration.ts`
|
||||
- Contract and producer tests discovered by repository search.
|
||||
- Generated-environment operator/developer docs and their executable documentation contract test.
|
||||
|
||||
Discrepancy sent to `tl-mosaic`: current main no longer contains the charter's `PANE_SHELL_SNIPPET`; #772 replaced it with an `/usr/bin/env -i` argv launch boundary, and current generated projections do not declare git identity. Code-read inventory is **NOT MEASURED** behavior.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add the process-environment set-comparison regression first and record RED.
|
||||
2. Add roster-derived `MOSAIC_GIT_IDENTITY=<agent name>` to the complete generated projection contract.
|
||||
3. Validate identity syntax and equality with `MOSAIC_AGENT_NAME`; pass it through the clean pane environment.
|
||||
4. Update affected projection tests and generated-environment docs.
|
||||
5. Run focused and baseline gates.
|
||||
6. Perform R7 by deleting the pane propagation entry, prove RED, restore, and prove GREEN.
|
||||
7. Run independent review, remediate, commit, queue guard, one push, self-post PR, verify provider attribution, and stop without CI polling.
|
||||
|
||||
## Budget
|
||||
|
||||
No explicit token cap was provided. Working cap: one narrow logical unit, no dependency installation unless existing tooling requires it, no unrelated refactor.
|
||||
|
||||
## Evidence log
|
||||
|
||||
### TDD and mutation evidence
|
||||
|
||||
- RED-first, repository launcher: `bash packages/mosaic/framework/tools/fleet/test-start-agent-session.sh` exited 64 on pre-fix source with `code=unknown-key key=MOSAIC_GIT_IDENTITY`. The generated seat could not launch with the required declared identity.
|
||||
- GREEN: the same repository launcher test emitted `ok - start-agent-session generated environment boundary`.
|
||||
- R7 delete-the-subject: removed only `"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"` from the repository launch array; the same test exited 1 with `FAIL: runtime pane omitted or changed generated environment keys: MOSAIC_GIT_IDENTITY`.
|
||||
- R7 restoration: restored that launch entry; the same test returned green.
|
||||
- Launcher under test is explicitly `packages/mosaic/framework/tools/fleet/start-agent-session.sh` through the test's `$START`, **not** the stale installed host copy.
|
||||
|
||||
### Situational and focused tests
|
||||
|
||||
- Repository launcher boundary: green, including set comparison of all nine generated projection entries and fail-before-tmux cases for missing, unsafe, mismatched, and local-shadow Git identity.
|
||||
- Fleet systemd launcher integration: `bash packages/mosaic/framework/systemd/user/test-fleet-units.sh` — green.
|
||||
- Focused Mosaic Vitest set: 6 files, 311 tests — green.
|
||||
- `bash -n` on changed shell files — green.
|
||||
- `git diff --check` — green.
|
||||
|
||||
### Baseline gates
|
||||
|
||||
- `pnpm typecheck` — 45/45 tasks green.
|
||||
- `pnpm lint` — 25/25 tasks green.
|
||||
- `pnpm format:check` — green.
|
||||
- `pnpm test:checkout` — green.
|
||||
- Repository-wide Vitest under a hermetic current-version npm prefix: Mosaic 81/81 files and 1510/1510 tests green; other workspace test tasks shown green before the framework-shell phase.
|
||||
- Canonical `pnpm test` is not fully green on this host for unrelated environment-sensitive gates:
|
||||
1. the first two runs exposed the globally installed Mosaic 0.0.48 update banner in three CLI smoke tests expecting empty stderr;
|
||||
2. after isolating that global-version input, the framework wake assertion aborted at the known `#973` Bash `BASH_LINENO` convention check (exit 97; observed `[3 5]`, expected `[3 4]`).
|
||||
No tests were weakened or bypassed; focused changed-surface tests are green. CI remains the canonical clean-environment result and is intentionally not polled after push per charter.
|
||||
|
||||
### Independent review
|
||||
|
||||
- Codex code review first pass: request changes for missing shell rejection-path coverage.
|
||||
- Remediation: added table-driven missing/unsafe/mismatch/local-shadow launcher cases, each asserting no tmux call.
|
||||
- Codex code re-review: **approve**, no findings, confidence 0.88.
|
||||
- Codex security review: risk `none`, no findings, confidence 0.97.
|
||||
|
||||
### Acceptance criteria mapping
|
||||
|
||||
| Acceptance criterion | Evidence |
|
||||
| --- | --- |
|
||||
| AC-FGI-01: launched process receives every generated key/value | Repository launcher process-environment `comm -23` set comparison; GREEN and R7 RED evidence above |
|
||||
| AC-FGI-02: missing, unsafe, or split identity fails before tmux | Table-driven shell cases plus TypeScript generated-boundary tests |
|
||||
| AC-FGI-03: focused/baseline/review evidence recorded | Commands and review outcomes above; host-sensitive full-suite limitations stated explicitly |
|
||||
|
||||
### Documentation checklist
|
||||
|
||||
- PRD updated with #1043 requirements and acceptance criteria.
|
||||
- Fleet launch runbook, generated-env concept, and generated-env reference updated.
|
||||
- No API/OpenAPI, sitemap, user publishing target, deployment, or external docs publication change applies.
|
||||
- `docs/TASKS.md` remains unmodified per its single-writer project contract.
|
||||
|
||||
## Round 2 — PR #1073 review 97 remediation
|
||||
|
||||
### Review blocker
|
||||
|
||||
The launched-process suite was signed-excluded from CI enumeration. Manual GREEN/R7 evidence therefore did not prove a PR workflow could detect regression.
|
||||
|
||||
### RED-first and canonical wiring
|
||||
|
||||
1. Removed the suite's signed exclusion before adding a CI execution path.
|
||||
2. `check-test-enumeration.sh` went RED with exact `UNENUMERATED` output for `test-start-agent-session.sh`: population 49, enumerated 30, excluded 18.
|
||||
3. Added both `framework/tools/fleet/test-start-agent-session.sh` and `framework/systemd/user/test-fleet-units.sh` to `@mosaicstack/mosaic`'s canonical `test:framework-shell` chain.
|
||||
4. The guard returned GREEN: population 49, enumerated 32, excluded 18, surfaces 45. The systemd suite is outside the guard's tools-only population but now has the same explicit canonical execution disposition.
|
||||
|
||||
### Workflow-level R7
|
||||
|
||||
- Deleted only the pane launch entry `"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"`.
|
||||
- Ran the exact `.woodpecker/ci.yml` test-step command, `pnpm test`, with only a temporary PATH-scoped npm shim reporting the checkout's current 0.0.49 version so the unrelated global 0.0.48 banner could not preempt the shell chain.
|
||||
- Result: exit 1 at `@mosaicstack/mosaic#test`, with the enumeration guard GREEN followed by `FAIL: runtime pane omitted or changed generated environment keys: MOSAIC_GIT_IDENTITY`.
|
||||
- Restored the launch entry. The canonical `test:framework-shell` chain then reached both newly wired suites and printed both GREEN markers before the known unrelated #973 host-only `BASH_LINENO` abort.
|
||||
- An actual provider PR workflow on the intentionally broken mutant is **NOT MEASURED**: the one-push constraint forbids pushing a red mutant and then a repaired head. Local execution proves the exact PR workflow command and dependency chain go RED on the subject deletion; CI on the repaired pushed head remains canonical.
|
||||
|
||||
### Workflow population
|
||||
|
||||
- **DEFINED:** 3 workflows (`ci.yml`, `ci-image.yml`, `publish.yml`).
|
||||
- **ELIGIBLE for `pull_request`:** 1/3 (`ci.yml`), based on top-level `when:` clauses.
|
||||
- **REPORTED:** Round-1 exact-head provider read reported 1/1 eligible context (`ci/woodpecker/pr/ci`). Post-remediation-head reported count is **NOT MEASURED** by this seat because CI polling is prohibited; workflow definitions and eligibility did not change.
|
||||
|
||||
### Independent remediation review
|
||||
|
||||
- First Round-2 review identified a CI-image blocker: the newly wired launcher suite used Perl, which the Alpine CI base does not install.
|
||||
- Replaced the suite's three Perl-only fixture mutations with POSIX/BusyBox-compatible `sed -i` substitutions; production behavior and assertions are unchanged.
|
||||
- Codex re-review: **APPROVE**, confidence 0.93, no findings.
|
||||
|
||||
### Vitest denominator reconciliation
|
||||
|
||||
The PR's `311/311` is correct for its explicitly named six-file command at both the original and remediation worktrees:
|
||||
|
||||
- generated environment boundary: 24
|
||||
- fleet documentation: 23
|
||||
- Tess service profile: 6
|
||||
- fleet regen command: 27
|
||||
- fleet agent CRUD command: 22
|
||||
- fleet command: 209
|
||||
- total: **311**
|
||||
|
||||
Review 97 reported 312/312 without naming its six files. That is a different or miscounted population and cannot replace the command-scoped 311 denominator; the PR follow-up will name the exact files and arithmetic.
|
||||
|
||||
## Round 3 — Alpine stale-marker portability
|
||||
|
||||
### Objective and plan
|
||||
|
||||
- Replace the GNU-only relative-date fixture with a deterministic POSIX/BusyBox timestamp while preserving the required stale-marker assertion.
|
||||
- Re-run the launcher suite in the canonical `ci-base:latest` Alpine image, then run applicable repository gates and independent review.
|
||||
- Update the PR body to name the repeated GNU-host/Alpine-CI portability pattern, run the mandatory queue guard, push once, verify provider attribution, and stop without CI polling.
|
||||
- Working budget: 8K tokens; scope is one fixture line plus delivery evidence. No production behavior changes.
|
||||
|
||||
### RED-first evidence
|
||||
|
||||
Before the fix, the canonical CI image command
|
||||
`docker run --rm -v "$PWD:/work" -w /work git.mosaicstack.dev/mosaicstack/stack/ci-base:latest bash packages/mosaic/framework/tools/fleet/test-start-agent-session.sh`
|
||||
exited 1 at the stale-marker setup with exact BusyBox output
|
||||
`touch: invalid date '10 seconds ago'`. The prior fresh-marker assertions had already executed, matching pipeline 2233's failure location.
|
||||
|
||||
### Root cause and fix
|
||||
|
||||
The test used GNU `touch -d` relative-date parsing although the PR workflow runs on Alpine/BusyBox. The fixture now uses POSIX `touch -t 200001010000.00`, a fixed timestamp that is unconditionally stale; the stale assertion remains mandatory and was not made tolerant of missing timestamp metadata.
|
||||
|
||||
### Structural pattern
|
||||
|
||||
This is the third GNU-host/Alpine-CI portability defect in the lane: GNU `grep` multi-match counting, Perl-only fixture mutation, and GNU `touch -d` date parsing. The repeated cause is shell suites authored on a GNU host but executed in an Alpine CI image; durable prevention belongs in CI-image execution or portability lint, not assertion weakening.
|
||||
|
||||
### GREEN and quality evidence
|
||||
|
||||
- Focused launcher suite in `ci-base:latest`: exit 0, `ok - start-agent-session generated environment boundary`.
|
||||
- Canonical test step in `ci-base:latest` with the pipeline's `pgvector/pgvector:pg17` service, readiness check, migration, and `pnpm test`: exit 0; 46/46 Turbo tasks; Mosaic 81/81 files and 1510/1510 tests; Gateway 57 passed/5 skipped files and 629 passed/11 skipped tests; enumeration 49 population / 32 enumerated / 18 signed exclusions / 45 named surfaces.
|
||||
- The first image-only `pnpm test` attempt lacked the pipeline PostgreSQL service and failed only on connection refusal after the launcher suite was GREEN. The rerun supplied the canonical service precondition and passed.
|
||||
- Canonical-image baseline: typecheck 45/45 tasks, lint 25/25 tasks, format check GREEN; `git diff --check` GREEN.
|
||||
- Independent Codex code review: APPROVE, confidence 0.96, 2/2 Round-3 files, no findings.
|
||||
- Independent Codex security review: risk none, confidence 0.99, 2/2 Round-3 files, no findings.
|
||||
|
||||
### Re-derived inventory and denominators
|
||||
|
||||
- Round-3 git delta: **2/2 files** — launcher suite and task scratchpad; 25 insertions / 1 deletion before evidence finalization.
|
||||
- Full PR path inventory against `origin/main` at `85d2108e`: **19/19 changed paths**; Round 3 adds no new PR path.
|
||||
- Workflow definition population: **1/3 pull-request-eligible** (`ci.yml` of `ci.yml`, `ci-image.yml`, `publish.yml`).
|
||||
- Do not re-litigate the settled 311/312 populations; both are valid for their separately named Tess6 and CRUD-core7 sets.
|
||||
|
||||
## Round 4 — bound stale-marker observation
|
||||
|
||||
### Objective and plan
|
||||
|
||||
- Make the heartbeat assertion discriminate an initially stale native marker from a fresh marker without changing the production staleness threshold or shortening the polling window.
|
||||
- Freeze only the sidecar's numeric observation clock during the stale-fixture arm so elapsed assertion time cannot turn a fresh mutant stale.
|
||||
- Prove two independent mutants RED: disable production stale-marker detection while retaining the stale fixture; replace the stale fixture with a fresh marker. Restore the tree and prove GREEN in the canonical Alpine image.
|
||||
- Re-derive the changed-path inventory, run applicable quality and independent review gates, commit with environment-only author/committer identity, queue-guard, push once, verify provider attribution using curl stdin config, and stop without CI polling.
|
||||
- Working budget: 8K tokens. Scope is the launcher test and its scratchpad evidence; production launcher behavior remains unchanged.
|
||||
|
||||
### Root cause and bounded observation
|
||||
|
||||
The 30 × 0.1-second assertion window overlaps the production `now - marker > interval * 2 + 1` threshold at interval 1. Depending on second boundaries and load, a fresh marker can age past the threshold before the assertion ends. A focused pre-fix fresh-mutant attempt returned RED while Review 101's full-suite run returned GREEN; the differing result is itself timing dependence, not a discriminating assertion.
|
||||
|
||||
The test now supplies a fixed numeric epoch only to the stale-fixture sidecar. Its real marker mtime is still read from the filesystem, but assertion runtime cannot advance `now`. Date formatting still delegates to the image's real `/bin/date`. Neither the production threshold nor the 30 × 0.1-second polling window changed.
|
||||
|
||||
### Two-mutant RED / restored GREEN
|
||||
|
||||
All three runs used `git.mosaicstack.dev/mosaicstack/stack/ci-base:latest`:
|
||||
|
||||
1. **Stale-detection mutant RED:** replaced only the production stale-age predicate with `false` while retaining the fixed stale marker; suite exit 1 with `FAIL: heartbeat sidecar did not resume after native marker became stale or absent`.
|
||||
2. **Fresh-marker mutant RED:** replaced only `touch -t 200001010000.00` with fresh `touch`; suite exit 1 with the same failed stale-resumption assertion. The fixed observation epoch kept the mutant fresh throughout all 30 polls.
|
||||
3. **Restored tree GREEN:** suite exit 0 with `ok - start-agent-session generated environment boundary`.
|
||||
|
||||
### Re-derived inventory
|
||||
|
||||
- Round-4 delta: **2/2 files** — launcher test plus task scratchpad; production launcher delta is empty.
|
||||
- Full PR inventory against `origin/main`: **19/19 paths**; Round 4 adds no path.
|
||||
- Production stale threshold remains `now - marker > iv * 2 + 1`; assertion polling remains 30 × 0.1 seconds.
|
||||
- Review 101's confirmed enumeration/workflow/CI and attribution evidence is accepted without re-polling or re-derivation.
|
||||
|
||||
## Residual risk
|
||||
|
||||
- Landing on `main` does not update the currently installed host launcher. Host framework installation/reseed and Jarvis live-seat validation are separate downstream events.
|
||||
- Canonical CI result is pending and will not be polled by this seat.
|
||||
@@ -112,6 +112,7 @@ EOF
|
||||
chmod 700 "$AGENT_HOME/fleet/agents"
|
||||
cat > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" <<EOF
|
||||
MOSAIC_AGENT_NAME=$AGENT_NAME
|
||||
MOSAIC_GIT_IDENTITY=$AGENT_NAME
|
||||
MOSAIC_AGENT_CLASS=code
|
||||
MOSAIC_AGENT_RUNTIME=pi
|
||||
MOSAIC_AGENT_MODEL=
|
||||
|
||||
@@ -97,7 +97,7 @@ is_sensitive_key() {
|
||||
|
||||
is_generated_key() {
|
||||
case "$1" in
|
||||
MOSAIC_AGENT_NAME|MOSAIC_AGENT_CLASS|MOSAIC_AGENT_RUNTIME|MOSAIC_AGENT_MODEL|MOSAIC_AGENT_REASONING|MOSAIC_AGENT_TOOL_POLICY|MOSAIC_AGENT_WORKDIR|MOSAIC_TMUX_SOCKET) return 0 ;;
|
||||
MOSAIC_AGENT_NAME|MOSAIC_GIT_IDENTITY|MOSAIC_AGENT_CLASS|MOSAIC_AGENT_RUNTIME|MOSAIC_AGENT_MODEL|MOSAIC_AGENT_REASONING|MOSAIC_AGENT_TOOL_POLICY|MOSAIC_AGENT_WORKDIR|MOSAIC_TMUX_SOCKET) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -114,6 +114,7 @@ validate_generated_value() {
|
||||
local value="$2"
|
||||
case "$key" in
|
||||
MOSAIC_AGENT_NAME) safe_agent_name "$value" || fail_env unsafe-agent-name "$key" "$value" ;;
|
||||
MOSAIC_GIT_IDENTITY) safe_agent_name "$value" || fail_env unsafe-git-identity "$key" "$value" ;;
|
||||
MOSAIC_AGENT_CLASS) safe_policy_name "$value" || fail_env unsafe-class "$key" "$value" ;;
|
||||
MOSAIC_AGENT_RUNTIME)
|
||||
case "$value" in claude|codex|opencode|pi) ;; *) fail_env unsupported-runtime "$key" "$value" ;; esac
|
||||
@@ -175,7 +176,7 @@ load_environment_file() {
|
||||
|
||||
load_environment_file "$GENERATED_ENV" generated
|
||||
for required_key in \
|
||||
MOSAIC_AGENT_NAME MOSAIC_AGENT_CLASS MOSAIC_AGENT_RUNTIME MOSAIC_AGENT_MODEL \
|
||||
MOSAIC_AGENT_NAME MOSAIC_GIT_IDENTITY MOSAIC_AGENT_CLASS MOSAIC_AGENT_RUNTIME MOSAIC_AGENT_MODEL \
|
||||
MOSAIC_AGENT_REASONING MOSAIC_AGENT_TOOL_POLICY MOSAIC_AGENT_WORKDIR MOSAIC_TMUX_SOCKET; do
|
||||
[ -n "${GENERATED_VALUES[$required_key]+set}" ] || fail_env missing-key "$required_key" ''
|
||||
done
|
||||
@@ -183,12 +184,15 @@ load_environment_file "$LOCAL_ENV" local
|
||||
|
||||
[ "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}" = "$AGENT_NAME" ] || \
|
||||
fail_env agent-name-mismatch MOSAIC_AGENT_NAME "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}"
|
||||
[ "${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}" = "$AGENT_NAME" ] || \
|
||||
fail_env git-identity-mismatch MOSAIC_GIT_IDENTITY "${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}"
|
||||
|
||||
MOSAIC_TMUX_SOCKET=${GENERATED_VALUES[MOSAIC_TMUX_SOCKET]}
|
||||
MOSAIC_AGENT_RUNTIME=${GENERATED_VALUES[MOSAIC_AGENT_RUNTIME]}
|
||||
MOSAIC_AGENT_MODEL=${GENERATED_VALUES[MOSAIC_AGENT_MODEL]}
|
||||
MOSAIC_AGENT_REASONING=${GENERATED_VALUES[MOSAIC_AGENT_REASONING]}
|
||||
MOSAIC_AGENT_WORKDIR=${GENERATED_VALUES[MOSAIC_AGENT_WORKDIR]}
|
||||
MOSAIC_GIT_IDENTITY=${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}
|
||||
MOSAIC_AGENT_CLASS=${GENERATED_VALUES[MOSAIC_AGENT_CLASS]}
|
||||
MOSAIC_AGENT_TOOL_POLICY=${GENERATED_VALUES[MOSAIC_AGENT_TOOL_POLICY]}
|
||||
MOSAIC_RUNTIME_BIN=${LOCAL_VALUES[MOSAIC_RUNTIME_BIN]:-}
|
||||
@@ -343,6 +347,7 @@ LAUNCH_ENV=(
|
||||
"PATH=$PANE_PATH"
|
||||
"MOSAIC_HOME=$MOSAIC_HOME"
|
||||
"MOSAIC_AGENT_NAME=$AGENT_NAME"
|
||||
"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"
|
||||
"MOSAIC_AGENT_CLASS=$MOSAIC_AGENT_CLASS"
|
||||
"MOSAIC_AGENT_RUNTIME=$MOSAIC_AGENT_RUNTIME"
|
||||
"MOSAIC_AGENT_MODEL=$MOSAIC_AGENT_MODEL"
|
||||
|
||||
@@ -62,6 +62,19 @@ env -0 > "${MOSAIC_HOME:?}/fleet/pane-environment"
|
||||
SHIM
|
||||
chmod +x "$FAKE_BIN/mosaic"
|
||||
|
||||
# Freeze numeric epoch reads only when a test arm supplies an observation bound.
|
||||
# Formatting reads still use the real BusyBox/POSIX date implementation.
|
||||
cat > "$FAKE_BIN/date" <<'SHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ -n "${MOSAIC_TEST_FIXED_EPOCH:-}" ] && [ "${1:-}" = '+%s' ]; then
|
||||
printf '%s\n' "$MOSAIC_TEST_FIXED_EPOCH"
|
||||
exit 0
|
||||
fi
|
||||
exec /bin/date "$@"
|
||||
SHIM
|
||||
chmod +x "$FAKE_BIN/date"
|
||||
|
||||
write_generated() {
|
||||
local home="$1"
|
||||
local agent="$2"
|
||||
@@ -71,6 +84,7 @@ write_generated() {
|
||||
chmod 600 "$home/fleet/run/holder-owner"
|
||||
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
||||
MOSAIC_AGENT_NAME=$agent
|
||||
MOSAIC_GIT_IDENTITY=$agent
|
||||
MOSAIC_AGENT_CLASS=code
|
||||
MOSAIC_AGENT_RUNTIME=pi
|
||||
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
|
||||
@@ -88,6 +102,7 @@ run_start() {
|
||||
local agent="$2"
|
||||
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
||||
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
|
||||
MOSAIC_TEST_FIXED_EPOCH="${MOSAIC_TEST_FIXED_EPOCH:-}" \
|
||||
MOSAIC_TEST_HOME="$home" \
|
||||
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
||||
MOSAIC_HOME="$home" "$START" "$agent"
|
||||
@@ -114,6 +129,42 @@ fi
|
||||
echo "$valid_args" | grep -qxF '/usr/bin/env' || fail "pane does not use absolute env"
|
||||
echo "$valid_args" | grep -qxF -- '-i' || fail "pane environment is not cleared"
|
||||
|
||||
# Git identity is generated authority, not an optional or independently mutable
|
||||
# local value. Each invalid form must fail before fake tmux receives a call.
|
||||
assert_git_identity_rejected() {
|
||||
local case_name="$1"
|
||||
local expected_code="$2"
|
||||
local home="$ROOT/git-identity-$case_name"
|
||||
local agent="coder-git-identity-$case_name"
|
||||
local generated="$home/fleet/agents/$agent.env.generated"
|
||||
write_generated "$home" "$agent"
|
||||
|
||||
case "$case_name" in
|
||||
missing) grep -v '^MOSAIC_GIT_IDENTITY=' "$generated" > "$generated.next" && mv "$generated.next" "$generated" ;;
|
||||
unsafe) sed -i 's|^MOSAIC_GIT_IDENTITY=.*$|MOSAIC_GIT_IDENTITY=bad/identity|' "$generated" ;;
|
||||
mismatch) sed -i 's|^MOSAIC_GIT_IDENTITY=.*$|MOSAIC_GIT_IDENTITY=other-agent|' "$generated" ;;
|
||||
local-shadow)
|
||||
printf 'MOSAIC_GIT_IDENTITY=%s\n' "$agent" > "$home/fleet/agents/$agent.env.local"
|
||||
chmod 600 "$home/fleet/agents/$agent.env.local"
|
||||
;;
|
||||
*) fail "unknown Git identity rejection case: $case_name" ;;
|
||||
esac
|
||||
chmod 600 "$generated"
|
||||
|
||||
: > "$TMUX_CALLS"
|
||||
if output=$(run_start "$home" "$agent" 2>&1); then
|
||||
fail "Git identity case $case_name was accepted"
|
||||
fi
|
||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before Git identity $case_name rejection"
|
||||
echo "$output" | grep -qF "code=$expected_code" || \
|
||||
fail "Git identity $case_name diagnostic omitted code $expected_code"
|
||||
}
|
||||
|
||||
assert_git_identity_rejected missing missing-key
|
||||
assert_git_identity_rejected unsafe unsafe-git-identity
|
||||
assert_git_identity_rejected mismatch git-identity-mismatch
|
||||
assert_git_identity_rejected local-shadow generated-key-shadow
|
||||
|
||||
# The generated-file parent is a security boundary too: even a private regular
|
||||
# file is untrusted if its parent can be replaced or written by another user.
|
||||
# Validation must happen before fake tmux receives even a has-session call.
|
||||
@@ -274,6 +325,18 @@ after_pane_env=$(printf '%s\n' "$pane_args" | grep -n -m1 -F '/usr/bin/env' | cu
|
||||
printf '%s\n' "$pane_args" | tail -n +"$after_pane_env" | grep -qxF -- '-i' || \
|
||||
fail "pane command did not clear its environment"
|
||||
pane_environment=$(tr '\0' '\n' < "$HOME_PANE_BOUNDARY/fleet/pane-environment")
|
||||
# Exercise the repository launcher at $START, not the independently installed
|
||||
# host copy. Set-compare every declared generated projection entry with the
|
||||
# launched process environment so a newly declared identity cannot be omitted
|
||||
# by a hand-maintained per-variable assertion.
|
||||
declared_generated_environment=$(sort "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.generated")
|
||||
missing_or_changed_generated_environment=$(comm -23 \
|
||||
<(printf '%s\n' "$declared_generated_environment") \
|
||||
<(printf '%s\n' "$pane_environment" | sort))
|
||||
if [ -n "$missing_or_changed_generated_environment" ]; then
|
||||
missing_or_changed_keys=$(printf '%s\n' "$missing_or_changed_generated_environment" | cut -d= -f1 | paste -sd, -)
|
||||
fail "runtime pane omitted or changed generated environment keys: $missing_or_changed_keys"
|
||||
fi
|
||||
echo "$pane_environment" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
|
||||
fail "runtime pane did not receive trusted HOME"
|
||||
echo "$pane_environment" | grep -qF "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
|
||||
@@ -290,6 +353,7 @@ write_interaction_generated() {
|
||||
chmod 600 "$home/fleet/run/holder-owner"
|
||||
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
||||
MOSAIC_AGENT_NAME=$agent
|
||||
MOSAIC_GIT_IDENTITY=$agent
|
||||
MOSAIC_AGENT_CLASS=operator-interaction
|
||||
MOSAIC_AGENT_RUNTIME=pi
|
||||
MOSAIC_AGENT_MODEL=openai/gpt-5.6-sol
|
||||
@@ -352,8 +416,12 @@ write_generated "$HOME_NATIVE_STALE" "coder-native-stale"
|
||||
write_heartbeat_local "$HOME_NATIVE_STALE" "coder-native-stale"
|
||||
STALE_HB="$HOME_NATIVE_STALE/run/coder-native-stale.hb"
|
||||
printf 'ts=native\npid=1\nstatus=busy\nmodel=stale-model\n' > "$STALE_HB"
|
||||
touch -d '10 seconds ago' "$STALE_HB.native"
|
||||
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_STALE" coder-native-stale
|
||||
touch -t 200001010000.00 "$STALE_HB.native"
|
||||
# Hold the sidecar's observation epoch constant: assertion runtime must not age
|
||||
# a fresh-marker mutant into the stale state that this fixture must distinguish.
|
||||
STALE_OBSERVATION_EPOCH=$(date +%s)
|
||||
MOSAIC_TEST_FIXED_EPOCH="$STALE_OBSERVATION_EPOCH" \
|
||||
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_STALE" coder-native-stale
|
||||
wait_for_sidecar_status "$STALE_HB"
|
||||
|
||||
HOME_NATIVE_ABSENT="$ROOT/native-absent"
|
||||
@@ -381,7 +449,7 @@ echo "$output" | grep -qF 'code=unknown-key' || fail "interaction did not use sh
|
||||
: > "$TMUX_CALLS"
|
||||
HOME_INTERACTION_POLICY="$ROOT/interaction-policy"
|
||||
write_interaction_generated "$HOME_INTERACTION_POLICY" "interaction-policy"
|
||||
perl -0pi -e 's/MOSAIC_AGENT_RUNTIME=pi/MOSAIC_AGENT_RUNTIME=codex/' \
|
||||
sed -i 's|^MOSAIC_AGENT_RUNTIME=pi$|MOSAIC_AGENT_RUNTIME=codex|' \
|
||||
"$HOME_INTERACTION_POLICY/fleet/agents/interaction-policy.env.generated"
|
||||
if output=$(run_interaction "$HOME_INTERACTION_POLICY" interaction-policy 2>&1); then
|
||||
fail "interaction wrapper accepted a policy-incompatible projection"
|
||||
|
||||
@@ -32,7 +32,6 @@ packages/mosaic/framework/tools/tmux/test-send-message-socket.sh | requires a re
|
||||
packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh | requires real tmux-pane fixtures on a throwaway socket; CI image ships no tmux; #1017 burndown (same condition as its sibling)
|
||||
|
||||
# --- single-suite directories: unmeasured in CI ---
|
||||
packages/mosaic/framework/tools/fleet/test-start-agent-session.sh | unmeasured in CI image; stubs tmux via a fake bin dir, likely CI-fit; #1017 burndown
|
||||
packages/mosaic/framework/tools/glpi/test-list-http-status.sh | unmeasured in CI image; stub-based (#807 regression harness), likely CI-fit; #1017 burndown
|
||||
packages/mosaic/framework/tools/orchestrator/test-board-roll.sh | unmeasured in CI image; file-fixture based, likely CI-fit; #1017 burndown
|
||||
packages/mosaic/framework/tools/woodpecker/test-ci-wait-exit-matrix.sh | unmeasured in CI image; drives ci-wait.sh against a stub pipeline-status.sh, likely CI-fit; #1017 burndown
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
|
||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
|
||||
@@ -131,13 +131,14 @@ async function exists(path: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
describe('projectRosterV2AgentGeneratedEnv', (): void => {
|
||||
it('maps a roster-v2 agent to exactly the eight generated projection keys', (): void => {
|
||||
it('maps a roster-v2 agent to exactly the nine generated projection keys', (): void => {
|
||||
const roster = parseRosterV2(rosterYaml, 'yaml');
|
||||
const agent = roster.agents.find((candidate) => candidate.name === 'coder0');
|
||||
expect(agent).toBeDefined();
|
||||
const values = projectRosterV2AgentGeneratedEnv(roster, agent!);
|
||||
expect(values).toEqual({
|
||||
MOSAIC_AGENT_NAME: 'coder0',
|
||||
MOSAIC_GIT_IDENTITY: 'coder0',
|
||||
MOSAIC_AGENT_CLASS: 'code',
|
||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||
MOSAIC_AGENT_MODEL: 'gpt-5.6-sol',
|
||||
|
||||
@@ -422,6 +422,7 @@ describe('fleet roster parsing', () => {
|
||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
||||
[
|
||||
'MOSAIC_AGENT_NAME=coder0',
|
||||
'MOSAIC_GIT_IDENTITY=coder0',
|
||||
// Reflects the roster's canonicalized compatibility class (A3a).
|
||||
'MOSAIC_AGENT_CLASS=code',
|
||||
'MOSAIC_AGENT_RUNTIME=codex',
|
||||
@@ -3799,6 +3800,7 @@ describe('fleet add command', () => {
|
||||
'utf8',
|
||||
);
|
||||
expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0');
|
||||
expect(envContent).toContain('MOSAIC_GIT_IDENTITY=coder0');
|
||||
expect(envContent).toContain('MOSAIC_AGENT_RUNTIME=codex');
|
||||
});
|
||||
|
||||
|
||||
@@ -484,6 +484,7 @@ function generateAgentEnvValues(
|
||||
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
||||
return {
|
||||
MOSAIC_AGENT_NAME: agent.name,
|
||||
MOSAIC_GIT_IDENTITY: agent.name,
|
||||
MOSAIC_AGENT_CLASS: agent.className,
|
||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||
MOSAIC_AGENT_MODEL: agent.modelHint ?? '',
|
||||
|
||||
@@ -358,6 +358,7 @@ function generatedValues(
|
||||
): Readonly<Record<string, string>> {
|
||||
return {
|
||||
MOSAIC_AGENT_NAME: agent.name,
|
||||
MOSAIC_GIT_IDENTITY: agent.name,
|
||||
MOSAIC_AGENT_CLASS: agent.className,
|
||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||
MOSAIC_AGENT_MODEL: agent.model,
|
||||
|
||||
@@ -380,7 +380,7 @@ const COMMAND_RECORDS: Readonly<Record<string, RegExp>> = {
|
||||
|
||||
const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
||||
'DATA.DOTENV.FLEET_LAUNCH':
|
||||
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
||||
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_GIT_IDENTITY=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
||||
'DATA.TEXT_TABLE.FLEET_TASKS':
|
||||
'| W-FLEET | in-progress | Fleet (agent-session execution layer) | Phase 2/5 | docs/fleet/TASKS.md | observability dogfooded on live stub fleet; control plane rides federation (W1) |',
|
||||
'DATA.TEXT_DIAGRAM.BACKLOG_FLOW':
|
||||
@@ -406,7 +406,7 @@ const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
||||
'DATA.JSON.MUTATION_RESULT':
|
||||
'{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}',
|
||||
'DATA.DOTENV.GENERATED_ENV':
|
||||
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
||||
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_GIT_IDENTITY=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
||||
'DATA.YAML.ROSTER_FIELDS':
|
||||
'version: 2\ngeneration: 1\ntransport: tmux\ntmux:\n socket_name: mosaic-fleet\n holder_session: _holder\ndefaults:\n working_directory: ~/src\n runtime: pi\nruntimes:\n pi:\n reset_command: /new\nagents:\n - name: coder0\n alias: Coder 0\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',
|
||||
};
|
||||
@@ -922,8 +922,8 @@ describe('fleet operator documentation', (): void => {
|
||||
);
|
||||
expect(
|
||||
surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'),
|
||||
).toHaveLength(858);
|
||||
expect(surfaces).toHaveLength(882);
|
||||
).toHaveLength(863);
|
||||
expect(surfaces).toHaveLength(887);
|
||||
|
||||
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
||||
const auxiliary: CodeSurface = {
|
||||
|
||||
@@ -597,6 +597,7 @@ export function projectRosterV2AgentGeneratedEnv(
|
||||
): Readonly<Record<string, string>> {
|
||||
return {
|
||||
MOSAIC_AGENT_NAME: agent.name,
|
||||
MOSAIC_GIT_IDENTITY: agent.name,
|
||||
MOSAIC_AGENT_CLASS: agent.className,
|
||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||
MOSAIC_AGENT_MODEL: agent.model,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
|
||||
const generatedValues = {
|
||||
MOSAIC_AGENT_NAME: 'coder0',
|
||||
MOSAIC_GIT_IDENTITY: 'coder0',
|
||||
MOSAIC_AGENT_CLASS: 'code',
|
||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
|
||||
@@ -45,6 +46,7 @@ describe('generated fleet agent environment boundary', (): void => {
|
||||
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
|
||||
[
|
||||
'MOSAIC_AGENT_NAME=coder0',
|
||||
'MOSAIC_GIT_IDENTITY=coder0',
|
||||
'MOSAIC_AGENT_CLASS=code',
|
||||
'MOSAIC_AGENT_RUNTIME=pi',
|
||||
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
|
||||
@@ -78,6 +80,22 @@ describe('generated fleet agent environment boundary', (): void => {
|
||||
expect(String(error)).toMatch(/key=.*sha256=/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['unsafe-git-identity', 'other/identity'],
|
||||
['git-identity-mismatch', 'reviewer0'],
|
||||
])('rejects %s before any launch consumer can use it', (code: string, identity: string): void => {
|
||||
expect((): void => {
|
||||
renderGeneratedAgentEnvironment({
|
||||
...generatedValues,
|
||||
MOSAIC_GIT_IDENTITY: identity,
|
||||
});
|
||||
}).toThrow(
|
||||
expect.objectContaining({
|
||||
diagnostic: expect.objectContaining({ code, key: 'MOSAIC_GIT_IDENTITY' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
|
||||
expect((): void => {
|
||||
renderGeneratedAgentEnvironment({
|
||||
|
||||
@@ -73,6 +73,7 @@ export class AgentEnvBoundaryError extends Error {
|
||||
|
||||
export const GENERATED_AGENT_ENV_KEYS = [
|
||||
'MOSAIC_AGENT_NAME',
|
||||
'MOSAIC_GIT_IDENTITY',
|
||||
'MOSAIC_AGENT_CLASS',
|
||||
'MOSAIC_AGENT_RUNTIME',
|
||||
'MOSAIC_AGENT_MODEL',
|
||||
@@ -402,6 +403,7 @@ function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
|
||||
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
|
||||
}
|
||||
const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME');
|
||||
const gitIdentity = requiredGeneratedValue(values, 'MOSAIC_GIT_IDENTITY');
|
||||
const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS');
|
||||
const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME');
|
||||
const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL');
|
||||
@@ -412,6 +414,12 @@ function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
|
||||
|
||||
if (!AGENT_NAME.test(name))
|
||||
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', name);
|
||||
if (!AGENT_NAME.test(gitIdentity)) {
|
||||
throw new AgentEnvBoundaryError('unsafe-git-identity', 'MOSAIC_GIT_IDENTITY', gitIdentity);
|
||||
}
|
||||
if (gitIdentity !== name) {
|
||||
throw new AgentEnvBoundaryError('git-identity-mismatch', 'MOSAIC_GIT_IDENTITY', gitIdentity);
|
||||
}
|
||||
if (!POLICY_NAME.test(className)) {
|
||||
throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className);
|
||||
}
|
||||
|
||||
@@ -1405,6 +1405,7 @@ function generatedValues(
|
||||
): Readonly<Record<string, string>> {
|
||||
return {
|
||||
MOSAIC_AGENT_NAME: agent.name,
|
||||
MOSAIC_GIT_IDENTITY: agent.name,
|
||||
MOSAIC_AGENT_CLASS: agent.className,
|
||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||
MOSAIC_AGENT_MODEL: agent.model,
|
||||
|
||||
Reference in New Issue
Block a user