Compare commits
4
Commits
main
..
f214613680
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f214613680 | ||
|
|
2bf610c9ba | ||
|
|
549b6fbaae | ||
|
|
5d342f77af |
@@ -60,14 +60,6 @@ The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md
|
|||||||
|
|
||||||
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
|
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
|
||||||
|
|
||||||
Mosaic also loads its Pi extensions from `~/.config/mosaic/runtime/pi/`. Inside Pi,
|
|
||||||
`/goal set <statement>` starts a bounded persistent loop that checks every turn and successful
|
|
||||||
compaction, requires two evidence-bearing completion reports, and can be inspected or stopped with
|
|
||||||
`/goal status`, `/goal pause`, `/goal resume`, and `/goal cancel`. Controller-owned goal-state
|
|
||||||
entries redact common credential shapes, but Pi's model/tool-call history is separate, so goals and
|
|
||||||
evidence must never contain secrets or raw sensitive output. Mosaic does not install this extension
|
|
||||||
into `~/.pi/agent/extensions/`.
|
|
||||||
|
|
||||||
### TUI & Gateway
|
### TUI & Gateway
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
-184
@@ -102,128 +102,6 @@ Context compaction, session replacement, and same-PID runtime reloads can leave
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pi Persistent Goal Loop (#1150)
|
|
||||||
|
|
||||||
### Problem and objective
|
|
||||||
|
|
||||||
A Pi agent can stop after a plausible-looking answer even when the operator's broader objective is
|
|
||||||
not complete, and ordinary compaction can weaken or omit the original objective. Mosaic needs an
|
|
||||||
optional, operator-controlled goal loop that keeps a Pi session oriented, checks progress at native
|
|
||||||
lifecycle boundaries, and resumes work until completion is verified or a bounded safety state is
|
|
||||||
reached.
|
|
||||||
|
|
||||||
The objective is a Mosaic-owned Pi extension deployed from the framework into
|
|
||||||
`~/.config/mosaic/runtime/pi/`. It must not install into or depend on `~/.pi/agent/extensions/`.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
#### In scope
|
|
||||||
|
|
||||||
1. `PGL-REQ-01`: The framework SHALL ship a dedicated Pi goal extension under
|
|
||||||
`packages/mosaic/framework/runtime/pi/`, seed it under `$MOSAIC_HOME/runtime/pi/`, and make
|
|
||||||
`mosaic pi` load it alongside the core Mosaic extension when present.
|
|
||||||
2. `PGL-REQ-02`: `/goal` SHALL support setting a goal plus status, pause, resume, cancel, and help
|
|
||||||
operations without silently replacing an active goal.
|
|
||||||
3. `PGL-REQ-03`: Active branch-specific goal state SHALL be persisted in Pi custom session entries,
|
|
||||||
restored on session start and tree navigation, and never rely on a compaction summary as its
|
|
||||||
source of truth.
|
|
||||||
4. `PGL-REQ-04`: A hidden goal contract SHALL be injected through Pi's `context` event before every
|
|
||||||
model request so it remains effective across tool turns, retries, and post-compaction requests.
|
|
||||||
5. `PGL-REQ-05`: The harness SHALL inspect every `turn_end` and successful `session_compact` event.
|
|
||||||
A structured terminating goal-report tool SHALL capture `continue`, evidence-bearing `achieved`,
|
|
||||||
or `blocked` status without requiring a redundant model turn.
|
|
||||||
6. `PGL-REQ-06`: An achievement claim SHALL remain provisional until a second consecutive
|
|
||||||
evidence-bearing verification report. Any continuation report or successful compaction during
|
|
||||||
verification SHALL reset the verification sequence.
|
|
||||||
7. `PGL-REQ-07`: Continuation SHALL be initiated at safe lifecycle boundaries, primarily
|
|
||||||
`agent_settled`; manual compaction and restored active sessions may schedule a deferred idle
|
|
||||||
continuation without re-entering compaction handlers.
|
|
||||||
8. `PGL-REQ-08`: The loop SHALL have operator cancellation plus bounded turn and repeated-no-progress
|
|
||||||
limits. Exhausted or blocked goals pause rather than continuing indefinitely.
|
|
||||||
9. `PGL-REQ-09`: Framework installation and update SHALL preserve normal manifest ownership: the
|
|
||||||
goal extension is framework-owned under `runtime/**`, while no goal extension or configuration
|
|
||||||
asset is created or modified under the operator's main Pi configuration. Pi remains the owner of
|
|
||||||
its native session files used by `appendEntry()`.
|
|
||||||
|
|
||||||
#### Out of scope
|
|
||||||
|
|
||||||
1. A mathematical guarantee that an arbitrary natural-language goal is semantically complete.
|
|
||||||
2. Automatically executing user-supplied shell predicates or accepting executable validation code in
|
|
||||||
`/goal` arguments.
|
|
||||||
3. Restarting Pi after process, host, or supervisor failure; the existing Mosaic fleet/runtime
|
|
||||||
supervisor owns process durability.
|
|
||||||
4. Gateway, database, web UI, Discord, or cross-harness goal orchestration in this slice.
|
|
||||||
|
|
||||||
### User and stakeholder requirements
|
|
||||||
|
|
||||||
- An operator can start a goal from Pi and see its current phase, evidence, limits, and latest report.
|
|
||||||
- The agent remains oriented after each turn and compaction until verified, paused, blocked,
|
|
||||||
exhausted, or cancelled.
|
|
||||||
- Local testing uses a file under `~/.config/mosaic/runtime/pi/`; the feature never writes an
|
|
||||||
extension asset to `~/.pi/agent/extensions/`.
|
|
||||||
- Framework updates deploy the same reviewed extension source through Mosaic's existing manifest
|
|
||||||
sync path.
|
|
||||||
|
|
||||||
### Non-functional requirements
|
|
||||||
|
|
||||||
1. **Safety:** bounded continuation, explicit cancellation, no arbitrary command execution, and no
|
|
||||||
completion without non-empty reported evidence.
|
|
||||||
2. **Reliability:** serialized continuation scheduling, branch-aware restoration, compaction-safe
|
|
||||||
context injection, and stale-timer cancellation on session shutdown.
|
|
||||||
3. **Performance:** no extra nested judge-model request on every turn; structured reporting uses the
|
|
||||||
active agent's final terminating tool call.
|
|
||||||
4. **Observability:** Pi status/notifications expose phase and bounded counters without recording
|
|
||||||
credentials or hidden model reasoning.
|
|
||||||
5. **Maintainability:** the state machine is deterministic and behavior-tested independently from Pi
|
|
||||||
provider/network access.
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. `AC-PGL-01`: A framework-sync fixture installs the extension at
|
|
||||||
`$MOSAIC_HOME/runtime/pi/goal-extension.ts`, and launcher tests prove both Mosaic Pi extensions are
|
|
||||||
emitted in deterministic order while absent optional files remain backward-compatible.
|
|
||||||
2. `AC-PGL-02`: Command tests prove set/status/pause/resume/cancel behavior, active-goal replacement
|
|
||||||
refusal, and bounded input handling.
|
|
||||||
3. `AC-PGL-03`: Lifecycle tests prove every turn is recorded, active context is injected on every
|
|
||||||
request, two evidence-bearing achievement reports are required, and `agent_settled` continues an
|
|
||||||
unmet goal without duplicate scheduling.
|
|
||||||
4. `AC-PGL-04`: Compaction and restoration tests prove goal state survives, verification is reset and
|
|
||||||
rechecked after compaction, manual compaction continuation is deferred until idle, and tree/session
|
|
||||||
branch state is reconstructed correctly.
|
|
||||||
5. `AC-PGL-05`: Limit tests prove max-turn and repeated-no-progress exhaustion stop autonomous
|
|
||||||
continuation, while pause/cancel/blocked states do not restart.
|
|
||||||
6. `AC-PGL-06`: Focused tests, package typecheck/lint/test, repository quality gates, a local Pi load
|
|
||||||
smoke test from `~/.config/mosaic/runtime/pi/`, independent review, and terminal-green CI pass before
|
|
||||||
issue #1150 closes.
|
|
||||||
|
|
||||||
### Constraints, risks, and assumptions
|
|
||||||
|
|
||||||
- Dependency: Pi's extension API must continue to provide `registerCommand`, `registerTool`,
|
|
||||||
`context`, `turn_end`, `agent_settled`, `session_compact`, session custom entries, and terminating
|
|
||||||
tool results.
|
|
||||||
- Risk: the working agent can overstate completion. Mitigation: structured evidence, a mandatory
|
|
||||||
second verification pass, explicit semantic limitations, and operator-visible reports.
|
|
||||||
- Risk: an impossible goal can consume unbounded resources. Mitigation: hard turn/no-progress bounds
|
|
||||||
and paused terminal states.
|
|
||||||
- Risk: automatic continuation can race compaction or session replacement. Mitigation: drive from
|
|
||||||
`agent_settled`, defer idle restarts, generation-check timers, and clear timers on shutdown.
|
|
||||||
- `ASSUMPTION:` Two consecutive evidence-bearing reports are the initial local verification policy;
|
|
||||||
rationale: it provides a real recheck without doubling every turn's model cost. Future policy may
|
|
||||||
add independent or deterministic validators.
|
|
||||||
- `ASSUMPTION:` Default limits are 40 turns and 6 repeated no-progress reports, configurable only by
|
|
||||||
bounded Mosaic environment settings; rationale: useful persistence with a finite autonomous budget.
|
|
||||||
- `ASSUMPTION:` Documentation remains canonical in-repo for this slice; no external docs publication
|
|
||||||
is requested.
|
|
||||||
|
|
||||||
### Testing and delivery intent
|
|
||||||
|
|
||||||
Use TDD for the deterministic controller and lifecycle invariants. Test with fake Pi lifecycle
|
|
||||||
objects first, then run a local load/smoke test from the deployed Mosaic path. Deliver source, tests,
|
|
||||||
launcher wiring, framework/runtime documentation, user/developer guides, and sitemap updates in one
|
|
||||||
reviewed squash PR to `main` with terminal-green CI.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
||||||
|
|
||||||
### Problem and objective
|
### Problem and objective
|
||||||
@@ -268,68 +146,6 @@ lands. M0 consists only of these normative requirements, the complete task DAG,
|
|||||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
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.
|
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.
|
|
||||||
|
|
||||||
### Framework shell assertion portability (#1098)
|
|
||||||
|
|
||||||
#### Problem and objective
|
|
||||||
|
|
||||||
The blocking framework-shell chain can report that a pane command omitted `/usr/bin/env -i` even when
|
|
||||||
`-i` matched successfully. A short-circuiting `grep -q` under `set -o pipefail` may close its pipe after
|
|
||||||
the match and cause an upstream producer to exit with SIGPIPE, turning a valid semantic result into a
|
|
||||||
nonzero aggregate pipeline. The objective is to inspect the captured NUL-delimited argv directly and
|
|
||||||
make failures carry the observed records needed for diagnosis.
|
|
||||||
|
|
||||||
#### Normative requirements
|
|
||||||
|
|
||||||
1. `FSP-REQ-01`: The pane-boundary test SHALL validate an adjacent `/usr/bin/env`, `-i` argv pair from
|
|
||||||
the authoritative NUL-delimited tmux capture without a short-circuit pipeline whose upstream status
|
|
||||||
can override a successful match.
|
|
||||||
2. `FSP-REQ-02`: Missing, reversed, or non-adjacent boundary tokens SHALL fail, while valid boundaries
|
|
||||||
SHALL remain valid regardless of trailing argv size, pipe capacity, process scheduling, or host/CI
|
|
||||||
utility implementation.
|
|
||||||
3. `FSP-REQ-03`: A failed boundary check SHALL print stable indexed, shell-escaped observed argv records
|
|
||||||
before exiting nonzero; the fixture SHALL continue to contain generated non-secret launch data only.
|
|
||||||
4. `FSP-REQ-04`: Verification SHALL include RED-first large-payload evidence, negative token-order
|
|
||||||
controls, the complete focused launcher suite, canonical Woodpecker CI, and independent review.
|
|
||||||
|
|
||||||
#### Acceptance criteria
|
|
||||||
|
|
||||||
1. `AC-FSP-01`: A large captured argv with adjacent `/usr/bin/env`, `-i` passes even when the former
|
|
||||||
`grep -q` pipeline returns nonzero from an upstream SIGPIPE.
|
|
||||||
2. `AC-FSP-02`: Missing executable, missing flag, and detached/reversed flag fixtures return nonzero and
|
|
||||||
emit the indexed observed argv.
|
|
||||||
3. `AC-FSP-03`: The focused suite passes on the development host and CI image, and the merged-main
|
|
||||||
Woodpecker pipeline is terminal green before #1098 closes.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Exact Cross-Harness Fleet Communications Contract (#766)
|
## Exact Cross-Harness Fleet Communications Contract (#766)
|
||||||
|
|||||||
@@ -14,13 +14,6 @@
|
|||||||
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
|
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
|
||||||
- [Skill bridge developer guide](guides/dev-guide.md#claude-code-skill-bridge) — path-validation, ownership, clobber-protection, install/update wiring, tests, and Pi/Codex scope notes.
|
- [Skill bridge developer guide](guides/dev-guide.md#claude-code-skill-bridge) — path-validation, ownership, clobber-protection, install/update wiring, tests, and Pi/Codex scope notes.
|
||||||
|
|
||||||
## Pi persistent goals
|
|
||||||
|
|
||||||
- [Persistent goal user guide](guides/user-guide.md#pi-persistent-goals) — `/goal` commands, verification behavior, limits, compaction/resume semantics, and limitations.
|
|
||||||
- [Goal extension developer guide](guides/dev-guide.md#pi-persistent-goal-extension) — framework ownership, launcher ordering, lifecycle design, tests, and local Mosaic-path smoke workflow.
|
|
||||||
- [Goal loop operations](guides/admin-guide.md#pi-goal-loop-operations) — deployment ownership, bounded settings, pause/resume procedures, and supervisor boundary.
|
|
||||||
- [Pi runtime reference](../packages/mosaic/framework/runtime/pi/RUNTIME.md#extensions) — deployed paths, command summary, and bounded environment settings.
|
|
||||||
|
|
||||||
## Fleet configuration management
|
## Fleet configuration management
|
||||||
|
|
||||||
- [Fleet configuration entry point](fleet/README.md) — desired-versus-observed decision tree and complete operator link map.
|
- [Fleet configuration entry point](fleet/README.md) — desired-versus-observed decision tree and complete operator link map.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Generated environment files are rebuildable projections, not an operator-editabl
|
|||||||
## Launch chain
|
## Launch chain
|
||||||
|
|
||||||
| Layer | Responsibility |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. |
|
||||||
@@ -24,7 +24,6 @@ secret-like key names, duplicate keys, comments, quoted/export syntax, and unsaf
|
|||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
MOSAIC_AGENT_NAME=<roster name>
|
MOSAIC_AGENT_NAME=<roster name>
|
||||||
MOSAIC_GIT_IDENTITY=<roster name>
|
|
||||||
MOSAIC_AGENT_CLASS=<roster class>
|
MOSAIC_AGENT_CLASS=<roster class>
|
||||||
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
||||||
MOSAIC_AGENT_MODEL=<roster model hint>
|
MOSAIC_AGENT_MODEL=<roster model hint>
|
||||||
@@ -34,10 +33,8 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
|
|||||||
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
||||||
```
|
```
|
||||||
|
|
||||||
`MOSAIC_GIT_IDENTITY` is not independently configurable: it must equal `MOSAIC_AGENT_NAME`, preventing
|
The generated launch contract supports `claude`, `codex`, `opencode`, and `pi`. mosaic fleet add
|
||||||
split runtime and repository identity authority. The generated launch contract supports `claude`,
|
rejects another runtime before it writes the roster or modifies generated, local, or quarantine state.
|
||||||
`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;
|
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.
|
it has no generated-launch adapter and cannot be added through this path.
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
The launcher consumes validated data, not shell configuration.
|
The launcher consumes validated data, not shell configuration.
|
||||||
|
|
||||||
1. Read and validate the canonical roster.
|
1. Read and validate the canonical roster.
|
||||||
2. Render deterministic <name>.env.generated data from that roster, including `MOSAIC_GIT_IDENTITY` derived exactly from the roster agent name.
|
2. Render deterministic <name>.env.generated data from that roster.
|
||||||
3. Parse optional <name>.env.local through a strict allowlist.
|
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.
|
4. Reject generated-key shadowing, unknown or sensitive-looking keys, unsafe paths/values, duplicates, malformed lines, shell syntax, and command overrides.
|
||||||
5. Reject a Git identity that is unsafe or differs from the generated agent name.
|
5. Derive the runtime command from validated runtime/model/reasoning data.
|
||||||
6. Derive the runtime command from validated runtime/model/reasoning data and pass every generated projection entry through the clean process environment boundary.
|
6. Target only the exact configured tmux socket and roster session after ownership checks.
|
||||||
7. Target only the exact configured tmux socket and roster session after ownership checks.
|
|
||||||
|
|
||||||
## File precedence and ownership
|
## File precedence and ownership
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ values, credential material, or command text.
|
|||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
MOSAIC_AGENT_NAME=<roster name>
|
MOSAIC_AGENT_NAME=<roster name>
|
||||||
MOSAIC_GIT_IDENTITY=<roster name>
|
|
||||||
MOSAIC_AGENT_CLASS=<roster class>
|
MOSAIC_AGENT_CLASS=<roster class>
|
||||||
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
MOSAIC_AGENT_RUNTIME=<roster runtime>
|
||||||
MOSAIC_AGENT_MODEL=<roster model hint>
|
MOSAIC_AGENT_MODEL=<roster model hint>
|
||||||
@@ -45,9 +44,8 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
|
|||||||
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
||||||
```
|
```
|
||||||
|
|
||||||
`MOSAIC_GIT_IDENTITY` is derived from and must equal `MOSAIC_AGENT_NAME`; it is not a separate
|
The generated launch contract supports only `claude`, `codex`, `opencode`, and `pi`. fleet add
|
||||||
operator-controlled identity authority. The generated launch contract supports only `claude`, `codex`,
|
uses that same runtime authority and rejects any other runtime before it writes the roster or changes
|
||||||
`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`
|
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
|
socket remains an observability canary; it has no generated-launch adapter and cannot be added through
|
||||||
this projection path.
|
this projection path.
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
3. [Provider Configuration](#provider-configuration)
|
3. [Provider Configuration](#provider-configuration)
|
||||||
4. [MCP Server Configuration](#mcp-server-configuration)
|
4. [MCP Server Configuration](#mcp-server-configuration)
|
||||||
5. [Environment Variables Reference](#environment-variables-reference)
|
5. [Environment Variables Reference](#environment-variables-reference)
|
||||||
6. [Pi Goal Loop Operations](#pi-goal-loop-operations)
|
6. [Local Fleet Canary](./fleet-local-canary.md)
|
||||||
7. [Local Fleet Canary](./fleet-local-canary.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -265,16 +264,6 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
|
|||||||
| `AGENT_SYSTEM_PROMPT` | — | Platform-level system prompt injected into all sessions |
|
| `AGENT_SYSTEM_PROMPT` | — | Platform-level system prompt injected into all sessions |
|
||||||
| `AGENT_USER_TOOLS` | all tools | Comma-separated allowlist of tools for non-admin users |
|
| `AGENT_USER_TOOLS` | all tools | Comma-separated allowlist of tools for non-admin users |
|
||||||
|
|
||||||
### Mosaic Pi goal loop
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
| ----------------------------- | ------- | -------------------------------------------------------------------- |
|
|
||||||
| `MOSAIC_GOAL_MAX_TURNS` | `40` | Per-goal autonomous turn limit; accepted range `1..500` |
|
|
||||||
| `MOSAIC_GOAL_MAX_NO_PROGRESS` | `6` | Consecutive identical progress-report limit; accepted range `1..100` |
|
|
||||||
|
|
||||||
These variables are consumed by the framework-owned Pi goal extension at goal creation. Invalid or
|
|
||||||
out-of-range values fall back to the defaults; they do not disable the bounds.
|
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
@@ -385,29 +374,3 @@ Session cleanup is scoped to one session identifier and only removes that sessio
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ----------------------- | ----------------------------- | ------------------------------------------ |
|
| ----------------------- | ----------------------------- | ------------------------------------------ |
|
||||||
| `MOSAIC_WORKSPACE_ROOT` | monorepo root (auto-detected) | Root path for mission workspace operations |
|
| `MOSAIC_WORKSPACE_ROOT` | monorepo root (auto-detected) | Root path for mission workspace operations |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pi Goal Loop Operations
|
|
||||||
|
|
||||||
The reviewed runtime asset is deployed at
|
|
||||||
`~/.config/mosaic/runtime/pi/goal-extension.ts` by framework install/update. Do not install another
|
|
||||||
copy under `~/.pi/agent/extensions/`; duplicate registration can create suffixed commands and two
|
|
||||||
competing lifecycle controllers.
|
|
||||||
|
|
||||||
Operational checks:
|
|
||||||
|
|
||||||
1. Run `mosaic pi` and verify `/goal help` is available.
|
|
||||||
2. Use `/goal status` to inspect phase, turn/no-progress limits, compaction checks, and evidence.
|
|
||||||
Reports persist in Pi session data; controller-owned state redacts common credential shapes, but
|
|
||||||
Pi's model/tool-call history is separate. Operators must not place secrets or raw sensitive output
|
|
||||||
in goals, pause reasons, or evidence.
|
|
||||||
3. Use `/goal pause <reason>` before planned maintenance or manual investigation. Pause and cancel
|
|
||||||
abort the current goal-driven run when Pi is busy.
|
|
||||||
4. Use `/goal resume` only after addressing a blocker; counters restart with the configured bounds.
|
|
||||||
5. Use `/goal cancel` before replacing an unfinished goal.
|
|
||||||
|
|
||||||
A blocked or exhausted goal remains stopped and visible; Mosaic does not automatically raise its
|
|
||||||
limits or restart the process. Framework sync owns file deployment, while Pi's native session file
|
|
||||||
owns branch replay. Process/host restart remains the responsibility of the existing runtime or fleet
|
|
||||||
supervisor.
|
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
5. [Adding New MCP Tools](#adding-new-mcp-tools)
|
5. [Adding New MCP Tools](#adding-new-mcp-tools)
|
||||||
6. [Database Schema and Migrations](#database-schema-and-migrations)
|
6. [Database Schema and Migrations](#database-schema-and-migrations)
|
||||||
7. [Claude Code Skill Bridge](#claude-code-skill-bridge)
|
7. [Claude Code Skill Bridge](#claude-code-skill-bridge)
|
||||||
8. [Pi Persistent Goal Extension](#pi-persistent-goal-extension)
|
8. [API Endpoint Reference](#api-endpoint-reference)
|
||||||
9. [API Endpoint Reference](#api-endpoint-reference)
|
9. [Local Fleet Canary](./fleet-local-canary.md)
|
||||||
10. [Local Fleet Canary](./fleet-local-canary.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -386,85 +385,6 @@ M1 intentionally manages Claude Code only. Pi's Mosaic launcher can discover the
|
|||||||
canonical root directly. Codex still relies on the existing full skill-sync
|
canonical root directly. Codex still relies on the existing full skill-sync
|
||||||
linker and needs separate parity analysis before this lifecycle API is extended.
|
linker and needs separate parity analysis before this lifecycle API is extended.
|
||||||
|
|
||||||
## Pi Persistent Goal Extension
|
|
||||||
|
|
||||||
The source of the Mosaic-owned Pi goal controller is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
packages/mosaic/framework/runtime/pi/goal-extension.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
The framework manifest classifies `runtime/**` as framework-owned. Both the bash installer and the
|
|
||||||
TypeScript file adapter therefore deploy the same reviewed source to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
$MOSAIC_HOME/runtime/pi/goal-extension.ts
|
|
||||||
# default: ~/.config/mosaic/runtime/pi/goal-extension.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not copy or link this extension into `~/.pi/agent/extensions/`. The launcher function
|
|
||||||
`discoverPiExtensionArgs()` emits the core `mosaic-extension.ts` first and the optional
|
|
||||||
`goal-extension.ts` second, preserving compatibility with an older installed framework that does
|
|
||||||
not have the goal file yet.
|
|
||||||
|
|
||||||
### Lifecycle design
|
|
||||||
|
|
||||||
| Pi API | Goal-controller responsibility |
|
|
||||||
| ------------------------------ | --------------------------------------------------------------------------------- |
|
|
||||||
| `registerCommand('goal')` | Set, inspect, pause, resume, or cancel one branch-specific goal |
|
|
||||||
| `registerTool(...)` | Record a terminating structured progress report with evidence |
|
|
||||||
| `context` | Inject the active goal contract before every provider request |
|
|
||||||
| `turn_end` | Record every turn, reject mixed final reports, and enforce the turn bound |
|
|
||||||
| `agent_settled` | Start one deduplicated continuation only after Pi has no retry/compact/queue work |
|
|
||||||
| `session_compact` | Record the compact check, reset provisional verification, and defer idle work |
|
|
||||||
| `session_start`/`session_tree` | Rebuild state from custom entries on the active branch |
|
|
||||||
| `session_shutdown` | Invalidate deferred callbacks and clear UI state |
|
|
||||||
|
|
||||||
State is appended as `mosaic-goal-state` custom entries, which do not enter model context. The
|
|
||||||
`context` hook creates a fresh hidden `mosaic-goal-context` message for each request instead of
|
|
||||||
trusting compaction summaries. The `mosaic_goal_report` result uses `terminate: true`; when it is the
|
|
||||||
sole final tool call, Pi avoids an unnecessary model response before the controller decides whether
|
|
||||||
to verify, continue, or stop.
|
|
||||||
|
|
||||||
Before state is appended or displayed, the controller applies bounded credential-pattern redaction
|
|
||||||
to the goal statement, report summary/evidence/next step, and stop reason. Fingerprints are computed
|
|
||||||
over redacted report content. Pi session entries are append-only, so a credential-bearing legacy
|
|
||||||
entry cannot honestly be erased by the extension: restoration fails closed, emits a warning, and
|
|
||||||
requires removal of the affected session before setting a new goal. This is defense-in-depth rather
|
|
||||||
than a secret-storage contract, and it does not rewrite Pi's separate model-message/tool-call
|
|
||||||
history. Goal prompts tell the agent not to submit credentials or raw sensitive output, and tests use
|
|
||||||
canaries to prove known forms do not reach new custom entries, status text, context, or tool details
|
|
||||||
while ordinary typed fields such as `token: string` remain intact.
|
|
||||||
|
|
||||||
Completion remains evidence-gated but semantic: two consecutive `achieved` reports are required,
|
|
||||||
and the second run is explicitly a verification pass. This avoids an extra judge-model request after
|
|
||||||
every turn. Deterministic validator commands are intentionally not accepted as `/goal` input in this
|
|
||||||
slice, so never describe this mechanism as proof of arbitrary natural-language completion.
|
|
||||||
|
|
||||||
### Tests and local smoke workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm --filter @mosaicstack/mosaic exec vitest run \
|
|
||||||
src/runtime/pi-goal-extension.spec.ts \
|
|
||||||
src/commands/launch.spec.ts \
|
|
||||||
src/config/file-adapter.test.ts
|
|
||||||
|
|
||||||
bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
For an additive local smoke test without reseeding unrelated live framework files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
install -D -m 0644 \
|
|
||||||
packages/mosaic/framework/runtime/pi/goal-extension.ts \
|
|
||||||
~/.config/mosaic/runtime/pi/goal-extension.ts
|
|
||||||
|
|
||||||
pi --extension ~/.config/mosaic/runtime/pi/goal-extension.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `/goal help`, `/goal set ...`, and `/goal status` in that test session. A released framework
|
|
||||||
sync installs the file, and a released Mosaic CLI loads it automatically through `mosaic pi`.
|
|
||||||
|
|
||||||
## API Endpoint Reference
|
## API Endpoint Reference
|
||||||
|
|
||||||
All endpoints are served by the gateway at `http://localhost:14242` by default.
|
All endpoints are served by the gateway at `http://localhost:14242` by default.
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
4. [Tasks](#tasks)
|
4. [Tasks](#tasks)
|
||||||
5. [Settings](#settings)
|
5. [Settings](#settings)
|
||||||
6. [CLI Usage](#cli-usage)
|
6. [CLI Usage](#cli-usage)
|
||||||
7. [Pi Persistent Goals](#pi-persistent-goals)
|
7. [Sub-package Commands](#sub-package-commands)
|
||||||
8. [Sub-package Commands](#sub-package-commands)
|
8. [Telemetry](#telemetry)
|
||||||
9. [Telemetry](#telemetry)
|
9. [Local Fleet Canary](./fleet-local-canary.md)
|
||||||
10. [Local Fleet Canary](./fleet-local-canary.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -308,57 +307,6 @@ mosaic prdy
|
|||||||
mosaic quality-rails
|
mosaic quality-rails
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pi Persistent Goals
|
|
||||||
|
|
||||||
`mosaic pi` loads a Mosaic-owned goal extension from
|
|
||||||
`~/.config/mosaic/runtime/pi/goal-extension.ts`. It is deliberately not installed in
|
|
||||||
`~/.pi/agent/extensions/`; framework installation and updates manage it with the rest of the Mosaic
|
|
||||||
runtime assets.
|
|
||||||
|
|
||||||
Start Pi, then set a goal:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/goal set Deliver the feature, tests, documentation, and verification evidence
|
|
||||||
# Shorthand:
|
|
||||||
/goal Deliver the feature, tests, documentation, and verification evidence
|
|
||||||
```
|
|
||||||
|
|
||||||
Control and inspect the loop with:
|
|
||||||
|
|
||||||
| Command | Behavior |
|
|
||||||
| ---------------------- | ------------------------------------------------------------------ |
|
|
||||||
| `/goal status` | Show phase, limits, compaction checks, latest report, and evidence |
|
|
||||||
| `/goal pause [reason]` | Stop autonomous continuation while preserving the goal |
|
|
||||||
| `/goal resume` | Resume with fresh turn and no-progress counters |
|
|
||||||
| `/goal cancel` | Cancel the goal and remove its active status |
|
|
||||||
| `/goal help` | Show command help |
|
|
||||||
|
|
||||||
While a goal is active, Mosaic injects its contract before every Pi model request and checks every
|
|
||||||
completed model/tool turn. The agent ends each work cycle with the structured
|
|
||||||
`mosaic_goal_report` tool. `achieved` is provisional until a second consecutive report rechecks the
|
|
||||||
whole goal with evidence. A continuation report or a successful compaction resets provisional
|
|
||||||
verification.
|
|
||||||
|
|
||||||
Goal statements and reports are stored in Pi session data. Mosaic redacts common credential shapes
|
|
||||||
before appending its goal-state entries and before goal tool output or `/goal status`, but
|
|
||||||
pattern-based redaction is not a secret store. Pi's own model-message and tool-call records are
|
|
||||||
outside that redactor. Never put tokens, passwords, private keys, connection strings, or raw
|
|
||||||
sensitive output in a goal or report; cite the command, artifact, and pass/fail result instead.
|
|
||||||
|
|
||||||
The loop stops instead of running forever when it is paused, blocked, cancelled, verified, reaches
|
|
||||||
its turn limit, or repeats the same no-progress report too many times. Defaults are 40 turns and 6
|
|
||||||
repeated no-progress reports. Operators may lower or raise them within enforced bounds before
|
|
||||||
launching Pi:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MOSAIC_GOAL_MAX_TURNS=60 MOSAIC_GOAL_MAX_NO_PROGRESS=8 mosaic pi
|
|
||||||
```
|
|
||||||
|
|
||||||
Goal state is branch-specific Pi session data. It survives compaction and session resume, but Pi's
|
|
||||||
process still must be relaunched or supervised after a process/host failure. This initial verifier
|
|
||||||
checks structured evidence twice; it cannot mathematically prove every arbitrary natural-language
|
|
||||||
goal. Use explicit acceptance criteria and inspect `/goal status` for consequential work.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Claude Code Skill Registration
|
### Claude Code Skill Registration
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
# #1099 pipefail + early-exit sweep
|
|
||||||
|
|
||||||
Baseline: `df4c591ab42aa1ae62c12935fdc0e772684864a0`
|
|
||||||
|
|
||||||
This is a site inventory, not a risk count. `FIXED` means the early-exiting consumer no longer has a piped upstream process whose SIGPIPE can become the result under `pipefail`. `NOT-LOAD-BEARING` means the pipeline status is explicitly discarded. `UNREACHABLE-AND-WHY` describes designed input, not a payload-size safety claim.
|
|
||||||
|
|
||||||
## Tranche 1 — runtime and general scripts
|
|
||||||
|
|
||||||
| Baseline site | Verdict | Construction / reason |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `tools/matrix-presence-harness/run.sh:38` | FIXED | nullglob array selects the first path; no pipeline |
|
|
||||||
| `tools/e2e-install-test.sh:139` | FIXED | capture help completely, then grep via redirection |
|
|
||||||
| `tools/install.sh:312` | FIXED | NUL `mapfile` reads all roots; count != 1 reaches the named malformed-archive diagnostic |
|
|
||||||
| `scripts/analysis/reflect-board-history.sh:76` | FIXED | capture Git history completely, then grep via redirection |
|
|
||||||
| `scripts/analysis/reflect-git-history.sh:67` | FIXED | grep reads from a here-string |
|
|
||||||
| `scripts/analysis/reflect-git-history.sh:69` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/authentik/user-create.sh:72` | FIXED | jq `first(...)` reads the response directly |
|
|
||||||
| `packages/mosaic/framework/tools/git/mutate-push-guard.sh:87` | FIXED | grep `-m1` reads the file directly; downstream `cut` consumes its complete scalar output |
|
|
||||||
| `packages/mosaic/framework/tools/orchestrator/session-resume.sh:94` | FIXED | `mapfile` plus bounded indexed loop replaces `head` pipeline |
|
|
||||||
| `packages/mosaic/framework/tools/prdy/prdy-status.sh:69` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:172` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:173` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:174` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:175` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:176` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:177` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:178` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/qa/typecheck-hook.sh:16` | FIXED | Bash regex extracts the first field without a pipeline |
|
|
||||||
| `packages/mosaic/framework/tools/qa/typecheck-hook.sh:56` | FIXED | grep and bounded sed each read from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/tmux/send-message.sh:113` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/tmux/send-message.sh:124` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/wake/detector.sh:126` | FIXED | one awk reads the manifest directly and exits after the first exact key |
|
|
||||||
| `packages/mosaic/framework/tools/wake/detector.sh:270` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/wake/detector.sh:278` | FIXED | grep reads from a here-string |
|
|
||||||
| `packages/mosaic/framework/tools/wake/digest.sh:647` | FIXED | capture complete locator output, then select first line by parameter expansion |
|
|
||||||
| `packages/mosaic/framework/tools/wake/reconcile.sh:149` | FIXED | one awk reads the manifest directly and exits after the first exact key |
|
|
||||||
|
|
||||||
## Explicit withdrawn / non-load-bearing sites
|
|
||||||
|
|
||||||
| Baseline site | Verdict | Reason |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `tools/install.sh:182` | NOT-LOAD-BEARING | `|| true` explicitly discards lookup status |
|
|
||||||
| `tools/install.sh:356` | UNREACHABLE-AND-WHY | `pnpm pack` writes one matching CLI tarball into a fresh directory immediately before lookup; citation withdrawn in #1099 |
|
|
||||||
| `tools/install.sh:357` | UNREACHABLE-AND-WHY | same fresh-directory invariant for gateway tarball; citation withdrawn in #1099 |
|
|
||||||
| `tools/install.sh:627` | NOT-LOAD-BEARING | `|| true` explicitly discards lookup status |
|
|
||||||
| `scripts/agent/session-start.sh:70` | NOT-LOAD-BEARING | optional scratchpad lookup has `|| true` |
|
|
||||||
| `packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh:58` | NOT-LOAD-BEARING | optional scratchpad lookup has `|| true` |
|
|
||||||
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:25` | UNREACHABLE-AND-WHY | withdrawn in #1099 after designed-input reachability measurement; preserved without re-litigation |
|
|
||||||
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:27` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
|
|
||||||
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:30` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
|
|
||||||
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:32` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
|
|
||||||
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:34` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
|
|
||||||
|
|
||||||
## Tranche 2 — non-wake test harnesses
|
|
||||||
|
|
||||||
All 22 baseline sites below are `FIXED`; the checked-in tranche fixture is passed through the same scanner and asserts all 22 occurrences and 21 normalized identities (the same response-split line occurs twice).
|
|
||||||
|
|
||||||
| Baseline site(s) | Verdict | Construction |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `systemd/user/test-fleet-units.sh:148` | FIXED | capture tmux output, then grep via redirection |
|
|
||||||
| `git/test-issue-comment-readback.sh:283,302` | FIXED | parameter expansion splits status/body without `head` |
|
|
||||||
| `git/test-pr-review-gitea-comment.sh:228` | FIXED | parameter expansion splits status/body |
|
|
||||||
| `git/test-lane-brief-pr-linkage.sh:72` | FIXED | grep reads from a here-string |
|
|
||||||
| `git/test-pr-review-repo-host-override.sh:225-226` | FIXED | grep reads from a here-string |
|
|
||||||
| `orchestrator/smoke-test.sh:67,72` | FIXED | parameter expansion selects first line |
|
|
||||||
| `orchestrator/test-board-roll.sh:99-100` | FIXED | grep reads from a here-string |
|
|
||||||
| `quality/scripts/test-upgrade-durable-snapshot.sh:180` | FIXED | complete sorted output is read with `mapfile`, then indexed |
|
|
||||||
| `quality/scripts/test-upgrade-rollback.sh:339,356` | FIXED | direct `grep -m1` file reads; cleanup captures before testing |
|
|
||||||
| `tmux/test-send-message-socket.sh:37,38,44-46,68,72` | FIXED | capture commands complete before redirected grep assertions |
|
|
||||||
| `tmux/test-send-message-verdict.sh:34` | FIXED | grep reads from a here-string |
|
|
||||||
|
|
||||||
## Tranche 3 — wake validation harnesses
|
|
||||||
|
|
||||||
All 26 baseline occurrences (25 normalized identities; one preimage selector occurs twice) are `FIXED` and mechanically bound through the wake fixture and shared scanner.
|
|
||||||
|
|
||||||
| Baseline site(s) | Verdict | Construction |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `wake/test-wake-digest-quarantine.sh:567` | FIXED | complete match populations are captured, then first line selected by parameter expansion |
|
|
||||||
| `wake/test-wake-preimage.sh:182-183,346-347` | FIXED | jq `first(...)` reads each JSONL file directly |
|
|
||||||
| `wake/validate-973/microtest-wake-assert.sh:153,170-171,176,204-209,233-234,251-252,286-287` | FIXED | scalar assertions use here-strings; diagnostics use non-early sed ranges; source line captured before matching |
|
|
||||||
| `wake/validate-973/validate-973.sh:110,119,180,182,187` | FIXED | scalar assertions use here-strings; diagnostic truncation uses consuming sed ranges |
|
|
||||||
|
|
||||||
The scoped inventory is complete: 26 runtime/general + 22 non-wake tests + 26 wake tests fixed; 11 explicitly withdrawn or non-load-bearing sites retain their documented verdicts.
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
# #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.
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# #1098 — Framework shell portability / red main
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Restore terminal-green `main` by making the `test-start-agent-session.sh` clean-environment assertion semantic and portable without removing either newly enumerated framework-shell suite.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Tracking issue: `mosaicstack/stack#1098`
|
|
||||||
- Branch: `fix/framework-shell-portability`
|
|
||||||
- Base: `origin/main` at `4fa2768962702d53e16e8b67ee6ad52ebcb0910e`
|
|
||||||
- Primary file: `packages/mosaic/framework/tools/fleet/test-start-agent-session.sh`
|
|
||||||
- Requirements source: `docs/PRD.md` § Framework shell assertion portability (#1098)
|
|
||||||
- Out of scope: deployed files under `~/.config/mosaic`, pnpm-store cleanup, checkout deletion, and changes to the launcher’s `/usr/bin/env -i` behavior.
|
|
||||||
|
|
||||||
## Acceptance criteria
|
|
||||||
|
|
||||||
1. The test inspects the captured NUL-delimited tmux argv semantically and accepts an adjacent `/usr/bin/env`, `-i` pair regardless of trailing payload size or pipe scheduling.
|
|
||||||
2. Missing `/usr/bin/env`, missing `-i`, and non-adjacent `-i` remain failures.
|
|
||||||
3. Failure output includes the observed argv records with stable indexes and shell escaping; it exposes no credentials because this fixture supplies only generated non-secret launch data.
|
|
||||||
4. The focused suite passes on the dev host and in the repository CI image; the blocking PR/main pipeline returns terminal green.
|
|
||||||
5. Independent review passes; PR is squash-merged and #1098 is closed only after merged-main CI is terminal green.
|
|
||||||
|
|
||||||
## Budget
|
|
||||||
|
|
||||||
- ASSUMPTION: 30K-token working budget; rationale: one shell-test defect plus full PR/CI lifecycle.
|
|
||||||
- Auto-reduction: focused shell and package gates first; rely on canonical Woodpecker for the full monorepo suite rather than duplicating a dependency install under constrained `/home`.
|
|
||||||
- Disk baseline before clone/build: `/home` 7.1G free (99% used), `/tmp` 2.4G free (92% used).
|
|
||||||
|
|
||||||
## Investigation
|
|
||||||
|
|
||||||
### First-hand CI evidence
|
|
||||||
|
|
||||||
- Public log: `GET https://ci.mosaicstack.dev/api/repos/47/logs/2269/53041`
|
|
||||||
- Decoded 1,436 entries (11 null `data` entries treated as empty log rows), 190,756 bytes.
|
|
||||||
- Failure: `FAIL: pane command did not clear its environment` immediately after the expected pane-PID warning.
|
|
||||||
- BusyBox primitives, complete assertion pipeline, real CI image, stale/current image digests, Turbo cache masking, gateway failure, and heartbeat-sidecar concurrent writing were independently excluded.
|
|
||||||
|
|
||||||
### Root cause
|
|
||||||
|
|
||||||
The assertion ends in:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
printf '%s\n' "$pane_args" | tail -n +"$after_pane_env" | grep -qxF -- '-i'
|
|
||||||
```
|
|
||||||
|
|
||||||
The script has `set -o pipefail`. `grep -q` exits as soon as it finds the valid `-i` record. Upstream `tail`/`printf` can then receive SIGPIPE, making the aggregate pipeline nonzero even though grep returned 0 and the semantic property is true. This depends on payload size, pipe capacity, and scheduling, explaining a local/image pass with a CI failure.
|
|
||||||
|
|
||||||
Discriminating stress control with `/usr/bin/env` followed immediately by `-i`:
|
|
||||||
|
|
||||||
- 8,192-byte trailing payload: `printf=0 tail=0 grep=0`, aggregate 0.
|
|
||||||
- 16,384-byte trailing payload: `printf=0 tail=141 grep=0`, aggregate 141.
|
|
||||||
- 32,768+ bytes: `printf=141 tail=141 grep=0`, aggregate 141.
|
|
||||||
- A full-reading `grep -xF` control remained 0 for every payload.
|
|
||||||
|
|
||||||
This is a third branch omitted by the earlier present-vs-corrupted split: the pair can be present and intact while `pipefail` reports an upstream SIGPIPE.
|
|
||||||
|
|
||||||
## TDD plan
|
|
||||||
|
|
||||||
1. RED: preserve the one-off stress reproducer above and add an automated large-argv semantic regression that fails under the current pipeline implementation.
|
|
||||||
2. GREEN: parse the authoritative NUL-delimited capture into a Bash array and search for an adjacent `/usr/bin/env`, `-i` pair without a short-circuit pipeline.
|
|
||||||
3. Add negative controls for missing, detached, and reversed tokens.
|
|
||||||
4. On failure, print indexed `%q` argv records before returning nonzero.
|
|
||||||
5. Run focused suite, mutation controls, shell syntax/format checks, then repository baseline gates feasible without dependency installation.
|
|
||||||
6. Independent review, queue guard, push, PR, CI, coordinator merge authorization, squash merge, merged-main CI, issue close.
|
|
||||||
|
|
||||||
## Progress
|
|
||||||
|
|
||||||
- [x] Checkout created and based on `origin/main` `4fa27689`.
|
|
||||||
- [x] CI log decoded directly.
|
|
||||||
- [x] Root-cause stress control reproduced semantic match + aggregate pipeline failure.
|
|
||||||
- [x] RED evidence: intact `/usr/bin/env`, `-i` fixture produced component statuses `0/141/0` and aggregate 141 under the former `grep -q` pipeline; full-reading semantic control stayed 0.
|
|
||||||
- [x] GREEN implementation: direct NUL-argv adjacency parser, indexed diagnostics, and full-reading scalar predicates replace all load-bearing early-exit pipelines in this test.
|
|
||||||
- [x] Baseline/situational tests:
|
|
||||||
- focused launcher suite: PASS on GNU host and cached Alpine CI image;
|
|
||||||
- paired `test-fleet-units.sh`: PASS;
|
|
||||||
- enumeration guard: PASS (`population=53`, `enumerated=36`, `excluded=18`), 14/14 mutation needles;
|
|
||||||
- `bash -n`, ShellCheck, `git diff --check`: PASS;
|
|
||||||
- static denominator after change: zero load-bearing `grep -q`/`head`/`-m1` pipeline candidates in `test-start-agent-session.sh`;
|
|
||||||
- delete-the-subject mutation removing production `-i`: RED with 78 indexed argv records, byte count, and explicit boundary failure.
|
|
||||||
- [x] Independent review:
|
|
||||||
- first Codex review: request changes — negative fixtures did not each assert diagnostics;
|
|
||||||
- remediation: centralized predicate + diagnostic wrapper and exercised all four negative fixtures;
|
|
||||||
- second Codex review: APPROVE, 0 blockers/should-fix/suggestions;
|
|
||||||
- Codex security review: risk none, 0 findings.
|
|
||||||
- [ ] PR CI, formal fleet review, merge, merged-main CI, issue closure.
|
|
||||||
|
|
||||||
## Documentation disposition
|
|
||||||
|
|
||||||
- Updated canonical `docs/PRD.md` with FSP requirements and acceptance criteria.
|
|
||||||
- This is an internal test/reliability change with no API, user workflow, deployment, navigation, or publishing-surface change; no user/admin/API/sitemap update is required.
|
|
||||||
- `docs/TASKS.md` remains unchanged because the project contract makes it orchestrator-only.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
- The CI failure did not print its captured argv, so the exact CI payload is unavailable. The stress control proves the assertion is non-portable and can emit the exact false verdict; branch CI is the canonical confirmation that replacing it resolves pipeline 2269’s failure class.
|
|
||||||
- Printing fixture argv is safe only while this test’s projection remains non-secret. The diagnostic must stay scoped to the test capture and shell-escaped.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# #1099 — pipefail + early-exit sweep
|
|
||||||
|
|
||||||
## Scope and decisions
|
|
||||||
|
|
||||||
- Baseline `df4c591ab42aa1ae62c12935fdc0e772684864a0`, after #1100 removed its 35 sites.
|
|
||||||
- Split into review-sized non-closing tranches: runtime/general; tmux/git/quality tests; wake validation/tests.
|
|
||||||
- Do not equate class membership with demonstrated risk. Do not use payload size or pipeline stage count as a safety proxy.
|
|
||||||
- Preserve the issue's withdrawn findings for `qa-hook-stdin.sh` and the two fresh-directory `pnpm pack` lookups. Fix `install.sh:312` because malformed multi-root input must reach its named handler.
|
|
||||||
|
|
||||||
## Tranche 1 TDD
|
|
||||||
|
|
||||||
RED-first control: `node --test scripts/pipefail-early-exit.test.mjs` reported exactly 26 non-accepted runtime/general sites, including `install.sh:312`, and exited 1. A checked-in fixture generated from immutable baseline `df4c591a` records all 26 normalized sites; the control passes every fixture entry through the same scanner, asserts exact identity/count/uniqueness, and separately requires zero findings in the current tree. It also inventories accepted sites rather than silently excluding whole files.
|
|
||||||
|
|
||||||
Construction choices:
|
|
||||||
|
|
||||||
- here-string/file redirection for scalar grep assertions;
|
|
||||||
- full capture then parameter expansion for first-line selection;
|
|
||||||
- arrays/`mapfile` for complete populations;
|
|
||||||
- direct jq/awk/grep selection where one tool can express the property;
|
|
||||||
- no `|| true` added to a load-bearing assertion.
|
|
||||||
|
|
||||||
Site-by-site verdicts: `docs/reports/quality/1099-pipefail-sweep.md`.
|
|
||||||
|
|
||||||
## Tranche 2 TDD
|
|
||||||
|
|
||||||
Expanded the unconditional scanner over 11 non-wake test harnesses. RED named exactly 22 source lines; a second immutable-baseline fixture now asserts those 22 entries through the same scanner. Rewrites preserve command status by capturing producers before redirected assertions, use parameter expansion for line selection, and use complete `mapfile` populations where ordering matters. Current-tree finding count is zero for tranches 1 and 2.
|
|
||||||
|
|
||||||
## Tranche 3 TDD
|
|
||||||
|
|
||||||
Expanded the shared scanner over four wake validation harnesses. RED named 26 occurrences. The wake fixture asserts 26 occurrences / 25 normalized identities through the same scanner; all scalar assertions now use redirection, direct jq selection, complete capture, or consuming diagnostic ranges. Current-tree finding count is zero across the full scoped population.
|
|
||||||
|
|
||||||
## Verification so far
|
|
||||||
|
|
||||||
- `bash -n` on every changed shell script: pass.
|
|
||||||
- structural Node control: pass.
|
|
||||||
- `test-mutate-push-guard.sh`: 8/8 pass.
|
|
||||||
- `test-send-message-verdict.sh`: 3/3 pass.
|
|
||||||
- `test-send-message-socket.sh`: pass.
|
|
||||||
- Independent review 143 found two semantic regressions: a help-probe `|| true` changed the failure truth table, and an unguarded Git capture changed non-Git data-dir behavior from rc 0 + JSON to silent rc 128. Both received RED-first regressions before correction; help status is now separate and required, and Git status remains condition-guarded.
|
|
||||||
- Wake static inventory remains aligned at 261/261 after line-neutral rewrites; no static-set mismatch. Wake detector/reconcile/digest/preimage suites terminate at their existing fail-closed #973 `BASH_LINENO` environment probe (exit 97, observed `[3 5]`, expected `[3 4]`) before subject tests. No bypass or skip was used; canonical CI remains required.
|
|
||||||
- ShellCheck reports only pre-existing source-following, unused-variable, and untouched `ls | head` findings; no new diagnostic was introduced.
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# #1150 — Pi persistent goal extension
|
|
||||||
|
|
||||||
- **Task ID:** ISSUE-1150 (no `docs/TASKS.md` row; that file is orchestrator-only)
|
|
||||||
- **Issue:** #1150 — `pi: add persistent /goal controller extension to Mosaic framework`
|
|
||||||
- **Branch:** `feat/1150-pi-goal-extension`
|
|
||||||
- **Mode:** Delivery
|
|
||||||
- **Status:** in progress
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Build and locally validate a Mosaic-owned Pi `/goal` extension. Source must ship from
|
|
||||||
`packages/mosaic/framework/runtime/pi/`, framework sync must deploy it under
|
|
||||||
`~/.config/mosaic/runtime/pi/`, and no extension/configuration asset may be written into `~/.pi`.
|
|
||||||
Pi's native session manager remains the owner of session entries.
|
|
||||||
|
|
||||||
## Scope and acceptance source
|
|
||||||
|
|
||||||
- Canonical requirements: `docs/PRD.md`, section **Pi Persistent Goal Loop (#1150)**.
|
|
||||||
- User intent: continuous goal orientation and status checking after each Pi turn and compaction,
|
|
||||||
tested locally before framework delivery.
|
|
||||||
- Documentation target: canonical in-repo user/developer/runtime docs; no external publication.
|
|
||||||
|
|
||||||
## Assumptions
|
|
||||||
|
|
||||||
- `ASSUMPTION:` Initial semantic verification uses two consecutive structured, evidence-bearing
|
|
||||||
reports from the working agent rather than a second model request after every turn. This keeps the
|
|
||||||
loop testable and avoids doubling model cost while making the limitation explicit.
|
|
||||||
- `ASSUMPTION:` Default autonomous bounds are 40 turns and 6 repeated no-progress reports, with only
|
|
||||||
bounded numeric environment overrides.
|
|
||||||
- `ASSUMPTION:` A local smoke copy to `~/.config/mosaic/runtime/pi/goal-extension.ts` is authorized by
|
|
||||||
the user's explicit request. Full framework reseed into the live home is not required for the smoke
|
|
||||||
test and would touch unrelated framework-owned files.
|
|
||||||
|
|
||||||
## Budget
|
|
||||||
|
|
||||||
- Working estimate: 30K implementation/review tokens.
|
|
||||||
- Hard user cap: none stated.
|
|
||||||
- Cost control: deterministic fake-Pi tests; no nested evaluator calls; only bounded arithmetic/load
|
|
||||||
smoke workflows against the installed runtime.
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
1. Update PRD and create tracking/scratchpad artifacts.
|
|
||||||
2. Read launcher, installer ownership, Pi extension, and documentation surfaces.
|
|
||||||
3. TDD: add fake-Pi behavior tests for commands, state restoration, turn checks, compaction, limits,
|
|
||||||
verification, and continuation deduplication.
|
|
||||||
4. Implement `runtime/pi/goal-extension.ts` and deterministic launcher discovery.
|
|
||||||
5. Add framework-sync/deployment acceptance coverage.
|
|
||||||
6. Update user, developer, runtime, framework README, and sitemap documentation.
|
|
||||||
7. Run focused tests, local Mosaic-path smoke test, then baseline repository gates.
|
|
||||||
8. Run independent review, remediate, commit, push/PR/CI/merge/issue closure per delivery gates.
|
|
||||||
|
|
||||||
## TDD decision
|
|
||||||
|
|
||||||
Applied. The continuation state machine and lifecycle scheduling are control-path logic where a race
|
|
||||||
or false terminal state can cause unbounded work or premature completion.
|
|
||||||
|
|
||||||
## Progress checkpoints
|
|
||||||
|
|
||||||
- [x] Issue #1150 created through Mosaic wrapper.
|
|
||||||
- [x] Isolated worktree created from `origin/main`.
|
|
||||||
- [x] PRD requirements and acceptance criteria added.
|
|
||||||
- [x] Task scratchpad created.
|
|
||||||
- [x] RED controller and security-regression tests written and observed failing before implementation.
|
|
||||||
- [x] Goal controller, launcher discovery, framework deployment coverage, and bounded state machine
|
|
||||||
implemented.
|
|
||||||
- [x] User, admin, developer, runtime, adapter, README, and sitemap documentation updated.
|
|
||||||
- [x] Final source copied additively to `~/.config/mosaic/runtime/pi/goal-extension.ts`; source and
|
|
||||||
deployed SHA-256 are identical.
|
|
||||||
- [x] Live Pi RPC smoke from the exact Mosaic path reached `achieved` with two verification passes and
|
|
||||||
no extension errors.
|
|
||||||
- [x] Baseline and situational checks completed, except the explicitly documented unavailable
|
|
||||||
PostgreSQL-only root integration case.
|
|
||||||
- [x] Independent code and OWASP/security reviews completed; all findings remediated and re-reviewed.
|
|
||||||
- [ ] Commit, push, PR, terminal-green CI, squash merge, and issue closure complete.
|
|
||||||
|
|
||||||
## Tests and evidence
|
|
||||||
|
|
||||||
### Situational
|
|
||||||
|
|
||||||
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/runtime/pi-goal-extension.spec.ts`
|
|
||||||
- final: 25 passed.
|
|
||||||
- Covers commands, per-turn checks, context injection, two-pass verification, mixed-report
|
|
||||||
rejection, bounded limits, compaction, branch restore, stale timers, credential redaction,
|
|
||||||
typed-field false-positive protection, and append-only legacy-state fail-closed behavior.
|
|
||||||
- Final focused launcher/controller/file-adapter run: 3 files / 67 tests passed.
|
|
||||||
- Final V8 coverage for `framework/runtime/pi/goal-extension.ts`:
|
|
||||||
- 99.17% statements/lines, 93.78% branches, 100% functions.
|
|
||||||
- Installer migration fixture: 24 passed and byte-compared the deployed framework asset.
|
|
||||||
- Standalone extension TypeScript check against installed Pi 0.84.1 types passed:
|
|
||||||
`pnpm --filter @mosaicstack/mosaic exec tsc --noEmit --pretty false --module NodeNext
|
|
||||||
--moduleResolution NodeNext --target ES2022 --skipLibCheck framework/runtime/pi/goal-extension.ts`.
|
|
||||||
- Live deployment/load evidence:
|
|
||||||
- source/deployed SHA-256:
|
|
||||||
`1f0a3806e0948ad5f49684273a7e535e9880c148f7fd16d13ee487fcd601f637`.
|
|
||||||
- `get_commands` identified `/goal` as an extension command sourced from
|
|
||||||
`~/.config/mosaic/runtime/pi/goal-extension.ts`; `/goal help` succeeded; zero extension errors.
|
|
||||||
- live arithmetic goal ended `achieved`, verification `2/2`, with 3 goal reports / 3 agent starts
|
|
||||||
and zero extension errors.
|
|
||||||
- no goal extension exists under `~/.pi` extension paths.
|
|
||||||
|
|
||||||
### Baseline
|
|
||||||
|
|
||||||
- `pnpm build`: passed before the final framework-only redaction remediation; the extension is not a
|
|
||||||
package build input and its final source passed the standalone Pi type check.
|
|
||||||
- `pnpm typecheck`: 45/45 tasks passed.
|
|
||||||
- `pnpm lint`: 25/25 tasks passed.
|
|
||||||
- `pnpm format:check`: passed.
|
|
||||||
- Final Mosaic package components:
|
|
||||||
- Vitest: 82 files / 1,539 tests passed.
|
|
||||||
- full `test:framework-shell` harness passed.
|
|
||||||
- the discovered pre-existing tmux loader-marker race was reproduced with constructor PID
|
|
||||||
evidence, fixed with a pane readiness/FIFO barrier, passed 3 consecutive focused runs, and passed
|
|
||||||
in the full shell harness.
|
|
||||||
- one combined rerun encountered the separate existing real-lease probe TOCTOU in
|
|
||||||
`install-ordering-guard.spec.ts`; an earlier final Vitest run was fully green and the changed
|
|
||||||
focused suites remained green.
|
|
||||||
- Gateway safe baseline excluding the prohibited PostgreSQL-only fixture: 55 files / 600 tests passed
|
|
||||||
(6 files / 12 tests skipped by their existing environment gates).
|
|
||||||
- Root `pnpm test` reached 43 successful workspace tasks and all changed-package Vitest tests, but
|
|
||||||
the unchanged `apps/gateway/src/__tests__/cross-user-isolation.test.ts` afterAll hook retried a
|
|
||||||
PostgreSQL connection and failed authentication (`28P01`). This checkout explicitly forbids local
|
|
||||||
PostgreSQL startup/access; the failure is unrelated to #1150 and cannot be remediated by starting
|
|
||||||
the database. The gateway suite excluding that PostgreSQL-only file and required CI are used as
|
|
||||||
the safe verification paths.
|
|
||||||
|
|
||||||
### Independent review
|
|
||||||
|
|
||||||
- Codex code review: approved, 0 findings across 15 files.
|
|
||||||
- Initial Codex security review: one medium CWE-532/A09 finding for raw report persistence.
|
|
||||||
- Remediation added central credential-pattern redaction, prompt/docs guidance, canary tests, typed
|
|
||||||
field false-positive guards, and sticky fail-closed restore for credential-bearing append-only
|
|
||||||
history.
|
|
||||||
- Codex security re-review: risk `none`, 0 findings, confidence 0.87.
|
|
||||||
- Focused remediation review findings were fixed; final focused re-review verdict: `APPROVE`.
|
|
||||||
- Focused independent review of the tmux readiness barrier: `APPROVE`, no actionable findings.
|
|
||||||
|
|
||||||
## Risks and blockers
|
|
||||||
|
|
||||||
- Live `~/.config/mosaic` is shared by active Pi/fleet processes. Local deployment remained a single
|
|
||||||
additive framework file and did not reload or restart unrelated sessions.
|
|
||||||
- Completion verification is semantic, not mathematical: the active agent supplies structured
|
|
||||||
evidence twice. Operators must still inspect consequential outcomes.
|
|
||||||
- Credential redaction is pattern-based defense-in-depth, not a secret store. It covers
|
|
||||||
controller-owned state/status/tool details, not Pi's separate model-message/tool-call history.
|
|
||||||
Goals and reports must never contain real secrets or raw sensitive output. Because Pi session
|
|
||||||
entries are append-only, a detected credential-bearing legacy branch fails closed and the affected
|
|
||||||
session must be removed.
|
|
||||||
- Current installed Pi is newer than the repository's historical gateway Pi dependency. The
|
|
||||||
extension was checked and smoke-tested against installed Pi 0.84.1 using stable documented APIs.
|
|
||||||
- Local root testing cannot safely execute the unchanged PostgreSQL-only integration fixture under
|
|
||||||
the checkout's explicit database safety constraints. Terminal-green PR CI remains mandatory before
|
|
||||||
merge.
|
|
||||||
- The unchanged real-lease default-probe test can observe different broker availability across its two
|
|
||||||
sequential probes; one combined package rerun hit that existing TOCTOU. The same final Vitest suite
|
|
||||||
passed in a separate run, and CI remains the merge authority.
|
|
||||||
@@ -13,8 +13,7 @@ Pi is the native Mosaic agent runtime. The `mosaic pi` launcher:
|
|||||||
|
|
||||||
1. Injects the full runtime contract via `--append-system-prompt`
|
1. Injects the full runtime contract via `--append-system-prompt`
|
||||||
2. Loads Mosaic skills via `--skill` flags
|
2. Loads Mosaic skills via `--skill` flags
|
||||||
3. Loads framework-owned `mosaic-extension.ts` and `goal-extension.ts` from
|
3. Loads the Mosaic extension via `--extension` for lifecycle hooks
|
||||||
`~/.config/mosaic/runtime/pi/` via ordered `--extension` flags
|
|
||||||
4. Detects active missions and injects initial prompts
|
4. Detects active missions and injects initial prompts
|
||||||
|
|
||||||
## Capabilities vs Other Runtimes
|
## Capabilities vs Other Runtimes
|
||||||
@@ -23,7 +22,6 @@ Pi is the native Mosaic agent runtime. The `mosaic pi` launcher:
|
|||||||
- Native thinking levels replace sequential-thinking MCP
|
- Native thinking levels replace sequential-thinking MCP
|
||||||
- Native skill discovery compatible with Mosaic SKILL.md format
|
- Native skill discovery compatible with Mosaic SKILL.md format
|
||||||
- Native extension system for lifecycle hooks (TypeScript, not bash shims)
|
- Native extension system for lifecycle hooks (TypeScript, not bash shims)
|
||||||
- Bounded persistent `/goal` loop with per-turn, post-compaction, and two-pass evidence checks
|
|
||||||
- Native session persistence and resume
|
- Native session persistence and resume
|
||||||
- Model-agnostic (Anthropic, OpenAI, Google, Ollama, custom providers)
|
- Model-agnostic (Anthropic, OpenAI, Google, Ollama, custom providers)
|
||||||
|
|
||||||
|
|||||||
@@ -94,14 +94,7 @@ The launcher:
|
|||||||
1. Verifies `~/.config/mosaic` exists
|
1. Verifies `~/.config/mosaic` exists
|
||||||
2. Verifies `SOUL.md` exists (auto-runs `mosaic init` if missing)
|
2. Verifies `SOUL.md` exists (auto-runs `mosaic init` if missing)
|
||||||
3. Injects `AGENTS.md` into the runtime
|
3. Injects `AGENTS.md` into the runtime
|
||||||
4. For Pi, loads the framework-owned core and persistent-goal extensions from
|
4. Forwards all arguments to the runtime CLI
|
||||||
`~/.config/mosaic/runtime/pi/`
|
|
||||||
5. Forwards all arguments to the runtime CLI
|
|
||||||
|
|
||||||
Inside `mosaic pi`, `/goal set <statement>` starts a bounded persistent goal loop. Use `/goal status`,
|
|
||||||
`/goal pause`, `/goal resume`, or `/goal cancel` to control it. The extension remains part of Mosaic
|
|
||||||
under `~/.config/mosaic/runtime/pi/goal-extension.ts`; it is not installed in Pi's main extension
|
|
||||||
directory.
|
|
||||||
|
|
||||||
You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtime adapters will tell the agent to read `~/.config/mosaic/AGENTS.md`.
|
You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtime adapters will tell the agent to read `~/.config/mosaic/AGENTS.md`.
|
||||||
|
|
||||||
@@ -121,7 +114,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
|
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
|
||||||
│ ├── codex/ ← instructions.md, RUNTIME.md
|
│ ├── codex/ ← instructions.md, RUNTIME.md
|
||||||
│ ├── opencode/ ← AGENTS.md, RUNTIME.md
|
│ ├── opencode/ ← AGENTS.md, RUNTIME.md
|
||||||
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts, goal-extension.ts
|
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts
|
||||||
│ └── mcp/ ← MCP server configs
|
│ └── mcp/ ← MCP server configs
|
||||||
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
|
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
|
||||||
├── skills-local/ ← Local cross-runtime skills
|
├── skills-local/ ← Local cross-runtime skills
|
||||||
@@ -133,7 +126,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
|
|
||||||
| Launch method | Injection mechanism |
|
| Launch method | Injection mechanism |
|
||||||
| ------------------- | ----------------------------------------------------------------------------------------- |
|
| ------------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + Mosaic extensions |
|
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + extension |
|
||||||
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
|
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
|
||||||
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
|
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
|
||||||
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
|
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
|
||||||
|
|||||||
@@ -51,26 +51,12 @@ Skills are discovered from:
|
|||||||
|
|
||||||
### Extensions
|
### Extensions
|
||||||
|
|
||||||
`mosaic pi` loads framework-owned extensions directly from `~/.config/mosaic/runtime/pi/` in this
|
The Mosaic Pi extension (`~/.config/mosaic/runtime/pi/mosaic-extension.ts`) handles:
|
||||||
order:
|
|
||||||
|
|
||||||
1. `mosaic-extension.ts` — session lifecycle, mission context, memory routing, lease/mutator gates,
|
- Session start/end lifecycle hooks
|
||||||
and fleet heartbeat reporting.
|
- Active mission detection and context injection
|
||||||
2. `goal-extension.ts` — optional persistent `/goal` controller with per-turn and post-compaction
|
- Memory routing to `~/.config/mosaic/memory/`
|
||||||
checks.
|
- MACP queue status reporting
|
||||||
|
|
||||||
The goal extension is deployed by Mosaic and MUST NOT be copied into `~/.pi/agent/extensions/`.
|
|
||||||
Use `/goal set <statement>` (or `/goal <statement>`) to start, then `/goal status`, `/goal pause`,
|
|
||||||
`/goal resume`, or `/goal cancel` to control it. An active goal is injected before every model
|
|
||||||
request, restored from branch-specific session entries, and considered achieved only after two
|
|
||||||
consecutive evidence-bearing reports. Common credential shapes are redacted before controller-owned
|
|
||||||
goal-state entries are persisted or
|
|
||||||
displayed; Pi's own model/tool-call history is separate. Goals and reports must contain references
|
|
||||||
and pass/fail summaries rather than secrets or raw sensitive output.
|
|
||||||
|
|
||||||
- `MOSAIC_GOAL_MAX_TURNS` — autonomous turn limit, default `40`, accepted range `1..500`.
|
|
||||||
- `MOSAIC_GOAL_MAX_NO_PROGRESS` — identical no-progress report limit, default `6`, accepted range
|
|
||||||
`1..100`.
|
|
||||||
|
|
||||||
### Sessions
|
### Sessions
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -66,10 +66,7 @@ if command -v tmux >/dev/null 2>&1 && command -v cc >/dev/null 2>&1; then
|
|||||||
trap 'tmux -L "$TEST_SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TEST_ROOT"' EXIT
|
trap 'tmux -L "$TEST_SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TEST_ROOT"' EXIT
|
||||||
MARKER="$TEST_ROOT/loader-marker"
|
MARKER="$TEST_ROOT/loader-marker"
|
||||||
LIBRARY="$TEST_ROOT/marker.so"
|
LIBRARY="$TEST_ROOT/marker.so"
|
||||||
FIXTURE_READY="$TEST_ROOT/loader-ready"
|
|
||||||
FIXTURE_FIFO="$TEST_ROOT/loader-block"
|
|
||||||
HOLDER_HOME="$TEST_ROOT/holder-home"
|
HOLDER_HOME="$TEST_ROOT/holder-home"
|
||||||
mkfifo "$FIXTURE_FIFO"
|
|
||||||
mkdir -p "$HOLDER_HOME/.config/mosaic/fleet/run"
|
mkdir -p "$HOLDER_HOME/.config/mosaic/fleet/run"
|
||||||
chmod 700 "$HOLDER_HOME/.config" "$HOLDER_HOME/.config/mosaic" \
|
chmod 700 "$HOLDER_HOME/.config" "$HOLDER_HOME/.config/mosaic" \
|
||||||
"$HOLDER_HOME/.config/mosaic/fleet" "$HOLDER_HOME/.config/mosaic/fleet/run"
|
"$HOLDER_HOME/.config/mosaic/fleet" "$HOLDER_HOME/.config/mosaic/fleet/run"
|
||||||
@@ -90,17 +87,7 @@ __attribute__((constructor)) static void mark_loader(void) {
|
|||||||
EOF
|
EOF
|
||||||
cc -shared -fPIC -o "$LIBRARY" "$TEST_ROOT/marker.c"
|
cc -shared -fPIC -o "$LIBRARY" "$TEST_ROOT/marker.c"
|
||||||
MOSAIC_LOADER_MARKER="$MARKER" LD_PRELOAD="$LIBRARY" \
|
MOSAIC_LOADER_MARKER="$MARKER" LD_PRELOAD="$LIBRARY" \
|
||||||
tmux -L "$TEST_SOCKET" new-session -d -s _holder \
|
tmux -L "$TEST_SOCKET" new-session -d -s _holder 'sleep 60'
|
||||||
"touch '$FIXTURE_READY'; read _ < '$FIXTURE_FIFO'"
|
|
||||||
# tmux starts the pane asynchronously. Wait until its contaminated shell has
|
|
||||||
# loaded the constructor and reached a builtin-only FIFO barrier before
|
|
||||||
# clearing the marker; otherwise that expected constructor can race with the
|
|
||||||
# clean holder assertion below and create a false failure.
|
|
||||||
for _attempt in {1..100}; do
|
|
||||||
[ -e "$FIXTURE_READY" ] && break
|
|
||||||
sleep 0.01
|
|
||||||
done
|
|
||||||
[ -e "$FIXTURE_READY" ] || fail "contaminated fixture pane did not become ready"
|
|
||||||
[ -s "$MARKER" ] || fail "contaminated fixture did not execute loader constructor"
|
[ -s "$MARKER" ] || fail "contaminated fixture did not execute loader constructor"
|
||||||
server_pid=$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')
|
server_pid=$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')
|
||||||
: > "$MARKER"
|
: > "$MARKER"
|
||||||
@@ -125,7 +112,6 @@ EOF
|
|||||||
chmod 700 "$AGENT_HOME/fleet/agents"
|
chmod 700 "$AGENT_HOME/fleet/agents"
|
||||||
cat > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" <<EOF
|
cat > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" <<EOF
|
||||||
MOSAIC_AGENT_NAME=$AGENT_NAME
|
MOSAIC_AGENT_NAME=$AGENT_NAME
|
||||||
MOSAIC_GIT_IDENTITY=$AGENT_NAME
|
|
||||||
MOSAIC_AGENT_CLASS=code
|
MOSAIC_AGENT_CLASS=code
|
||||||
MOSAIC_AGENT_RUNTIME=pi
|
MOSAIC_AGENT_RUNTIME=pi
|
||||||
MOSAIC_AGENT_MODEL=
|
MOSAIC_AGENT_MODEL=
|
||||||
@@ -158,8 +144,7 @@ EOF
|
|||||||
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \
|
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \
|
||||||
MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START"
|
MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START"
|
||||||
tmux -L "$TEST_SOCKET" has-session -t '=_holder:0.0' || fail "fresh holder was not created"
|
tmux -L "$TEST_SOCKET" has-session -t '=_holder:0.0' || fail "fresh holder was not created"
|
||||||
ld_preload_env="$(tmux -L "$TEST_SOCKET" show-environment -g LD_PRELOAD 2>/dev/null)" || true
|
if tmux -L "$TEST_SOCKET" show-environment -g LD_PRELOAD 2>/dev/null | grep -q '^LD_PRELOAD='; then
|
||||||
if grep -q '^LD_PRELOAD=' <<<"$ld_preload_env"; then
|
|
||||||
fail "fresh holder retained LD_PRELOAD"
|
fail "fresh holder retained LD_PRELOAD"
|
||||||
fi
|
fi
|
||||||
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ if [[ -n "$GROUP" ]]; then
|
|||||||
group_response=$(curl -sk \
|
group_response=$(curl -sk \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
"${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}")
|
"${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}")
|
||||||
group_pk=$(jq -r "first(.results[] | select(.name == \"$GROUP\") | .pk) // empty" <<<"$group_response")
|
group_pk=$(echo "$group_response" | jq -r ".results[] | select(.name == \"$GROUP\") | .pk" | head -1)
|
||||||
if [[ -n "$group_pk" ]]; then
|
if [[ -n "$group_pk" ]]; then
|
||||||
payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}')
|
payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}')
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ is_sensitive_key() {
|
|||||||
|
|
||||||
is_generated_key() {
|
is_generated_key() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
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 ;;
|
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 ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,6 @@ validate_generated_value() {
|
|||||||
local value="$2"
|
local value="$2"
|
||||||
case "$key" in
|
case "$key" in
|
||||||
MOSAIC_AGENT_NAME) safe_agent_name "$value" || fail_env unsafe-agent-name "$key" "$value" ;;
|
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_CLASS) safe_policy_name "$value" || fail_env unsafe-class "$key" "$value" ;;
|
||||||
MOSAIC_AGENT_RUNTIME)
|
MOSAIC_AGENT_RUNTIME)
|
||||||
case "$value" in claude|codex|opencode|pi) ;; *) fail_env unsupported-runtime "$key" "$value" ;; esac
|
case "$value" in claude|codex|opencode|pi) ;; *) fail_env unsupported-runtime "$key" "$value" ;; esac
|
||||||
@@ -176,7 +175,7 @@ load_environment_file() {
|
|||||||
|
|
||||||
load_environment_file "$GENERATED_ENV" generated
|
load_environment_file "$GENERATED_ENV" generated
|
||||||
for required_key in \
|
for required_key in \
|
||||||
MOSAIC_AGENT_NAME MOSAIC_GIT_IDENTITY MOSAIC_AGENT_CLASS MOSAIC_AGENT_RUNTIME MOSAIC_AGENT_MODEL \
|
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; do
|
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" ''
|
[ -n "${GENERATED_VALUES[$required_key]+set}" ] || fail_env missing-key "$required_key" ''
|
||||||
done
|
done
|
||||||
@@ -184,15 +183,12 @@ load_environment_file "$LOCAL_ENV" local
|
|||||||
|
|
||||||
[ "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}" = "$AGENT_NAME" ] || \
|
[ "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}" = "$AGENT_NAME" ] || \
|
||||||
fail_env agent-name-mismatch MOSAIC_AGENT_NAME "${GENERATED_VALUES[MOSAIC_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_TMUX_SOCKET=${GENERATED_VALUES[MOSAIC_TMUX_SOCKET]}
|
||||||
MOSAIC_AGENT_RUNTIME=${GENERATED_VALUES[MOSAIC_AGENT_RUNTIME]}
|
MOSAIC_AGENT_RUNTIME=${GENERATED_VALUES[MOSAIC_AGENT_RUNTIME]}
|
||||||
MOSAIC_AGENT_MODEL=${GENERATED_VALUES[MOSAIC_AGENT_MODEL]}
|
MOSAIC_AGENT_MODEL=${GENERATED_VALUES[MOSAIC_AGENT_MODEL]}
|
||||||
MOSAIC_AGENT_REASONING=${GENERATED_VALUES[MOSAIC_AGENT_REASONING]}
|
MOSAIC_AGENT_REASONING=${GENERATED_VALUES[MOSAIC_AGENT_REASONING]}
|
||||||
MOSAIC_AGENT_WORKDIR=${GENERATED_VALUES[MOSAIC_AGENT_WORKDIR]}
|
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_CLASS=${GENERATED_VALUES[MOSAIC_AGENT_CLASS]}
|
||||||
MOSAIC_AGENT_TOOL_POLICY=${GENERATED_VALUES[MOSAIC_AGENT_TOOL_POLICY]}
|
MOSAIC_AGENT_TOOL_POLICY=${GENERATED_VALUES[MOSAIC_AGENT_TOOL_POLICY]}
|
||||||
MOSAIC_RUNTIME_BIN=${LOCAL_VALUES[MOSAIC_RUNTIME_BIN]:-}
|
MOSAIC_RUNTIME_BIN=${LOCAL_VALUES[MOSAIC_RUNTIME_BIN]:-}
|
||||||
@@ -347,7 +343,6 @@ LAUNCH_ENV=(
|
|||||||
"PATH=$PANE_PATH"
|
"PATH=$PANE_PATH"
|
||||||
"MOSAIC_HOME=$MOSAIC_HOME"
|
"MOSAIC_HOME=$MOSAIC_HOME"
|
||||||
"MOSAIC_AGENT_NAME=$AGENT_NAME"
|
"MOSAIC_AGENT_NAME=$AGENT_NAME"
|
||||||
"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"
|
|
||||||
"MOSAIC_AGENT_CLASS=$MOSAIC_AGENT_CLASS"
|
"MOSAIC_AGENT_CLASS=$MOSAIC_AGENT_CLASS"
|
||||||
"MOSAIC_AGENT_RUNTIME=$MOSAIC_AGENT_RUNTIME"
|
"MOSAIC_AGENT_RUNTIME=$MOSAIC_AGENT_RUNTIME"
|
||||||
"MOSAIC_AGENT_MODEL=$MOSAIC_AGENT_MODEL"
|
"MOSAIC_AGENT_MODEL=$MOSAIC_AGENT_MODEL"
|
||||||
|
|||||||
@@ -14,82 +14,6 @@ fail() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pane_command_clears_environment() {
|
|
||||||
local calls_file="$1"
|
|
||||||
local -a argv=()
|
|
||||||
local index
|
|
||||||
mapfile -d '' -t argv < "$calls_file"
|
|
||||||
for ((index = 0; index + 1 < ${#argv[@]}; index++)); do
|
|
||||||
if [ "${argv[$index]}" = /usr/bin/env ] && [ "${argv[$((index + 1))]}" = -i ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
print_pane_argv() {
|
|
||||||
local calls_file="$1"
|
|
||||||
local -a argv=()
|
|
||||||
local bytes index
|
|
||||||
mapfile -d '' -t argv < "$calls_file"
|
|
||||||
bytes=$(wc -c < "$calls_file")
|
|
||||||
printf 'observed pane argv: records=%s bytes=%s\n' "${#argv[@]}" "$bytes" >&2
|
|
||||||
for ((index = 0; index < ${#argv[@]}; index++)); do
|
|
||||||
printf ' [%03d] %q\n' "$index" "${argv[$index]}" >&2
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
check_pane_environment_boundary() {
|
|
||||||
local calls_file="$1"
|
|
||||||
if pane_command_clears_environment "$calls_file"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
print_pane_argv "$calls_file"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
contains_literal() {
|
|
||||||
grep -F -- "$2" <<< "$1" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
contains_line() {
|
|
||||||
grep -xF -- "$2" <<< "$1" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Portability regression: inspect the authoritative NUL-delimited argv instead
|
|
||||||
# of piping a newline reconstruction through `grep -q` under pipefail. The old
|
|
||||||
# pipeline could report failure after a successful match when an upstream
|
|
||||||
# producer received SIGPIPE. A large trailing argument keeps that failure class
|
|
||||||
# covered without making stream size part of the semantic contract.
|
|
||||||
PORTABILITY_CALLS="$ROOT/portability-calls"
|
|
||||||
printf -v PORTABILITY_PADDING '%*s' 32768 ''
|
|
||||||
PORTABILITY_PADDING=${PORTABILITY_PADDING// /x}
|
|
||||||
printf '%s\0' /usr/bin/env -i "$PORTABILITY_PADDING" > "$PORTABILITY_CALLS"
|
|
||||||
pane_command_clears_environment "$PORTABILITY_CALLS" || \
|
|
||||||
fail "valid large pane argv was rejected by the environment-boundary assertion"
|
|
||||||
|
|
||||||
assert_pane_boundary_rejected() {
|
|
||||||
local case_name="$1"
|
|
||||||
local expected_records="$2"
|
|
||||||
local diagnostic
|
|
||||||
if diagnostic=$(check_pane_environment_boundary "$PORTABILITY_CALLS" 2>&1); then
|
|
||||||
fail "pane boundary accepted invalid $case_name fixture"
|
|
||||||
fi
|
|
||||||
contains_literal "$diagnostic" "records=$expected_records bytes=" || \
|
|
||||||
fail "pane argv diagnostic omitted counts for $case_name fixture"
|
|
||||||
contains_literal "$diagnostic" '[000]' || \
|
|
||||||
fail "pane argv diagnostic omitted indexed arguments for $case_name fixture"
|
|
||||||
}
|
|
||||||
|
|
||||||
printf '%s\0' tmux -i > "$PORTABILITY_CALLS"
|
|
||||||
assert_pane_boundary_rejected missing-env 2
|
|
||||||
printf '%s\0' /usr/bin/env HOME=/untrusted > "$PORTABILITY_CALLS"
|
|
||||||
assert_pane_boundary_rejected missing-i 2
|
|
||||||
printf '%s\0' /usr/bin/env HOME=/untrusted -i > "$PORTABILITY_CALLS"
|
|
||||||
assert_pane_boundary_rejected non-adjacent-i 3
|
|
||||||
printf '%s\0' -i /usr/bin/env > "$PORTABILITY_CALLS"
|
|
||||||
assert_pane_boundary_rejected reversed-boundary 2
|
|
||||||
|
|
||||||
cat > "$FAKE_BIN/tmux" <<'SHIM'
|
cat > "$FAKE_BIN/tmux" <<'SHIM'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -138,19 +62,6 @@ env -0 > "${MOSAIC_HOME:?}/fleet/pane-environment"
|
|||||||
SHIM
|
SHIM
|
||||||
chmod +x "$FAKE_BIN/mosaic"
|
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() {
|
write_generated() {
|
||||||
local home="$1"
|
local home="$1"
|
||||||
local agent="$2"
|
local agent="$2"
|
||||||
@@ -160,7 +71,6 @@ write_generated() {
|
|||||||
chmod 600 "$home/fleet/run/holder-owner"
|
chmod 600 "$home/fleet/run/holder-owner"
|
||||||
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
||||||
MOSAIC_AGENT_NAME=$agent
|
MOSAIC_AGENT_NAME=$agent
|
||||||
MOSAIC_GIT_IDENTITY=$agent
|
|
||||||
MOSAIC_AGENT_CLASS=code
|
MOSAIC_AGENT_CLASS=code
|
||||||
MOSAIC_AGENT_RUNTIME=pi
|
MOSAIC_AGENT_RUNTIME=pi
|
||||||
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
|
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
|
||||||
@@ -178,7 +88,6 @@ run_start() {
|
|||||||
local agent="$2"
|
local agent="$2"
|
||||||
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
||||||
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
|
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
|
||||||
MOSAIC_TEST_FIXED_EPOCH="${MOSAIC_TEST_FIXED_EPOCH:-}" \
|
|
||||||
MOSAIC_TEST_HOME="$home" \
|
MOSAIC_TEST_HOME="$home" \
|
||||||
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
||||||
MOSAIC_HOME="$home" "$START" "$agent"
|
MOSAIC_HOME="$home" "$START" "$agent"
|
||||||
@@ -191,55 +100,19 @@ AGENT_VALID="coder0"
|
|||||||
write_generated "$HOME_VALID" "$AGENT_VALID"
|
write_generated "$HOME_VALID" "$AGENT_VALID"
|
||||||
run_start "$HOME_VALID" "$AGENT_VALID"
|
run_start "$HOME_VALID" "$AGENT_VALID"
|
||||||
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
contains_literal "$valid_args" new-session || fail "valid generated projection did not reach tmux"
|
echo "$valid_args" | grep -qF new-session || fail "valid generated projection did not reach tmux"
|
||||||
contains_literal "$valid_args" mosaic || fail "fixed mosaic launcher command missing"
|
echo "$valid_args" | grep -qF 'mosaic' || fail "fixed mosaic launcher command missing"
|
||||||
contains_literal "$valid_args" yolo || fail "fixed yolo launcher command missing"
|
echo "$valid_args" | grep -qF 'yolo' || fail "fixed yolo launcher command missing"
|
||||||
contains_literal "$valid_args" pi || fail "roster runtime missing"
|
echo "$valid_args" | grep -qF 'pi' || fail "roster runtime missing"
|
||||||
if contains_literal "$valid_args" 'bash -c'; then
|
if echo "$valid_args" | grep -qF 'bash -c'; then
|
||||||
fail "launcher constructed a shell command payload"
|
fail "launcher constructed a shell command payload"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The pane must start through an absolute clean-environment boundary. Its
|
# The pane must start through an absolute clean-environment boundary. Its
|
||||||
# runtime command remains an argv vector, but no holder/session environment
|
# runtime command remains an argv vector, but no holder/session environment
|
||||||
# control variable can pass through the pane command.
|
# control variable can pass through the pane command.
|
||||||
check_pane_environment_boundary "$TMUX_CALLS" || \
|
echo "$valid_args" | grep -qxF '/usr/bin/env' || fail "pane does not use absolute env"
|
||||||
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
|
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"
|
|
||||||
contains_literal "$output" "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
|
# 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.
|
# file is untrusted if its parent can be replaced or written by another user.
|
||||||
@@ -252,7 +125,7 @@ if output=$(run_start "$HOME_UNSAFE_PARENT" coder-parent 2>&1); then
|
|||||||
fail "generated file under a world-writable parent was accepted"
|
fail "generated file under a world-writable parent was accepted"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before unsafe parent rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before unsafe parent rejection"
|
||||||
contains_literal "$output" 'code=unsafe-permissions' || fail "unsafe parent diagnostic missing"
|
echo "$output" | grep -qF 'code=unsafe-permissions' || fail "unsafe parent diagnostic missing"
|
||||||
|
|
||||||
: > "$TMUX_CALLS"
|
: > "$TMUX_CALLS"
|
||||||
HOME_SYMLINK_PARENT="$ROOT/symlink-parent"
|
HOME_SYMLINK_PARENT="$ROOT/symlink-parent"
|
||||||
@@ -263,7 +136,7 @@ if output=$(run_start "$HOME_SYMLINK_PARENT" coder-symlink-parent 2>&1); then
|
|||||||
fail "generated file under a symlinked parent was accepted"
|
fail "generated file under a symlinked parent was accepted"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before symlinked parent rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before symlinked parent rejection"
|
||||||
contains_literal "$output" 'code=unsafe-directory' || fail "symlinked parent diagnostic missing"
|
echo "$output" | grep -qF 'code=unsafe-directory' || fail "symlinked parent diagnostic missing"
|
||||||
|
|
||||||
# Every managed ancestor is a boundary: MOSAIC_HOME, fleet, and agents. A
|
# Every managed ancestor is a boundary: MOSAIC_HOME, fleet, and agents. A
|
||||||
# symlink or group/world-writable ancestor must fail before environment parsing,
|
# symlink or group/world-writable ancestor must fail before environment parsing,
|
||||||
@@ -301,8 +174,8 @@ assert_managed_ancestor_rejected() {
|
|||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before $hazard $ancestor rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before $hazard $ancestor rejection"
|
||||||
[ ! -e "$home/work" ] || fail "workdir was created before $hazard $ancestor rejection"
|
[ ! -e "$home/work" ] || fail "workdir was created before $hazard $ancestor rejection"
|
||||||
contains_literal "$output" 'code=unsafe-' || fail "managed ancestor diagnostic missing"
|
echo "$output" | grep -qF "code=unsafe-" || fail "managed ancestor diagnostic missing"
|
||||||
if contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND'; then
|
if echo "$output" | grep -qF 'key=MOSAIC_AGENT_COMMAND'; then
|
||||||
fail "environment parsing ran before $hazard $ancestor rejection"
|
fail "environment parsing ran before $hazard $ancestor rejection"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -323,9 +196,9 @@ if output=$(run_start "$HOME_SHADOW" coder1 2>&1); then
|
|||||||
fail "generated-key shadow was accepted"
|
fail "generated-key shadow was accepted"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before generated-key shadow rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before generated-key shadow rejection"
|
||||||
contains_literal "$output" 'key=MOSAIC_AGENT_RUNTIME' || fail "shadow diagnostic omitted key"
|
echo "$output" | grep -qF 'key=MOSAIC_AGENT_RUNTIME' || fail "shadow diagnostic omitted key"
|
||||||
contains_literal "$output" 'sha256=' || fail "shadow diagnostic omitted hash"
|
echo "$output" | grep -qF 'sha256=' || fail "shadow diagnostic omitted hash"
|
||||||
if contains_literal "$output" codex; then
|
if echo "$output" | grep -qF 'codex'; then
|
||||||
fail "shadow diagnostic leaked value"
|
fail "shadow diagnostic leaked value"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -341,9 +214,9 @@ if output=$(run_start "$HOME_COMMAND" coder2 2>&1); then
|
|||||||
fail "arbitrary command override was accepted"
|
fail "arbitrary command override was accepted"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before command rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before command rejection"
|
||||||
contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND' || fail "command diagnostic omitted key"
|
echo "$output" | grep -qF 'key=MOSAIC_AGENT_COMMAND' || fail "command diagnostic omitted key"
|
||||||
contains_literal "$output" 'sha256=' || fail "command diagnostic omitted hash"
|
echo "$output" | grep -qF 'sha256=' || fail "command diagnostic omitted hash"
|
||||||
if contains_literal "$output" "$COMMAND_VALUE"; then
|
if echo "$output" | grep -qF "$COMMAND_VALUE"; then
|
||||||
fail "command diagnostic leaked command value"
|
fail "command diagnostic leaked command value"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -357,7 +230,7 @@ if output=$(run_start "$HOME_PERMS" coder3 2>&1); then
|
|||||||
fail "world-readable local input was accepted"
|
fail "world-readable local input was accepted"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before permissions rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before permissions rejection"
|
||||||
contains_literal "$output" 'code=unsafe-permissions' || fail "permission diagnostic missing"
|
echo "$output" | grep -qF 'code=unsafe-permissions' || fail "permission diagnostic missing"
|
||||||
|
|
||||||
# A unit/holder-like clean bootstrap must yield a pane with trusted HOME and
|
# A unit/holder-like clean bootstrap must yield a pane with trusted HOME and
|
||||||
# computed PATH only. The pane command itself must not carry loader, shell
|
# computed PATH only. The pane command itself must not carry loader, shell
|
||||||
@@ -387,35 +260,25 @@ PATH="$PANE_STALE_PATH" \
|
|||||||
MOSAIC_TEST_EXECUTE_PANE=1 \
|
MOSAIC_TEST_EXECUTE_PANE=1 \
|
||||||
"$START" coder-pane-boundary
|
"$START" coder-pane-boundary
|
||||||
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
contains_line "$pane_args" "HOME=$PANE_TRUSTED_HOME" || \
|
echo "$pane_args" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
|
||||||
fail "pane did not restore trusted HOME"
|
fail "pane did not restore trusted HOME"
|
||||||
contains_literal "$pane_args" "HOME=$PANE_STALE_HOME" && \
|
echo "$pane_args" | grep -qF "HOME=$PANE_STALE_HOME" && \
|
||||||
fail "pane inherited stale HOME"
|
fail "pane inherited stale HOME"
|
||||||
contains_literal "$pane_args" "$PANE_STALE_PATH" && fail "pane inherited stale PATH"
|
echo "$pane_args" | grep -qF "$PANE_STALE_PATH" && fail "pane inherited stale PATH"
|
||||||
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
|
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
|
||||||
contains_literal "$pane_args" "$blocked" && fail "pane inherited $blocked"
|
echo "$pane_args" | grep -qF "$blocked" && fail "pane inherited $blocked"
|
||||||
done
|
done
|
||||||
|
|
||||||
check_pane_environment_boundary "$TMUX_CALLS" || \
|
after_pane_env=$(printf '%s\n' "$pane_args" | grep -n -m1 -F '/usr/bin/env' | cut -d: -f1)
|
||||||
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
|
[ -n "$after_pane_env" ] || fail "pane command did not use absolute env"
|
||||||
|
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")
|
pane_environment=$(tr '\0' '\n' < "$HOME_PANE_BOUNDARY/fleet/pane-environment")
|
||||||
# Exercise the repository launcher at $START, not the independently installed
|
echo "$pane_environment" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
|
||||||
# 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
|
|
||||||
contains_line "$pane_environment" "HOME=$PANE_TRUSTED_HOME" || \
|
|
||||||
fail "runtime pane did not receive trusted HOME"
|
fail "runtime pane did not receive trusted HOME"
|
||||||
contains_literal "$pane_environment" "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
|
echo "$pane_environment" | grep -qF "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
|
||||||
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
|
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
|
||||||
contains_literal "$pane_environment" "$blocked" && fail "runtime pane received $blocked"
|
echo "$pane_environment" | grep -qF "$blocked" && fail "runtime pane received $blocked"
|
||||||
done
|
done
|
||||||
|
|
||||||
write_interaction_generated() {
|
write_interaction_generated() {
|
||||||
@@ -427,7 +290,6 @@ write_interaction_generated() {
|
|||||||
chmod 600 "$home/fleet/run/holder-owner"
|
chmod 600 "$home/fleet/run/holder-owner"
|
||||||
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
|
||||||
MOSAIC_AGENT_NAME=$agent
|
MOSAIC_AGENT_NAME=$agent
|
||||||
MOSAIC_GIT_IDENTITY=$agent
|
|
||||||
MOSAIC_AGENT_CLASS=operator-interaction
|
MOSAIC_AGENT_CLASS=operator-interaction
|
||||||
MOSAIC_AGENT_RUNTIME=pi
|
MOSAIC_AGENT_RUNTIME=pi
|
||||||
MOSAIC_AGENT_MODEL=openai/gpt-5.6-sol
|
MOSAIC_AGENT_MODEL=openai/gpt-5.6-sol
|
||||||
@@ -490,12 +352,8 @@ write_generated "$HOME_NATIVE_STALE" "coder-native-stale"
|
|||||||
write_heartbeat_local "$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"
|
STALE_HB="$HOME_NATIVE_STALE/run/coder-native-stale.hb"
|
||||||
printf 'ts=native\npid=1\nstatus=busy\nmodel=stale-model\n' > "$STALE_HB"
|
printf 'ts=native\npid=1\nstatus=busy\nmodel=stale-model\n' > "$STALE_HB"
|
||||||
touch -t 200001010000.00 "$STALE_HB.native"
|
touch -d '10 seconds ago' "$STALE_HB.native"
|
||||||
# Hold the sidecar's observation epoch constant: assertion runtime must not age
|
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_STALE" coder-native-stale
|
||||||
# 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"
|
wait_for_sidecar_status "$STALE_HB"
|
||||||
|
|
||||||
HOME_NATIVE_ABSENT="$ROOT/native-absent"
|
HOME_NATIVE_ABSENT="$ROOT/native-absent"
|
||||||
@@ -516,22 +374,22 @@ if output=$(run_interaction "$HOME_INTERACTION_MALFORMED" interaction-malformed
|
|||||||
fail "interaction wrapper accepted malformed generated data"
|
fail "interaction wrapper accepted malformed generated data"
|
||||||
fi
|
fi
|
||||||
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before interaction strict-parser rejection"
|
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before interaction strict-parser rejection"
|
||||||
contains_literal "$output" 'code=unknown-key' || fail "interaction did not use shared strict parser first"
|
echo "$output" | grep -qF 'code=unknown-key' || fail "interaction did not use shared strict parser first"
|
||||||
|
|
||||||
# A syntactically valid but policy-incompatible projection reaches the pinned
|
# A syntactically valid but policy-incompatible projection reaches the pinned
|
||||||
# interaction policy check only after strict parsing and never starts tmux.
|
# interaction policy check only after strict parsing and never starts tmux.
|
||||||
: > "$TMUX_CALLS"
|
: > "$TMUX_CALLS"
|
||||||
HOME_INTERACTION_POLICY="$ROOT/interaction-policy"
|
HOME_INTERACTION_POLICY="$ROOT/interaction-policy"
|
||||||
write_interaction_generated "$HOME_INTERACTION_POLICY" "interaction-policy"
|
write_interaction_generated "$HOME_INTERACTION_POLICY" "interaction-policy"
|
||||||
sed -i 's|^MOSAIC_AGENT_RUNTIME=pi$|MOSAIC_AGENT_RUNTIME=codex|' \
|
perl -0pi -e 's/MOSAIC_AGENT_RUNTIME=pi/MOSAIC_AGENT_RUNTIME=codex/' \
|
||||||
"$HOME_INTERACTION_POLICY/fleet/agents/interaction-policy.env.generated"
|
"$HOME_INTERACTION_POLICY/fleet/agents/interaction-policy.env.generated"
|
||||||
if output=$(run_interaction "$HOME_INTERACTION_POLICY" interaction-policy 2>&1); then
|
if output=$(run_interaction "$HOME_INTERACTION_POLICY" interaction-policy 2>&1); then
|
||||||
fail "interaction wrapper accepted a policy-incompatible projection"
|
fail "interaction wrapper accepted a policy-incompatible projection"
|
||||||
fi
|
fi
|
||||||
interaction_policy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
interaction_policy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
contains_literal "$interaction_policy_args" new-session && \
|
echo "$interaction_policy_args" | grep -qF 'new-session' && \
|
||||||
fail "interaction pinned-policy rejection created a tmux session"
|
fail "interaction pinned-policy rejection created a tmux session"
|
||||||
contains_literal "$output" 'operator interaction service requires runtime pi' || \
|
echo "$output" | grep -qF 'operator interaction service requires runtime pi' || \
|
||||||
fail "interaction pinned-policy check did not follow strict parsing"
|
fail "interaction pinned-policy check did not follow strict parsing"
|
||||||
|
|
||||||
# Exact stop derives the socket exclusively from the validated generated
|
# Exact stop derives the socket exclusively from the validated generated
|
||||||
@@ -544,10 +402,10 @@ HOME="$HOME_STOP" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
|||||||
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
||||||
MOSAIC_HOME="$HOME_STOP" MOSAIC_TMUX_SOCKET=ambient-socket "$START" --stop coder-stop
|
MOSAIC_HOME="$HOME_STOP" MOSAIC_TMUX_SOCKET=ambient-socket "$START" --stop coder-stop
|
||||||
stop_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
stop_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
contains_line "$stop_args" mosaic-test || fail "exact stop did not use the validated generated socket"
|
echo "$stop_args" | grep -qxF 'mosaic-test' || fail "exact stop did not use the validated generated socket"
|
||||||
contains_line "$stop_args" kill-session || fail "exact stop did not request session termination"
|
echo "$stop_args" | grep -qxF 'kill-session' || fail "exact stop did not request session termination"
|
||||||
contains_line "$stop_args" '=coder-stop' || fail "exact stop did not exact-match the generated agent name"
|
echo "$stop_args" | grep -qxF '=coder-stop' || fail "exact stop did not exact-match the generated agent name"
|
||||||
if contains_literal "$stop_args" ambient-socket; then
|
if echo "$stop_args" | grep -qF 'ambient-socket'; then
|
||||||
fail "exact stop trusted an ambient socket"
|
fail "exact stop trusted an ambient socket"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ cp "$TARGET" "$BAK"
|
|||||||
export MOSAIC_TEST_WORK_DIR="$WORK/.work"
|
export MOSAIC_TEST_WORK_DIR="$WORK/.work"
|
||||||
|
|
||||||
# --- where the prose lives: usage() { ... EOF ---------------------------------
|
# --- where the prose lives: usage() { ... EOF ---------------------------------
|
||||||
PROSE_LO="$(grep -n -m1 '^usage() {' "$BAK" | cut -d: -f1)"
|
PROSE_LO="$(grep -n '^usage() {' "$BAK" | head -1 | cut -d: -f1)"
|
||||||
PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")"
|
PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")"
|
||||||
if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then
|
if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then
|
||||||
echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2
|
echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2
|
||||||
|
|||||||
@@ -7,40 +7,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}"
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}"
|
||||||
REPO_DIR="$WORK_DIR/repo"
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
BIN_DIR="$WORK_DIR/bin"
|
BIN_DIR="$WORK_DIR/bin"
|
||||||
HOME_DIR="$WORK_DIR/home"
|
|
||||||
LOG_FILE="$WORK_DIR/calls.log"
|
LOG_FILE="$WORK_DIR/calls.log"
|
||||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||||
|
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR"
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||||
|
|
||||||
git -C "$REPO_DIR" init -q
|
git -C "$REPO_DIR" init -q
|
||||||
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
||||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
||||||
#
|
|
||||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
||||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
||||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
||||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
|
||||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credentials below are silently
|
|
||||||
# ignored. This suite is the one where the consequence is not subtle: it FAILS
|
|
||||||
# outright on a provisioned seat (rc=1 bare, rc=0 with $HOME sandboxed, one
|
|
||||||
# variable changed) and passes everywhere else, including CI, which has no
|
|
||||||
# per-agent token to leak.
|
|
||||||
#
|
|
||||||
# CONTAINMENT: the sandboxed HOME in the four run helpers below. It only has to
|
|
||||||
# bound a failure that the pin should already have prevented.
|
|
||||||
#
|
|
||||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
||||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
||||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
||||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
|
||||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
|
||||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
|
||||||
#
|
|
||||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
||||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
||||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
||||||
|
|
||||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||||
{
|
{
|
||||||
@@ -112,7 +86,6 @@ run_in_repo() {
|
|||||||
(
|
(
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
PATH="$BIN_DIR:$PATH" \
|
PATH="$BIN_DIR:$PATH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||||
"$@"
|
"$@"
|
||||||
@@ -310,7 +283,6 @@ run_in_repo2() {
|
|||||||
(
|
(
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
PATH="$BIN_DIR2:$PATH" \
|
PATH="$BIN_DIR2:$PATH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||||
"$@"
|
"$@"
|
||||||
@@ -371,7 +343,7 @@ write_fixture() { printf '%s' "$1" > "$FIXTURE_XDG/tea/config.yml"; }
|
|||||||
token_fallback() {
|
token_fallback() {
|
||||||
(
|
(
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
HOME="$HOME_DIR" XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
||||||
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||||
get_gitea_token_for_login "$1" "$2"
|
get_gitea_token_for_login "$1" "$2"
|
||||||
' _ "$1" "$2"
|
' _ "$1" "$2"
|
||||||
@@ -382,7 +354,7 @@ token_fallback() {
|
|||||||
token_pyyaml() {
|
token_pyyaml() {
|
||||||
(
|
(
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
HOME="$HOME_DIR" XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
||||||
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||||
get_gitea_token_for_login "$1" "$2"
|
get_gitea_token_for_login "$1" "$2"
|
||||||
' _ "$1" "$2"
|
' _ "$1" "$2"
|
||||||
|
|||||||
@@ -61,54 +61,15 @@ STATE_FILE="$WORK_DIR/comments.json"
|
|||||||
# A dedicated scratch dir the wrapper is pointed at via TMPDIR, so the leak
|
# A dedicated scratch dir the wrapper is pointed at via TMPDIR, so the leak
|
||||||
# check can assert every POST/GET body + metadata temp file is cleaned up.
|
# check can assert every POST/GET body + metadata temp file is cleaned up.
|
||||||
TMP_SCRATCH="$WORK_DIR/scratch"
|
TMP_SCRATCH="$WORK_DIR/scratch"
|
||||||
HOME_DIR="$WORK_DIR/home"
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH" "$HOME_DIR"
|
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH"
|
||||||
git -C "$REPO_DIR" init -q
|
git -C "$REPO_DIR" init -q
|
||||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
||||||
#
|
|
||||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
||||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
||||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
||||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
|
||||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credential below is silently
|
|
||||||
# ignored. The stub curl then rejects the unrecognised bearer, and this suite
|
|
||||||
# fails at its FIRST case with `Gitea authenticated-identity read failed with
|
|
||||||
# HTTP 401`. An empty repo-local value shadows the global one and reads back
|
|
||||||
# empty at rc=0. Measured: without this pin the suite is RED on every seat.
|
|
||||||
#
|
|
||||||
# CONTAINMENT: the sandboxed HOME in run_comment(). It only has to bound a
|
|
||||||
# failure that the pin should already have prevented.
|
|
||||||
#
|
|
||||||
# THIS SUITE WAS THE HARDEST OF THE FIVE TO SEE, and the reason is worth stating
|
|
||||||
# because it generalises: run_comment() sends the wrapper's stdout AND stderr to
|
|
||||||
# $OUTPUT_FILE, and the EXIT trap above deletes $WORK_DIR. So the 401 — the only
|
|
||||||
# thing that says what went wrong — exists only inside a directory that is gone
|
|
||||||
# by the time anyone looks. The suite exits 1 with ZERO bytes on stdout and
|
|
||||||
# stderr. A suite that discards or deletes its own evidence turns any post-hoc
|
|
||||||
# assay into a non-measurement: "nothing found" there means "no surviving
|
|
||||||
# trace", never "clean". It was found by intercepting the identity read at its
|
|
||||||
# SOURCE (a PATH shim over `git` logging every `mosaic.gitIdentity` read to a
|
|
||||||
# file outside $WORK_DIR), which is deletion-proof by construction, rather than
|
|
||||||
# by grepping for the symptom.
|
|
||||||
#
|
|
||||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
||||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
||||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
||||||
# by construction and every suite reads clean however vulnerable it is. To
|
|
||||||
# measure, REPLICATE a seat (a decoy HOME whose .gitconfig sets
|
|
||||||
# mosaic.gitIdentity, with no per-slot token) so step 0 reaches its fail-loud
|
|
||||||
# branch — or intercept the read as described above.
|
|
||||||
#
|
|
||||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
||||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
||||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
||||||
|
|
||||||
ISSUE_NUMBER=7
|
ISSUE_NUMBER=7
|
||||||
REPO_SLUG="mosaicstack/stack"
|
REPO_SLUG="mosaicstack/stack"
|
||||||
@@ -319,10 +280,7 @@ print("201")
|
|||||||
print(json.dumps(record))
|
print(json.dumps(record))
|
||||||
PY
|
PY
|
||||||
)
|
)
|
||||||
response_status="${result%%$'\n'*}"
|
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||||
response_body=""
|
|
||||||
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
|
|
||||||
write_response "$response_status" "$response_body"
|
|
||||||
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE"/issues/comments/* ]]; then
|
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE"/issues/comments/* ]]; then
|
||||||
result=$(ISSUE_COMMENT_GET_ID="${path##*/}" python3 - <<'PY'
|
result=$(ISSUE_COMMENT_GET_ID="${path##*/}" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
@@ -341,10 +299,7 @@ else:
|
|||||||
print(json.dumps(match))
|
print(json.dumps(match))
|
||||||
PY
|
PY
|
||||||
)
|
)
|
||||||
response_status="${result%%$'\n'*}"
|
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||||
response_body=""
|
|
||||||
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
|
|
||||||
write_response "$response_status" "$response_body"
|
|
||||||
else
|
else
|
||||||
echo "Unexpected curl request: $method $url" >&2
|
echo "Unexpected curl request: $method $url" >&2
|
||||||
exit 97
|
exit 97
|
||||||
@@ -411,7 +366,6 @@ run_comment() {
|
|||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
PATH="$BIN_DIR:$PATH" \
|
PATH="$BIN_DIR:$PATH" \
|
||||||
TMPDIR="$TMP_SCRATCH" \
|
TMPDIR="$TMP_SCRATCH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
XDG_CONFIG_HOME="$XDG_DIR" \
|
XDG_CONFIG_HOME="$XDG_DIR" \
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \
|
ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \
|
||||||
|
|||||||
@@ -7,38 +7,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-interactive-auth}"
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-interactive-auth}"
|
||||||
REPO_DIR="$WORK_DIR/repo"
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
BIN_DIR="$WORK_DIR/bin"
|
BIN_DIR="$WORK_DIR/bin"
|
||||||
HOME_DIR="$WORK_DIR/home"
|
|
||||||
LOG_FILE="$WORK_DIR/calls.log"
|
LOG_FILE="$WORK_DIR/calls.log"
|
||||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||||
|
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR"
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||||
git -C "$REPO_DIR" init -q
|
git -C "$REPO_DIR" init -q
|
||||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
||||||
#
|
|
||||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
||||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
||||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
||||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
|
||||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credential below is silently
|
|
||||||
# ignored and the suite runs against a production credential. An empty repo-local
|
|
||||||
# value shadows the global one and reads back empty at rc=0. Measured: this suite
|
|
||||||
# resolves a per-slot token without it.
|
|
||||||
#
|
|
||||||
# CONTAINMENT: the sandboxed HOME in run_wrapper(). It only has to bound a failure
|
|
||||||
# that the pin should already have prevented.
|
|
||||||
#
|
|
||||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
||||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
||||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
||||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
|
||||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
|
||||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
|
||||||
#
|
|
||||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
||||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
||||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
||||||
|
|
||||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||||
{"gitea":{"mosaicstack":{"url":"https://git.mosaicstack.dev","token":"test-token"}}}
|
{"gitea":{"mosaicstack":{"url":"https://git.mosaicstack.dev","token":"test-token"}}}
|
||||||
@@ -75,7 +50,6 @@ run_wrapper() {
|
|||||||
(
|
(
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
PATH="$BIN_DIR:$PATH" \
|
PATH="$BIN_DIR:$PATH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ section_nums() { # $1 = output $2 = header-prefix
|
|||||||
}
|
}
|
||||||
|
|
||||||
fail() { echo "FAIL: $1" >&2; exit 1; }
|
fail() { echo "FAIL: $1" >&2; exit 1; }
|
||||||
contains() { grep -qx "$2" <<<"$1"; }
|
contains() { printf '%s\n' "$1" | grep -qx "$2"; }
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fixed (current) script behavior
|
# Fixed (current) script behavior
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ WORK_ROOT="${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
|
|||||||
SANDBOX="$WORK_ROOT/pr-merge-empty-uid-test-$$"
|
SANDBOX="$WORK_ROOT/pr-merge-empty-uid-test-$$"
|
||||||
MOCK_BIN="$SANDBOX/bin"
|
MOCK_BIN="$SANDBOX/bin"
|
||||||
REPO_DIR="$SANDBOX/repo"
|
REPO_DIR="$SANDBOX/repo"
|
||||||
HOME_DIR="$SANDBOX/home"
|
|
||||||
LOG_FILE="$SANDBOX/mock.log"
|
LOG_FILE="$SANDBOX/mock.log"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
@@ -16,7 +15,7 @@ cleanup() {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
mkdir -p "$MOCK_BIN" "$REPO_DIR" "$HOME_DIR"
|
mkdir -p "$MOCK_BIN" "$REPO_DIR"
|
||||||
: > "$LOG_FILE"
|
: > "$LOG_FILE"
|
||||||
|
|
||||||
cat > "$MOCK_BIN/tea" <<'EOF'
|
cat > "$MOCK_BIN/tea" <<'EOF'
|
||||||
@@ -110,48 +109,7 @@ chmod +x "$MOCK_BIN/curl"
|
|||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
git init -q
|
git init -q
|
||||||
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
||||||
#
|
|
||||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
||||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
||||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
||||||
# repo. Step 0 runs BEFORE the credential loader AND before the GITEA_TOKEN env
|
|
||||||
# check, so the `GITEA_TOKEN=redacted-test-token` exported below is silently
|
|
||||||
# overridden and a REAL per-slot token from $HOME is what flows through the
|
|
||||||
# wrapper. Measured on a provisioned seat before this pin: all 5 mock-curl calls
|
|
||||||
# carried the real per-slot token in argv and the fixture token was never used at
|
|
||||||
# ALL. Three consequences specific to this suite:
|
|
||||||
# 1. pr-merge.sh passes the token as `-H "Authorization: token $token"` and the
|
|
||||||
# mock curl logs full argv, so the real credential is written to $LOG_FILE
|
|
||||||
# on disk — transiently: the suite truncates that file between phases and
|
|
||||||
# the EXIT trap removes $SANDBOX, so it leaves NO post-hoc trace. That is
|
|
||||||
# why this suite was the hardest of the three to detect; observing it needs
|
|
||||||
# an instrument that captures argv while the run is live.
|
|
||||||
# 2. Every failure path dumps $OUTPUT/$LOG_FILE to stderr through
|
|
||||||
# `sed 's/redacted-test-token/***REDACTED***/g'` — a redaction pattern that
|
|
||||||
# is the literal fixture string and therefore CANNOT match the token
|
|
||||||
# actually in use.
|
|
||||||
# 3. The leak assertion at "Token leaked to pr-merge.sh output" greps for that
|
|
||||||
# same fixture string, so on a provisioned seat it passes vacuously: it is
|
|
||||||
# searching for a value the run never used.
|
|
||||||
# An empty repo-local value shadows the global one and reads back empty at rc=0.
|
|
||||||
#
|
|
||||||
# CONTAINMENT: the sandboxed HOME exported below. It only has to bound a failure
|
|
||||||
# that the pin should already have prevented.
|
|
||||||
#
|
|
||||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
||||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
||||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
||||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
|
||||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
|
||||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
|
||||||
#
|
|
||||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
||||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
||||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
||||||
|
|
||||||
# $SANDBOX/$HOME_DIR were derived from the real $HOME above, before this export.
|
|
||||||
export HOME="$HOME_DIR"
|
|
||||||
export PATH="$MOCK_BIN:$PATH"
|
export PATH="$MOCK_BIN:$PATH"
|
||||||
export PR_MERGE_TEST_LOG="$LOG_FILE"
|
export PR_MERGE_TEST_LOG="$LOG_FILE"
|
||||||
export GITEA_LOGIN="git.mosaicstack.dev"
|
export GITEA_LOGIN="git.mosaicstack.dev"
|
||||||
|
|||||||
@@ -8,68 +8,12 @@ WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-metadata-gitea}"
|
|||||||
REPO_DIR="$WORK_DIR/repo"
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
FIXTURE_DIR="$WORK_DIR/fixtures"
|
FIXTURE_DIR="$WORK_DIR/fixtures"
|
||||||
STUB_DIR="$WORK_DIR/stubs"
|
STUB_DIR="$WORK_DIR/stubs"
|
||||||
HOME_DIR="$WORK_DIR/home"
|
|
||||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
|
||||||
|
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
mkdir -p "$REPO_DIR" "$FIXTURE_DIR" "$STUB_DIR" "$HOME_DIR"
|
mkdir -p "$REPO_DIR" "$FIXTURE_DIR" "$STUB_DIR"
|
||||||
|
|
||||||
git -C "$REPO_DIR" init -q
|
git -C "$REPO_DIR" init -q
|
||||||
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
||||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
||||||
#
|
|
||||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
||||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
||||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
||||||
# repo. Step 0 runs BEFORE the credential loader AND before the GITEA_TOKEN env
|
|
||||||
# check, so the `GITEA_TOKEN="stub-token"` set in the run helpers below is
|
|
||||||
# silently overridden and a REAL per-slot token from $HOME is what reaches curl.
|
|
||||||
# Measured on a provisioned seat before this pin: both stub-curl calls carried
|
|
||||||
# the real token in argv. An empty repo-local value shadows the global one and
|
|
||||||
# reads back empty at rc=0.
|
|
||||||
#
|
|
||||||
# CONTAINMENT: the sandboxed HOME in the three run helpers below. It only has to
|
|
||||||
# bound a failure that the pin should already have prevented.
|
|
||||||
#
|
|
||||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
||||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
||||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
||||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
|
||||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
|
||||||
# per-slot token) so step 0 reaches its fail-loud branch. See
|
|
||||||
# test-gitea-token-identity.sh for the stronger `env -i HOME=…` form used where a
|
|
||||||
# suite's whole subject IS identity resolution.
|
|
||||||
#
|
|
||||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
||||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
||||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
||||||
|
|
||||||
# The pin above removes step 0, but this suite has a SECOND, independent
|
|
||||||
# dependency on operator state, and closing only the first would leave the suite
|
|
||||||
# red on any hermetic environment. The `GITEA_TOKEN="stub-token"` /
|
|
||||||
# `GITEA_URL="https://git.example.test"` pair the run helpers set is INERT: step 2
|
|
||||||
# of get_gitea_token accepts GITEA_TOKEN only when GITEA_URL matches the remote
|
|
||||||
# host, and this repo's origin is git.uscllc.com, so that pair can never satisfy
|
|
||||||
# it. Before this fixture the only credential that could reach the authenticated
|
|
||||||
# curl branch was a REAL one — from step 0 on an agent seat, or from step 1
|
|
||||||
# reading the operator's own ~/.config/mosaic/credentials.json. That is why the
|
|
||||||
# "curl success path" case passed: not because the stub credential worked, but
|
|
||||||
# because a production credential was available.
|
|
||||||
#
|
|
||||||
# A fixture is used rather than relying on the sandboxed HOME making step 1 find
|
|
||||||
# nothing: a test that passes because production configuration is ABSENT fails
|
|
||||||
# the moment it is present. Step 1 now resolves deterministically to a value that
|
|
||||||
# is a fixture on every machine.
|
|
||||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
|
||||||
{
|
|
||||||
"gitea": {
|
|
||||||
"usc": {
|
|
||||||
"url": "https://git.uscllc.com",
|
|
||||||
"token": "stub-token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JSON
|
|
||||||
|
|
||||||
cat > "$FIXTURE_DIR/gitea-standard.json" <<'JSON'
|
cat > "$FIXTURE_DIR/gitea-standard.json" <<'JSON'
|
||||||
{
|
{
|
||||||
@@ -187,8 +131,6 @@ run_curl_success_case() {
|
|||||||
set +e
|
set +e
|
||||||
output=$(cd "$REPO_DIR" && \
|
output=$(cd "$REPO_DIR" && \
|
||||||
PATH="$STUB_DIR:$PATH" \
|
PATH="$STUB_DIR:$PATH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
||||||
TMPDIR="$tmpdir" \
|
TMPDIR="$tmpdir" \
|
||||||
GITEA_TOKEN="stub-token" \
|
GITEA_TOKEN="stub-token" \
|
||||||
GITEA_URL="https://git.example.test" \
|
GITEA_URL="https://git.example.test" \
|
||||||
@@ -228,8 +170,6 @@ run_curl_early_exit_cleanup_case() {
|
|||||||
set +e
|
set +e
|
||||||
output=$(cd "$REPO_DIR" && \
|
output=$(cd "$REPO_DIR" && \
|
||||||
PATH="$STUB_DIR:$PATH" \
|
PATH="$STUB_DIR:$PATH" \
|
||||||
HOME="$HOME_DIR" \
|
|
||||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
||||||
TMPDIR="$tmpdir" \
|
TMPDIR="$tmpdir" \
|
||||||
GITEA_TOKEN="stub-token" \
|
GITEA_TOKEN="stub-token" \
|
||||||
GITEA_URL="https://git.example.test" \
|
GITEA_URL="https://git.example.test" \
|
||||||
@@ -264,8 +204,7 @@ run_curl_early_exit_cleanup_case() {
|
|||||||
run_case() {
|
run_case() {
|
||||||
local fixture="$1" expected_number="$2" expected_head="$3"
|
local fixture="$1" expected_number="$2" expected_head="$3"
|
||||||
local output
|
local output
|
||||||
output=$(cd "$REPO_DIR" && HOME="$HOME_DIR" MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
output=$(cd "$REPO_DIR" && MOSAIC_GITEA_PR_METADATA_RAW_FILE="$fixture" "$SCRIPT_DIR/pr-metadata.sh" -n "$expected_number")
|
||||||
MOSAIC_GITEA_PR_METADATA_RAW_FILE="$fixture" "$SCRIPT_DIR/pr-metadata.sh" -n "$expected_number")
|
|
||||||
PR_METADATA_OUTPUT="$output" python3 - "$expected_number" "$expected_head" <<'PY'
|
PR_METADATA_OUTPUT="$output" python3 - "$expected_number" "$expected_head" <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -225,10 +225,7 @@ write_response() {
|
|||||||
emit() {
|
emit() {
|
||||||
# Split a two-line "status\n<json body>" python result into the response.
|
# Split a two-line "status\n<json body>" python result into the response.
|
||||||
local result="$1"
|
local result="$1"
|
||||||
response_status="${result%%$'\n'*}"
|
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||||
response_body=""
|
|
||||||
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
|
|
||||||
write_response "$response_status" "$response_body"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mode="${PR_REVIEW_TEST_MODE:-}"
|
mode="${PR_REVIEW_TEST_MODE:-}"
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ grep -q 'Unknown action: bogus-action' "$OUTPUT_FILE"
|
|||||||
|
|
||||||
# --- Case 2: -h/--help documents both overrides.
|
# --- Case 2: -h/--help documents both overrides.
|
||||||
HELP_TEXT="$("$SCRIPT_DIR/pr-review.sh" -h)"
|
HELP_TEXT="$("$SCRIPT_DIR/pr-review.sh" -h)"
|
||||||
grep -q -- '-r, --repo' <<<"$HELP_TEXT"
|
echo "$HELP_TEXT" | grep -q -- '-r, --repo'
|
||||||
grep -q -- '-H, --host' <<<"$HELP_TEXT"
|
echo "$HELP_TEXT" | grep -q -- '-H, --host'
|
||||||
|
|
||||||
# --- Case 3 (comment): a TRUE no-git-origin dir + -r/-H must not silently die
|
# --- Case 3 (comment): a TRUE no-git-origin dir + -r/-H must not silently die
|
||||||
# and must not fail with "not a git repository or no origin remote" either.
|
# and must not fail with "not a git repository or no origin remote" either.
|
||||||
|
|||||||
@@ -91,12 +91,10 @@ fi
|
|||||||
|
|
||||||
if [[ -n "$dirty_files" ]]; then
|
if [[ -n "$dirty_files" ]]; then
|
||||||
echo " Modified files:"
|
echo " Modified files:"
|
||||||
mapfile -t dirty_lines <<<"$dirty_files"
|
echo "$dirty_files" | head -20 | while IFS= read -r line; do
|
||||||
file_count="${#dirty_lines[@]}"
|
echo " $line"
|
||||||
display_count=$((file_count < 20 ? file_count : 20))
|
|
||||||
for ((i = 0; i < display_count; i++)); do
|
|
||||||
echo " ${dirty_lines[$i]}"
|
|
||||||
done
|
done
|
||||||
|
file_count="$(echo "$dirty_files" | wc -l)"
|
||||||
if (( file_count > 20 )); then
|
if (( file_count > 20 )); then
|
||||||
echo " ... and $(( file_count - 20 )) more"
|
echo " ... and $(( file_count - 20 )) more"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ if jq -e '.next_task == "T-001"' "$capsule_file" >/dev/null 2>&1; then pass_case
|
|||||||
if grep -Fq 'Target runtime:** codex' <<< "$codex_continue_output"; then pass_case "continue prompt contains target runtime codex"; else fail_case "continue prompt contains target runtime codex"; fi
|
if grep -Fq 'Target runtime:** codex' <<< "$codex_continue_output"; then pass_case "continue prompt contains target runtime codex"; else fail_case "continue prompt contains target runtime codex"; fi
|
||||||
|
|
||||||
codex_run_prompt="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
codex_run_prompt="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
||||||
if [[ "${codex_run_prompt%%$'\n'*}" == "Now initiating Orchestrator mode..." ]]; then pass_case "codex run prompt first line is mode declaration"; else fail_case "codex run prompt first line is mode declaration"; fi
|
if [[ "$(printf '%s\n' "$codex_run_prompt" | head -n1)" == "Now initiating Orchestrator mode..." ]]; then pass_case "codex run prompt first line is mode declaration"; else fail_case "codex run prompt first line is mode declaration"; fi
|
||||||
if grep -Fq 'Do NOT ask clarifying questions before your first tool actions' <<< "$codex_run_prompt"; then pass_case "codex run prompt includes no-questions hard gate"; else fail_case "codex run prompt includes no-questions hard gate"; fi
|
if grep -Fq 'Do NOT ask clarifying questions before your first tool actions' <<< "$codex_run_prompt"; then pass_case "codex run prompt includes no-questions hard gate"; else fail_case "codex run prompt includes no-questions hard gate"; fi
|
||||||
if grep -Fq '"next_task": "T-001"' <<< "$codex_run_prompt"; then pass_case "codex run prompt embeds capsule json"; else fail_case "codex run prompt embeds capsule json"; fi
|
if grep -Fq '"next_task": "T-001"' <<< "$codex_run_prompt"; then pass_case "codex run prompt embeds capsule json"; else fail_case "codex run prompt embeds capsule json"; fi
|
||||||
|
|
||||||
claude_run_prompt="$(MOSAIC_COORD_RUNTIME=claude bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
claude_run_prompt="$(MOSAIC_COORD_RUNTIME=claude bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
||||||
if [[ "${claude_run_prompt%%$'\n'*}" == "## Continuation Mission" ]]; then pass_case "claude run prompt remains continuation prompt format"; else fail_case "claude run prompt remains continuation prompt format"; fi
|
if [[ "$(printf '%s\n' "$claude_run_prompt" | head -n1)" == "## Continuation Mission" ]]; then pass_case "claude run prompt remains continuation prompt format"; else fail_case "claude run prompt remains continuation prompt format"; fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Smoke test summary: pass=$PASS fail=$FAIL"
|
echo "Smoke test summary: pass=$PASS fail=$FAIL"
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ L="$WORK/live5.md"; G="$WORK/ledger5.md"; echo "# LEDGER" > "$G"
|
|||||||
make_board "$L" 6 1 400
|
make_board "$L" 6 1 400
|
||||||
before_l=$(cat "$L"); before_g=$(cat "$G")
|
before_l=$(cat "$L"); before_g=$(cat "$G")
|
||||||
out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 --dry-run 2>&1) || note "dry-run exited nonzero: $out"
|
out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 --dry-run 2>&1) || note "dry-run exited nonzero: $out"
|
||||||
grep -qi "dry run" <<<"$out" || note "dry-run did not announce itself"
|
echo "$out" | grep -qi "dry run" || note "dry-run did not announce itself"
|
||||||
grep -q "would roll" <<<"$out" || note "dry-run did not report a plan"
|
echo "$out" | grep -q "would roll" || note "dry-run did not report a plan"
|
||||||
[[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE"
|
[[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE"
|
||||||
[[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER"
|
[[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER"
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ present=0
|
|||||||
|
|
||||||
for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do
|
for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do
|
||||||
pattern="${entry#*|}"
|
pattern="${entry#*|}"
|
||||||
if grep -qiE "$pattern" <<<"$PRD_CONTENT"; then
|
if echo "$PRD_CONTENT" | grep -qiE "$pattern"; then
|
||||||
present=$((present + 1))
|
present=$((present + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -169,13 +169,13 @@ main() {
|
|||||||
# classify_surface PATH → surface name (highest-risk match wins, mirrors TS)
|
# classify_surface PATH → surface name (highest-risk match wins, mirrors TS)
|
||||||
classify_surface() {
|
classify_surface() {
|
||||||
local p="$1"
|
local p="$1"
|
||||||
if grep -qiE 'auth|login|session|token|permission|rbac|credential|secret' <<<"$p"; then echo auth; return; fi
|
if printf '%s' "$p" | grep -qiE 'auth|login|session|token|permission|rbac|credential|secret'; then echo auth; return; fi
|
||||||
if grep -qiE 'migration|prisma|schema|\.sql|entity|repository|seed' <<<"$p"; then echo data; return; fi
|
if printf '%s' "$p" | grep -qiE 'migration|prisma|schema|\.sql|entity|repository|seed'; then echo data; return; fi
|
||||||
if grep -qiE 'docker|\.woodpecker|compose|traefik|deploy|helm|k8s|terraform' <<<"$p"; then echo infra; return; fi
|
if printf '%s' "$p" | grep -qiE 'docker|\.woodpecker|compose|traefik|deploy|helm|k8s|terraform'; then echo infra; return; fi
|
||||||
if grep -qiE 'package\.json|tsconfig|turbo\.json|pnpm-|\.config\.|eslint|vite' <<<"$p"; then echo build; return; fi
|
if printf '%s' "$p" | grep -qiE 'package\.json|tsconfig|turbo\.json|pnpm-|\.config\.|eslint|vite'; then echo build; return; fi
|
||||||
if grep -qE '\.tsx|\.css|components/|apps/web/' <<<"$p"; then echo ui; return; fi
|
if printf '%s' "$p" | grep -qE '\.tsx|\.css|components/|apps/web/'; then echo ui; return; fi
|
||||||
if grep -qE '\.spec\.|\.test\.|__tests__/' <<<"$p"; then echo test; return; fi
|
if printf '%s' "$p" | grep -qE '\.spec\.|\.test\.|__tests__/'; then echo test; return; fi
|
||||||
if grep -qE '\.md$|docs/' <<<"$p"; then echo docs; return; fi
|
if printf '%s' "$p" | grep -qE '\.md$|docs/'; then echo docs; return; fi
|
||||||
echo none
|
echo none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ JSON_INPUT=$(cat)
|
|||||||
if command -v jq &>/dev/null; then
|
if command -v jq &>/dev/null; then
|
||||||
FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // empty' 2>/dev/null || echo "")
|
FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // empty' 2>/dev/null || echo "")
|
||||||
else
|
else
|
||||||
file_path_pattern='"file_path"[[:space:]]*:[[:space:]]*"([^"]*)"'
|
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' | head -1)
|
||||||
if [[ "$JSON_INPUT" =~ $file_path_pattern ]]; then
|
|
||||||
FILE_PATH="${BASH_REMATCH[1]}"
|
|
||||||
else
|
|
||||||
FILE_PATH=""
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Only check TypeScript files
|
# Only check TypeScript files
|
||||||
@@ -58,7 +53,7 @@ OUTPUT=$(npx tsc --noEmit --pretty --maxNodeModuleJsDepth 0 2>&1) || STATUS=$?
|
|||||||
if [ "${STATUS:-0}" -ne 0 ]; then
|
if [ "${STATUS:-0}" -ne 0 ]; then
|
||||||
# Filter output to only show errors related to the edited file (if possible)
|
# Filter output to only show errors related to the edited file (if possible)
|
||||||
BASENAME=$(basename "$FILE_PATH")
|
BASENAME=$(basename "$FILE_PATH")
|
||||||
RELEVANT=$(grep -A2 "$BASENAME" <<<"$OUTPUT" 2>/dev/null || sed -n '1,20p' <<<"$OUTPUT")
|
RELEVANT=$(echo "$OUTPUT" | grep -A2 "$BASENAME" 2>/dev/null || echo "$OUTPUT" | head -20)
|
||||||
|
|
||||||
echo "TypeScript type errors detected after editing $FILE_PATH:"
|
echo "TypeScript type errors detected after editing $FILE_PATH:"
|
||||||
echo "$RELEVANT"
|
echo "$RELEVANT"
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ chk "F1 fresh: CONSTITUTION/AGENTS/STANDARDS/TOOLS seeded" \
|
|||||||
"[ -f '$T1/CONSTITUTION.md' ] && [ -f '$T1/AGENTS.md' ] && [ -f '$T1/STANDARDS.md' ] && [ -f '$T1/TOOLS.md' ]"
|
"[ -f '$T1/CONSTITUTION.md' ] && [ -f '$T1/AGENTS.md' ] && [ -f '$T1/STANDARDS.md' ] && [ -f '$T1/TOOLS.md' ]"
|
||||||
chk "F1 fresh: AGENTS == shipped default" "cmp -s '$T1/AGENTS.md' '$DEFA/AGENTS.md'"
|
chk "F1 fresh: AGENTS == shipped default" "cmp -s '$T1/AGENTS.md' '$DEFA/AGENTS.md'"
|
||||||
chk "F1 fresh: framework-version stamped 3" "[ \"\$(cat '$T1/.framework-version' 2>/dev/null)\" = 3 ]"
|
chk "F1 fresh: framework-version stamped 3" "[ \"\$(cat '$T1/.framework-version' 2>/dev/null)\" = 3 ]"
|
||||||
chk "F1 fresh: Pi goal extension deploys under Mosaic runtime" \
|
|
||||||
"cmp -s '$T1/runtime/pi/goal-extension.ts' '$FW/runtime/pi/goal-extension.ts'"
|
|
||||||
chk "F1 fresh: installer creates no nested main Pi config" "[ ! -e '$T1/.pi' ]"
|
|
||||||
|
|
||||||
# F2 — legacy install with a user-edited AGENTS.md (the sanctioned pre-constitution customization)
|
# F2 — legacy install with a user-edited AGENTS.md (the sanctioned pre-constitution customization)
|
||||||
T2=$(mktemp -d); mkdir -p "$T2/credentials"
|
T2=$(mktemp -d); mkdir -p "$T2/credentials"
|
||||||
@@ -92,8 +89,6 @@ chk "F6 reseed: per-agent env bytes survive" "cmp -s '$T6/fleet/agents/coder0.en
|
|||||||
chk "F6 reseed: heartbeat bytes survive" "cmp -s '$T6/fleet/run/coder0.hb' '$E6/run.expected'"
|
chk "F6 reseed: heartbeat bytes survive" "cmp -s '$T6/fleet/run/coder0.hb' '$E6/run.expected'"
|
||||||
chk "F6 reseed: framework examples are refreshed" "grep -q orchestrator '$T6/fleet/examples/general.yaml'"
|
chk "F6 reseed: framework examples are refreshed" "grep -q orchestrator '$T6/fleet/examples/general.yaml'"
|
||||||
chk "F6 reseed: framework roster schema is refreshed" "cmp -s '$T6/fleet/roster.schema.json' '$FW/fleet/roster.schema.json'"
|
chk "F6 reseed: framework roster schema is refreshed" "cmp -s '$T6/fleet/roster.schema.json' '$FW/fleet/roster.schema.json'"
|
||||||
chk "F6 reseed: Pi goal extension is refreshed from framework source" \
|
|
||||||
"cmp -s '$T6/runtime/pi/goal-extension.ts' '$FW/runtime/pi/goal-extension.ts'"
|
|
||||||
|
|
||||||
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5" "$T6" "$E6"
|
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5" "$T6" "$E6"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -176,12 +176,8 @@ run_snap() {
|
|||||||
|
|
||||||
# Resolve the single pre-update-* snapshot dir under a state dir (newest if many).
|
# Resolve the single pre-update-* snapshot dir under a state dir (newest if many).
|
||||||
snap_dir() {
|
snap_dir() {
|
||||||
local -a snapshots=()
|
|
||||||
mapfile -t snapshots < <(
|
|
||||||
find "$1/mosaic/backups" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null \
|
find "$1/mosaic/backups" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null \
|
||||||
| LC_ALL=C sort -r
|
| LC_ALL=C sort -r | head -1
|
||||||
)
|
|
||||||
printf '%s\n' "${snapshots[0]:-}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "── Part 1/2/3: durable snapshot scope, perms, no-leak ──────────────────"
|
echo "── Part 1/2/3: durable snapshot scope, perms, no-leak ──────────────────"
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ chk "[reset-fail] the manual-recovery pointer is emitted (not a silent set -e ex
|
|||||||
"grep -q 'Snapshot restore could not reset' '$OUTG'"
|
"grep -q 'Snapshot restore could not reset' '$OUTG'"
|
||||||
chk "[reset-fail] the recovery message points at a preserved snapshot dir" \
|
chk "[reset-fail] the recovery message points at a preserved snapshot dir" \
|
||||||
"grep -q 'preserved at: .*mosaic-snapshot' '$OUTG'"
|
"grep -q 'preserved at: .*mosaic-snapshot' '$OUTG'"
|
||||||
SNAP_E="$(grep -m1 -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTG")"
|
SNAP_E="$(grep -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTG" | head -1)"
|
||||||
chk "[reset-fail] the named snapshot directory actually survives for recovery" \
|
chk "[reset-fail] the named snapshot directory actually survives for recovery" \
|
||||||
"[ -n '$SNAP_E' ] && [ -d '$SNAP_E' ]"
|
"[ -n '$SNAP_E' ] && [ -d '$SNAP_E' ]"
|
||||||
chk "[reset-fail] operator secret value never appears in installer output" \
|
chk "[reset-fail] operator secret value never appears in installer output" \
|
||||||
@@ -353,8 +353,7 @@ chk "[control] without the D2 recovery line the operator gets no snapshot pointe
|
|||||||
"! grep -q 'Snapshot restore could not reset' '$OUTH'"
|
"! grep -q 'Snapshot restore could not reset' '$OUTH'"
|
||||||
[ -n "${SNAP_E:-}" ] && rm -rf "$SNAP_E"
|
[ -n "${SNAP_E:-}" ] && rm -rf "$SNAP_E"
|
||||||
# Reap any snapshot the reset-fail runs left in /tmp (reset failed → never cleaned).
|
# Reap any snapshot the reset-fail runs left in /tmp (reset failed → never cleaned).
|
||||||
orphan_snapshot="$(grep -m1 -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTH" 2>/dev/null || true)"
|
grep -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTH" 2>/dev/null | head -1 | while read -r s; do rm -rf "$s"; done
|
||||||
[ -n "$orphan_snapshot" ] && rm -rf "$orphan_snapshot"
|
|
||||||
|
|
||||||
# Cleanup (generated installer controls are also removed by the EXIT trap).
|
# Cleanup (generated installer controls are also removed by the EXIT trap).
|
||||||
for d in "$HA" "$REFA" "$HB" "$REFB" "$HC" "$HD" "$HE" "$REFE" "$HF" "$REFF" "$HG" "$HH"; do rm -rf "$d"; done
|
for d in "$HA" "$REFA" "$HB" "$REFB" "$HC" "$HD" "$HE" "$REFE" "$HF" "$REFF" "$HG" "$HH"; do rm -rf "$d"; done
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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)
|
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 ---
|
# --- 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/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/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
|
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
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
|
|||||||
sleep 1.2
|
sleep 1.2
|
||||||
pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null)
|
pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null)
|
||||||
|
|
||||||
if grep -qF "$QUEUED_RE" <<<"$pane"; then
|
if printf '%s' "$pane" | grep -qF "$QUEUED_RE"; then
|
||||||
status="queued"; break
|
status="queued"; break
|
||||||
fi
|
fi
|
||||||
# Locate the REPL input box (prompt glyph). If we cannot see it, we have NO
|
# Locate the REPL input box (prompt glyph). If we cannot see it, we have NO
|
||||||
@@ -121,7 +121,7 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
|
|||||||
fi
|
fi
|
||||||
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
|
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
|
||||||
# (Submitted messages scroll up into history; a draft stays on the ❯ line.)
|
# (Submitted messages scroll up into history; a draft stays on the ❯ line.)
|
||||||
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$promptline"; then
|
if [ -n "$snippet" ] && printf '%s' "$promptline" | grep -qF "$snippet"; then
|
||||||
status="draft"; continue
|
status="draft"; continue
|
||||||
fi
|
fi
|
||||||
# Input box located AND clear of our tail => positively submitted. This is the
|
# Input box located AND clear of our tail => positively submitted. This is the
|
||||||
|
|||||||
@@ -34,20 +34,16 @@ tmux new-session -d -s "$DEFAULT_TARGET" -c "$TMPDIR" 'PS1="❯ " exec bash --no
|
|||||||
|
|
||||||
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
|
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
named_pane="$(capture_named)" || fail "could not capture named socket pane"
|
capture_named | grep -qF "named socket hello" || fail "send-message.sh did not deliver to named socket"
|
||||||
grep -qF "named socket hello" <<<"$named_pane" || fail "send-message.sh did not deliver to named socket"
|
if capture_default | grep -qF "named socket hello"; then
|
||||||
default_pane="$(capture_default)" || fail "could not capture default socket pane"
|
|
||||||
if grep -qF "named socket hello" <<<"$default_pane"; then
|
|
||||||
fail "send-message.sh leaked named-socket message to default tmux server"
|
fail "send-message.sh leaked named-socket message to default tmux server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
|
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
named_pane="$(capture_named)" || fail "could not capture named socket pane"
|
capture_named | grep -qF "[tester:source ->" || fail "agent-send.sh did not include preamble"
|
||||||
grep -qF "[tester:source ->" <<<"$named_pane" || fail "agent-send.sh did not include preamble"
|
capture_named | grep -qF "agent socket hello" || fail "agent-send.sh did not deliver to named socket"
|
||||||
grep -qF "agent socket hello" <<<"$named_pane" || fail "agent-send.sh did not deliver to named socket"
|
if capture_default | grep -qF "agent socket hello"; then
|
||||||
default_pane="$(capture_default)" || fail "could not capture default socket pane"
|
|
||||||
if grep -qF "agent socket hello" <<<"$default_pane"; then
|
|
||||||
fail "agent-send.sh leaked named-socket message to default tmux server"
|
fail "agent-send.sh leaked named-socket message to default tmux server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,11 +65,11 @@ done
|
|||||||
sleep 0.2
|
sleep 0.2
|
||||||
for i in $(seq 1 "$CONC_N"); do
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
|
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
|
||||||
grep -qF "CONCPAYLOAD-${i}-END" <<<"$pane" \
|
printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${i}-END" \
|
||||||
|| fail "concurrent send dropped payload for pane conc-$i"
|
|| fail "concurrent send dropped payload for pane conc-$i"
|
||||||
for j in $(seq 1 "$CONC_N"); do
|
for j in $(seq 1 "$CONC_N"); do
|
||||||
[ "$j" = "$i" ] && continue
|
[ "$j" = "$i" ] && continue
|
||||||
if grep -qF "CONCPAYLOAD-${j}-END" <<<"$pane"; then
|
if printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${j}-END"; then
|
||||||
fail "concurrent send cross-delivered payload $j to pane conc-$i"
|
fail "concurrent send cross-delivered payload $j to pane conc-$i"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ tmux -L "$SOCKET" new-session -d -s repl -c "$TMP" \
|
|||||||
'PS1="❯ " exec bash --noprofile --norc -i'
|
'PS1="❯ " exec bash --noprofile --norc -i'
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
|
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
|
||||||
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
|
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qF "✓ delivered"; then
|
||||||
ok "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered"
|
ok "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered"
|
||||||
else
|
else
|
||||||
no "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
|
no "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ _manifest_val() {
|
|||||||
# _manifest_val KEY — echo VALUE for KEY=VALUE in the manifest (blank if none).
|
# _manifest_val KEY — echo VALUE for KEY=VALUE in the manifest (blank if none).
|
||||||
local key="$1"
|
local key="$1"
|
||||||
[ -f "$MANIFEST" ] || return 0
|
[ -f "$MANIFEST" ] || return 0
|
||||||
awk -v key="$key" 'index($0, key "=") == 1 { sub(/^[^=]*=/, ""); gsub(/[[:space:]]/, ""); print; exit }' "$MANIFEST"
|
sed -n "s/^${key}=//p" "$MANIFEST" | head -n1 | tr -d '[:space:]'
|
||||||
}
|
}
|
||||||
|
|
||||||
# _load_watchlist — validate the watch-list path + JSON + schema_version range.
|
# _load_watchlist — validate the watch-list path + JSON + schema_version range.
|
||||||
@@ -267,7 +267,7 @@ _poll_source() {
|
|||||||
if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then
|
if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then
|
||||||
snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")"
|
snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")"
|
||||||
snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")"
|
snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")"
|
||||||
if [ -n "$snap_sha" ] && ! grep -Eq '^[0-9a-f]{7,64}$' <<<"$snap_sha"; then
|
if [ -n "$snap_sha" ] && ! printf '%s' "$snap_sha" | grep -Eq '^[0-9a-f]{7,64}$'; then
|
||||||
echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2
|
echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2
|
||||||
snap_sha=""
|
snap_sha=""
|
||||||
snap_ts=""
|
snap_ts=""
|
||||||
@@ -275,7 +275,7 @@ _poll_source() {
|
|||||||
# A ts must be a sane positive epoch BEFORE any arithmetic touches it: a
|
# A ts must be a sane positive epoch BEFORE any arithmetic touches it: a
|
||||||
# negative or absurdly large value would make the shell integer comparison
|
# negative or absurdly large value would make the shell integer comparison
|
||||||
# below error out and silently KEEP the bad ts — validate first, compare after.
|
# below error out and silently KEEP the bad ts — validate first, compare after.
|
||||||
if [ -n "$snap_ts" ] && ! grep -Eq '^[0-9]{1,12}$' <<<"$snap_ts"; then
|
if [ -n "$snap_ts" ] && ! printf '%s' "$snap_ts" | grep -Eq '^[0-9]{1,12}$'; then
|
||||||
echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2
|
echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2
|
||||||
snap_ts=""
|
snap_ts=""
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -644,8 +644,7 @@ cmd_render() {
|
|||||||
oseq="$(jq -r '.observed_seq // "?"' <<<"$line")"
|
oseq="$(jq -r '.observed_seq // "?"' <<<"$line")"
|
||||||
oclass="$(jq -r '.class // "actionable"' <<<"$line")"
|
oclass="$(jq -r '.class // "actionable"' <<<"$line")"
|
||||||
oloc="$(jq -c '.locators // {}' <<<"$line")"
|
oloc="$(jq -c '.locators // {}' <<<"$line")"
|
||||||
olabel="$(_locator_line "$oloc")"
|
olabel="$(_locator_line "$oloc" | head -n1)"
|
||||||
olabel="${olabel%%$'\n'*}"
|
|
||||||
printf ' * seq %s [%s] %s\n' "$oseq" "$(_scrub_inline "$oclass")" "$olabel"
|
printf ' * seq %s [%s] %s\n' "$oseq" "$(_scrub_inline "$oclass")" "$olabel"
|
||||||
done <<<"$pending"
|
done <<<"$pending"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ EOF
|
|||||||
_manifest_val() {
|
_manifest_val() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
[ -f "$MANIFEST" ] || return 0
|
[ -f "$MANIFEST" ] || return 0
|
||||||
awk -v key="$key" 'index($0, key "=") == 1 { sub(/^[^=]*=/, ""); gsub(/[[:space:]]/, ""); print; exit }' "$MANIFEST"
|
sed -n "s/^${key}=//p" "$MANIFEST" | head -n1 | tr -d '[:space:]'
|
||||||
}
|
}
|
||||||
|
|
||||||
# _load_watchlist — validate path + JSON + shape + Gate B schema range (mirrors
|
# _load_watchlist — validate path + JSON + shape + Gate B schema range (mirrors
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ echo "== Q16 (guard): Q2's ENUM-B fixture must STAY address-free — the reconci
|
|||||||
# Token concatenated so THIS guard's own source lines never contain the
|
# Token concatenated so THIS guard's own source lines never contain the
|
||||||
# literal fixture id and cannot self-match.
|
# literal fixture id and cannot self-match.
|
||||||
enum_id='ENUM''-B'
|
enum_id='ENUM''-B'
|
||||||
fixture_lines="$(has_match -F "\"id\":\"$enum_id\"" "$self")"; fixture_matches="$(has_match -F '"observed_seq":5' <<<"$fixture_lines")"; fixture_line="${fixture_matches%%$'\n'*}"
|
fixture_line="$(has_match -F "\"id\":\"$enum_id\"" "$self" | has_match -F '"observed_seq":5' | head -n1)"
|
||||||
[ -n "$fixture_line" ] || fail_msg "Q16: could not locate Q2's $enum_id fixture line (renamed/renumbered? update this guard)"
|
[ -n "$fixture_line" ] || fail_msg "Q16: could not locate Q2's $enum_id fixture line (renamed/renumbered? update this guard)"
|
||||||
fixture_json="$(printf '%s' "$fixture_line" | sed "s/.*'\({.*}\)'.*/\1/")"
|
fixture_json="$(printf '%s' "$fixture_line" | sed "s/.*'\({.*}\)'.*/\1/")"
|
||||||
# Positive controls FIRST (blind-instrument rule): the extraction must yield
|
# Positive controls FIRST (blind-instrument rule): the extraction must yield
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ echo "== P3: detector orders the cause line BEFORE the delta it explains =="
|
|||||||
printf 'r1 state v2\n' >"$fx/repo_r1"
|
printf 'r1 state v2\n' >"$fx/repo_r1"
|
||||||
"$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: second poll failed"
|
"$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: second poll failed"
|
||||||
sd="$(state_dir)"
|
sd="$(state_dir)"
|
||||||
pre_seq="$(jq -nr 'first(inputs | select(.locators.kind == "preimage") | .observed_seq) // empty' "$sd/pending.jsonl")"
|
pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
|
||||||
src_seq="$(jq -nr 'first(inputs | select(.locators.kind == "repo") | .observed_seq) // empty' "$sd/pending.jsonl")"
|
src_seq="$(jq -r 'select(.locators.kind == "repo") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
|
||||||
[ -n "$pre_seq" ] || fail_msg "P3: no preimage cause entry enqueued"
|
[ -n "$pre_seq" ] || fail_msg "P3: no preimage cause entry enqueued"
|
||||||
[ -n "$src_seq" ] || fail_msg "P3: no source delta entry enqueued"
|
[ -n "$src_seq" ] || fail_msg "P3: no source delta entry enqueued"
|
||||||
if [ -n "$pre_seq" ] && [ -n "$src_seq" ]; then
|
if [ -n "$pre_seq" ] && [ -n "$src_seq" ]; then
|
||||||
@@ -343,8 +343,8 @@ echo "== P11: reconcile surfaces the cause line before its enumerations =="
|
|||||||
printf '# adapter changed while detector down\n' >>"$fx/adapter.sh"
|
printf '# adapter changed while detector down\n' >>"$fx/adapter.sh"
|
||||||
"$RECON" reconcile >/dev/null 2>&1 # rc 1 expected (unaccounted enumerated)
|
"$RECON" reconcile >/dev/null 2>&1 # rc 1 expected (unaccounted enumerated)
|
||||||
sd="$(state_dir)"
|
sd="$(state_dir)"
|
||||||
pre_seq="$(jq -nr 'first(inputs | select(.locators.kind == "preimage") | .observed_seq) // empty' "$sd/pending.jsonl")"
|
pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
|
||||||
enum_seq="$(jq -nr 'first(inputs | select(.locators.reconciled == true) | .observed_seq) // empty' "$sd/pending.jsonl")"
|
enum_seq="$(jq -r 'select(.locators.reconciled == true) | .observed_seq' "$sd/pending.jsonl" | head -n1)"
|
||||||
[ -n "$pre_seq" ] || fail_msg "P11: reconcile must enqueue the preimage cause line"
|
[ -n "$pre_seq" ] || fail_msg "P11: reconcile must enqueue the preimage cause line"
|
||||||
[ -n "$enum_seq" ] || fail_msg "P11: reconcile must still enumerate the unaccounted source"
|
[ -n "$enum_seq" ] || fail_msg "P11: reconcile must still enumerate the unaccounted source"
|
||||||
if [ -n "$pre_seq" ] && [ -n "$enum_seq" ]; then
|
if [ -n "$pre_seq" ] && [ -n "$enum_seq" ]; then
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ site_line() { # site_line FILE MARKER -> first physical line of that call
|
|||||||
local f="$1" marker="$2" ln
|
local f="$1" marker="$2" ln
|
||||||
ln="$(grep -n "# SITE:${marker}\$" "$f" | cut -d: -f1)"
|
ln="$(grep -n "# SITE:${marker}\$" "$f" | cut -d: -f1)"
|
||||||
# continuation marker sits on the tail line; the call starts one line up
|
# continuation marker sits on the tail line; the call starts one line up
|
||||||
source_line="$(sed -n "${ln}p" "$f")"; if ! grep -Eq 'has_match|count_lines' <<<"$source_line"; then
|
if ! sed -n "${ln}p" "$f" | grep -Eq 'has_match|count_lines'; then
|
||||||
ln=$((ln - 1))
|
ln=$((ln - 1))
|
||||||
fi
|
fi
|
||||||
printf '%s' "$ln"
|
printf '%s' "$ln"
|
||||||
@@ -167,13 +167,13 @@ rcB=$?
|
|||||||
sort "$LEDGER" >"$TMP/got-c1"
|
sort "$LEDGER" >"$TMP/got-c1"
|
||||||
n_expected="$(grep -c . "$TMP/expected-c1")"
|
n_expected="$(grep -c . "$TMP/expected-c1")"
|
||||||
if [ "$rcA" -eq 0 ] && [ "$rcB" -eq 0 ] &&
|
if [ "$rcA" -eq 0 ] && [ "$rcB" -eq 0 ] &&
|
||||||
grep -q 'mini-a: OK' <<<"$outA"&&
|
printf '%s' "$outA" | grep -q 'mini-a: OK' &&
|
||||||
grep -q 'mini-b: OK' <<<"$outB"&&
|
printf '%s' "$outB" | grep -q 'mini-b: OK' &&
|
||||||
[ "$n_expected" -gt 1 ] &&
|
[ "$n_expected" -gt 1 ] &&
|
||||||
cmp -s "$TMP/expected-c1" "$TMP/got-c1"; then
|
cmp -s "$TMP/expected-c1" "$TMP/got-c1"; then
|
||||||
check C1 0 ""
|
check C1 0 ""
|
||||||
else
|
else
|
||||||
check C1 1 "rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff "$TMP/expected-c1" "$TMP/got-c1" 2>&1 | sed -n '1,10p' | tr '\n' ' ')"
|
check C1 1 "rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff "$TMP/expected-c1" "$TMP/got-c1" 2>&1 | head -n 10 | tr '\n' ' ')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- C2: early exit -> short ledger, comparison catches it -----------------
|
# --- C2: early exit -> short ledger, comparison catches it -----------------
|
||||||
@@ -201,12 +201,12 @@ abort_case() { # abort_case NAME MARKER HELPER
|
|||||||
bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ "$rc" -ne 0 ] &&
|
if [ "$rc" -ne 0 ] &&
|
||||||
! grep -q 'mini-a: OK' <<<"$out"&&
|
! printf '%s' "$out" | grep -q 'mini-a: OK' &&
|
||||||
! grep -q 'mini-a: FAILED' <<<"$out"&&
|
! printf '%s' "$out" | grep -q 'mini-a: FAILED' &&
|
||||||
grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" <<<"$out"&&
|
printf '%s' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" &&
|
||||||
grep -q "WAKE-ASSERT ABORT" <<<"$out"&&
|
printf '%s' "$out" | grep -q "WAKE-ASSERT ABORT" &&
|
||||||
grep -q "$site" <<<"$out"&&
|
printf '%s' "$out" | grep -q "$site" &&
|
||||||
grep -q "grep exit 2" <<<"$out"&&
|
printf '%s' "$out" | grep -q "grep exit 2" &&
|
||||||
grep -q "^${helper} ${site}\$" "$ledger"; then
|
grep -q "^${helper} ${site}\$" "$ledger"; then
|
||||||
check "$name" 0 ""
|
check "$name" 0 ""
|
||||||
else
|
else
|
||||||
@@ -230,8 +230,8 @@ if [ "$got" = "1" ]; then check C8 0 ""; else check C8 1 "env-prefix did not rea
|
|||||||
out="$(WAKE_ASSERT_FORCE_GREP_ERROR_AT="mini-a.sh:9999" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
out="$(WAKE_ASSERT_FORCE_GREP_ERROR_AT="mini-a.sh:9999" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ "$rc" -eq 0 ] &&
|
if [ "$rc" -eq 0 ] &&
|
||||||
grep -q 'mini-a: OK' <<<"$out"&&
|
printf '%s' "$out" | grep -q 'mini-a: OK' &&
|
||||||
! grep -q 'WAKE-ASSERT ARMED' <<<"$out"; then
|
! printf '%s' "$out" | grep -q 'WAKE-ASSERT ARMED'; then
|
||||||
check C9 0 ""
|
check C9 0 ""
|
||||||
else
|
else
|
||||||
check C9 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
|
check C9 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
|
||||||
@@ -248,8 +248,8 @@ chmod +x "$TMP/fake-bash"
|
|||||||
out="$(WAKE_ASSERT_PIN_BASH="$TMP/fake-bash" bash -c '. "$WAKE_COMMON" && wake_assert_init && echo REACHED-PAST-INIT' 2>&1)"
|
out="$(WAKE_ASSERT_PIN_BASH="$TMP/fake-bash" bash -c '. "$WAKE_COMMON" && wake_assert_init && echo REACHED-PAST-INIT' 2>&1)"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ "$rc" -ne 0 ] &&
|
if [ "$rc" -ne 0 ] &&
|
||||||
! grep -q 'REACHED-PAST-INIT' <<<"$out"&&
|
! printf '%s' "$out" | grep -q 'REACHED-PAST-INIT' &&
|
||||||
grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated' <<<"$out"; then
|
printf '%s' "$out" | grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated'; then
|
||||||
check C10 0 ""
|
check C10 0 ""
|
||||||
else
|
else
|
||||||
check C10 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
|
check C10 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
|
||||||
@@ -283,8 +283,8 @@ out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-c.sh" "$TMP" 2>&1)"
|
|||||||
rc=$?
|
rc=$?
|
||||||
summary_ln="$(site_line "$TMP/mini-c.sh" c-summary)"
|
summary_ln="$(site_line "$TMP/mini-c.sh" c-summary)"
|
||||||
if [ "$rc" -eq 1 ] &&
|
if [ "$rc" -eq 1 ] &&
|
||||||
grep -q 'wake mini-c harness: FAILED (1 assertion(s))' <<<"$out"&&
|
printf '%s' "$out" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' &&
|
||||||
! grep -q 'all invariants passed' <<<"$out"&&
|
! printf '%s' "$out" | grep -q 'all invariants passed' &&
|
||||||
grep -q "^count_lines mini-c.sh:${summary_ln}\$" "$LEDGER"; then
|
grep -q "^count_lines mini-c.sh:${summary_ln}\$" "$LEDGER"; then
|
||||||
check C11 0 ""
|
check C11 0 ""
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ if cmp -s "$TMP/expected.txt" "$TMP/static.txt"; then
|
|||||||
echo "STATIC-INVENTORY equals expected set ($(grep -c . "$TMP/static.txt") rows from source text)"
|
echo "STATIC-INVENTORY equals expected set ($(grep -c . "$TMP/static.txt") rows from source text)"
|
||||||
else
|
else
|
||||||
flag "static inventory (source text) differs from expected set (artifact):"
|
flag "static inventory (source text) differs from expected set (artifact):"
|
||||||
diff "$TMP/expected.txt" "$TMP/static.txt" | sed -n '1,20p' | sed 's/^/ /'
|
diff "$TMP/expected.txt" "$TMP/static.txt" | head -n 20 | sed 's/^/ /'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 3: green instrumented run ----------------------------------------------
|
# --- 3: green instrumented run ----------------------------------------------
|
||||||
@@ -116,7 +116,7 @@ LEDGER="$TMP/ledger"
|
|||||||
for s in "${SUITES[@]}"; do
|
for s in "${SUITES[@]}"; do
|
||||||
out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$WAKE/$s" 2>&1)"
|
out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$WAKE/$s" 2>&1)"
|
||||||
rc=$?
|
rc=$?
|
||||||
if grep -Eq "$(sentinel_for "$s")" <<<"$out"; then
|
if printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$s")"; then
|
||||||
sent="present"
|
sent="present"
|
||||||
else
|
else
|
||||||
sent="ABSENT"
|
sent="ABSENT"
|
||||||
@@ -177,14 +177,14 @@ while read -r helper site form; do
|
|||||||
rc=$?
|
rc=$?
|
||||||
bad=""
|
bad=""
|
||||||
[ "$rc" -ne 0 ] || bad="$bad exit=0"
|
[ "$rc" -ne 0 ] || bad="$bad exit=0"
|
||||||
grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" <<<"$out"||
|
printf '%s\n' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" ||
|
||||||
bad="$bad no-ARMED-line"
|
bad="$bad no-ARMED-line"
|
||||||
grep -q "WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit" <<<"$out"||
|
printf '%s\n' "$out" | grep -q "WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit" ||
|
||||||
bad="$bad no-ABORT-line"
|
bad="$bad no-ABORT-line"
|
||||||
# AND-polarity check (a match is the defect): a grep error (rc>=2) must be
|
# AND-polarity check (a match is the defect): a grep error (rc>=2) must be
|
||||||
# its own loud arm — it cannot fall through as "no sentinel = pass".
|
# its own loud arm — it cannot fall through as "no sentinel = pass".
|
||||||
rc_sent=0
|
rc_sent=0
|
||||||
grep -Eq "$(sentinel_for "$f")" <<<"$out"|| rc_sent=$?
|
printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$f")" || rc_sent=$?
|
||||||
case "$rc_sent" in
|
case "$rc_sent" in
|
||||||
0) bad="$bad sentinel-emitted" ;;
|
0) bad="$bad sentinel-emitted" ;;
|
||||||
1) : ;;
|
1) : ;;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
"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 && 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/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.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 && 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/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.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": {
|
"dependencies": {
|
||||||
"@mosaicstack/brain": "workspace:*",
|
"@mosaicstack/brain": "workspace:*",
|
||||||
|
|||||||
@@ -131,14 +131,13 @@ async function exists(path: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('projectRosterV2AgentGeneratedEnv', (): void => {
|
describe('projectRosterV2AgentGeneratedEnv', (): void => {
|
||||||
it('maps a roster-v2 agent to exactly the nine generated projection keys', (): void => {
|
it('maps a roster-v2 agent to exactly the eight generated projection keys', (): void => {
|
||||||
const roster = parseRosterV2(rosterYaml, 'yaml');
|
const roster = parseRosterV2(rosterYaml, 'yaml');
|
||||||
const agent = roster.agents.find((candidate) => candidate.name === 'coder0');
|
const agent = roster.agents.find((candidate) => candidate.name === 'coder0');
|
||||||
expect(agent).toBeDefined();
|
expect(agent).toBeDefined();
|
||||||
const values = projectRosterV2AgentGeneratedEnv(roster, agent!);
|
const values = projectRosterV2AgentGeneratedEnv(roster, agent!);
|
||||||
expect(values).toEqual({
|
expect(values).toEqual({
|
||||||
MOSAIC_AGENT_NAME: 'coder0',
|
MOSAIC_AGENT_NAME: 'coder0',
|
||||||
MOSAIC_GIT_IDENTITY: 'coder0',
|
|
||||||
MOSAIC_AGENT_CLASS: 'code',
|
MOSAIC_AGENT_CLASS: 'code',
|
||||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||||
MOSAIC_AGENT_MODEL: 'gpt-5.6-sol',
|
MOSAIC_AGENT_MODEL: 'gpt-5.6-sol',
|
||||||
|
|||||||
@@ -422,7 +422,6 @@ describe('fleet roster parsing', () => {
|
|||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
||||||
[
|
[
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
'MOSAIC_GIT_IDENTITY=coder0',
|
|
||||||
// Reflects the roster's canonicalized compatibility class (A3a).
|
// Reflects the roster's canonicalized compatibility class (A3a).
|
||||||
'MOSAIC_AGENT_CLASS=code',
|
'MOSAIC_AGENT_CLASS=code',
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
@@ -3800,7 +3799,6 @@ describe('fleet add command', () => {
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0');
|
expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0');
|
||||||
expect(envContent).toContain('MOSAIC_GIT_IDENTITY=coder0');
|
|
||||||
expect(envContent).toContain('MOSAIC_AGENT_RUNTIME=codex');
|
expect(envContent).toContain('MOSAIC_AGENT_RUNTIME=codex');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -484,7 +484,6 @@ function generateAgentEnvValues(
|
|||||||
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
||||||
return {
|
return {
|
||||||
MOSAIC_AGENT_NAME: agent.name,
|
MOSAIC_AGENT_NAME: agent.name,
|
||||||
MOSAIC_GIT_IDENTITY: agent.name,
|
|
||||||
MOSAIC_AGENT_CLASS: agent.className,
|
MOSAIC_AGENT_CLASS: agent.className,
|
||||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||||
MOSAIC_AGENT_MODEL: agent.modelHint ?? '',
|
MOSAIC_AGENT_MODEL: agent.modelHint ?? '',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { tmpdir } from 'node:os';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import {
|
import {
|
||||||
buildPiSkillArgs,
|
buildPiSkillArgs,
|
||||||
discoverPiExtensionArgs,
|
|
||||||
enumerateSkillDirs,
|
enumerateSkillDirs,
|
||||||
piForceSkillNames,
|
piForceSkillNames,
|
||||||
registerRuntimeLaunchers,
|
registerRuntimeLaunchers,
|
||||||
@@ -179,52 +178,6 @@ describe('buildPiSkillArgs', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discoverPiExtensionArgs', () => {
|
|
||||||
it('loads the core and goal extensions in deterministic order from Mosaic home', () => {
|
|
||||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
|
|
||||||
const runtimeDir = join(root, 'runtime', 'pi');
|
|
||||||
mkdirSync(runtimeDir, { recursive: true });
|
|
||||||
writeFileSync(join(runtimeDir, 'goal-extension.ts'), '// goal\n');
|
|
||||||
writeFileSync(join(runtimeDir, 'mosaic-extension.ts'), '// core\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(discoverPiExtensionArgs(root)).toEqual([
|
|
||||||
'--extension',
|
|
||||||
join(runtimeDir, 'mosaic-extension.ts'),
|
|
||||||
'--extension',
|
|
||||||
join(runtimeDir, 'goal-extension.ts'),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
rmSync(root, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('remains backward-compatible when the optional goal extension is absent', () => {
|
|
||||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
|
|
||||||
const runtimeDir = join(root, 'runtime', 'pi');
|
|
||||||
mkdirSync(runtimeDir, { recursive: true });
|
|
||||||
writeFileSync(join(runtimeDir, 'mosaic-extension.ts'), '// core\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(discoverPiExtensionArgs(root)).toEqual([
|
|
||||||
'--extension',
|
|
||||||
join(runtimeDir, 'mosaic-extension.ts'),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
rmSync(root, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits no extension arguments when Mosaic runtime assets are absent', () => {
|
|
||||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
|
|
||||||
try {
|
|
||||||
expect(discoverPiExtensionArgs(root)).toEqual([]);
|
|
||||||
} finally {
|
|
||||||
rmSync(root, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('enumerateSkillDirs (real FS)', () => {
|
describe('enumerateSkillDirs (real FS)', () => {
|
||||||
let root: string;
|
let root: string;
|
||||||
|
|
||||||
|
|||||||
@@ -715,15 +715,9 @@ export function buildPiSkillArgs(
|
|||||||
return ['--no-skills', ...forcedSkillArgs];
|
return ['--no-skills', ...forcedSkillArgs];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PI_EXTENSION_FILES = ['mosaic-extension.ts', 'goal-extension.ts'] as const;
|
function discoverPiExtension(): string[] {
|
||||||
|
const ext = join(MOSAIC_HOME, 'runtime', 'pi', 'mosaic-extension.ts');
|
||||||
export function discoverPiExtensionArgs(mosaicHome: string = MOSAIC_HOME): string[] {
|
return existsSync(ext) ? ['--extension', ext] : [];
|
||||||
const args: string[] = [];
|
|
||||||
for (const fileName of PI_EXTENSION_FILES) {
|
|
||||||
const extensionPath = join(mosaicHome, 'runtime', 'pi', fileName);
|
|
||||||
if (existsSync(extensionPath)) args.push('--extension', extensionPath);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Launch functions ────────────────────────────────────────────────────────
|
// ─── Launch functions ────────────────────────────────────────────────────────
|
||||||
@@ -798,7 +792,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
|||||||
const prompt = buildRuntimePrompt('pi');
|
const prompt = buildRuntimePrompt('pi');
|
||||||
const cliArgs = ['--append-system-prompt', prompt];
|
const cliArgs = ['--append-system-prompt', prompt];
|
||||||
cliArgs.push(...buildPiSkillArgs(args));
|
cliArgs.push(...buildPiSkillArgs(args));
|
||||||
cliArgs.push(...discoverPiExtensionArgs());
|
cliArgs.push(...discoverPiExtension());
|
||||||
if (hasMissionNoArgs) {
|
if (hasMissionNoArgs) {
|
||||||
cliArgs.push(missionPrompt);
|
cliArgs.push(missionPrompt);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -209,20 +209,6 @@ describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deploys the Mosaic-owned Pi goal extension only inside the Mosaic runtime tree', async () => {
|
|
||||||
const sourceRuntime = join(fixture.sourceDir, 'runtime', 'pi');
|
|
||||||
mkdirSync(sourceRuntime, { recursive: true });
|
|
||||||
writeFileSync(join(sourceRuntime, 'goal-extension.ts'), '// persistent goal extension\n');
|
|
||||||
|
|
||||||
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
||||||
await adapter.syncFramework('fresh');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
readFileSync(join(fixture.mosaicHome, 'runtime', 'pi', 'goal-extension.ts'), 'utf-8'),
|
|
||||||
).toBe('// persistent goal extension\n');
|
|
||||||
expect(existsSync(join(fixture.mosaicHome, '.pi'))).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is a no-op for seeding when defaults/ dir does not exist', async () => {
|
it('is a no-op for seeding when defaults/ dir does not exist', async () => {
|
||||||
rmSync(fixture.defaultsDir, { recursive: true });
|
rmSync(fixture.defaultsDir, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -358,7 +358,6 @@ function generatedValues(
|
|||||||
): Readonly<Record<string, string>> {
|
): Readonly<Record<string, string>> {
|
||||||
return {
|
return {
|
||||||
MOSAIC_AGENT_NAME: agent.name,
|
MOSAIC_AGENT_NAME: agent.name,
|
||||||
MOSAIC_GIT_IDENTITY: agent.name,
|
|
||||||
MOSAIC_AGENT_CLASS: agent.className,
|
MOSAIC_AGENT_CLASS: agent.className,
|
||||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||||
MOSAIC_AGENT_MODEL: agent.model,
|
MOSAIC_AGENT_MODEL: agent.model,
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ const COMMAND_RECORDS: Readonly<Record<string, RegExp>> = {
|
|||||||
|
|
||||||
const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
||||||
'DATA.DOTENV.FLEET_LAUNCH':
|
'DATA.DOTENV.FLEET_LAUNCH':
|
||||||
'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>',
|
'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>',
|
||||||
'DATA.TEXT_TABLE.FLEET_TASKS':
|
'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) |',
|
'| 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':
|
'DATA.TEXT_DIAGRAM.BACKLOG_FLOW':
|
||||||
@@ -406,7 +406,7 @@ const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
|||||||
'DATA.JSON.MUTATION_RESULT':
|
'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}',
|
'{\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':
|
'DATA.DOTENV.GENERATED_ENV':
|
||||||
'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>',
|
'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>',
|
||||||
'DATA.YAML.ROSTER_FIELDS':
|
'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',
|
'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(
|
expect(
|
||||||
surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'),
|
surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'),
|
||||||
).toHaveLength(863);
|
).toHaveLength(858);
|
||||||
expect(surfaces).toHaveLength(887);
|
expect(surfaces).toHaveLength(882);
|
||||||
|
|
||||||
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
||||||
const auxiliary: CodeSurface = {
|
const auxiliary: CodeSurface = {
|
||||||
|
|||||||
@@ -597,7 +597,6 @@ export function projectRosterV2AgentGeneratedEnv(
|
|||||||
): Readonly<Record<string, string>> {
|
): Readonly<Record<string, string>> {
|
||||||
return {
|
return {
|
||||||
MOSAIC_AGENT_NAME: agent.name,
|
MOSAIC_AGENT_NAME: agent.name,
|
||||||
MOSAIC_GIT_IDENTITY: agent.name,
|
|
||||||
MOSAIC_AGENT_CLASS: agent.className,
|
MOSAIC_AGENT_CLASS: agent.className,
|
||||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||||
MOSAIC_AGENT_MODEL: agent.model,
|
MOSAIC_AGENT_MODEL: agent.model,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
|
|
||||||
const generatedValues = {
|
const generatedValues = {
|
||||||
MOSAIC_AGENT_NAME: 'coder0',
|
MOSAIC_AGENT_NAME: 'coder0',
|
||||||
MOSAIC_GIT_IDENTITY: 'coder0',
|
|
||||||
MOSAIC_AGENT_CLASS: 'code',
|
MOSAIC_AGENT_CLASS: 'code',
|
||||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||||
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
|
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
|
||||||
@@ -46,7 +45,6 @@ describe('generated fleet agent environment boundary', (): void => {
|
|||||||
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
|
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
|
||||||
[
|
[
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
'MOSAIC_GIT_IDENTITY=coder0',
|
|
||||||
'MOSAIC_AGENT_CLASS=code',
|
'MOSAIC_AGENT_CLASS=code',
|
||||||
'MOSAIC_AGENT_RUNTIME=pi',
|
'MOSAIC_AGENT_RUNTIME=pi',
|
||||||
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
|
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
|
||||||
@@ -80,22 +78,6 @@ describe('generated fleet agent environment boundary', (): void => {
|
|||||||
expect(String(error)).toMatch(/key=.*sha256=/);
|
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 => {
|
it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
|
||||||
expect((): void => {
|
expect((): void => {
|
||||||
renderGeneratedAgentEnvironment({
|
renderGeneratedAgentEnvironment({
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export class AgentEnvBoundaryError extends Error {
|
|||||||
|
|
||||||
export const GENERATED_AGENT_ENV_KEYS = [
|
export const GENERATED_AGENT_ENV_KEYS = [
|
||||||
'MOSAIC_AGENT_NAME',
|
'MOSAIC_AGENT_NAME',
|
||||||
'MOSAIC_GIT_IDENTITY',
|
|
||||||
'MOSAIC_AGENT_CLASS',
|
'MOSAIC_AGENT_CLASS',
|
||||||
'MOSAIC_AGENT_RUNTIME',
|
'MOSAIC_AGENT_RUNTIME',
|
||||||
'MOSAIC_AGENT_MODEL',
|
'MOSAIC_AGENT_MODEL',
|
||||||
@@ -403,7 +402,6 @@ function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
|
|||||||
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
|
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
|
||||||
}
|
}
|
||||||
const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME');
|
const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME');
|
||||||
const gitIdentity = requiredGeneratedValue(values, 'MOSAIC_GIT_IDENTITY');
|
|
||||||
const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS');
|
const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS');
|
||||||
const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME');
|
const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME');
|
||||||
const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL');
|
const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL');
|
||||||
@@ -414,12 +412,6 @@ function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
|
|||||||
|
|
||||||
if (!AGENT_NAME.test(name))
|
if (!AGENT_NAME.test(name))
|
||||||
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', 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)) {
|
if (!POLICY_NAME.test(className)) {
|
||||||
throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className);
|
throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1405,7 +1405,6 @@ function generatedValues(
|
|||||||
): Readonly<Record<string, string>> {
|
): Readonly<Record<string, string>> {
|
||||||
return {
|
return {
|
||||||
MOSAIC_AGENT_NAME: agent.name,
|
MOSAIC_AGENT_NAME: agent.name,
|
||||||
MOSAIC_GIT_IDENTITY: agent.name,
|
|
||||||
MOSAIC_AGENT_CLASS: agent.className,
|
MOSAIC_AGENT_CLASS: agent.className,
|
||||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||||
MOSAIC_AGENT_MODEL: agent.model,
|
MOSAIC_AGENT_MODEL: agent.model,
|
||||||
|
|||||||
@@ -1,796 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
interface FakeEntry {
|
|
||||||
type: string;
|
|
||||||
customType?: string;
|
|
||||||
data?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SentMessage {
|
|
||||||
message: {
|
|
||||||
customType: string;
|
|
||||||
content: string;
|
|
||||||
display: boolean;
|
|
||||||
};
|
|
||||||
options?: {
|
|
||||||
triggerTurn?: boolean;
|
|
||||||
deliverAs?: 'steer' | 'followUp' | 'nextTurn';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FakeContext {
|
|
||||||
cwd: string;
|
|
||||||
ui: {
|
|
||||||
notifications: Array<{ message: string; level?: string }>;
|
|
||||||
statuses: Map<string, string | undefined>;
|
|
||||||
notify(message: string, level?: string): void;
|
|
||||||
setStatus(key: string, value: string | undefined): void;
|
|
||||||
};
|
|
||||||
sessionManager: {
|
|
||||||
getBranch(): FakeEntry[];
|
|
||||||
};
|
|
||||||
isIdle(): boolean;
|
|
||||||
hasPendingMessages(): boolean;
|
|
||||||
abort(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventHandler = (
|
|
||||||
event: Record<string, unknown>,
|
|
||||||
context: FakeContext,
|
|
||||||
) => unknown | Promise<unknown>;
|
|
||||||
|
|
||||||
type CommandHandler = (args: string, context: FakeContext) => unknown | Promise<unknown>;
|
|
||||||
|
|
||||||
interface FakeToolResult {
|
|
||||||
content: Array<{ type: string; text: string }>;
|
|
||||||
details?: unknown;
|
|
||||||
terminate?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FakeTool {
|
|
||||||
name: string;
|
|
||||||
execute(
|
|
||||||
toolCallId: string,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
onUpdate: undefined,
|
|
||||||
context: FakeContext,
|
|
||||||
): Promise<FakeToolResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GoalExtensionFactory {
|
|
||||||
(api: FakePiApi): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GoalExtensionModule {
|
|
||||||
default: GoalExtensionFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FakePiApi {
|
|
||||||
on(event: string, handler: EventHandler): void;
|
|
||||||
registerCommand(name: string, options: { description: string; handler: CommandHandler }): void;
|
|
||||||
registerTool(tool: FakeTool): void;
|
|
||||||
appendEntry(customType: string, data?: unknown): void;
|
|
||||||
sendMessage(message: SentMessage['message'], options?: SentMessage['options']): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGoalExtensionModule(value: unknown): value is GoalExtensionModule {
|
|
||||||
if (typeof value !== 'object' || value === null) return false;
|
|
||||||
return typeof Reflect.get(value, 'default') === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
const goalExtensionUrl = new URL('../../framework/runtime/pi/goal-extension.ts', import.meta.url)
|
|
||||||
.href;
|
|
||||||
const importedGoalExtension: unknown = await import(goalExtensionUrl);
|
|
||||||
if (!isGoalExtensionModule(importedGoalExtension)) {
|
|
||||||
throw new Error('Pi goal extension must export a default registration function');
|
|
||||||
}
|
|
||||||
const registerGoalExtension = importedGoalExtension.default;
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakePi {
|
|
||||||
readonly handlers = new Map<string, EventHandler[]>();
|
|
||||||
readonly commands = new Map<string, CommandHandler>();
|
|
||||||
readonly tools = new Map<string, FakeTool>();
|
|
||||||
readonly entries: FakeEntry[] = [];
|
|
||||||
readonly sentMessages: SentMessage[] = [];
|
|
||||||
readonly notifications: Array<{ message: string; level?: string }> = [];
|
|
||||||
readonly statuses = new Map<string, string | undefined>();
|
|
||||||
branch: FakeEntry[] = [];
|
|
||||||
idle = true;
|
|
||||||
pending = false;
|
|
||||||
abortCount = 0;
|
|
||||||
|
|
||||||
readonly context: FakeContext = {
|
|
||||||
cwd: '/tmp/project',
|
|
||||||
ui: {
|
|
||||||
notifications: this.notifications,
|
|
||||||
statuses: this.statuses,
|
|
||||||
notify: (message: string, level?: string): void => {
|
|
||||||
this.notifications.push({ message, level });
|
|
||||||
},
|
|
||||||
setStatus: (key: string, value: string | undefined): void => {
|
|
||||||
this.statuses.set(key, value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sessionManager: {
|
|
||||||
getBranch: (): FakeEntry[] => [...this.branch],
|
|
||||||
},
|
|
||||||
isIdle: (): boolean => this.idle,
|
|
||||||
hasPendingMessages: (): boolean => this.pending,
|
|
||||||
abort: (): void => {
|
|
||||||
this.abortCount += 1;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
readonly api: FakePiApi = {
|
|
||||||
on: (event: string, handler: EventHandler): void => {
|
|
||||||
this.handlers.set(event, [...(this.handlers.get(event) ?? []), handler]);
|
|
||||||
},
|
|
||||||
registerCommand: (
|
|
||||||
name: string,
|
|
||||||
options: { description: string; handler: CommandHandler },
|
|
||||||
): void => {
|
|
||||||
this.commands.set(name, options.handler);
|
|
||||||
},
|
|
||||||
registerTool: (tool: FakeTool): void => {
|
|
||||||
this.tools.set(tool.name, tool);
|
|
||||||
},
|
|
||||||
appendEntry: (customType: string, data?: unknown): void => {
|
|
||||||
const entry: FakeEntry = { type: 'custom', customType, data };
|
|
||||||
this.entries.push(entry);
|
|
||||||
this.branch.push(entry);
|
|
||||||
},
|
|
||||||
sendMessage: (message: SentMessage['message'], options?: SentMessage['options']): void => {
|
|
||||||
this.sentMessages.push({ message, options });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(initialBranch: FakeEntry[] = []) {
|
|
||||||
this.branch = [...initialBranch];
|
|
||||||
registerGoalExtension(this.api);
|
|
||||||
}
|
|
||||||
|
|
||||||
async emit(event: string, value: Record<string, unknown> = {}): Promise<unknown[]> {
|
|
||||||
const results: unknown[] = [];
|
|
||||||
for (const handler of this.handlers.get(event) ?? []) {
|
|
||||||
results.push(await handler(value, this.context));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async goal(args: string): Promise<void> {
|
|
||||||
const handler = this.commands.get('goal');
|
|
||||||
if (handler === undefined) throw new Error('/goal was not registered');
|
|
||||||
await handler(args, this.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async report(params: Record<string, unknown>): Promise<FakeToolResult> {
|
|
||||||
const tool = this.tools.get('mosaic_goal_report');
|
|
||||||
if (tool === undefined) throw new Error('mosaic_goal_report was not registered');
|
|
||||||
return await tool.execute('goal-report-1', params, undefined, undefined, this.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function latestGoalStateData(pi: FakePi): Record<string, unknown> {
|
|
||||||
for (let index = pi.entries.length - 1; index >= 0; index -= 1) {
|
|
||||||
const entry = pi.entries[index];
|
|
||||||
if (entry?.customType === 'mosaic-goal-state' && isRecord(entry.data)) return entry.data;
|
|
||||||
}
|
|
||||||
throw new Error('No persisted Mosaic goal state found');
|
|
||||||
}
|
|
||||||
|
|
||||||
function stateField(pi: FakePi, field: string): unknown {
|
|
||||||
return latestGoalStateData(pi)[field];
|
|
||||||
}
|
|
||||||
|
|
||||||
function activeGoalStatementFromContext(result: unknown): string {
|
|
||||||
if (!isRecord(result)) throw new Error('Context handler did not return an object');
|
|
||||||
const messages = result['messages'];
|
|
||||||
if (!Array.isArray(messages)) throw new Error('Context result did not include messages');
|
|
||||||
const goalMessage = messages.find(
|
|
||||||
(message: unknown): boolean =>
|
|
||||||
isRecord(message) && message['customType'] === 'mosaic-goal-context',
|
|
||||||
);
|
|
||||||
if (!isRecord(goalMessage) || typeof goalMessage['content'] !== 'string') {
|
|
||||||
throw new Error('Goal context message was not injected');
|
|
||||||
}
|
|
||||||
return goalMessage['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
vi.unstubAllEnvs();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mosaic Pi goal extension commands', () => {
|
|
||||||
it('shows help and handles controls safely when no goal exists', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal('');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('/goal set');
|
|
||||||
await pi.goal('status');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('No Mosaic goal is set');
|
|
||||||
|
|
||||||
for (const command of ['pause', 'resume', 'cancel']) {
|
|
||||||
await pi.goal(command);
|
|
||||||
expect(pi.notifications.at(-1)?.level).toBe('warning');
|
|
||||||
}
|
|
||||||
expect(pi.entries).toHaveLength(0);
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets, reports, pauses, resumes, and cancels a bounded goal', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal('set Deliver the local goal extension with tests');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(stateField(pi, 'statement')).toBe('Deliver the local goal extension with tests');
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
expect(pi.sentMessages[0]?.options?.triggerTurn).toBe(true);
|
|
||||||
|
|
||||||
await pi.goal('status');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('Deliver the local goal extension');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('active');
|
|
||||||
|
|
||||||
await pi.goal('pause');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('paused');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
|
|
||||||
await pi.goal('resume');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
|
|
||||||
await pi.goal('cancel');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('cancelled');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts /goal <statement> shorthand but refuses to replace an active goal', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal('First goal');
|
|
||||||
const firstGoalId = stateField(pi, 'goalId');
|
|
||||||
await pi.goal('set Second goal');
|
|
||||||
|
|
||||||
expect(stateField(pi, 'goalId')).toBe(firstGoalId);
|
|
||||||
expect(stateField(pi, 'statement')).toBe('First goal');
|
|
||||||
expect(pi.notifications.at(-1)?.level).toBe('warning');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('/goal cancel');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid phase transitions, aborts busy work, and supports clear as cancel', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Preserve transition safety');
|
|
||||||
|
|
||||||
await pi.goal('resume');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('cannot be resumed');
|
|
||||||
pi.idle = false;
|
|
||||||
await pi.goal('pause maintenance window');
|
|
||||||
expect(stateField(pi, 'stopReason')).toBe('maintenance window');
|
|
||||||
expect(pi.abortCount).toBe(1);
|
|
||||||
await pi.goal('pause');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('cannot be paused');
|
|
||||||
await pi.goal('clear');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('cancelled');
|
|
||||||
expect(pi.abortCount).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects empty and oversized goal statements without starting a run', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal('set');
|
|
||||||
await pi.goal(`set ${'x'.repeat(8_001)}`);
|
|
||||||
|
|
||||||
expect(pi.entries).toHaveLength(0);
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
expect(pi.notifications.at(-1)?.level).toBe('warning');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mosaic Pi goal lifecycle', () => {
|
|
||||||
it('injects one fresh active contract before every model context', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Keep the agent oriented');
|
|
||||||
|
|
||||||
const existingGoalContext = {
|
|
||||||
role: 'custom',
|
|
||||||
customType: 'mosaic-goal-context',
|
|
||||||
content: 'stale',
|
|
||||||
};
|
|
||||||
const existingContinuation = {
|
|
||||||
role: 'custom',
|
|
||||||
customType: 'mosaic-goal-continuation',
|
|
||||||
content: 'stale continuation',
|
|
||||||
};
|
|
||||||
const first = await pi.emit('context', {
|
|
||||||
messages: [existingGoalContext, existingContinuation],
|
|
||||||
});
|
|
||||||
const second = await pi.emit('context', { messages: [] });
|
|
||||||
|
|
||||||
expect(activeGoalStatementFromContext(first[0])).toContain('Keep the agent oriented');
|
|
||||||
expect(activeGoalStatementFromContext(first[0])).toContain('mosaic_goal_report');
|
|
||||||
expect(activeGoalStatementFromContext(first[0])).not.toContain('stale');
|
|
||||||
if (!isRecord(first[0]) || !Array.isArray(first[0]['messages'])) {
|
|
||||||
throw new Error('Expected filtered context messages');
|
|
||||||
}
|
|
||||||
expect(first[0]['messages']).toHaveLength(1);
|
|
||||||
expect(activeGoalStatementFromContext(second[0])).toContain('Keep the agent oriented');
|
|
||||||
|
|
||||||
await pi.goal('cancel');
|
|
||||||
expect(
|
|
||||||
await pi.emit('context', {
|
|
||||||
messages: [existingGoalContext, existingContinuation],
|
|
||||||
}),
|
|
||||||
).toEqual([{ messages: [] }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('lets an existing busy run adopt the goal and waits behind a pending message', async () => {
|
|
||||||
const busy = new FakePi();
|
|
||||||
busy.idle = false;
|
|
||||||
await busy.goal('set Join the current run safely');
|
|
||||||
expect(busy.sentMessages).toHaveLength(0);
|
|
||||||
|
|
||||||
busy.pending = true;
|
|
||||||
await busy.emit('agent_settled');
|
|
||||||
expect(busy.sentMessages).toHaveLength(0);
|
|
||||||
busy.pending = false;
|
|
||||||
busy.idle = true;
|
|
||||||
await busy.emit('agent_settled');
|
|
||||||
expect(busy.sentMessages).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('records every turn and continues once when an active run settles', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Finish all acceptance criteria');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
|
|
||||||
await pi.emit('turn_end', { turnIndex: 0, message: {}, toolResults: [] });
|
|
||||||
expect(stateField(pi, 'turnCount')).toBe(1);
|
|
||||||
expect(stateField(pi, 'lastCheckSource')).toBe('turn');
|
|
||||||
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
expect(pi.sentMessages[0]?.message.content).toContain('Goal remains active');
|
|
||||||
|
|
||||||
await pi.emit('agent_start');
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('requires two consecutive evidence-bearing achievement reports', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Prove the feature works');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
|
|
||||||
const first = await pi.report({
|
|
||||||
status: 'achieved',
|
|
||||||
summary: 'Focused tests pass',
|
|
||||||
evidence: ['pnpm test: 12 passed'],
|
|
||||||
});
|
|
||||||
expect(first.terminate).toBe(true);
|
|
||||||
expect(stateField(pi, 'phase')).toBe('verifying');
|
|
||||||
expect(stateField(pi, 'verificationPasses')).toBe(1);
|
|
||||||
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
expect(pi.sentMessages[0]?.message.content).toContain('verification pass');
|
|
||||||
|
|
||||||
await pi.emit('agent_start');
|
|
||||||
const second = await pi.report({
|
|
||||||
status: 'achieved',
|
|
||||||
summary: 'Independent recheck confirms completion',
|
|
||||||
evidence: ['rerun: 12 passed', 'framework path verified'],
|
|
||||||
});
|
|
||||||
expect(second.terminate).toBe(true);
|
|
||||||
expect(stateField(pi, 'phase')).toBe('achieved');
|
|
||||||
expect(stateField(pi, 'verificationPasses')).toBe(2);
|
|
||||||
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects malformed progress reports and reports submitted without an active goal', async () => {
|
|
||||||
const noGoal = new FakePi();
|
|
||||||
await expect(
|
|
||||||
noGoal.report({ status: 'continue', summary: 'work', evidence: [] }),
|
|
||||||
).rejects.toThrow(/No active Mosaic goal/);
|
|
||||||
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Validate report boundaries');
|
|
||||||
const invalidReports: Record<string, unknown>[] = [
|
|
||||||
{},
|
|
||||||
{ status: 'invalid', summary: 'work', evidence: [] },
|
|
||||||
{ status: 'continue', evidence: [] },
|
|
||||||
{ status: 'continue', summary: ' ', evidence: [] },
|
|
||||||
{ status: 'continue', summary: 'x'.repeat(2_001), evidence: [] },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: 'not-an-array' },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: Array.from({ length: 21 }, () => 'x') },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: [4] },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: [''] },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: ['x'.repeat(1_001)] },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: [], nextStep: 4 },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: [], nextStep: ' ' },
|
|
||||||
{ status: 'continue', summary: 'work', evidence: [], nextStep: 'x'.repeat(2_001) },
|
|
||||||
];
|
|
||||||
for (const report of invalidReports) {
|
|
||||||
await expect(pi.report(report)).rejects.toThrow();
|
|
||||||
}
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects an achievement claim without evidence', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Require evidence');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
pi.report({ status: 'achieved', summary: 'Trust me', evidence: [] }),
|
|
||||||
).rejects.toThrow(/evidence/i);
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redacts credential-shaped goal and report text before persistence or display', async () => {
|
|
||||||
const githubToken = `ghp_${'a'.repeat(32)}`;
|
|
||||||
const anthropicKey = `sk-ant-api03-${'b'.repeat(40)}`;
|
|
||||||
const bearerToken = 'header.payload.signature-canary';
|
|
||||||
const databaseUrl = 'postgresql://mosaic:[email protected]/mosaic';
|
|
||||||
const password = 'password-canary';
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal(`set Rotate ${githubToken} without retaining it`);
|
|
||||||
const context = await pi.emit('context', { messages: [] });
|
|
||||||
expect(activeGoalStatementFromContext(context[0])).not.toContain(githubToken);
|
|
||||||
|
|
||||||
const result = await pi.report({
|
|
||||||
status: 'achieved',
|
|
||||||
summary: `Validated ${anthropicKey}`,
|
|
||||||
evidence: [`Authorization: Bearer ${bearerToken}`, `DATABASE_URL=${databaseUrl}`],
|
|
||||||
nextStep: `password=${password}`,
|
|
||||||
});
|
|
||||||
await pi.goal('status');
|
|
||||||
|
|
||||||
const persisted = JSON.stringify(latestGoalStateData(pi));
|
|
||||||
const displayed = pi.notifications.at(-1)?.message ?? '';
|
|
||||||
const toolOutput = JSON.stringify(result);
|
|
||||||
for (const secret of [githubToken, anthropicKey, bearerToken, databaseUrl, password]) {
|
|
||||||
expect(persisted).not.toContain(secret);
|
|
||||||
expect(displayed).not.toContain(secret);
|
|
||||||
expect(toolOutput).not.toContain(secret);
|
|
||||||
}
|
|
||||||
expect(persisted).toContain('[REDACTED-SECRET]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves ordinary typed fields that resemble sensitive assignment names', async () => {
|
|
||||||
const typedFields = 'token: string, password: boolean, secret: false';
|
|
||||||
const pi = new FakePi();
|
|
||||||
|
|
||||||
await pi.goal(`set Preserve TypeScript fields: ${typedFields}`);
|
|
||||||
await pi.report({
|
|
||||||
status: 'continue',
|
|
||||||
summary: `Schema still contains ${typedFields}`,
|
|
||||||
evidence: [`interface Config { ${typedFields} }`],
|
|
||||||
nextStep: `Keep ${typedFields} unchanged`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stateField(pi, 'statement')).toContain(typedFields);
|
|
||||||
expect(JSON.stringify(stateField(pi, 'lastReport'))).toContain(typedFields);
|
|
||||||
expect(JSON.stringify(latestGoalStateData(pi))).not.toContain('[REDACTED-SECRET]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stops autonomous continuation when the max-turn limit is reached', async () => {
|
|
||||||
vi.stubEnv('MOSAIC_GOAL_MAX_TURNS', '2');
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Bound this run');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
|
|
||||||
await pi.emit('turn_end', { turnIndex: 0, message: {}, toolResults: [] });
|
|
||||||
await pi.emit('turn_end', { turnIndex: 1, message: {}, toolResults: [] });
|
|
||||||
|
|
||||||
expect(stateField(pi, 'phase')).toBe('exhausted');
|
|
||||||
expect(pi.abortCount).toBe(1);
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resets the no-progress sequence when a continuation report changes', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Track changing progress');
|
|
||||||
|
|
||||||
await pi.report({
|
|
||||||
status: 'continue',
|
|
||||||
summary: 'First checkpoint',
|
|
||||||
evidence: ['file A changed'],
|
|
||||||
nextStep: 'Run focused tests',
|
|
||||||
});
|
|
||||||
await pi.report({
|
|
||||||
status: 'continue',
|
|
||||||
summary: 'Second checkpoint',
|
|
||||||
evidence: ['focused tests passed'],
|
|
||||||
nextStep: 'Review the diff',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(stateField(pi, 'noProgressReports')).toBe(1);
|
|
||||||
await pi.goal('status');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('Next step: Review the diff');
|
|
||||||
expect(pi.notifications.at(-1)?.message).toContain('focused tests passed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stops after a bounded number of identical no-progress reports', async () => {
|
|
||||||
vi.stubEnv('MOSAIC_GOAL_MAX_NO_PROGRESS', '2');
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Detect stalled work');
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
status: 'continue',
|
|
||||||
summary: 'No change yet',
|
|
||||||
evidence: ['same observation'],
|
|
||||||
nextStep: 'Try again',
|
|
||||||
};
|
|
||||||
await pi.report(report);
|
|
||||||
await pi.report(report);
|
|
||||||
|
|
||||||
expect(stateField(pi, 'phase')).toBe('exhausted');
|
|
||||||
expect(stateField(pi, 'noProgressReports')).toBe(2);
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a goal report mixed with another tool result in the same turn', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Require a sole final report');
|
|
||||||
await pi.report({
|
|
||||||
status: 'achieved',
|
|
||||||
summary: 'Premature mixed claim',
|
|
||||||
evidence: ['one observation'],
|
|
||||||
});
|
|
||||||
|
|
||||||
await pi.emit('turn_end', {
|
|
||||||
turnIndex: 0,
|
|
||||||
message: {},
|
|
||||||
toolResults: [{ toolName: 'mosaic_goal_report' }, { toolName: 'read' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(stateField(pi, 'verificationPasses')).toBe(0);
|
|
||||||
expect(stateField(pi, 'lastCheckOutcome')).toBe('mixed-goal-report-rejected');
|
|
||||||
expect(pi.notifications.at(-1)?.level).toBe('warning');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marks blocked reports terminal until the operator resumes', async () => {
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Stop on a real blocker');
|
|
||||||
|
|
||||||
await pi.report({
|
|
||||||
status: 'blocked',
|
|
||||||
summary: 'Missing required access',
|
|
||||||
evidence: ['provider returned 403'],
|
|
||||||
});
|
|
||||||
expect(stateField(pi, 'phase')).toBe('blocked');
|
|
||||||
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
|
|
||||||
await pi.goal('resume');
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(stateField(pi, 'turnCount')).toBe(0);
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mosaic Pi goal compaction and restoration', () => {
|
|
||||||
it('resets provisional verification and defers manual-compaction continuation until idle', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Survive compaction');
|
|
||||||
await pi.report({
|
|
||||||
status: 'achieved',
|
|
||||||
summary: 'Initial claim',
|
|
||||||
evidence: ['focused test passed'],
|
|
||||||
});
|
|
||||||
expect(stateField(pi, 'phase')).toBe('verifying');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
|
|
||||||
await pi.emit('session_compact', { reason: 'manual', willRetry: false });
|
|
||||||
expect(stateField(pi, 'phase')).toBe('active');
|
|
||||||
expect(stateField(pi, 'verificationPasses')).toBe(0);
|
|
||||||
expect(stateField(pi, 'compactionCount')).toBe(1);
|
|
||||||
expect(stateField(pi, 'lastCheckSource')).toBe('compact');
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
expect(pi.sentMessages[0]?.message.content).toContain('compaction');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not re-enter an active automatic compaction and relies on the settled backstop', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Avoid compaction races');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
pi.idle = false;
|
|
||||||
|
|
||||||
await pi.emit('session_compact', { reason: 'threshold', willRetry: false });
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
|
|
||||||
pi.idle = true;
|
|
||||||
await pi.emit('agent_settled');
|
|
||||||
expect(pi.sentMessages).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('restores branch-specific state on session start and tree navigation', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const source = new FakePi();
|
|
||||||
await source.goal('set Restore this exact branch goal');
|
|
||||||
const activeState = latestGoalStateData(source);
|
|
||||||
|
|
||||||
const restored = new FakePi([
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: activeState },
|
|
||||||
]);
|
|
||||||
await restored.emit('session_start', { reason: 'resume' });
|
|
||||||
expect(restored.statuses.get('mosaic-goal')).toContain('active');
|
|
||||||
const context = await restored.emit('context', { messages: [] });
|
|
||||||
expect(activeGoalStatementFromContext(context[0])).toContain('Restore this exact branch goal');
|
|
||||||
|
|
||||||
await restored.goal('pause');
|
|
||||||
const pausedState = latestGoalStateData(restored);
|
|
||||||
restored.branch = [{ type: 'custom', customType: 'mosaic-goal-state', data: pausedState }];
|
|
||||||
await restored.emit('session_tree', {});
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
expect(stateField(restored, 'phase')).toBe('paused');
|
|
||||||
expect(restored.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('restores only fully valid persisted states and ignores malformed entries', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const source = new FakePi();
|
|
||||||
await source.goal('set Validate persisted branch state');
|
|
||||||
await source.report({
|
|
||||||
status: 'continue',
|
|
||||||
summary: 'Valid report',
|
|
||||||
evidence: ['valid evidence'],
|
|
||||||
nextStep: 'Continue validation',
|
|
||||||
});
|
|
||||||
const valid = latestGoalStateData(source);
|
|
||||||
|
|
||||||
const validRestore = new FakePi([
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: valid },
|
|
||||||
]);
|
|
||||||
await validRestore.emit('session_start', { reason: 'resume' });
|
|
||||||
expect(stateField(validRestore, 'statement')).toBe('Validate persisted branch state');
|
|
||||||
await validRestore.emit('session_shutdown', { reason: 'reload' });
|
|
||||||
|
|
||||||
const validReport = latestGoalStateData(source)['lastReport'];
|
|
||||||
if (!isRecord(validReport)) throw new Error('Expected a valid persisted report fixture');
|
|
||||||
const integerFields = [
|
|
||||||
'turnCount',
|
|
||||||
'reportCount',
|
|
||||||
'verificationPasses',
|
|
||||||
'requiredVerificationPasses',
|
|
||||||
'noProgressReports',
|
|
||||||
'maxTurns',
|
|
||||||
'maxNoProgressReports',
|
|
||||||
'compactionCount',
|
|
||||||
];
|
|
||||||
const corruptions: Array<(state: Record<string, unknown>) => unknown> = [
|
|
||||||
(): unknown => null,
|
|
||||||
(state): unknown => ({ ...state, version: 99 }),
|
|
||||||
(state): unknown => ({ ...state, goalId: '' }),
|
|
||||||
(state): unknown => ({ ...state, statement: '' }),
|
|
||||||
(state): unknown => ({ ...state, statement: 'x'.repeat(8_001) }),
|
|
||||||
(state): unknown => ({ ...state, phase: 'unknown' }),
|
|
||||||
(state): unknown => ({ ...state, lastCheckSource: 'unknown' }),
|
|
||||||
(state): unknown => ({ ...state, startedAt: 4 }),
|
|
||||||
(state): unknown => ({ ...state, lastCheckAt: 4 }),
|
|
||||||
(state): unknown => ({ ...state, lastReport: null }),
|
|
||||||
(state): unknown => ({ ...state, lastProgressFingerprint: 4 }),
|
|
||||||
(state): unknown => ({ ...state, stopReason: 4 }),
|
|
||||||
...integerFields.map((field: string) => (state: Record<string, unknown>): unknown => ({
|
|
||||||
...state,
|
|
||||||
[field]: -1,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
corruptions.push(
|
|
||||||
(state: Record<string, unknown>): unknown => ({ ...state, maxTurns: 501 }),
|
|
||||||
(state: Record<string, unknown>): unknown => ({ ...state, maxNoProgressReports: 101 }),
|
|
||||||
(state: Record<string, unknown>): unknown => ({ ...state, requiredVerificationPasses: 3 }),
|
|
||||||
);
|
|
||||||
const reportCorruptions: Array<Record<string, unknown>> = [
|
|
||||||
{ ...validReport, status: 'bad' },
|
|
||||||
{ ...validReport, summary: '' },
|
|
||||||
{ ...validReport, evidence: 'bad' },
|
|
||||||
{ ...validReport, evidence: [4] },
|
|
||||||
{ ...validReport, fingerprint: '' },
|
|
||||||
{ ...validReport, reportedAt: '' },
|
|
||||||
{ ...validReport, nextStep: 4 },
|
|
||||||
];
|
|
||||||
for (const corruptReport of reportCorruptions) {
|
|
||||||
corruptions.push((state: Record<string, unknown>): unknown => ({
|
|
||||||
...state,
|
|
||||||
lastReport: corruptReport,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const corrupt of corruptions) {
|
|
||||||
const candidate = corrupt(structuredClone(valid));
|
|
||||||
const restored = new FakePi([
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: candidate },
|
|
||||||
]);
|
|
||||||
await restored.emit('session_start', { reason: 'resume' });
|
|
||||||
expect(restored.statuses.get('mosaic-goal')).toBeUndefined();
|
|
||||||
expect(await restored.emit('context', { messages: [] })).toEqual([undefined]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const failClosed = new FakePi([
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: valid },
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: { ...valid, version: 99 } },
|
|
||||||
]);
|
|
||||||
await failClosed.emit('session_start', { reason: 'resume' });
|
|
||||||
expect(failClosed.statuses.get('mosaic-goal')).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails closed instead of reusing credential-bearing legacy branch state', async () => {
|
|
||||||
const source = new FakePi();
|
|
||||||
await source.goal('set Build a valid restore fixture');
|
|
||||||
const cleanState = latestGoalStateData(source);
|
|
||||||
const legacyState = structuredClone(cleanState);
|
|
||||||
legacyState['statement'] = `Legacy secret ghp_${'z'.repeat(32)}`;
|
|
||||||
|
|
||||||
const restored = new FakePi([
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: legacyState },
|
|
||||||
{ type: 'custom', customType: 'mosaic-goal-state', data: cleanState },
|
|
||||||
]);
|
|
||||||
await restored.emit('session_start', { reason: 'resume' });
|
|
||||||
|
|
||||||
expect(restored.statuses.get('mosaic-goal')).toBeUndefined();
|
|
||||||
expect(await restored.emit('context', { messages: [] })).toEqual([undefined]);
|
|
||||||
expect(restored.notifications.at(-1)?.level).toBe('warning');
|
|
||||||
expect(restored.notifications.at(-1)?.message).toContain('was not restored');
|
|
||||||
expect(restored.entries).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('schedules an active tree-restored goal and preserves terminal state through compaction', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const source = new FakePi();
|
|
||||||
await source.goal('set Restore active tree work');
|
|
||||||
const active = latestGoalStateData(source);
|
|
||||||
|
|
||||||
const restored = new FakePi();
|
|
||||||
restored.branch = [{ type: 'custom', customType: 'mosaic-goal-state', data: active }];
|
|
||||||
await restored.emit('session_tree', {});
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
expect(restored.sentMessages).toHaveLength(1);
|
|
||||||
expect(restored.sentMessages[0]?.message.content).toContain('tree navigation');
|
|
||||||
|
|
||||||
await restored.goal('cancel');
|
|
||||||
await restored.emit('session_compact', { reason: 'manual', willRetry: false });
|
|
||||||
expect(stateField(restored, 'phase')).toBe('cancelled');
|
|
||||||
expect(stateField(restored, 'compactionCount')).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cancels deferred continuation when the session shuts down', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const pi = new FakePi();
|
|
||||||
await pi.goal('set Do not leak a stale timer');
|
|
||||||
pi.sentMessages.length = 0;
|
|
||||||
|
|
||||||
await pi.emit('session_compact', { reason: 'manual', willRetry: false });
|
|
||||||
await pi.emit('session_shutdown', { reason: 'reload' });
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
|
|
||||||
expect(pi.sentMessages).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -72,9 +72,8 @@ elif [[ -n "$DATA_DIR" ]]; then
|
|||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
[[ -z "$file" ]] && continue
|
[[ -z "$file" ]] && continue
|
||||||
done_total=$((done_total + 1))
|
done_total=$((done_total + 1))
|
||||||
history_rc=0
|
if git -C "$DATA_DIR" log --since="${WINDOW_DAYS} days ago" --pretty='%s' -- "$file" 2>/dev/null \
|
||||||
history="$(git -C "$DATA_DIR" log --since="${WINDOW_DAYS} days ago" --pretty='%s' -- "$file" 2>/dev/null)" || history_rc=$?
|
| grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo'; then
|
||||||
if [[ "$history_rc" -eq 0 ]] && grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo' <<<"$history"; then
|
|
||||||
detectable=$((detectable + 1))
|
detectable=$((detectable + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find "$DATA_DIR" -type f -name '*.json' 2>/dev/null)
|
done < <(find "$DATA_DIR" -type f -name '*.json' 2>/dev/null)
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ for line in "${LINES[@]}"; do
|
|||||||
# - build/test/lint/type/ci signals → CI would have caught it
|
# - build/test/lint/type/ci signals → CI would have caught it
|
||||||
# - security/auth/permission/data/migration → human review would flag it
|
# - security/auth/permission/data/migration → human review would flag it
|
||||||
# - everything else (logic/UX/assumption/edge) → only-self-reflection bucket
|
# - everything else (logic/UX/assumption/edge) → only-self-reflection bucket
|
||||||
if grep -qiE 'test|lint|type|build|ci|compile|typo' <<<"$subj"; then
|
if printf '%s' "$subj" | grep -qiE 'test|lint|type|build|ci|compile|typo'; then
|
||||||
ci=$((ci + 1))
|
ci=$((ci + 1))
|
||||||
elif grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection' <<<"$subj"; then
|
elif printf '%s' "$subj" | grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection'; then
|
||||||
human=$((human + 1))
|
human=$((human + 1))
|
||||||
else
|
else
|
||||||
selfonly=$((selfonly + 1))
|
selfonly=$((selfonly + 1))
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
[
|
|
||||||
"tools/matrix-presence-harness/run.sh:TSX_CLI=\"$(ls -d \"${REPO}\"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs 2>/dev/null | head -1)\"",
|
|
||||||
"tools/e2e-install-test.sh:if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then",
|
|
||||||
"tools/install.sh:EXTRACTED_DIR=\"$(find \"$WORK_DIR\" -maxdepth 1 -mindepth 1 -type d | head -1)\"",
|
|
||||||
"scripts/analysis/reflect-board-history.sh:if git -C \"$DATA_DIR\" log --since=\"${WINDOW_DAYS} days ago\" --pretty='%s' -- \"$file\" 2>/dev/null | grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo'; then",
|
|
||||||
"scripts/analysis/reflect-git-history.sh:if printf '%s' \"$subj\" | grep -qiE 'test|lint|type|build|ci|compile|typo'; then",
|
|
||||||
"scripts/analysis/reflect-git-history.sh:elif printf '%s' \"$subj\" | grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection'; then",
|
|
||||||
"packages/mosaic/framework/tools/authentik/user-create.sh:group_pk=$(echo \"$group_response\" | jq -r \".results[] | select(.name == \\\"$GROUP\\\") | .pk\" | head -1)",
|
|
||||||
"packages/mosaic/framework/tools/git/mutate-push-guard.sh:PROSE_LO=\"$(grep -n '^usage() {' \"$BAK\" | head -1 | cut -d: -f1)\"",
|
|
||||||
"packages/mosaic/framework/tools/orchestrator/session-resume.sh:echo \"$dirty_files\" | head -20 | while IFS= read -r line; do",
|
|
||||||
"packages/mosaic/framework/tools/prdy/prdy-status.sh:if echo \"$PRD_CONTENT\" | grep -qiE \"$pattern\"; then",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'auth|login|session|token|permission|rbac|credential|secret'; then echo auth; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'migration|prisma|schema|\\.sql|entity|repository|seed'; then echo data; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'docker|\\.woodpecker|compose|traefik|deploy|helm|k8s|terraform'; then echo infra; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'package\\.json|tsconfig|turbo\\.json|pnpm-|\\.config\\.|eslint|vite'; then echo build; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.tsx|\\.css|components/|apps/web/'; then echo ui; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.spec\\.|\\.test\\.|__tests__/'; then echo test; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.md$|docs/'; then echo docs; return; fi",
|
|
||||||
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:FILE_PATH=$(echo \"$JSON_INPUT\" | grep -o '\"file_path\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | sed 's/.*\"\\([^\"]*\\)\"$/\\1/' | head -1)",
|
|
||||||
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:RELEVANT=$(echo \"$OUTPUT\" | grep -A2 \"$BASENAME\" 2>/dev/null || echo \"$OUTPUT\" | head -20)",
|
|
||||||
"packages/mosaic/framework/tools/tmux/send-message.sh:if printf '%s' \"$pane\" | grep -qF \"$QUEUED_RE\"; then",
|
|
||||||
"packages/mosaic/framework/tools/tmux/send-message.sh:if [ -n \"$snippet\" ] && printf '%s' \"$promptline\" | grep -qF \"$snippet\"; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/detector.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'",
|
|
||||||
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_sha\" ] && ! printf '%s' \"$snap_sha\" | grep -Eq '^[0-9a-f]{7,64}$'; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_ts\" ] && ! printf '%s' \"$snap_ts\" | grep -Eq '^[0-9]{1,12}$'; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/digest.sh:olabel=\"$(_locator_line \"$oloc\" | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/reconcile.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'"
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
[
|
|
||||||
"packages/mosaic/framework/systemd/user/test-fleet-units.sh:if tmux -L \"$TEST_SOCKET\" show-environment -g LD_PRELOAD 2>/dev/null | grep -q '^LD_PRELOAD='; then",
|
|
||||||
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
|
|
||||||
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
|
|
||||||
"packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh:contains() { printf '%s\\n' \"$1\" | grep -qx \"$2\"; }",
|
|
||||||
"packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
|
|
||||||
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-r, --repo'",
|
|
||||||
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-H, --host'",
|
|
||||||
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$codex_run_prompt\" | head -n1)\" == \"Now initiating Orchestrator mode...\" ]]; then pass_case \"codex run prompt first line is mode declaration\"; else fail_case \"codex run prompt first line is mode declaration\"; fi",
|
|
||||||
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$claude_run_prompt\" | head -n1)\" == \"## Continuation Mission\" ]]; then pass_case \"claude run prompt remains continuation prompt format\"; else fail_case \"claude run prompt remains continuation prompt format\"; fi",
|
|
||||||
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -qi \"dry run\" || note \"dry-run did not announce itself\"",
|
|
||||||
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -q \"would roll\" || note \"dry-run did not report a plan\"",
|
|
||||||
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh:find \"$1/mosaic/backups\" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null | LC_ALL=C sort -r | head -1",
|
|
||||||
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:SNAP_E=\"$(grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTG\" | head -1)\"",
|
|
||||||
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTH\" 2>/dev/null | head -1 | while read -r s; do rm -rf \"$s\"; done",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"named socket hello\" || fail \"send-message.sh did not deliver to named socket\"",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"named socket hello\"; then",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"[tester:source ->\" || fail \"agent-send.sh did not include preamble\"",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"agent socket hello\" || fail \"agent-send.sh did not deliver to named socket\"",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"agent socket hello\"; then",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${i}-END\" || fail \"concurrent send dropped payload for pane conc-$i\"",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${j}-END\"; then",
|
|
||||||
"packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh:if [ \"$rc\" -eq 0 ] && printf '%s' \"$out\" | grep -qF \"✓ delivered\"; then"
|
|
||||||
]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
[
|
|
||||||
"packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh:fixture_line=\"$(has_match -F \"\\\"id\\\":\\\"$enum_id\\\"\" \"$self\" | has_match -F '\"observed_seq\":5' | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:pre_seq=\"$(jq -r 'select(.locators.kind == \"preimage\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:src_seq=\"$(jq -r 'select(.locators.kind == \"repo\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:pre_seq=\"$(jq -r 'select(.locators.kind == \"preimage\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:enum_seq=\"$(jq -r 'select(.locators.reconciled == true) | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:if ! sed -n \"${ln}p\" \"$f\" | grep -Eq 'has_match|count_lines'; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$outA\" | grep -q 'mini-a: OK' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$outB\" | grep -q 'mini-b: OK' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:check C1 1 \"rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff \"$TMP/expected-c1\" \"$TMP/got-c1\" 2>&1 | head -n 10 | tr '\\n' ' ')\"",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'mini-a: OK' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'mini-a: FAILED' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"WAKE-ASSERT ARMED: forcing real grep error at $site\" &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"WAKE-ASSERT ABORT\" &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"$site\" &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"grep exit 2\" &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'mini-a: OK' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'WAKE-ASSERT ARMED'; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'REACHED-PAST-INIT' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated'; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'all invariants passed' &&",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:diff \"$TMP/expected.txt\" \"$TMP/static.txt\" | head -n 20 | sed 's/^/ /'",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:if printf '%s\\n' \"$out\" | grep -Eq \"$(sentinel_for \"$s\")\"; then",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -q \"WAKE-ASSERT ARMED: forcing real grep error at $site\" ||",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -q \"WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit\" ||",
|
|
||||||
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -Eq \"$(sentinel_for \"$f\")\" || rc_sent=$?"
|
|
||||||
]
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import { spawnSync } from 'node:child_process';
|
|
||||||
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import test from 'node:test';
|
|
||||||
|
|
||||||
const ROOT = new URL('../', import.meta.url);
|
|
||||||
const EXPECTED_BASELINE_SITES = 26;
|
|
||||||
const EXPECTED_TEST_BASELINE_SITES = 22;
|
|
||||||
const EXPECTED_WAKE_BASELINE_SITES = 26;
|
|
||||||
const TARGETS = [
|
|
||||||
'tools/matrix-presence-harness/run.sh',
|
|
||||||
'tools/e2e-install-test.sh',
|
|
||||||
'tools/install.sh',
|
|
||||||
'scripts/agent/session-start.sh',
|
|
||||||
'scripts/analysis/reflect-board-history.sh',
|
|
||||||
'scripts/analysis/reflect-git-history.sh',
|
|
||||||
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
|
|
||||||
'packages/mosaic/framework/tools/authentik/user-create.sh',
|
|
||||||
'packages/mosaic/framework/tools/git/mutate-push-guard.sh',
|
|
||||||
'packages/mosaic/framework/tools/orchestrator/session-resume.sh',
|
|
||||||
'packages/mosaic/framework/tools/prdy/prdy-status.sh',
|
|
||||||
'packages/mosaic/framework/tools/qa/reflect-stop-hook.sh',
|
|
||||||
'packages/mosaic/framework/tools/qa/typecheck-hook.sh',
|
|
||||||
'packages/mosaic/framework/tools/tmux/send-message.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/detector.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/digest.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/reconcile.sh',
|
|
||||||
'packages/mosaic/framework/systemd/user/test-fleet-units.sh',
|
|
||||||
'packages/mosaic/framework/tools/git/test-issue-comment-readback.sh',
|
|
||||||
'packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh',
|
|
||||||
'packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh',
|
|
||||||
'packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh',
|
|
||||||
'packages/mosaic/framework/tools/orchestrator/smoke-test.sh',
|
|
||||||
'packages/mosaic/framework/tools/orchestrator/test-board-roll.sh',
|
|
||||||
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh',
|
|
||||||
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh',
|
|
||||||
'packages/mosaic/framework/tools/tmux/test-send-message-socket.sh',
|
|
||||||
'packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/test-wake-preimage.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh',
|
|
||||||
'packages/mosaic/framework/tools/wake/validate-973/validate-973.sh',
|
|
||||||
];
|
|
||||||
|
|
||||||
// These statuses are explicitly non-load-bearing or unreachable at designed input.
|
|
||||||
// They remain inventoried until the final #1099 tranche records every verdict.
|
|
||||||
const ACCEPTED = [
|
|
||||||
['tools/install.sh', 'mosaic-bak-', '|| true'],
|
|
||||||
['tools/install.sh', 'mosaicstack-mosaic-*.tgz', 'head -1'],
|
|
||||||
['tools/install.sh', 'mosaicstack-gateway-*.tgz', 'head -1'],
|
|
||||||
['scripts/agent/session-start.sh', 'docs/scratchpads/*.md', '|| true'],
|
|
||||||
[
|
|
||||||
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
|
|
||||||
'docs/scratchpads/*.md',
|
|
||||||
'|| true',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const earlyExit =
|
|
||||||
/(?<!\|)\|(?!\|)[^;\n]*(?:grep\b[^;\n]*(?:-[A-Za-z]*q|--quiet|-m\s*1)|head\b(?:\s|$))/;
|
|
||||||
|
|
||||||
function scan(sources) {
|
|
||||||
const found = [];
|
|
||||||
for (const [file, rawSource] of sources) {
|
|
||||||
const source = rawSource.replace(/\\\n\s*/g, ' ');
|
|
||||||
for (const rawLine of source.split('\n')) {
|
|
||||||
const line = rawLine.trim();
|
|
||||||
if (!earlyExit.test(line)) continue;
|
|
||||||
const accepted = ACCEPTED.some(
|
|
||||||
([acceptedFile, ...fragments]) =>
|
|
||||||
acceptedFile === file && fragments.every((item) => line.includes(item)),
|
|
||||||
);
|
|
||||||
if (!accepted) found.push(`${file}:${line}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function currentSources() {
|
|
||||||
return Promise.all(
|
|
||||||
TARGETS.map(async (file) => [file, await readFile(new URL(file, ROOT), 'utf8')]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertBaselineFixture(file, expectedCount, expectedUnique = expectedCount) {
|
|
||||||
const baseline = JSON.parse(await readFile(new URL(file, ROOT), 'utf8'));
|
|
||||||
assert.equal(baseline.length, expectedCount);
|
|
||||||
assert.equal(new Set(baseline).size, expectedUnique);
|
|
||||||
const fixtureSources = baseline.map((site) => {
|
|
||||||
const separator = site.indexOf(':');
|
|
||||||
assert.ok(separator > 0, `invalid baseline site: ${site}`);
|
|
||||||
return [site.slice(0, separator), site.slice(separator + 1)];
|
|
||||||
});
|
|
||||||
assert.deepEqual(scan(fixtureSources), baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
test('the registered runtime baseline denominator is exactly 26 unsafe sites', async () => {
|
|
||||||
await assertBaselineFixture(
|
|
||||||
'scripts/fixtures/pipefail-early-exit-baseline.json',
|
|
||||||
EXPECTED_BASELINE_SITES,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the registered test baseline denominator is exactly 22 unsafe sites', async () => {
|
|
||||||
await assertBaselineFixture(
|
|
||||||
'scripts/fixtures/pipefail-early-exit-test-baseline.json',
|
|
||||||
EXPECTED_TEST_BASELINE_SITES,
|
|
||||||
21,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the registered wake baseline denominator is exactly 26 unsafe sites', async () => {
|
|
||||||
await assertBaselineFixture(
|
|
||||||
'scripts/fixtures/pipefail-early-exit-wake-baseline.json',
|
|
||||||
EXPECTED_WAKE_BASELINE_SITES,
|
|
||||||
25,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('load-bearing pipefail paths do not pipe into early-exiting consumers', async () => {
|
|
||||||
assert.deepEqual(scan(await currentSources()), []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gateway verify capability preserves the complete help-probe truth table', async () => {
|
|
||||||
const directory = await mkdtemp(path.join(tmpdir(), 'gateway-help-probe-'));
|
|
||||||
const mosaic = path.join(directory, 'mosaic');
|
|
||||||
const probe = new URL('tools/e2e-gateway-verify-supported.sh', ROOT).pathname;
|
|
||||||
try {
|
|
||||||
await writeFile(
|
|
||||||
mosaic,
|
|
||||||
'#!/usr/bin/env bash\nprintf \'%s\\n\' "${MOCK_HELP_OUTPUT:-}"\nexit "${MOCK_HELP_RC:-0}"\n',
|
|
||||||
);
|
|
||||||
await chmod(mosaic, 0o755);
|
|
||||||
const run = (rc, output) =>
|
|
||||||
spawnSync('bash', [probe], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
PATH: `${directory}:${process.env.PATH}`,
|
|
||||||
MOCK_HELP_RC: String(rc),
|
|
||||||
MOCK_HELP_OUTPUT: output,
|
|
||||||
},
|
|
||||||
}).status;
|
|
||||||
|
|
||||||
assert.equal(run(0, 'commands: verify'), 0);
|
|
||||||
assert.equal(run(0, 'commands: install'), 1);
|
|
||||||
assert.equal(run(1, 'commands: verify'), 1);
|
|
||||||
} finally {
|
|
||||||
await rm(directory, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('wake JSONL selectors take the first match across the complete input stream', async () => {
|
|
||||||
const source = await readFile(
|
|
||||||
new URL('packages/mosaic/framework/tools/wake/test-wake-preimage.sh', ROOT),
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
assert.equal((source.match(/jq -nr 'first\(inputs \| select\(/g) ?? []).length, 4);
|
|
||||||
|
|
||||||
const directory = await mkdtemp(path.join(tmpdir(), 'wake-jsonl-first-'));
|
|
||||||
const input = path.join(directory, 'pending.jsonl');
|
|
||||||
const filter = 'first(inputs | select(.locators.kind == "preimage") | .observed_seq) // empty';
|
|
||||||
try {
|
|
||||||
await writeFile(
|
|
||||||
input,
|
|
||||||
'{"locators":{"kind":"repo"},"observed_seq":1}\n' +
|
|
||||||
'{"locators":{"kind":"preimage"},"observed_seq":4}\n' +
|
|
||||||
'{"locators":{"kind":"preimage"},"observed_seq":9}\n',
|
|
||||||
);
|
|
||||||
let result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
|
|
||||||
assert.equal(result.status, 0, result.stderr);
|
|
||||||
assert.equal(result.stdout, '4\n');
|
|
||||||
|
|
||||||
await writeFile(input, '{"locators":{"kind":"repo"},"observed_seq":1}\n');
|
|
||||||
result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
|
|
||||||
assert.equal(result.status, 0, result.stderr);
|
|
||||||
assert.equal(result.stdout, '');
|
|
||||||
|
|
||||||
await writeFile(input, '{invalid json}\n');
|
|
||||||
result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
|
|
||||||
assert.notEqual(result.status, 0);
|
|
||||||
} finally {
|
|
||||||
await rm(directory, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('board-history preserves non-git data-dir as a non-detectable result', async () => {
|
|
||||||
const directory = await mkdtemp(path.join(tmpdir(), 'reflect-board-non-git-'));
|
|
||||||
try {
|
|
||||||
await writeFile(path.join(directory, 'task.json'), '{}\n');
|
|
||||||
const result = spawnSync(
|
|
||||||
'bash',
|
|
||||||
[
|
|
||||||
new URL('scripts/analysis/reflect-board-history.sh', ROOT).pathname,
|
|
||||||
'--data-dir',
|
|
||||||
directory,
|
|
||||||
],
|
|
||||||
{ encoding: 'utf8' },
|
|
||||||
);
|
|
||||||
assert.equal(result.status, 0, result.stderr);
|
|
||||||
assert.match(result.stdout, /"done_tasks": 1/);
|
|
||||||
assert.match(result.stdout, /"detectable_outcomes": 0/);
|
|
||||||
} finally {
|
|
||||||
await rm(directory, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Exit 0 only when the capability probe itself succeeds and advertises verify.
|
|
||||||
# A failed help command and a successful response without verify are both
|
|
||||||
# unsupported, matching the historical e2e-install-test.sh conditional.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
gateway_help_rc=0
|
|
||||||
gateway_help="$(mosaic gateway --help 2>&1)" || gateway_help_rc=$?
|
|
||||||
[[ "$gateway_help_rc" -eq 0 ]] || exit 1
|
|
||||||
grep -q 'verify' <<<"$gateway_help"
|
|
||||||
@@ -136,7 +136,7 @@ fi
|
|||||||
echo "=== [inner] Running mosaic gateway verify ==="
|
echo "=== [inner] Running mosaic gateway verify ==="
|
||||||
# `gateway verify` was added in feat/mosaic-first-run-ux.
|
# `gateway verify` was added in feat/mosaic-first-run-ux.
|
||||||
# If the installed version pre-dates this, skip gracefully.
|
# If the installed version pre-dates this, skip gracefully.
|
||||||
if ! bash /repo/tools/e2e-gateway-verify-supported.sh; then
|
if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then
|
||||||
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
|
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
|
||||||
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
|
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
|
||||||
echo "[SKIP] Re-run after the new version is published to validate this step."
|
echo "[SKIP] Re-run after the new version is published to validate this step."
|
||||||
|
|||||||
+4
-8
@@ -308,17 +308,13 @@ ensure_monorepo() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Gitea archives extract to exactly one <repo-name>/ inside the work dir.
|
# Gitea archives extract to <repo-name>/ inside the work dir
|
||||||
# Read the complete population so a malformed multi-root archive reaches the
|
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||||||
# named diagnostic instead of aborting on an upstream SIGPIPE under pipefail.
|
if [[ -z "$EXTRACTED_DIR" ]] || [[ ! -d "$EXTRACTED_DIR" ]]; then
|
||||||
local -a extracted_dirs=()
|
fail "Could not locate extracted source in archive."
|
||||||
mapfile -d '' -t extracted_dirs < <(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d -print0)
|
|
||||||
if [[ "${#extracted_dirs[@]}" -ne 1 ]] || [[ ! -d "${extracted_dirs[0]:-}" ]]; then
|
|
||||||
fail "Could not locate exactly one extracted source directory in archive."
|
|
||||||
ls -la "$WORK_DIR" >&2
|
ls -la "$WORK_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
EXTRACTED_DIR="${extracted_dirs[0]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
|
# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
|
||||||
|
|||||||
@@ -35,10 +35,7 @@ export DARK_THRESHOLD_MS="${DARK_THRESHOLD_MS:-6000}"
|
|||||||
export AGENT_SLUGS="${AGENT_SLUGS:-alpha,bravo,charlie}"
|
export AGENT_SLUGS="${AGENT_SLUGS:-alpha,bravo,charlie}"
|
||||||
export VICTIM_SLUG="${VICTIM_SLUG:-charlie}"
|
export VICTIM_SLUG="${VICTIM_SLUG:-charlie}"
|
||||||
|
|
||||||
shopt -s nullglob
|
TSX_CLI="$(ls -d "${REPO}"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs 2>/dev/null | head -1)"
|
||||||
TSX_CANDIDATES=("${REPO}"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs)
|
|
||||||
shopt -u nullglob
|
|
||||||
TSX_CLI="${TSX_CANDIDATES[0]:-}"
|
|
||||||
if [[ -z "${TSX_CLI}" ]]; then
|
if [[ -z "${TSX_CLI}" ]]; then
|
||||||
echo "run.sh: tsx not found under node_modules — run pnpm install first" >&2
|
echo "run.sh: tsx not found under node_modules — run pnpm install first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user