feat: operationalise project bus for Sandbox
This commit is contained in:
@@ -161,7 +161,10 @@ must not be mounted on NFS or used for active-active replicas.
|
||||
- `tests/` — service, concurrency, governance, and HTTP contract tests;
|
||||
- `docs/` — architecture, tool contract, threat model, production notes;
|
||||
- `deploy/` — container and Compose example;
|
||||
- `work-packages/` — governed implementation and activation scopes;
|
||||
- `evidence/` — WP-PA-001 verification and independent-review handoff.
|
||||
|
||||
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.
|
||||
Sandbox activation is governed by
|
||||
[docs/SANDBOX_ACTIVATION_RUNBOOK.md](docs/SANDBOX_ACTIVATION_RUNBOOK.md).
|
||||
|
||||
+12
-1
@@ -8,6 +8,7 @@ services:
|
||||
PROJECT_BUS_BOOTSTRAP_TOKEN: "${PROJECT_BUS_BOOTSTRAP_TOKEN:?set a strong bootstrap token}"
|
||||
PROJECT_BUS_TOKENS_JSON: "${PROJECT_BUS_TOKENS_JSON:-}"
|
||||
PROJECT_BUS_ALLOWED_ORIGINS: "${PROJECT_BUS_ALLOWED_ORIGINS:-}"
|
||||
PROJECT_BUS_LOG_LEVEL: "${PROJECT_BUS_LOG_LEVEL:-INFO}"
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
@@ -19,7 +20,17 @@ services:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- 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:
|
||||
project-bus-data:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -14,6 +14,7 @@ dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
project-bus = "project_bus.__main__:main"
|
||||
project-bus-ops = "project_bus.operations:main"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user