Compare commits

..
Author SHA1 Message Date
code-infra-01andGPT-6.1-Sol de99f7263f fix(framework): validate milestone title before provider reads (#1841)
ci/woodpecker/pr/ci Pipeline was successful
Refuse missing or empty creation titles before provider detection while preserving title-free listing. Prove zero-provider usage failures and unchanged synthetic list and creation payload behavior.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 19:41:12 -06:00
code-infra-01andGPT-6.1-Sol ade6f3d26f test(framework): isolate issue usage fixture origins (#1841)
Use owned empty Git fixtures so parse-acceptance arms cannot inherit the enclosing checkout remote. Retain no-provider assertions and deliberate GitHub normalization coverage.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 19:41:04 -06:00
code-infra-01andGPT-6.1-Sol d3a2b8c870 fix(framework): validate raw provider header bytes (#1841)
Capture transport bytes without universal newline conversion and validate CRLF framing and field bytes before parsing. Prove malformed redirects refuse before any follow-up while valid OWS and response blocks remain supported.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 19:40:54 -06:00
code-infra-01andGPT-6.1-Sol 1a569f734c test(framework): isolate local PR diff provider fixture (#1841)
ci/woodpecker/pr/ci Pipeline failed
Inject a synthetic Gitea detector only for owned local fixture origins. Keep actual fetch/ref/diff and fail-closed reviewer assertions intact.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 19:00:27 -06:00
code-infra-01andGPT-6.1-Sol b03f3a70bd fix(framework): reject redirect URI glob expansion (#1841)
ci/woodpecker/pr/ci Pipeline was canceled
Validate raw and resolved URI components before transport and disable curl globbing. Preserve encoded literal targets and prove overlapping defenses with real loopback TLS controls.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 18:54:25 -06:00
code-infra-01andGPT-6.1-Sol 85ea376dc0 fix(framework): constrain authenticated read redirects (#1841)
ci/woodpecker/pr/ci Pipeline failed
Follow bounded same-origin HTTPS GET redirects without replaying mutations. Prove real wrapper entry paths and declare TLS fixture prerequisites in all consuming pipeline steps.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 18:26:34 -06:00
code-infra-01andGPT-6.1-Sol 79682f703f test(framework): make fake provider launchers portable to BusyBox (#1841)
ci/woodpecker/pr/ci Pipeline failed
Replace only the two env -S fixture prefixes with a POSIX shell trampoline. Keep Python isolation, quoted argv, fixture bodies and assertions unchanged.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 17:33:28 -06:00
code-infra-01andGPT-6.1-Sol 1effc9c143 docs(framework): index shipped forge wrappers and fixture helper (#1841)
ci/woodpecker/pr/ci Pipeline failed
Add the nine missing git-tool index rows to the existing tables. Identify the schema fixture as an offline test helper and retain all existing tool entries.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 17:02:18 -06:00
code-infra-01andGPT-6.1-Sol 00d62c62ca ci(framework): enumerate forge wrapper regression suites (#1841)
ci/woodpecker/pr/ci Pipeline failed
Add eleven shell suites and seven Python suites to the existing framework test command. Keep all prior invocations and exclusions unchanged.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 16:35:17 -06:00
code-infra-01andGPT-6.1-Sol 5cde8cd15b docs(framework): record round-two credential contract and evidence (#1841)
ci/woodpecker/pr/ci Pipeline failed
Resolve R2-C3 by documenting anonymous authentication consistently. Record complete URL, transport-error and wrapper trace controls, correct the earlier S6 coverage claim, and preserve the independent baseline failure.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 16:06:10 -06:00
code-infra-01andGPT-6.1-Sol fc7b9d61b3 fix(framework): keep shell credentials inside private workflows (#1841)
Disable tracing before caller-side token capture and retain privacy through the entire workflow. Use anonymous curl config descriptors, keep request stdin available, and cover all token-consuming wrappers with trace and argv controls. Preserve signal handling and stateful readback fixtures.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 16:03:00 -06:00
code-infra-01andGPT-6.1-Sol bec37b6fdf fix(framework): bind provider URLs and reject curl errors (#1841)
Validate complete public and enterprise API URLs in transport fixtures. Treat nonzero curl exits as transport failures for every provider, preserving unknown mutation outcomes.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 16:01:05 -06:00
code-infra-01andGPT-6.1-Sol a193cf2845 docs(framework): record forge review fixes and verification (#1841)
Map review findings to fix commits and regressions; preserve offline sweep, targeted correction, mutant controls and baseline failures separately. Record deferred inherited literals and unverified live transport behavior.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 15:31:26 -06:00
code-infra-01andGPT-6.1-Sol 8325df7063 test(framework): isolate issue-close actor credentials (#1841)
Replace the inherited shared-token fixture dependency with a synthetic actor and host-bound token in a private fixture home. Keep the comment-before-close negative and positive controls intact.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 15:30:47 -06:00
code-infra-01andGPT-6.1-Sol 02c7f2b183 test(framework): finish explicit credential fixture migration (#1841)
Keep metadata fixture arms offline, bind explicit/default-port fixture credentials and avoid new shell diagnostics in isolated credential arms.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 15:25:38 -06:00
code-infra-01andGPT-6.1-Sol e62adbcc27 test(framework): use independent offline workflow controls (#1841)
Exercise separate T3-wake and notifier wrapper paths against expected verdicts; reject wrong-ready and missing-full-stop mutants. Keep schema fixtures offline, operator patterns external and generated guard fixtures in scratch.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 15:22:25 -06:00
code-infra-01andGPT-6.1-Sol d939601116 fix(framework): bind forge policy and credentials to trusted identities (#1841)
Resolve merge authority from the pinned target commit; reject full PR identity races and malformed conversation state. Require explicit host-bound actors, verify mutation identity and deliver auth through anonymous descriptors. Migrate legacy fixture credentials to explicit actors.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 15:21:39 -06:00
code-infra-01andGPT-6.1-Sol 55d6cc18e3 feat(framework): package provider-neutral PR workflow skills and parity evidence (#1841)
Promote the four attributed workflows and their git/watch skill dependencies. Document capabilities, merge declarations, unsupported profiles, optional notification prerequisites and measured verification limits. Exclude generated Python bytecode from published git tools.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 14:41:45 -06:00
code-infra-01andGPT-6.1-Sol 5d187488d4 fix(framework): verify enterprise review URLs and ambiguous outcomes (#1841)
Preserve provider-qualified conversation ownership on enterprise API prefixes and keep post-write malformed readback outcomes unknown. Make the absence assertion independent of scratch directory names.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 14:36:25 -06:00
code-infra-01andGPT-6.1-Sol 630cc86355 fix(framework): harden forge adapters and preserve SSH compatibility (#1841)
Keep credentials out of client argv, validate snapshot schema and ownership evidence, reject opaque GraphQL writes and isolate suite scratch roots. Retain existing SCP and SSH transport parsing.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 14:31:15 -06:00
code-infra-01andGPT-6.1-Sol f59a4c3483 feat(framework): enforce declared merge methods and verified integration controls (#1841)
Add schema-v3 merge policy with squash-only defaults, protected scheduling, explicit capability failures, normalized CI checks and guarded review mutations. Qualify legacy fake providers with public metadata fixtures.

Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 14:17:10 -06:00
code-infra-01andGPT-6.1-Sol bbfdacaa38 feat(framework): draft normalized forge status and review adapters (#1841)
Co-Authored-By: GPT-6.1-Sol (Codex) <[email protected]>
2026-11-04 13:54:56 -06:00
code-be-02 924438016e fix(mosaic): honor seat-owned Gitea slots (#1529)
ci/woodpecker/push/publish Pipeline was successful
Co-authored-by: Mosaic fleet seat code-be-02 <[email protected]>
2026-10-10 00:50:38 +00:00
fred 2101c9b446 fix(git-tools): issue-comment.sh resolves API base without monolith GITEA_URL (#1502)
ci/woodpecker/push/publish Pipeline was successful
2026-09-11 22:50:23 +00:00
code-infra-01 5d27700026 fix(#1257): confirm delivery by draft transition, not prompt detection (adopts #1262) (#1332)
ci/woodpecker/push/publish Pipeline was successful
Co-authored-by: code-infra-01 <[email protected]>
2026-09-04 22:25:13 +00:00
orch-01 d6302f8e6f docs: make Portainer optional deployment path (#1492)
ci/woodpecker/push/publish Pipeline was successful
2026-09-02 23:23:07 +00:00
150 changed files with 7233 additions and 4967 deletions

No files matched your search

+5 -3
View File
@@ -72,7 +72,8 @@ steps:
sanitization:
image: *node_image
commands:
- apk add --no-cache bash
# Real loopback TLS transport fixtures require curl and openssl.
- apk add --no-cache bash curl openssl
- bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh
# Resident line-count ceiling over framework-owned resident files
# (Constitution + dispatcher + each RUNTIME.md slice). See DESIGN §7 / R9.
@@ -205,7 +206,7 @@ steps:
# shared command; everything else in this step is PIPELINE-LEVEL
# prerequisite the canonical command expects its caller to provide (SDLC-D-034):
# the ci-postgres service + pg_isready wait + db:migrate (postgres path),
# `apk add openssl`, and the pinned pi install. None of those can move into
# `apk add openssl curl`, and the pinned pi install. None of those can move into
# the runner (it must also work locally on the PGlite path with no database).
test:
image: *node_image
@@ -220,7 +221,8 @@ steps:
# Dockerfile.ci, but ci-base only rebuilds on push-to-main/tag — this
# `apk add` guarantees openssl is present on PR pipelines too (and is a
# fast no-op once the rebuilt image already ships it).
- apk add --no-cache openssl
# Real loopback TLS transport fixtures also require curl.
- apk add --no-cache openssl curl
# Pi runtime (Invariant R): invariant_r_unittest.py hard-requires an
# installed `pi` binary at exactly this measured version — the test
# boots Pi's real tool registry to prove the read-only carve-out
+3 -2
View File
@@ -97,12 +97,13 @@ steps:
echo "[verify] commit identity confirmed: $CHECKOUT_SHA"
# (b) Canonical terminal verification. Caller-provided prerequisites the
# runner expects (see .woodpecker/ci.yml comments): bash/rsync for the
# guard stages, openssl + the pinned pi binary for the test stage. git is
# guard stages, openssl/curl for real loopback TLS transport tests, and
# the pinned pi binary for the test stage. git is
# baked into ci-base but re-asserted here so the identity check above can
# never silently depend on a stale baked image. DATABASE_URL is
# deliberately NOT set: the canonical command must hold on the PGlite
# path too and never sets or requires a database itself.
- apk add --no-cache bash rsync openssl git
- apk add --no-cache bash rsync openssl git curl
- npm install -g @earendil-works/[email protected]
- pnpm verify:release
depends_on:
@@ -81,4 +81,4 @@ The page may be promoted to an operative runbook only after deny-all is intentio
## Related contract
- [M1 logical identity and fencing decision](../../DEVELOPER-GUIDE/architecture/decisions/mos-runtime-portability-m1.md)
- [MOS-PORT requirements](../../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#mos-runtime-portability-workstream-mos-port)
- [MOS-PORT requirements](../../PRD.md#mos-runtime-portability-workstream-mos-port)
@@ -98,4 +98,4 @@ A valid lease or grant is therefore not a claim of exactly-once delivery, produc
## Related contract
- [M1 connector lease operations — held/non-operative](../../../ADMIN-GUIDE/operations/mos-connector-lease-operations.md)
- [MOS-PORT requirements](../../../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#mos-runtime-portability-workstream-mos-port)
- [MOS-PORT requirements](../../../PRD.md#mos-runtime-portability-workstream-mos-port)
+1 -3
View File
@@ -1,10 +1,8 @@
---
kind: tracking
status: superseded
status: active
---
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's federated-tier-as-canonical-MVP-deployment-topology and Federation-v1-as-top-priority framing is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
# Mission Manifest — MVP
> Top-level rollup tracking Mosaic Stack MVP execution.
+902 -16
View File
@@ -1,24 +1,910 @@
---
kind: shim
kind: spec
status: active
source_of_truth: true
current_rev: docs/PRDs/2026-08-31_PRD_rev1/
---
# PRD: Mosaic Stack
# PRD: Mosaic Stack — North Star
This file is a permanent shim, not the PRD body (GOV.1 lifecycle rule). The
project source of truth is the **current revision bundle**:
This document is the product source of truth for Mosaic Stack.
**[docs/PRDs/2026-08-31_PRD_rev1/](./PRDs/2026-08-31_PRD_rev1/PRD.md)** —
rev1, ratified 2026-09-01 (Jason Woltje). It consolidates the 2026-08-26 North
Star (D1–D15), the fleet north star, the agent-runtime L1/L2 contracts, and the
control-plane-surfaces findings into sectioned documents (AUTHN, AUTHZ, CLI,
DATA, GOV.1–5, HARN, PROV, ROLE, SEAT, SESS, UI, VIS) with a single decision
map and a closed open-questions list.
- **Part I** defines the product north star. It is written from the ratified
decision set D1–D14 (operator decision session, 2026-08-25; decision owner
Jason Woltje). Each section cites the decisions it implements.
- **Part II** preserves the active workstream contracts unchanged. Open issues
bind to them; this rewrite does not alter a single normative word in them.
- The previous v0.1.0 beta PRD body is archived verbatim at
[docs/archive/PRD-v0.1.md](./archive/PRD-v0.1.md) and is no longer authority.
- The delivery roadmap lives in [docs/ROADMAP.md](./ROADMAP.md). Per D11, every
planned phase appears there from day one, even as a placeholder.
Revision bundles are frozen at ratification and never deleted. The prior
revision, rev0 (2026-08-26 North Star), is archived verbatim at
[docs/PRDs/2026-08-26_PRD_rev0/PRD.md](./PRDs/2026-08-26_PRD_rev0/PRD.md).
Updating the PRD means ratifying a new bundle under `docs/PRDs/` and repointing
`current_rev:` here; this path never changes.
## Metadata
- **Owner / decision authority:** Jason Woltje
- **Status:** active (supersedes the v0.1.0 PRD as product authority)
- **Date:** 2026-08-26
- **Decision registry:** D1–D14, recorded in Part I §12
- **SSOT rule:** this repository's `docs/` tree is the product source of truth
(D5). Estate brains hold operational records, not product canon; only
product-relevant material migrates here (D6).
---
## Part I — Product north star
### 1. What Mosaic Stack is (D1)
Mosaic Stack is an **open-source, AI-first platform for people who want a
self-hosted environment for agentic management and a life operating system.**
It serves personal, business, and employee needs from one deployment, and the
work is offered freely.
"AI-first" means agents are first-class operators of the system, not a bolted-on
chat box: the platform exists to let humans direct fleets of agents over their
projects, tasks, communications, and infrastructure, with the same tools and
the same guarantees whether a human or an agent is acting.
### 2. Who it is for (D1, D9)
The operator of a deployment is its user. Mosaic Stack is **not a hosted
business**: running the system as a service for external customers is outside
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
can separate their world — for example, several LLCs plus a personal domain —
while every deployment is self-hosted by its own operator.
"Company" in the hierarchy is organizational separation for one operator's
world, not a customer account.
### 3. Deployment modes (D3)
Two modes, chosen at install time:
| | Standalone / personal | Enterprise |
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
Brains are configurable as external git repositories (recommended, not
required); git tracking is always on locally.
**Federation** (connecting deployments: system-level config, assigned users,
rights and data-access control, trusts with boundaries, exfiltration
monitoring) is intentionally not fully designed. It is deferred, appears on the
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
### 4. Structure and tenancy (D2, D9, D13)
The hierarchy:
```
company/organization (N per deployment)
└─ estate (each in exactly one company)
└─ project (each in exactly one estate)
└─ workspace (project-specific; carries the Kanban)
```
Rules:
- Users can create N companies, N estates, N projects.
- Tasks bubble UP the hierarchy so whole-system status is visible at every
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
- Granular RBAC: admins restrict access per company, estate, and project;
grants are evaluated down the chain. Assets are transferable subject to the
structure.
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
ratified in
[docs/requirements/native-kanban-sot.md](./requirements/native-kanban-sot.md)
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
### 5. Identity (D10)
Built-in auth (better-auth) is the **account system of record**. Authentik and
other external IdPs federate in via OIDC as login methods; they never become
the system of record. Perimeter shims (forward-auth in front of a web host) are
deployment workarounds, not the design.
### 6. Onboarding (D4)
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
is extensible** — new wizards attach as tabs.
Standalone flow captures: system and company name; component choices (Mosaic
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
DB/PostgreSQL vs external; vector DB); the initial user
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
(harness choice and install, OAuth or API-key login, multi-account, model
choice with recommendation, agent name and persona, account assignment,
optional comms auto-enroll); a user onboarding profile (disabilities including
ADHD/autism/PDA/vision, professional background, education, desired agent
communication style, optional voice-matching interview, family/pets/friends/
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
configuration; an initial estate, an initial project, and seeded example data.
Enterprise uses the same skeleton with personal data optional; the focus moves
to business structure, org chart, RBAC, M365 and external systems, immediate
OIDC, SSO prominent.
Profile answers feed `USER.md` and/or the user's data store subject to the
custody rule in §7.
### 7. Data custody (D6, D14)
- **Sensitive profile categories** (disabilities, family, communication style,
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
structural data, consent records, and pointers — never the content. "User
data does not leak" is enforced by architecture, not policy (D14).
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
optional in Standalone, not required. Keeping it is the recommended default
because it preserves forward-compatibility with the one-way Enterprise
conversion (D3).
- Estate brains hold operational records. Only product-relevant material
migrates into this repository's docs; operational records stay in their
brains and are linked (D6).
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
same official framework tooling the CLI uses. The CLI remains the primary
execution method; the webUI uses the tools to operate and configure the
system. The webUI never bypasses tooling to reach the database or filesystem
directly.
Consequence for planning: when a desired webUI operation has no backing tool,
the gap is scored **"blocked on tooling"** and the tool is built first. The
product baseline therefore always includes all three D8 inputs: the tool
inventory (what exists and what is missing), the webUI→tool mapping, and the
measured current state of the `next` branch.
### 9. v1 slice (D11)
v1 is deliberately small:
1. **Standalone onboarding wizard** — system/company name, component choices,
initial user, initial estate + project, seeded examples, re-runnable.
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
read-only task bubble-up.
3. **Basic RBAC** on the hierarchy.
4. **Minimal agent enrollment** — one harness, API key, name/persona.
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
Enterprise conversion, federation. Every deferred item appears in
[docs/ROADMAP.md](./ROADMAP.md) per the D11 rule: nothing exists only in heads.
### 10. Relationship to the fleet north star
[docs/fleet/NORTH_STAR.md](./fleet/NORTH_STAR.md) (generated from
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
agent fleet that builds and operates the system should run (NS-1..NS-10,
workstreams A–L). This PRD is the **product** north star. They are not
competitors: the fleet north star is subordinate product-wise — its workstream
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
PRD does not redefine fleet invariants. The subordination rule is ratified in
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
cite and subordinate it, never fork it"). A change that would put the two in
conflict must amend one of them explicitly, never fork a third document
(drafting addition — see §12.1).
### 11. Explicit non-goals
- Hosted/SaaS operation for external customers (D9).
- A webUI that writes to the database or filesystem around the tooling (D12).
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
- Fully-designed federation in v1 (D3 — roadmap placeholder only).
### D15 — Tiered containerized deployment (2026-08-30, containerization lane)
The stack ships a tiered deployment target, additive to the architecture
gate (D8): (1) Standalone tier — docker compose is the canonical
single-host deployment: postgres, valkey, openbao, gateway, appservice
and the served webUI in one composition, with migrations, health checks,
and a documented install/upgrade path; the registry (CI-published
images) is the only deployment source. (2) Enterprise tier — Kubernetes
manifests for the same service set, phase-gated on the standalone tier
holding its acceptance bar. The v1 acceptance bar for the standalone
tier: compose-up healthy; webUI hosts agent chat; an in-stack agent can
open a PR to this repo; CI validates it; the running deployment adopts
the merged change (pull + restart). Federation (D3 clause) remains
deferred and unforeclosed. Implementation plan:
docs/plans/2026-08-30_containerization.md.
## 12. Decision registry
| ID | Decision (short form) |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred |
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
| D5 | North star = this rewrite of docs/PRD.md; stack docs/ = product SSOT |
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
| D7 | Spec-inventory sweep launched immediately (executed; INPUTS baseline frozen by operator ruling T2, 2026-08-25) |
| D8 | webUI sits over official framework tooling; CLI primary |
| D9 | Not a hosted business; company = organizational separation for one operator |
| D10 | better-auth is the account system of record; external IdPs via OIDC |
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
| D13 | workspace_id stays the hard isolation unit; hierarchy is parent structure above; kanban SOT amended, not rewritten |
| D14 | Sensitive profile data in the user's own brain only; postgres holds structure/consent/pointers |
| D15 | Tiered containerized deployment: compose standalone tier (five-point v1 bar) + phase-gated k8s enterprise tier; registry-only image source | 2026-08-30 containerization lane; plan docs/plans/2026-08-30_containerization.md |
The full decision texts are recorded in the operator decision log (USC estate
brain, webui-audit lane, `GRILL.md`).
### 12.1 Drafting additions beyond D1–D14
Independent review of this rewrite identified rules in this document that are
not present in the D1–D14 record or the frozen T2 baseline. They are listed
here so their ratification is explicit: approval of the PR that introduces
this document, by the decision owner, ratifies them. If any is rejected it is
removed, not silently kept.
1. **Federation forward-compatibility gate:** "nothing in v1 may foreclose
federation" (§3), and scoping federation later requires its own PRD plus
threat model ([ROADMAP](./ROADMAP.md) P5). D3 defers federation; these
protective gates are additions.
2. **North-star amendment rule:** a product/fleet north-star conflict must be
resolved by amending one of the two documents explicitly, never by forking
a third (§10). The subordination itself is T2-ratified; this amendment
procedure is an addition.
---
## Part II — Active workstream contracts (preserved unchanged)
The sections below are normative, in-flight workstream contracts carried over
verbatim from the previous revision of this file. Open issues bind to them.
This rewrite moved no text and changed no requirement in them; they are
governed by their own issues and review gates, and they graduate out of this
file individually when their workstreams close.
## Current addendum: #1194 — Installed framework-tool drift detection
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
### Problem and objective
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
### Requirements
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
### Acceptance criteria
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
---
## 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)
### Problem and objective
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
gateway-backed agent catalog.
### Normative requirements
| ID | Requirement |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
### Acceptance criteria
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
### M0 implementation gate
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
### 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)
### Problem and objective
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
infer, producing incorrect host, session, socket, or helper targets. The objective is one
roster-resolved communications contract that every supported harness receives unchanged.
### Normative requirements
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
resolver. A second lenient communications parser is forbidden.
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
generation.
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
fail closed.
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
substitute, or fuzzy-match targeting values.
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
contract.
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
communications data through the common runtime composer.
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
`TOOLS.md` content SHALL remain preserved.
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
NOT rewrite active context, restart a session, or mutate a live fleet.
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
### Acceptance criteria
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
identity contains exact host/session/socket/helper values.
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
exact targeting and fail-closed behavior.
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
discovery command; no fuzzy session selection is emitted.
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
helper executable, agent-send socket isolation, and exact-target tests pass.
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
exact-agent relaunch; no implementation path performs automatic session mutation.
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
symlink-target safety, and repeated-run idempotence.
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
role/class change produces a different communications generation.
---
## KBN-101 Database Runtime/Migration Role Split (#771)
### Problem and objective
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
### Normative requirements
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
### Acceptance criteria
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
---
## Tess Interaction Agent Workstream (TESS)
### Problem and Objective
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
### Scope
#### In Scope
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
#### Out of Scope
1. Replacing Mos as coding/general fleet orchestrator.
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
4. Unrestricted shell execution from Discord.
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
6. Replacing tmux before Matrix/native transport reaches operational parity.
### Stakeholder and User Requirements
- Jason must be able to converse with the same Tess session from Discord and CLI.
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
### Non-Functional Requirements
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
### Acceptance Criteria
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
### Constraints, Dependencies, Risks, and Assumptions
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
### Testing and Delivery Intent
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
---
## Official Channel Plugin Workstream (#756)
### Problem and Objective
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
### Scope
#### In Scope
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
#### Out of Scope
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
2. Dynamic Discord authorization administration in the web UI.
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
4. Implementing Matrix or Slack adapters in this slice.
### Non-Functional Requirements
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
### Acceptance Criteria
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
### Constraints, Risks, and Assumptions
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
### Testing and Delivery Intent
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
---
## Mos Runtime Portability Workstream (MOS-PORT)
### Problem and Objective
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
### M1 Requirements
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
### M1 Acceptance Criteria
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
### Deferred to Later #754 Milestones
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
---
## Workspace placement guard hardening (#1174)
### Problem and objective
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
will route around it.
### Scope and requirements
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
operands, not from every HOME-shaped word in the command.
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
operands and SHALL be refused when they resolve under HOME.
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
without relying on an enumerable allowlist of flag spellings.
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
handling SHALL preserve existing fail-closed checkout coverage.
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
repeated separators, and existing symlink parents before the HOME boundary comparison.
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
#1197.
### Acceptance and verification
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
guard allows a HOME-valued source with an explicit safe destination.
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
against the prior head where the defect existed.
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
after shell command boundaries.
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
terminal-green CI pass before merge.
5. Any option-classification residual is documented with its deliberate failure direction.
### Constraints, risks, and assumptions
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
acceptable repair.
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
while separate-value options have explicit grammar and must be classified as such.
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
chosen residual direction and pin every currently supported placement option in behavior tests.
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
---
## Release Integrity Workstream (RI, #1275)
### Problem and objective
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
### Normative requirements
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
### Acceptance criteria
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
### Out of scope
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
## Official CLI Capability and Tool Migration Workstream (T78)
Normative contract on integration trunk `next`:
[docs/requirements/cli-capability-migration.md](./requirements/cli-capability-migration.md):
migrates agent-facing operations from directly invoked scripts into documented, first-class
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
carries its own implementation hold and delivery stages.
-910
View File
@@ -1,910 +0,0 @@
---
kind: spec
status: active
source_of_truth: true
---
# PRD: Mosaic Stack — North Star
This document is the product source of truth for Mosaic Stack.
- **Part I** defines the product north star. It is written from the ratified
decision set D1–D14 (operator decision session, 2026-08-25; decision owner
Jason Woltje). Each section cites the decisions it implements.
- **Part II** preserves the active workstream contracts unchanged. Open issues
bind to them; this rewrite does not alter a single normative word in them.
- The previous v0.1.0 beta PRD body is archived verbatim at
[docs/archive/PRD-v0.1.md](./archive/PRD-v0.1.md) and is no longer authority.
- The delivery roadmap lives in [docs/ROADMAP.md](./ROADMAP.md). Per D11, every
planned phase appears there from day one, even as a placeholder.
## Metadata
- **Owner / decision authority:** Jason Woltje
- **Status:** active (supersedes the v0.1.0 PRD as product authority)
- **Date:** 2026-08-26
- **Decision registry:** D1–D14, recorded in Part I §12
- **SSOT rule:** this repository's `docs/` tree is the product source of truth
(D5). Estate brains hold operational records, not product canon; only
product-relevant material migrates here (D6).
---
## Part I — Product north star
### 1. What Mosaic Stack is (D1)
Mosaic Stack is an **open-source, AI-first platform for people who want a
self-hosted environment for agentic management and a life operating system.**
It serves personal, business, and employee needs from one deployment, and the
work is offered freely.
"AI-first" means agents are first-class operators of the system, not a bolted-on
chat box: the platform exists to let humans direct fleets of agents over their
projects, tasks, communications, and infrastructure, with the same tools and
the same guarantees whether a human or an agent is acting.
### 2. Who it is for (D1, D9)
The operator of a deployment is its user. Mosaic Stack is **not a hosted
business**: running the system as a service for external customers is outside
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
can separate their world — for example, several LLCs plus a personal domain —
while every deployment is self-hosted by its own operator.
"Company" in the hierarchy is organizational separation for one operator's
world, not a customer account.
### 3. Deployment modes (D3)
Two modes, chosen at install time:
| | Standalone / personal | Enterprise |
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
Brains are configurable as external git repositories (recommended, not
required); git tracking is always on locally.
**Federation** (connecting deployments: system-level config, assigned users,
rights and data-access control, trusts with boundaries, exfiltration
monitoring) is intentionally not fully designed. It is deferred, appears on the
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
### 4. Structure and tenancy (D2, D9, D13)
The hierarchy:
```
company/organization (N per deployment)
└─ estate (each in exactly one company)
└─ project (each in exactly one estate)
└─ workspace (project-specific; carries the Kanban)
```
Rules:
- Users can create N companies, N estates, N projects.
- Tasks bubble UP the hierarchy so whole-system status is visible at every
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
- Granular RBAC: admins restrict access per company, estate, and project;
grants are evaluated down the chain. Assets are transferable subject to the
structure.
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
ratified in
[docs/requirements/native-kanban-sot.md](./requirements/native-kanban-sot.md)
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
### 5. Identity (D10)
Built-in auth (better-auth) is the **account system of record**. Authentik and
other external IdPs federate in via OIDC as login methods; they never become
the system of record. Perimeter shims (forward-auth in front of a web host) are
deployment workarounds, not the design.
### 6. Onboarding (D4)
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
is extensible** — new wizards attach as tabs.
Standalone flow captures: system and company name; component choices (Mosaic
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
DB/PostgreSQL vs external; vector DB); the initial user
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
(harness choice and install, OAuth or API-key login, multi-account, model
choice with recommendation, agent name and persona, account assignment,
optional comms auto-enroll); a user onboarding profile (disabilities including
ADHD/autism/PDA/vision, professional background, education, desired agent
communication style, optional voice-matching interview, family/pets/friends/
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
configuration; an initial estate, an initial project, and seeded example data.
Enterprise uses the same skeleton with personal data optional; the focus moves
to business structure, org chart, RBAC, M365 and external systems, immediate
OIDC, SSO prominent.
Profile answers feed `USER.md` and/or the user's data store subject to the
custody rule in §7.
### 7. Data custody (D6, D14)
- **Sensitive profile categories** (disabilities, family, communication style,
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
structural data, consent records, and pointers — never the content. "User
data does not leak" is enforced by architecture, not policy (D14).
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
optional in Standalone, not required. Keeping it is the recommended default
because it preserves forward-compatibility with the one-way Enterprise
conversion (D3).
- Estate brains hold operational records. Only product-relevant material
migrates into this repository's docs; operational records stay in their
brains and are linked (D6).
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
same official framework tooling the CLI uses. The CLI remains the primary
execution method; the webUI uses the tools to operate and configure the
system. The webUI never bypasses tooling to reach the database or filesystem
directly.
Consequence for planning: when a desired webUI operation has no backing tool,
the gap is scored **"blocked on tooling"** and the tool is built first. The
product baseline therefore always includes all three D8 inputs: the tool
inventory (what exists and what is missing), the webUI→tool mapping, and the
measured current state of the `next` branch.
### 9. v1 slice (D11)
v1 is deliberately small:
1. **Standalone onboarding wizard** — system/company name, component choices,
initial user, initial estate + project, seeded examples, re-runnable.
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
read-only task bubble-up.
3. **Basic RBAC** on the hierarchy.
4. **Minimal agent enrollment** — one harness, API key, name/persona.
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
Enterprise conversion, federation. Every deferred item appears in
[docs/ROADMAP.md](./ROADMAP.md) per the D11 rule: nothing exists only in heads.
### 10. Relationship to the fleet north star
[docs/fleet/NORTH_STAR.md](./fleet/NORTH_STAR.md) (generated from
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
agent fleet that builds and operates the system should run (NS-1..NS-10,
workstreams A–L). This PRD is the **product** north star. They are not
competitors: the fleet north star is subordinate product-wise — its workstream
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
PRD does not redefine fleet invariants. The subordination rule is ratified in
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
cite and subordinate it, never fork it"). A change that would put the two in
conflict must amend one of them explicitly, never fork a third document
(drafting addition — see §12.1).
### 11. Explicit non-goals
- Hosted/SaaS operation for external customers (D9).
- A webUI that writes to the database or filesystem around the tooling (D12).
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
- Fully-designed federation in v1 (D3 — roadmap placeholder only).
### D15 — Tiered containerized deployment (2026-08-30, containerization lane)
The stack ships a tiered deployment target, additive to the architecture
gate (D8): (1) Standalone tier — docker compose is the canonical
single-host deployment: postgres, valkey, openbao, gateway, appservice
and the served webUI in one composition, with migrations, health checks,
and a documented install/upgrade path; the registry (CI-published
images) is the only deployment source. (2) Enterprise tier — Kubernetes
manifests for the same service set, phase-gated on the standalone tier
holding its acceptance bar. The v1 acceptance bar for the standalone
tier: compose-up healthy; webUI hosts agent chat; an in-stack agent can
open a PR to this repo; CI validates it; the running deployment adopts
the merged change (pull + restart). Federation (D3 clause) remains
deferred and unforeclosed. Implementation plan:
docs/plans/2026-08-30_containerization.md.
## 12. Decision registry
| ID | Decision (short form) |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred |
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
| D5 | North star = this rewrite of docs/PRD.md; stack docs/ = product SSOT |
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
| D7 | Spec-inventory sweep launched immediately (executed; INPUTS baseline frozen by operator ruling T2, 2026-08-25) |
| D8 | webUI sits over official framework tooling; CLI primary |
| D9 | Not a hosted business; company = organizational separation for one operator |
| D10 | better-auth is the account system of record; external IdPs via OIDC |
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
| D13 | workspace_id stays the hard isolation unit; hierarchy is parent structure above; kanban SOT amended, not rewritten |
| D14 | Sensitive profile data in the user's own brain only; postgres holds structure/consent/pointers |
| D15 | Tiered containerized deployment: compose standalone tier (five-point v1 bar) + phase-gated k8s enterprise tier; registry-only image source | 2026-08-30 containerization lane; plan docs/plans/2026-08-30_containerization.md |
The full decision texts are recorded in the operator decision log (USC estate
brain, webui-audit lane, `GRILL.md`).
### 12.1 Drafting additions beyond D1–D14
Independent review of this rewrite identified rules in this document that are
not present in the D1–D14 record or the frozen T2 baseline. They are listed
here so their ratification is explicit: approval of the PR that introduces
this document, by the decision owner, ratifies them. If any is rejected it is
removed, not silently kept.
1. **Federation forward-compatibility gate:** "nothing in v1 may foreclose
federation" (§3), and scoping federation later requires its own PRD plus
threat model ([ROADMAP](./ROADMAP.md) P5). D3 defers federation; these
protective gates are additions.
2. **North-star amendment rule:** a product/fleet north-star conflict must be
resolved by amending one of the two documents explicitly, never by forking
a third (§10). The subordination itself is T2-ratified; this amendment
procedure is an addition.
---
## Part II — Active workstream contracts (preserved unchanged)
The sections below are normative, in-flight workstream contracts carried over
verbatim from the previous revision of this file. Open issues bind to them.
This rewrite moved no text and changed no requirement in them; they are
governed by their own issues and review gates, and they graduate out of this
file individually when their workstreams close.
## Current addendum: #1194 — Installed framework-tool drift detection
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
### Problem and objective
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
### Requirements
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
### Acceptance criteria
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
---
## 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)
### Problem and objective
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
gateway-backed agent catalog.
### Normative requirements
| ID | Requirement |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
### Acceptance criteria
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
### M0 implementation gate
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
### 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)
### Problem and objective
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
infer, producing incorrect host, session, socket, or helper targets. The objective is one
roster-resolved communications contract that every supported harness receives unchanged.
### Normative requirements
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
resolver. A second lenient communications parser is forbidden.
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
generation.
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
fail closed.
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
substitute, or fuzzy-match targeting values.
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
contract.
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
communications data through the common runtime composer.
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
`TOOLS.md` content SHALL remain preserved.
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
NOT rewrite active context, restart a session, or mutate a live fleet.
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
### Acceptance criteria
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
identity contains exact host/session/socket/helper values.
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
exact targeting and fail-closed behavior.
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
discovery command; no fuzzy session selection is emitted.
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
helper executable, agent-send socket isolation, and exact-target tests pass.
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
exact-agent relaunch; no implementation path performs automatic session mutation.
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
symlink-target safety, and repeated-run idempotence.
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
role/class change produces a different communications generation.
---
## KBN-101 Database Runtime/Migration Role Split (#771)
### Problem and objective
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
### Normative requirements
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
### Acceptance criteria
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
---
## Tess Interaction Agent Workstream (TESS)
### Problem and Objective
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
### Scope
#### In Scope
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
#### Out of Scope
1. Replacing Mos as coding/general fleet orchestrator.
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
4. Unrestricted shell execution from Discord.
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
6. Replacing tmux before Matrix/native transport reaches operational parity.
### Stakeholder and User Requirements
- Jason must be able to converse with the same Tess session from Discord and CLI.
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
### Non-Functional Requirements
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
### Acceptance Criteria
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
### Constraints, Dependencies, Risks, and Assumptions
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
### Testing and Delivery Intent
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
---
## Official Channel Plugin Workstream (#756)
### Problem and Objective
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
### Scope
#### In Scope
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
#### Out of Scope
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
2. Dynamic Discord authorization administration in the web UI.
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
4. Implementing Matrix or Slack adapters in this slice.
### Non-Functional Requirements
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
### Acceptance Criteria
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
### Constraints, Risks, and Assumptions
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
### Testing and Delivery Intent
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
---
## Mos Runtime Portability Workstream (MOS-PORT)
### Problem and Objective
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
### M1 Requirements
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
### M1 Acceptance Criteria
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
### Deferred to Later #754 Milestones
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
---
## Workspace placement guard hardening (#1174)
### Problem and objective
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
will route around it.
### Scope and requirements
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
operands, not from every HOME-shaped word in the command.
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
operands and SHALL be refused when they resolve under HOME.
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
without relying on an enumerable allowlist of flag spellings.
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
handling SHALL preserve existing fail-closed checkout coverage.
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
repeated separators, and existing symlink parents before the HOME boundary comparison.
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
#1197.
### Acceptance and verification
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
guard allows a HOME-valued source with an explicit safe destination.
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
against the prior head where the defect existed.
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
after shell command boundaries.
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
terminal-green CI pass before merge.
5. Any option-classification residual is documented with its deliberate failure direction.
### Constraints, risks, and assumptions
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
acceptable repair.
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
while separate-value options have explicit grammar and must be classified as such.
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
chosen residual direction and pin every currently supported placement option in behavior tests.
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
---
## Release Integrity Workstream (RI, #1275)
### Problem and objective
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
### Normative requirements
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
### Acceptance criteria
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
### Out of scope
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
## Official CLI Capability and Tool Migration Workstream (T78)
Normative contract on integration trunk `next`:
[docs/requirements/cli-capability-migration.md](./requirements/cli-capability-migration.md):
migrates agent-facing operations from directly invoked scripts into documented, first-class
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
carries its own implementation hold and delivery stages.
-35
View File
@@ -1,35 +0,0 @@
---
kind: record
status: superseded
---
# PRD rev0 — archive record
`PRD.md` in this directory is the 2026-08-26 North Star PRD, archived **verbatim** at
ratification of rev1 (2026-09-01). It is byte-identical to `origin/next:docs/PRD.md` at
commit `9aa4983c` (SHA-256
`60cc2f98697471850caa3440d79139d70f67eda585a2ee465fdcd517bc36afdf`). Per GOV.1 the archived
bytes are never edited — not even to repair links — so the digest stays verifiable.
**Its relative links were written for `docs/PRD.md` and do not resolve from this directory.**
That is an accepted, intentional consequence of archive-never-edit (owner disposition: the
control-plane-surfaces lane, 2026-09-02, review `CPS-PRD-REV1-REVIEW-Q90` F3). Resolve them
with this table; every target still exists in the tree.
| Link text in `PRD.md` (lines) | Resolves to |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `./archive/PRD-v0.1.md` (17) | [../../archive/PRD-v0.1.md](../../archive/PRD-v0.1.md) |
| `./ROADMAP.md` (18, 177, 249) | [../../ROADMAP.md](../../ROADMAP.md) |
| `./requirements/native-kanban-sot.md` (98) | [../../requirements/native-kanban-sot.md](../../requirements/native-kanban-sot.md) |
| `./fleet/NORTH_STAR.md` (181) | [../../fleet/NORTH_STAR.md](../../fleet/NORTH_STAR.md) |
| `./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md` (444) | [../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) |
| `./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` (444) | [../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) |
| `./TASKS.md` (462) | [../../TASKS.md](../../TASKS.md) |
| `./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md` (623) | [../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md](../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md) |
| `./requirements/cli-capability-migration.md` (906) | [../../requirements/cli-capability-migration.md](../../requirements/cli-capability-migration.md) |
Rule for future archives (recorded here; GOV.1 carries the general archive contract): every
`docs/PRDs/<date>_PRD_revN/` archived from a different original location ships a `README.md`
like this one — digest, original path, and a link-resolution table — instead of edited bytes.
Current revision: see [`docs/PRD.md`](../../PRD.md).
@@ -1,85 +0,0 @@
---
id: AUTHN.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# AUTHN.1 — Authentication accounts
Agent-side provider credentials: the accounts seats use to reach providers.
(Human login identity is D10 territory — better-auth as system of record — and
is out of this section's scope.)
## Authentication configuration surface (WebUI page + CLI)
| Control | Notes |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| in-browser OAuth establishment | the OAuth flow runs in-browser; whether the backing terminal flow is tmux-bridged is open: [[GOV.5-open-questions]] Q-N1 |
| configured accounts list | provider, mode (OAuth/API), status, holder |
| force renew | |
| deactivate | deactivated accounts drop out of every seat/harness selector |
| allowed harnesses | which harnesses may use this account |
## Custody rules
- Secrets live with the **credential broker** (OpenBao/Vault or flat files per
deployment mode — D3), never in the brain tree, never in manifests, never in
Postgres records. Enforced role manifests declare
`credentials: {store: none, providerTokens: denied}` — the enforced roles
hold no credentials at all; accounts are a launcher/broker concern.
- Multi-account per provider is a requirement (onboarding D4 already captures
multi-account enrollment).
- Account shape in the seat record (single account vs per-provider map) is
open: [[GOV.5-open-questions]] Q-D2.
## Credential-broker custody rules (pulled 2026-08-31, generalized from the vault draft)
- Reads require a token scoped to the needed paths; provisioning and writes go
through a declared channel with documented purpose. An ordinary role never
mints credentials or creates production paths.
- Canonical secret path: `environment / service / component / secret-name`,
lowercase kebab-case, nothing sensitive encoded in the path; environments
never cross-reference each other's mounts. Standard field names
(`username`/`password`, `token`, `host`/`port`/`url`).
- Only the needed field is extracted into the consuming process; values are
never echoed to logs or transcripts — read success is proven by field
presence and digest, never by printing the value.
- Least privilege, short-lived tokens, no local copies, immediate rotation on
compromise; every access audited by the broker.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `ADMIN-GUIDE/security/sso-providers.md` (D10 ground
truth: better-auth + Authentik/WorkOS/Keycloak OIDC).
**Pending pulls**: DRAFT S2 `identity-lifecycle.md` (D10 + the #1430 bootstrap
fix) and `custody-schema.md`; brain `docs/guides/proposed/operations/vault.md`
(credential-broker custody rules this section states without operational detail).
## S2 contract feed (extraction 2026-08-31)
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section (identity-lifecycle, contract 4, plus
wizard AUTHN clauses):
- better-auth tables are the **only** account system of record (D10); IdPs are
login methods only; account creation grants nothing.
- `registration_mode` open/invite/closed, defaults **closed** post-bootstrap,
forced closed during the epoch, enforced at a better-auth hook.
- Bootstrap/first-admin invariant (#1430): zero-to-one-admin exactly once per
epoch, one atomic transaction, durable fail-closed `bootstrap_state`,
re-runnable; first-admin-via-SSO runs as a bootstrap-writer transaction,
never JIT. **v1 first admin is password-only — a disclosed PRD deviation.**
- JIT defaults OFF per-provider always; JIT users get `member`, never
elevated; **role/authorization attributes are never mapped from IdP
claims**. Linking keyed `(issuer, subject)`; explicit linking = step-up
reauth ≤10 min; automatic linking gated by off-by-default
`trusted_for_linking` + verified email.
- Deactivation (ban) must bound all entry paths — **live defect: the
admin-bearer-token path does not check banned status**. Deletion deferred;
the existing hard-delete endpoint and `mosaic auth users delete` are
mandated for removal.
## Seat auth shape ruling (Q-D2, Jason 2026-09-01)
Per-provider map in `profile.json`, values are credential-broker references —
never secret material. The broker custody rules above govern resolution;
extraction stays field-scoped and digest-proven.
@@ -1,146 +0,0 @@
---
id: AUTHZ.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# AUTHZ.1 — Capability authority, enforcement, and accepted risk
The agent-side authority model: what binds a seat, where it is enforced, what
is closed by construction, and what is accepted as residual risk. Sources: the
L2 authorization contracts, `mosaic-core` (measured 2026-08-31), and the lane's
`AUTHORIZATION-GAPS.md`.
## Glossary
**Privilege escapation** _(Jason, 2026-08-31)_ — the outcome class in which an
agent exercises authority it was never granted, regardless of mechanism.
Deliberately collapses escalation and misdirection: the outcome is identical;
the distinction matters only when choosing a control.
## Authority composition is pure intersection
```
role capability ceiling
∩ assignment scope ∩ lease scope ∩ workflow state
∩ target policy ∩ trusted backend availability
= effective capability grant
```
No operation adds capability. Only an authenticated principal with
role-management authority may create, edit, activate, bind, or roll back roles
(L2-D13); agents cannot, ever. Orchestrators cannot deploy seats at all —
coordination goes through `mosaic coord` requests to the coordinator service
(register OD-08/OD-09). Cycle detection is unnecessary because no grant edge
exists.
## Enforcement point: `mosaic-core`
A tracked, non-npm Pi extension loaded via role-scoped settings. Per seat, per
launch: trusted-path manifest load
(`<brainHome>/fleet/roles/<role>/mosaic-core.manifest.json` — committed,
non-symlink, role cross-checked), fail-closed state machine
(`not-applied | applied | fail-closed`), exact active-tool reconciliation,
per-call interception with read containment (canonicalization + realpath +
credential-path deny), per-incarnation append-only journal. Capabilities C1–C8
are the authority; tools are their bound projections; `tools[]` must equal the
bound bindings exactly. The capability map is closed and hardcoded in the
canary phase (gap G7 — by design for now; L2-D21 is the end state).
## The firewall rules
1. **Agents never reach role-management surfaces** (L2-D13). A seat that can
invoke role management can self-grant, voiding the model.
2. **The coordinator is a service, never a seat** (gap G4; register OD-08). A
coordinator-as-seat turns the intersection chain into a cycle.
## Closed vs accepted
**Closed by construction: escalation.** The intersection chain admits no
capability-adding edge.
**Accepted within reason: misdirection.** A rogue or colluding orchestrator can
still _route work_ to a seat that already legitimately holds a capability. The
authority is genuine, so no structural control can refuse it. Jason's ruling:
escalation and misdirection are tiered **by outcome — identically**; locking
down far enough to mechanically prevent misdirection cripples the system to
inoperability. Mitigation posture: **audit and visibility, not prevention** —
see the audit surface in [[UI.1-webui-surfaces]] and gap G3 below.
## Role separation as the working mitigation
Design intent (not yet enforced by any manifest — gap C3 in the lane ledger):
| Role | Must NOT |
| -------------- | -------------------------------- |
| coding agent | create remote git repositories |
| gate agent | code |
| review agent | code |
| interact agent | code, create remote repositories |
Breaking the monopoly means no single seat holds an exit path; escape requires
inter-seat coordination, which is exactly what the audit exists to catch.
Register OD-25–OD-34 (review/gate/arena families, independence rules) carry the
role taxonomy this matrix projects onto.
## Gap register (measured 2026-08-31)
| ID | Gap | Status |
| --- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| G1 | No least-privilege language anywhere in L1/L2/NORTH-STAR (grep-verified) | **L2-D52 amendment drafted** |
| G2 | Assignment issuance criteria unspecified (renewal names criteria; issuance does not) — full-ceiling requests validate cleanly | **L2-D52 amendment drafted** |
| G3 | No misdirection audit exists — no tooling, agent, or surface | open → [[UI.1-webui-surfaces]] §Audit |
| G4 | Coordinator-as-seat would collapse the model | firewall — never violate |
| G5 | Seat config mixes authority classes (role binding beside a model dropdown) | open → [[SEAT.1-seat-profile]] |
| G6 | `role-harness-config/DESIGN.md` scope defect (unstated surface) | fix drafted (amendment in `proposed/docs/`) |
| G7 | Capability map closed/hardcoded | by design (canary phase) |
Amendments staged in `proposed/docs/` per the lane convention; ledger items
A3/A4 track ratification. The auditor-identity question (an auditor agent is
itself a seat, itself subject to misdirection) is on the grill:
[[GOV.5-open-questions]] Q-A1.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/mutator-class-gate.md`
(the default-deny whole-class gate this section's language must match),
`lease-broker-protocol.md` + `lease-broker-security.md` (incl. the named
promote-lease-lost-ACK residual), `ADMIN-GUIDE/security/discord-ingress.md`
(the one implemented admission/role model).
**Pending pulls**: DRAFT S2 `rbac-grant-model.md` (granular RBAC per rev0 §4).
## S2 contract feed (extraction 2026-08-31)
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
- **Three-layer authority (contract 2)**: platform role (member/admin,
instance administration only, **no implicit tenant access** — two live admin
bypass paths named non-conformant and scheduled for retirement:
`command-authorization.service.ts` admin short-circuit, `mcp.service.ts`
scope derivation); hierarchy grants (viewer/member/owner, deny-by-default,
down-chain, effective = max, live fail-closed); workspace membership
(its own mechanism, REQ-ID-001). The layers are non-substitutable.
- **Agents are not a valid grant subject** — grant subject is exactly-one-of
user_id/team_id. Structural enforcement of the agents-never-reach-role-
surfaces ruling, stronger than policy.
- **Consent ≠ authorization (contract 7 §5.7)**: consent records govern
agentic/feature data access, are distinct from hierarchy grants, and confer
no platform authorization; default-deny with **no platform-admin bypass**;
consent mutation is subject-only (admins refused at write time).
- **Bounded revocation propagation**: next authz decision denies; open
Socket.IO connections re-evaluated within 30s or next inbound message.
- **company-CRUD capability**: platform-scoped, admin-assigned, audited
delegation of exactly one visibility-mutation command (`platform_capabilities`
table) — the model's template for narrow capability delegation.
- **Membership locality + no-existence-oracle (contract 8 §3)**:
member-readable workspaces contribute only at their own node, never promoted
upward; unreadable vs nonexistent are byte-equivalent.
## Audit implementation ruling (Q-A1/Q-A2, Jason 2026-09-01)
The authorization audit is **mechanical tooling**: deterministic checks over
the grant/assignment record, witness-style (the S2 writer-coverage pattern),
feeding the audit page read-only. Agents may consume audit output but never
produce the verdict — prompt adherence is not an enforcement mechanism. Q-A2
(who audits the auditor) dissolves: the auditor is code, audited by ordinary
review and CI.
@@ -1,158 +0,0 @@
---
id: CLI.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# CLI.1 — CLI surface and parity obligation
## The rule
The CLI is the **primary execution method** (D8); the WebUI operates the same
tooling over the Gateway API and never bypasses it (D12). Register OD-49 fixes
`mosaic config` as the stable installation-configuration command family backed
by one desired-state engine; register OD-53 makes every interface (CLI, TUI,
WebUI, API, automation) a client of that same engine.
Parity is therefore **structural, not aspirational**: a capability that exists
in the CLI without a WebUI surface is an incomplete projection; a WebUI wish
with no backing tool is **"blocked on tooling"** and the tool is built first
(D8 consequence). Neither side ever grows private logic.
## Parity matrix obligation
The ratified bundle must carry (or cite, per D8's baseline inputs) three
artifacts, kept current:
1. **Tool inventory** — what official tooling exists and what is missing.
2. **WebUI→tool mapping** — every page control mapped to the tool it calls
([[UI.1-webui-surfaces]] page inventory is the row source).
3. **Measured `next`-branch state** — what actually works today.
All three artifacts were measured 2026-08-31 against `origin/next` commit
`9aa4983c` and appear below. Grill: [[GOV.5-open-questions]] Q-C1 (matrix
freshness ownership after ratification).
## Artifact 1 — tool inventory (measured, `origin/next` @ `9aa4983c`)
Registration root: `packages/mosaic/src/cli.ts` (commander); command modules
under `packages/mosaic/src/commands/`; `coord`/`prdy`/`doctor`/runtime
launchers dispatch to bash tools under `packages/mosaic/framework/tools/`
(subcommand tables at `commands/launch.ts:1151–1255`); sibling packages
(`brain`, `forge`, `macp`, `quality-rails`, `log`, `memory`, `queue`,
`storage`) register their own families.
Control-plane-relevant families, by rev1 domain:
| Domain | Families (measured) |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SEAT | `fleet` (init/install/systemd/lifecycle/roster/add/remove/verify/ps), `fleet` roster-v2 CRUD (`get/plan/create/update/delete`), `fleet apply`/`reconcile`/`doctor`/`regen`, `fleet provision`, `fleet migrate-v1 preview`, `agent` (configs + enroll + nested fleet-agent commands), `promote`, `comms send` |
| ROLE | `fleet persona` (`list/show/customize` — baseline ⊕ `roles.local/` overrides), `fleet profile` (`list/show` topology templates) |
| HARN | `config` (framework config + hooks), `compose-contract <harness>`, `skill`, `seq`, `init`/`sync`/`bootstrap`, `doctor`, runtime launchers (`claude`/`codex`/`opencode`/`pi`, experimental `claudex`, `yolo`) |
| PROV | `gateway config` (raw provider API-key env vars only), `wizard` (setup-time provider config) |
| AUTHN | `login`, `auth users {list,create,delete}`, `auth sso {list,test}` (stubbed — see gaps), `auth sessions list` (stubbed), `gateway` token lifecycle (`config rotate-token/recover-token`) |
| SESS | `tui`, `sessions {list,resume,destroy}`, `interaction` (durable-session surface: enroll/attach/send/chat/stop/recover), `coord`, `watch`, `mission` |
| Governance/other | `prdy {init,update,validate,status}`, `federation {grant,peer}`, `macp tasks gate`, `telemetry`, `upgrade`/`update`/`restore`/`uninstall`, `q`, sibling-package families |
Notable structural facts: there is **no top-level `mosaic role` verb** — role
management lives at `fleet persona`, three levels deep; and `doctor`/`status`
exist twice (top-level framework-scoped vs `fleet`-scoped), shadowing by name.
## Artifact 2 + 3 — WebUI→tool mapping with measured state
Rows are the [[UI.1-webui-surfaces]] page domains; measured against
`apps/web/src/spa/pages/` and the gateway controllers on the same commit.
| Surface function | WebUI today | CLI today | Parity state |
| ---------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Seat lifecycle & roster (SEAT) | **no Seats page** (routes are only /admin, /settings, /projects, /tasks, /chat) | complete (`fleet`/`agent` families) | CLI-ahead — page is D2's work, tooling exists |
| Role/persona config (ROLE) | no page | `fleet persona` | CLI-ahead; naming mismatch: no `mosaic role` verb for the ROLE page to mirror |
| User role/ban (AUTHN) | admin UsersTab toggles role/ban via admin endpoints directly | `auth users` lacks `set-role`/`ban`/`unban` | **WebUI-only mutation — violates the D12 rule as implemented** |
| SSO admin (AUTHN) | SsoProviderSection reads _public_ `/api/sso/providers` discovery | `auth sso list/test` stubbed: "admin endpoint missing" | **blocked on gateway tooling**; CLI and WebUI don't even hit the same surface |
| Auth-session admin (AUTHN) | — | `auth sessions list` stubbed (no server endpoint) | blocked on gateway tooling |
| Provider list/test (PROV) | settings ProvidersTab: `GET /api/providers`, `POST /api/providers/test` | none — only `gateway config` raw env-var writes | **WebUI-only read/test — no `mosaic provider` family exists** |
| Default harness/provider/model selection (HARN/SESS) | `GET/PUT /api/chat/preferences/selection` per user | none persists the stored preference (`tui --model` is per-session only) | WebUI-only mutation |
| Authorization hierarchy & grants (UI-audit) | **no page** | **no command** | **the largest D12 gap**: `hierarchy.controller.ts` exposes full CRUD (companies, estates, platform-projects, grants incl. `grants/:id/change`) with audit repository and grant evaluation behind it — reachable only by raw API |
| Federation grants/peers | no page | `federation grant/peer` | CLI-ahead (posture pending Q-T1) |
**Consequences for the build order** (D8: tool first, then surface):
`mosaic provider {list,test}`, `auth users {set-role,ban,unban}`, a stored
harness-selection command, the missing gateway admin endpoints for SSO/session
listing, and a CLI face for the hierarchy/grant surface all precede their
pages. The two **WebUI-only mutations** (role/ban toggle, harness selection)
are standing D12 violations to remediate, not precedents to extend. The
hierarchy CRUD surface is the natural backing for [[UI.1-webui-surfaces]]'s
authorization audit page — but it must get a CLI face and an audit read-path
before the page ships.
## Command families in scope for the control plane
`mosaic config` (OD-49 desired-state engine), `mosaic coord` (agent coordination
boundary — register OD-09), `mosaic prdy` (PRD creation/acceptance — register
OD-22), role management (one canonical API, L2-D14), seat lifecycle
(launch/relaunch per register OD-59), `mosaic doctor` (drift detection classes,
e.g. the #1194 framework-tool drift addendum in [[GOV.4-workstream-contracts]]).
## `mosaic config` v1 subset (pulled 2026-08-31 from the minimal-subset spec)
The Q14 ruling (2026-08-29) fixes the current scope: shipped surface
`edit/get/set/show/hooks/path` **plus exactly two new read-only verbs** —
`mosaic config validate` and `mosaic config plan` (`--file` | `--preset`,
mutually exclusive; `--format table|json`). `apply`, `add`, `restructure`,
`migrate`, `remove`, `export` are **out of v1 pending a full-engine ruling**
([[GOV.5-open-questions]] Q-D5) — the configuration-lifecycle draft's "stable
namespace" table describing the full family is aspirational, not current state.
Contract highlights: **valid** vs **conformant** are distinct verdicts with
distinct exit codes (nonconformance is a diagnostic, not a parser failure);
`plan` emits `create|update|blocked` operations with risk classes
(`none|review-required|full-engine-required`), a SHA-256 `planId`, and
`applySupported: false` always in v1; destructive/unsupported drift is
`blocked`, never silently normalized; absolute no-mutation during
validate/plan (no writes, no network, no credential calls); results ride the
T78 `CapabilityResultV1` envelope (capability IDs
`config.installation.validate`/`.plan`); inputs capped, YAML
aliases/anchors/tags rejected, no secret-shaped fields accepted or echoed.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `requirements/cli-capability-migration.md` (T78,
`source_of_truth: true`), `fleet/reference/cli.md` (local fleet CLI vs
gateway-backed catalog), `USER-GUIDE/getting-started/quickstart.md`.
**Pending pulls**: DRAFT S2 `tool-gateway-mapping.md`; brain
`docs/specs/2026-08-29_mosaic-config-minimal-subset.md` (`mosaic config
validate/plan`, cites OD-49–OD-55) and
`docs/guides/proposed/workflows/configuration-lifecycle.md` (the OD-49 engine
family definition).
## S2 contract feed (extraction 2026-08-31)
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
- **Contract 5 §4.5 is the parity clause this section's matrix enforces**:
CLI remains the primary execution method for every Gateway command; no
WebUI-only command exists; a Gateway command without CLI exposure is a
conformance gap tracked at the family's implementing issue. The
hierarchy/grants CRUD gap measured in this section is exactly such a
tracked conformance gap once contract 5 ratifies.
- **Command envelope**: typed request/result DTOs (no `any`), closed
per-family error taxonomy, audit correlation id, fail-closed — aligns with
the T78 `CapabilityResultV1` direction already in this section.
- **Contract 9 (api-artifacts)**: `ApiAuthClass` closed six-value enum
(`none`/`session`/`api-key`/`admin`/`federation`/`bootstrap`); OPENAPI.yaml
generated, CI byte-drift-gated, never hand-edited; hard ordering — nothing
under contract 9 lands before contract 5 (PR #1438) is on the trunk.
- **Roll-up (contract 8)** ships as a query-only tool with no command
counterpart (A5 rank 5) — the taxonomy precedent for read-only surfaces in
the parity matrix.
- **Mandated removals** the CLI inventory must track: `mosaic auth users
delete` (with the hard-delete endpoint) is required to be disabled/removed
by contract 4.
## Parity freshness ruling (Q-C1, Jason 2026-09-01)
The parity matrix becomes a generated artifact with a CI drift-gate witness in
the stack repo (the contract-9 pattern): CI regenerates the tool inventory and
WebUI→tool mapping from code and fails on divergence from the committed
matrix. No human cadence to forget. Building the witness is E6-return
follow-up work.
@@ -1,159 +0,0 @@
---
id: DATA.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# DATA.1 — Record-class authority and the configuration data model
Merges J1 (2026-08-23), operator register OD-13/OD-48/OD-49–OD-52, and the lane's
`CONFIG-MODEL.md` findings into one authority table. See
[[GOV.3-decision-map]] for registry identities.
## The rule (J1)
- **Git owns** reviewed governance and declarative definitions.
- **PostgreSQL owns** runtime state and projections.
- Flat files on disk are **generated projections**, never authority (L2-D19).
## Authority table
| Record | Authority | Rationale |
| --------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Role Definition / Role Revision | **Git** | reviewed governance; revisions immutable and digested |
| `mosaic-core.manifest.json` | **Git** | committed, non-symlink, trusted-path — the loader refuses anything else |
| PRD revision bundles (`docs/PRDs/`) | **Git** | immutable accepted versions (register OD-17) |
| Portable config blueprint | **Git** (`fleet/configuration/installation.yaml`, register OD-50) | declarative desired state |
| Host bindings | ignored `config/installation.local.yaml` (OD-50) | host-local, never authority over roles/gates (OD-52) |
| Role Binding (seat → revision) | **Postgres** | runtime state; control-plane mutable |
| Seat record (harness, model, workdir, auth account) | **Postgres**, projected to flat files | runtime state |
| Leases, checkpoints, session/incarnation state | **Postgres** / coordinator (register OD-57–OD-60) | runtime state with fencing |
| `settings.json`, `launch.env` | **generated projection** | L2-D19; no writer may treat them as source |
Transition rule: the WebUI may edit flat files during the transition, but the
end state is exactly the table above — every flat file regenerated from Git or
Postgres, never authored directly.
## Seat file consolidation (lane Q1 — proposed, not ratified)
Current flat-file state (measured 2026-08-31):
| File | Carries |
| ---------------------------------------------- | -------------------------------------------------------------- |
| `fleet/agents/<seat>/launch.env` | model, workdir, reasoning level (hand-maintained, git-ignored) |
| `fleet/agents/<seat>/profile.json` | role — read by `mosaic-core`'s trusted-path loader |
| `fleet/roles/<role>/.pi/agent/settings.json` | provider, model, extensions, skills paths |
| `fleet/roles/<role>/mosaic-core.manifest.json` | capability/tool authority (schema v3) |
Proposal: one `profile.json` rules seat information (role, harness, model,
reasoning, workdir, overlay, authentication account); `launch.sh` reads it
instead of `launch.env`. Register OD-48 already ratifies `profile.json` as the
seat's **structured identity** file, which this consolidation completes.
**Blocking consideration:** `mosaic-core` reads `profile.json` at every
`session_start` to resolve the role. Widening the file widens the read surface
of a trusted-path load. The loader must keep ignoring unknown keys (it reads
only `.role` and already does); the file must stay non-symlink and committed.
Verify `lib/loader.ts seatRole()` before landing. Open on the grill list:
[[GOV.5-open-questions]] Q-D1.
## Multi-provider authentication shape (lane Q2 — open)
`profile.json` must name the authentication account a seat uses, across
providers (Claude, OpenAI, ZAI, N others; OAuth or API key; local providers).
Unresolved: one account vs a per-provider map, and the credential-broker
relationship. The enforced role manifests hold **no** credentials
(`store: none, providerTokens: denied`) — this is a launcher/broker concern,
never a manifest concern. Grill: [[GOV.5-open-questions]] Q-D2. See
[[AUTHN.1-auth-accounts]] for the account model itself.
## Reconciliation obligation (lane Q4)
Every change made through CLI or WebUI automatically configures authentication,
`settings.json`, and required symlinks — the user never touches a file. Two
directions with different timing (L2-D17): capability **removal** denies
centrally and immediately; capability **addition** waits for runtime
reconciliation and attestation.
**Hazard to settle first:** Pi settings ownership is ambiguous today
(`launch-seat.sh:259–261` symlinks `.pi/agent/settings.json` under
`MOSAIC_SEAT_HOME=1` while the `MOSAIC_SEAT_CONFIG=1` seed fires on `! -s`,
which the symlink satisfies). Settle ownership before the WebUI becomes a third
writer. Grill: [[GOV.5-open-questions]] Q-D3.
## Configuration file authority (pulled 2026-08-31 from the mosaic-config v1 spec)
Four config records with fixed authority (brain spec `2026-08-29_mosaic-config-minimal-subset.md`, register OD-49–OD-55):
| Record | Path | Authority |
| ------------------ | --------------------------------------------------- | --------------------------------------------------- |
| Central registry | `~/.config/mosaic/config.json` | resolves brainHome/socket/paths |
| Portable blueprint | `<brainHome>/fleet/configuration/installation.yaml` | tracked, secret-free desired state |
| Host bindings | `<brainHome>/config/installation.local.yaml` | git-ignored; **runtime and working directory only** |
| Packaged presets | immutable, versioned (`bootstrap-minimal@1`) | never `latest` |
Precedence, high to low: constitution/safety (deny-wins, OD-51) → framework
schema/profile/role/roster contracts → blueprint/preset → host bindings →
framework binding defaults → observed state (**compared, never authoritative**).
Host bindings can never change profile, seat selection, roles, authority,
reviews, gates, or safety (OD-52) — the data-model enforcement of the
[[AUTHZ.1-capability-authority]] intersection chain. Validation distinguishes
**valid** (structurally sound) from **conformant** (observed == desired) with
distinct exit codes.
_Triage note:_ the intended-state reconciler spec was judged **operator
host-ops tooling** on full read (systemd/tmux monitoring of the operator
estate) — not product scope; its conformance idea is already covered by the
validate/plan model above. E2-inputs pull downgraded to SKIP.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `requirements/native-kanban-sot.md` (ratified, the
D13 base), `native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md` + `KBN-101-ENVELOPE-A.md`
- `SHARED-CONTRACT.md` (frozen contracts), `ADMIN-GUIDE/operations/upgrade-safety-and-recovery.md`
(PGlite tier support boundary), `fleet/reference/roster-v2-fields.md`.
**Pending pulls**: DRAFT S2 contracts `hierarchy-schema.md`, `custody-schema.md`,
`rollup-projection.md`, `mode-conversion.md` (**predates D15 — reconcile first**,
Q-T4); brain `docs/specs/2026-08-28_intended-state-reconciler.md` (reconciler spec).
**Conflicts on the grill**: deployment/federation posture, Q-T1.
## S2 contract feed (extraction 2026-08-31)
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
- **Hierarchy schema (contract 1)**: five tables, single-parent FK chains, no
parentage edge tables, **no `owner_id` column** — ownership only via grants.
All mutations through the sole-writable-SOT audited Gateway command path;
three-prong writer-coverage CI witness.
- **Custody schema (contract 7)**: sensitive content only in the user's
git-tracked brain; Postgres holds pointers/consent/registry only ("not as
text, not as excerpts, not as embeddings"); content-first-then-pointer write
protocol with brain fence; HMAC content hashes (no oracle); mode-independent
schemas with a `custody_config` singleton.
- **Roll-up (contract 8)**: the corpus's strongest projection-never-authority
statement — non-authoritative, recomputable, never gates work, enforced by
read-only DB transactions (mechanical, not conventional). Direct precedent
for this section's record-authority chain.
- **Route metadata records (contract 9)**: metadata as _registration input_ —
auth guard derived from the record makes record-vs-code divergence on those
fields structurally impossible; generated OPENAPI.yaml is committed and
PR-reviewed yet strictly non-authoritative ("generation documents the code;
it does not ratify it") — drafting precedent: "generated" ≠ "uncommitted".
- **Authoritative DB settings rows** — ruled (Q-T5, Jason 2026-09-01):
generated settings _files_ are projections of the active Role Revision,
never authority (L2-D19); DB settings records written through audited
Gateway commands (`platform_mode`, `registration_mode`, `custody_config`,
`bootstrap.seed-company-name`, and successors) are records of authority
like any other SOT row.
## Seat-record rulings (Jason 2026-09-01)
- **Q-D1 — one seat record**: `launch.env` folds into `profile.json`; loader
tolerance verified (`seatRole()` reads only `role` from a generic record).
- **Q-D2 — per-provider auth map**: `profile.json` carries a per-provider map
of credential-broker references (`environment/service/component/secret-name`),
never secrets; one seat, N providers, zero secrets in the brain tree.
- **Q-D3 — one settings writer**: the role-projection engine is the sole
writer of seat settings files; the launcher invokes the projector rather
than seeding; hand edits are drift flagged by `validate` (L2-D19 + Q-T5).
@@ -1,107 +0,0 @@
---
id: GOV.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# GOV.1 — PRD lifecycle: SOT, shim, revisions, archival
Ratified structurally by Jason, 2026-08-31 (this lane's grill session). Applies
to the Mosaic Stack PRD in `mosaicstack/stack` (integration trunk `next`).
## The PRD is the project SOT
The PRD is the source of truth for the entire project, independent of any
mission. It is not linked to a current mission and is never overwritten by
milestone work — the pre-2026-08-26 pattern of repurposing `docs/PRD.md` per
milestone is retired. Mission documents are separate: they **reference** the
PRD; they never usurp it. This maintains alignment over time.
## Shim
`docs/PRD.md` is a permanent shim, not the PRD body:
- Frontmatter: `kind: shim`, `current_rev:` pointing at the live revision
bundle.
- Body: one-paragraph summary and a link into `docs/PRDs/`.
- Updating the PRD means ratifying a new revision bundle and repointing the
shim. The shim's path never changes, so every external reference to
`docs/PRD.md` stays valid forever.
## Revision bundles
Each ratified revision is a **frozen bundle directory**:
```
docs/PRDs/YYYY-MM-DD_PRD_revN/
PRD.md # the assembled PRD for this revision
PRD.0-index.md # order authority + domain registry as of this revision
<DOMAIN>.<n>-*.md # every section document, frozen with the PRD
```
The PRD and its supporting sections freeze **as a set** — a revision whose
sections keep moving underneath it is not a revision. Live editing never
happens in `docs/PRDs/`; the next revision is drafted in a lane
(class‑2 draft-natives per the lane's `proposed/README.md`) and lands as a new
bundle.
## Archival, never deletion
A superseded revision is never deleted and never edited. Versioning is
maintained: every revision that was ever current remains in `docs/PRDs/`
verbatim. Supersession is expressed only from outside the bundle: the shim
points elsewhere, and the dated directory names plus the shim's git history are
the supersession record. The frozen bundle itself is never touched — not even
to add a `superseded_by:` marker.
## Immutability is convention, not enforcement
No hook or CI guard protects `docs/PRDs/` today. If teeth are wanted later, a
CI check that files under `docs/PRDs/` never change after merge is cheap; that
is a separate, future decision.
## Lineage
- rev0 = the 2026-08-26 "North Star" PRD currently at `origin/next:docs/PRD.md`
(commit `9aa4983c`, sha256 `60cc2f98…36afdf`; lane snapshot
[rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)). On ratification of rev1 it archives as
`docs/PRDs/2026-08-26_PRD_rev0/PRD.md` — a one-file bundle, so every revision
has the same shape.
- rev1 = this lane's draft bundle (`proposed/docs/PRDs/2026-08-31_PRD_rev1/`),
combining rev0 with the control-plane-surfaces and agent-runtime-ng lane
findings and the reconciled docs corpus.
Related: [[PRD.0-index]] for naming and ordering; the lane `proposed/README.md`
for draft-stage conventions.
## Registry mechanics (pulled 2026-08-31 from the prd-registry draft)
The operator draft `operations/prd-registry.md` independently specifies the
same lifecycle and adds mechanics this doc adopts:
- **The registry, not the shim, is authoritative.** The shim is generated,
regenerated on every acceptance and amendment; a missing or ambiguous shim
entry is a generation defect, never an authority question.
- Registered versions carry: stable PRD ID, canonical filename/slug, version +
status, acceptance timestamp **and actor**, lineage, content digest,
requirement IDs. Anonymous or inferred acceptance is invalid.
- Missions pin PRD ID + version + digest + in-scope requirement IDs; a digest
mismatch between pin and artifact **blocks the readiness transition** (both
digests shown as evidence).
- Amendment creates a new immutable version; superseded versions remain
queryable as lineage; **requirement IDs are never reused**.
- Agents read and resolve; they never register versions, rewrite artifacts, or
select an implicit latest.
## Registry prefixes (Q-G2, 2026-09-01)
Every decision-bearing document declares a unique registry prefix; file-local
bare D-numbering is prohibited. Three registries are live: the stack PRD
registry (**D1–D15**, and successors **Dn** as new stack decisions ratify),
the operator DECISION-REGISTER (**OD-01…OD-65**, renamed from its former
zero-padded `D01`–`D65` form per this ruling), and the agent-runtime-ng
contract decisions (**L1-Dnn**/**L2-Dnn**) — three distinct namespaces that
must never be conflated. Any new decision-bearing document must declare its
own unique prefix in its header before citing decisions. In text written
before 2026-09-01, a zero-padded bare `Dnn` reads as the operator register's
`OD-nn`.
@@ -1,151 +0,0 @@
---
id: GOV.2
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# GOV.2 — Documentation inventory & supersession triage
E2 record and, at ratification, the PRD's answer to mandate item 4 (no central
location; drift and naming confusion). Verdict vocabulary, per document:
**canonical** | **superseded-by <ref>** | **conflict-with <ref>** |
**working-notes** | **dead** | **operator-only** (brain corpora: correct home is
the operator estate; nothing migrates).
Full per-file verdict tables live in the lane evidence record
`fleet/lanes/control-plane-surfaces/TRIAGE-2026-08-31_e2-verdicts.md`
(point-in-time; this section carries the durable conclusions).
## Corpora
| # | Corpus | Files | Scanned at |
| --- | ----------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------- |
| 1 | `mosaicstack/stack` `origin/next:docs/` | 346 | commit `9aa4983c`, triaged 2026-08-31 (141 live files per-file; archive dirs swept for orphaned decisions) |
| 2 | `~/.mosaic/docs/` (excl. guides/proposed) | ~76 | triaged 2026-08-31 |
| 3 | `~/.mosaic/docs/guides/proposed/` | 79 | triaged 2026-08-31 |
| 4 | `fleet/lanes/agent-runtime-ng/` + `fleet/lanes/control-plane-surfaces/` | — | live lanes, canonical by definition for their scope |
## Corpus 1 — stack `origin/next:docs/` — conclusions
195 of 346 files (56%) were pre-triaged by the repo's own archive structure
(`archive/` 135, `_old_structure/` 60). The 141 live files triaged per-file:
**The healthy core.** The five guide trees (DEVELOPER-GUIDE, ADMIN-GUIDE,
USER-GUIDE), `fleet/` (concepts/how-to/reference/operations/migration),
`native-kanban-sot/`, `webui/`, `API/`, `tess/`, `release-integrity/`, and the
root atlas docs (README, ROADMAP, SITEMAP) are overwhelmingly **canonical** and
internally consistent. Load-bearing canonical anchors for this PRD:
`requirements/native-kanban-sot.md` (ratified, D13), `KBN-101-DB-ROLE-SPLIT.md`
(frozen), `requirements/cli-capability-migration.md` (T78),
`fleet/NORTH_STAR.md` + `FLEET-DOCTRINE.md`, `mutator-class-gate.md` and the
lease-broker pair (AUTHZ ground truth), `compaction-revocation.md` (SESS ground
truth), `sso-providers.md` (D10/AUTHN ground truth),
`mos-runtime-portability-m1.md` (the only current PROV identity ADR),
`web-dashboard.md` (UI route-by-route ground truth).
**The prime successor material.** The nine DRAFT "webui-audit S2" contracts in
`requirements/` (hierarchy-schema, rbac-grant-model, onboarding-wizard,
identity-lifecycle, tool-gateway-mapping, mode-conversion, custody-schema,
rollup-projection, api-artifacts) are unratified but decision-traceable per
clause to rev0 D-numbers — the most direct feed for DATA/AUTHZ/AUTHN/UI/CLI.
`mode-conversion.md` predates D15 and needs reconciliation before it ratifies
([[GOV.5-open-questions]] Q-T4). Per-contract extraction completed 2026-08-31
(lane `S2-EXTRACTION-2026-08-31.md`): normative cores, dependency edges, and
ruling cross-checks pulled into the DATA/AUTHZ/AUTHN/UI/CLI sections; two
reconciliation questions raised (Q-T4 sharpened — no S2 file references D15;
Q-T5 — projection-rule scope vs authoritative DB settings records).
**Conflicts requiring a ruling** (all carried in [[GOV.5-open-questions]] Q-T1):
| Document | Conflict |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| root `MISSION-MANIFEST.md` (2026-07-14) | makes federated-tier the "canonical MVP deployment topology", Federation v1 top-priority — vs D3 (deferred) and D15 (compose standalone canonical) |
| `federation/MISSION-MANIFEST.md` (2026-04-21) | Federation v1 as active in-progress M3 — vs D3 and ROADMAP P5 "deliberately undesigned" |
| `guides/deployment.md` | blocks Compose activation pending KBN-101 gates — vs D15's `docker compose up` v1 bar |
| `scratchpads/mvp-20260312.md` | records a _completed_ Federation M2 milestone (peer certs, grants, ScopeService, Step-CA) — vs D3's "deferred" framing |
Code reality, verified 2026-08-31 at `9aa4983c` (dossier: lane
`FEDERATION-DOSSIER-2026-08-31.md`): federation M1–M3 are shipped and wired
behind a `tier === 'federated'` gate — M3 landed 2026-06-24/25, beyond what any
doc records — M4–M7 absent, dormant since 2026-06-25, absent from the canonical
`docker-compose.yml` (so code topology is consistent with D15), and tracked
nowhere since the TASKS.md → NORTH_STAR.yaml supersession. The three stale docs'
claims date to 2026-04 by true content edits. **Ruled B ("shipped but
frozen"), Jason 2026-09-01** — see [[GOV.5-open-questions]] Q-T1 for the
amendment consequences the E6 return carries.
**Superseded set** (all with explicit in-file or index-level signals): root
`TASKS.md`, `fleet/TASKS.md`, `federation/TASKS.md` (→ `fleet/NORTH_STAR.yaml`);
`fleet/PRD.md` and `fleet/PRD-fleet-suite.md` (→ root `PRD.md`, per
`fleet/README.md`); `native-kanban-sot` initial NO-GO review (→ GO re-review).
`plans/`, `reports/`, `scratchpads/` are working-notes/evidence, never spec —
consistent with their own README disclaimers.
**Orphaned decisions found in the archive sweep** (ratified once, absent from
D1–D15 and every live doc; disposition on the grill, Q-T2):
1. "No Python" monorepo ruling (`archive/planning/monorepo-consolidation/board-review.md:742`).
2. Matrix/MACP "exactly three supported modes" install-topology ruling, Mode A
split-domain primary; its DNS/domain prerequisite ruling still open
(`archive/planning/matrix-macp/rfc-002:133`, `rfc-001:428`).
3. OpenBrain cut from WP1/WP2 consolidation scope (`board-review.md:611`).
## Corpora 2 + 3 — `~/.mosaic/docs/` — conclusions
The overwhelming majority is **operator-only**: generic engineering standards,
fleet role playbooks, SDLC gates, ops pages, fleet Q&A rulings, incident
methods, host-specific plans. Correct home is the brain; nothing migrates.
Notable verdicts:
- `docs/PRD.md` — **trap confirmed** (N1): it is the pi `/goal` extension PRD.
Superseded in substance by [[GOV.4-workstream-contracts]] §Pi Persistent Goal
Loop (#1150).
- `docs/guides/proposed/workflows/prd-lifecycle.md` — superseded by
[[GOV.1-prd-lifecycle]] (the draft lifecycle this bundle ratified).
- `docs/plans/2026-08-25_unified-roadmap.md` (T72 consolidation charter) —
superseded by this rev1 consolidation, its successor.
- `docs/MOSAIC-CANON.md` vs `docs/STRUCTURE-CANON.md` — mutual conflict: both
claim to be "the canonical definition of a mosaic-brain" (653 vs 127 lines,
divergent sections); STRUCTURE-CANON is the copy everything links to.
Operator-side ruling needed (Q-T3).
- Dead: `plans/2026-08-22_config-json-schema.md` (delivered as stack#1382), two
closed questions.
**Migration candidates** — nine working-notes whose product content should feed
rev1 sections during refinement (full list with rationale in the lane evidence
record): `specs/2026-08-29_mosaic-config-minimal-subset.md` → CLI;
`specs/2026-08-28_intended-state-reconciler.md` → DATA;
`operations/seat-identity.md` → SEAT; `operations/vault.md` → AUTHN;
`runtime/adapter-contract.md` → HARN; `SPECIALIZATION-MODEL.md` → ROLE;
`workflows/session-lifecycle.md` → SESS; `operations/prd-registry.md` → GOV;
`workflows/configuration-lifecycle.md` → CLI/GOV.
`COORDINATION-CONTROL-PLANE.md` and `workflows/coordination-lifecycle.md` are
product-adjacent but belong to the `agent-runtime-ng` lane's L1/L2 scope, not
this bundle (corpus-4 boundary).
## Corpus 4 — the two lanes
Canonical for their scope by definition (they are the drafting record).
`agent-runtime-ng` owns L1/L2 contract text; `control-plane-surfaces` owns
surface/config findings and this bundle. One-way dependency: surfaces cite
contracts, never the reverse.
## Naming defects register (mandate item 4)
| # | Defect | Fix proposed |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| N1 | `~/.mosaic/docs/PRD.md` is the pi goal-extension PRD wearing the project-PRD name (confirmed 2026-08-31) | rename to a goal-extension-scoped name during return |
| N2 | Stack local `main` is a divergent unpushed fork that shadows `next` | already a lane convention; PRD states trunk identity explicitly |
| N3 | Pre-2026-08-26 pattern: `docs/PRD.md` overwritten per milestone | retired by [[GOV.1-prd-lifecycle]] shim model |
| N4 | Two decision registries share the D-prefix ID space (stack D1–D15 vs operator D01–D65); "D8" is ambiguous without registry name | OD- prefix applied in rev1 (Q-G2 ruled 2026-09-01); collision rule in [[GOV.3-decision-map]] |
| N5 | Triple "PRD" collision in the stack tree: root `PRD.md` vs superseded `fleet/PRD.md` and `fleet/PRD-fleet-suite.md`, with no local supersession signal on the latter two | supersession banner in-file; long-term, the GOV.1 rule that the bare name `PRD.md` is reserved for the shim |
| N6 | "Tess" and "Ultron" each name two different things: non-authoritative roster-class display aliases (fleet how-tos) vs the named product agent / validator identity (TESS workstream, native-kanban-sot) | rev1 text always qualifies which sense is meant; flag for upstream rename of the aliases |
| N7 | Six stack `TASKS.md` files under three authority regimes (banner-superseded / explicitly-not-superseded / silently active) — the filename signals nothing | uniform status frontmatter on every TASKS.md; superseded ones point at NORTH_STAR.yaml |
| N8 | Duplicate basenames across stack dirs: `gateway-security-20260313.md` (qa vs code-review, different content), `2026-08-10-docs-catalog-audit.md` (plan vs report), `1099-pipefail-sweep.md` (report vs scratchpad copy) | disambiguate on next touch; prune the unpromoted scratchpad copy |
| N9 | `guides/` is outside the canonical tree per `docs/README.md` yet "protected current authority" per `SITEMAP.md` — contract and sitemap disagree | reconcile the documentation contract; likely fold the four guides into the guide trees |
| N10 | Two live front-matter schemas (`type`/`status: current…` per docs/README.md vs the newer `kind`/`status: active…` used by most files) — collision documented in the w4 worklist, unresolved | settle the schema in the documentation contract as part of E6 return |
| N11 | Three uncross-referenced descriptions of the `/goal` capability: brain `docs/PRD.md`, `operations/goals.md`, and GOV.4 §#1150 | reconcile under the #1150 identity; brain docs cite it |
| N12 | Brain-side: `MOSAIC-CANON.md` vs `STRUCTURE-CANON.md` both claim canon status | operator ruling (Q-T3); retire or fold the unreferenced copy |
| N13 | Forward-looking: rev0 Part II (RI-N3) rules `docs/PRD.md` "not a peer authority" once `docs/prdy/` lands — a third contender in "which PRD is real" | GOV.1 disambiguation: prdy is tooling-facing storage; the shim + bundle remain the human-facing SOT chain |
| N14 | Commit `a480ee83` (2026-08-21) mass-stamped `status: active` frontmatter across stack docs without content review — status metadata rubber-stamps stale docs as current (root MISSION-MANIFEST's "Last Updated: 2026-07-14" is likewise cosmetic; true content edit 2026-04-19) | status/date frontmatter changes only alongside content review; triage dates by git content edits, never frontmatter |
@@ -1,121 +0,0 @@
---
id: GOV.3
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# GOV.3 — Consolidated decision map
Every ratified decision set that binds this PRD, in one place, with the
collisions between their numbering spaces made explicit. This section exists
because the estate carried at least four independent decision registries whose
IDs overlap — a reader seeing "D8" could not know which law was meant.
## The registries
| Registry | IDs | Ratified | Where | Scope |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Stack PRD registry | **D1–D15** | 2026-08-25/30 | rev0 §12 → [[GOV.3-decision-map]] (this file, below) | product north star |
| Operator decision register | **OD-01–OD-65** (renamed from D01–D65 per Q-G2, 2026-09-01; the brain-side source doc renames on its next touch and carries a redirect table) | 2026-08-28 (Q1–Q92 review) | the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped) | roles, coordination, PRD lifecycle, configuration, checkpoints |
| L2 authorization decisions | **L2-D01–L2-D51** (+ proposed **L2-D52**) | rolling | `fleet/lanes/agent-runtime-ng/MECHANICAL-AGENT-RUNTIME-L2-AUTHORIZATION.md` | mechanical agent-runtime authorization |
| Control-plane rulings | **J1–J5** | 2026-08-23 | `fleet/lanes/docs/mosaic-control-plane/rulings-J1-J5.md` | record-class authority |
| PRD structural rulings | (unnumbered, 8 rulings) | 2026-08-31 | [[PRD.0-index]] §Structural rulings | this bundle's lifecycle |
**Collision rule:** zero-padded `D01`-form IDs = operator register; bare `D1`-form
= stack PRD registry; `L2-D` = L2; `J` = control-plane rulings. Writing a bare
"D8"-style reference without its registry name is a defect (naming register
[[GOV.2-docs-inventory]] N4).
## Stack PRD registry D1–D15 (carried from rev0 §12)
| ID | Decision (short form) |
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred. **Amended 2026-09-01 (Q-T1 ruling B, "shipped but frozen")**: federation M1–M3 exist in code behind `tier === 'federated'` (M3 landed 2026-06-24/25), are excluded from the v1 bar and frozen; tracked as a dormant workstream in `docs/fleet/NORTH_STAR.yaml`; the frozen cert/auth code carries a security re-audit gate before any resumption; the design itself stays deferred and unforeclosed |
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
| D5 | North star = docs/PRD.md rewrite; stack docs/ = product SSOT |
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
| D7 | Spec-inventory sweep (executed; T2 baseline frozen 2026-08-25) |
| D8 | webUI sits over official framework tooling; CLI primary |
| D9 | Not a hosted business; company = organizational separation for one operator |
| D10 | better-auth is the account system of record; external IdPs via OIDC |
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
| D13 | workspace_id stays the hard isolation unit; kanban SOT amended, not rewritten |
| D14 | Sensitive profile data in the user's own brain only |
| D15 | Tiered containerized deployment: compose standalone + phase-gated k8s |
Full texts: rev0 §12 and the operator decision log (USC estate brain,
webui-audit lane, `GRILL.md`).
## Operator register decisions this PRD leans on hardest
Full set: the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped). Load-bearing here:
| ID | Ruling (short) | Consumed by |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| OD-02/OD-03 | one role per seat; role change = clean session, ephemeral context discarded | [[SEAT.1-seat-profile]], [[SESS.1-session-continuity]] |
| OD-08/OD-09 | coordinator service owns leases/deployment; orchestrators never deploy seats directly | [[AUTHZ.1-capability-authority]] |
| OD-16–OD-23 | PRD owns requirements; immutable accepted versions; `docs/PRD.md` = generated pointer under `docs/PRDs/`; missions pin PRD version+digest; `mosaic prdy` owns PRD creation | [[GOV.1-prd-lifecycle]] — **independently re-derived in the 2026-08-31 grill before this register was consulted; the two agree** |
| OD-48 | instance contract: `profile.json` structured identity, `overlay.json` generated composition | [[SEAT.1-seat-profile]] |
| OD-49–OD-53 | `mosaic config` desired-state engine; blueprint + host-binding split; precedence chain; **all interfaces (CLI/TUI/WebUI/API) share one CLI-backed engine** | [[DATA.1-record-authority]], [[CLI.1-parity]], [[UI.1-webui-surfaces]] |
| OD-54 | WebUI drafts are revisioned server-side desired-state; no effect until planned and applied | [[UI.1-webui-surfaces]] |
| OD-57–OD-61 | checkpoints tied to incarnation+lease; coordinator-run relaunch (checkpoint→stop→apply→clean incarnation→restore); fencing; full restart recovery | [[SESS.1-session-continuity]] — **this is the ratified mechanism for mid-stream harness/model/provider switching** |
| OD-62–OD-65 | watchdog, outage fail-closed, failure isolation/reporting | [[AUTHZ.1-capability-authority]], [[UI.1-webui-surfaces]] (audit/alerts) |
## Reconciliation notes
- Register OD-13 (repository-backed mission state canonical first, DB later behind
the same interface) and J1 (Git owns governance, PostgreSQL owns runtime
state) are compatible: OD-13 governs _mission_ state migration order; J1 governs
steady-state record classes. [[DATA.1-record-authority]] carries the merged
table.
- Register OD-18's "generated pointer" is stricter than the 2026-08-31 grill's
hand-maintained shim: **adopted** — the shim should be generated by tooling,
not hand-edited ([[GOV.1-prd-lifecycle]] inherits this).
- Proposed, not yet ratified: **L2-D52** (least-privilege Assignment issuance),
staged at `proposed/docs/MECHANICAL-AGENT-RUNTIME-L2-AUTHORIZATION--least-privilege-issuance.md`.
## Extraction cross-check notes (2026-08-31)
- Five highly product-normative operator drafts carry **no decision-register
citations at all** (seat-identity, vault, adapter-contract,
SPECIALIZATION-MODEL, prd-registry). Their rules were pulled into sections on
their merits; before E6 they must be cross-checked against the register
rather than assumed pre-vetted.
- The intended-state-reconciler spec uses a **file-local D1–D6 numbering** that
is neither the stack registry nor the operator register — a live instance of
the N4 prefix-collision defect. Do not conflate when compiling
cross-references.
- The session-lifecycle draft is the densest register consumer (OD-03/OD-04/OD-08,
OD-56–OD-65) and is likely the canonical drafting source for OD-56–OD-65; its
one-relaunch-path gap is Q-S4.
## Re-ratified orphaned decisions (Q-T2, Jason 2026-09-01)
Ratified once in archived planning docs, absent from every live document until
this map; re-ratified as live constraints:
- **No Python in the monorepo** (source:
`archive/planning/monorepo-consolidation/board-review.md:742`).
- **Matrix/MACP: exactly three supported install modes, Mode A (split-domain)
primary** (source: `archive/planning/matrix-macp/rfc-002:133`). Its
DNS/domain prerequisite ruling remains open — [[GOV.5-open-questions]] Q-T6
blocks Matrix install work, not this map.
- **OpenBrain excluded from WP1/WP2 consolidation scope** (source:
`board-review.md:611`).
## Registry prefix ruling (Q-G2, Jason 2026-09-01)
Distinct prefixes at source: stack keeps **D1–D15**; the operator
DECISION-REGISTER renames to **OD-01…OD-65** with a redirect table in the
source doc. Applied in this bundle: every stack-side citation of the operator
register now reads **OD-nn**; the brain-side source doc itself still carries
its old zero-padded `D01`–`D65` numbering and renames (with the redirect
table) on its next touch. File-local D-numbering in drafts (the live N4
instance: the reconciler spec's D1–D6) is prohibited — every decision doc
declares a unique registry prefix. For any text predating 2026-09-01 not yet
swept into this bundle, the old reading rule still applies: a zero-padded bare
`Dnn` is the operator register (now read as `OD-nn`); a bare `Dn`/`Dnn` in the
1–15 range without a zero pad is the stack registry.
@@ -1,671 +0,0 @@
---
id: GOV.4
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# GOV.4 — Active workstream contracts (preserved unchanged)
Carried verbatim from rev0 ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) lines
258–910) under rev0's own rule: open issues bind to these contracts; this
revision moves no text and changes no requirement in them. They graduate out
individually when their workstreams close.
The sections below are normative, in-flight workstream contracts carried over
verbatim from the previous revision of this file. Open issues bind to them.
This rewrite moved no text and changed no requirement in them; they are
governed by their own issues and review gates, and they graduate out of this
file individually when their workstreams close.
## Current addendum: #1194 — Installed framework-tool drift detection
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
### Problem and objective
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
### Requirements
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
### Acceptance criteria
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
---
## 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)
### Problem and objective
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
gateway-backed agent catalog.
### Normative requirements
| ID | Requirement |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
### Acceptance criteria
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
### M0 implementation gate
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
are defined in [docs/TASKS.md](../../TASKS.md) and must remain one card/one PR.
### 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)
### Problem and objective
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
infer, producing incorrect host, session, socket, or helper targets. The objective is one
roster-resolved communications contract that every supported harness receives unchanged.
### Normative requirements
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
resolver. A second lenient communications parser is forbidden.
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
generation.
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
fail closed.
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
substitute, or fuzzy-match targeting values.
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
contract.
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
communications data through the common runtime composer.
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
`TOOLS.md` content SHALL remain preserved.
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
NOT rewrite active context, restart a session, or mutate a live fleet.
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
### Acceptance criteria
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
identity contains exact host/session/socket/helper values.
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
exact targeting and fail-closed behavior.
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
discovery command; no fuzzy session selection is emitted.
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
helper executable, agent-send socket isolation, and exact-target tests pass.
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
exact-agent relaunch; no implementation path performs automatic session mutation.
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
symlink-target safety, and repeated-run idempotence.
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
role/class change produces a different communications generation.
---
## KBN-101 Database Runtime/Migration Role Split (#771)
### Problem and objective
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
### Normative requirements
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
### Acceptance criteria
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
---
## Tess Interaction Agent Workstream (TESS)
### Problem and Objective
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
### Scope
#### In Scope
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
#### Out of Scope
1. Replacing Mos as coding/general fleet orchestrator.
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
4. Unrestricted shell execution from Discord.
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
6. Replacing tmux before Matrix/native transport reaches operational parity.
### Stakeholder and User Requirements
- Jason must be able to converse with the same Tess session from Discord and CLI.
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
### Non-Functional Requirements
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
### Acceptance Criteria
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
### Constraints, Dependencies, Risks, and Assumptions
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
### Testing and Delivery Intent
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
---
## Official Channel Plugin Workstream (#756)
### Problem and Objective
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
### Scope
#### In Scope
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
#### Out of Scope
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
2. Dynamic Discord authorization administration in the web UI.
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
4. Implementing Matrix or Slack adapters in this slice.
### Non-Functional Requirements
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
### Acceptance Criteria
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
### Constraints, Risks, and Assumptions
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
### Testing and Delivery Intent
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
---
## Mos Runtime Portability Workstream (MOS-PORT)
### Problem and Objective
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
### M1 Requirements
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
### M1 Acceptance Criteria
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
### Deferred to Later #754 Milestones
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
---
## Workspace placement guard hardening (#1174)
### Problem and objective
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
will route around it.
### Scope and requirements
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
operands, not from every HOME-shaped word in the command.
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
operands and SHALL be refused when they resolve under HOME.
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
without relying on an enumerable allowlist of flag spellings.
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
handling SHALL preserve existing fail-closed checkout coverage.
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
repeated separators, and existing symlink parents before the HOME boundary comparison.
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
#1197.
### Acceptance and verification
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
guard allows a HOME-valued source with an explicit safe destination.
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
against the prior head where the defect existed.
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
after shell command boundaries.
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
terminal-green CI pass before merge.
5. Any option-classification residual is documented with its deliberate failure direction.
### Constraints, risks, and assumptions
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
acceptable repair.
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
while separate-value options have explicit grammar and must be classified as such.
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
chosen residual direction and pin every currently supported placement option in behavior tests.
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
---
## Release Integrity Workstream (RI, #1275)
### Problem and objective
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
### Normative requirements
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
### Acceptance criteria
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
### Out of scope
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
## Official CLI Capability and Tool Migration Workstream (T78)
Normative contract on integration trunk `next`:
[docs/requirements/cli-capability-migration.md](../../requirements/cli-capability-migration.md):
migrates agent-facing operations from directly invoked scripts into documented, first-class
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
carries its own implementation hold and delivery stages.
## Graduation ruling (Q-G3, Jason 2026-09-01)
Graduation of a Part II workstream contract is a ratification act: Jason marks
it (grill or direct ruling). The graduated contract text archives inside the
then-current PRD revision bundle — this section gains a graduated-set record
per revision — keeping the frozen-bundle model intact.
@@ -1,171 +0,0 @@
---
id: GOV.5
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# GOV.5 — Open questions (the grill list)
Every question the corpus could not settle. This is the E5 ms-grill-me input;
ratification is blocked until this list is empty or every remaining row is
explicitly deferred with an owner. IDs are stable; answered questions get their
ruling recorded here and flow into the owning section.
**Frontier status (2026-09-01, grill rounds 3–8 complete): EMPTY.** Every row
is ruled, dissolved, or deferred-with-owner. The operator-side Q-T3 canon
coalescence closed in round 8 and was executed the same day (brain commit
`59d43270`). E6 ratification executed 2026-09-01; this list is frozen with the
bundle.
## Data model
- **Q-D1** — **RULED, Jason 2026-09-01: consolidate.** `launch.env` folds
into `profile.json` (one seat record). Precondition verified same day:
`mosaic-core/lib/loader.ts seatRole()` parses profile.json as a generic
record and reads only `role` — widened files tolerated by construction.
Migration staged as lane work (ledger B1).
- **Q-D2** — **RULED, Jason 2026-09-01: per-provider map + broker refs.**
`profile.json` carries a per-provider map whose values are credential-broker
references (`environment/service/component/secret-name`), never secrets —
one seat, N providers, zero secrets in the brain tree. Flows to
[[DATA.1-record-authority]] and [[AUTHN.1-auth-accounts]].
- **Q-D3** — **RULED, Jason 2026-09-01: the projection engine owns.** One
writer: the role-projection engine (`role apply` path). The launcher seeds
nothing itself — it invokes the projector; Pi and every harness get
regenerated files on each role/seat change; hand edits are drift, flagged by
`validate`. Consistent with L2-D19 + Q-T5. Unblocks the reconciliation
features (lane ledger B3 → D8).
- **Q-D4** — **RULED, Jason 2026-09-01: role ceiling + seat choice.** The
Role Revision defines the allowed model set (policy ceiling); the seat
records preferences within it; effective models = intersection, consistent
with the L2-D39 intersection chain. Flows to [[HARN.1-harness-config]] and
role-harness-config DESIGN Q2 (same ruling, both doors).
- **Q-D5** — **DEFERRED with owner (Jason 2026-09-01)**: the mutating
config-engine half gets its own ruling after v1 read-only `validate`/`plan`
ships; owner = the mosaic-config workstream. ([[CLI.1-parity]] §v1 subset)
## Sessions
- **Q-S1** — **DEFERRED with owner (Jason 2026-09-01)**: the
session-id ↔ incarnation-id contract is settled inside the session-lifecycle
draft before it lands (which now carries the Q-S4 two-path requirement);
owner = that draft's ratification. ([[SESS.1-session-continuity]])
- **Q-S2** — **RULED, Jason 2026-09-01: step-up re-auth required.** Role
rebinding is an authority-changing act: fresh principal re-authentication
≤10 minutes before the confirmation lands, matching the S2
identity-lifecycle linking precedent. Flows to [[SEAT.1-seat-profile]]
§role-binding and the UI.1 seat-page spec.
- **Q-S3** — **DEFERRED with owner (Jason 2026-09-01)**: the measurable
continuity-degradation bar is settled inside the session-lifecycle draft
before it lands; owner = that draft's ratification.
- **Q-S4** — **RULED, Jason 2026-09-01: two-path requirement ratified.** The
session-lifecycle draft may not land with one relaunch path. Role change →
clean-session path (new incarnation + fencing token, ephemeral context
discarded, OD-02/OD-03); harness/model/provider change → continuity path (same
Stack session id, checkpointed context restored, OD-57–OD-61, no noticeable
degradation). Binding requirement on the draft, recorded in
[[SESS.1-session-continuity]] §state machine.
## Audit
- **Q-A1** — **RULED, Jason 2026-09-01: mechanical tooling.** The audit is
deterministic tooling over the grant/assignment record (witness-style, per
the S2 writer-coverage pattern); its output feeds the audit page read-only.
Agents may consume audit output but never produce the verdict ("prompt
adherence is not an enforcement mechanism"). Flows to
[[AUTHZ.1-capability-authority]] and [[UI.1-webui-surfaces]] §audit.
- **Q-A2** — **DISSOLVED by the Q-A1 ruling**: the auditor is code, audited by
ordinary review and CI witnesses, not a seat subject to misdirection.
- **Q-A3** — **DEFERRED with owner (Jason 2026-09-01)**: the computable
misdirection metric is designed inside the mechanical audit tooling ruled by
Q-A1; owner = the audit-tooling workstream (lane ledger C2).
## Surfaces
- **Q-N1** — **DEFERRED with owner (Jason 2026-09-01)**: technical
investigation of the in-browser OAuth flow (tmux-bridged terminal vs
server-side) runs before the auth page builds; owner = the auth-page
workstream (lane ledger D6). ([[AUTHN.1-auth-accounts]])
- **Q-C1** — **RULED, Jason 2026-09-01: CI witness in the stack repo.** The
parity matrix becomes a generated artifact with a drift-gate witness
(contract-9 pattern): CI regenerates the inventory from code and fails on
divergence from the committed matrix. Flows to [[CLI.1-parity]]; the
witness itself is E6-return follow-up work.
## Governance
- **Q-G1** — **RULED, Jason 2026-09-01: both ratified.** L2-D52
(least-privilege Assignment issuance, closes G1+G2) and the G6 WebUI
surface-scope fix applied per their return procedures after digest
re-verification; amendment files flipped to ratified; lane ledger A3/A4
closed.
- **Q-G2** — **RULED, Jason 2026-09-01: distinct prefixes at source.**
Stack keeps D1–D15; the operator DECISION-REGISTER renames to **OD-01…OD-65**
with a redirect table at the source doc; file-local D-numbering in drafts is
prohibited going forward (each decision doc declares a unique registry
prefix, rule lands in [[GOV.1-prd-lifecycle]]). Applied at E6 return for
stack references; brain-side rename on next DECISION-REGISTER touch.
- **Q-G3** — **RULED, Jason 2026-09-01: Jason marks; archive in bundle.**
Graduation is a ratification act — Jason marks it (grill or direct ruling);
the graduated contract text archives inside the then-current PRD revision
bundle ([[GOV.4-workstream-contracts]] gains a graduated-set section),
keeping the frozen-bundle model intact.
## Triage-raised (E2 sweep, 2026-08-31)
- **Q-T1** — **RULED B, Jason 2026-09-01: "shipped but frozen."** Amend D3 to
acknowledge federation M1–M3 exist (Step-CA, enrollment, grants, mTLS auth
guard, ScopeService, list/get/capabilities verbs; M3 landed 2026-06-24/25),
are excluded from the v1 bar, and are frozen; re-home tracking in
NORTH_STAR.yaml as a dormant workstream; frozen cert/auth code carries a
**security re-audit gate** before any resumption. Consequences at E6 return:
supersession/status banners on the three stale docs (root MISSION-MANIFEST,
federation/MISSION-MANIFEST, scratchpads/mvp-20260312), reconcile
guides/deployment.md with D15, NORTH_STAR.yaml dormant entry. The P5
scope ambiguity (governance-federation vs shipped mTLS-query federation)
stays open inside the future federation PRD, not rev1. Evidence: lane
`FEDERATION-DOSSIER-2026-08-31.md`.
- **Q-T2** — **RULED, Jason 2026-09-01: all three re-ratified** into the
rev1 decision map ([[GOV.3-decision-map]] §re-ratified orphans): "No Python"
in the monorepo; Matrix/MACP exactly-three-install-modes with Mode A
(split-domain) primary; OpenBrain excluded from consolidation scope. The
Matrix ruling's open DNS/domain prerequisite gets its own row (Q-T6).
- **Q-T3** — **RULED (partial), Jason 2026-09-01: coalesce under the
STRUCTURE-CANON name.** MOSAIC-CANON's more comprehensive content is
authoritative; `STRUCTURE-CANON.md` is the logical surviving document name;
the two coalesce into one. Conflict report delivered and all six
decision points ruled (grill round 8, 2026-09-01): per-seat credential
slots win; doc paths corrected to `fleet/auth/`/`fleet/memory/`;
ENTITY.md/README.md stay required with a 42-seat backfill task; merge
executed with the full reference sweep, MOSAIC-CANON reduced to a pointer
shim. Record: lane `CANON-COALESCENCE-2026-09-01.md`. Operator-side; not a
rev1 blocker.
- **Q-T4** — **RULED, Jason 2026-09-01, two parts.**
**(a) Two independent axes**: "Standalone/Enterprise" in the S2 corpus is a
multi-tenancy/isolation _mode_ (`platform_mode`, D3/D11); D15's "compose
standalone tier" is deployment _packaging_. Orthogonal. rev1 text always
says "standalone mode" vs "compose tier"; mode-conversion.md needs a
terminology note only, not a rewrite.
**(b) Own track, rev1 cites**: rev1 ratifies citing the nine contracts as
DRAFT successor material with status noted; each contract ratifies on its
own PR when its family lands. Extraction record: lane
`S2-EXTRACTION-2026-08-31.md`.
- **Q-T5** — **RULED, Jason 2026-09-01: scope to files.** Adopted wording:
"Generated settings _files_ are projections of the active Role Revision,
never authority (L2-D19). DB settings records written through audited
Gateway commands (`platform_mode`, `registration_mode`, `custody_config`,
`bootstrap.seed-company-name`, and their successors) are records of
authority like any other SOT row." No corpus conflict remains. Case law:
contract 1 §5.4, contract 8 §2.4, contract 9 §3.2.
- **Q-T6** — **DEFERRED with owner (Jason 2026-09-01)**: the Matrix/MACP
Mode A DNS/domain prerequisite (`archive/planning/matrix-macp/rfc-001:428`)
rules before any Matrix install work resumes; owner = whoever reopens
Matrix. Until then Mode A is primary-on-paper only.
## Deferred-by-scope (recorded, not blocking rev1)
- Federation design (D3 — roadmap placeholder; nothing in v1 may foreclose it).
_Q-T1 ruled B (2026-09-01): D3 to be amended — M1–M3 acknowledged, frozen,
security re-audit gate before resumption; design itself stays deferred._
- OS/kernel-level seat sandboxing (explicit lane non-goal; role-lane
discipline, not process containment).
@@ -1,64 +0,0 @@
---
id: HARN.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# HARN.1 — Harness configuration
A harness is an installed agent runtime (claude, pi, codex, opencode, …).
Shared contracts speak capability language; harness commands, model IDs,
hooks, and settings live in runtime adapters (register OD-38).
## Harness configuration surface (WebUI page + CLI)
| Control | Notes |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| install harness | single button push; installer runs server-side through official tooling |
| enable / disable | disabled harnesses are not selectable on any seat page |
| available models | an **allowlist** a seat may select from — not a selection. Whether `enabledModels` is role policy or harness/seat preference is open: [[GOV.5-open-questions]] Q-H1 |
| reasoning level defaults | |
| provider | which provider(s) back this harness ([[PROV.1-providers]]) |
| linked auth accounts | which accounts may drive this harness ([[AUTHN.1-auth-accounts]]) |
Enable/disable and install are runtime state (Postgres-owned) projected into
whatever flat state the launcher needs ([[DATA.1-record-authority]]).
## Runtime adapter contract (pulled 2026-08-31 from adapter-contract draft)
Every harness adapter binds a required capability set or **fails closed**:
repository ops via wrapper capability, scoped file/command execution,
structured reasoning, shared-memory capture/search/recall, inter-seat
messaging/wake, checkpoint persistence + mechanical telemetry, a `mosaic coord`
client that cannot mutate Kanban state or deploy seats directly, and credential
resolution through the seat's own slot. Rules:
- An unavailable capability is a **named blocker**, never silent degradation.
**"Prompt adherence is not an enforcement mechanism"** — a harness that
cannot persist checkpoints, emit telemetry, or honor fencing does not run
workflows that need them.
- Each adapter publishes a capability→binding table (capability, binding
surface, config source, verification check) and proves its bindings at
session start; verification failure is a named blocker.
- Adapters bind capabilities but **never redefine role authority, delivery
policy, gate outcomes, or review independence** — a harness whose native
workflow conflicts with shared policy keeps the shared policy and records the
conflict as an adapter limitation.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: the `fleet/` book — `concepts/desired-vs-observed-state.md`
(roster-v2 sole writable authority), `concepts/generated-env-launch-chain.md` +
`reference/generated-env-boundary.md`, `reference/roster-v2-fields.md`,
`operations/reconcile-and-recover.md` (lock/generation semantics),
`NORTH_STAR.md`/`FLEET-DOCTRINE.md` (delivery-fleet north star, subordinate to
this PRD per rev0 §10).
**Pending pulls**: brain `docs/guides/proposed/runtime/adapter-contract.md`
(the register-OD-38 runtime-adapter capability contract this section cites).
## enabledModels ruling (Q-D4, Jason 2026-09-01)
The Role Revision defines the allowed model set — a policy ceiling. The seat
records model preferences within that set. Effective models = the
intersection, consistent with the L2-D39 authority-intersection chain. A seat
preference outside the role ceiling is refused, not silently clamped.
@@ -1,89 +0,0 @@
---
id: PRD.0
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# PRD.0 — Index: naming standard, domain registry, reading order
This file is the order authority for the PRD section documents. Lexical sort of
the directory is **not** authoritative; this index is.
## Naming standard (ratified 2026-08-31)
`<DOMAIN>.<n[.n[.n]]>-<kebab-slug>.md` — e.g. `AUTHN.1.1-oidc.md`
- **Domain code**: short uppercase code from the registry below. Codes are
append-only; a code is never reused or renamed.
- **Number**: hierarchical, dotted, **append-only at every level**. A new
subtopic under `AUTHN.1` takes the next free number (`AUTHN.1.3`). Nothing
ever renumbers; depth absorbs insertions. Added topics augment, never
reshuffle.
- **Slug**: kebab-case; names only what the number does not. The domain word is
never repeated in the slug (`AUTHN.1.1-oidc.md`, not
`AUTHN.1.1-authentication-oidc.md`).
- **Separators**: dots between number levels only; one hyphen between number
and slug; hyphens inside the slug. No underscores, no spaces.
- **ID in three places** that must agree: filename, frontmatter `id:`, H1.
Wikilinks use the basename, e.g. `[[AUTHN.1.1-oidc]]` (illustrative — no such section exists yet).
- **Flat directory**: hierarchy lives in the number, not nested folders.
## Domain registry (append-only)
| Code | Domain |
| ----- | ----------------------------------------------------------------------------------------------- |
| PRD | The PRD assembly itself: index, preamble, revision log |
| GOV | Governance: document lifecycle, decision registers, amendment process, ratification |
| VIS | Vision / north star: what the Stack is, premises, non-goals |
| AUTHZ | Authorization & enforcement: capabilities, role policy, mosaic-core, L2 contracts, gap register |
| AUTHN | Authentication: provider accounts, OAuth/API keys, renewal, deactivation, allowed harnesses |
| ROLE | Roles: manifests, role config surfaces, role/seat separation |
| SEAT | Seats: profiles, launch config, seat config surfaces, profile.json consolidation |
| HARN | Harnesses: install/enable, model availability, reasoning, linked auth |
| PROV | Providers: supported providers, local providers (Ollama, LM Studio), provider config |
| SESS | Sessions: Stack session identity, continuity, mid-stream harness/model/provider switching |
| UI | WebUI: pages, page-scope rules, interaction patterns, audit surfaces |
| CLI | mosaic CLI: command surface, CLI↔WebUI parity |
| DATA | Record classes & storage: J1 git/DB authority split, flat-file vs DB, reconciliation |
New domains append below this line with a dated note.
## Reading order
Order is by lifecycle of understanding, not by code:
1. [[PRD.0-index]] (this file)
2. [[GOV.1-prd-lifecycle]]
3. [[GOV.2-docs-inventory]]
4. [[GOV.3-decision-map]]
5. [[VIS.1-north-star]]
6. [[DATA.1-record-authority]]
7. [[AUTHZ.1-capability-authority]]
8. [[ROLE.1-role-governance]] → [[SEAT.1-seat-profile]] (separation is load-bearing; role before seat)
9. [[HARN.1-harness-config]] → [[PROV.1-providers]] → [[AUTHN.1-auth-accounts]]
10. [[SESS.1-session-continuity]]
11. [[UI.1-webui-surfaces]] → [[CLI.1-parity]] (surfaces last; they project everything above)
12. [[GOV.4-workstream-contracts]] (preserved contracts; bind after the model is understood)
13. [[GOV.5-open-questions]] (the grill list; ratification gate)
Pulled sources (inputs, never ratified): [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md),
the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped).
Sections are added to this list as they are authored; an unlisted file is a
defect.
## Structural rulings (2026-08-31, Jason)
- The finished PRD is the **project SOT**. `docs/PRD.md` in the stack repo
becomes a shim to the current dated revision. Missions reference the PRD,
never usurp it. See [[GOV.1-prd-lifecycle]].
- The entire PRD lives in the **stack repo** (mosaicstack/stack, trunk `next`).
Brain documents are operator-instance documents that cite it.
- This work is the **successor** to the 2026-08-26 "North Star" PRD on
`origin/next` (snapshot: [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)).
- Revisions **archive, never delete**. Each ratified revision is a frozen
bundle directory — `docs/PRDs/YYYY-MM-DD_PRD_revN/` holding the PRD, this
index, and every section doc as a set (layout: [[GOV.1-prd-lifecycle]]
§Revision bundles).
- Shim and revision immutability are **convention, not enforcement** (no hook
or CI guard yet).
-103
View File
@@ -1,103 +0,0 @@
---
kind: spec
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
succeeds: origin/next:docs/PRD.md (2026-08-26 North Star, commit 9aa4983c, sha256 60cc2f98697471850caa3440d79139d70f67eda585a2ee465fdcd517bc36afdf)
---
# PRD: Mosaic Stack — rev1
The project source of truth, successor to the 2026-08-26 North Star PRD
(rev0). Ratified 2026-09-01; this bundle is frozen — the next revision is
drafted in a lane and lands as a new bundle ([[GOV.1-prd-lifecycle]]). This file assembles the section documents in this bundle; the
sections own the detail. Order authority and naming: [[PRD.0-index]].
Lifecycle (shim, frozen revision bundles, archival): [[GOV.1-prd-lifecycle]].
The PRD is **mission-independent**: missions pin an accepted PRD version and
reference it (register OD-16/OD-19); they never usurp it. rev0 ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)) is archived verbatim beside this bundle;
`docs/PRD.md` is the permanent shim pointing here.
## Metadata
- **Owner / decision authority:** Jason Woltje
- **Status:** ratified 2026-09-01 (Jason Woltje). Drafted as a class-2 draft-native in lane `fleet/lanes/control-plane-surfaces` (estate brain); grill record in [[GOV.5-open-questions]]
- **Base text:** rev0, pinned at `origin/next` commit `9aa4983c`
- **Pulled sources (inputs, never ratified; not shipped in this bundle):** [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) and the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped)
## Revision log
| Rev | Date | State | Notes |
| ---- | ---------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| rev0 | 2026-08-26 | superseded 2026-09-01; archived verbatim as `docs/PRDs/2026-08-26_PRD_rev0/PRD.md` | North Star PRD (Part I product north star from D1–D14 + D15; Part II workstream contracts) |
| rev1 | 2026-08-31 | **ratified 2026-09-01** (Jason; grill rounds 1–8 closed the GOV.5 frontier) | rev0 + control-plane surfaces (seats, roles, harnesses, providers, authentication, sessions, WebUI/CLI), consolidated decision map, authorization gap register, docs-estate consolidation |
## Mandate (2026-08-31 drafting directive)
1. Combine the official PRD (rev0) with the `control-plane-surfaces` and
`agent-runtime-ng` lane findings.
2. Ingest and reconcile the pertinent document corpus — stack `docs/` on
`origin/next`, `~/.mosaic/docs`, `~/.mosaic/docs/guides/proposed`
([[GOV.2-docs-inventory]] is the audit trail).
3. Specify all functions of the site and the north star in one place, with
full `mosaic` CLI ↔ WebUI parity.
4. Remove the no-central-location ambiguity; clear up drift and naming issues;
clarify ambiguous structural language.
5. Walk open questions via ms-grill-me before ratification
([[GOV.5-open-questions]]).
---
## Part I — Product north star
**[[VIS.1-north-star]]** — what Mosaic Stack is, who it is for, deployment
modes, hierarchy and tenancy, identity, onboarding, data custody, the
webUI-over-tooling architecture gate, the v1 slice, the fleet-north-star
subordination, non-goals, and tiered containerized deployment. rev0 Part I
preserved as base text with marked rev1 annotations.
## Part II — Platform model (control plane)
| Section | Owns |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [[DATA.1-record-authority]] | record-class authority (J1), the configuration data model, seat-file consolidation, reconciliation obligation |
| [[AUTHZ.1-capability-authority]] | intersection authority model, `mosaic-core` enforcement, firewalls, privilege escapation, accepted risk, gap register G1–G7 |
| [[ROLE.1-role-governance]] | role definitions/revisions, manifest invariants, role surface, seat/role separation rule |
| [[SEAT.1-seat-profile]] | instance contract, seat surface, the separated role-binding control (G5), OD-02/OD-03 semantics |
| [[HARN.1-harness-config]] | harness install/enable, model allowlists, adapter boundary (register OD-38) |
| [[PROV.1-providers]] | hosted and local providers, named instances, activation |
| [[AUTHN.1-auth-accounts]] | agent-side provider accounts, OAuth/API, custody rules, broker boundary |
| [[SESS.1-session-continuity]] | Stack session id, incarnation layering, mid-stream switching via register OD-57–OD-61, the two-operations rule |
## Part III — Surfaces
| Section | Owns |
| ----------------------- | ------------------------------------------------------------------------------------------------ |
| [[UI.1-webui-surfaces]] | governing rules and the complete page/function inventory, including the authorization-audit page |
| [[CLI.1-parity]] | CLI primacy, the one-engine rule (OD-53), the parity-matrix obligation, command families |
## Part IV — Governance
| Section | Owns |
| ------------------------ | --------------------------------------------------------------------- |
| [[GOV.1-prd-lifecycle]] | SOT rule, shim, frozen revision bundles, archival |
| [[GOV.2-docs-inventory]] | corpus inventory, supersession verdicts, naming-defects register |
| [[GOV.3-decision-map]] | every binding decision registry, collision rule, reconciliation notes |
| [[GOV.5-open-questions]] | the grill list; ratification gate |
## Part V — Active workstream contracts (preserved unchanged)
**[[GOV.4-workstream-contracts]]** — rev0 Part II carried verbatim: #1194
drift detection, Compaction Refresh Trust Lifecycle, Pi Persistent Goal Loop,
FCM, cross-harness comms, KBN-101, TESS, channel plugins, MOS-PORT, workspace
placement guard, Release Integrity, T78 CLI migration. Open issues bind to
them; they graduate out individually as workstreams close.
---
## Ratification
Per [[GOV.1-prd-lifecycle]]: this bundle freezes into
`mosaicstack/stack docs/PRDs/` (branch off `origin/next`), rev0 archives as a
one-file bundle, `docs/PRD.md` becomes the generated pointer (register OD-18).
Gate: [[GOV.5-open-questions]] empty or explicitly deferred; then reviewed PR
per stack delivery gates.
@@ -1,34 +0,0 @@
---
id: PROV.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# PROV.1 — Provider configuration
A provider is a model-inference source: hosted (Claude, OpenAI, ZAI, N others)
or local (Ollama, LM Studio, other).
## Provider configuration surface (WebUI page + CLI)
| Control | Notes |
| --------------------- | -------------------------------------------------------------------------------------- |
| provider selection | dropdown of supported providers |
| name | user-chosen instance name (multiple named instances of one provider type are expected) |
| auth mode | OAuth or API key — the account itself lives in [[AUTHN.1-auth-accounts]] |
| local provider setup | endpoint/port for Ollama, LM Studio, other local providers |
| activate / deactivate | inactive providers are not selectable downstream |
Provider records are runtime state (Postgres-owned, projected). Credentials
never enter provider records; they live with the credential broker
([[AUTHN.1-auth-accounts]]).
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/decisions/mos-runtime-portability-m1.md`
(the only current ADR for the logical-agent/connector-lease identity model);
`ADMIN-GUIDE/operations/mos-connector-lease-operations.md` — connector
activation is a **deliberate deny-all hold**; nothing in this section may imply
it is live.
**Drafts noted**: `rfcs/optional-ai-egress-gateways.md` (non-operative;
separates `IProviderAdapter` from egress-gateway concerns).
@@ -1,85 +0,0 @@
---
id: ROLE.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# ROLE.1 — Role governance and configuration
## What a role is
A role is the reviewed, Git-owned capability ceiling for a class of seats:
Role Definition → immutable, digested Role Revisions → the active revision
projects `mosaic-core.manifest.json` and role-scoped settings. Authority table:
[[DATA.1-record-authority]]. Enforcement: [[AUTHZ.1-capability-authority]].
## The separation rule (Jason, 2026-08-31 — closes gap G6)
- The **seat** configuration surface NEVER directly modifies role config.
- The **role** configuration surface NEVER directly modifies seat config.
- A seat page writes at most a per-seat **overlay**, never the role file.
The original `role-harness-config/DESIGN.md` sentence constrained a surface it
never named — both readings were faithful, and reviewer context decided the
meaning. The staged amendment names the surface explicitly.
## Role configuration surface (WebUI page + CLI)
| Control | Notes |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| manifest editing | capability grants against the C1–C8 (later open) registry; schema-validated before commit |
| revision management | create revision, diff against active, activate, roll back — every revision immutable and digested |
| role links | which seats bind this role (read-only here; binding happens on the seat surface — see [[SEAT.1-seat-profile]]) |
| projection status | whether each bound seat's on-disk projection matches the active revision (`role check` class) |
All writes go through the one canonical role-management API (L2-D14) shared
with the CLI — the WebUI holds no separate role logic. Role management is
**principal-only** (L2-D13): no agent identity may ever invoke these
operations, and the API enforces that, not the page.
## Manifest invariants (must survive any surface)
- Committed, non-symlink, trusted-path — `mosaic-core`'s loader refuses
violations; no surface may "fix" that by writing a symlink.
- Nothing env-overridable, nothing cwd-relative.
- `tools[]` equals exactly the bound bindings of granted capabilities.
- Role cross-checked against path at load.
## Specialization model (pulled 2026-08-31 from SPECIALIZATION-MODEL draft)
Four layers: **Role** (decision ownership and prohibited actions — few,
stable) → **Seat** (durable identity performing the role) → **Specialization**
(recurring domain/tools/behavior — open-ended, composable, never changes
authority) → **Task** (current activity). Rules:
- A seat has **exactly one role at a time**; never activate a second role
inside a session. If authority changes, `mosaic config` reconfigures the seat
and the coordinator starts a **clean session** — seat identity, history, and
authorship survive; the old lease is revoked and a new incarnation starts.
(Independent confirmation of register OD-02/OD-03 and the
[[SESS.1-session-continuity]] two-operations rule.)
- Promotion to a new role only when decision ownership or prohibited actions
materially differ; otherwise a formal specialization profile. Promotion
triggers: different authority/external side effects, distinct
credential/identity/data boundaries, added compliance controls, stable
machine-readable I/O contract, required independence, deterministic gate
behavior, repeated cross-seat use.
- Ad-hoc task-scoped specialization is valid only inside existing authority,
with no new credential/safety/independence boundary; it dies with the task
unless intentionally promoted.
- Anti-patterns: per-topic role explosion; role-subtype hierarchies no workflow
consumes; model IDs or harness syntax inside specialization definitions;
using specialization to bypass role authority or gates.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `fleet/reference/role-classes.md`,
`fleet/concepts/role-authority-and-leases.md`, `fleet/how-to/customize-roles.md`
(baseline + `roles.local` resolver), `fleet/migration/legacy-class-aliases.md`,
`ADMIN-GUIDE/security/discord-ingress.md` (viewer/operator/admin precedent).
**Pending pulls**: brain `docs/guides/proposed/SPECIALIZATION-MODEL.md`
(Role/Seat/Specialization/Task layering — the conceptual basis of this
section's separation rule); `plans/2026-08-29-agent-enrollment-command-design.md`
(enrollment authority composed across three contracts — fragility to fix or document).
**Naming hazard**: "Tess"/"Ultron" are roster-class display aliases in fleet
how-tos and named product identities elsewhere (defect N6) — qualify every use.
@@ -1,87 +0,0 @@
---
id: SEAT.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# SEAT.1 — Seat identity, profile, and configuration
## Instance contract (register OD-48)
| File | Carries |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `profile.json` | structured identity — and, post-consolidation, the full seat record ([[DATA.1-record-authority]] §consolidation) |
| `overlay.json` | generated composition |
| seat-local `AGENTS.md` | narrative specialization |
| `SOUL.md` | persona |
## Seat configuration surface (WebUI page + CLI)
| Control | Notes |
| ---------------------- | ------------------------------------------------------------------------------------- |
| harness | from **enabled** harnesses only ([[HARN.1-harness-config]]) |
| model | constrained by the harness's available-models allowlist |
| reasoning level | |
| work dir | |
| authentication account | from configured, active accounts allowed for that harness ([[AUTHN.1-auth-accounts]]) |
| overlay | per-seat overlay only — never the role file (ROLE separation rule) |
| role binding | **separated section — see below** |
## The role-binding control (gap G5)
Role Binding is the single highest-authority action in the system,
principal-only under L2-D13. `model` is a preference. They must not share one
undifferentiated form — a privilege grant must not inherit the ceremony of a
dropdown. Requirements:
- Visually and structurally separate section on the page.
- Distinct confirmation step; re-authentication of the principal is under
consideration ([[GOV.5-open-questions]] Q-S2).
- Register OD-02/OD-03 bind the semantics: a seat has exactly one role; an
**active session never switches roles**. A role change reconfigures the
existing seat, preserves identity and history, **discards ephemeral context,
and starts a clean session**. The surface must say so before confirming.
- Role-transition history is recorded: old role, new role, reason, authorizer,
checkpoint, activation time (register OD-04).
Role changes are therefore a _different operation_ from harness/model/provider
changes ([[SESS.1-session-continuity]]) and must not share a code path.
## Seat identity and credential rules (pulled 2026-08-31 from seat-identity draft)
- **One seat = one identity = one token slot.** A second copy of a token
anywhere is drift and is removed without reading it.
- Agents never mint their own tokens; provisioning, rotation, and scope changes
are operator authority. Credential refusal is _correct behavior_ — the fix is
the seat's identity, never another seat's or a shared credential.
- Fail-closed everywhere: an empty/unreadable slot is a designed state reported
at launch; the credential helper refuses, records, notifies — never falls
back to a shared or owner credential.
- Git identity resolution order: explicit environment identity → configured
identity → git's own answer. Identity is named on every invocation and never
persisted inside a shared clone/worktree config (silent attribution rewrite).
Commit author must identify the seat that did the work.
- Tokens are compared by digest, never by value; scopes are verified from the
authority's own report, never transcription.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `fleet/reference/agent-mutations.md`,
`fleet/reference/lifecycle-transitions.md` (`enabled`/`desired_state` authority),
`fleet/how-to/create-update-delete-agent.md`, `guides/fleet-local-canary.md`.
**Pending pulls**: brain `docs/guides/proposed/operations/seat-identity.md`
(credential-resolution mechanics under the OD-48 instance contract).
## Role-binding step-up ruling (Q-S2, Jason 2026-09-01)
Confirming a role-binding change requires fresh principal re-authentication no
older than 10 minutes — the same step-up bar the S2 identity-lifecycle
contract sets for account linking. An active session alone is insufficient;
this closes the stolen-session → privilege-misdirection path through the seat
surface.
## Seat record consolidation ruling (Q-D1, Jason 2026-09-01)
`launch.env` consolidates into `profile.json`: one seat record. Verified:
`mosaic-core/lib/loader.ts seatRole()` reads only the `role` key from a
generically-parsed record, so widened files are tolerated by construction.
@@ -1,121 +0,0 @@
---
id: SESS.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# SESS.1 — Session identity and mid-stream switching
## Requirement (Jason, 2026-08-31)
An agent session stays active on the system, tied to a **Stack session id**.
Changing harness, model, or provider mid-stream preserves the session id and
fully switches context from one provider/harness to another, with no user
intervention and no noticeable performance degradation.
## Two operations, two code paths — never merged
| | Harness / model / provider switch | Role switch |
| ----------- | --------------------------------- | ------------------------------------------------------- |
| Session id | preserved | seat identity preserved; session is **clean** |
| Context | fully transferred | **ephemeral context discarded** (register OD-03) |
| Governed by | this section | [[SEAT.1-seat-profile]] §role-binding |
| Why | continuity requirement | an active session never switches roles (register OD-02) |
## The ratified mechanism already exists: register OD-57–OD-61
The 2026-08-28 register confirms the machinery this requirement needs:
- **OD-57 checkpoints** — atomic, schema-valid, revisioned seat checkpoints tied
to incarnation and lease; freshness enforced mechanically.
- **OD-59 relaunch** — the coordinator requests and validates a checkpoint, stops
the session, applies configuration, starts a **clean incarnation**, restores
the assignment, verifies readiness.
- **OD-60 fencing** — leases, epochs, incarnation IDs, fencing tokens prevent a
stale session from mutating state after the switch.
- **OD-61 restart recovery** — the relaunched seat restores role, mission, task,
PRD pin, constraints, evidence, blockers, leases, dependencies, and next
action **without prior conversation**.
A mid-stream harness switch is therefore an OD-59 relaunch keyed to a persistent
Stack session id: checkpoint → stop → reconfigure → new incarnation →
restore → resume. What OD-59 does not yet promise is the _experience_ bar — no
user intervention, no noticeable degradation — which is this PRD's addition.
## Identity layering
`mosaic-core` mints a per-launch **incarnation id** and keys its journal on it,
deliberately not on any session id. A harness switch is a new process → new
incarnation → new journal, **while the Stack session id persists**. So:
```
Stack session id (durable; user-facing continuity)
└─ incarnation id (per launch; enforcement journal, fencing per OD-60)
```
The precise contract between the two ids — minting, custody, what the
coordinator records at each relaunch — must be specified before build:
[[GOV.5-open-questions]] Q-S1.
## Open hard problem
Context-transfer fidelity between harnesses with different context formats,
tool-call encodings, and system-prompt injection points. The checkpoint (OD-57)
is the transfer vehicle; whether a checkpoint alone meets "no noticeable
degradation" across harness families is unproven: [[GOV.5-open-questions]] Q-S3.
## Session lifecycle state machine (pulled 2026-08-31 from the session-lifecycle draft — with one required extension)
The operator draft (`workflows/session-lifecycle.md`, the densest
decision-register consumer: OD-03/OD-04/OD-08, OD-56–OD-65) supplies the checkpoint/
lease/fencing machinery this section's continuity requirement runs on:
- **States**: Active → Relaunch-requested (triggers per OD-59: context
utilization, session age, milestone, drift, degraded health, role
reconfiguration, authorized request) → Checkpointing (atomic, revisioned,
bound to identity + incarnation + epoch + lease, OD-57) → Relaunching
(validated checkpoint, old lease revoked → **new incarnation, new fencing
token**, OD-59) → Restoring (readiness proof: role, task, PRD pin, blockers,
next action, OD-61) → Active/Degraded. Role change routes through
Reconfiguring first (old-role record, transition history, revoked lease,
OD-03/OD-04).
- **Fencing**: a stale session cannot mutate after its replacement holds the
new token (OD-60); mutation authority is lease-gated and not renewed while the
checkpoint is stale. Coordinator outage fails closed for new
assignments/relaunches/renewals; existing leases run to expiry; read-only
work continues (OD-63).
- **Checkpoint contents** (required fields): role, config version, mission,
outcome node, task, PRD pin, constraints, completed work with evidence refs,
blockers and failed attempts, active leases/external ops, next action with
required inputs. The checkpoint is an operational projection — mission truth
stays in the ledger (OD-58). Telemetry is append-only and never the resumable
checkpoint (OD-56).
**Structural gap found at extraction (must be fixed before this machine
ratifies):** the draft models exactly **one** relaunch mechanism — every
trigger, without exception, mints a new incarnation and fencing token. There is
no continuity-preserving path at all, and harness/model/provider switching does
not appear among the triggers. This PRD's two-operations rule (above) requires
**two code paths**: the state machine must gain a switch path that preserves
the Stack session id and full context per OD-57–OD-61 while still rotating the
fencing token safely. Adopting the draft's table verbatim would silently
collapse the two operations back into one — the exact defect register OD-02/OD-03
vs the continuity requirement exists to prevent.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/compaction-revocation.md`
(the only current continuity/revocation lifecycle — observer/generation-fencing,
test-consumed), `channel-protocol.md`.
**Pending pulls**: brain `docs/guides/proposed/workflows/session-lifecycle.md`
(checkpoint/relaunch/recovery/role-reconfig — complements this section's
switching focus; its role-reconfig path must respect the OD-02/OD-03 clean-session rule).
## Two-path requirement ratified (Q-S4, Jason 2026-09-01)
The state-machine gap flagged above is now a binding requirement: the
session-lifecycle draft may not land with a single relaunch path. Role change
→ clean-session path (new incarnation + fencing token, context discarded,
OD-02/OD-03). Harness/model/provider change → continuity path (same Stack session
id, OD-57 checkpoint restored under OD-61, no noticeable degradation). The two
paths must not share a code path.
@@ -1,108 +0,0 @@
---
id: UI.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# UI.1 — WebUI control-plane surfaces (all functions of the site)
The complete function inventory of the WebUI control plane. Every page obeys
the governing rules; every control ultimately calls the same engine as the CLI.
## Governing rules
1. **Full CLI parity** — every aspect of the `mosaic` CLI surfaces in the WebUI
([[CLI.1-parity]] carries the matrix obligation).
2. **One canonical API** (L2-D14; register OD-53) — CLI, TUI, WebUI, API, and
automation share one CLI-backed schema, resolver, planner, authorization,
transaction, validation, and audit engine. The WebUI holds no separate
logic.
3. **The webUI sits OVER official tooling** (D8/D12 hard rule) — no page ever
reaches the database or filesystem around the tooling; a missing tool means
the gap is "blocked on tooling" and the tool is built first.
4. **Strict surface separation** — seat pages never modify role config; role
pages never modify seat config ([[ROLE.1-role-governance]]).
5. **No direct settings-file authorship** — settings are generated projections
(L2-D19; [[DATA.1-record-authority]]).
6. **Agents can never reach these surfaces** (L2-D13; the API refuses agent
identity — the enforcement is not the page's absence).
7. **WebUI drafts** (register OD-54) — draft configuration is revisioned
server-side desired-state; drafts have no effect until planned and applied.
## Interaction conventions
Logically separated pages; dropdowns, activate/deactivate buttons, drag-drop
actions performed on-page.
## Page inventory
| Page | Section doc | Functions |
| ---------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Seat configuration | [[SEAT.1-seat-profile]] | harness, model, reasoning, workdir, auth account, overlay; separated role-binding section |
| Role configuration | [[ROLE.1-role-governance]] | manifest editing, revision create/diff/activate/rollback, role links, projection status |
| Harness configuration | [[HARN.1-harness-config]] | install (button), enable/disable, available-models allowlist, reasoning defaults, provider link, linked accounts |
| Provider configuration | [[PROV.1-providers]] | provider dropdown, named instances, OAuth/API mode, local providers, activate/deactivate |
| Authentication | [[AUTHN.1-auth-accounts]] | in-browser OAuth establishment, account list, force renew, deactivate, allowed harnesses |
| Authorization audit | below | effective grants, escapation potential, drift |
## Page: Authorization audit (closes gap G3)
Surfaces, per seat, to the user:
- **Effective capability grant** — the live intersection
(role ∩ assignment ∩ lease ∩ workflow ∩ target policy ∩ backend).
- **Misdirection potential** — which seats hold capabilities that would let
another seat's work be routed around its own role lane.
- **Escalation potential** — any path that would add capability. Should be
provably empty; the audit's job is proving it _stays_ empty.
- **Drift** — seats whose on-disk projection diverges from their active role
revision (`role check` class).
- **Failure/blocked surfacing** (register OD-64) — the canonical alert stream's
WebUI adapter.
Implementation choice (dedicated auditor agent vs mechanical tooling) and the
auditor-identity problem are on the grill: [[GOV.5-open-questions]] Q-A1/Q-A2.
## Cross-cutting requirement
Every change made through these pages — or the CLI — automatically reconciles
authentication, `settings.json`, and required symlinks
([[DATA.1-record-authority]] §reconciliation; removal-fast / addition-attested
per L2-D17). The user never touches a file.
**Measured 2026-08-31** ([[CLI.1-parity]] Artifacts 2–3): the shipped WebUI
already contains two D12 violations — the admin role/ban toggles and the stored
harness/provider/model selection mutate state with no backing CLI command.
Remediation, not precedent. The server-side hierarchy/grants CRUD surface
(`hierarchy.controller.ts`) is the natural backing for the authorization audit
page below, but needs a CLI face and an audit read-path first.
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
**Canonical ground truth**: `USER-GUIDE/product/web-dashboard.md` (route-by-route
current state, incl. explicit gaps — no New Project/Task UI),
`webui/PHASE-P-STRUCTURE.md` (Next→Vite SPA migration).
**Pending pulls**: DRAFT S2 `onboarding-wizard.md` (D4/D11/D8),
`tool-gateway-mapping.md` (the D8/D12 gate made concrete), `api-artifacts.md`.
## S2 contract feed (extraction 2026-08-31)
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
- **Contract 5 verbatim-affirms the parity rule**: "The webUI is a Gateway
client only"; "No webUI-only command exists; a Gateway command without CLI
exposure is a conformance gap." "Blocked on tooling" closure is mandatory;
UI workarounds (direct DB/filesystem, legacy endpoints, domain logic in the
web app) are non-conformant. This is the ratifiable D8/D12 text this
section's violation findings measure against.
- **Legacy non-substitutes** barred from backing any P1 surface, frozen for
new consumers: `/api/projects`, `/api/tasks` CRUD, `POST /api/workspaces`,
`/api/teams` reads, `POST /api/bootstrap/setup`, MCP `brain_*` mutations.
- **Onboarding wizard (contract 3)** is the reference pattern for every config
page this section specifies: pure client-side composition of Gateway
commands, exactly one disclosed server-side composed transaction (bootstrap
finalize), wizard state always derived from canonical state — never a
persisted answer file that can drift.
- **Company visibility** (`private` default vs `directory`) is a UI-facing
disclosure control with a bounded existence-only carve-out.
- P1 build rank order (T10): hierarchy → hierarchy RBAC → typed kanban →
agent enrollment → authorized roll-up → onboarding orchestration.
@@ -1,214 +0,0 @@
---
id: VIS.1
status: ratified
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
---
# VIS.1 — Product north star
Successor text to rev0 Part I ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) lines
33–215, preserved there verbatim). Base text unchanged except marked **rev1**
annotations; the decision registry moves to [[GOV.3-decision-map]].
### 1. What Mosaic Stack is (D1)
Mosaic Stack is an **open-source, AI-first platform for people who want a
self-hosted environment for agentic management and a life operating system.**
It serves personal, business, and employee needs from one deployment, and the
work is offered freely.
"AI-first" means agents are first-class operators of the system, not a bolted-on
chat box: the platform exists to let humans direct fleets of agents over their
projects, tasks, communications, and infrastructure, with the same tools and
the same guarantees whether a human or an agent is acting.
### 2. Who it is for (D1, D9)
The operator of a deployment is its user. Mosaic Stack is **not a hosted
business**: running the system as a service for external customers is outside
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
can separate their world — for example, several LLCs plus a personal domain —
while every deployment is self-hosted by its own operator.
"Company" in the hierarchy is organizational separation for one operator's
world, not a customer account.
### 3. Deployment modes (D3)
Two modes, chosen at install time:
| | Standalone / personal | Enterprise |
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
Brains are configurable as external git repositories (recommended, not
required); git tracking is always on locally.
**Federation** (connecting deployments: system-level config, assigned users,
rights and data-access control, trusts with boundaries, exfiltration
monitoring) is intentionally not fully designed. It is deferred, appears on the
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
_D3 as amended 2026-09-01 (Q-T1 ruling B):_ federation milestones M1–M3
(Step-CA, enrollment, grants, mTLS auth guard, ScopeService, list/get/
capabilities verbs) are **shipped but frozen** — present in code behind the
`tier === 'federated'` gate, dormant since 2026-06-25, absent from the canonical
compose topology (D15), excluded from the v1 bar, tracked as a dormant
workstream in `docs/fleet/NORTH_STAR.yaml`, and gated on a security re-audit
before any resumption. See [[GOV.5-open-questions]] Q-T1.
### 4. Structure and tenancy (D2, D9, D13)
The hierarchy:
```
company/organization (N per deployment)
└─ estate (each in exactly one company)
└─ project (each in exactly one estate)
└─ workspace (project-specific; carries the Kanban)
```
Rules:
- Users can create N companies, N estates, N projects.
- Tasks bubble UP the hierarchy so whole-system status is visible at every
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
- Granular RBAC: admins restrict access per company, estate, and project;
grants are evaluated down the chain. Assets are transferable subject to the
structure.
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
ratified in
[docs/requirements/native-kanban-sot.md](../../requirements/native-kanban-sot.md)
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
### 5. Identity (D10)
Built-in auth (better-auth) is the **account system of record**. Authentik and
other external IdPs federate in via OIDC as login methods; they never become
the system of record. Perimeter shims (forward-auth in front of a web host) are
deployment workarounds, not the design.
### 6. Onboarding (D4)
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
is extensible** — new wizards attach as tabs.
Standalone flow captures: system and company name; component choices (Mosaic
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
DB/PostgreSQL vs external; vector DB); the initial user
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
(harness choice and install, OAuth or API-key login, multi-account, model
choice with recommendation, agent name and persona, account assignment,
optional comms auto-enroll); a user onboarding profile (disabilities including
ADHD/autism/PDA/vision, professional background, education, desired agent
communication style, optional voice-matching interview, family/pets/friends/
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
configuration; an initial estate, an initial project, and seeded example data.
Enterprise uses the same skeleton with personal data optional; the focus moves
to business structure, org chart, RBAC, M365 and external systems, immediate
OIDC, SSO prominent.
Profile answers feed `USER.md` and/or the user's data store subject to the
custody rule in §7.
### 7. Data custody (D6, D14)
- **Sensitive profile categories** (disabilities, family, communication style,
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
structural data, consent records, and pointers — never the content. "User
data does not leak" is enforced by architecture, not policy (D14).
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
optional in Standalone, not required. Keeping it is the recommended default
because it preserves forward-compatibility with the one-way Enterprise
conversion (D3).
- Estate brains hold operational records. Only product-relevant material
migrates into this repository's docs; operational records stay in their
brains and are linked (D6).
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
same official framework tooling the CLI uses. The CLI remains the primary
execution method; the webUI uses the tools to operate and configure the
system. The webUI never bypasses tooling to reach the database or filesystem
directly.
Consequence for planning: when a desired webUI operation has no backing tool,
the gap is scored **"blocked on tooling"** and the tool is built first. The
product baseline therefore always includes all three D8 inputs: the tool
inventory (what exists and what is missing), the webUI→tool mapping, and the
measured current state of the `next` branch.
### 9. v1 slice (D11)
v1 is deliberately small:
1. **Standalone onboarding wizard** — system/company name, component choices,
initial user, initial estate + project, seeded examples, re-runnable.
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
read-only task bubble-up.
3. **Basic RBAC** on the hierarchy.
4. **Minimal agent enrollment** — one harness, API key, name/persona.
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
Enterprise conversion, federation. Every deferred item appears in
[docs/ROADMAP.md](../../ROADMAP.md) per the D11 rule: nothing exists only in heads.
### 10. Relationship to the fleet north star
[docs/fleet/NORTH_STAR.md](../../fleet/NORTH_STAR.md) (generated from
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
agent fleet that builds and operates the system should run (NS-1..NS-10,
workstreams A–L). This PRD is the **product** north star. They are not
competitors: the fleet north star is subordinate product-wise — its workstream
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
PRD does not redefine fleet invariants. The subordination rule is ratified in
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
cite and subordinate it, never fork it"). A change that would put the two in
conflict must amend one of them explicitly, never fork a third document
(drafting addition — see §12.1).
### 11. Explicit non-goals
- Hosted/SaaS operation for external customers (D9).
- A webUI that writes to the database or filesystem around the tooling (D12).
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
- Fully-designed federation in v1 (D3 — roadmap placeholder only; the shipped M1–M3 code is frozen, not a v1 feature).
### D15 — Tiered containerized deployment (2026-08-30, containerization lane)
The stack ships a tiered deployment target, additive to the architecture
gate (D8): (1) Standalone tier — docker compose is the canonical
single-host deployment: postgres, valkey, openbao, gateway, appservice
and the served webUI in one composition, with migrations, health checks,
and a documented install/upgrade path; the registry (CI-published
images) is the only deployment source. (2) Enterprise tier — Kubernetes
manifests for the same service set, phase-gated on the standalone tier
holding its acceptance bar. The v1 acceptance bar for the standalone
tier: compose-up healthy; webUI hosts agent chat; an in-stack agent can
open a PR to this repo; CI validates it; the running deployment adopts
the merged change (pull + restart). Federation (D3 clause) remains
deferred and unforeclosed. Implementation plan:
docs/plans/2026-08-30_containerization.md.
---
## rev1 annotations (2026-08-31)
- §8's architecture gate (webUI over official tooling, CLI primary) is
elaborated for the control plane by [[UI.1-webui-surfaces]] and
[[CLI.1-parity]]; register decision OD-53 confirms all interfaces share one
CLI-backed engine.
- §4's RBAC and §5's identity are joined by the **agent-side** authority model
in [[AUTHZ.1-capability-authority]]: role capability ceilings enforced at the
harness by `mosaic-core`, composed by pure intersection.
- The fleet north star subordination (§10) gains a control-plane consequence:
the WebUI workstream consumes this PRD's surface specifications
([[UI.1-webui-surfaces]]) rather than defining its own.
+1 -1
View File
@@ -59,7 +59,7 @@ Active workstream is **W1 — Federation v1**. Workers should:
## Fleet configuration management (#758) — M0–M5 implementation DAG
> **PRD:** [Fleet declarative configuration management](./PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
> **PRD:** [Fleet declarative configuration management](./PRD.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
>
> Every row below is one independently reviewable card and **one PR**. `depends_on` is a
> hard DAG edge; no card may silently absorb another card's scope. All source cards require
+1 -3
View File
@@ -1,10 +1,8 @@
---
kind: tracking
status: superseded
status: active
---
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's framing of Federation v1 as an active, in-progress mission (M3) is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
# Mission Manifest — Federation v1
> Persistent document tracking full mission scope, status, and session history.
+14 -15
View File
@@ -39,21 +39,20 @@ The Mosaic Backlog is the backlog of record + dispatch engine, built on Mosaic's
## Workstreams
| id | title |
| --- | ----------------------------------------------------------------------------------------------------------------- |
| A | Substrate — Mosaic Backlog on native Postgres storage service |
| B | Supervisor — movement guarantee, two-agent floor, dispatch/claim |
| C | Planner — goal decomposition into independently-shippable cards |
| D | Merge-gate — single approver, pr-merge.sh after CI wait |
| E | Meta-loop — session-review + enhancer improvement PRs |
| F | Safety-rails — TTL claims, advisory spend, PAUSE kill-switch |
| G | Kill-switch — operator PAUSE honored before dispatch and merge |
| H | Personas & system profiles — cross-domain library, system-type provisioning, update-surviving customization |
| I | Operator surface — launcher, fleet visibility, reliable steering (tier 0) |
| J | Web control plane — browser surface over the gateway (tier 1) |
| K | Clients — desktop and mobile over the same backend (tier 2) |
| L | Auth profiles — per-provider accounts, per-session selection (tier 2) |
| M | Federation — DORMANT; M1–M3 shipped and frozen (PRD rev1 D3 as amended; security re-audit gate before resumption) |
| id | title |
| --- | ----------------------------------------------------------------------------------------------------------- |
| A | Substrate — Mosaic Backlog on native Postgres storage service |
| B | Supervisor — movement guarantee, two-agent floor, dispatch/claim |
| C | Planner — goal decomposition into independently-shippable cards |
| D | Merge-gate — single approver, pr-merge.sh after CI wait |
| E | Meta-loop — session-review + enhancer improvement PRs |
| F | Safety-rails — TTL claims, advisory spend, PAUSE kill-switch |
| G | Kill-switch — operator PAUSE honored before dispatch and merge |
| H | Personas & system profiles — cross-domain library, system-type provisioning, update-surviving customization |
| I | Operator surface — launcher, fleet visibility, reliable steering (tier 0) |
| J | Web control plane — browser surface over the gateway (tier 1) |
| K | Clients — desktop and mobile over the same backend (tier 2) |
| L | Auth profiles — per-provider accounts, per-session selection (tier 2) |
## Goals (backlog projection)
+1 -8
View File
@@ -145,15 +145,8 @@ workstreams:
title: Clients — desktop and mobile over the same backend (tier 2)
- id: L
title: Auth profiles — per-provider accounts, per-session selection (tier 2)
# M is DORMANT by ruling (PRD rev1, D3 as amended 2026-09-01, GOV.5 Q-T1
# ruling B). Federation M1–M3 exist in code behind `tier === 'federated'`
# (M3 landed 2026-06-24/25), are excluded from the v1 bar and frozen. It
# projects no goals on purpose: none may be added before a security
# re-audit of the frozen cert/auth code and a federation PRD revision.
- id: M
title: Federation — DORMANT; M1–M3 shipped and frozen (PRD rev1 D3 as amended; security re-audit gate before resumption)
# NOTE: workstreams C, D, E, F and M are declared but currently project no goals.
# NOTE: workstreams C, D, E and F are declared but currently project no goals.
# That is planning debt, not an editing error: their goals have not been written
# yet. The A5 validator below reports it rather than letting it stay invisible.
+1 -1
View File
@@ -1,6 +1,6 @@
# Fleet Configuration Management
This book documents the local roster-v2 desired-state control plane delivered under issue #758. The normative requirements are the [FCM section of the repository PRD](../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#fleet-declarative-configuration-management-workstream-fcm-758), not the older fleet-suite or observability planning pages.
This book documents the local roster-v2 desired-state control plane delivered under issue #758. The normative requirements are the [FCM section of the repository PRD](../PRD.md#fleet-declarative-configuration-management-workstream-fcm-758), not the older fleet-suite or observability planning pages.
## Authority boundary
+4 -19
View File
@@ -5,24 +5,12 @@ status: active
# Deployment Guide
> **Status: non-operative for PostgreSQL, federated (federation is frozen — PRD rev1 D3 as
> amended; not a v1 route), and bare-metal production.** The checked-in
> **Status: non-operative for PostgreSQL, federated, and bare-metal production.** The checked-in
> Compose PostgreSQL service mounts legacy initialization SQL and the KBN-101 bootstrap, runner,
> secret-renderer, and process-exec interfaces do not exist yet. This page does not authorize a
> production deployment, database initialization, manual DDL, secret provisioning, or service
> activation.
## Relationship to the PRD (D15)
Per PRD rev1 Decision D15 (`docs/PRD.md`), the compose standalone tier — `docker compose up` — is
the canonical v1 deployment topology; this guide describes the interim path to that bar, not a
competing one. The KBN-101 holds documented below (bootstrap, runner, secret-renderer, process-exec)
are operational gates on the road to the standalone-tier bar, not an alternative or federated
topology. They remain fully binding: nothing in this guide authorizes PostgreSQL, federated, or
bare-metal production activation until the named KBN-101-00/03/05 artifacts land, pass review, and
satisfy the order specified below. Federation M1–M3 references elsewhere in this guide are
historical/frozen (PRD rev1 D3 as amended) and do not describe a live or v1-bound route.
## Current safe local route
Use PGlite only for current in-process data-layer work; it requires no PostgreSQL. A Gateway/Web
@@ -34,14 +22,12 @@ docker compose up -d valkey
```
This command intentionally does not start PostgreSQL. Do not run a broad Compose start, use its
PostgreSQL initialization mount, infer that current Compose is a production/federated (federation
is frozen — PRD rev1 D3 as amended; not a v1 route) route, or
PostgreSQL initialization mount, infer that current Compose is a production/federated route, or
start Gateway/Web until KBN-101-02 supplies fail-closed local-tier/DSN isolation.
## Held future procedure
PostgreSQL local, federated (federation is frozen — PRD rev1 D3 as amended; not a v1 route),
Compose, and bare-metal production activation are held until these
PostgreSQL local, federated, Compose, and bare-metal production activation are held until these
artifacts land and pass their independent gates:
1. **KBN-101-00** external privileged bootstrap artifact;
@@ -83,5 +69,4 @@ For local PGlite development, diagnose application behavior without introducing
connection.
Non-database local services may be inspected with their ordinary local health/log tools. Those
checks do not certify PostgreSQL, federated (federation is frozen — PRD rev1 D3 as amended; not a
v1 route) deployment, or production readiness.
checks do not certify PostgreSQL, federated deployment, or production readiness.
+1 -1
View File
@@ -8,7 +8,7 @@ status: active
> Single-writer: the RI-050 orchestrator (jarvis, dragon-lin) only. Workers read but never modify.
>
> **Mission:** alpha 0.0.50 release-integrity floor (decisions SDLC-D-033..038).
> **PRD:** [PRD rev1 GOV.4 § Release Integrity Workstream](../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#release-integrity-workstream-ri-1275)
> **PRD:** [docs/PRD.md § Release Integrity Workstream](../PRD.md#release-integrity-workstream-ri-1275)
> **Issue:** #1275 (remains open until RI-V-001 closes)
> **Base branch:** `next` (all cards branch from `origin/next`, squash-merge via PR)
>
@@ -0,0 +1,12 @@
{
"summary": "No important actionable issues found. Helper failures remain terminal, and the previously reported token-export regression is fixed. Bash syntax, package JSON, and caller-token export checks passed. The full regression suite was not run because the sandbox is read-only.",
"verdict": "approve",
"confidence": 0.88,
"findings": [],
"stats": {
"files_reviewed": 6,
"blockers": 0,
"should_fix": 0,
"suggestions": 0
}
}
@@ -0,0 +1,13 @@
{
"summary": "No confident security findings in the six supplied changed files. Seat lookups use the production helper, helper failures remain terminal, and returned credentials are validated. Bash syntax checks passed. Runtime regression tests were not run because the sandbox is read-only.",
"risk_level": "none",
"confidence": 0.87,
"findings": [],
"stats": {
"files_reviewed": 6,
"critical": 0,
"high": 0,
"medium": 0,
"low": 0
}
}
@@ -0,0 +1,23 @@
# #1311 Credential seat-store alignment
## Scope
Restore the missing `load_credentials` Gitea seat-slot behavior in the Stack framework. A known fleet seat must obtain its token through the production credential helper, while its Gitea URL remains provider configuration. A missing seat slot must fail closed and never fall back to the service store.
## Evidence
- Base: `2101c9b4468b22f57b2f02bb2c9da12067225819` (`origin/next`).
- Historical branch `origin/fix/credentials-gitea-seat-slots` contains the pre-refactor direct-loader fix, but it predates the current Python wrapper and ancestry fence.
- Red reproduction: `test-credentials-gitea-seats.sh` failed on base for populated seats, empty-seat no-fallback, and cross-seat ancestry cases.
- Canonical source: `packages/mosaic/framework/`. The package installer and `mosaic-doctor` describe the shipped framework as the source for deployed framework tools. The brain runtime copy is an estate runtime copy, not this framework change's source.
## Decision
The loader delegates seat token resolution to its sibling `git-credential-mosaic` production entrypoint. It does not invoke `.impl` directly or read a seat slot itself. This preserves the wrapper's environment sanitization and the implementation's process-ancestry fence. Non-seat and no-identity service-store behavior, non-Gitea services, and pre-existing `GITEA_TOKEN` precedence remain unchanged. The loader continues to export a caller-supplied token so child tool processes receive it.
## Validation and review
- The final hermetic matrix passes for Mosaic and USC seat slots, empty and cross-seat refusals, service-store paths, explicit and unexported caller tokens, and Woodpecker isolation. It fails against `origin/next` for the expected missing behavior.
- `bash -n` passes for the loader and new suite. The framework test-enumeration guard passes with the new suite enumerated.
- `pnpm --dir packages/mosaic run test:framework-shell` reaches `invariant_r_unittest.py` and stops on its existing host-runtime check: the test expects Pi `0.84.1` while this host reports `1.0.3`. No file in that invariant or its runtime probe changed in this task. The suite passes its earlier systemd check when the user-bus environment is supplied.
- Initial independent code review found that the new conditional stopped exporting a caller-supplied unexported `GITEA_TOKEN`. The fix exports it in both Gitea arms and adds Mosaic/USC child-process regressions. Re-review approved with no findings. Independent security review reported risk level `none`.
@@ -0,0 +1,81 @@
# Forge wrapper review round 1 (#1841)
Reviewed candidate: 55d6cc18e378306146505303c9b73da01164a3a9.
Original base: 924438016e2eb1c2ca352bfbf9107143b8b342bb.
Implementation checkpoint: 8325df706 (new commits, no history rewrite).
The security and code findings are addressed by the following commits and controls.
All provider fixtures use synthetic credentials; no test writes to a live forge.
| Finding | Fix commit | Regression evidence |
| --- | --- | --- |
| S1 | d93960111 | `Policy.test_head_and_uncommitted_policy_cannot_authorize_merge`: a committed head and uncommitted declaration cannot widen the pinned target policy; absent policy remains squash-only; unavailable target objects refuse. |
| S2 | d93960111 | `CLI.test_missing_identity_refuses_before_requests`, `test_mutation_verifies_actor`, `test_enterprise_auth_uses_explicit_host_store`: no inherited provider login authority. |
| S3 | d93960111 | `Auth.test_sigterm_leaves_no_credential_files`: anonymous descriptor delivery, no token in argv, no named credential file after termination. |
| S4 | d93960111 | `ReviewRegressions.test_malformed_integration_is_unknown`: empty, false, string and malformed objects refuse; only null means absence. |
| S5 | d93960111 | `ReviewRegressions.test_null_thread_is_not_resolved`: resolution must be boolean. |
| S6 | d93960111; fixture completion 02c7f2b18 and 8325df706 | `Auth.test_no_identity_cannot_borrow_shared_or_git_config`, `test_host_bound_credential_and_xtrace`, `test_provider_store_separates_seat_and_service`; explicit-actor fixture migration. |
| C1 | d93960111 | `ReviewRegressions.test_same_sha_retarget_snapshot_refused`, `test_retarget_merge_never_writes`: full head/base ref, SHA and repository identity stay pinned across policy, aggregation and pre-write/readback checks. |
| C2 | e62adbcc2 | `Parity.test_independent_paths_match_expected_timeline_verdicts`, `test_profiles_use_same_independent_entry_paths`, `test_ready_and_stop_oracle`, `test_missing_notifier_cannot_arm`: separate wake/status and notifier/predicate/reread paths, recorded calls, independent expected verdicts and no writes. |
| C3 | e62adbcc2, 02c7f2b18 | Offline schema fixtures in `test-detect-platform-outside-repo.sh`, `test-issue-create-body-safety.sh`, `test-pr-metadata-gitea.sh`; unexpected network clients denied in the isolated sweep. |
| C4 | e62adbcc2 | `test-pr-skill-contract.py`: optional operator-local forbidden patterns default to empty; generic placeholder control. |
| C5 | e62adbcc2 | `git diff --check 924438016e2eb1c2ca352bfbf9107143b8b342bb`: exit 0. |
The policy adapter reads the declaration with `git show` from the provider's pinned
base commit in the origin-matched object store. It never consults the PR checkout's
policy or invokes its local declaration loader for mutation authority. The installed
validator still checks the target declaration. Missing target objects fail closed.
GitHub uses the explicit host-bound credential transport rather than inherited CLI
sessions. Every authenticated adapter path requires an actor, and mutation transport
verifies the authenticated account first. The inherited credential resolver no longer
uses shared tokens or git-config identity. Anonymous pipes carry authorization while
stdin remains available for request JSON. Credential tracing is disabled in a subshell.
The independent parity oracle was also run against two scratch-copy mutants. The
unmodified oracle exited 0; replacing ready with waiting exited 1; removing the
full-stop scheduled-integration guard exited 1. The mutation-control driver exited 0
only after asserting those child results. Live T3 and notifier delivery remain unverified.
## Verification
The per-suite results are recorded under `review_round_1` in
[the evidence file](1841-forge-wrapper-evidence.json). Python suites all exit 0:
103 adapter contract cases, 10 CLI cases, 4 authentication cases, 4 guard cases,
6 policy cases, 5 skill cases and 6 parity cases (138 runner cases total).
The declaration validator passes 101 assertions; Ajv schema and negative controls
exit 0. Bash syntax checks pass for all 107 git shell files. Full ShellCheck exits 1
on both the prior candidate (106 files) and the revised tree (107 files), with no
new file/code/message diagnostics. Corrected credential fixtures separately exit 0.
Workspace preflight, typecheck, lint and format checks exit 0. The three package
skill/manifest Vitest suites pass 97 tests. No dependencies were installed during
this review round; the existing system ShellCheck binary was used. Generated
push-guard fixtures now use task scratch storage and clean up instead of leaving
malformed JSON beneath the source tree.
The isolated shell sweep ran all 67 suites: 65 passed, the milestone baseline failed,
and the issue-close fixture still depended on a shared token. Commit 8325df706
converted that final fixture to a private synthetic actor; its isolated rerun and
ShellCheck both exit 0. There are therefore 66 passing suites after the targeted
correction, with the original sweep codes and rerun preserved separately.
The milestone-create usage suite still exits 1: missing title reaches the no-origin
error before the expected usage exit 2. The same isolated suite was run against the
pinned base and exits 1 there. This baseline failure is separate from the fixes.
## Follow-up and limits
S7 is intentionally excluded: inherited deployment-specific host/service literals
remain in `tools/git/detect-platform.sh` and `tools/git/README.md`. The eventual PR
body must list their cleanup under Follow-up, per the review scope decision.
Qualified older native review mutations and unverifiable scheduled-integration
operations remain unsupported as previously documented. No server upgrade or
repository setting change was attempted. Live provider mutation, real integration
scheduling, CI delivery and notifier delivery are unverified. Independent review of
the new head, PR creation and merge remain separate gates.
No additional local fixes were carried in this round. The original carried fix
remains Python import isolation in the shared detection/auth helper; no wholesale
operator-local tool directory was copied.
@@ -0,0 +1,77 @@
# Forge wrapper review round 2 (#1841)
Reviewed candidate: a193cf28458a7fc08f9ff4d6395526c5efd7918c.
Original base: 924438016e2eb1c2ca352bfbf9107143b8b342bb.
## Fixes and regression controls
R2-C1 and R2-S2 are fixed in bec37b6fd. Public GitHub REST and GraphQL use
`https://api.github.com`; enterprise REST uses `/api/v3` and GraphQL uses
`/api/graphql` on the declared host. Credential lookup remains bound to the
logical forge host. The CLI fake accepts only test-declared complete URLs and
rejects unexpected schemes, authorities, paths and queries. Its public and
enterprise snapshot regression requires a ready verdict, not merely equal
results from two paths.
Every nonzero curl exit now refuses before HTTP status or body parsing, including
when stdout contains a complete-looking successful HTTP response. The transport
contract covers exit 28 and 56 for reads and writes on public GitHub, enterprise
GitHub, Gitea and Forgejo; successful controls remain accepted. Failed writes
retain an unknown outcome. The focused URL and curl controls each failed with
exit 1 before the corresponding fix and passed with exit 0 afterward.
R2-S1 extends the earlier S6 fix to the complete shell credential lifetime. The
round-1 direct resolver test did not cover caller-side command-substitution
tracing; its earlier S6 completion claim was too broad. All 13 token-consuming
shell wrappers now execute credential resolution and all consumers in a private
subprocess with xtrace disabled. The caller never owns those token variables,
and tracing is never restored while they exist. The shared request helper sends
an escaped authentication config over an anonymous descriptor, preserves request
stdin, checks curl's exit status and closes the descriptor. Named authentication
config files and raw authentication headers in exec argv have been removed from
these consumers. The existing merge transport continues to use anonymous stdin
config and retains its once-only credential resolution.
`Auth.test_all_legacy_callers_keep_credentials_private_under_xtrace` executes an
authenticated endpoint in each wrapper under `bash -x`, records only safe
observations and checks stderr and argv for exposure. An inventory assertion
requires controls for every shell token consumer. Quote/backslash escaping is
checked independently. Scratch-copy mutants that remove metadata's private
boundary or put its token in curl argv are detected. Existing termination and
identity-refusal controls remain. The edit wrapper's signal test also caught a
subprocess signal-target regression; it now targets its actual Bash process and
preserves termination status without continuing to mutate.
Legacy stateful server fixtures now accept readable anonymous descriptors rather
than requiring regular authentication files. Review readback cleanup and edit
signal tests pass; these continue to assert persisted state, exact identity and
failure behavior. No live provider writes are involved.
R2-C3 updates the README to one current credential contract: private untraced
credential lifetime and anonymous authentication config delivery. Temporary files
are limited to response and policy data in approved scratch storage.
## Verification and limits
The per-command results and finding mapping are recorded in the `review_round_2`
section of [the evidence file](1841-forge-wrapper-evidence.json).
No dependencies were installed. No additional operator-local fixes were copied.
The inherited deployment-specific literals identified as S7 remain deferred to
the eventual PR's Follow-up section. Live provider writes, scheduler behavior,
notifier delivery, terminal-green CI and independent review of this revised head
remain unverified. This round does not create a PR or merge a branch.
The final isolated run contains 75 suites: all seven Python suites pass (146
runner cases), 66 of 67 git shell suites pass, and declaration validation passes.
The shell driver exits 1 solely for the separately reproduced milestone-create
missing-title baseline failure. Schema and negative controls, watch parity and
its mutant controls pass with external network clients denied. Bash syntax
passes all 107 shell files. Full ShellCheck exits 1 for both the prior candidate
and this tree; the final targeted callback annotation removes the last new
file/code/message diagnostic. Existing diagnostics remain.
Workspace preflight, typecheck, lint and format checks exit 0. The three package
skill/manifest Vitest suites pass 97 tests. `git diff --check` against the original
base exits 0. Commit hooks emit existing template configuration warnings, then
exit 0; hooks were not bypassed.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,79 @@
# Forge wrapper gaps (#1841)
Baseline: 924438016e2eb1c2ca352bfbf9107143b8b342bb, fetched integration head.
The implementation keeps provider APIs in wrappers, workflow authority in skills and
notifications in transport adapters. No live provider mutation is part of fixture tests.
Independent candidate review and integration remain separate delivery gates.
## Scope and decisions
- A: versioned capability/status envelope, exact-head checks and watch predicate.
- B: complete review/comment enumeration and verified native replies/resolution.
- C: declared merge methods and separate automatic-integration inspection/control.
- D: direct-write guard coverage, CI-wait mapping, compatibility fixtures and documentation.
- Skills: packaged workflows/dependencies with upstream attribution and transport parity tests.
The optional schema-v3 merge policy was approved during implementation. Absent policy
remains squash-only. Task, promotion and explicit absorb mappings constrain method requests;
provider allow_* settings further restrict them. Invalid/unknown policy never grants authority.
Schema 1/2 stay readable; version-3 consumers retain branch gates.
Gitea/Forgejo scheduled merge state remains unsupported because inspected schemas lack
readback. Native review mutations remain unsupported on the older inspected profiles.
Unknown capabilities and required evidence fail closed. App-bound required GitHub checks
remain unknown pending a source-specific matcher. Live authentication, repository settings,
provider mutations and notifier delivery are unverified.
## Compatibility and safety work
Source is upstream framework content. Local fixes are evaluated individually; Python import
isolation was carried into the shared detection/auth helper. No wholesale local tool copy was
used. Additional fixes bind metadata detection to authority, quote declaration values as shell
words, and execute legacy credential/CI bridges from a safe working directory.
Legacy fixture clients now explicitly supply public API metadata instead of relying on host
name guessing. The no-PyYAML fixture uses an isolated no-site interpreter, because production
-I intentionally ignores PYTHONPATH. The obsolete aggregate-only CI fixture migrated to the
normalized exact-head/empty-check controls. The unknown-version validator fixture moved to
version 4 when version 3 became supported.
## Verification checkpoint
Focused adapter, shell-client, policy, guard, skill and parity tests pass. The declaration suite
also passes its schema-version migration controls. New shell entry points pass ShellCheck.
Preflight, workspace typecheck and lint pass after installing locked dependencies. The package
dry-run includes the new skills, licenses and helper assets.
Final shell sweep ran 66 suites. After the isolated declaration rerun, 65 passed; the
milestone-create missing-title usage case remains exit 1 and reproduces on the pinned base.
Its provider/no-origin error precedes the expected usage exit 2. This unrelated baseline
failure is documented instead of absorbed. An earlier issue-close failure under ambient
host state disappeared in the fully isolated run.
All 68 declaration consumption assertions pass. The unknown-version fixture now uses v4;
the v3 validator and shell-quoting control pass. New shell entry points pass ShellCheck;
changed existing wrappers have no diagnostics beyond the measured baseline. All 106 git
shell files pass bash syntax checks. Package skill/manifest tests pass 97 cases, and the real
catalog discovers all six promoted skills. Publish-list assertions retain source and licenses
and exclude test bytecode, with a two-entry bytecode negative control before the exclusion.
See 1841-forge-wrapper-evidence.json for per-suite exit codes and file mapping. Locked workspace
dependencies were installed for required push checks. ShellCheck was extracted into temporary
scratch without a system install. No services, containers, migration runner, application server,
local framework rollout or provider mutation test was started. Independent review, PR, CI,
integration and release remain pending. A host notifier is an explicit optional prerequisite
outside supported T3 notifications; this change does not install one.
Parity tests replay normalized provider evidence through mocked transports. They do not prove
live T3 support or notifier delivery. A failed required read blocks either workflow; unsupported
schedule inspection blocks full-stop in either transport. Scheduling success is never a merge
completion claim.
## Review round 2
Public/enterprise API routing, transport-error refusal and complete shell
credential privacy are recorded in [round-2 evidence](1841-forge-review-round-2.md).
The round-1 S6 claim is corrected there: resolver tracing alone did not protect
caller-side token assignments. Review fixes remain on the original branch, with
new commits and no history rewrite; PR creation and merge remain held.
+1 -3
View File
@@ -1,10 +1,8 @@
---
kind: record
status: superseded
status: active
---
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's record of a completed Federation M2 milestone is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
# Mission Scratchpad — MVP
> Append-only log. NEVER delete entries. NEVER overwrite sections.
+1 -1
View File
@@ -46,7 +46,7 @@ whitelisted — see the tool header.
| tmux | `tools/tmux/agent-send.sh` | inter-agent messaging (see "Most-used" above) |
| git | `tools/git/*.sh` | issues, PRs, milestones, CI queue guard (platform-auto-detected) |
| woodpecker | `tools/woodpecker/*.sh` | CI pipelines (`-a mosaic`\|`usc`; match git remote host) |
| portainer | `tools/portainer/*.sh` | Docker Swarm stacks (status/redeploy/list) |
| portainer | `tools/portainer/*.sh` | Optional Docker Swarm tools when a Portainer credential is available |
| coolify | `tools/coolify/*.sh` | **DEPRECATED** — superseded by Portainer; do not use for new deployments |
| authentik | `tools/authentik/*.sh` | identity (users/groups/apps/flows) |
| cloudflare | `tools/cloudflare/*.sh` | DNS (zones/records; `-a` instance) |
@@ -53,21 +53,23 @@ sends, it does not auto-reply.
### Exit codes
| rc | Meaning |
| --- | ---------------------------------------------- |
| 0 | delivered or queued |
| 1 | target session not found |
| 2 | text reached the pane but is **still a draft** |
| 3 | usage error (bad class, missing `-s`) |
| rc | Meaning |
| --- | -------------------------------------------------------------------------------------------- |
| 0 | delivered or queued |
| 1 | target session not found |
| 2 | submission unconfirmed: draft still on the input line, or no positive evidence of submission |
| 3 | usage error (bad class, missing `-s`) |
**Never retry on rc=2.** The message is in the target pane; retrying double-sends it. Confirm
instead:
**Never retry on rc=2.** The message may be in the target pane, and a retry can double-send it.
Confirm instead:
```bash
tmux capture-pane -p -t <session>:0.0 | tail -20
```
rc=2 is the normal result when the target is an idle pi seat.
rc=0 is the normal result for both idle and busy pi seats (submission confirmed by draft
transition, not by prompt glyph). rc=2 on a healthy seat is exceptional — treat it as a real
report and investigate the pane.
## Durable comms
@@ -136,7 +136,8 @@ The human is escalation-only for missing access, hard policy conflicts, or irrev
### Supported Targets
- **Portainer**: Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`.
- **Docker Swarm**: If a stack README documents `docker stack deploy` on the manager, use that deploy path and its stated verification procedure.
- **Portainer (optional)**: Use only when the estate holds a Portainer credential. Do not propose Portainer otherwise. Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`.
- **Coolify**: Deploy via `~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>`, then verify with `service-status.sh`.
- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health.
- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates.
@@ -30,18 +30,25 @@ supply it explicitly on any host where the provider CLI's default account is an
| `issue-close.sh` | Close an issue |
| `issue-reopen.sh` | Reopen a closed issue |
| Pull requests | |
| ---------------- | --------------------------------------------------------- |
| `pr-create.sh` | Open a pull request |
| `pr-edit.sh` | Edit PR title, body, base branch, or draft/ready state |
| `pr-view.sh` | Show one PR |
| `pr-list.sh` | List PRs |
| `pr-diff.sh` | Fetch a PR's diff |
| `pr-metadata.sh` | PR metadata as JSON (head SHA, base, state, mergeability) |
| `pr-review.sh` | **Place a review verdict — see the dialect note below** |
| `pr-ci-wait.sh` | Block until the PR's CI reaches a terminal state |
| `pr-merge.sh` | Merge a PR |
| `pr-close.sh` | Close a PR without merging |
| Pull requests | |
| ----------------------- | ----------------------------------------------------------- |
| `pr-create.sh` | Open a pull request |
| `pr-edit.sh` | Edit PR title, body, base branch, or draft/ready state |
| `pr-view.sh` | Show one PR |
| `pr-list.sh` | List PRs |
| `pr-diff.sh` | Fetch a PR's diff |
| `pr-metadata.sh` | PR metadata as JSON (head SHA, base, state, mergeability) |
| `pr-review.sh` | **Place a review verdict — see the dialect note below** |
| `pr-ci-wait.sh` | Block until the PR's CI reaches a terminal state |
| `pr-merge.sh` | Merge a PR |
| `pr-close.sh` | Close a PR without merging |
| `pr-status.sh` | Aggregate exact-head PR readiness and required evidence. |
| `pr-watch-check.sh` | Evaluate the watch predicate from a status snapshot. |
| `pr-review-comments.sh` | Enumerate review comments with provider-qualified IDs. |
| `pr-review-reply.sh` | Reply to a review thread when the provider supports it. |
| `pr-review-resolve.sh` | Resolve or reopen a supported review thread. |
| `pr-auto-merge.sh` | Inspect or control supported automatic merge scheduling. |
| `pr-merge-queue.sh` | Inspect or control supported native merge-queue membership. |
| Milestones | |
| --------------------- | ------------------ |
@@ -60,10 +67,12 @@ supply it explicitly on any host where the provider CLI's default account is an
| `mutate-push-guard.sh` | Regenerate the guard's mutation-coverage table from measurement, so the table cannot drift from the guard |
| `verify-clean-clone.sh` | Prove the **committed** artifact runs, from a clean clone — not the working tree |
| Context | |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `detect-platform.sh` | Resolve the provider (Gitea vs GitHub) for the current repo; every other wrapper uses it |
| `lane-brief.sh` | Live dispatch brief for a repo "lane" (milestone/label) straight from the provider |
| Context | |
| -------------------------- | ---------------------------------------------------------------------------------------- |
| `detect-platform.sh` | Resolve the provider (Gitea vs GitHub) for the current repo; every other wrapper uses it |
| `lane-brief.sh` | Live dispatch brief for a repo "lane" (milestone/label) straight from the provider |
| `forge-capabilities.sh` | Report versioned provider capabilities and unsupported operations. |
| `fixture-schema-client.sh` | Offline test helper returning a synthetic public schema; rejects other requests. |
| Workspace | |
| -------------------- | ------------------------------------------------------------------------ |
@@ -226,11 +235,11 @@ Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal
### Repo Structure Declaration (T51)
```bash
# Validate a .mosaic/repo.json declaration (schema v1/v2, host:/ grammar,
# Validate a .mosaic/repo.json declaration (schema v1/v2/v3, host:/ grammar,
# ref grammar, cross-field rules, remote normalization; spec §5)
~/.config/mosaic/tools/structure/validate-repo-json.sh <repo>/.mosaic/repo.json
# CI authoring rule: new/edited declarations must be schema_version 2
# CI authoring rule: new/edited declarations must be schema_version 2 or 3
~/.config/mosaic/tools/structure/validate-repo-json.sh <repo>/.mosaic/repo.json --require-v2
# Display mode (warns and omits root-dependent checks when MOSAIC_HOST_ROOT unset)
@@ -360,3 +369,7 @@ mosaic-ensure-excalidraw --check # verify registration
- Prefer `trash` over `rm` when available — recoverable beats gone forever
- Never run destructive commands without explicit instruction
- Write it down — "mental notes" don't survive session restarts; files do
Repository declarations also support schema v3 with an optional, strictly validated merge policy.
See [the git wrapper merge-policy contract](../tools/git/README.md#repository-merge-policy-schema-version-3).
Absent policy remains squash-only; command-line method requests do not grant merge authority.
@@ -1,16 +1,16 @@
---
name: mosaic-deploy
description: 'Full end-to-end deploy flow for Mosaic Stack projects: push branch → open PR → wait for CI → merge → redeploy Portainer stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature. Orchestrates mosaic-gitea, mosaic-woodpecker, and mosaic-portainer skills.'
description: 'Full end-to-end deployment flow: push branch → open PR → wait for CI → merge → deploy using the path documented by the stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature.'
---
# mosaic-deploy
End-to-end deployment flow for Mosaic Stack projects.
End-to-end deployment flow.
## Full Deploy Sequence
```
push branch → open PR → CI passes → merge → portainer redeploy
push branch → open PR → CI passes → merge → documented deploy path
```
### Step 1: Push branch and open PR
@@ -49,25 +49,32 @@ review. Fix the cause; never route around it with a raw API call, a shared
credential, or `force_merge`. Exceptional cases go to the operator or the
coordinating seat, still merged through the wrapper.
### Step 4: Redeploy Portainer stack
### Step 4: Deploy Through the Documented Path
Read the stack README before deploying:
- If it documents `docker stack deploy` on the manager, use that deploy path and its verification procedure.
- Use Portainer only when the estate holds a Portainer credential. Do not propose Portainer otherwise.
For an authorized Portainer deployment:
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name> -p
```
Check deployment:
Check a Portainer deployment:
```bash
~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name>
~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 50
```
## Stack Name Map
## Optional Portainer Stack Map
Maintain your estate's project → stack-name mapping in a skills-local override of
this skill (local copies take precedence over the shipped canonical one). Example
shape:
For deployments that use Portainer, maintain a project → stack-name mapping in a
skills-local override of this skill (local copies take precedence over the shipped
canonical one). Example shape:
| Project | Stack Name |
| ------------ | ----------------- |
@@ -77,6 +84,6 @@ shape:
## Notes
- Workers open PRs but **never merge** — orchestrator or Merge Guard handles step 3+
- Docker Swarm image pinning: if `-p` doesn't pull a new image, SSH to the Docker node (e.g. `node-01`) and run `docker pull <image>` manually, then redeploy
- Docker Swarm image pinning: `-p` does not change a digest-pinned image. Follow the stack README's documented deployment procedure.
- Worktrees: all coding work in `~/src/<repo>-worktrees/<task-slug>`, never in main checkout
- Always clean up worktree after push: `git worktree remove ~/src/<repo>-worktrees/<task-slug>`
@@ -1,93 +1,8 @@
---
name: mosaic-gitea
description: Interact with Gitea repositories on git.mosaicstack.dev — create/merge/close PRs, manage issues, milestones, and CI queue waits. Use when working with Mosaic Stack git repos. Wraps scripts in ~/.config/mosaic/tools/git/. Works on both Gitea and GitHub (auto-detected via detect-platform.sh).
description: Use when an older workflow requests the Mosaic Gitea wrapper reference.
---
# mosaic-gitea
# Git wrapper compatibility
Git operations via Mosaic wrapper scripts. Platform-aware (Gitea or GitHub).
## Setup
Scripts auto-detect platform from git remote. Run from inside the repo directory.
Credentials come from the framework credentials loader (never from a shared env
file):
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh
load_credentials gitea-mosaicstack
# Exports: GITEA_TOKEN, GITEA_URL
```
## Script Reference
All scripts in `~/.config/mosaic/tools/git/`.
### Pull Requests
| Script | Purpose | Key flags |
| ---------------- | ----------------------- | -------------------------------------------------------- |
| `pr-create.sh` | Create a PR | `-t "Title" [-b "Body"] [-B base] [-H head] [-i issue#]` |
| `pr-list.sh` | List open PRs | `[-s state]` |
| `pr-view.sh` | View PR details | `-n <pr#>` |
| `pr-merge.sh` | Squash-merge a PR | `-n <pr#> [-d]` (delete branch) |
| `pr-close.sh` | Close a PR | `-n <pr#>` |
| `pr-diff.sh` | Show PR diff | `-n <pr#>` |
| `pr-review.sh` | Submit a review | `-n <pr#> [-a approve\|request-changes]` |
| `pr-ci-wait.sh` | Wait for CI on PR | `-n <pr#>` |
| `pr-metadata.sh` | Get PR metadata as JSON | `-n <pr#>` |
### Issues
| Script | Purpose | Key flags |
| ------------------ | --------------- | --------------------------------------------------- |
| `issue-create.sh` | Create an issue | `-t "Title" [-b "Body"] [-l labels] [-m milestone]` |
| `issue-list.sh` | List issues | `[-s state] [-l label]` |
| `issue-view.sh` | View issue | `-n <issue#>` |
| `issue-close.sh` | Close issue | `-n <issue#>` |
| `issue-comment.sh` | Add comment | `-n <issue#> -c "Comment"` |
| `issue-assign.sh` | Assign issue | `-n <issue#> -u username` |
| `issue-edit.sh` | Edit issue | `-n <issue#> [-t title] [-b body]` |
### Milestones
| Script | Purpose | Key flags |
| --------------------- | ---------------- | ---------------------------- |
| `milestone-create.sh` | Create milestone | `-t "Title" [-d "due date"]` |
| `milestone-list.sh` | List milestones | — |
| `milestone-close.sh` | Close milestone | `-n <number>` |
### CI / Queue
| Script | Purpose | Key flags |
| -------------------- | ---------------------- | --------------------------- |
| `ci-queue-wait.sh` | Wait for CI queue slot | `[-t timeout] [-B branch]` |
| `detect-platform.sh` | Detect git platform | outputs `gitea` or `github` |
## Common Workflows
**Create PR from current branch:**
```bash
cd ~/src/<repo>
~/.config/mosaic/tools/git/pr-create.sh -t "feat: my feature" -b "Description" -i <issue#>
```
**Merge a PR (squash, with CI queue guard):**
```bash
cd ~/src/<repo>
~/.config/mosaic/tools/git/pr-merge.sh -n <pr#> -d
```
Branch protection is a gate, not an obstacle: if it blocks a merge, fix the cause —
a failing check, a moved head, or a missing review. Never bypass it with a raw
API call, a shared credential, or `force_merge`. Exceptional cases go to the
operator or the coordinating seat, still merged through the wrapper.
## Notes
- Mosaic policy: squash merges only, targeting `main`
- Gitea SSH: `[email protected]` → resolves via `~/.ssh/config` to the Gitea host's SSH port (e.g. `gitea.example.internal:2222`)
- Workers push branches and open PRs but **never merge** — orchestrator handles merges
Use [ms-git-operations](../ms-git-operations/SKILL.md) for the provider-neutral wrapper contract and capability table. Repository policy and provider capabilities determine behavior. For PR waiting use [ms-babysit-pr](../ms-babysit-pr/SKILL.md). Do not keep separate forge-specific write recipes.
@@ -1,15 +1,19 @@
---
name: mosaic-portainer
description: Manage Portainer stacks on the Mosaic infrastructure. Use when asked to list, start, stop, redeploy, or check logs of Docker Swarm stacks via Portainer. Wraps scripts in ~/.config/mosaic/tools/portainer/. Requires load_credentials portainer first.
description: Manage Docker Swarm stacks through Portainer when a Portainer credential is available. Use when asked to list, start, stop, redeploy, or check logs through Portainer.
---
# mosaic-portainer
Manage Portainer stacks via pre-built Mosaic scripts.
Manage Portainer stacks through supplied scripts.
## Decision Gate
Portainer is optional. Use this skill only when the estate holds a Portainer credential. If a stack README documents `docker stack deploy` on the manager, that is the deploy path. Do not propose Portainer otherwise.
## Setup
Always load credentials before running scripts:
After confirming a Portainer credential is available, load it before running scripts:
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh
@@ -33,11 +37,11 @@ All scripts live in `~/.config/mosaic/tools/portainer/`.
## Common Workflows
**Redeploy a stack with fresh images:**
**Redeploy a stack through Portainer:**
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n mosaic-stack -p
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name> -p
```
**Check all stack statuses:**
@@ -51,12 +55,10 @@ source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
~/.config/mosaic/tools/portainer/stack-logs.sh -n mosaic-stack -l 100
~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 100
```
## Notes
- Portainer URL: `https://portainer.example.internal:9443`
- Primary Docker host: `node-01`, managed via Portainer agent
- Docker Swarm image updates: `stack-redeploy.sh -p` does NOT guarantee new image pull if digest is pinned; SSH to node and `docker pull` first if needed
- Credentials: `load_credentials portainer` (framework credentials store)
- `stack-redeploy.sh -p` does not override a digest-pinned image. Follow the stack README's documented deployment procedure for pinned images.
- Credentials are loaded through `load_credentials portainer`.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Theo Browne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,24 @@
---
name: ms-babysit-pr
description: Use when the user asks to monitor, watch or babysit a pull request through review and CI.
---
# Babysit a PR
Follow ms-full-send's proof and authority gates, ms-git-operations and ms-watch. Monitoring grants no merge authority. Full-stop never merges or schedules.
Handle current events before waiting. Read `pr-status.sh -n NUMBER --json`, pin head/base, then enumerate `pr-review-comments.sh -n NUMBER --json` completely. Verify findings against source, including old and resolved/outdated comments. Fix real in-scope defects, prove changes and refresh independent review on the new head. Use `pr-review-reply.sh --comment-id ID --body-file PATH --expect-head SHA --json` for native replies and `pr-review-resolve.sh --thread-id ID --resolve --expect-head SHA --json` for authorized resolution. PR number and repository targeting are required too. Never substitute a PR-level issue comment for a native reply. Unsupported conversation operations and incomplete lists leave explicit blockers.
T3 linking and notifications are optional transports. When available, register the URL with `link_pull_request`, verify it through `list_thread_pull_requests`, and report failures. They never supply readiness truth.
- Delegated children return URL, head SHA and pending evidence to their parent. They establish no competing watches.
- If `watch_pull_request` supports the forge, handle current events, arm one watch and yield. On wake, reread pr-status. A runtime error is not proof of unsupported forge; report it without creating a second owner.
- If T3 watching is absent or explicitly unsupported, use one bounded ms-watch over `pr-watch-check.sh -n NUMBER --baseline ABS_JSON --json`, with explicit host/repo. Store a private baseline envelope per owner/host/repo/PR. The predicate performs one snapshot and never runs pr-ci-wait or another blocking wait.
- Watch terminal success, failure, cancellation, closure, merge and relevant changes. Preserve retry metadata on rate limits. Never convert a read error to unchanged pending.
- No supported notifier means a watch-delivery blocker. Do not claim a shell polling loop is equivalent delivery.
Wake verdicts are `merged`, `closed_unmerged`, `ready_to_merge`, `waiting`, `blocked` or `timed_out`. Failed CI is blocked. Unknown required evidence blocks readiness. Full-stop hands off a ready candidate; full-send may merge only with explicit role authority. Both paths use the same snapshot and exit/error mapping.
Call `unwatch_pull_request` or stop the fallback watch before final handoff. Report URL, head/base/merge SHAs, checks, review verdict and SHA, thread completeness, integration state, blockers, authority and watch ownership. Scheduling success is not a terminal merge result.
Adapted from [t3dotgg/tstack](https://github.com/t3dotgg/tstack/tree/1085050f3baed1ac724caa4a8166145fb2ac88af/skills/babysit-pr) (MIT), revision 1085050f3baed1ac724caa4a8166145fb2ac88af. See [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Theo Browne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,19 @@
---
name: ms-council
description: Use when the user asks for a council or an independent panel of reviews on one candidate.
---
# Council
Collect independent reviews of one exact candidate. Follow ms-full-send's identity, proof, storage and repository boundaries. Council grants no merge authority.
1. Pin candidate SHA, scope, acceptance criteria and raw test evidence. Resolve known defects first unless the requested review is about replacing the design.
2. Use the requested membership, subject to available policy and budget. Otherwise choose independent capable reviewers from the live model catalog. Do not hardcode coding/review roles by provider. Report unavailable models and explicit policy fallbacks.
3. In T3, use orchestrator_capabilities for live provider/model IDs and option descriptors. Delegate a separate read-only task per member, with explicit target options and retained taskId/clientRequestId. Elsewhere use supported delegation with equivalent scope. Do not create top-level threads for child reviews. Children neither mutate nor watch the candidate.
4. Give each member the same evidence without other members' findings. Require paths, severity, confidence, reproducible findings, verdict, reviewed SHA and optional score. Scores never replace approval.
5. Reproduce findings, fix accepted defects, explain rejected suggestions and preserve dissent. Changed candidates invalidate previous exact-head approval. Each new T3 round is a fresh delegate_task including original brief, prior findings/responses and unresolved objections, with a new stable round key.
6. Through pr-edit.sh, record reviewed SHA, verdicts, findings, dissent, round count and optional scores while preserving existing PR content. Without a PR return the same evidence to the coordinator. Missing scores remain missing; do not invent them. A co-author is not independent.
Follow the repository's review gates and authority assignments. Budget/access exhaustion leaves an explicit incomplete verdict, never an approval.
Adapted from [t3dotgg/tstack](https://github.com/t3dotgg/tstack/tree/1085050f3baed1ac724caa4a8166145fb2ac88af/skills/council) (MIT), revision 1085050f3baed1ac724caa4a8166145fb2ac88af. See [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Theo Browne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,23 @@
---
name: ms-full-send
description: Use when the user explicitly asks for full send or unattended end-to-end delivery.
---
# Full send
Own the requested change through the repository's declared completion gates. Invocation authorizes routine delivery within scope. It never waives independent review, hooks, protected operations or the assigned merge role. Repository instructions and the operator's authority boundary apply.
Use ms-git-operations for every forge action. Inspect existing worktrees and the declared integration branch first. Preserve other authors' work. Use the acting identity's own credential. Keep generated data on operator-approved disk-backed storage and set TMPDIR, TMP and TEMP before tools that create temporary files. Never force-push or bypass hooks.
1. Finish the scoped change and prove its behavior. Record baseline and candidate SHAs, test commands and actual exit codes. Follow the repository's delivery and review guides when present.
2. Obtain independent review on the exact candidate SHA. Select available models according to applicable policy, without hardcoding a provider/model division of labor. A co-author cannot approve the work. Use ms-council when a panel is requested. Changed candidates require fresh review evidence.
3. Run `ci-queue-wait.sh --purpose push` before an ordinary task-branch push. Create or edit the PR through wrappers, with explicit head/base and concise verification evidence.
4. Follow ms-babysit-pr for checks, findings and waiting. Require `pr-status.sh -n NUMBER --expect-head SHA --json` to return a complete, ready snapshot. Enumerate inline findings through `pr-review-comments.sh`; use native reply and resolution wrappers with exact-head pins. Unsupported operations leave explicit missing evidence, never fabricated issue-comment replies.
5. Revalidate the current intended base, independent review SHA, required checks, thread completeness and mergeability immediately before integration. Resolve the declared merge method; default remains squash when no merge policy is declared. `--method` requests a method and cannot override repository policy. Use `pr-merge.sh -n NUMBER --expect-head SHA --json`, within assigned merge authority. Workers return the candidate to the coordinator when the coordinator owns integration.
6. Verify merged SHA and required post-merge CI, then close tracking only when authorized. Merge, deployment and product acceptance are distinct states. Report remaining gates explicitly.
Auto-merge and queue enabling can merge immediately. They require explicit merge authority, declared method resolution and verified server-side protection against stale approvals. They are off by default. Never use scheduling to bypass final-head review or failed gates.
Report URL, head/base/merge SHAs, checks, reviewer verdict and reviewed SHA, thread completeness, auto/queue state, blockers, authority mode and watch ownership. A high review score, scheduling success or an open PR is not delivery completion. Retire any owned watch before final handoff.
Adapted from [t3dotgg/tstack](https://github.com/t3dotgg/tstack/tree/1085050f3baed1ac724caa4a8166145fb2ac88af/skills/full-send) (MIT), revision 1085050f3baed1ac724caa4a8166145fb2ac88af. See [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Theo Browne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,18 @@
---
name: ms-full-stop
description: Use when the user says full stop or asks for a reviewed PR ready for them to merge.
---
# Full stop
Follow ms-full-send's preparation, implementation, proof, independent review and ms-babysit-pr workflow. These handoff limits override integration. Never merge, enable auto-merge, enqueue a PR, push the integration/default branch, or delegate any of those actions. Only scoped task-branch commits and ordinary pushes are authorized.
Before editing an existing PR, before every push, and at handoff, inspect `pr-auto-merge.sh -n NUMBER --action inspect --json` and `pr-merge-queue.sh -n NUMBER --action inspect --json`. Establish disabled/absent states from verified readback. An unsupported inspection of a scheduler with raw scheduling support is unknown safety evidence, never proof it is disabled. Unknown, access-denied and unsupported inspection block dependent work identically inside and outside T3. For a new branch without a PR, record that no PR exists and inspect as soon as one is created.
Disable/dequeue only when explicit authority permits cancellation; re-read afterward. Otherwise report the blocker to the operator/coordinator. Inspection and cancellation never authorize enabling. Direct-commit repositories receive a prepared diff and evidence within the declared handoff boundary.
At handoff, fetch the intended base and reread `pr-status.sh -n NUMBER --expect-head SHA --json`. Require independent review on that exact SHA, complete required checks and conversations, open/non-draft state and clean mergeability. Report `ready_to_merge`, `waiting`, `blocked`, `closed_unmerged`, `merged` or `timed_out` from measured evidence. `ready_to_merge` means handoff, never permission to merge. A merge performed by someone else is reported as observed, not performed.
Retire/unwatch before handoff. Include URL or prepared diff, head/base/merge SHAs, checks, reviewer verdict and SHA, thread completeness, auto/queue states, blockers, authority mode and watch ownership. Readiness is not delivery completion.
Adapted from [t3dotgg/tstack](https://github.com/t3dotgg/tstack/tree/1085050f3baed1ac724caa4a8166145fb2ac88af/skills/full-stop) (MIT), revision 1085050f3baed1ac724caa4a8166145fb2ac88af. See [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
---
name: ms-git-operations
description: Use for git-host operations, pull requests, issues, milestones, reviews and CI queue guards.
---
# Git operations
Use installed `tools/git/` wrappers for provider operations. They own API dialects and identity; skills own workflow authority. Never bypass wrappers, hooks, repository protection or exact-head gates. Use the acting identity's own credential without persisting it in shared git config. Missing credentials fail closed. Read wrapper help; do not invent flags.
Authenticated normalized adapters require explicit `MOSAIC_GIT_IDENTITY` and a host-bound token for every provider. Missing identity refuses before requests; mutations verify the authenticated actor. Shared token variables, git config identity and inherited provider logins grant no authority. See the wrapper reference for credential provisioning.
New PR commands accept `-n NUMBER --host HOST --repo OWNER/REPO --json`, or infer an unambiguous origin. Review mutations require `--expect-head FULL_SHA`. Python helper calls use import isolation. Preserve legacy wrapper codes unless using the versioned JSON contract. See [wrapper reference](../../tools/git/README.md) for full schemas and permissions.
| Operation | Wrapper | GitHub | Qualified Gitea/Forgejo |
| ------------------------------ | ----------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| Capabilities | forge-capabilities.sh | supported | versioned profiles |
| Snapshot / predicate | pr-status.sh / pr-watch-check.sh | supported | partial fields explicit |
| Inline enumeration | pr-review-comments.sh | supported | supported |
| Native reply / resolution | pr-review-reply.sh / pr-review-resolve.sh | supported | Gitea 28.2 supported; older inspected profiles unsupported |
| Merge methods | pr-merge.sh --json | merge, squash, rebase | also rebase-merge and fast-forward-only |
| Auto-merge / queue | pr-auto-merge.sh / pr-merge-queue.sh | conditional on settings and verified protection | unsupported without documented state readback |
| Create / edit / review / close | existing pr wrappers | retained | retained API-compatible dispatch |
| CI guard / wait | ci-queue-wait.sh / pr-ci-wait.sh | retained separate contracts | retained separate contracts |
Envelope v1 contains schema_version, operation, provider, host, repo, number, head_sha, data and error. Exit codes: 0 verified result, 1 unchanged pending predicate, 2 usage, 3 POLICY-REFUSED or gate failure, 4 unsupported method/operation or forge setting, 5 access/auth, 6 malformed/incomplete/unverified, 75 transient/unknown outcome, 124 timeout. Error contains kind, message, retry_after_seconds and write_outcome. A 401/403/404 is not capability evidence. Never blindly retry an ambiguous mutation.
Merge policy is read from the trusted target/base commit pinned by the provider, never from the PR head or uncommitted checkout. Fetch that revision first; missing objects refuse. Head and base ref, SHA and repository must remain unchanged through policy, readiness and the final mutation check.
Merge policy lives in optional schema-v3 repo.json `merge`, validated through repo-decl.sh and its shared validator. allowed*methods is a non-empty subset of squash, merge, rebase, rebase-merge, fast-forward-only; default_method must belong to it. Optional shapes map task, promotion and absorb to allowed methods. No policy means squash only. An explicit method must be allowed and match any shape mapping. Forge allow*\* settings are intersected afterward; never change them. Absorb requires an explicit shape mapping. Task requests cannot invent integration authority.
Inspect auto-merge/queue separately from enabling/cancellation. Enable/enqueue can merge immediately and require explicit merge authority plus server-enforced checks, reviews and stale-approval dismissal. Full-stop never enables. Unknown schedule state blocks full-stop even when raw scheduling endpoints exist.
Use ms-babysit-pr and ms-watch for waiting. The CI queue guard controls push/merge admission, not a provider PR merge queue. Run `ci-queue-wait.sh --purpose push` before ordinary push. Required negative or unknown evidence closes the gate.
@@ -0,0 +1,16 @@
---
name: ms-watch
description: Use before creating a watch or arranging a later check.
---
# Watch
Use an existing owner/notifier first. If one command answers now, run it. Durable recurring work needs a scheduler; this bounded watch is for a wait owned by the current agent.
For PRs use ms-babysit-pr. Prefer a supported T3 watch. Otherwise verify an operator-provisioned notifier such as `tools/agent-watch/agent-watch.sh` with a unique owner-scoped name, absolute condition paths, interval at least ten seconds and a finite timeout. The notifier is an optional runtime prerequisite, not a bundled guarantee. If it is absent or cannot deliver to this agent, report a watch-delivery blocker. Do not create a polling loop in the agent turn.
The condition returns 0 for a relevant event/terminal result, 1 for unchanged pending, and any other code for a broken read. Preserve errors and retry metadata. Never mask errors with grep/test, watch only for green, or call a blocking CI wait inside the predicate.
Use `pr-watch-check.sh -n NUMBER --repo OWNER/REPO --host HOST --baseline ABS_JSON --json`. Initialize the baseline from a successful pr-status envelope in private disk-backed storage. Keep it separate per owner/host/repo/PR. Verify the control case returns 1 before arming. Watch timeout maps to timed_out, provider errors to blocked, and every wake rereads status. A watch notification is not readiness evidence.
Arm exactly one watch, yield, and retire it on final handoff. No competing child watches. Do not retry ambiguous writes, and never use raw API or credential recipes in predicates.
@@ -24,6 +24,14 @@
# $HOME points at a per-profile directory that has no credentials file.
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
#
# Gitea has one additional identity-aware path. When the resolved git identity
# names a fleet seat, gitea-mosaicstack and gitea-usc obtain the token through
# tools/git/git-credential-mosaic rather than reading a slot directly. That
# production entrypoint enforces the clean-environment and process-ancestry
# fence before it reads a seat slot. A seat-slot miss is terminal: this loader
# never substitutes the service-store token for it. URLs remain provider
# configuration and continue to come from this loader's service store.
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
@@ -94,6 +102,85 @@ _mosaic_load_woodpecker_legacy() {
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
}
_mosaic_resolve_git_identity() {
local ident="${MOSAIC_GIT_IDENTITY:-}"
if [[ -z "$ident" ]]; then
ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)"
fi
printf '%s' "$ident"
}
_mosaic_git_identity_is_seat() {
local ident="$1" brain_home
[[ -n "$ident" ]] || return 1
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
[[ -d "$brain_home/fleet/agents/$ident" ]]
}
_mosaic_gitea_seat_token_from_helper() {
# Use the production wrapper, not its Bash implementation. The wrapper
# removes BASH_ENV/function injection before the implementation evaluates
# MOSAIC_AGENT_NAME ancestry, so a loader consumer cannot bypass that fence.
local host="$1" ident="$2" script_dir helper response key value
local username="" password="" username_seen=0 password_seen=0
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
helper="$script_dir/../git/git-credential-mosaic"
if [[ ! -x "$helper" ]]; then
echo "Error: Gitea seat credential helper is unavailable: $helper" >&2
return 1
fi
if ! response="$(printf 'protocol=https\nhost=%s\n\n' "$host" | "$helper" get)"; then
return 1
fi
while IFS='=' read -r key value; do
[[ -n "$key" ]] || continue
case "$key" in
username)
if (( username_seen )); then
echo 'Error: Gitea seat credential helper returned duplicate username fields' >&2
return 1
fi
username="$value"
username_seen=1
;;
password)
if (( password_seen )); then
echo 'Error: Gitea seat credential helper returned duplicate password fields' >&2
return 1
fi
password="$value"
password_seen=1
;;
*)
echo 'Error: Gitea seat credential helper returned an invalid protocol field' >&2
return 1
;;
esac
done <<< "$response"
if [[ "$username_seen" -ne 1 || "$password_seen" -ne 1 || "$username" != "$ident" || -z "$password" ]]; then
echo "Error: Gitea seat credential helper did not return a valid credential for '$ident'" >&2
return 1
fi
printf '%s' "$password"
}
_mosaic_gitea_token() {
# $1 is the Gitea host and $2 is the legacy service-store jq path.
# Seats are delegated to the fenced helper; all other identities retain the
# existing service-store behavior. A failed seat delegation returns nonzero
# to the caller and deliberately cannot fall through to _mosaic_read_cred.
local host="$1" service_jq_path="$2" ident
ident="$(_mosaic_resolve_git_identity)"
if _mosaic_git_identity_is_seat "$ident"; then
_mosaic_gitea_seat_token_from_helper "$host" "$ident"
return
fi
_mosaic_read_cred "$service_jq_path"
}
load_credentials() {
local service="$1"
@@ -183,16 +270,31 @@ EOF
;;
gitea-mosaicstack)
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
GITEA_URL="${GITEA_URL%/}"
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
# An explicit caller value retains the loader's established precedence.
# Otherwise, a known seat delegates to the ancestry-fenced helper and a
# non-seat identity uses the established service-store lookup.
if [[ -z "${GITEA_TOKEN:-}" ]]; then
local _gitea_token
_gitea_token="$(_mosaic_gitea_token 'git.mosaicstack.dev' '.gitea.mosaicstack.token')" || return 1
GITEA_TOKEN="$_gitea_token"
fi
# Preserve the loader's contract even when the caller supplied an
# unexported shell variable before invoking load_credentials.
export GITEA_TOKEN
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
;;
gitea-usc)
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
GITEA_URL="${GITEA_URL%/}"
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
if [[ -z "${GITEA_TOKEN:-}" ]]; then
local _gitea_token
_gitea_token="$(_mosaic_gitea_token 'git.uscllc.com' '.gitea.usc.token')" || return 1
GITEA_TOKEN="$_gitea_token"
fi
export GITEA_TOKEN
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
;;
woodpecker-*)
@@ -0,0 +1,287 @@
#!/usr/bin/env bash
# Hermetic regression for load_credentials Gitea seat-slot resolution.
#
# It runs against copied framework tools under a fake HOME, fixture credentials,
# and fake seat slots only. No real credential path is read.
#
# Contract pinned here:
# G1-G3 an ancestry-owned seat resolves its own host-scoped token through
# the production git-credential-mosaic entrypoint, whether identity
# comes from env or per-worktree git config.
# G4 an owned seat with no slot fails closed and never uses the service
# token.
# G5 a seat cannot request another seat's slot through load_credentials;
# the helper's ancestry fence remains the authorization boundary.
# G6/G7 no seat identity and a non-seat identity retain service-store
# behavior.
# G8-G10 explicitly supplied GITEA_TOKEN values keep their established
# precedence and are exported for child tool processes.
# G11 non-Gitea services remain unaffected.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/credentials-gitea-seats}"
FAKE_HOME="$WORK_DIR/home"
FRAMEWORK_TOOLS="$FAKE_HOME/.config/mosaic/tools"
BRAIN_DIR="$WORK_DIR/brain"
REPO_DIR="$WORK_DIR/repo"
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
SPOOL_DIR="$WORK_DIR/spool"
LOADER="$FRAMEWORK_TOOLS/_lib/credentials.sh"
HELPER="$FRAMEWORK_TOOLS/git/git-credential-mosaic"
rm -rf "$WORK_DIR"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$FRAMEWORK_TOOLS/_lib" "$FRAMEWORK_TOOLS/git" "$BRAIN_DIR/fleet/agents" \
"$REPO_DIR" "$SPOOL_DIR"
cp "$SCRIPT_DIR/credentials.sh" "$LOADER"
cp "$SCRIPT_DIR/../git/git-credential-mosaic" "$HELPER"
cp "$SCRIPT_DIR/../git/git-credential-mosaic.impl" "$FRAMEWORK_TOOLS/git/git-credential-mosaic.impl"
chmod +x "$HELPER" "$FRAMEWORK_TOOLS/git/git-credential-mosaic.impl"
git -C "$REPO_DIR" init -q
git -C "$REPO_DIR" config user.name 'Credential seat test'
git -C "$REPO_DIR" config user.email '[email protected]'
cat > "$CREDENTIALS_FILE" <<'JSON'
{
"gitea": {
"mosaicstack": {
"url": "https://git.mosaicstack.dev",
"token": "fixture-service-token"
},
"usc": {
"url": "https://git.uscllc.com",
"token": "fixture-usc-service-token"
}
},
"woodpecker": {
"default": "mosaic",
"mosaic": {
"url": "https://ci.example.invalid",
"token": "fixture-woodpecker-token"
}
}
}
JSON
for seat in seat-owner seat-config seat-usc seat-victim empty-seat; do
mkdir -p "$BRAIN_DIR/fleet/agents/$seat/secrets"
done
printf '%s' 'fixture-owner-slot-token' > "$BRAIN_DIR/fleet/agents/seat-owner/secrets/gitea-mosaicstack-seat-owner.token"
printf '%s' 'fixture-config-slot-token' > "$BRAIN_DIR/fleet/agents/seat-config/secrets/gitea-mosaicstack-seat-config.token"
printf '%s' 'fixture-usc-slot-token' > "$BRAIN_DIR/fleet/agents/seat-usc/secrets/gitea-usc-seat-usc.token"
printf '%s' 'fixture-victim-slot-token' > "$BRAIN_DIR/fleet/agents/seat-victim/secrets/gitea-mosaicstack-seat-victim.token"
chmod 600 "$BRAIN_DIR"/fleet/agents/*/secrets/*.token
# Establishes a seat identity in an exec-frozen ancestor. The empty-name root
# carries the lineage fence, preventing the helper from seeing this suite's
# real parent process outside its hermetic fixture.
cat > "$WORK_DIR/lineage-root.sh" <<'ROOT'
#!/usr/bin/env bash
set -euo pipefail
caller="$1"
carrier="$2"
shift 2
env MOSAIC_AGENT_NAME="$caller" PATH="$PATH" HOME="$HOME" \
bash "$carrier" "$@"
ROOT
cat > "$WORK_DIR/lineage-carrier.sh" <<'CARRIER'
#!/usr/bin/env bash
set -euo pipefail
loader="$1"
brain="$2"
credentials="$3"
repo="$4"
spool="$5"
identity_source="$6"
target="$7"
preexisting_token="$8"
service="${9:-gitea-mosaicstack}"
cd "$repo"
unset GITEA_URL GITEA_TOKEN MOSAIC_GIT_IDENTITY
git config --unset mosaic.gitIdentity 2>/dev/null || true
case "$identity_source" in
env) export MOSAIC_GIT_IDENTITY="$target" ;;
config) git config mosaic.gitIdentity "$target" ;;
none) ;;
*) echo "unknown identity source: $identity_source" >&2; exit 2 ;;
esac
if [[ "$preexisting_token" != '-' ]]; then
export GITEA_TOKEN="$preexisting_token"
fi
export MOSAIC_BRAIN_HOME="$brain"
export MOSAIC_CREDENTIALS_FILE="$credentials"
export MOSAIC_CREDENTIAL_SPOOL="$spool"
# shellcheck source=/dev/null
source "$loader"
load_credentials "$service"
printf 'url=%s\ntoken=%s\n' "$GITEA_URL" "$GITEA_TOKEN"
CARRIER
chmod +x "$WORK_DIR/lineage-root.sh" "$WORK_DIR/lineage-carrier.sh"
run_lineage() {
local caller="$1" source="$2" target="$3" preset="$4" service="${5:-gitea-mosaicstack}"
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_LINEAGE_FENCE=1 \
bash "$WORK_DIR/lineage-root.sh" "$caller" "$WORK_DIR/lineage-carrier.sh" \
"$LOADER" "$BRAIN_DIR" "$CREDENTIALS_FILE" "$REPO_DIR" "$SPOOL_DIR" \
"$source" "$target" "$preset" "$service"
}
run_plain() {
local source="$1" target="$2" preset="$3" service="${4:-gitea-mosaicstack}"
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_BRAIN_HOME="$BRAIN_DIR" \
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" \
LOADER="$LOADER" REPO_DIR="$REPO_DIR" IDENTITY_SOURCE="$source" TARGET="$target" \
PRESET="$preset" SERVICE="$service" bash -c '
set -euo pipefail
cd "$REPO_DIR"
unset GITEA_URL GITEA_TOKEN MOSAIC_GIT_IDENTITY
git config --unset mosaic.gitIdentity 2>/dev/null || true
case "$IDENTITY_SOURCE" in
env) export MOSAIC_GIT_IDENTITY="$TARGET" ;;
config) git config mosaic.gitIdentity "$TARGET" ;;
none) ;;
*) exit 2 ;;
esac
if [[ "$PRESET" != "-" ]]; then export GITEA_TOKEN="$PRESET"; fi
source "$LOADER"
load_credentials "$SERVICE"
printf "url=%s\\ntoken=%s\\n" "$GITEA_URL" "$GITEA_TOKEN"
'
}
run_unexported_token() {
local service="$1"
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_BRAIN_HOME="$BRAIN_DIR" \
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" LOADER="$LOADER" SERVICE="$service" \
bash -s <<'UNEXPORTED'
set -euo pipefail
unset GITEA_URL GITEA_TOKEN MOSAIC_GIT_IDENTITY
GITEA_TOKEN='fixture-unexported-token'
source "$LOADER"
load_credentials "$SERVICE"
child_token="$(bash -c 'printf "%s" "${GITEA_TOKEN:-}"')"
[[ "$child_token" == "$GITEA_TOKEN" ]] || {
echo 'GITEA_TOKEN was not exported to a child process' >&2
exit 1
}
printf 'url=%s\ntoken=%s\n' "$GITEA_URL" "$GITEA_TOKEN"
UNEXPORTED
}
fail=0
assert_success() {
local desc="$1" expected_token="$2" expected_url="$3"
shift 3
local err="$WORK_DIR/stderr.tmp" out rc token url
: > "$err"
set +e
out=$("$@" 2>"$err")
rc=$?
set -e
if [[ "$rc" -ne 0 ]]; then
echo "FAIL: $desc — expected success, got rc=$rc" >&2
cat "$err" >&2
fail=1
return
fi
token="$(printf '%s\n' "$out" | awk -F= '/^token=/{print substr($0, 7)}')"
url="$(printf '%s\n' "$out" | awk -F= '/^url=/{print substr($0, 5)}')"
if [[ "$token" != "$expected_token" ]]; then
echo "FAIL: $desc — resolved the wrong token source" >&2
fail=1
fi
if [[ "$url" != "$expected_url" ]]; then
echo "FAIL: $desc — URL did not come from provider configuration" >&2
fail=1
fi
}
assert_refused() {
local desc="$1" expected_reason="$2"
shift 2
local err="$WORK_DIR/stderr.tmp" out rc
: > "$err"
set +e
out=$("$@" 2>"$err")
rc=$?
set -e
local diagnostics
diagnostics="$(cat "$err")"
if [[ "$rc" -eq 0 ]]; then
echo "FAIL: $desc — expected refusal, got success" >&2
fail=1
fi
if [[ -n "$out" ]]; then
echo "FAIL: $desc — refusal emitted credential output" >&2
fail=1
fi
if [[ "$diagnostics" != *"$expected_reason"* ]]; then
echo "FAIL: $desc — refusal did not retain helper reason $expected_reason" >&2
fail=1
fi
if [[ "$out$diagnostics" == *'fixture-service-token'* || "$out$diagnostics" == *'fixture-victim-slot-token'* ]]; then
echo "FAIL: $desc — refusal exposed or fell back to another store" >&2
fail=1
fi
}
# G1: env identity, owning seat, populated Mosaic slot.
assert_success 'G1 env-owned seat uses its Mosaic slot through the helper' 'fixture-owner-slot-token' 'https://git.mosaicstack.dev' \
run_lineage seat-owner env seat-owner -
# G2: the same contract when the helper and loader resolve git config identity.
assert_success 'G2 config-owned seat uses its Mosaic slot through the helper' 'fixture-config-slot-token' 'https://git.mosaicstack.dev' \
run_lineage seat-config config seat-config -
# G3: the USC arm remains host-scoped rather than borrowing Mosaic credentials.
assert_success 'G3 USC-owned seat uses its USC slot through the helper' 'fixture-usc-slot-token' 'https://git.uscllc.com' \
run_lineage seat-usc env seat-usc - gitea-usc
# G4: a seat slot miss must be terminal, never service-store fallback.
assert_refused 'G4 empty owned seat refuses without service fallback' 'no-token-for-identity' \
run_lineage empty-seat env empty-seat -
# G5: a child cannot select another seat by rewriting MOSAIC_GIT_IDENTITY.
assert_refused 'G5 cross-seat identity is refused by ancestry fencing' 'cross-seat-identity-refused' \
run_lineage seat-owner env seat-victim -
# G6/G7: non-seat paths retain the existing shared service-store behavior.
assert_success 'G6 no identity keeps the service-store path' 'fixture-service-token' 'https://git.mosaicstack.dev' \
run_plain none '' -
assert_success 'G7 non-seat identity keeps the service-store path' 'fixture-service-token' 'https://git.mosaicstack.dev' \
run_plain env service-automation -
# G8: caller-provided env values retain the established loader precedence.
assert_success 'G8 explicit GITEA_TOKEN remains caller-owned' 'fixture-preexisting-token' 'https://git.mosaicstack.dev' \
run_lineage seat-owner env seat-owner fixture-preexisting-token
# G9/G10: pre-existing shell variables keep the loader's export contract.
assert_success 'G9 unexported Mosaic token reaches child processes' 'fixture-unexported-token' 'https://git.mosaicstack.dev' \
run_unexported_token gitea-mosaicstack
assert_success 'G10 unexported USC token reaches child processes' 'fixture-unexported-token' 'https://git.uscllc.com' \
run_unexported_token gitea-usc
# G11: only Gitea has seat slots; Woodpecker remains service-scoped.
wp_out=$(env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_BRAIN_HOME="$BRAIN_DIR" \
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" MOSAIC_GIT_IDENTITY=seat-owner \
LOADER="$LOADER" bash -c '
set -euo pipefail
unset WOODPECKER_URL WOODPECKER_TOKEN
source "$LOADER"
load_credentials woodpecker
printf "%s|%s" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
')
if [[ "$wp_out" != 'https://ci.example.invalid|fixture-woodpecker-token' ]]; then
echo 'FAIL: G11 Woodpecker changed under a Gitea seat identity' >&2
fail=1
fi
if [[ "$fail" -eq 0 ]]; then
echo 'credentials Gitea seat-store regression passed'
fi
exit "$fail"
@@ -82,6 +82,27 @@ STUB
chmod +x "$FAKE_BIN/tea" "$FAKE_BIN/codex"
export PATH="$FAKE_BIN:$PATH"
# The local bare repositories have no forge authority. Inject a synthetic
# provider only for these owned fixture origins; production detection stays strict.
FIXTURE_DETECTOR="$TMP_DIR/detect-platform.sh"
export PR_FIXTURE_ROOT="$TMP_DIR" DETECT_LOG="$TMP_DIR/detect.log"
: > "$DETECT_LOG"
cat > "$FIXTURE_DETECTOR" <<'STUB'
detect_platform() {
local remote_url
remote_url=$(git remote get-url origin) || return 1
case "$remote_url" in
"$PR_FIXTURE_ROOT"/*/origin.git)
PLATFORM=gitea
export PLATFORM
printf 'fixture-gitea\n' >> "$DETECT_LOG"
printf 'gitea\n'
;;
*) return 1 ;;
esac
}
STUB
# The valid fixture is a fresh clone on the non-main base. The PR head exists only
# at refs/pull/795/head, so local HEAD cannot accidentally satisfy the assertion.
if [[ "${1:-all}" != "fail-closed" ]]; then
@@ -90,6 +111,8 @@ if [[ "${1:-all}" != "fail-closed" ]]; then
cd "$VALID_WORK"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
# shellcheck source=/dev/null
source "$FIXTURE_DETECTOR"
diff_context=$(build_diff_context pr 795)
assert_contains "$diff_context" "actual-pr-change"
@@ -123,7 +146,7 @@ if [[ "${1:-all}" != "pr-head" ]]; then
cp "$SCRIPT_DIR/schemas/code-review-schema.json" \
"$SCRIPT_DIR/schemas/security-review-schema.json" \
"$SANDBOX/tools/codex/schemas/"
cp "$SCRIPT_DIR/../git/detect-platform.sh" "$SANDBOX/tools/git/"
cp "$FIXTURE_DETECTOR" "$SANDBOX/tools/git/detect-platform.sh"
cat > "$SANDBOX/tools/git/pr-review.sh" <<'STUB'
#!/bin/bash
@@ -156,3 +179,9 @@ STUB
echo "ok - empty ${review_kind} PR diff fails closed before Codex and auto-post"
done
fi
expected_detections=3
[[ "${1:-all}" == "pr-head" ]] && expected_detections=1
[[ "${1:-all}" == "fail-closed" ]] && expected_detections=2
[[ "$(wc -l < "$DETECT_LOG")" -eq "$expected_detections" ]] || \
fail "synthetic provider seam was not used by every fixture path"
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+209 -3
View File
@@ -21,7 +21,11 @@ A successful provider write command—or a wrapper message based only on that co
## Credential handling
The Gitea API token is **never passed on a curl command line.** An `Authorization: token <value>` argument would be visible to any local process that can read the process table (`ps` / `/proc/<pid>/cmdline`) for the lifetime of the request. Instead, every authenticated curl call writes the header into a private, mode-`0600` config file under `$TMPDIR` and passes it with `curl --config <file>` (`gitea_write_auth_config`), so only the file _path_ — never the token — appears in argv. Each such file is unlinked on every exit path (success and failure) by the caller's `RETURN` trap.
Credential resolution and the complete legacy request workflow run in a private
subprocess with xtrace disabled. Tokens stay out of caller traces and process
arguments. `gitea_curl_with_token` supplies an anonymous config descriptor to curl;
request bodies retain stdin. The normalized adapter uses the same anonymous
transport contract. No named authentication scratch files are created.
## `tea` invocation notes (Gitea)
@@ -44,8 +48,8 @@ account/token configured through `tools/_lib/credentials.sh`. That means every a
fleet commits, pushes, and opens PRs under one identity — with no cryptographic
separation between an author and a reviewer.
Both `git-credential-mosaic` and `get_gitea_token()` resolve an optional **per-agent
identity**:
`git-credential-mosaic`, `get_gitea_token()`, and the `gitea-mosaicstack` /
`gitea-usc` arms of `load_credentials` resolve an optional **per-agent identity**:
1. `MOSAIC_GIT_IDENTITY` environment variable, or
2. `git config --get mosaic.gitIdentity` (set per-worktree; persists on disk across
@@ -65,6 +69,12 @@ The store is chosen by what the identity **is**, not by which file happens to ex
`<brain>` is `MOSAIC_BRAIN_HOME` if set, else `~/.mosaic` — the same resolution
`packages/mosaic/src/fleet/brain-home.ts` performs.
The Gitea arms of `load_credentials` obtain a seat token through the production
`git-credential-mosaic` entrypoint, rather than reading the slot directly. That retains
the entrypoint's clean-environment and process-ancestry fence. The Gitea URL remains
provider configuration from the service store. A caller-supplied `GITEA_TOKEN` retains
its established environment precedence.
**There is no precedence between the two stores and no fallback from one to the other.**
A seat whose slot is empty is refused even when a same-named token sits in the framework
store. One credential lives in exactly one location: a second copy is drift rather than
@@ -194,3 +204,199 @@ Windows' bundled `bash`/`sh` when configured via `credential.helper`, without a
counterpart. A `tea`-login-based per-agent identity for the PowerShell wrappers is a
separate, larger design (mapping identities to `tea login` profiles) and is out of scope
here.
## Versioned PR contract
The provider-neutral entry points below accept `--repo OWNER/REPO --host HOST`, or
infer an unambiguous origin. PR operations require `-n NUMBER`. Use `--host`, not
`-H` (head in older wrappers). `--json` opts existing merge/CI-wait callers into
version 1; new commands always emit one JSON envelope. Diagnostics use stderr.
| Command | Additional arguments |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| forge-capabilities.sh | `--operation status\|review_comments\|review_reply\|review_resolve\|merge\|rebase_merge\|fast_forward_only\|auto_merge\|merge_queue` |
| pr-status.sh | `--expect-head FULL_SHA` optional; one bounded snapshot |
| pr-watch-check.sh | `--baseline ABSOLUTE_ENVELOPE_JSON`; one snapshot, no internal wait |
| pr-review-comments.sh | Complete paginated reviews and inline comments |
| pr-review-reply.sh | `--comment-id OPAQUE_ID --body-file PATH --expect-head FULL_SHA` |
| pr-review-resolve.sh | `--thread-id OPAQUE_ID --resolve\|--unresolve --expect-head FULL_SHA` |
| pr-merge.sh | `--json --expect-head FULL_SHA [-m METHOD]` |
| pr-auto-merge.sh | `--action inspect\|enable\|disable [--method METHOD]`; mutations require expected head |
| pr-merge-queue.sh | `--action inspect\|enqueue\|dequeue [--method METHOD]`; mutations require expected head |
| pr-ci-wait.sh | `[-t SECONDS] [-i SECONDS] [--expect-head FULL_SHA] [--json]` |
Methods are `squash`, `merge`, `rebase`, `rebase-merge`, `fast-forward-only`.
GitHub supports only the first three. Never request force-merge or manually-merged.
Normalized merge requires the local origin/declaration context, even with an explicit
host/repository, and preserves queue, branch, independent-review and exact-head gates.
Legacy squash mode retains its existing flags/output/codes. Explicit non-squash methods
use the verified adapter. Message-trailer and branch-deletion flags belong to legacy mode;
the normalized adapter refuses unknown flags rather than silently dropping them.
The envelope schema is [pr-status.schema.json](pr-status.schema.json). Fields are
`schema_version`, `operation`, `provider`, `host`, `repo`, `number`, `head_sha`, `data`
and nullable `error`. Errors contain `kind`, `message`, nullable
`retry_after_seconds`, and `write_outcome` (`none`, `confirmed`, `unknown`). No blind
write retries. Reconcile an unknown outcome against the exact returned ID/state.
| Code | Version 1 meaning |
| ---- | ----------------------------------------------------------------------- |
| 0 | Successful read or verified mutation; inspect ready/blockers separately |
| 1 | Watch predicate unchanged and pending only |
| 2 | Usage error |
| 3 | Gate failure or POLICY-REFUSED (`error.kind=policy`) |
| 4 | Unsupported API/method or forge allow\_\* setting disabled |
| 5 | Access/auth, including hidden-resource 404; never capability evidence |
| 6 | Malformed, incomplete, unknown capability or failed verification |
| 75 | Transient/rate limit, moved head or unknown transport outcome |
| 124 | Bounded wait timeout |
CI wait consumes normalized checks. Default mode maps gate/provider/read failures to
legacy exit 1, usage to 2 and timeout to 124. `--json` retains version-1 error codes.
Missing checks never become green through repeated empty polls. The CI admission guard
and push guard retain their legacy codes; CI admission is separate from a PR merge queue.
Status includes head/base refs, SHAs and repositories; closure/merge SHA; draft and
mergeability; checks with required/observed/complete; independent current-head reviews;
thread completeness; integration states; capabilities; ready/blockers; observation time
and canonical fingerprint. Head/base/closure are reread after aggregation. Incomplete
reads return nonzero with ready=false. Nullable fields remain unknown, never default false.
Fingerprint excludes observation time and includes review/comment and integration evidence.
The observed snapshot is not an atomic lock: mutations still pin the head at the provider.
Required branch policy must be readable. Empty check history is not a declared no-CI policy.
App-bound GitHub required checks currently remain unknown rather than trusting a same-name
check from another app. Code-owner review additionally requires the provider decision.
Unsupported branch-pattern policy/readback leaves missing evidence and blocks readiness.
## Qualified provider profiles and permissions
GitHub uses documented REST and GraphQL APIs. `GH_HOST` explicitly selects a configured
enterprise host. Other hosts must identify Gitea/Forgejo through public swagger metadata;
unfamiliar hosts are never guessed to be Gitea. Qualified profiles are Gitea 1.24.7 and
28.2.0, and the inspected Forgejo 16.0.0 development schema. Unknown versions expose unknown
capabilities until qualified. Legacy dispatch reports `gitea` for the compatible Forgejo API.
All profiles list every review and review-comment page. GitHub also paginates nested
reviewThreads/comments. Authoritative IDs remain provider-qualified. Gitea lists do not
invent a thread graph: thread_id/reply_to are null without evidence. For Gitea 28.2 resolution,
pass the listed `gitea:comment:ID` as the resolution selector; this names the API's review
comment, not a fabricated thread relationship. Resolved and outdated comments remain listed.
| Operation | GitHub | Gitea 28.2 | Gitea 1.24.7 / inspected Forgejo |
| --------------------------------- | ----------- | ---------------------- | -------------------------------- |
| Snapshot / inline enumeration | supported | partial state explicit | partial state explicit |
| Native reply / resolution | supported | supported | exit 4 before mutation |
| Merge/squash/rebase | supported | supported | supported |
| Rebase-merge / fast-forward-only | exit 4 | supported | supported |
| Verified auto-merge / merge queue | conditional | exit 4 | exit 4 |
| Repository policy refuses method | exit 3 | exit 3 | exit 3 |
| Forge setting disables method | exit 4 | exit 4 | exit 4 |
Public schemas inspected: Gitea swagger.v1.json; GitHub
[REST pulls](https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request) and
[GraphQL pull operations](https://docs.github.com/en/graphql/reference/pulls).
Gitea/Forgejo expose raw deferred-merge/cancellation endpoints, but the inspected schemas
provide no schedule-state readback. These wrappers therefore inspect/schedule/cancel as
unsupported without making a mutation. Deferred merge is not a native merge queue.
Reads require PR/review/check and branch-protection visibility. Replies/resolution require
conversation write permission. Merge and integration controls require repository merge
permission and applicable product settings. 401/403 is access failure; documented rate-limit
headers yield 75 and a retry delay. Neither permissions nor feature settings are modified.
Enable/enqueue require explicit authority, declared method policy, repository allow\_\* settings,
server-enforced checks and independent reviews, stale-approval dismissal and no admin bypass.
Queue configuration must agree with the policy method. Inspect/disable/dequeue are separate
operations and do not require merge policy. Every mutation requires expected head/readback;
scheduling may merge immediately and returns observed merged state rather than queue success.
## Repository merge policy (schema version 3)
Schema 1/2 remain readable. Schema 3 adds optional `merge`, with strict key/value validation
through the shared declaration validator and repo-decl helper. Consumers apply the existing
branch gates to both versions 2 and 3. Absent merge policy means allowed=[squash], default=squash.
```json
{
"schema_version": 3,
"integration_trunk": "integration",
"release_branch": "stable",
"flow": "trunk-release",
"canonical_remote": "https://git.example.test/team/project",
"canonical_clone": "host:/src/project",
"merge": {
"allowed_methods": ["squash", "merge"],
"default_method": "squash",
"shapes": { "task": "squash", "promotion": "merge", "absorb": "merge" }
}
}
```
`allowed_methods` is non-empty and unique; default and shape methods must belong to it.
Unknown keys/shape names fail validation. Promotion is declared trunk→release; absorb is
release→trunk and requires its explicit shape mapping; other allowed contributions are task.
Other targets still fail transition validation. Method requests cannot override a shape mapping.
Invalid declarations never fall back to a wider merge policy.
## Watch transport parity and release
T3 linking/watching is optional notification transport. Read normalized status after every wake.
Fallback watching uses pr-watch-check with one owner-scoped private baseline and a supported
notifier, never pr-ci-wait in a predicate. Unknown schedule state blocks full-stop in both paths.
Children return pending evidence instead of owning watches. Retire watches at handoff.
Run `test-pr-watch-parity.sh`, `test-forge-contract.py`, `test-forge-cli.py`,
`test-merge-policy.py`, `test-pr-skill-contract.py` and all legacy test-\*.sh suites.
Fixtures deny unexpected network calls. Parity tests mock notification transports; they do not
claim live notifier delivery, authenticated provider writes or T3 forge support.
The package manifest includes framework assets. Release/install the reviewed package before
operator-owned tool/skill synchronization. Verify deployed files by hash against that release;
an upgrade alone does not prove owned copies were synchronized. No local rollout is performed
by these tests or by promoting the skill sources.
Set absolute `TMPDIR` to approved scratch storage for response and policy files.
The credential transport contract is described below. Fallback notification requires
an operator-provisioned notifier; its absence is an explicit watch-delivery blocker.
This change packages the status/predicate and skills and does not install a notifier.
### Trusted policy and authentication
The normalized merge and scheduling adapters read `.mosaic/repo.json` from the
PR target's pinned base commit, using the matching local origin's object store.
Fetch the target revision before invoking a mutation. Missing objects refuse;
head commits and uncommitted declarations cannot grant merge authority. Full
head and base identities (ref, SHA, repository) must remain unchanged through
policy evaluation, readiness aggregation, admission, mutation and readback.
Authenticated adapter reads and writes require explicit `MOSAIC_GIT_IDENTITY`.
A mutation first verifies `/user` matches that identity. Inherited client login
sessions and shared token environment variables grant no authority. Existing
provider-specific seat/service token stores remain supported. Other hosts,
including GitHub, use `<brain>/credentials/forge/<host>/<identity>.token`, where
`MOSAIC_BRAIN_HOME` selects `<brain>` (default `~/.mosaic`). Provision the
selected actor's token separately. Authorization reaches curl through an
anonymous pipe; request JSON uses stdin. Tokens never enter named scratch files.
Legacy shell wrappers keep credential resolution and the complete request workflow
in a private subprocess with tracing disabled. Caller tracing never receives a
secret variable; each request consumes authorization through an anonymous descriptor.
The skill firewall accepts optional newline-separated forbidden patterns via
`MOSAIC_OPERATOR_FORBIDDEN_PATTERNS`; its default is empty. Deployment-specific
patterns belong in operator configuration, outside the framework source.
Public GitHub REST requests use `https://api.github.com`, and GraphQL uses
`https://api.github.com/graphql`. Enterprise REST requests use
`https://<host>/api/v3`, and GraphQL uses `https://<host>/api/graphql`.
The normalized adapter disables default curl configuration as its first curl
option. It follows at most three GET redirects (301, 302, 303, 307, 308), resolving
relative or absolute targets only within the original HTTPS API origin, including
its effective port. Missing or ambiguous Location headers, invalid targets,
userinfo, origin changes, downgrade, cycles and exhausted bounds refuse before a
next request. Non-GET requests, including mutations, are never redirected or replayed.
Authorization remains in anonymous descriptors throughout the read chain.
Credentials remain bound to the selected forge host, such as `github.com`,
while the adapter selects its documented API authority. CLI fixtures match
complete request URLs, including scheme, authority, path and query.
@@ -7,6 +7,14 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
# All workflow state belongs to this one private subprocess.
# shellcheck disable=SC2030,SC2031
mosaic_private_forge_main() (
set +x
BRANCH=""
TARGET_REPO=""
HEAD_SHA=""
@@ -343,7 +351,7 @@ gitea_get_branch_head_sha() {
# error. A not-yet-pushed feature branch has no in-flight pipeline, so the
# pre-push queue guard must treat 404 as "queue clear", not crash.
local resp code body
resp=$(curl -sS -H "User-Agent: curl/8" -H "Authorization: token ${token}" -w $'\n%{http_code}' "$url")
resp=$(gitea_curl_with_token "$token" -sS -H "User-Agent: curl/8" -w $'\n%{http_code}' "$url")
code="${resp##*$'\n'}"
body="${resp%$'\n'*}"
if [[ "$code" == "404" ]]; then
@@ -367,7 +375,7 @@ gitea_get_commit_status_json() {
local sha="$3"
local token="$4"
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
gitea_curl_with_token "$token" -fsSL -H "User-Agent: curl/8" "$url"
}
# Repository-admin state for the acting credential, Gitea flavor. The guard's
@@ -382,7 +390,7 @@ gitea_repo_admin_state() {
local token="$3"
local url="https://${host}/api/v1/repos/${repo}"
local resp code body
resp=$(curl -sS -H "User-Agent: curl/8" -H "Authorization: token ${token}" -w $'\n%{http_code}' "$url") || return 2
resp=$(gitea_curl_with_token "$token" -sS -H "User-Agent: curl/8" -w $'\n%{http_code}' "$url") || return 2
code="${resp##*$'\n'}"
body="${resp%$'\n'*}"
if [[ "$code" != "200" ]]; then
@@ -473,6 +481,12 @@ if [[ "$NO_CI_EXPECTED" -eq 1 && "$REQUIRE_STATUS" -eq 1 ]]; then
exit 1
fi
if [[ "$NO_CI_EXPECTED" -eq 1 && -z "${MOSAIC_GIT_IDENTITY:-}" ]]; then
record_assertion_event 'ASSERTION_UNATTRIBUTABLE' 'actor-unattributable' 'unknown' || echo 'Warning: attribution audit unavailable.' >&2
echo "Error: ASSERTION_UNATTRIBUTABLE state=unknown purpose=${PURPOSE} asserted-by=unknown reason=no-ci-expected branch=${BRANCH}; explicit MOSAIC_GIT_IDENTITY is required (exit 78)." >&2
exit 78
fi
OWNER="unknown"
REPO="unknown"
PLATFORM="unknown"
@@ -514,7 +528,7 @@ if git rev-parse --show-toplevel >/dev/null 2>&1 \
repo_decl_load
if [[ "$DECL_STATE" == invalid ]]; then
repo_decl_report_invalid
elif [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" == 2 ]]; then
elif [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" -ge 2 ]]; then
route="feature"
if [[ "$BRANCH" == "$DECL_TRUNK" ]]; then
route="trunk (integration head)"
@@ -676,3 +690,6 @@ while true; do
;;
esac
done
)
mosaic_private_forge_main "$@"
@@ -15,22 +15,30 @@ detect_platform() {
return 1
fi
# Check for GitHub
if [[ "$remote_url" == *"github.com"* ]]; then
PLATFORM="github"
export PLATFORM
echo "github"
return 0
# Parse the authority, rather than matching github.com anywhere in a URL.
local host profile rc
host=$(get_remote_host) || return 1
if [[ "$host" == github.com || ( -n "${GH_HOST:-}" && "$host" == "$GH_HOST" ) ]]; then
PLATFORM=github; export PLATFORM; echo github; return 0
fi
# Check for common Gitea indicators
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
if [[ "$remote_url" != *"gitlab.com"* ]] && \
[[ "$remote_url" != *"bitbucket.org"* ]]; then
# Assume Gitea for self-hosted repos
PLATFORM="gitea"
export PLATFORM
echo "gitea"
profile=$(curl --silent --show-error --fail --max-time 10 --url "https://$host/swagger.v1.json") || {
echo "error: forge metadata could not be verified for $host" >&2
return 1
}
rc=$(printf '%s' "$profile" | python3 -I -c '
import json,sys
try:
info=json.load(sys.stdin)["info"]
title=str(info["title"]).lower(); version=str(info["version"])
print("forgejo" if "forgejo" in title or "gitea" in title and "+gitea-" in version else "gitea" if "gitea" in title else "unknown")
except (ValueError, KeyError, TypeError):
print("unknown")
') || return 1
if [[ "$rc" == gitea || "$rc" == forgejo ]]; then
# Legacy scripts dispatch both API-compatible providers as gitea.
FORGE_PROVIDER="$rc"; PLATFORM=gitea
export FORGE_PROVIDER PLATFORM
echo gitea
return 0
fi
@@ -87,7 +95,7 @@ get_repo_slug() {
gitea_url_matches_host() {
local url="${1:-}" host="${2:-}"
[[ -n "$url" && -n "$host" ]] || return 1
python3 - "$url" "$host" <<'PY'
python3 -I - "$url" "$host" <<'PY'
import sys
from urllib.parse import urlparse
@@ -156,7 +164,7 @@ find_tea_login_for_host() {
command -v tea >/dev/null 2>&1 || return 1
logins_json=$(tea login list --output json 2>/dev/null) || return 1
TEA_LOGINS_JSON="$logins_json" python3 - "$host" <<'PY'
TEA_LOGINS_JSON="$logins_json" python3 -I - "$host" <<'PY'
import json
import os
import sys
@@ -226,7 +234,7 @@ tea_login_exists() {
local logins_json
command -v tea >/dev/null 2>&1 || return 1
logins_json=$(tea login list --output json 2>/dev/null) || return 1
TEA_LOGINS_JSON="$logins_json" python3 - "$login_name" <<'PY_INNER'
TEA_LOGINS_JSON="$logins_json" python3 -I - "$login_name" <<'PY_INNER'
import json, os, sys
want = sys.argv[1]
try:
@@ -246,7 +254,7 @@ tea_login_matches_host() {
command -v tea >/dev/null 2>&1 || return 1
logins_json=$(tea login list --output json 2>/dev/null) || return 1
TEA_LOGINS_JSON="$logins_json" python3 - "$login_name" "$host" <<'PY'
TEA_LOGINS_JSON="$logins_json" python3 -I - "$login_name" "$host" <<'PY'
import json
import os
import sys
@@ -281,7 +289,7 @@ print_gitea_login_diagnostic() {
available=$(
command -v tea >/dev/null 2>&1 || { echo "(tea CLI not installed)"; exit 0; }
logins_json=$(tea login list --output json 2>/dev/null) || { echo "(could not query tea login list)"; exit 0; }
TEA_LOGINS_JSON="$logins_json" python3 - <<'PY'
TEA_LOGINS_JSON="$logins_json" python3 -I - <<'PY'
import json, os
from urllib.parse import urlparse
try:
@@ -390,7 +398,7 @@ get_gitea_authenticated_user() {
command -v tea >/dev/null 2>&1 || return 1
response=$(tea api --login "$login_name" /user 2>/dev/null) || return 1
TEA_AUTHENTICATED_USER_JSON="$response" python3 - <<'PY'
TEA_AUTHENTICATED_USER_JSON="$response" python3 -I - <<'PY'
import json
import os
@@ -412,7 +420,7 @@ get_default_tea_login() {
command -v tea >/dev/null 2>&1 || return 1
logins_json=$(tea login list --output json 2>/dev/null) || return 1
TEA_LOGINS_JSON="$logins_json" python3 - <<'PY'
TEA_LOGINS_JSON="$logins_json" python3 -I - <<'PY'
import json
import os
@@ -501,7 +509,7 @@ get_host_from_url() {
local url="${1:-}"
[[ -n "$url" ]] || return 1
python3 - "$url" <<'PY'
python3 -I - "$url" <<'PY'
import sys
from urllib.parse import urlparse
@@ -531,7 +539,7 @@ get_gitea_repo_slug_for_url() {
remote_url=$(git remote get-url origin 2>/dev/null) || return 1
if [[ "$remote_url" =~ ^https?:// ]]; then
python3 - "$remote_url" "$configured_url" <<'PY'
python3 -I - "$remote_url" "$configured_url" <<'PY'
import sys
from urllib.parse import urlparse
@@ -648,160 +656,55 @@ get_gitea_url_for_host() {
}
# Resolve a Gitea API token for the given host.
# Priority: Mosaic credential loader → GITEA_TOKEN env → ~/.git-credentials
get_gitea_token() {
local host="$1"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local cred_loader="$script_dir/../_lib/credentials.sh"
# 0. Per-agent identity (Gate-16 author≠reviewer). If MOSAIC_GIT_IDENTITY, or the
# per-worktree `git config mosaic.gitIdentity`, resolves to an agent that has a
# stored credential for this host, act AS that agent so API tooling
# (pr-create, issue-create, …) authors under the right identity — matching the
# git credential helper, which this block deliberately mirrors.
local _ident="${MOSAIC_GIT_IDENTITY:-}"
local _ident_src="MOSAIC_GIT_IDENTITY"
if [[ -z "$_ident" ]]; then
_ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)"
_ident_src="git config mosaic.gitIdentity"
# Explicit identity and host-bound storage only. Disable tracing before handling
# credentials; the subshell preserves the caller's xtrace state.
get_gitea_token() (
set +x
local host="$1" _ident="${MOSAIC_GIT_IDENTITY:-}"
if [[ ! "$_ident" =~ ^[A-Za-z0-9_.-]+$ || ! "$host" =~ ^[A-Za-z0-9.-]+(:[0-9]+)?$ ]]; then
echo 'Error: explicit MOSAIC_GIT_IDENTITY and a valid host are required.' >&2
return 1
fi
# Recognized Gitea hosts are the ones carrying the per-identity token scheme.
local _idpfx=""
case "$host" in
git.uscllc.com) _idpfx=gitea-usc ;;
git.mosaicstack.dev) _idpfx=gitea-mosaicstack ;;
esac
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
local _brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
if [[ -n "$_ident" && -n "$_idpfx" ]]; then
# Credential store selection: an identity is a SEAT or a SERVICE, and which
# one it is decides where its credential lives. No precedence between the
# two stores and no fallback from one to the other — a seat with an empty
# slot fails loud rather than reading a service credential of the same name.
# One credential, one location: two copies diverge, and the stale copy fails
# in a way that reads as a revoked token rather than as drift.
local _idtok _ident_kind
local _brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}" _idtok
if [[ -n "$_idpfx" ]]; then
if [[ -d "$_brain_home/fleet/agents/$_ident" ]]; then
_ident_kind="seat"
_idtok="$_brain_home/fleet/agents/$_ident/secrets/${_idpfx}-${_ident}.token"
else
_ident_kind="service identity"
_idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${_idpfx}-${_ident}.token"
fi
if [[ -r "$_idtok" ]]; then
cat "$_idtok"
return 0
fi
# FAIL LOUD: an explicit git identity was requested for a recognized Gitea host,
# but no credential exists for THAT identity. Refuse to fall through to the
# shared/default credential loader below — silently borrowing another identity's
# token would post PRs/issues/reviews under the WRONG agent (e.g. rev2's review
# attributed to coder3), corrupting Gate-16 author≠reviewer separation. Hard-stop
# instead so the caller aborts loudly rather than acting as the wrong identity.
echo "Error: git identity '$_ident' requested (via $_ident_src) for host '$host', but no per-slot token at $_idtok." >&2
echo " Resolved as a ${_ident_kind}; there is no fallback between the seat and service stores." >&2
echo " Refusing to borrow another slot's token. Provision the credential at that path, or unset the identity to use shared credentials." >&2
else
_idtok="$_brain_home/credentials/forge/$host/$_ident.token"
fi
if [[ ! -r "$_idtok" ]]; then
echo 'Error: selected identity has no readable host-bound credential.' >&2
return 1
fi
cat "$_idtok"
)
# FAIL LOUD: no identity resolved, on a host that HAS a fleet. Where seats exist,
# every record must name the agent that made it, so an unattributable request is
# refused rather than handed the shared account. `fleet/agents` existing is the
# same signal brain-home.ts uses to decide a brain is active. A host with no fleet
# keeps the shared path below unchanged: there the shared account is the operator's
# own and there is no attribution to lose.
if [[ -z "$_ident" && -n "$_idpfx" && -d "$_brain_home/fleet/agents" ]]; then
echo "Error: no git identity resolved for host '$host', but this host runs a fleet ($_brain_home/fleet/agents)." >&2
echo " Refusing to fall back to the shared account: records it creates cannot be attributed to the agent that made them." >&2
echo " Set MOSAIC_GIT_IDENTITY=<agent-id> or 'git config mosaic.gitIdentity <agent-id>'." >&2
# Consume a credential inside an untraced shell function. Only an anonymous
# config descriptor reaches the curl process; stdin remains the request body.
# Callers must keep credential resolution and this call in their private boundary.
gitea_curl_with_token() {
set +x
local token="$1" escaped auth_fd request_rc=0
shift
if [[ -z "$token" || ${#token} -gt 4096 || "$token" == *[[:cntrl:]]* ]]; then
echo 'Error: invalid authentication credential.' >&2
return 1
fi
# 1. Mosaic credential loader (host → service mapping, run in subshell to avoid polluting env)
if [[ -f "$cred_loader" ]]; then
local token
token=$(
# shellcheck source=/dev/null
source "$cred_loader"
# Host-specific wrapper resolution must not inherit caller/global GITEA_*.
# load_credentials intentionally preserves existing env vars for interactive use,
# but metadata/merge wrappers need credentials matching the remote host.
unset GITEA_TOKEN GITEA_URL
case "$host" in
git.mosaicstack.dev) load_credentials gitea-mosaicstack 2>/dev/null ;;
git.uscllc.com) load_credentials gitea-usc 2>/dev/null ;;
*)
local matched=false
for svc in gitea-mosaicstack gitea-usc; do
unset GITEA_TOKEN GITEA_URL
load_credentials "$svc" 2>/dev/null || continue
if gitea_url_matches_host "${GITEA_URL:-}" "$host"; then
matched=true
break
fi
done
if [[ "$matched" != true ]]; then
unset GITEA_TOKEN GITEA_URL
fi
;;
esac
echo "${GITEA_TOKEN:-}"
)
if [[ -n "$token" ]]; then
echo "$token"
return 0
fi
fi
# 2. GITEA_TOKEN env var (only when GITEA_URL, if present, matches the remote host)
if [[ -n "${GITEA_TOKEN:-}" ]]; then
if [[ -z "${GITEA_URL:-}" ]] || gitea_url_matches_host "$GITEA_URL" "$host"; then
echo "$GITEA_TOKEN"
return 0
fi
fi
# 3. ~/.git-credentials file
local creds="$HOME/.git-credentials"
if [[ -f "$creds" ]]; then
local token
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
if [[ -n "$token" ]]; then
echo "$token"
return 0
fi
fi
return 1
}
# Stage the Gitea bearer credential for curl OUTSIDE the process argument vector.
# Passing "-H 'Authorization: token <value>'" on the curl command line exposes
# the token to anyone who can read the process table (ps / /proc/<pid>/cmdline)
# for the lifetime of the request. Instead, write the header into a private
# (mode 0600) curl config file and have callers pass it with `curl --config`, so
# only the FILE PATH — never the token — appears in argv. Prints the temp file
# path on success; the caller OWNS the file and MUST remove it on every exit
# path (success and failure). $1 = bearer token. Callers must not log the token.
gitea_write_auth_config() {
local token="$1" auth_file
auth_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-gitea-auth.XXXXXX") || return 1
# mktemp already creates the file with 0600; be explicit in case of an
# unusual umask so the credential is never briefly group/other readable.
chmod 600 "$auth_file" 2>/dev/null || true
# `header = "..."` is curl's config syntax for an extra request header. Only
# this filename reaches curl's argv; the token stays on disk, readable solely
# by this user, and is unlinked by the caller's trap after the request.
if ! printf 'header = "Authorization: token %s"\n' "$token" > "$auth_file"; then
rm -f "$auth_file"
return 1
fi
printf '%s' "$auth_file"
escaped="${token//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
exec {auth_fd}< <(printf 'header = "Authorization: token %s"\n' "$escaped")
curl --config "/dev/fd/$auth_fd" "$@" || request_rc=$?
exec {auth_fd}<&-
return "$request_rc"
}
# Resolve the API token for a SPECIFIC tea login name from tea's own config
@@ -828,7 +731,7 @@ get_gitea_token_for_login() {
config_file="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
[[ -f "$config_file" ]] || return 1
LOGIN_NAME="$login_name" REPO_HOST="$repo_host" python3 - "$config_file" <<'PY'
LOGIN_NAME="$login_name" REPO_HOST="$repo_host" python3 -I - "$config_file" <<'PY'
import datetime
import os
import re
@@ -1656,7 +1559,7 @@ get_gitea_basic_auth() {
return 1
fi
python3 - "$host" "$creds" <<'PY'
python3 -I - "$host" "$creds" <<'PY'
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Provision synthetic, actor/host-bound credentials for offline wrapper suites."""
import json
import re
import pathlib
import sys
home, brain, actor, host, token = sys.argv[1:]
root = pathlib.Path(brain)
p = root / 'credentials/forge' / host / (actor + '.token')
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(token)
# Existing provider-specific stores derive their prefixes from configured
# service keys. The fixture never imports an operator's credential loader.
credentials = pathlib.Path(home) / '.config/mosaic/credentials.json'
if credentials.is_file():
for service, value in json.loads(credentials.read_text()).get('gitea', {}).items():
if value.get('url', '').removeprefix('https://').rstrip('/') == host:
p = pathlib.Path(home) / '.config/mosaic/secrets/gitea-tokens' / ('gitea-' + service + '-' + actor + '.token')
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(token)
# Provider store layout is an inherited compatibility contract. Reuse its
# declared mapping without embedding deployment names in new fixture code.
source=(pathlib.Path(__file__).parent/'detect-platform.sh').read_text()
for mapped_host, prefix in re.findall(r'^\s*([A-Za-z0-9.-]+)\)\s+_idpfx=([A-Za-z0-9-]+)',source,re.MULTILINE):
if mapped_host == host:
p=pathlib.Path(home)/'.config/mosaic/secrets/gitea-tokens'/(prefix+'-'+actor+'.token')
p.parent.mkdir(parents=True,exist_ok=True)
p.write_text(token)
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Public schema fixture; all other network calls fail closed.
if [[ "$*" == *'/swagger.v1.json'* ]]; then
printf '%s\n' '{"info":{"title":"Gitea API","version":"1.24.7"}}'
exit 0
fi
printf 'Unexpected network request in offline suite\n' >&2
exit 97
@@ -0,0 +1,937 @@
#!/usr/bin/env python3
"""Provider adapters for the versioned forge wrapper contract.
Run with Python -I. No repository modules, provider code or fixture files are imported.
All network operations have finite timeouts and pagination bounds. Writes are never retried.
"""
import argparse
import datetime
import hashlib
import json
import ipaddress
import os
import pathlib
import re
import subprocess
import sys
import time
import tempfile
import urllib.parse
HERE = pathlib.Path(__file__).resolve().parent
LIMIT = 100
MAX_PAGES = 100
class Failure(Exception):
def __init__(self, code, kind, message, retry=None, outcome='none'):
super().__init__(message)
self.code, self.kind, self.retry, self.outcome = code, kind, retry, outcome
REDIRECT_STATUSES = (301, 302, 303, 307, 308)
MAX_REDIRECTS = 3
def redirect_uri(value):
"""Check raw RFC 3986 URI components before parser normalization."""
if (not value or re.search(r"[^A-Za-z0-9._~!$&'()*+;=:%/@?\[\]-]", value)
or re.search(r'%(?![0-9A-Fa-f]{2})|%(?:0[0-9A-Fa-f]|1[0-9A-Fa-f]|7[fF]|5[cC])', value)):
raise ValueError('invalid URI characters or encoding')
parts = urllib.parse.urlsplit(value)
# Brackets delimit an IP literal only in authority, never path/query globs.
if (not re.fullmatch(r"[A-Za-z0-9._~!$&'()*+;=:%/@-]*", parts.path)
or not re.fullmatch(r"[A-Za-z0-9._~!$&'()*+;=:%/@?-]*", parts.query)):
raise ValueError('invalid URI component')
return parts
def redirect_target(current, location, origin):
"""Resolve one unambiguous HTTPS target without changing API authority."""
try:
given = redirect_uri(location)
if (given.scheme and (given.scheme.lower() != 'https' or not given.netloc)
or location.startswith('//') and not given.netloc):
raise ValueError('incomplete or non-HTTPS target')
target = redirect_uri(urllib.parse.urljoin(current, location))
hostname = target.hostname
if hostname and ':' in hostname:
ipaddress.IPv6Address(hostname)
elif not hostname or not re.fullmatch(r'[A-Za-z0-9.-]+', hostname):
raise ValueError('invalid host')
if (target.scheme.lower() != 'https'
or target.username is not None or target.password is not None
or target.netloc.endswith(':') or target.fragment
or target.port is not None and not 1 <= target.port <= 65535):
raise ValueError('invalid authority')
target_origin = (target.scheme.lower(), hostname.lower(), target.port or 443)
if target_origin != origin:
raise ValueError('different API origin')
authority = ('[' + hostname.lower() + ']' if ':' in hostname else hostname.lower())
authority += ':' + str(target.port) if target.port is not None else ''
resolved = urllib.parse.urlunsplit(('https', authority, target.path or '/', target.query, ''))
redirect_uri(resolved)
return resolved
except ValueError:
raise Failure(6, 'malformed', 'Provider redirect changes or obscures the HTTPS API origin')
def capabilities(provider, version):
names = ('status', 'review_comments', 'review_reply', 'review_resolve', 'merge',
'rebase_merge', 'fast_forward_only', 'auto_merge', 'merge_queue')
result = {n: {'support': 'unknown', 'reason': 'No qualified API profile', 'schema_source': None} for n in names}
def mark(names, support, reason, source):
for n in names:
result[n] = dict(support=support, reason=reason, schema_source=source)
if provider == 'github':
source = 'https://docs.github.com/en/graphql/reference/objects#pullrequest'
mark(names, 'supported', 'Documented API; repository permissions and settings still apply', source)
mark(('rebase_merge', 'fast_forward_only'), 'unsupported', 'GitHub merge API has no such method', 'https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request')
elif (provider, version) in (('gitea', '28.2.0'), ('gitea', '1.24.7'), ('forgejo', '16.0.0-dev')):
source = 'swagger.v1.json:' + provider + ':' + version
mark(('status', 'review_comments', 'merge', 'rebase_merge', 'fast_forward_only'), 'supported', 'Qualified versioned schema', source)
mark(('review_reply', 'review_resolve'), 'supported' if version == '28.2.0' else 'unsupported', 'Native conversation routes present' if version == '28.2.0' else 'No native conversation mutation routes in qualified schema', source)
mark(('auto_merge', 'merge_queue'), 'unsupported', 'No documented schedule readback or native merge queue; raw scheduling/cancellation is not safely verifiable', source)
return result
def fingerprint(data):
stable = {k: v for k, v in data.items() if k not in ('observed_at', 'fingerprint')}
return hashlib.sha256(json.dumps(stable, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def checks(statuses, runs, required, allow_none=False):
observed = {}
for s in sorted(statuses, key=lambda s: s.get('id', 0)):
name = s.get('context')
if not name:
raise Failure(6, 'malformed', 'Status context missing')
observed[name] = s.get('state') if s.get('state') in ('success','failure','error','pending','cancelled') else 'unknown'
for r in sorted(runs, key=lambda r: r.get('id', 0)):
name = r.get('name')
if not name:
raise Failure(6, 'malformed', 'Check run name missing')
conclusion = r.get('conclusion')
observed[name] = ('pending' if r.get('status') != 'completed' else
'success' if conclusion in ('success', 'neutral', 'skipped') else
'cancelled' if conclusion == 'cancelled' else 'failure' if conclusion in ('failure', 'timed_out', 'action_required', 'startup_failure') else 'unknown')
state = 'unknown'
if required is not None:
selected = [observed.get(n, 'missing') for n in required] if required else list(observed.values())
if not selected:
state = 'not_required' if allow_none and required == [] else 'missing'
else:
state = next((s for s in ('failure', 'error', 'cancelled', 'missing', 'unknown', 'pending', 'in_progress', 'queued') if s in selected), 'success')
state = {'error': 'failure', 'in_progress': 'pending', 'queued': 'pending'}.get(state, state)
return dict(state=state, required=required, observed=observed, complete=True)
def reviews(items, head, author, required_count=1):
latest = {}
for r in sorted(items, key=lambda r: r.get('id', 0)):
login = (r.get('user') or {}).get('login')
state = str(r.get('state', '')).upper()
if login and state not in ('COMMENTED', 'PENDING'):
latest[login] = r
rejected = any(str(r.get('state')).upper() in ('REQUEST_CHANGES', 'CHANGES_REQUESTED') for r in latest.values())
approved = sum(login != author and str(r.get('state')).upper() == 'APPROVED' and r.get('commit_id') == head for login, r in latest.items()) >= max(1, required_count) and not rejected
return dict(decision='changes_requested' if rejected else 'approved' if approved else 'missing', reviewed_sha=head if approved else None, complete=True, approved=approved)
def run(argv, data=None, cwd=None, env=None, pass_fds=(), binary=False):
try:
return subprocess.run(argv, input=data.encode() if binary and data is not None else data, text=not binary, capture_output=True, timeout=45, cwd=cwd, env=env, pass_fds=pass_fds)
except (OSError, subprocess.TimeoutExpired):
raise Failure(75, 'transient', 'Provider client unavailable or timed out')
class Client:
def __init__(self, host, repo):
self.host, self.repo = host, repo
self.provider, self.version, self.token = 'unknown', '', None
self.write_attempted = False
self.actor_verified = False
self.root = '/repos/' + repo
def detect(self):
if self.host == 'github.com' or self.host == os.environ.get('GH_HOST'):
self.provider, self.version = 'github', 'documented'
return
schema = self.http('/swagger.v1.json', public=True)
info = schema.get('info', {})
title, version = str(info.get('title', '')).lower(), str(info.get('version', ''))
if 'forgejo' in title or '+gitea-' in version:
self.provider = 'forgejo'
# This profile is the inspected 16.0.0 development schema only.
self.version = '16.0.0-dev' if version.startswith('16.0.0-dev') else version
elif 'gitea' in title:
self.provider, self.version = 'gitea', version
else:
raise Failure(4, 'unsupported', 'Host metadata does not identify a qualified forge')
def credential(self):
if self.token is None:
# Fixed script/arguments; no user input becomes shell source.
p = run(['bash', '-c', 'source "$1/detect-platform.sh"; get_gitea_token "$2"', 'forge-auth', str(HERE), self.host], cwd='/')
if p.returncode or not p.stdout.strip():
raise Failure(5, 'access', 'Acting identity has no credential for this host')
self.token = p.stdout.strip()
return self.token
def http(self, path, method='GET', body=None, public=False):
writing = method != 'GET' and not (path == 'graphql' and body and body.get('query', '').lstrip().startswith('query'))
if not public:
actor = os.environ.get('MOSAIC_GIT_IDENTITY', '')
if not re.fullmatch(r'[A-Za-z0-9_.-]+', actor):
raise Failure(5, 'access', 'Explicit MOSAIC_GIT_IDENTITY is required')
token = self.credential()
if writing and not self.actor_verified:
user = self.http('/user')
if not isinstance(user, dict) or user.get('login') != actor:
raise Failure(5, 'access', 'Authenticated actor differs from explicit identity')
self.actor_verified = True
prefix = '/api/v3' if self.provider == 'github' and self.host != 'github.com' else '' if self.provider == 'github' else '/api/v1'
route = ('/graphql' if self.host == 'github.com' else '/api/graphql') if path == 'graphql' else prefix + path
authority = 'api.github.com' if self.provider == 'github' and self.host == 'github.com' and not public else self.host
url = 'https://' + authority + (path if public else route)
return self._request(url, method, body, public, token if not public else None, writing)
def _request(self, url, method, body, public, token, writing):
initial = urllib.parse.urlsplit(url)
origin = (initial.scheme.lower(), initial.hostname.lower(), initial.port or 443)
visited = {(initial.hostname.lower(), initial.port or 443, initial.path or '/', initial.query)}
for hop in range(MAX_REDIRECTS + 1):
args = ['curl', '--disable', '--globoff', '--silent', '--show-error', '--include', '--max-time', '30', '--request', method, '--header', 'Accept: application/json', '--header', 'User-Agent: mosaic-forge-wrapper']
if body is not None:
args += ['--header', 'Content-Type: application/json', '--data-binary', '@-']
args += ['--url', url]
if not public:
if len(token) > 4096 or any(ord(c) < 32 or ord(c) == 127 for c in token):
raise Failure(5, 'access', 'Credential contains invalid control characters')
escaped = token.replace('\\', '\\\\').replace('"', '\\"')
# Anonymous pipe: no credential pathname survives termination. The body
# retains stdin, while curl reads authentication from the inherited fd.
read_fd, write_fd = os.pipe()
try:
os.write(write_fd, ('header = "Authorization: token ' + escaped + '"\n').encode())
os.close(write_fd)
write_fd = None
self.auth_fd = read_fd
p = self.transport(args + ['--config', '/dev/fd/' + str(read_fd)], body, writing)
finally:
self.auth_fd = None
os.close(read_fd)
if write_fd is not None:
os.close(write_fd)
else:
p = self.transport(args, body, writing)
if p.returncode:
raise Failure(75, 'transient', 'Provider transport failed; reconcile before retrying a mutation', outcome='unknown' if writing else 'none')
# Preserve curl's bytes: text-mode universal newlines can turn a
# forbidden bare CR into a plausible header boundary before validation.
raw = p.stdout
if not isinstance(raw, bytes):
raise Failure(6, 'malformed', 'Provider response bytes missing')
status, headers = None, {}
# Proxies can emit CONNECT and servers interim response blocks.
while raw.startswith(b'HTTP/'):
block, sep, raw = raw.partition(b'\r\n\r\n')
if not sep:
raise Failure(6, 'malformed', 'Provider response headers incomplete')
byte_lines = block.split(b'\r\n')
if (not re.fullmatch(rb'HTTP/[0-9.]+ [0-9]{3}(?: [\x09\x20-\x7e\x80-\xff]*)?', byte_lines[0])
or any(not re.fullmatch(rb"[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\x09\x20-\x7e\x80-\xff]*", line) for line in byte_lines[1:])):
raise Failure(6, 'malformed', 'Provider response header framing or value is invalid')
# Only validated CRLF separators and field values reach text parsing.
lines = [line.decode('latin-1') for line in byte_lines]
status = int(lines[0].split()[1])
headers = {k.lower(): v.strip(' \t') for line in lines[1:] for k, v in [line.split(':', 1)]}
if status is None:
raise Failure(6, 'malformed', 'Provider did not return HTTP metadata')
if status in REDIRECT_STATUSES:
if method != 'GET':
raise Failure(3 if writing else 6, 'gate' if writing else 'malformed', 'Provider mutation or non-GET redirect refused', outcome='unknown' if writing else 'none')
locations = [(k, v.strip(' \t')) for line in lines[1:] if ':' in line for k, v in [line.split(':', 1)] if k.lower().strip() == 'location']
if (len(locations) != 1 or locations[0][0].lower() != 'location'
or any(line[:1].isspace() or ':' not in line for line in lines[1:])):
raise Failure(6, 'malformed', 'Provider redirect Location is missing or ambiguous')
target = redirect_target(url, locations[0][1], origin)
# Normalize default ports for loop detection as well as origin checks.
parts = urllib.parse.urlsplit(target)
identity = (parts.hostname.lower(), parts.port or 443, parts.path, parts.query)
if identity in visited or hop >= MAX_REDIRECTS:
raise Failure(6, 'malformed', 'Provider redirect loop or hop limit exceeded')
visited.add(identity)
url = target
continue
if status == 429 or status == 403 and (headers.get('x-ratelimit-remaining') == '0' or 'retry-after' in headers):
retry = headers.get('retry-after')
if retry is None:
reset = headers.get('x-ratelimit-reset','')
retry = str(max(1,int(reset)-int(time.time()))) if reset.isdigit() else '60'
raise Failure(75, 'rate_limit', 'Provider rate limit; use a bounded later wake', int(retry) if retry.isdigit() else 60, 'unknown' if writing else 'none')
if status in (401, 403, 404):
raise Failure(5, 'access', 'Provider access denied or resource hidden (HTTP %s)' % status, outcome='unknown' if writing else 'none')
if status >= 500:
raise Failure(75, 'transient', 'Provider server error', outcome='unknown' if writing else 'none')
if not 200 <= status < 300:
raise Failure(3 if writing else 6, 'gate' if writing else 'malformed', 'Provider refused request (HTTP %s)' % status)
if not raw.strip() and status == 204:
return None
try:
result = json.loads(raw)
except ValueError:
raise Failure(6, 'malformed', 'Provider returned invalid JSON', outcome='unknown' if writing else 'none')
return result
def transport(self, args, body, writing):
if writing:
self.write_attempted = True
try:
return run(args, json.dumps(body) if body is not None else None, pass_fds=(self.auth_fd,) if getattr(self, 'auth_fd', None) is not None else (), binary=True)
except Failure as e:
if writing:
e.outcome = 'unknown'
raise
def pages(self, path, key=None):
result = []
expected_total = 0
for page in range(1, MAX_PAGES + 1):
query = ('&' if '?' in path else '?') + ('per_page' if self.provider == 'github' else 'limit') + '=100&page=' + str(page)
value = self.http(path + query)
if key:
total = value.get('total_count')
if not isinstance(total,int) or isinstance(total,bool) or total < 0:
raise Failure(6,'incomplete','Check-run total_count missing or invalid')
expected_total = max(expected_total,total)
value = value.get(key)
if not isinstance(value, list):
raise Failure(6, 'malformed', 'Provider collection is not a list')
result.extend(value)
if len(value) < LIMIT:
if len(result) < expected_total:
raise Failure(6,'incomplete','Provider returned fewer records than total_count')
return result
raise Failure(6, 'incomplete', 'Pagination bound reached; collection incomplete')
def gql(self, query, variables):
result = self.http('graphql', 'POST', {'query': query, 'variables': variables})
if not isinstance(result, dict) or result.get('errors') or not isinstance(result.get('data'), dict):
raise Failure(6, 'incomplete', 'GraphQL returned errors or incomplete data', outcome='unknown' if query.lstrip().startswith('mutation') else 'none')
return result['data']
def pr(self, number):
value = self.http(self.root + '/pulls/' + str(number))
if not isinstance(value, dict) or value.get('number') != number or not re.fullmatch('[0-9a-fA-F]{40}', (value.get('head') or {}).get('sha', '')):
raise Failure(6, 'malformed', 'PR identity or head SHA missing')
return value
def require(client, operation):
cap = capabilities(client.provider, client.version)[operation]
if cap['support'] != 'supported':
raise Failure(4 if cap['support'] == 'unsupported' else 6, 'unsupported' if cap['support'] == 'unsupported' else 'incomplete', cap['reason'])
def base_identity(p):
b = p['base']
h = p['head']
return (b.get('ref'), b.get('sha'), (b.get('repo') or {}).get('full_name'), h.get('ref'), h.get('sha'), (h.get('repo') or {}).get('full_name'))
def same_base(p, original):
if base_identity(p) != base_identity(original):
raise Failure(75, 'base_moved', 'PR target changed; discard policy and readiness')
return p
def same_head(client, number, expected):
p = client.pr(number)
if p['head']['sha'] != expected:
raise Failure(75, 'head_moved', 'PR head changed; discard readiness and refresh review')
return p
def github_conversations(client, number):
owner, repo = client.repo.split('/')
cursor, seen, threads, integration = None, set(), [], None
fields = '''id isResolved isOutdated comments(first:100) { nodes { databaseId id replyTo { databaseId } } pageInfo { hasNextPage endCursor } }'''
for _ in range(MAX_PAGES):
q = '''query($owner:String!,$repo:String!,$number:Int!,$cursor:String) {
repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
id reviewDecision autoMergeRequest { mergeMethod } mergeQueueEntry { id }
reviewThreads(first:100,after:$cursor) { nodes { ''' + fields + ''' } pageInfo { hasNextPage endCursor } }
} } }'''
pr = client.gql(q, dict(owner=owner, repo=repo, number=number, cursor=cursor))['repository']['pullRequest']
if not isinstance(pr, dict) or not pr.get('id') or 'autoMergeRequest' not in pr or 'mergeQueueEntry' not in pr:
raise Failure(6, 'incomplete', 'Missing integration state')
auto, queue = pr['autoMergeRequest'], pr['mergeQueueEntry']
if auto is not None and (not isinstance(auto, dict) or auto.get('mergeMethod') not in ('SQUASH', 'MERGE', 'REBASE')):
raise Failure(6, 'malformed', 'Unknown auto-merge state')
if queue is not None and (not isinstance(queue, dict) or not isinstance(queue.get('id'), str) or not queue['id']):
raise Failure(6, 'malformed', 'Unknown merge-queue state')
if integration is None:
integration = dict(id=pr['id'], review_decision=pr.get('reviewDecision'), auto_merge={'state':'enabled' if pr['autoMergeRequest'] else 'disabled', 'method':((pr['autoMergeRequest'] or {}).get('mergeMethod') or '').lower() or None}, queue={'state':'queued' if pr['mergeQueueEntry'] else 'absent', 'entry_id':(pr['mergeQueueEntry'] or {}).get('id')})
connection = pr['reviewThreads']
for thread in connection['nodes']:
if type(thread.get('isResolved')) is not bool:
raise Failure(6, 'malformed', 'Review thread resolution must be boolean')
comments = thread['comments']
nodes = list(comments['nodes'])
nested_seen = set()
for _ in range(MAX_PAGES):
if comments['pageInfo']['hasNextPage'] is False:
break
nested_cursor = comments['pageInfo']['endCursor']
if not nested_cursor or nested_cursor in nested_seen:
raise Failure(6, 'incomplete', 'Review comment cursor missing or repeated')
nested_seen.add(nested_cursor)
comments = client.gql('''query($id:ID!,$cursor:String!) { node(id:$id) { ... on PullRequestReviewThread { comments(first:100,after:$cursor) { nodes { databaseId id replyTo { databaseId } } pageInfo { hasNextPage endCursor } } } } }''', {'id':thread['id'], 'cursor':nested_cursor})['node']['comments']
nodes.extend(comments['nodes'])
else:
raise Failure(6, 'incomplete', 'Nested comment pagination bound reached')
thread['comments'] = nodes
threads.append(thread)
if connection['pageInfo']['hasNextPage'] is False:
return threads, integration
cursor = connection['pageInfo']['endCursor']
if not cursor or cursor in seen:
raise Failure(6, 'incomplete', 'Review thread cursor missing or repeated')
seen.add(cursor)
raise Failure(6, 'incomplete', 'Review thread pagination bound reached')
def review_comments(client, number):
require(client, 'review_comments')
root = client.root + '/pulls/' + str(number)
revs = client.pages(root + '/reviews')
comments, threads, integration = [], [], None
if client.provider == 'github':
raw = client.pages(root + '/comments')
threads, integration = github_conversations(client, number)
mapping = {}
for t in threads:
for c in t['comments']:
cid = c.get('databaseId')
if cid is None or cid in mapping:
raise Failure(6, 'incomplete', 'Thread comment identity missing or duplicated')
mapping[cid] = t
for c in raw:
if c.get('id') not in mapping:
raise Failure(6, 'incomplete', 'REST comment absent from authoritative thread graph')
if set(mapping) != {c.get('id') for c in raw}:
raise Failure(6, 'incomplete', 'Comment collections disagree')
else:
raw = []
mapping = {}
for r in revs:
if r.get('id') is None:
raise Failure(6, 'malformed', 'Review ID missing')
page = client.pages(root + '/reviews/' + str(r['id']) + '/comments')
for c in page:
c = dict(c, pull_request_review_id=r['id'])
raw.append(c)
seen = set()
for c in raw:
cid = c.get('id')
if cid is None or cid in seen:
raise Failure(6, 'incomplete', 'Comment ID missing or duplicated')
seen.add(cid)
t = mapping.get(cid)
parent = c.get('in_reply_to_id')
# A resolver proves a resolved comment; it does not prove a thread graph.
resolved = t['isResolved'] if t else bool(c.get('resolver')) if 'resolver' in c else None
comments.append(dict(id=f'{client.provider}:comment:{cid}', review_id=c.get('pull_request_review_id'), thread_id=f"github:thread:{t['id']}" if t else None, reply_to=f'{client.provider}:comment:{parent}' if parent else None, author=(c.get('user') or {}).get('login'), body=c.get('body'), path=c.get('path'), line=c.get('line') or c.get('position') or c.get('new_position'), commit_sha=c.get('commit_id'), outdated=t['isOutdated'] if t else c.get('outdated'), resolved=resolved, url=c.get('html_url') or c.get('url')))
return dict(complete=True, reviews=revs, comments=comments), threads, integration
def protection(client, base):
if client.provider == 'github':
p = client.http(client.root + '/branches/' + urllib.parse.quote(base, safe='') + '/protection')
status = p.get('required_status_checks')
required = sorted(set((status or {}).get('contexts', []) + [c['context'] for c in (status or {}).get('checks', [])])) if status is not None else []
review = p.get('required_pull_request_reviews') or {}
client.review_count = review.get('required_approving_review_count', 1)
client.codeowners_required = review.get('require_code_owner_reviews') is True
enforced = bool(required and review.get('required_approving_review_count', 0) > 0 and review.get('dismiss_stale_reviews') is True and (p.get('enforce_admins') or {}).get('enabled') is True)
if status is not None and any(c.get('app_id') not in (None,-1) for c in status.get('checks',[])):
# App-bound contexts need source-specific matching; unknown never clears a gate.
required = None
return required, enforced
rules = client.pages(client.root + '/branch_protections')
matching = [r for r in rules if r.get('branch_name') == base]
if len(matching) != 1:
return None, False
r = matching[0]
client.review_count = r.get('required_approvals',1)
return (r.get('status_check_contexts') if r.get('enable_status_check') is True else None), False
def snapshot(client, number, expected):
require(client, 'status')
p = client.pr(number)
head = p['head']['sha']
if expected and head != expected:
raise Failure(3, 'gate', 'Expected head differs from PR head')
base = p['base']
required, enforced = protection(client, base['ref'])
root = client.root
statuses = client.pages(root + '/commits/' + head + '/statuses')
runs = client.pages(root + '/commits/' + head + '/check-runs', 'check_runs') if client.provider == 'github' else []
check = checks(statuses, runs, required)
comments, threads, integration = review_comments(client, number)
issue_comments = client.pages(root + '/issues/' + str(number) + '/comments')
review = reviews(comments['reviews'], head, (p.get('user') or {}).get('login'), getattr(client,'review_count',1))
if getattr(client,'codeowners_required',False) and (integration or {}).get('review_decision') != 'APPROVED':
review.update(approved=False,decision='codeowner_approval_unknown',reviewed_sha=None)
unresolved = sum(t['isResolved'] is False for t in threads) if client.provider == 'github' else sum(c['resolved'] is False for c in comments['comments']) if all(c['resolved'] is not None for c in comments['comments']) else None
thread_state = dict(unresolved=unresolved, complete=comments['complete'] and unresolved is not None)
state = 'merged' if p.get('merged') is True else p['state']
if state not in ('open','closed','merged'):
raise Failure(6,'malformed','Provider PR state is unknown')
mergeability = 'conflict' if p.get('mergeable') is False else 'clean' if p.get('mergeable') is True else 'unknown'
blockers = []
if not re.fullmatch('[0-9a-fA-F]{40}',base.get('sha','')):
blockers.append('base_sha_unknown')
if not (p['head'].get('repo') or {}).get('full_name') or not (base.get('repo') or {}).get('full_name'):
blockers.append('repository_unknown')
if state != 'open':
blockers.append(state)
if p.get('draft') is not False:
blockers.append('draft_or_unknown')
if check['state'] not in ('success', 'not_required'):
blockers.append('checks_' + check['state'])
if not review['approved']:
blockers.append('review_' + review['decision'])
if unresolved != 0:
blockers.append('review_threads_unresolved_or_unknown')
if mergeability != 'clean':
blockers.append('mergeability_' + mergeability)
data = dict(url=p.get('html_url'), state=state, draft=p.get('draft'), head_ref=p['head'].get('ref'), head_sha=head, base_ref=base.get('ref'), base_sha=base.get('sha'), head_repository=(p['head'].get('repo') or {}).get('full_name'), base_repository=(base.get('repo') or {}).get('full_name'), merge_sha=p.get('merge_commit_sha') if state == 'merged' else None, mergeability=mergeability, checks=check, reviews=review, threads=thread_state, auto_merge=integration['auto_merge'] if integration else {'state':'unsupported'}, queue=integration['queue'] if integration else {'state':'unsupported'}, capabilities=capabilities(client.provider, client.version), ready=not blockers, blockers=blockers, comments=comments['comments'], review_records=comments['reviews'], issue_comments=issue_comments, protection_enforced=enforced, observed_at=datetime.datetime.now(datetime.timezone.utc).isoformat())
after = same_head(client, number, head)
same_base(after, p)
if after.get('state') != p.get('state') or after.get('merged') != p.get('merged'):
raise Failure(75, 'snapshot_changed', 'PR base or closure changed during aggregation')
data['fingerprint'] = fingerprint(data)
return data
def owned_comment(client, number, opaque):
data, threads, integration = review_comments(client, number)
matches = [c for c in data['comments'] if c['id'] == opaque]
if len(matches) != 1:
raise Failure(3, 'gate', 'Comment is not uniquely owned by the requested PR')
return matches[0]
def reply(client, number, opaque, body, expected):
require(client, 'review_reply')
same_head(client, number, expected)
target = owned_comment(client, number, opaque)
if not re.fullmatch(re.escape(client.provider) + r':comment:[1-9][0-9]*', opaque):
raise Failure(2, 'usage', 'Use the provider-qualified comment ID from pr-review-comments')
actor = client.http('/user').get('login')
if not actor:
raise Failure(5, 'access', 'Acting user could not be verified')
same_head(client, number, expected)
cid = opaque.rsplit(':', 1)[1]
path = client.root + '/pulls/' + str(number) + '/comments/' + cid + '/replies'
created = client.http(path, 'POST', {'body':body})
if not isinstance(created, dict) or not isinstance(created.get('id'), int):
raise Failure(6, 'verification', 'Reply response has no created ID; reconcile before retry', outcome='unknown')
new_id = f"{client.provider}:comment:{created['id']}"
try:
if client.provider == 'github':
item = client.http(client.root + '/pulls/comments/' + str(created['id']))
expected_url = '/repos/' + client.repo + '/pulls/' + str(number)
parsed = urllib.parse.urlparse(item.get('pull_request_url', ''))
authority = urllib.parse.urlparse('https://' + ('api.github.com' if client.host == 'github.com' else client.host))
actual_path = parsed.path.removeprefix('/api/v3') if client.host != 'github.com' else parsed.path
if actual_path != expected_url or parsed.scheme != 'https' or parsed.hostname != authority.hostname or (parsed.port or 443) != (authority.port or 443) or parsed.username or parsed.password or item.get('in_reply_to_id') != int(cid):
raise Failure(6, 'verification', 'Reply repository or parent readback differs', outcome='unknown')
actual = {'id':f"github:comment:{item.get('id')}", 'body':item.get('body'), 'author':(item.get('user') or {}).get('login'), 'commit_sha':item.get('commit_id')}
else:
actual = owned_comment(client, number, new_id)
if actual.get('id') != new_id or actual.get('body') != body or actual.get('author') != actor or target.get('commit_sha') and actual.get('commit_sha') != target['commit_sha']:
raise Failure(6, 'verification', 'Created reply identity, author, body or commit readback differs', outcome='unknown')
same_head(client, number, expected)
except Failure as e:
e.outcome = 'unknown'
raise
return dict(id=new_id, reply_to=opaque, thread_id=target.get('thread_id'), verified=True, head_sha=expected)
def resolve(client, number, opaque, desired, expected):
require(client, 'review_resolve')
same_head(client, number, expected)
data, threads, integration = review_comments(client, number)
if client.provider == 'github':
matches = [t for t in threads if 'github:thread:' + t['id'] == opaque]
if len(matches) != 1:
raise Failure(3, 'gate', 'Thread is not uniquely owned by the requested PR')
target = matches[0]
if target.get('isResolved') is desired:
return dict(thread_id=opaque, resolved=desired, verified=True, changed=False)
same_head(client, number, expected)
mutation = 'resolveReviewThread' if desired else 'unresolveReviewThread'
client.gql('mutation($id:ID!) { ' + mutation + '(input:{threadId:$id}) { thread { id isResolved } } }', {'id':target['id']})
else:
# This API resolves a review comment. Its ID is a provider selector,
# never an invented path/line-based thread graph.
matches = [c for c in data['comments'] if c['id'] == opaque]
if len(matches) != 1 or not re.fullmatch('gitea:comment:[1-9][0-9]*', opaque):
raise Failure(3, 'gate', 'Resolution selector is not owned by this PR')
if matches[0]['resolved'] is desired:
return dict(thread_id=opaque, resolved=desired, verified=True, changed=False)
same_head(client, number, expected)
client.http(client.root + '/pulls/comments/' + opaque.rsplit(':',1)[1] + ('/resolve' if desired else '/unresolve'), 'POST')
try:
new_data, new_threads, _ = review_comments(client, number)
states = [t['isResolved'] for t in new_threads if 'github:thread:' + t['id'] == opaque] if client.provider == 'github' else [c['resolved'] for c in new_data['comments'] if c['id'] == opaque]
if states != [desired]:
raise Failure(6, 'verification', 'Resolution readback does not prove requested state', outcome='unknown')
same_head(client, number, expected)
except Failure as e:
e.outcome = 'unknown'
raise
return dict(thread_id=opaque, resolved=desired, verified=True, changed=True)
def forge_method(client, method):
if method not in ('squash','merge','rebase','rebase-merge','fast-forward-only'):
raise Failure(2, 'usage', 'Unknown merge method')
if client.provider == 'github' and method in ('rebase-merge','fast-forward-only'):
raise Failure(4, 'unsupported', 'GitHub has no ' + method + ' merge method')
settings = client.http(client.root)
key = ({'squash':'allow_squash_merge','merge':'allow_merge_commit','rebase':'allow_rebase_merge'} if client.provider == 'github' else {'squash':'allow_squash_merge','merge':'allow_merge_commits','rebase':'allow_rebase','rebase-merge':'allow_rebase_explicit','fast-forward-only':'allow_fast_forward_only_merge'})[method]
if settings.get(key) is False:
raise Failure(4, 'unsupported', 'Forge repository setting ' + key + ' disables method')
if settings.get(key) is not True:
raise Failure(6, 'incomplete', 'Forge repository setting ' + key + ' could not be verified')
return settings
def declared_method(client, p, requested):
root = run(['git','rev-parse','--show-toplevel'])
if root.returncode:
raise Failure(3,'policy','POLICY-REFUSED: mutation requires a local repository declaration context')
origin = run(['git','remote','get-url','origin']).stdout.strip()
parsed = urllib.parse.urlparse(origin if not origin.startswith('git@') else 'ssh://' + origin.replace(':','/',1))
if parsed.hostname != client.host.split(':')[0] or parsed.path.strip('/').removesuffix('.git') != client.repo:
raise Failure(3,'policy','POLICY-REFUSED: explicit target differs from local origin')
base = p['base']
if (base.get('repo') or {}).get('full_name') != client.repo or not re.fullmatch('[0-9a-fA-F]{40}', base.get('sha', '')):
raise Failure(3, 'policy', 'POLICY-REFUSED: trusted target revision unavailable')
revision = base['sha']
if run(['git', 'cat-file', '-e', revision + '^{commit}']).returncode:
raise Failure(3, 'policy', 'POLICY-REFUSED: fetch the pinned target revision before merging')
listing = run(['git', 'ls-tree', revision, '--', '.mosaic/repo.json'])
if listing.returncode:
raise Failure(6, 'verification', 'Cannot inspect pinned target declaration')
if not listing.stdout.strip():
if requested not in (None, 'squash'):
raise Failure(3, 'policy', 'POLICY-REFUSED: absent merge policy permits squash only')
if base['ref'] != client.http(client.root).get('default_branch'):
raise Failure(3, 'policy', 'POLICY-REFUSED: absent declaration requires the repository default base')
return 'squash'
content = run(['git', 'show', revision + ':.mosaic/repo.json'])
if content.returncode:
raise Failure(6, 'verification', 'Cannot read pinned target declaration')
# This temporary file contains public policy, never authentication material.
with tempfile.TemporaryDirectory() as directory:
declaration = pathlib.Path(directory) / 'repo.json'
declaration.write_text(content.stdout)
validator = run([str(HERE.parent/'structure/validate-repo-json.sh'), str(declaration), '--mode', 'display'])
if validator.returncode:
raise Failure(6, 'malformed', 'Invalid target repository declaration: ' + validator.stdout.strip())
doc = json.loads(content.stdout)
canonical = doc.get('canonical_remote', '').removesuffix('.git').rstrip('/')
normal = run([str(HERE.parent/'structure/validate-repo-json.sh'), '--normalize-remote', canonical])
origin_normal = run([str(HERE.parent/'structure/validate-repo-json.sh'), '--normalize-remote', origin])
if normal.returncode or origin_normal.returncode or normal.stdout != origin_normal.stdout:
raise Failure(3, 'policy', 'POLICY-REFUSED: target declaration canonical remote differs from origin')
args = ['python3', '-I', str(HERE/'merge-policy.py'), str(declaration), '--head', p['head']['ref'], '--base', base['ref']]
if requested:
args += ['--method', requested]
result = run(args)
value = json.loads(result.stdout)
if result.returncode:
raise Failure(result.returncode, 'policy' if result.returncode == 3 else 'malformed', value.get('error', 'Merge policy refused'))
return value['method']
def queue_guard(client, p):
# This is the CI push/merge guard, never GitHub's PR merge queue.
git_dir = run(['git','rev-parse','--absolute-git-dir'])
work_tree = run(['git','rev-parse','--show-toplevel'])
if git_dir.returncode or work_tree.returncode:
raise Failure(3,'gate','CI admission needs the verified local repository context')
guard_env = dict(os.environ,GIT_DIR=git_dir.stdout.strip(),GIT_WORK_TREE=work_tree.stdout.strip())
result = run([str(HERE/'ci-queue-wait.sh'),'--purpose','merge','--repo',client.repo,'--sha',p['head']['sha'],'-B',p['head']['ref'],'-t','1','-i','1'],cwd='/',env=guard_env)
if result.returncode:
mapping = {75:75, 78:3, 124:124}
raise Failure(mapping.get(result.returncode,3),'gate','CI queue guard refused (legacy exit %s)' % result.returncode)
def merge(client, number, requested, expected):
require(client, 'merge')
p = same_head(client,number,expected)
method = declared_method(client,p,requested)
forge_method(client,method)
status = snapshot(client,number,expected)
if (status['base_ref'], status['base_sha'], status['base_repository'], status['head_ref'], status['head_sha'], status['head_repository']) != base_identity(p):
raise Failure(75, 'base_moved', 'PR identity changed after policy evaluation')
if not status['ready']:
raise Failure(3,'gate','Merge readiness failed: ' + ', '.join(status['blockers']))
queue_guard(client,p)
same_base(same_head(client,number,expected), p)
if client.provider == 'github':
client.http(client.root+'/pulls/'+str(number)+'/merge','PUT',{'merge_method':method,'sha':expected})
else:
key = 'do' if client.version == '28.2.0' else 'Do'
client.http(client.root+'/pulls/'+str(number)+'/merge','POST',{key:method,'head_commit_id':expected})
try:
after = same_base(same_head(client,number,expected), p)
if after.get('merged') is not True or not after.get('merge_commit_sha'):
raise Failure(6,'verification','Merge not proven by PR readback',outcome='unknown')
except Failure as e:
e.outcome='unknown'
raise
return dict(requested_method=requested, effective_method=method, merged=True, merge_sha=after['merge_commit_sha'],head_sha=expected,base_ref=after['base']['ref'])
def integration(client, number, operation, action, requested, expected):
require(client,operation)
if action not in (('inspect','enable','disable') if operation == 'auto_merge' else ('inspect','enqueue','dequeue')):
raise Failure(2,'usage','Invalid action for integration operation')
if action != 'inspect' and not expected:
raise Failure(2,'usage','Mutation requires --expect-head')
p = same_head(client,number,expected) if expected else client.pr(number)
_, state = github_conversations(client,number)
key = 'auto_merge' if operation == 'auto_merge' else 'queue'
before = state[key]
if action == 'inspect':
same_base(same_head(client,number,p['head']['sha']), p)
return dict(supported=True,state=before['state'],method=before.get('method'),entry_id=before.get('entry_id'),expected_head=expected,head_sha=p['head']['sha'],verified=True)
enabling = action in ('enable','enqueue')
method = before.get('method')
if enabling:
method = declared_method(client,p,requested)
settings = forge_method(client,method)
_, enforced = protection(client,p['base']['ref'])
if not enforced:
raise Failure(3,'gate','Scheduling requires enforced checks, independent reviews, stale approval dismissal and no administrator bypass')
if p.get('state') != 'open' or p.get('draft') is not False:
raise Failure(3,'gate','Scheduling requires an open non-draft PR')
owner, repo = client.repo.split('/')
queue = client.gql('query($owner:String!,$repo:String!,$branch:String!) { repository(owner:$owner,name:$repo) { mergeQueue(branch:$branch) { configuration { mergeMethod } } } }',{'owner':owner,'repo':repo,'branch':p['base']['ref']})['repository']['mergeQueue']
queue_method = ((queue or {}).get('configuration') or {}).get('mergeMethod')
if action == 'enqueue' and not queue_method:
raise Failure(4,'unsupported','No configured merge queue method available')
if queue_method and queue_method.lower() != method:
raise Failure(3,'policy','POLICY-REFUSED: queue effective method conflicts with requested/declaration method')
if action == 'enable' and settings.get('allow_auto_merge') is not True:
raise Failure(4,'unsupported','Repository allow_auto_merge is disabled or unknown')
queue_guard(client,p)
same_base(same_head(client,number,expected), p)
variables = {'id':state['id']}
if action == 'enable':
query = 'mutation($id:ID!,$head:GitObjectID!,$method:PullRequestMergeMethod!) { enablePullRequestAutoMerge(input:{pullRequestId:$id,expectedHeadOid:$head,mergeMethod:$method}) { pullRequest { id } } }'
variables.update(head=expected,method=method.upper())
elif action == 'disable':
query = 'mutation($id:ID!) { disablePullRequestAutoMerge(input:{pullRequestId:$id}) { pullRequest { id } } }'
elif action == 'enqueue':
query = 'mutation($id:ID!,$head:GitObjectID!) { enqueuePullRequest(input:{pullRequestId:$id,expectedHeadOid:$head}) { mergeQueueEntry { id } } }'
variables['head']=expected
else:
query = 'mutation($id:ID!) { dequeuePullRequest(input:{id:$id}) { mergeQueueEntry { id } } }'
client.gql(query,variables)
try:
after = same_base(same_head(client,number,expected), p)
if after.get('merged') is True:
if not after.get('merge_commit_sha'):
raise Failure(6,'verification','Merged PR lacks merge SHA',outcome='unknown')
return dict(supported=True,state='merged',method=method,entry_id=None,expected_head=expected,verified=True,merge_sha=after['merge_commit_sha'])
_, readback = github_conversations(client,number)
desired = ('enabled' if enabling else 'disabled') if operation == 'auto_merge' else ('queued' if enabling else 'absent')
actual = readback[key]
if actual['state'] != desired or enabling and operation == 'auto_merge' and actual.get('method','').lower() != method or desired == 'queued' and not actual.get('entry_id'):
raise Failure(6,'verification','Integration readback does not prove requested state',outcome='unknown')
same_base(same_head(client,number,expected), p)
except Failure as e:
e.outcome='unknown'
raise
return dict(supported=True,state=desired,method=method,entry_id=actual.get('entry_id'),expected_head=expected,verified=True)
def workflow_verdict(data, mode):
if data.get('state') == 'merged':
return 'merged'
if data.get('state') == 'closed':
return 'closed_unmerged'
if mode == 'full-stop' and (data.get('auto_merge',{}).get('state') != 'disabled' or data.get('queue',{}).get('state') != 'absent'):
return 'blocked'
if data.get('reviews',{}).get('decision') in ('changes_requested','codeowner_approval_unknown'):
return 'blocked'
if data.get('checks',{}).get('state') in ('failure','cancelled','unknown','missing'):
return 'blocked'
if data.get('ready') is True:
return 'ready_to_merge'
if data.get('mergeability') in ('conflict','unknown') or any(b.startswith('review_threads') or 'unknown' in b for b in data.get('blockers',[])):
return 'blocked'
return 'waiting'
def ci_wait(client, number, expected, timeout, interval):
if timeout <= 0 or interval <= 0:
raise Failure(2,'usage','Timeout and interval must be positive')
deadline = time.monotonic() + timeout
while True:
data = snapshot(client,number,expected)
state = data['checks']['state']
if state in ('success','not_required'):
return data
if state in ('failure','cancelled'):
raise Failure(3,'gate','CI reached terminal ' + state)
if time.monotonic() >= deadline:
raise Failure(124,'timeout','CI wait timed out')
time.sleep(min(interval,max(0,deadline-time.monotonic())))
def main():
class Parser(argparse.ArgumentParser):
def error(self, message):
print(message, file=sys.stderr)
print(json.dumps(dict(schema_version=1,operation=sys.argv[1] if len(sys.argv)>1 else 'unknown',provider='unknown',host=None,repo=None,number=None,head_sha=None,data=None,error=dict(kind='usage',message=message,retry_after_seconds=None,write_outcome='none'))))
raise SystemExit(2)
parser = Parser(description='Versioned forge adapter. See README for provider limitations.')
parser.add_argument('operation', choices=['capabilities', 'status', 'watch-check', 'review-comments', 'review-reply', 'review-resolve', 'merge', 'auto-merge', 'merge-queue', 'ci-wait'])
parser.add_argument('-n', '--number', type=int)
parser.add_argument('-r', '--repo')
parser.add_argument('--host')
parser.add_argument('--json', action='store_true')
parser.add_argument('--operation', dest='requested_operation')
parser.add_argument('--expect-head')
parser.add_argument('--baseline')
parser.add_argument('-m', '--method')
parser.add_argument('--action')
parser.add_argument('-t','--timeout',type=int,default=1800)
parser.add_argument('-i','--interval',type=int,default=15)
parser.add_argument('--comment-id')
parser.add_argument('--thread-id')
parser.add_argument('--body-file')
resolution = parser.add_mutually_exclusive_group()
resolution.add_argument('--resolve', action='store_true')
resolution.add_argument('--unresolve', action='store_true')
args = parser.parse_args()
client, data, head, error, code = None, None, None, None, 0
try:
if args.number is not None and args.number < 1:
raise Failure(2, 'usage', 'PR number must be positive')
if args.operation != 'capabilities' and not args.number:
raise Failure(2, 'usage', '-n NUMBER is required')
if args.expect_head and not re.fullmatch('[0-9a-fA-F]{40}', args.expect_head):
raise Failure(2, 'usage', '--expect-head requires a full SHA')
if args.operation in ('review-reply','review-resolve','merge') and not args.expect_head:
raise Failure(2,'usage','Mutation requires --expect-head')
if args.operation == 'review-reply' and (not args.comment_id or not args.body_file or not pathlib.Path(args.body_file).is_file()):
raise Failure(2,'usage','Reply requires --comment-id and a readable --body-file')
if args.operation == 'review-resolve' and (not args.thread_id or not (args.resolve or args.unresolve)):
raise Failure(2,'usage','Resolution requires --thread-id and --resolve or --unresolve')
if args.operation in ('auto-merge','merge-queue') and args.action not in (('inspect','enable','disable') if args.operation == 'auto-merge' else ('inspect','enqueue','dequeue')):
raise Failure(2,'usage','Explicit --action is required')
if args.operation in ('auto-merge','merge-queue') and args.action != 'inspect' and not args.expect_head:
raise Failure(2,'usage','Mutation requires --expect-head')
if args.operation == 'watch-check' and (not args.baseline or not os.path.isabs(args.baseline)):
raise Failure(2,'usage','--baseline requires an absolute path')
if not args.host or not args.repo:
p = run(['git', 'remote', 'get-url', 'origin'])
remote = p.stdout.strip()
if remote.startswith('git@'):
remote = 'ssh://' + remote.replace(':', '/', 1)
parsed = urllib.parse.urlparse(remote)
args.host = args.host or parsed.hostname
args.repo = args.repo or parsed.path.strip('/').removesuffix('.git')
if args.host:
args.host = args.host.lower()
if not args.host or not re.fullmatch(r'[A-Za-z0-9.-]+(?::[0-9]+)?', args.host) or not args.repo or not re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', args.repo) or any(part in ('.','..') for part in args.repo.split('/')):
raise Failure(2, 'usage', 'Use an unambiguous origin or --host HOST --repo OWNER/REPO')
if args.operation != 'capabilities' and not re.fullmatch(r'[A-Za-z0-9_.-]+', os.environ.get('MOSAIC_GIT_IDENTITY', '')):
raise Failure(5, 'access', 'Explicit MOSAIC_GIT_IDENTITY is required before provider requests')
client = Client(args.host, args.repo)
client.detect()
if args.operation == 'capabilities':
data = {'version': client.version, 'operations': capabilities(client.provider, client.version)}
if args.requested_operation:
if args.requested_operation not in data['operations']:
raise Failure(2, 'usage', 'Unknown capability operation')
require(client, args.requested_operation)
elif args.operation == 'ci-wait':
data = ci_wait(client,args.number,args.expect_head,args.timeout,args.interval)
head = data['head_sha']
elif args.operation in ('merge','auto-merge','merge-queue'):
if args.operation == 'merge':
if not args.expect_head:
raise Failure(2,'usage','Merge requires --expect-head')
data = merge(client,args.number,args.method,args.expect_head)
else:
data = integration(client,args.number,'auto_merge' if args.operation == 'auto-merge' else 'merge_queue',args.action,args.method,args.expect_head)
head = data.get('head_sha') or args.expect_head
elif args.operation == 'review-comments':
before = client.pr(args.number)['head']['sha']
data, _, _ = review_comments(client, args.number)
same_head(client, args.number, before)
head = before
elif args.operation in ('review-reply', 'review-resolve'):
if not args.expect_head:
raise Failure(2, 'usage', 'Mutation requires --expect-head')
head = args.expect_head
if args.operation == 'review-reply':
if not args.comment_id or not args.body_file:
raise Failure(2, 'usage', 'Reply requires --comment-id and --body-file')
try:
body = pathlib.Path(args.body_file).read_text()
except OSError:
raise Failure(2, 'usage', 'Body file unreadable')
if not body.strip():
raise Failure(2, 'usage', 'Reply body must not be empty')
data = reply(client, args.number, args.comment_id, body, args.expect_head)
else:
if not args.thread_id or not (args.resolve or args.unresolve):
raise Failure(2, 'usage', 'Resolution requires --thread-id and --resolve or --unresolve')
data = resolve(client, args.number, args.thread_id, args.resolve, args.expect_head)
else:
data = snapshot(client, args.number, args.expect_head)
head = data['head_sha']
if args.operation == 'watch-check':
if not args.baseline or not os.path.isabs(args.baseline):
raise Failure(2, 'usage', '--baseline must be an absolute JSON path')
try:
baseline = json.loads(pathlib.Path(args.baseline).read_text())
except (OSError, ValueError):
raise Failure(6, 'malformed', 'Baseline cannot be read as JSON')
if baseline.get('schema_version') != 1 or baseline.get('error') is not None or not re.fullmatch('[0-9a-f]{64}',(baseline.get('data') or {}).get('fingerprint','')):
raise Failure(6,'malformed','Baseline is not a successful version-1 snapshot')
if any(baseline.get(k) != v for k, v in [('provider', client.provider), ('host', args.host), ('repo', args.repo), ('number', args.number)]):
raise Failure(6, 'malformed', 'Baseline belongs to a different PR/watch owner')
code = 0 if data['state'] != 'open' or data['ready'] or workflow_verdict(data,'full-send') == 'blocked' or data['fingerprint'] != (baseline.get('data') or {}).get('fingerprint') else 1
except Failure as e:
if data is None and args.operation in ('status','watch-check','ci-wait'):
data = dict(ready=False,checks={'state':'unknown','complete':False},reviews={'decision':'unknown','complete':False},threads={'unresolved':None,'complete':False})
code = e.code
if client and client.write_attempted and e.outcome == 'none' and code in (6,75,124):
e.outcome = 'unknown'
error = dict(kind=e.kind, message=str(e), retry_after_seconds=e.retry, write_outcome=e.outcome)
print(str(e), file=sys.stderr)
except (ValueError, TypeError, KeyError, AttributeError) as e:
code = 6
error = dict(kind='malformed', message='Provider evidence malformed: ' + str(e), retry_after_seconds=None, write_outcome='unknown' if client and client.write_attempted else 'none')
print(json.dumps(dict(schema_version=1, operation=args.operation, provider=client.provider if client else 'unknown', host=args.host, repo=args.repo, number=args.number, head_sha=head, data=data, error=error), sort_keys=True))
if args.operation == 'ci-wait' and not args.json and code not in (0,2,124):
return 1
return code
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" capabilities "$@"
@@ -48,6 +48,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
usage() {
echo "Usage: grant-reviewer.sh -u <user> [-r <owner>/<repo>] [-t <team>]"
echo ""
@@ -170,12 +176,7 @@ ORG="${REPO_SLUG%%/*}"
REPO_NAME="${REPO_SLUG#*/}"
RESPONSE_FILE=$(mktemp "${TMPDIR:-/tmp}/mosaic-grant-reviewer-resp.XXXXXX")
AUTH_CONFIG=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$RESPONSE_FILE"
echo "Error: could not stage Gitea credential for reviewer grant" >&2
exit 1
}
trap 'rm -f "$RESPONSE_FILE" "$AUTH_CONFIG"' EXIT
trap 'rm -f "$RESPONSE_FILE"' EXIT
# gitea_api <step> <method> <path> [json-payload]
# Runs one API call with the staged credential (token never in argv). Sets
@@ -189,9 +190,8 @@ gitea_api() {
if [[ -n "$payload" ]]; then
payload_args=(-H 'Content-Type: application/json' -d "$payload")
fi
if ! GITEA_API_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' \
if ! GITEA_API_STATUS=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$RESPONSE_FILE" -w '%{http_code}' \
-X "$method" \
--config "$AUTH_CONFIG" \
"${payload_args[@]}" \
"$GITEA_API_ROOT$path"); then
echo "Error: Gitea transport failed during $step" >&2
@@ -341,3 +341,6 @@ fi
echo "Granted: '$REVIEWER' is a member of team '$TEAM' (id $TEAM_ID) with access to '$REPO_SLUG' (code read, issues/pulls write) — verified by read-back"
echo "Note: under branch protection with required approvals this reviewer's approvals are official=false unless the branch's approvals whitelist includes the team (operator decision; reviews submitted before whitelisting must be re-submitted)."
)
mosaic_private_forge_main "$@"
@@ -9,6 +9,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
ISSUE_NUMBER=""
COMMENT=""
@@ -96,9 +102,8 @@ import os
print(json.dumps({"body": os.environ["COMMENT"]}))
PY
)
curl -fsS -X POST \
gitea_curl_with_token "$token" -fsS -X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$url" >/dev/null
@@ -109,9 +114,8 @@ gitea_issue_close_api() {
host=$(get_remote_host) || return 1
token=$(get_gitea_token "$host") || return 1
url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}"
curl -fsS -X PATCH \
gitea_curl_with_token "$token" -fsS -X PATCH \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"$url" >/dev/null
@@ -175,3 +179,6 @@ else
echo "Error: Unknown platform"
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -28,6 +28,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
ISSUE_NUMBER=""
COMMENT=""
@@ -155,8 +161,15 @@ gitea_resolve_api_for_login() {
}
fi
configured_url=$(get_gitea_url_for_host "$host") || {
echo "Error: Configured Gitea URL not found for comment read-back verification" >&2
return 1
# No monolith-configured Gitea URL for this host (#1450): seat-token-only
# hosts carry no gitea-mosaicstack/gitea-usc credentials.sh entry and no
# bare GITEA_URL, so get_gitea_url_for_host has nothing to match against.
# Synthesize the API base directly from the git remote's own host --
# exactly the trust model issue-create.sh's REST fallback already uses
# successfully on these hosts. This is NOT a cross-host guess: $host came
# from get_remote_host() reading THIS repo's own origin remote, so the
# resolved base always matches the repo actually being acted on.
configured_url="https://${host}"
}
repo=$(get_gitea_repo_slug_for_url "$configured_url") || {
echo "Error: Could not resolve Gitea owner/repository relative to configured URL" >&2
@@ -177,18 +190,12 @@ gitea_resolve_api_for_login() {
# a concurrent write from a DIFFERENT identity cannot satisfy verification.
# Prints the login on success.
gitea_authenticated_login() {
local response_file auth_config status
local response_file status
response_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-issue-comment-whoami.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$response_file"
echo "Error: could not stage Gitea credential for identity read" >&2
return 1
}
trap 'rm -f "$response_file" "$auth_config"' RETURN
trap 'rm -f "$response_file"' RETURN
if ! status=$(curl -sS -o "$response_file" -w '%{http_code}' \
--config "$auth_config" \
if ! status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$response_file" -w '%{http_code}' \
"$GITEA_API_ROOT/user"); then
echo "Error: Gitea authenticated-identity read transport failed" >&2
return 1
@@ -230,7 +237,7 @@ PY
# Args: $1 = issue number, $2 = comment body, $3 = acting identity login.
gitea_create_comment_verified() {
local issue_number="$1" comment_body="$2" acting_login="$3"
local payload write_file readback_file auth_config write_status readback_status created_id
local payload write_file readback_file write_status readback_status created_id
payload=$(COMMENT_BODY="$comment_body" python3 -c '
import json
@@ -240,16 +247,10 @@ print(json.dumps({"body": os.environ["COMMENT_BODY"]}))
')
write_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-issue-comment-write.XXXXXX")
readback_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-issue-comment-getid.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$write_file" "$readback_file"
echo "Error: could not stage Gitea credential for comment write" >&2
return 1
}
trap 'rm -f "$write_file" "$readback_file" "$auth_config"' RETURN
trap 'rm -f "$write_file" "$readback_file"' RETURN
if ! write_status=$(curl -sS -o "$write_file" -w '%{http_code}' \
if ! write_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$write_file" -w '%{http_code}' \
-X POST \
--config "$auth_config" \
-H 'Content-Type: application/json' \
-d "$payload" \
"$GITEA_API_BASE/issues/$issue_number/comments"); then
@@ -278,8 +279,7 @@ print(created_id)
PY
) || return 1
if ! readback_status=$(curl -sS -o "$readback_file" -w '%{http_code}' \
--config "$auth_config" \
if ! readback_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$readback_file" -w '%{http_code}' \
"$GITEA_API_BASE/issues/comments/$created_id"); then
echo "Error: Gitea comment read-back transport failed" >&2
return 1
@@ -414,3 +414,6 @@ else
echo "Error: Unknown platform"
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -7,6 +7,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Default values
TITLE=""
BODY=""
@@ -49,9 +55,8 @@ PY
)
url="https://${host}/api/v1/repos/${repo}/issues"
curl -fsS -X POST \
gitea_curl_with_token "$token" -fsS -X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$url"
@@ -200,3 +205,6 @@ case "$PLATFORM" in
exit 1
;;
esac
)
mosaic_private_forge_main "$@"
@@ -8,6 +8,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
ISSUE_NUMBER=""
COMMENT=""
@@ -94,9 +100,8 @@ import os
print(json.dumps({"body": os.environ["COMMENT"]}))
PY
)
curl -fsS -X POST \
gitea_curl_with_token "$token" -fsS -X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$url" >/dev/null
@@ -107,9 +112,8 @@ gitea_issue_reopen_api() {
host=$(get_remote_host) || return 1
token=$(get_gitea_token "$host") || return 1
url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}"
curl -fsS -X PATCH \
gitea_curl_with_token "$token" -fsS -X PATCH \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d '{"state":"open"}' \
"$url" >/dev/null
@@ -156,3 +160,6 @@ else
echo "Error: Unknown platform"
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -7,6 +7,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2,
# distinct from provider, credential, and verification failures (exit 1).
@@ -36,12 +42,12 @@ gitea_issue_view_api() {
}
url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}"
local -a curl_args=(-fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}")
local -a curl_args=(-fsS -H "User-Agent: curl/8")
if ! command -v python3 >/dev/null 2>&1; then
# No renderer: raw JSON is all this path can give. Comments are a
# second resource, so fetch them too rather than only the count.
curl "${curl_args[@]}" "$url"
curl "${curl_args[@]}" "${url}/comments"
gitea_curl_with_token "$token" "${curl_args[@]}" "$url"
gitea_curl_with_token "$token" "${curl_args[@]}" "${url}/comments"
return
fi
# Render issue + comments as text (#1357 F2). The old fallback dumped the
@@ -49,10 +55,10 @@ gitea_issue_view_api() {
# invisible on this path and the wrapper could never show what
# `tea issues --comments` shows.
{
curl "${curl_args[@]}" "$url"
gitea_curl_with_token "$token" "${curl_args[@]}" "$url"
echo
echo "__MOSAIC_COMMENTS__"
curl "${curl_args[@]}" "${url}/comments"
gitea_curl_with_token "$token" "${curl_args[@]}" "${url}/comments"
} | python3 -c '
import json, sys
raw = sys.stdin.read()
@@ -145,3 +151,6 @@ else
echo "Error: Unknown platform"
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Validate and resolve the optional v3 merge declaration. No provider writes."""
import argparse
import json
import pathlib
import sys
METHODS = {'squash', 'merge', 'rebase', 'rebase-merge', 'fast-forward-only'}
SHAPES = {'task', 'promotion', 'absorb'}
def policy(doc):
if 'merge' not in doc:
return {'allowed_methods':['squash'], 'default_method':'squash', 'shapes':{}}
if doc.get('schema_version') != 3:
raise ValueError('merge requires schema_version 3')
p = doc['merge']
if not isinstance(p, dict) or set(p) - {'allowed_methods','default_method','shapes'}:
raise ValueError('merge must be an object containing only allowed_methods, default_method and shapes')
allowed = p.get('allowed_methods')
if not isinstance(allowed, list) or not allowed or any(not isinstance(m,str) or m not in METHODS for m in allowed) or len(allowed) != len(set(allowed)):
raise ValueError('merge.allowed_methods must be a non-empty unique subset of supported methods')
if not isinstance(p.get('default_method'), str) or p['default_method'] not in allowed:
raise ValueError('merge.default_method must be in allowed_methods')
shapes = p.get('shapes', {})
if not isinstance(shapes, dict) or any(k not in SHAPES or not isinstance(v,str) or v not in allowed for k,v in shapes.items()):
raise ValueError('merge.shapes accepts task, promotion and absorb, with methods in allowed_methods')
return dict(p, shapes=shapes)
def resolve(doc, head, base, request):
p = policy(doc)
if not head or not base or head == base:
return 3, {'error':'POLICY-REFUSED: head/base must identify a non-self transition'}
trunk, release = doc.get('integration_trunk'), doc.get('release_branch')
shape = 'promotion' if head == trunk and base == release and trunk != release else 'absorb' if head == release and base == trunk and trunk != release else 'task'
mapped = p['shapes'].get(shape)
selected = request or mapped or p['default_method']
if selected not in p['allowed_methods'] or mapped and selected != mapped:
return 3, {'error':'POLICY-REFUSED: requested method conflicts with repository merge policy', 'shape':shape}
if shape == 'absorb' and not mapped:
return 3, {'error':'POLICY-REFUSED: absorb transition requires an explicit shape mapping', 'shape':shape}
if base != trunk and not (shape == 'promotion' and doc.get('flow') == 'trunk-release'):
return 3, {'error':'POLICY-REFUSED: undeclared target transition', 'shape':shape}
return 0, dict(method=selected, shape=shape, allowed_methods=p['allowed_methods'])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('file')
parser.add_argument('--validate', action='store_true')
parser.add_argument('--head')
parser.add_argument('--base')
parser.add_argument('--method')
a = parser.parse_args()
try:
doc = json.loads(pathlib.Path(a.file).read_text())
if not isinstance(doc,dict):
raise ValueError('declaration must be an object')
p = policy(doc)
code, value = (0,p) if a.validate else resolve(doc,a.head,a.base,a.method)
except (OSError, ValueError, TypeError) as e:
code, value = 6, {'error':'VALIDATION_ERROR merge: ' + str(e)}
print(json.dumps(value, sort_keys=True))
return code
if __name__ == '__main__':
sys.exit(main())
@@ -80,6 +80,12 @@ while [[ $# -gt 0 ]]; do
esac
done
# Creation usage errors must stop before provider detection or credentials.
# Listing does not require a title.
if [[ "$LIST_ONLY" != true && -z "$TITLE" ]]; then
usage_error "Title is required (-t) for creating milestones"
fi
PLATFORM=$(detect_platform)
if [[ "$LIST_ONLY" == true ]]; then
@@ -106,10 +112,6 @@ if [[ "$LIST_ONLY" == true ]]; then
exit 0
fi
if [[ -z "$TITLE" ]]; then
usage_error "Title is required (-t) for creating milestones"
fi
case "$PLATFORM" in
github)
# GitHub uses the API for milestone creation
@@ -221,7 +221,7 @@ fi
repo_decl_warn_absent_irreversible "worktree branch-creation"
;;
valid)
if [ "$DECL_SCHEMA" = 2 ] && [ "$DECL_POLICY" = "orchestrator-precreated" ]; then
if [ "$DECL_SCHEMA" -ge 2 ] && [ "$DECL_POLICY" = "orchestrator-precreated" ]; then
echo "repo-decl: worktree_policy=orchestrator-precreated (TRANSITIONAL, spec 4.5): tasking pre-creates worktrees; creating one directly is the interim path until the J3/#1174 amendment unblocks the wrapper consumer." >&2
fi
if [ -n "${MOSAIC_HOST_ROOT:-}" ] && [ -n "$DECL_WT_ROOT" ]; then
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" auto-merge "$@"
@@ -1,372 +1,5 @@
#!/bin/bash
# pr-ci-wait.sh - Wait for PR CI status to reach terminal state (GitHub/Gitea)
# Usage: pr-ci-wait.sh -n <pr_number> [-r owner/repo] [-t timeout_sec] [-i interval_sec]
#!/usr/bin/env bash
# CI wait consumes normalized checks; default exits preserve 0/1/2/124.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
PR_NUMBER=""
TIMEOUT_SEC=1800
INTERVAL_SEC=15
REPO_OVERRIDE=""
HOST_OVERRIDE=""
usage() {
cat <<EOF
Usage: $(basename "$0") -n <pr_number> [-t timeout_sec] [-i interval_sec]
Options:
-n, --number NUMBER PR number (required)
-r, --repo OWNER/REPO Repository slug (default: infer from git origin)
--host HOST Gitea host for --repo API calls (or set GITEA_HOST/GITEA_URL)
-t, --timeout SECONDS Max wait time in seconds (default: 1800)
-i, --interval SECONDS Poll interval in seconds (default: 15)
-h, --help Show this help
Examples:
$(basename "$0") -n 643
$(basename "$0") -n 643 --repo ddk/ai-bma
$(basename "$0") -n 643 -t 900 -i 10
EOF
}
# get_remote_host and get_gitea_token are provided by detect-platform.sh
extract_state_from_status_json() {
# Capture piped JSON BEFORE invoking `python3 - <<PY`. The heredoc binds
# stdin to the Python program text — so json.load(sys.stdin) inside would
# try to re-read stdin after `-` already consumed it for the program,
# yielding EOF and returning "unknown" every time. Pass payload via env.
local payload
payload=$(cat)
PR_CI_STATUS_JSON="$payload" python3 - <<'PY'
import json
import os
import sys
try:
payload = json.loads(os.environ.get("PR_CI_STATUS_JSON", ""))
except Exception:
print("unknown")
raise SystemExit(0)
state = (payload.get("state") or "").lower()
if state in {"success", "pending", "failure", "error"}:
print(state)
raise SystemExit(0)
statuses = payload.get("statuses") or []
values = []
for item in statuses:
if not isinstance(item, dict):
continue
value = (item.get("status") or item.get("state") or "").lower()
if value:
values.append(value)
if any(v in {"failure", "error"} for v in values):
print("failure")
elif values and all(v == "success" for v in values):
print("success")
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
print("pending")
elif not values and not state:
# No pipeline/status of any kind reported for this commit. Distinct from
# "unknown" (an ambiguous/unrecognized status that should keep polling):
# this signals a repo/commit that simply has no CI configured.
print("no-status")
else:
print("unknown")
PY
}
print_status_summary() {
# Same stdin-collision fix as extract_state_from_status_json above.
local payload
payload=$(cat)
PR_CI_STATUS_JSON="$payload" python3 - <<'PY'
import json
import os
import sys
try:
payload = json.loads(os.environ.get("PR_CI_STATUS_JSON", ""))
except Exception:
print("[pr-ci-wait] status payload unavailable")
raise SystemExit(0)
statuses = payload.get("statuses") or []
if not statuses:
print("[pr-ci-wait] no status contexts reported yet")
raise SystemExit(0)
for item in statuses:
if not isinstance(item, dict):
continue
name = item.get("context") or item.get("name") or "unknown-context"
state = item.get("status") or item.get("state") or "unknown-state"
target = item.get("target_url") or item.get("url") or ""
if target:
print(f"[pr-ci-wait] {name}: {state} ({target})")
else:
print(f"[pr-ci-wait] {name}: {state}")
PY
}
github_get_pr_head_sha() {
gh pr view "$PR_NUMBER" --repo "$OWNER/$REPO" --json headRefOid --jq '.headRefOid'
}
github_get_commit_status_json() {
local owner="$1"
local repo="$2"
local sha="$3"
gh api "repos/${owner}/${repo}/commits/${sha}/status"
}
gitea_get_pr_head_sha() {
local host="$1"
local repo="$2"
local token="$3"
local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}"
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
import json, sys
data = json.load(sys.stdin)
print((data.get("head") or {}).get("sha", ""))
'
}
gitea_get_commit_status_json() {
local host="$1"
local repo="$2"
local token="$3"
local sha="$4"
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
}
gitea_get_default_branch() {
local host="$1"
local repo="$2"
local token="$3"
local url="https://${host}/api/v1/repos/${repo}"
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
import json, sys
print((json.load(sys.stdin) or {}).get("default_branch", ""))
'
}
github_get_default_branch() {
gh api "repos/${OWNER}/${REPO}" --jq '.default_branch'
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--number)
PR_NUMBER="$2"
shift 2
;;
-r|--repo)
REPO_OVERRIDE="$2"
shift 2
;;
--host)
HOST_OVERRIDE="$2"
shift 2
;;
-t|--timeout)
TIMEOUT_SEC="$2"
shift 2
;;
-i|--interval)
INTERVAL_SEC="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$PR_NUMBER" ]]; then
echo "Error: PR number is required (-n)." >&2
usage >&2
exit 1
fi
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then
echo "Error: timeout and interval must be integer seconds." >&2
exit 1
fi
if [[ -n "$REPO_OVERRIDE" ]]; then
REPO_INFO="$REPO_OVERRIDE"
PLATFORM=$(detect_platform 2>/dev/null || echo gitea)
else
detect_platform > /dev/null
REPO_INFO=$(get_repo_info)
fi
if [[ -z "$REPO_INFO" || "$REPO_INFO" == error:* || "$REPO_INFO" != */* ]]; then
echo "Error: Could not determine repository from git origin. Run from a repo or pass --repo owner/repo." >&2
exit 1
fi
OWNER=${REPO_INFO%%/*}
REPO=${REPO_INFO##*/}
START_TS=$(date +%s)
DEADLINE_TS=$((START_TS + TIMEOUT_SEC))
if [[ "$PLATFORM" == "github" ]]; then
if ! command -v gh >/dev/null 2>&1; then
echo "Error: gh CLI is required for GitHub CI status polling." >&2
exit 1
fi
HEAD_SHA=$(github_get_pr_head_sha)
if [[ -z "$HEAD_SHA" ]]; then
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
exit 1
fi
echo "[pr-ci-wait] Platform=github PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
elif [[ "$PLATFORM" == "gitea" ]]; then
if [[ -n "$HOST_OVERRIDE" ]]; then
HOST="$HOST_OVERRIDE"
elif [[ -n "$REPO_OVERRIDE" ]]; then
HOST=$(get_gitea_api_host_for_repo_override) || {
echo "Error: Gitea host is required with --repo. Pass --host or set GITEA_HOST/GITEA_URL." >&2
exit 1
}
else
HOST=$(get_remote_host) || {
echo "Error: Could not determine Gitea host from git origin." >&2
exit 1
}
fi
TOKEN=$(get_gitea_token "$HOST") || {
echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2
exit 1
}
HEAD_SHA=$(gitea_get_pr_head_sha "$HOST" "$OWNER/$REPO" "$TOKEN")
if [[ -z "$HEAD_SHA" ]]; then
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
exit 1
fi
echo "[pr-ci-wait] Platform=gitea host=${HOST} repo=${OWNER}/${REPO} PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
else
echo "Error: Unsupported platform '${PLATFORM}'." >&2
exit 1
fi
# No-CI determination is TWO-TIER (primary: CI history; secondary: empty-poll streak).
#
# PRIMARY — "does this repo run CI at all?" Probed once, up front, from the DEFAULT
# BRANCH's commit status. A repo whose default branch carries CI statuses
# demonstrably runs CI, so an EMPTY status on the PR head means the pipeline simply
# has not registered YET (webhook/queue lag) — NOT that the repo is CI-less. In that
# case we must NEVER fast-green; we keep polling until the pipeline registers or the
# timeout fires (both safe). This closes the webhook-lag false-green: a slow-to-
# register pipeline feeding a merge gate can no longer be mistaken for "no CI".
#
# SECONDARY — the empty-poll streak below applies ONLY to genuinely CI-less repos
# (default branch also has no CI history, e.g. device-imaging class), where burning
# the full timeout would be pure waste. There, NO_CI_MAX empty polls => fast-exit 0.
#
# Probe failure is treated conservatively as REPO_HAS_CI=1 (assume CI present): we
# would rather wait-then-timeout than risk a false-green, per the merge-gate priority.
REPO_HAS_CI=1
detect_repo_ci() {
local def_branch def_status
# Every early exit returns 0: a probe miss must leave the conservative
# REPO_HAS_CI=1 default in place, never abort the caller under `set -e`.
if [[ "$PLATFORM" == "github" ]]; then
def_branch=$(github_get_default_branch 2>/dev/null) || {
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
[[ -n "$def_branch" ]] || return 0
def_status=$(github_get_commit_status_json "$OWNER" "$REPO" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
else
def_branch=$(gitea_get_default_branch "$HOST" "$OWNER/$REPO" "$TOKEN" 2>/dev/null) || {
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
[[ -n "$def_branch" ]] || return 0
def_status=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
fi
if [[ "$def_status" == "no-status" || -z "$def_status" ]]; then
REPO_HAS_CI=0
echo "[pr-ci-wait] default branch '${def_branch}' has no CI status history — treating repo as CI-less (empty-poll fast-exit enabled)."
else
REPO_HAS_CI=1
echo "[pr-ci-wait] default branch '${def_branch}' has CI history (state=${def_status}) — repo runs CI; empty status on PR head => awaiting registration, will not fast-green."
fi
}
detect_repo_ci || true
NO_CI_STREAK=0
NO_CI_MAX=3
while true; do
NOW_TS=$(date +%s)
if (( NOW_TS > DEADLINE_TS )); then
echo "Error: Timed out waiting for CI status on PR #$PR_NUMBER after ${TIMEOUT_SEC}s." >&2
exit 124
fi
if [[ "$PLATFORM" == "github" ]]; then
STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA")
else
STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$HEAD_SHA")
fi
STATE=$(printf '%s' "$STATUS_JSON" | extract_state_from_status_json)
echo "[pr-ci-wait] state=${STATE} pr=#${PR_NUMBER} sha=${HEAD_SHA}"
case "$STATE" in
success)
printf '%s' "$STATUS_JSON" | print_status_summary
echo "[pr-ci-wait] CI is green for PR #$PR_NUMBER."
exit 0
;;
failure|error)
printf '%s' "$STATUS_JSON" | print_status_summary
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
exit 1
;;
no-status)
if [[ "$REPO_HAS_CI" == "1" ]]; then
# PRIMARY tier: repo demonstrably runs CI but this commit's pipeline
# has not registered yet (webhook/queue lag). Do NOT fast-green — keep
# polling until it registers or the timeout fires. Reset the streak so
# a later genuine CI-less misread can't accumulate across this state.
NO_CI_STREAK=0
echo "[pr-ci-wait] empty status on PR head but repo runs CI — awaiting pipeline registration (webhook lag), not fast-greening."
else
# SECONDARY tier: genuinely CI-less repo (default branch has no CI
# history either). Empty polls => fast-exit green after NO_CI_MAX.
NO_CI_STREAK=$((NO_CI_STREAK + 1))
if (( NO_CI_STREAK >= NO_CI_MAX )); then
echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls, default branch also CI-less); treating as green."
exit 0
fi
fi
sleep "$INTERVAL_SEC"
;;
pending|unknown)
# A pipeline exists but hasn't reached a terminal state (or is
# transiently ambiguous) — keep waiting, and reset the no-CI streak
# since this commit is not in the "no CI at all" condition.
NO_CI_STREAK=0
sleep "$INTERVAL_SEC"
;;
*)
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
NO_CI_STREAK=0
sleep "$INTERVAL_SEC"
;;
esac
done
exec python3 -I "$SCRIPT_DIR/forge-api.py" ci-wait "$@"
@@ -7,6 +7,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Default values
TITLE=""
BODY=""
@@ -33,9 +39,8 @@ gitea_default_branch() {
# `curl | python` reports only python's status, so an HTTP failure that
# still emits parseable JSON would masquerade as success. curl's own
# exit status is authoritative here.
if ! body=$(curl -fsS \
if ! body=$(gitea_curl_with_token "$token" -fsS \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
"$url" 2>/dev/null); then
return 1
fi
@@ -105,9 +110,8 @@ PY
)
url="https://${host}/api/v1/repos/${repo}/pulls"
curl -fsS -X POST \
gitea_curl_with_token "$token" -fsS -X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$url"
@@ -243,9 +247,13 @@ if [ -f "$SCRIPT_DIR/repo-decl.sh" ]; then
repo_decl_load
else
DECL_STATE=absent; DECL_SCHEMA=""
# Compatibility callbacks are invoked by declaration checks below.
# shellcheck disable=SC2317
repo_decl_warn() { printf 'repo-decl: %s\n' "$*" >&2; }
repo_decl_report_invalid() { :; }
# shellcheck disable=SC2317
repo_decl_warn_absent_reversible() { :; }
# shellcheck disable=SC2317
repo_decl_warn_absent_irreversible() { :; }
repo_decl_remote_matches() { return 0; }
repo_decl_check_transition() { return 2; }
@@ -254,10 +262,10 @@ EFFECTIVE_BASE="$BASE_BRANCH"
case "$DECL_STATE" in
invalid) repo_decl_report_invalid ;;
esac
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" != 2 ]]; then
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" -lt 2 ]]; then
repo_decl_warn "declaration is v$DECL_SCHEMA — carries no consumable flow/trunk fields; legacy behavior"
fi
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" == 2 ]]; then
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" -ge 2 ]]; then
# Write path: a normalized-remote mismatch refuses (spec 5.3).
if ! repo_decl_remote_matches; then
echo "Error: origin remote does not match the declared canonical_remote (spec 5.3, write path) — refusing to create a PR against the wrong forge. Fix the origin remote or the declaration." >&2
@@ -358,3 +366,6 @@ case "$PLATFORM" in
exit 1
;;
esac
)
mosaic_private_forge_main "$@"
+10 -1
View File
@@ -7,6 +7,12 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
PR_NUMBER=""
OUTPUT_FILE=""
@@ -106,7 +112,7 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
GITEA_API_TOKEN=$(get_gitea_token "$HOST" || true)
if [[ -n "$GITEA_API_TOKEN" ]]; then
DIFF_CONTENT=$(curl -sS -H "User-Agent: curl/8" -H "Authorization: token $GITEA_API_TOKEN" "$DIFF_URL")
DIFF_CONTENT=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -H "User-Agent: curl/8" "$DIFF_URL")
else
DIFF_CONTENT=$(curl -sS -H "User-Agent: curl/8" "$DIFF_URL")
fi
@@ -120,3 +126,6 @@ else
echo "Error: Unknown platform" >&2
exit 1
fi
)
mosaic_private_forge_main "$@"
+17 -13
View File
@@ -8,6 +8,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
PR_NUMBER=""
TITLE=""
BODY=""
@@ -17,16 +23,16 @@ DRAFT_MODE=""
LOGIN_OVERRIDE=""
REPO_OVERRIDE=""
HOST_OVERRIDE=""
AUTH_CONFIG=""
cleanup() {
[[ -z "$AUTH_CONFIG" ]] || rm -f -- "$AUTH_CONFIG"
}
# Signal and EXIT traps invoke these callbacks.
# shellcheck disable=SC2317
cleanup() { :; }
# shellcheck disable=SC2317
terminate() {
local signal="$1"
trap - "$signal"
cleanup
kill -s "$signal" "$$"
kill -s "$signal" "$BASHPID"
}
trap cleanup EXIT
trap 'terminate HUP' HUP
@@ -166,18 +172,13 @@ case "$PLATFORM" in
echo "Error: login '$GITEA_LOGIN_NAME' is not configured for target host '$HOST'" >&2
exit 1
}
AUTH_CONFIG=$(gitea_write_auth_config "$TOKEN") || {
echo "Error: could not stage private Gitea authentication" >&2
exit 1
}
unset TOKEN
API_BASE="https://${HOST}/api/v1"
# Resolve identity through the SAME private curl config used for the
# mutation. Tea login names are globally scoped and can be duplicated
# across hosts; a separate `tea api --login NAME` could validate another
# credential than this host-bound token.
AUTHENTICATED_USER=$(curl -fsS --config "$AUTH_CONFIG" -H "User-Agent: mosaic-pr-edit" "$API_BASE/user" \
AUTHENTICATED_USER=$(gitea_curl_with_token "$TOKEN" -fsS -H "User-Agent: mosaic-pr-edit" "$API_BASE/user" \
| python3 -c 'import json,sys; value=json.load(sys.stdin).get("login"); print(value) if isinstance(value,str) and value else sys.exit(1)') || {
echo "Error: could not authenticate the host-bound credential for '$GITEA_LOGIN_NAME'" >&2
exit 1
@@ -188,7 +189,7 @@ case "$PLATFORM" in
}
REPO_API="$API_BASE/repos/${REPO_SLUG}"
curl -fsS --config "$AUTH_CONFIG" -H "User-Agent: mosaic-pr-edit" "$REPO_API" >/dev/null || {
gitea_curl_with_token "$TOKEN" -fsS -H "User-Agent: mosaic-pr-edit" "$REPO_API" >/dev/null || {
echo "Error: target repository preflight failed for https://${HOST}/${REPO_SLUG}" >&2
exit 1
}
@@ -204,10 +205,13 @@ if os.environ["DRAFT_MODE"]: payload["draft"] = os.environ["DRAFT_MODE"] == "dra
print(json.dumps(payload))
PY
)
curl -fsS --config "$AUTH_CONFIG" -X PATCH \
gitea_curl_with_token "$TOKEN" -fsS -X PATCH \
-H "User-Agent: mosaic-pr-edit" -H "Content-Type: application/json" \
-d "$PAYLOAD" "$REPO_API/pulls/${PR_NUMBER}"
echo "Updated Gitea pull request #$PR_NUMBER as '$AUTHENTICATED_USER'" >&2
;;
*) echo "Error: Could not detect git platform" >&2; exit 1 ;;
esac
)
mosaic_private_forge_main "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" merge-queue "$@"
@@ -8,6 +8,28 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=packages/mosaic/framework/tools/git/detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Versioned JSON mode and explicit non-squash methods use the normalized adapter.
for ((arg_i=1; arg_i<=$#; arg_i++)); do
if [[ "${!arg_i}" == --json ]]; then
exec python3 -I "$SCRIPT_DIR/forge-api.py" merge "$@"
fi
done
# A declared merge policy also applies to callers that have not adopted --json.
declaration_root=$(git rev-parse --show-toplevel 2>/dev/null) || declaration_root=""
if [[ -n "$declaration_root" && -f "$declaration_root/.mosaic/repo.json" ]] && python3 -I -c 'import json,sys
try:
d=json.load(open(sys.argv[1])); sys.exit(0 if isinstance(d,dict) and "merge" in d else 1)
except (ValueError,OSError):
sys.exit(0)' "$declaration_root/.mosaic/repo.json"; then
exec python3 -I "$SCRIPT_DIR/forge-api.py" merge "$@"
fi
# Default values
PR_NUMBER=""
MERGE_METHOD="squash"
@@ -26,7 +48,7 @@ Merge a pull request on the current repository (Gitea or GitHub).
Options:
-n, --number NUMBER PR number to merge (required)
-m, --method METHOD Merge method: squash only (default: squash)
-m, --method METHOD Merge method (default: declared policy, legacy squash); --json uses verified v1 contract
-d, --delete-branch Delete the head branch after merge
--dry-run Run metadata/login preflight without merging
--expect-head SHA Refuse unless the PR head matches this full commit SHA
@@ -121,8 +143,8 @@ if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
fi
if [[ "$MERGE_METHOD" != "squash" ]]; then
echo "Error: Mosaic policy enforces squash merge only. Received '$MERGE_METHOD'." >&2
exit 1
echo "POLICY-REFUSED: absent merge policy permits squash only." >&2
exit 3
fi
if [[ -n "$EXPECT_HEAD" && ! "$EXPECT_HEAD" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: --expect-head must be a full 40-character hexadecimal commit SHA." >&2
@@ -159,8 +181,11 @@ if [ -f "$SCRIPT_DIR/repo-decl.sh" ]; then
repo_decl_load
else
DECL_STATE=absent; DECL_SCHEMA=""
# Compatibility callbacks are invoked by declaration checks below.
# shellcheck disable=SC2317
repo_decl_warn() { printf 'repo-decl: %s\n' "$*" >&2; }
repo_decl_report_invalid() { :; }
# shellcheck disable=SC2317
repo_decl_warn_absent_reversible() { :; }
repo_decl_warn_absent_irreversible() { :; }
repo_decl_remote_matches() { return 0; }
@@ -169,7 +194,7 @@ fi
if [[ "$DECL_STATE" == invalid ]]; then
repo_decl_report_invalid
fi
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" == 2 ]]; then
if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" -ge 2 ]]; then
if ! repo_decl_remote_matches; then
echo "Error: origin remote does not match the declared canonical_remote (spec 5.3, write path) — refusing to merge against the wrong forge. Fix the origin remote or the declaration." >&2
exit 1
@@ -298,6 +323,8 @@ print(ascii(message))
PY
}
# EXIT invokes this cleanup callback.
# shellcheck disable=SC2317
cleanup_merge_temp_dirs() {
local path
for path in "${MERGE_TEMP_DIRS[@]}"; do
@@ -716,3 +743,6 @@ case "$PLATFORM" in
esac
echo "PR #$PR_NUMBER merged successfully"
)
mosaic_private_forge_main "$@"
@@ -8,6 +8,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=packages/mosaic/framework/tools/git/detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
PR_NUMBER=""
OUTPUT_FILE=""
@@ -64,7 +70,7 @@ write_metadata() {
curl_gitea_pull() {
local api_url="$1"
local token basic_auth raw_code body_file http_code
local token raw_code body_file http_code
body_file=$(mktemp)
# shellcheck disable=SC2329 # Invoked by the RETURN trap below.
@@ -76,30 +82,16 @@ curl_gitea_pull() {
}
trap cleanup_gitea_pull_body RETURN
token=$(get_gitea_token "$HOST" || true)
if [[ -n "$token" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" -H "Authorization: token $token" "$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
cat "$body_file" || return $?
return 0
fi
http_code="$raw_code"
fi
basic_auth=$(get_gitea_basic_auth "$HOST" || true)
if [[ -n "$basic_auth" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -u "$basic_auth" -H "User-Agent: curl/8" "$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
cat "$body_file" || return $?
return 0
fi
http_code="$raw_code"
fi
if [[ -z "${http_code:-}" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" "$api_url" || true)
http_code="$raw_code"
token=$(get_gitea_token "$HOST") || {
echo 'Error: identity-bound metadata credential unavailable.' >&2
return 1
}
raw_code=$(gitea_curl_with_token "$token" -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" "$api_url") || return $?
if [[ "$raw_code" =~ ^2 ]]; then
cat "$body_file" || return $?
return 0
fi
http_code="$raw_code"
python3 - "$http_code" "$body_file" <<'PY' >&2
import json
@@ -262,3 +254,6 @@ else
echo "Error: Unknown platform" >&2
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" review-comments "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" review-reply "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" review-resolve "$@"
@@ -35,6 +35,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=packages/mosaic/framework/tools/git/detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
# Credential resolution and all consumers execute inside this private boundary.
# Never restore xtrace while credentials remain in scope.
mosaic_private_forge_main() (
set +x
# Parse arguments
PR_NUMBER=""
ACTION=""
@@ -223,7 +229,7 @@ PY
# Args: $1 = PR number, $2 = comment body, $3 = acting identity login.
gitea_create_comment_verified() {
local pr_number="$1" comment_body="$2" acting_login="$3"
local payload write_file readback_file auth_config write_status readback_status created_id
local payload write_file readback_file write_status readback_status created_id
payload=$(COMMENT_BODY="$comment_body" python3 -c '
import json
@@ -233,16 +239,10 @@ print(json.dumps({"body": os.environ["COMMENT_BODY"]}))
')
write_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-write.XXXXXX")
readback_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-getid.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$write_file" "$readback_file"
echo "Error: could not stage Gitea credential for comment write" >&2
return 1
}
trap 'rm -f "$write_file" "$readback_file" "$auth_config"' RETURN
trap 'rm -f "$write_file" "$readback_file"' RETURN
if ! write_status=$(curl -sS -o "$write_file" -w '%{http_code}' \
if ! write_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$write_file" -w '%{http_code}' \
-X POST \
--config "$auth_config" \
-H 'User-Agent: mosaic-pr-review' \
-H 'Content-Type: application/json' \
-d "$payload" \
@@ -272,8 +272,7 @@ print(created_id)
PY
) || return 1
if ! readback_status=$(curl -sS -o "$readback_file" -w '%{http_code}' \
--config "$auth_config" \
if ! readback_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$readback_file" -w '%{http_code}' \
-H 'User-Agent: mosaic-pr-review' \
"$GITEA_API_BASE/issues/comments/$created_id"); then
echo "Error: Gitea comment read-back transport failed" >&2
@@ -411,7 +410,7 @@ PY
# residue.
gitea_resolve_api_for_login() {
local effective_login="$1" override_explicit="${2:-}" host configured_url repo
local preflight_auth_config preflight_status
local preflight_status
if [[ -n "$HOST_OVERRIDE" ]]; then
host="$HOST_OVERRIDE"
@@ -459,15 +458,9 @@ gitea_resolve_api_for_login() {
GITEA_WEB_BASE="${configured_url%/}"
if [[ -n "$REPO_OVERRIDE" ]]; then
preflight_auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
echo "Error: could not stage Gitea credential for --repo preflight" >&2
return 1
}
preflight_status=$(curl -sS -o /dev/null -w '%{http_code}' \
--config "$preflight_auth_config" \
preflight_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o /dev/null -w '%{http_code}' \
-H 'User-Agent: mosaic-pr-review' \
"$GITEA_API_BASE") || preflight_status="000"
rm -f "$preflight_auth_config"
if [[ "$preflight_status" != "200" ]]; then
echo "Error: repo '$repo' not reachable at $configured_url (HTTP $preflight_status) — wrong host? pass -H/--host <gitea-host> or cd into the target checkout" >&2
return 1
@@ -481,18 +474,12 @@ gitea_resolve_api_for_login() {
# concurrent review from a DIFFERENT identity cannot satisfy verification.
# Prints the login on success.
gitea_authenticated_login() {
local response_file auth_config status
local response_file status
response_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-whoami.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$response_file"
echo "Error: could not stage Gitea credential for identity read" >&2
return 1
}
trap 'rm -f "$response_file" "$auth_config"' RETURN
trap 'rm -f "$response_file"' RETURN
if ! status=$(curl -sS -o "$response_file" -w '%{http_code}' \
--config "$auth_config" \
if ! status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$response_file" -w '%{http_code}' \
-H 'User-Agent: mosaic-pr-review' \
"$GITEA_API_ROOT/user"); then
echo "Error: Gitea authenticated-identity read transport failed" >&2
@@ -521,15 +508,14 @@ PY
}
# GET /pulls/{n} into a caller-owned response file and print its head commit
# SHA. This core sets NO RETURN trap and reuses a caller-provided auth config +
# SHA. This core sets NO RETURN trap and reuses a caller-provided
# response file, so it is safe to call from INSIDE another trapped function
# (the post-verify re-read below) without clobbering that function's cleanup
# trap. $1 = PR number, $2 = response file, $3 = curl auth config file.
# trap. $1 = PR number, $2 = response file.
gitea_read_pr_head_into() {
local pr_number="$1" pr_file="$2" auth_config="$3" status
local pr_number="$1" pr_file="$2" status
if ! status=$(curl -sS -o "$pr_file" -w '%{http_code}' \
--config "$auth_config" \
if ! status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$pr_file" -w '%{http_code}' \
-H 'User-Agent: mosaic-pr-review' \
"$GITEA_API_BASE/pulls/$pr_number"); then
echo "Error: Gitea PR head read transport failed" >&2
@@ -561,17 +547,12 @@ PY
# stale review left over from an earlier push cannot be mistaken for this one.
# Prints the head SHA on success.
gitea_pr_head_sha() {
local pr_number="$1" pr_file auth_config
local pr_number="$1" pr_file
pr_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-head.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$pr_file"
echo "Error: could not stage Gitea credential for PR head read" >&2
return 1
}
trap 'rm -f "$pr_file" "$auth_config"' RETURN
trap 'rm -f "$pr_file"' RETURN
gitea_read_pr_head_into "$pr_number" "$pr_file" "$auth_config"
gitea_read_pr_head_into "$pr_number" "$pr_file"
}
# Submit a review to a Gitea PR via the supported REST API and verify it against
@@ -591,7 +572,7 @@ gitea_pr_head_sha() {
# $5 = PR head sha.
gitea_submit_review_verified() {
local pr_number="$1" event="$2" review_body="$3" acting_login="$4" head_sha="$5"
local payload write_file readback_file recheck_file auth_config
local payload write_file readback_file recheck_file
local write_status readback_status created_id live_head
payload=$(REVIEW_EVENT="$event" REVIEW_BODY="$review_body" REVIEW_COMMIT="$head_sha" python3 -c '
@@ -607,16 +588,10 @@ print(json.dumps({
write_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-submit.XXXXXX")
readback_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-getid.XXXXXX")
recheck_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-recheck.XXXXXX")
auth_config=$(gitea_write_auth_config "$GITEA_API_TOKEN") || {
rm -f "$write_file" "$readback_file" "$recheck_file"
echo "Error: could not stage Gitea credential for review submit" >&2
return 1
}
trap 'rm -f "$write_file" "$readback_file" "$recheck_file" "$auth_config"' RETURN
trap 'rm -f "$write_file" "$readback_file" "$recheck_file"' RETURN
if ! write_status=$(curl -sS -o "$write_file" -w '%{http_code}' \
if ! write_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$write_file" -w '%{http_code}' \
-X POST \
--config "$auth_config" \
-H 'User-Agent: mosaic-pr-review' \
-H 'Content-Type: application/json' \
-d "$payload" \
@@ -647,8 +622,7 @@ print(created_id)
PY
) || return 1
if ! readback_status=$(curl -sS -o "$readback_file" -w '%{http_code}' \
--config "$auth_config" \
if ! readback_status=$(gitea_curl_with_token "$GITEA_API_TOKEN" -sS -o "$readback_file" -w '%{http_code}' \
-H 'User-Agent: mosaic-pr-review' \
"$GITEA_API_BASE/pulls/$pr_number/reviews/$created_id"); then
echo "Error: Gitea review read-back transport failed" >&2
@@ -713,9 +687,9 @@ PY
# live tip carries unreviewed code — yet the wrapper would still report
# success. Re-read the LIVE PR head and require it STILL equals the submitted
# SHA; if it advanced, fail closed (nonzero, no created id emitted, no
# success line). This reuses the submit-scoped auth config + recheck file so
# success line). This reuses the submit-scoped recheck file so
# it neither leaks the token to argv nor clobbers this function's cleanup.
live_head=$(gitea_read_pr_head_into "$pr_number" "$recheck_file" "$auth_config") || {
live_head=$(gitea_read_pr_head_into "$pr_number" "$recheck_file") || {
echo "Error: could not re-read Gitea PR head after review verification" >&2
return 1
}
@@ -856,3 +830,6 @@ else
echo "Error: Unknown platform"
exit 1
fi
)
mosaic_private_forge_main "$@"
@@ -0,0 +1,251 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Forge wrapper envelope v1",
"type": "object",
"required": [
"schema_version",
"operation",
"provider",
"host",
"repo",
"number",
"head_sha",
"data",
"error"
],
"additionalProperties": false,
"properties": {
"schema_version": {
"const": 1
},
"operation": {
"type": "string"
},
"provider": {
"enum": ["github", "gitea", "forgejo", "unknown"]
},
"host": {
"type": ["string", "null"]
},
"repo": {
"type": ["string", "null"]
},
"number": {
"type": ["integer", "null"]
},
"head_sha": {
"type": ["string", "null"]
},
"data": {
"type": ["object", "null"]
},
"error": {
"oneOf": [
{
"type": "null"
},
{
"type": "object",
"required": ["kind", "message", "retry_after_seconds", "write_outcome"],
"additionalProperties": false,
"properties": {
"kind": {
"type": "string"
},
"message": {
"type": "string"
},
"retry_after_seconds": {
"type": ["integer", "null"]
},
"write_outcome": {
"enum": ["none", "confirmed", "unknown"]
}
}
}
]
}
},
"$defs": {
"snapshot": {
"type": "object",
"required": [
"url",
"head_ref",
"base_ref",
"head_repository",
"base_repository",
"head_sha",
"base_sha",
"merge_sha",
"state",
"draft",
"mergeability",
"ready",
"blockers",
"observed_at",
"fingerprint",
"capabilities",
"checks",
"reviews",
"threads",
"auto_merge",
"queue"
],
"properties": {
"url": {
"type": ["string", "null"]
},
"head_ref": {
"type": ["string", "null"]
},
"base_ref": {
"type": ["string", "null"]
},
"head_repository": {
"type": ["string", "null"]
},
"base_repository": {
"type": ["string", "null"]
},
"head_sha": {
"type": ["string", "null"],
"pattern": "^[0-9a-fA-F]{40}$"
},
"base_sha": {
"type": ["string", "null"],
"pattern": "^[0-9a-fA-F]{40}$"
},
"merge_sha": {
"type": ["string", "null"],
"pattern": "^[0-9a-fA-F]{40}$"
},
"state": {
"enum": ["open", "closed", "merged"]
},
"draft": {
"type": ["boolean", "null"]
},
"mergeability": {
"enum": ["clean", "conflict", "unknown"]
},
"ready": {
"type": "boolean"
},
"blockers": {
"type": "array",
"items": {
"type": "string"
}
},
"observed_at": {
"type": "string"
},
"fingerprint": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"capabilities": {
"type": "object"
},
"checks": {
"type": "object",
"required": ["state", "required", "observed", "complete"],
"properties": {
"state": {
"enum": [
"pending",
"success",
"failure",
"cancelled",
"missing",
"not_required",
"unknown"
]
},
"required": {
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"observed": {
"type": "object"
},
"complete": {
"type": "boolean"
}
}
},
"reviews": {
"type": "object",
"required": ["decision", "reviewed_sha", "complete"],
"properties": {
"decision": {
"type": "string"
},
"reviewed_sha": {
"type": ["string", "null"],
"pattern": "^[0-9a-fA-F]{40}$"
},
"complete": {
"type": "boolean"
}
}
},
"threads": {
"type": "object",
"required": ["unresolved", "complete"],
"properties": {
"unresolved": {
"type": ["integer", "null"],
"minimum": 0
},
"complete": {
"type": "boolean"
}
}
},
"auto_merge": {
"type": "object",
"required": ["state"],
"properties": {
"state": {
"enum": ["enabled", "disabled", "unsupported", "unknown"]
}
}
},
"queue": {
"type": "object",
"required": ["state"],
"properties": {
"state": {
"enum": ["queued", "absent", "unsupported", "unknown"]
}
}
}
}
}
},
"allOf": [
{
"if": {
"properties": {
"operation": {
"enum": ["status", "watch-check", "ci-wait"]
},
"error": {
"type": "null"
}
}
},
"then": {
"properties": {
"data": {
"$ref": "#/$defs/snapshot"
}
}
}
}
]
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" status "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 -I "$SCRIPT_DIR/forge-api.py" watch-check "$@"
@@ -73,21 +73,21 @@ repo_decl_load() {
# Valid: extract the consumed fields via the same python stdlib the
# ecosystem already uses. Field-level grammar was the validator's job.
eval "$(python3 - "$DECL_FILE" <<'PY'
import json, sys
eval "$(python3 -I - "$DECL_FILE" <<'PY'
import json, sys, shlex
d = json.load(open(sys.argv[1]))
def q(k):
v = d.get(k, "")
return v if isinstance(v, str) else ""
sv = d.get("schema_version", 1)
print(f"DECL_SCHEMA={sv if isinstance(sv, int) and not isinstance(sv, bool) else 0!r}")
print(f"DECL_TRUNK={q('integration_trunk')!r}")
print(f"DECL_RELEASE={q('release_branch')!r}")
print(f"DECL_FLOW={q('flow')!r}")
print(f"DECL_REMOTE={q('canonical_remote')!r}")
print(f"DECL_POLICY={q('worktree_policy')!r}")
print(f"DECL_WT_ROOT={q('worktree_root')!r}")
print(f"DECL_CLONE={q('canonical_clone')!r}")
print(f"DECL_TRUNK={shlex.quote(q('integration_trunk'))}")
print(f"DECL_RELEASE={shlex.quote(q('release_branch'))}")
print(f"DECL_FLOW={shlex.quote(q('flow'))}")
print(f"DECL_REMOTE={shlex.quote(q('canonical_remote'))}")
print(f"DECL_POLICY={shlex.quote(q('worktree_policy'))}")
print(f"DECL_WT_ROOT={shlex.quote(q('worktree_root'))}")
print(f"DECL_CLONE={shlex.quote(q('canonical_clone'))}")
PY
)" || {
DECL_STATE=invalid
@@ -177,6 +177,14 @@ repo_decl_check_transition() { # $1 head, $2 base
return 1
;;
trunk-release)
if [ "$head" = "$DECL_RELEASE" ] && [ "$base" = "$DECL_TRUNK" ]; then
# Absorb is opt-in through a declared shape mapping, never a CLI exception.
if python3 -I "$REPO_DECL_SCRIPT_DIR/merge-policy.py" "$DECL_FILE" --head "$head" --base "$base" >/dev/null; then
return 0
fi
repo_decl_warn "absorb transition requires explicit merge policy"
return 1
fi
if [ "$base" = "$DECL_TRUNK" ] && [ "$head" != "$DECL_TRUNK" ] && [ "$head" != "$DECL_RELEASE" ]; then
return 0 # feature -> trunk
fi
@@ -196,3 +204,9 @@ repo_decl_check_transition() { # $1 head, $2 base
;;
esac
}
# Optional merge policy. Caller must first require DECL_STATE=valid and matching origin.
repo_decl_merge_method() {
[ "$DECL_STATE" = valid ] || { repo_decl_warn "merge policy requires a valid declaration"; return 3; }
python3 -I "$REPO_DECL_SCRIPT_DIR/merge-policy.py" "$DECL_FILE" --head "$1" --base "$2" "${@:3}"
}
@@ -17,6 +17,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-branch-absent}"
REPO_DIR="$WORK_DIR/repo"
@@ -37,6 +38,12 @@ git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
# post-fix branch-lookup code paths faithfully.
cat > "$STUB_DIR/curl" <<'SH'
#!/usr/bin/env bash
# Public schema fixture for fail-closed provider detection.
if [[ "$*" == *'/swagger.v1.json'* ]]; then
[[ " $* " != *' --include '* ]] || printf 'HTTP/2 200\r\n\r\n'
printf '%s\n' '{"info":{"title":"Gitea API","version":"1.24.7"}}'
exit 0
fi
set -euo pipefail
has_w=0
@@ -89,6 +96,9 @@ run_ci_queue_wait() {
cd "$REPO_DIR"
export PATH="$STUB_DIR:$PATH"
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
export MOSAIC_BRAIN_HOME="$WORK_DIR/fixture-brain"
export MOSAIC_GIT_IDENTITY=test-identity
python3 -I "$SCRIPT_DIR/fixture-forge-credential.py" "$WORK_DIR/home" "$MOSAIC_BRAIN_HOME" "$MOSAIC_GIT_IDENTITY" git.example.test stub-token
export GITEA_TOKEN="stub-token"
export GITEA_URL="https://git.example.test"
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit/ci-queue-wait.jsonl"
@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# shellcheck disable=SC2030,SC2031
# Each fixture subshell intentionally isolates its credential environment.
# Regression harness for ci-queue-wait.sh's --no-ci-expected assertion:
# the sanctioned merge path for a repository with no CI configured at all.
#
@@ -25,6 +27,7 @@
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-no-ci-expected}"
REPO_DIR="$WORK_DIR/repo"
@@ -41,6 +44,12 @@ git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
# repository-object endpoint (admin state) selected by MOSAIC_STUB_ADMIN_MODE.
cat > "$STUB_DIR/curl" <<'SH'
#!/usr/bin/env bash
# Public schema fixture for fail-closed provider detection.
if [[ "$*" == *'/swagger.v1.json'* ]]; then
[[ " $* " != *' --include '* ]] || printf 'HTTP/2 200\r\n\r\n'
printf '%s\n' '{"info":{"title":"Gitea API","version":"1.24.7"}}'
exit 0
fi
set -euo pipefail
has_w=0
@@ -105,6 +114,9 @@ run_guard() {
cd "$REPO_DIR" || exit
export PATH="$STUB_DIR:$PATH"
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
export MOSAIC_BRAIN_HOME="$WORK_DIR/fixture-brain"
export MOSAIC_GIT_IDENTITY=test-identity
python3 -I "$SCRIPT_DIR/fixture-forge-credential.py" "$WORK_DIR/home" "$MOSAIC_BRAIN_HOME" "$MOSAIC_GIT_IDENTITY" git.example.test stub-token
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit-$name.jsonl"
export MOSAIC_STUB_URL_LOG="$URL_LOG"
export GITEA_TOKEN="stub-token"
@@ -123,6 +135,9 @@ run_guard_no_identity() {
cd "$REPO_DIR" || exit
export PATH="$STUB_DIR:$PATH"
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
export MOSAIC_BRAIN_HOME="$WORK_DIR/fixture-brain"
export MOSAIC_GIT_IDENTITY=test-identity
python3 -I "$SCRIPT_DIR/fixture-forge-credential.py" "$WORK_DIR/home" "$MOSAIC_BRAIN_HOME" "$MOSAIC_GIT_IDENTITY" git.example.test stub-token
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit-$name.jsonl"
export MOSAIC_STUB_URL_LOG="$URL_LOG"
export GITEA_TOKEN="stub-token"
@@ -303,7 +318,7 @@ out_h=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=admin run_guard
rc_h=$?
set -u
if expect_rc h 78 "$rc_h"; then
expect_text h "ASSERTION_UNATTRIBUTABLE state=no-status purpose=merge asserted-by=unknown reason=no-ci-expected branch=main" "$out_h"
expect_text h "ASSERTION_UNATTRIBUTABLE state=unknown purpose=merge asserted-by=unknown reason=no-ci-expected branch=main" "$out_h"
expect_text h "queue-clear" "$out_h" absent
if ! grep -q '"outcome":"ASSERTION_UNATTRIBUTABLE"' "$WORK_DIR/audit-h.jsonl" 2>/dev/null; then
echo "FAIL h: expected an ASSERTION_UNATTRIBUTABLE JSONL audit record" >&2
@@ -32,6 +32,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-no-status}"
REPO_DIR="$WORK_DIR/repo"
@@ -48,6 +49,12 @@ git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
# is selected by MOSAIC_STUB_STATUS_MODE.
cat > "$STUB_DIR/curl" <<'SH'
#!/usr/bin/env bash
# Public schema fixture for fail-closed provider detection.
if [[ "$*" == *'/swagger.v1.json'* ]]; then
[[ " $* " != *' --include '* ]] || printf 'HTTP/2 200\r\n\r\n'
printf '%s\n' '{"info":{"title":"Gitea API","version":"1.24.7"}}'
exit 0
fi
set -euo pipefail
has_w=0
@@ -103,6 +110,9 @@ run_ci_queue_wait() {
cd "$REPO_DIR"
export PATH="$STUB_DIR:$PATH"
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
export MOSAIC_BRAIN_HOME="$WORK_DIR/fixture-brain"
export MOSAIC_GIT_IDENTITY=test-identity
python3 -I "$SCRIPT_DIR/fixture-forge-credential.py" "$WORK_DIR/home" "$MOSAIC_BRAIN_HOME" "$MOSAIC_GIT_IDENTITY" git.example.test stub-token
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit.jsonl"
export GITEA_TOKEN="stub-token"
export GITEA_URL="https://git.example.test"
@@ -4,6 +4,7 @@
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-tristate}"
REPO_DIR="$WORK_DIR/repo"
@@ -60,6 +61,12 @@ git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
cat > "$STUB_DIR/curl" <<'SH'
#!/usr/bin/env bash
# Public schema fixture for fail-closed provider detection.
if [[ "$*" == *'/swagger.v1.json'* ]]; then
[[ " $* " != *' --include '* ]] || printf 'HTTP/2 200\r\n\r\n'
printf '%s\n' '{"info":{"title":"Gitea API","version":"1.24.7"}}'
exit 0
fi
set -euo pipefail
url=""
@@ -143,6 +150,9 @@ run_guard() {
cd "$REPO_DIR" || exit
export PATH="$STUB_DIR:$PATH"
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
export MOSAIC_BRAIN_HOME="$WORK_DIR/fixture-brain"
export MOSAIC_GIT_IDENTITY=test-identity
python3 -I "$SCRIPT_DIR/fixture-forge-credential.py" "$WORK_DIR/home" "$MOSAIC_BRAIN_HOME" "$MOSAIC_GIT_IDENTITY" git.example.test stub-token
if [[ "$status_mode" == "credential-unresolvable" ]]; then
export HOME="$WORK_DIR/empty-home"
mkdir -p "$HOME"
@@ -20,6 +20,10 @@ fail=0
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/bin"
cp "$HERE/fixture-schema-client.sh" "$TMP/bin/curl"
export PATH="$TMP/bin:$PATH"
run_outside() { # $1=function name -> "rc:sawmessage"
local fn="$1" out rc
out=$( cd "$TMP" && bash -c "set -e; source '$HERE/detect-platform.sh'; $fn" 2>&1 ); rc=$?
@@ -0,0 +1,216 @@
"""Synthetic-only credential, xtrace, and termination controls."""
import importlib.util
import os
import pathlib
import signal
import subprocess
import tempfile
import time
import unittest
HERE=pathlib.Path(__file__).resolve().parent
LEGACY_CURL = '''#!/bin/sh
""":"
exec python3 -I "$0" "$@"
":"""
import json,os,pathlib,sys,urllib.parse
a=sys.argv[1:]; secret=os.environ['TEST_AUTH_SECRET']; url=next((x for x in reversed(a) if x.startswith(('https://','http://'))),'')
u=urllib.parse.urlsplit(url)
if u.scheme!='https' or u.netloc!='forge.example.test' or not (u.path.startswith('/api/v1/') or u.path=='/swagger.v1.json'): sys.exit(98)
method='GET'
for flag in ['-X','--request']:
if flag in a: method=a[a.index(flag)+1]
config=''; anonymous=False
for flag in ['--config','-K']:
if flag in a:
value=a[a.index(flag)+1]; anonymous=value=='-' or value.startswith('/dev/fd/')
config=sys.stdin.read() if value=='-' else pathlib.Path(value).read_text()
observed={'path':u.path,'method':method,'argv_leak':any(secret in x for x in a),'authenticated':config=='header = '+json.dumps('Authorization: token '+secret)+chr(10),'anonymous':anonymous}
with open(os.environ['TEST_AUTH_OBSERVATIONS'],'a') as f: f.write(json.dumps(observed)+chr(10))
if observed['argv_leak']: sys.exit(97)
head='a'*40; base='b'*40
pull=dict(number=1,state='open',draft=False,merged=False,mergeable=True,title='Title',head={'sha':head,'ref':'task','repo':{'full_name':'owner/repo'}},base={'sha':base,'ref':'main','repo':{'full_name':'owner/repo'}},user={'login':'author'})
if u.path=='/swagger.v1.json': body={'info':{'title':'Gitea API','version':'1.24.7'}}
elif u.path.endswith('/user'): body={'login':'fixture-user'}
elif '/branches/' in u.path: body={'commit':{'id':head}}
elif u.path.endswith('/status'): body={'state':'success','statuses':[{'context':'build','status':'success'}]}
elif u.path.endswith('/pulls/1'): body=pull
elif u.path.endswith('/pulls/1.diff'): body='diff --git a/example b/example'
elif u.path.endswith('/issues/1'): body=dict(number=1,title='Title',state='open',user={'login':'author'},created_at='',body='Body',comments=0)
elif u.path.endswith('/comments'): body=[] if method=='GET' else {'id':7,'body':'body','user':{'login':'fixture-user'},'issue_url':'https://forge.example.test/owner/repo/issues/1'}
elif u.path.endswith('/repos/owner/repo'): body={'default_branch':'main','permissions':{'admin':True}}
else: body={'id':7,'number':1,'login':'fixture-user','name':'reviewers','merged':True}
text=body if isinstance(body,str) else json.dumps(body)
if '-o' in a: pathlib.Path(a[a.index('-o')+1]).write_text(text)
else: print(text,end='')
if '-w' in a:
fmt=a[a.index('-w')+1].replace('%{http_code}','200').replace(chr(92)+'n',chr(10))
print(fmt,end='')
'''
LEGACY_CASES = {
'ci-queue-wait.sh': (['--purpose','push','-B','task','-t','1','-i','1'], 'GET', '/commits/'+'a'*40+'/status'),
'grant-reviewer.sh': (['-u','reviewer'], 'GET', '/orgs/owner'),
'issue-close.sh': (['-i','1'], 'PATCH', '/issues/1'),
'issue-comment.sh': (['-i','1','-b','body'], 'POST', '/issues/1/comments'),
'issue-create.sh': (['-t','Title','-b','body'], 'POST', '/issues'),
'issue-reopen.sh': (['-i','1'], 'PATCH', '/issues/1'),
'issue-view.sh': (['-i','1'], 'GET', '/issues/1'),
'pr-create.sh': (['-t','Title','-H','task','-B','main'], 'POST', '/pulls'),
'pr-diff.sh': (['-n','1'], 'GET', '/pulls/1.diff'),
'pr-edit.sh': (['-n','1','--login','fixture-user','--title','Title'], 'PATCH', '/pulls/1'),
'pr-merge.sh': (['-n','1','--expect-head','a'*40], 'POST', '/pulls/1/merge'),
'pr-metadata.sh': (['-n','1'], 'GET', '/pulls/1'),
'pr-review.sh': (['-n','1','-a','comment','-c','body'], 'POST', '/issues/1/comments'),
}
class Auth(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory(); self.addCleanup(self.tmp.cleanup)
self.root=pathlib.Path(self.tmp.name)
self.env=dict(os.environ,HOME=str(self.root),MOSAIC_BRAIN_HOME=str(self.root/'brain'),GIT_CONFIG_GLOBAL='/dev/null',GITEA_TOKEN='shared-synthetic',GITEA_URL='https://forge.example.test',TMPDIR=str(self.root),TMP=str(self.root),TEMP=str(self.root))
self.env.pop('MOSAIC_GIT_IDENTITY',None)
self.token=self.root/'brain/credentials/forge/forge.example.test/fixture-user.token'
self.token.parent.mkdir(parents=True); self.token.write_text('private-synthetic-token')
def credential(self,trace=False):
return subprocess.run(['bash',*(['-x'] if trace else []),'-c','source "$1/detect-platform.sh"; get_gitea_token forge.example.test','fixture',str(HERE)],env=self.env,cwd=self.root,capture_output=True,text=True)
def test_no_identity_cannot_borrow_shared_or_git_config(self):
subprocess.run(['git','init','-q',str(self.root)],check=True,env=self.env)
subprocess.run(['git','-C',str(self.root),'config','mosaic.gitIdentity','fixture-user'],check=True,env=self.env)
p=self.credential(); self.assertNotEqual(p.returncode,0); self.assertEqual(p.stdout,'')
def test_host_bound_credential_and_xtrace(self):
self.env['MOSAIC_GIT_IDENTITY']='fixture-user'
p=self.credential(True); self.assertEqual(p.returncode,0,p.stderr)
self.assertEqual(p.stdout.strip(),'private-synthetic-token')
self.assertNotIn('private-synthetic-token',p.stderr)
self.token.unlink(); p=self.credential(); self.assertNotEqual(p.returncode,0); self.assertEqual(p.stdout,'')
def test_provider_store_separates_seat_and_service(self):
import re
mappings=re.findall(r'^\s*([A-Za-z0-9.-]+)\)\s+_idpfx=([A-Za-z0-9-]+)',(HERE/'detect-platform.sh').read_text(),re.MULTILINE)
self.assertTrue(mappings)
self.env['MOSAIC_GIT_IDENTITY']='fixture-user'
for host,prefix in mappings:
service=self.root/'.config/mosaic/secrets/gitea-tokens'/(prefix+'-fixture-user.token')
service.parent.mkdir(parents=True,exist_ok=True); service.write_text('service-synthetic')
seat=self.root/'brain/fleet/agents/fixture-user/secrets'
seat.mkdir(parents=True,exist_ok=True)
command=['bash','-c','source "$1/detect-platform.sh"; get_gitea_token "$2"','fixture',str(HERE),host]
p=subprocess.run(command,env=self.env,capture_output=True,text=True)
self.assertNotEqual(p.returncode,0,'seat borrowed service token')
token=seat/(prefix+'-fixture-user.token'); token.write_text('seat-synthetic')
p=subprocess.run(command,env=self.env,capture_output=True,text=True)
self.assertEqual(p.returncode,0,p.stderr); self.assertEqual(p.stdout,'seat-synthetic')
def legacy_fixture(self):
import shutil
tools=self.root/'tools'
shutil.copytree(HERE,tools,ignore=shutil.ignore_patterns('__pycache__','.work'))
repo=self.root/'repo'; repo.mkdir()
subprocess.run(['git','init','-q','-b','task',str(repo)],check=True,env=self.env)
subprocess.run(['git','-C',str(repo),'remote','add','origin','https://forge.example.test/owner/repo'],check=True,env=self.env)
bin_dir=self.root/'bin';bin_dir.mkdir()
(bin_dir/'curl').write_text(LEGACY_CURL);(bin_dir/'curl').chmod(0o755)
(bin_dir/'tea').write_text('#!/bin/sh\nexit 1\n');(bin_dir/'tea').chmod(0o755)
for name in ['gh','glab','wget','ssh']:
(bin_dir/name).write_text('#!/bin/sh\nexit 99\n');(bin_dir/name).chmod(0o755)
config=self.root/'.config/tea';config.mkdir(parents=True)
(config/'config.yml').write_text('logins:\n - name: fixture-user\n url: https://forge.example.test\n token: private-synthetic-token\n')
credentials=self.root/'credentials.json';credentials.write_text('{}')
self.env.update(PATH=str(bin_dir)+os.pathsep+os.environ['PATH'],MOSAIC_GIT_IDENTITY='fixture-user',XDG_CONFIG_HOME=str(self.root/'.config'),MOSAIC_CREDENTIALS_FILE=str(credentials),GITEA_LOGIN='fixture-user',GITEA_URL='https://forge.example.test',TEST_AUTH_SECRET='private-synthetic-token',TEST_AUTH_OBSERVATIONS=str(self.root/'observations.jsonl'),GIT_CONFIG_NOSYSTEM='1')
for key in ['GIT_DIR','GIT_WORK_TREE','BASH_ENV']: self.env.pop(key,None)
return tools,repo
def legacy_call(self,tools,repo,name):
import json
log=self.root/'observations.jsonl'
if log.exists(): log.unlink()
args,method,suffix=LEGACY_CASES[name]
p=subprocess.run(['bash','-x',str(tools/name),*args],cwd=repo,env=self.env,capture_output=True,text=True,timeout=30)
observations=[json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
# Never put captured traces, argv or credential values in assertion output.
leaked=self.env['TEST_AUTH_SECRET'] in p.stdout+p.stderr
authenticated=any(o['authenticated'] and o['method']==method and o['path'].endswith(suffix) for o in observations)
return dict(exit_code=p.returncode,trace_leak=leaked,argv_leak=any(o['argv_leak'] for o in observations),authenticated=authenticated,anonymous=all(o['anonymous'] for o in observations if o['authenticated']))
def test_all_legacy_callers_keep_credentials_private_under_xtrace(self):
tools,repo=self.legacy_fixture()
for name in LEGACY_CASES:
with self.subTest(wrapper=name):
observed=self.legacy_call(tools,repo,name)
self.assertTrue(observed['authenticated'],name+' did not reach its credential-consuming request; exit '+str(observed['exit_code']))
self.assertFalse(observed['trace_leak'],name+' exposed credential in captured trace')
self.assertFalse(observed['argv_leak'],name+' exposed credential in argv')
self.assertTrue(observed['anonymous'],name+' did not use anonymous authentication')
def test_legacy_anonymous_config_escapes_credential(self):
tools,repo=self.legacy_fixture()
secret='quoted"'+chr(92)+'credential'
self.token.write_text(secret)
self.env['TEST_AUTH_SECRET']=secret
observed=self.legacy_call(tools,repo,'pr-metadata.sh')
self.assertTrue(observed['authenticated'],'credential config escaping failed')
self.assertTrue(observed['anonymous'])
self.assertFalse(observed['trace_leak'] or observed['argv_leak'])
def test_metadata_xtrace_mutant_is_detected(self):
tools,repo=self.legacy_fixture()
metadata=tools/'pr-metadata.sh'
source=metadata.read_text()
marker='mosaic_private_forge_main() (\n set +x'
self.assertIn(marker,source)
metadata.write_text(source.replace(marker,'mosaic_private_forge_main() (\n :',1))
observed=self.legacy_call(tools,repo,'pr-metadata.sh')
self.assertTrue(observed['authenticated'])
self.assertTrue(observed['trace_leak'],'removing caller privacy was not detected')
def test_legacy_argv_mutant_is_detected(self):
tools,repo=self.legacy_fixture()
helper=tools/'detect-platform.sh'
source=helper.read_text()
marker='curl --config "/dev/fd/$auth_fd" "$@"'
self.assertIn(marker,source)
helper.write_text(source.replace(marker,'curl -H "Authorization: token $token" "$@"',1))
observed=self.legacy_call(tools,repo,'pr-metadata.sh')
self.assertTrue(observed['argv_leak'],'raw-header mutation was not detected')
self.assertFalse(observed['trace_leak'])
self.assertNotEqual(observed['exit_code'],0)
def test_legacy_consumer_inventory_has_wrapper_controls(self):
import re
consumers={p.name for p in HERE.glob('*.sh') if not p.name.startswith(('test-','fixture-')) and p.name!='detect-platform.sh' and re.search(r'\$\(get_gitea_token(?:_for_login)?\b',p.read_text())}
self.assertEqual(consumers,set(LEGACY_CASES))
def test_sigterm_leaves_no_credential_files(self):
# Child blocks inside curl's transport; only anonymous fds hold auth.
self.env['MOSAIC_GIT_IDENTITY']='fixture-user'
script='''import importlib.util,os,pathlib,signal,sys,time
s=importlib.util.spec_from_file_location('a',sys.argv[1]); a=importlib.util.module_from_spec(s); s.loader.exec_module(a)
c=a.Client('forge.example.test','owner/repo'); c.provider='gitea'
def transport(args,body,writing):
assert all('private-synthetic-token' not in x for x in args)
config=args[args.index('--config')+1]
assert config.startswith('/dev/fd/')
assert 'private-synthetic-token' in pathlib.Path(config).read_text()
pathlib.Path(sys.argv[2]).write_text('ready')
while True: time.sleep(1)
c.transport=transport
c.http('/repos/owner/repo')
'''
ready=self.root/'ready'
before=set(self.root.rglob('*'))
child=subprocess.Popen(['python3','-I','-c',script,str(HERE/'forge-api.py'),str(ready)],env=self.env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
try:
deadline=time.monotonic()+10
while not ready.exists() and child.poll() is None and time.monotonic()<deadline: time.sleep(.02)
self.assertTrue(ready.exists(),'transport did not receive auth fd')
child.terminate(); child.communicate(timeout=5)
self.assertEqual(child.returncode,-signal.SIGTERM)
self.assertEqual(set(self.root.rglob('*'))-before,{ready})
finally:
if child.poll() is None: child.kill(); child.communicate()
if __name__=='__main__': unittest.main()
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff. Show more