feat: operationalise project bus for Sandbox

This commit is contained in:
Codex Lead Engineer
2026-07-30 20:06:34 +02:00
parent a2229bc269
commit 8e4bee1de4
11 changed files with 702 additions and 1 deletions
+3
View File
@@ -161,7 +161,10 @@ must not be mounted on NFS or used for active-active replicas.
- `tests/` — service, concurrency, governance, and HTTP contract tests; - `tests/` — service, concurrency, governance, and HTTP contract tests;
- `docs/` — architecture, tool contract, threat model, production notes; - `docs/` — architecture, tool contract, threat model, production notes;
- `deploy/` — container and Compose example; - `deploy/` — container and Compose example;
- `work-packages/` — governed implementation and activation scopes;
- `evidence/` — WP-PA-001 verification and independent-review handoff. - `evidence/` — WP-PA-001 verification and independent-review handoff.
See [docs/TOOL_CONTRACT.md](docs/TOOL_CONTRACT.md) before integrating a new See [docs/TOOL_CONTRACT.md](docs/TOOL_CONTRACT.md) before integrating a new
actor and [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) before deployment. actor and [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) before deployment.
Sandbox activation is governed by
[docs/SANDBOX_ACTIVATION_RUNBOOK.md](docs/SANDBOX_ACTIVATION_RUNBOOK.md).
+12 -1
View File
@@ -8,6 +8,7 @@ services:
PROJECT_BUS_BOOTSTRAP_TOKEN: "${PROJECT_BUS_BOOTSTRAP_TOKEN:?set a strong bootstrap token}" PROJECT_BUS_BOOTSTRAP_TOKEN: "${PROJECT_BUS_BOOTSTRAP_TOKEN:?set a strong bootstrap token}"
PROJECT_BUS_TOKENS_JSON: "${PROJECT_BUS_TOKENS_JSON:-}" PROJECT_BUS_TOKENS_JSON: "${PROJECT_BUS_TOKENS_JSON:-}"
PROJECT_BUS_ALLOWED_ORIGINS: "${PROJECT_BUS_ALLOWED_ORIGINS:-}" PROJECT_BUS_ALLOWED_ORIGINS: "${PROJECT_BUS_ALLOWED_ORIGINS:-}"
PROJECT_BUS_LOG_LEVEL: "${PROJECT_BUS_LOG_LEVEL:-INFO}"
ports: ports:
- "127.0.0.1:8080:8080" - "127.0.0.1:8080:8080"
volumes: volumes:
@@ -19,7 +20,17 @@ services:
- no-new-privileges:true - no-new-privileges:true
cap_drop: cap_drop:
- ALL - ALL
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2)"
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
stop_grace_period: 15s
volumes: volumes:
project-bus-data: project-bus-data:
+6
View File
@@ -0,0 +1,6 @@
# Copy outside Git, restrict to the service administrator, and replace every
# placeholder. Never commit the populated file.
PROJECT_BUS_BOOTSTRAP_TOKEN=replace-with-at-least-24-random-characters
PROJECT_BUS_TOKENS_JSON={"replace-po-token":{"actor_id":"sandbox-po","auth_subject":"sandbox-po","display_name":"Dillard / Product Owner"},"replace-pm-token":{"actor_id":"sandbox-pm","auth_subject":"sandbox-pm","display_name":"Sandbox PM"},"replace-pa-token":{"actor_id":"sandbox-pa","auth_subject":"sandbox-pa","display_name":"Sandbox PA"},"replace-codex-token":{"actor_id":"codex-implementer","auth_subject":"codex-implementer","display_name":"Codex Implementer"},"replace-claude-token":{"actor_id":"claude-reviewer","auth_subject":"claude-reviewer","display_name":"Claude Code Reviewer"}}
PROJECT_BUS_ALLOWED_ORIGINS=
PROJECT_BUS_LOG_LEVEL=INFO
+39
View File
@@ -0,0 +1,39 @@
{
"project": {
"project_id": "epimonos-sandbox",
"name": "Epimonos Sandbox",
"description": "BASDM-governed Sandbox project coordination"
},
"actors": [
{
"actor_id": "sandbox-po",
"auth_subject": "sandbox-po",
"display_name": "Dillard / Product Owner",
"role": "PO"
},
{
"actor_id": "sandbox-pm",
"auth_subject": "sandbox-pm",
"display_name": "Sandbox PM",
"role": "PM"
},
{
"actor_id": "sandbox-pa",
"auth_subject": "sandbox-pa",
"display_name": "Sandbox PA",
"role": "PA"
},
{
"actor_id": "codex-implementer",
"auth_subject": "codex-implementer",
"display_name": "Codex Implementer",
"role": "IMPLEMENTER"
},
{
"actor_id": "claude-reviewer",
"auth_subject": "claude-reviewer",
"display_name": "Claude Code Reviewer",
"role": "REVIEWER"
}
]
}
+151
View File
@@ -0,0 +1,151 @@
# Sandbox Activation Runbook
This runbook operationalises the Project Bus for the Epimonos Sandbox project.
It does not itself authorize an internet-facing endpoint or replacement of the
existing formal project channels.
## Authority and transition rule
WP-PA-001 is the accepted software baseline. WP-PA-002 covers deployment and
activation. SG, PTO and the existing project threads remain authoritative
until the independent reviewer returns `APPROVE` on the operationalisation
commit and the Sandbox PM closes the WP-PA-002 gate with `ACCEPT`.
Before that gate, Project Bus records are shadow records. Differences are
resolved in favour of the formal threads and recorded as corrections rather
than silently rewriting history.
## Deployment preparation
1. Use a dedicated single-node host or controlled test VM.
2. Keep the endpoint private unless PM-to-PO consultation has approved
internet exposure.
3. Copy `deploy/project-bus.env.example` to a root/service-owner-readable file
outside the repository.
4. Generate independent random bearer credentials of at least 32 bytes for
SYSTEM, PO, PM, PA, Codex and Claude. Never reuse Git or account tokens.
5. Replace the example values and set permissions to owner-read/write only.
6. Configure TLS/authenticated ingress before any non-local exposure.
7. Pin the reviewed Git commit or immutable image digest.
Start:
```bash
docker compose --env-file /secure/path/project-bus.env \
-f deploy/compose.yaml up -d --build
docker compose -f deploy/compose.yaml ps
```
The container must report healthy and run as the non-root image user.
## Project bootstrap
The bootstrap command reads the SYSTEM credential only from an environment
variable. The manifest contains identities and roles, never credentials.
```bash
export PROJECT_BUS_BOOTSTRAP_TOKEN='read-from-secret-store'
project-bus-ops bootstrap \
--endpoint http://127.0.0.1:8080/mcp \
--manifest deploy/sandbox-project.json
unset PROJECT_BUS_BOOTSTRAP_TOKEN
```
Run the same command a second time. It must replay successfully and create no
duplicate project, actor or event records.
After bootstrap, remove the SYSTEM credential from the steady-state service
configuration and restart. Retain it offline only if the operating policy
requires future project creation; otherwise rotate/revoke it.
## Actor connection and smoke test
For each actor credential:
1. call `initialize` without authentication;
2. call `get_project` for `epimonos-sandbox`;
3. verify returned roster and formal role expectations;
4. call `sync_since` with that client's durable cursor;
5. call `list_pending_actions`;
6. persist the new cursor only after processing all returned events.
Store each actor token in that client's secret facility. Do not place tokens in
`.mcp.json`, project instructions, prompts, shell history or Git.
Claude Code endpoint registration:
```bash
claude mcp add --scope project --transport http \
epimonos-project-bus https://approved-project-bus.example/mcp
```
The endpoint configuration may be project-scoped; the credential must remain
in a user/workload secret store.
## Initial shadow records
Once the actors authenticate, the PM registers WP-PA-002 through
`issue_work_package`, Codex claims it, and the PA links the implementation
commit and evidence. These records mirror the formal thread record during the
bootstrap phase. Claude submits the independent review through both the
existing formal channel and the bus.
The PM then closes the WP-PA-002 gate. Only an `ACCEPT` outcome after an
independent `APPROVE` permits the separately recorded operational decision
that the bus becomes the primary project interface.
## Backup and restore
Create a consistent online backup:
```bash
project-bus-ops backup \
--source /data/project-bus.db \
--destination /secure/backup/project-bus-$(date +%Y%m%dT%H%M%S).db
```
The command uses SQLite's online backup API and runs `PRAGMA integrity_check`.
Copy the backup off-host under the applicable retention policy.
Quarterly and before primary-interface activation:
1. restore a backup into an isolated instance;
2. start the exact reviewed application version against the restored database;
3. authenticate with a temporary test credential;
4. compare the restored maximum event cursor with the backup receipt;
5. run `get_project`, `sync_since` and `list_pending_actions`;
6. destroy the isolated restore instance and revoke the test credential.
## Rotation and revocation
Static-token changes require a restart in v0.1. Generate a replacement,
update the external secret configuration, restart, verify the new credential,
then remove the old credential and restart again. Treat exposure as an
incident; rotate first and investigate from the immutable event trail and edge
logs.
## Cursor recovery
Each consumer owns a durable cursor per project. When processing fails, retain
the last successfully processed cursor and replay from it. Reads are safe to
repeat; mutations require stable idempotency keys. Never advance a cursor
before all returned events have been durably processed.
## Rollback
Before the activation gate, stop the bus and return solely to SG/PTO/project
threads; no authority has transferred. After activation, announce the incident
in the fallback formal channels, stop writes, preserve the database and logs,
restore the latest verified backup if required, and record reconciliation
events when service resumes. Never delete or edit prior events.
## Gate evidence checklist
- exact reviewed commit/image digest;
- healthy non-root container;
- two successful bootstrap runs with unchanged event cursor on replay;
- successful authenticated checks for every formal actor;
- backup receipt, restore result and matching event cursor;
- secret scan with no credential material in Git;
- independent Claude review report;
- PM `ACCEPT` gate event and explicit primary-interface operational decision.
@@ -0,0 +1,77 @@
# WP-PA-002 Implementation Evidence
Baseline: `a2229bc26902e79d63a368e8ab002ebefd4f0863`
Implementer: Codex (`IMPLEMENTER`)
Date: 2026-07-30
## Scope delivered
- repeatable Sandbox project and actor bootstrap manifest;
- operational CLI for authenticated bootstrap and consistent SQLite backup;
- hardened loopback-only Compose profile and external secret template;
- activation, rollback, credential rotation, cursor recovery, backup and
restore runbook;
- regression coverage against the real migrated database schema;
- independent-review brief for Claude Code.
## Local verification
Python: `3.12.13`
```text
PYTHONPATH=src python3.12 -m unittest discover -s tests -v
Ran 24 tests in 7.315s
OK
PIP_NO_CACHE_DIR=1 <clean-venv>/bin/pip install --no-deps .
<clean-venv>/bin/python -m unittest discover -s tests -v
Ran 24 tests in 6.201s
OK
python3.12 -m compileall -q src tests
git diff --check
```
Both final commands completed without output or error.
The operational exercise used a fresh migrated database and verified:
- first bootstrap creates the project and five distinct formal actors;
- replay with the same manifest and stable idempotency keys creates no
additional events;
- PO, PM, PA, Codex and Claude identities authenticate independently and can
read the roster, synchronize events and query role-filtered pending actions;
- online backup completes with `PRAGMA integrity_check = ok`;
- the restored database has the same maximum `event_log.cursor` as its source.
The backup exercise initially exposed an incorrect `events(sequence)` query.
The implementation now queries the actual migrated schema,
`event_log(cursor)`, and
`test_backup_is_consistent_and_integrity_checked` prevents regression.
## Container verification handoff
No Docker or Podman runtime is installed in the implementer's Work
environment. The implementer therefore did not claim container-build or
runtime evidence. The independent reviewer must build the exact review commit
in a Docker-capable environment and verify:
- successful image and Compose build;
- healthy service;
- runtime UID `65532`;
- read-only root filesystem, dropped capabilities and
`no-new-privileges`;
- loopback-only published port;
- persistent database volume.
This is an explicit review task in `WP-PA-002-REVIEW-BRIEF.md`, not a waived
acceptance criterion.
## Secret handling and authority
No populated environment file or actor credential is part of the worktree.
`deploy/project-bus.env.example` contains placeholders only. The runbook keeps
SG, PTO and existing project threads authoritative until an independent
`APPROVE` and PM `ACCEPT`. No endpoint, VM, purchase or internet-facing
deployment was created by this work package.
+32
View File
@@ -0,0 +1,32 @@
# Independent Review Brief — WP-PA-002
Reviewer: Claude Code (`REVIEWER`)
Implementer: Codex (`IMPLEMENTER`)
Review only the exact commit supplied by the PA. Do not change, commit or push
implementation code. Verify that the commit descends from accepted WP-PA-001
commit `a2229bc26902e79d63a368e8ab002ebefd4f0863`.
## Required review
1. Match every acceptance criterion in `work-packages/WP-PA-002.md` to
implementation and independent evidence.
2. Build and run the packaged application under Python 3.12.
3. Build and run the container, confirm its health check and non-root identity.
4. Bootstrap a fresh database twice and prove replay creates no extra events.
5. Authenticate separately as PO, PM, PA, Codex and Claude; verify the roster,
event sync and pending-actions reads.
6. Create a live backup, restore it in isolation, run integrity checks and
compare event cursors.
7. Search Git history and the reviewed tree for committed credentials.
8. Adversarially verify that bootstrap/operations tooling cannot weaken the
governance invariants accepted in WP-PA-001.
9. Verify the runbook preserves SG/PTO/project-thread authority until the
independent `APPROVE` and PM `ACCEPT` activation gate.
10. Check that no deployment is described as production-ready beyond the
constraints in `docs/PRODUCTION_PROFILE.md`.
Write `REVIEW-REPORT-WP-PA-002.md` with exactly one verdict:
`APPROVE`, `CHANGES_REQUESTED`, or `REJECT`. Include exact commit SHA,
environment, commands, results, findings with severity, residual risks, and a
machine-readable summary.
+1
View File
@@ -14,6 +14,7 @@ dependencies = []
[project.scripts] [project.scripts]
project-bus = "project_bus.__main__:main" project-bus = "project_bus.__main__:main"
project-bus-ops = "project_bus.operations:main"
[tool.setuptools] [tool.setuptools]
package-dir = {"" = "src"} package-dir = {"" = "src"}
+187
View File
@@ -0,0 +1,187 @@
from __future__ import annotations
import argparse
import json
import os
import sqlite3
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Protocol
from .models import Role
class OperationsError(RuntimeError):
pass
class ToolCaller(Protocol):
def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: ...
class HTTPMCPClient:
def __init__(self, endpoint: str, token: str, timeout: float = 10.0):
self.endpoint = endpoint
self.token = token
self.timeout = timeout
self._request_id = 0
def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
self._request_id += 1
payload = {
"jsonrpc": "2.0",
"id": self._request_id,
"method": "tools/call",
"params": {"name": name, "arguments": arguments},
}
request = urllib.request.Request(
self.endpoint,
data=json.dumps(payload, separators=(",", ":")).encode(),
headers={
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:
result = json.load(response)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
raise OperationsError(f"MCP request failed: {error}") from error
if "error" in result:
raise OperationsError(f"JSON-RPC error: {result['error']}")
tool_result = result.get("result", {})
if tool_result.get("isError"):
content = tool_result.get("content", [])
message = content[0].get("text", "unknown tool error") if content else "unknown tool error"
raise OperationsError(message)
content = tool_result.get("content", [])
if not content or content[0].get("type") != "text":
raise OperationsError("MCP tool returned no JSON text content")
try:
return json.loads(content[0]["text"])
except json.JSONDecodeError as error:
raise OperationsError("MCP tool returned invalid JSON text") from error
def load_manifest(path: Path) -> dict[str, Any]:
try:
manifest = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise OperationsError(f"Cannot read manifest: {error}") from error
project = manifest.get("project")
actors = manifest.get("actors")
if not isinstance(project, dict) or not isinstance(actors, list) or not actors:
raise OperationsError("Manifest requires a project object and non-empty actors array")
for field in ("project_id", "name"):
if not isinstance(project.get(field), str) or not project[field]:
raise OperationsError(f"Manifest project.{field} must be a non-empty string")
seen_ids: set[str] = set()
seen_subjects: set[str] = set()
for index, actor in enumerate(actors):
if not isinstance(actor, dict):
raise OperationsError(f"Manifest actor {index} must be an object")
for field in ("actor_id", "auth_subject", "display_name", "role"):
if not isinstance(actor.get(field), str) or not actor[field]:
raise OperationsError(f"Manifest actor {index}.{field} must be a non-empty string")
try:
role = Role(actor["role"])
except ValueError as error:
raise OperationsError(f"Manifest actor {index} has invalid role") from error
if role is Role.SYSTEM:
raise OperationsError("SYSTEM is a transport bootstrap identity, not a project actor")
if actor["actor_id"] in seen_ids or actor["auth_subject"] in seen_subjects:
raise OperationsError("Actor IDs and auth subjects must be unique")
seen_ids.add(actor["actor_id"])
seen_subjects.add(actor["auth_subject"])
return manifest
def bootstrap_project(client: ToolCaller, manifest: dict[str, Any]) -> dict[str, Any]:
project = manifest["project"]
project_id = project["project_id"]
created = client.call(
"create_project",
{
"project_id": project_id,
"name": project["name"],
"description": project.get("description", ""),
"idempotency_key": f"bootstrap:{project_id}:project:v1",
},
)
registered = []
for actor in manifest["actors"]:
registered.append(
client.call(
"register_actor",
{
"project_id": project_id,
**actor,
"idempotency_key": f"bootstrap:{project_id}:actor:{actor['actor_id']}:v1",
},
)
)
return {"project": created, "actors": registered}
def backup_database(source: Path, destination: Path) -> dict[str, Any]:
if source.resolve() == destination.resolve():
raise OperationsError("Backup destination must differ from source")
destination.parent.mkdir(parents=True, exist_ok=True)
try:
with sqlite3.connect(source) as source_db, sqlite3.connect(destination) as backup_db:
source_db.backup(backup_db)
integrity = backup_db.execute("PRAGMA integrity_check").fetchone()[0]
cursor_row = backup_db.execute(
"SELECT COALESCE(MAX(cursor), 0) FROM event_log"
).fetchone()
except sqlite3.Error as error:
raise OperationsError(f"Database backup failed: {error}") from error
if integrity != "ok":
raise OperationsError(f"Backup integrity check failed: {integrity}")
return {"destination": str(destination), "integrity": integrity, "event_cursor": cursor_row[0]}
def _bootstrap_command(arguments: argparse.Namespace) -> int:
token = os.environ.get(arguments.token_env)
if not token:
raise OperationsError(f"Required secret environment variable is unset: {arguments.token_env}")
manifest = load_manifest(arguments.manifest)
result = bootstrap_project(HTTPMCPClient(arguments.endpoint, token), manifest)
print(json.dumps(result, indent=2))
return 0
def _backup_command(arguments: argparse.Namespace) -> int:
print(json.dumps(backup_database(arguments.source, arguments.destination), indent=2))
return 0
def parser() -> argparse.ArgumentParser:
result = argparse.ArgumentParser(description="Project Bus operational tooling")
subparsers = result.add_subparsers(dest="command", required=True)
bootstrap = subparsers.add_parser("bootstrap", help="Provision a project and actor roster")
bootstrap.add_argument("--endpoint", required=True)
bootstrap.add_argument("--manifest", type=Path, required=True)
bootstrap.add_argument("--token-env", default="PROJECT_BUS_BOOTSTRAP_TOKEN")
bootstrap.set_defaults(handler=_bootstrap_command)
backup = subparsers.add_parser("backup", help="Create and integrity-check a live SQLite backup")
backup.add_argument("--source", type=Path, required=True)
backup.add_argument("--destination", type=Path, required=True)
backup.set_defaults(handler=_backup_command)
return result
def main() -> None:
arguments = parser().parse_args()
try:
raise SystemExit(arguments.handler(arguments))
except OperationsError as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(2) from error
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
from __future__ import annotations
import json
import sqlite3
import tempfile
import unittest
from pathlib import Path
from project_bus.db import Database
from project_bus.operations import OperationsError, backup_database, bootstrap_project, load_manifest
class RecordingClient:
def __init__(self):
self.calls = []
def call(self, name, arguments):
self.calls.append((name, arguments))
return {"name": name, "idempotency_key": arguments["idempotency_key"]}
class OperationsTests(unittest.TestCase):
def manifest(self):
return {
"project": {
"project_id": "sandbox",
"name": "Sandbox",
"description": "test",
},
"actors": [
{
"actor_id": "po",
"auth_subject": "po",
"display_name": "PO",
"role": "PO",
},
{
"actor_id": "reviewer",
"auth_subject": "reviewer",
"display_name": "Reviewer",
"role": "REVIEWER",
},
],
}
def test_bootstrap_uses_stable_idempotency_keys(self):
first = RecordingClient()
second = RecordingClient()
bootstrap_project(first, self.manifest())
bootstrap_project(second, self.manifest())
self.assertEqual(first.calls, second.calls)
self.assertEqual(first.calls[0][0], "create_project")
self.assertEqual([call[0] for call in first.calls[1:]], ["register_actor", "register_actor"])
def test_manifest_rejects_system_and_duplicate_subjects(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "manifest.json"
invalid = self.manifest()
invalid["actors"][0]["role"] = "SYSTEM"
path.write_text(json.dumps(invalid), encoding="utf-8")
with self.assertRaises(OperationsError):
load_manifest(path)
invalid = self.manifest()
invalid["actors"][1]["auth_subject"] = "po"
path.write_text(json.dumps(invalid), encoding="utf-8")
with self.assertRaises(OperationsError):
load_manifest(path)
def test_backup_is_consistent_and_integrity_checked(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "source.db"
destination = Path(directory) / "backup.db"
Database(source).migrate()
with sqlite3.connect(source) as connection:
connection.execute(
"""
INSERT INTO projects(project_id, name, description, created_at)
VALUES ('sandbox', 'Sandbox', '', '2026-07-30T00:00:00Z')
"""
)
connection.execute(
"""
INSERT INTO actors(actor_id, auth_subject, display_name, created_at)
VALUES ('system', 'system', 'System', '2026-07-30T00:00:00Z')
"""
)
connection.execute(
"""
INSERT INTO event_log(
event_id, project_id, event_type, actor_id, actor_role,
aggregate_type, aggregate_id, payload_json, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"evt_test",
"sandbox",
"test.created",
"system",
"SYSTEM",
"test",
"test-1",
"{}",
"2026-07-30T00:00:00Z",
),
)
receipt = backup_database(source, destination)
self.assertEqual(receipt["integrity"], "ok")
self.assertEqual(receipt["event_cursor"], 1)
with sqlite3.connect(destination) as connection:
self.assertEqual(
connection.execute("SELECT COUNT(*) FROM event_log").fetchone()[0],
1,
)
def test_backup_refuses_in_place_destination(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "source.db"
source.touch()
with self.assertRaises(OperationsError):
backup_database(source, source)
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
# WP-PA-002 — Project Bus Operationalisation
Status: `ISSUED`
Project: Epimonos Sandbox
Issued by: Sandbox PM
Coordinated by: PA
Implementer: Codex (`IMPLEMENTER`)
Independent reviewer: Claude Code (`REVIEWER`)
Predecessor: WP-PA-001, accepted at commit
`a2229bc26902e79d63a368e8ab002ebefd4f0863`
## Objective
Bring the accepted Project Bus MVP into controlled operational use for the
Sandbox project without yet declaring it the primary project interface.
## Scope
- a repeatable, idempotent Sandbox project bootstrap;
- explicit actor/role provisioning without credentials in Git;
- a hardened single-node Compose deployment profile;
- health, authenticated smoke, backup and restore verification;
- client connection guidance and durable cursor ownership;
- an activation, rollback and incident runbook;
- evidence for an independent implementation and security review.
The Project Bus remains a separate generic component and repository. This work
package does not change the Generic Sandbox API, Sandbox Manager Core, Incus
provider, Open WebUI integration, private Epimonos implementation, or the
canonical Sandbox documentation baseline.
## Acceptance criteria
1. A fresh instance can be deployed from the repository with no secret values
committed.
2. The Sandbox project and its formal actors can be provisioned repeatedly
without duplicate state or conflicting idempotency keys.
3. Each configured actor is authenticated by transport-derived identity and
can read the project using its own credential.
4. The operational smoke test checks health, MCP initialization, project
membership, event synchronization, and role-filtered pending actions.
5. A live SQLite backup can be made, integrity checked, restored into an
isolated instance, and compared with the source event cursor.
6. The runbook defines activation, rollback, token revocation/rotation,
cursor recovery, backup, restore and incident handling.
7. SG, PTO and existing project threads remain the formal channels until
Claude has independently approved the exact implementation commit and the
PM has closed the activation gate with `ACCEPT`.
8. No internet-facing deployment, purchase, or new long-lived infrastructure
is created without the existing PM-to-PO consultation rule.
9. The implementation and its evidence receive an independent Claude Code
review; the implementer does not self-review or close the gate.
## Required evidence
- exact implementation commit SHA and clean worktree;
- source and packaged-install test results under Python 3.12;
- container build and non-root runtime proof;
- fresh bootstrap plus idempotent replay transcript;
- per-actor authenticated smoke transcript;
- backup/restore/integrity transcript;
- secret scan and repository diff;
- independent review report with `APPROVE`, `CHANGES_REQUESTED`, or `REJECT`.
## Activation gate
The PM may declare the Project Bus the primary Sandbox project interface only
after all acceptance criteria pass and an independent review has returned
`APPROVE`. Until then, bus records are shadow records and SG/PTO/project
threads remain authoritative.