feat: implement project bus MVP
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Architecture
|
||||
|
||||
## Boundary
|
||||
|
||||
The Project Bus coordinates formal project events. It does not execute source
|
||||
code, clone repositories, inspect artifacts, send chat messages, implement a
|
||||
project methodology, or decide what a project should build. Those activities
|
||||
remain with authenticated actors and external systems.
|
||||
|
||||
## Components
|
||||
|
||||
1. `ProjectBusRequestHandler` implements a small stateless MCP Streamable HTTP
|
||||
transport over JSON-RPC 2.0.
|
||||
2. `AuthProvider` turns transport credentials into an immutable principal.
|
||||
3. `MCPApplication` publishes tools and maps calls to domain operations.
|
||||
4. `ProjectBusService` enforces role, lifecycle, independence, idempotency,
|
||||
and gate invariants.
|
||||
5. `Database` owns migration and transaction boundaries.
|
||||
6. SQLite relational projections support current-state queries; `event_log`
|
||||
is the immutable ordered synchronization source.
|
||||
|
||||
## Write path
|
||||
|
||||
A mutating call is authenticated before tool dispatch. The service opens
|
||||
`BEGIN IMMEDIATE`, resolves the principal to an active project membership,
|
||||
checks the idempotency tuple `(project, actor, key)`, validates current state,
|
||||
updates the relevant projection, appends exactly one event, stores the
|
||||
canonical response, and commits.
|
||||
|
||||
The event and projection update therefore succeed or fail together.
|
||||
Idempotency replays return the original response without adding an event.
|
||||
|
||||
## Event ordering and cursors
|
||||
|
||||
SQLite allocates a monotonic integer `cursor` for each event. `sync_since`
|
||||
filters by project, returns ascending cursors, and returns the last delivered
|
||||
cursor. Consumers persist that value only after processing the response. The
|
||||
cursor is global to the database, so gaps within a project's stream are
|
||||
normal. Consumers must not infer missing project events from gaps.
|
||||
|
||||
This is pull synchronization. The MCP server does not wake a ChatGPT thread or
|
||||
push into a dormant CLI session.
|
||||
|
||||
## State model
|
||||
|
||||
Work packages move through:
|
||||
|
||||
`OPEN → CLAIMED → RESULT_SUBMITTED → IN_REVIEW → REVIEWED → ACCEPTED|REJECTED`
|
||||
|
||||
`HOLD` leaves a reviewed package in `REVIEWED`. A new review can be requested
|
||||
after a completed review. This MVP treats corrections after
|
||||
`CHANGES_REQUESTED` as a new work package or a new explicitly governed
|
||||
iteration; it does not silently overwrite submitted result evidence.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
- Authentication proves an external subject; project membership grants a role.
|
||||
- The database is trusted to enforce integrity and append-only triggers.
|
||||
- Artifact URIs and commit hashes are references, not trusted content.
|
||||
- Reverse proxy identity headers are not consumed by the built-in adapter.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Production Profile
|
||||
|
||||
The MVP is suitable for local integration and controlled proof-of-concept use.
|
||||
Internet-facing or business-critical deployment requires a separate production
|
||||
hardening work package.
|
||||
|
||||
## Required changes
|
||||
|
||||
1. Implement the persistence interface for PostgreSQL 16+.
|
||||
Use transactions, `SELECT … FOR UPDATE` or conditional updates, unique
|
||||
idempotency constraints, and database roles that cannot update/delete the
|
||||
event table.
|
||||
2. Replace `StaticTokenAuthProvider` with validated OIDC/JWT or mTLS workload
|
||||
identity. Bind issuer, audience, subject, expiry, and revocation policy.
|
||||
3. Put the service behind TLS and authenticated ingress. Do not trust arbitrary
|
||||
forwarded identity headers.
|
||||
4. Run multiple stateless instances only after PostgreSQL conformance and
|
||||
concurrency tests pass.
|
||||
5. Add structured audit export, metrics, tracing correlation, alerts, backup,
|
||||
point-in-time recovery, and restore exercises.
|
||||
6. Add membership lifecycle, token revocation, retention/export policy, and
|
||||
project deletion/archival decisions. Event deletion is intentionally absent.
|
||||
7. Pin and scan the base image; generate an SBOM and sign release artifacts.
|
||||
8. Run load, fault, client interoperability, and penetration tests.
|
||||
|
||||
## MCP activation
|
||||
|
||||
The deployed endpoint must support `POST /mcp` with `application/json`. The
|
||||
current service returns normal JSON instead of SSE and uses no session IDs.
|
||||
Validate that each intended client accepts this stateless profile before
|
||||
production gate acceptance.
|
||||
|
||||
Chat threads are not push subscribers. A client, automation, or PA invocation
|
||||
must call `sync_since(last_cursor)`. The cursor itself belongs in durable client
|
||||
state and is advanced only after successful processing.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Threat Model
|
||||
|
||||
## Assets
|
||||
|
||||
- project instructions, decisions, results, reviews, and escalations;
|
||||
- actor-role assignments and identity bindings;
|
||||
- event ordering and audit integrity;
|
||||
- bearer credentials;
|
||||
- commit, artifact, and baseline references;
|
||||
- service/database availability.
|
||||
|
||||
## Adversaries
|
||||
|
||||
- an unauthenticated network client;
|
||||
- an authenticated actor exceeding their formal mandate;
|
||||
- an implementer attempting self-review or self-acceptance;
|
||||
- a compromised bearer token;
|
||||
- a malicious artifact URI or oversized request;
|
||||
- an operator or database account attempting audit-history mutation;
|
||||
- concurrent clients exploiting check/write races.
|
||||
|
||||
## Implemented controls
|
||||
|
||||
| Threat | MVP control |
|
||||
|---|---|
|
||||
| Identity spoofing in tool args | no actor argument; identity comes from transport |
|
||||
| Token timing comparison | SHA-256 digest and constant-time comparison |
|
||||
| Unauthorized project access | active membership lookup per call |
|
||||
| PA decision overreach | explicit decision/gate/baseline denial |
|
||||
| Product decision overreach | PO-only product decisions |
|
||||
| Self-review | reviewer role plus actor independence checks |
|
||||
| Self-acceptance | PM/PO role and result-submitter identity check |
|
||||
| Premature gate acceptance | independent APPROVE review required |
|
||||
| Double claim / TOCTOU | `BEGIN IMMEDIATE` and conditional state update |
|
||||
| Duplicate delivery | scoped request hash and idempotency record |
|
||||
| Audit update/delete | SQLite triggers reject both operations |
|
||||
| Cross-project reads | project filter plus membership resolution |
|
||||
| Browser cross-origin request | exact configurable Origin allowlist |
|
||||
| Oversized request | one MiB body limit |
|
||||
| MIME confusion/caching | strict JSON input, nosniff, no-store |
|
||||
| SQL injection | parameterized SQL; one controlled placeholder expansion |
|
||||
|
||||
Artifact URIs and review evidence are recorded as untrusted data and are never
|
||||
fetched or executed by the bus.
|
||||
|
||||
## Residual risks and production requirements
|
||||
|
||||
1. Static bearer credentials have no built-in expiry, rotation endpoint,
|
||||
audience, issuer, or proof-of-possession. Replace the adapter with validated
|
||||
OIDC/JWT or mTLS identities.
|
||||
2. Plain HTTP is available. Use TLS and prohibit direct public access to the
|
||||
application port.
|
||||
3. SQLite append-only triggers do not protect against an operator replacing
|
||||
the entire database file or disabling triggers. Use restricted DB roles,
|
||||
PostgreSQL permissions, PITR, backups, and an external immutable audit
|
||||
export for production assurance.
|
||||
4. The event payload is not cryptographically chained or signed. Add hash
|
||||
chaining/signatures only if the agreed threat model requires tamper
|
||||
evidence against privileged database operators.
|
||||
5. No application rate limiter exists. Apply per-identity limits and request
|
||||
quotas at the trusted edge.
|
||||
6. The server emits access metadata but no security audit sink or metrics.
|
||||
Integrate structured logs, alerts, and privacy-aware retention.
|
||||
7. Membership deactivation and credential revocation are not exposed as MVP
|
||||
tools. Operators must treat compromised tokens as an immediate
|
||||
configuration/restart incident.
|
||||
8. Role assignment is one role per actor/project. Shared human/agent accounts
|
||||
undermine independence even when IDs differ and are prohibited operationally.
|
||||
9. Evidence and artifact digests are caller assertions; the bus does not
|
||||
verify repository ownership or retrieve content.
|
||||
10. Dependency-free MCP handling reduces supply-chain surface but has not yet
|
||||
been interoperability-tested against every intended client version.
|
||||
|
||||
## Security verification before activation
|
||||
|
||||
- run all tests and an independent code review;
|
||||
- test the exact Claude Code and ChatGPT MCP clients;
|
||||
- replace all example tokens and restrict origin/host exposure;
|
||||
- confirm backup/restore and audit export;
|
||||
- review log contents for sensitive project data;
|
||||
- add abuse limits and operational monitoring;
|
||||
- decide whether privileged-operator tamper evidence is required.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# MCP Tool Contract
|
||||
|
||||
Protocol version: `2025-03-26`
|
||||
Endpoint: `POST /mcp`
|
||||
Transport profile: stateless Streamable HTTP with JSON responses
|
||||
|
||||
`initialize`, `ping`, and `tools/list` do not require credentials. Every
|
||||
`tools/call` requires `Authorization: Bearer …`. The server does not implement
|
||||
server-to-client SSE notifications in this MVP; `GET /mcp` returns `405`.
|
||||
|
||||
## Common rules
|
||||
|
||||
- Every project-scoped call resolves the authenticated principal to one active
|
||||
membership.
|
||||
- Every mutation requires a stable `idempotency_key`.
|
||||
- Reusing a key with the same operation and canonical arguments replays the
|
||||
original response. Reusing it with different input returns `conflict`.
|
||||
- IDs use letters, numbers, `.`, `_`, `:`, `/`, or `-`, up to 128 characters.
|
||||
- Tool domain failures are MCP tool results with `isError: true`.
|
||||
- JSON-RPC syntax/method failures use JSON-RPC error objects.
|
||||
- Missing or invalid credentials return HTTP `401`.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Allowed role / invariant | Primary event |
|
||||
|---|---|---|
|
||||
| `create_project` | SYSTEM only | `project.created` |
|
||||
| `register_actor` | SYSTEM, PO, PM; PM cannot appoint PO | `actor.registered` |
|
||||
| `issue_work_package` | PO, PM | `work_package.issued` |
|
||||
| `claim_work_package` | Architect, Lead Engineer, Implementer; atomic OPEN claim | `work_package.claimed` |
|
||||
| `submit_result` | claiming actor only | `work_package.result_submitted` |
|
||||
| `request_review` | PO, PM, PA, implementation/design roles; reviewer independent | `review.requested` |
|
||||
| `submit_review` | assigned independent review actor | `review.submitted` |
|
||||
| `record_decision` | product: PO; other kinds: PO/PM; never PA | `decision.recorded` |
|
||||
| `raise_escalation` | every active member | `escalation.raised` |
|
||||
| `resolve_escalation` | PO, PM | `escalation.resolved` |
|
||||
| `link_commit` | every active member; valid hexadecimal object ID | `commit.linked` |
|
||||
| `link_artifact` | every active member | `artifact.linked` |
|
||||
| `close_gate` | PO, PM; ACCEPT needs independent approval | `gate.closed` |
|
||||
| `baseline_release` | PO, PM; included work packages accepted | `baseline.released` |
|
||||
| `sync_since` | every active member; max 500 events | none |
|
||||
| `list_pending_actions` | every active member, role-filtered reviews/escalations | none |
|
||||
| `get_work_package` | every active member | none |
|
||||
| `get_project` | every active member; includes actor-role roster | none |
|
||||
|
||||
The authoritative argument schema is returned by `tools/list`. Clients should
|
||||
discover it instead of hard-coding optional defaults.
|
||||
|
||||
## Event envelope
|
||||
|
||||
`sync_since` returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"cursor": 42,
|
||||
"event_id": "evt_...",
|
||||
"project_id": "example",
|
||||
"event_type": "review.submitted",
|
||||
"actor_id": "reviewer-1",
|
||||
"actor_role": "REVIEWER",
|
||||
"aggregate_type": "review",
|
||||
"aggregate_id": "rev_...",
|
||||
"correlation_id": "WP-001",
|
||||
"causation_event_id": null,
|
||||
"created_at": "2026-07-30T00:00:00.000Z",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
Event payloads are version-zero MVP structures. Consumers should ignore
|
||||
unknown fields and key behavior off `event_type`, not prose.
|
||||
|
||||
## MCP interoperability limits
|
||||
|
||||
The transport implements the required request/response subset used by MCP
|
||||
tools over Streamable HTTP. It is stateless, does not issue `Mcp-Session-Id`,
|
||||
does not accept JSON-RPC batches, and provides no resumable SSE channel.
|
||||
Interoperability must be checked against the concrete ChatGPT and Claude Code
|
||||
clients during activation.
|
||||
Reference in New Issue
Block a user