diff --git a/README.md b/README.md index dfae1e0..3d09c3a 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ binary execution. The compatibility record is in - [smoke-contextstream.mjs](smoke-contextstream.mjs) — real stdio MCP and hosted-grounding smoke test - [demo-script.md](demo-script.md) — reproducible 60–90 second flagship demo +- [demo/fixture](demo/fixture) — disposable signed-invitation repository used + for the Claude-to-Codex continuity proof - [community-runbook.md](community-runbook.md) — public ContextStream Builders community launch checklist - [block-outreach.md](block-outreach.md) — concise proof-first outreach and diff --git a/demo-script.md b/demo-script.md index f08a01f..ce9422b 100644 --- a/demo-script.md +++ b/demo-script.md @@ -4,8 +4,12 @@ Target length: 75 seconds. This is a continuity proof, not a feature tour. ## Before recording -- Create one ContextStream project with a small repository, a product - requirement, one architecture decision, and one known constraint. +- Copy [demo/fixture](demo/fixture) into a disposable working directory and + initialize it as a fresh Git repository. The baseline deliberately verifies + token signatures without enforcing the ContextStream-backed expiry policy. +- Create one ContextStream project for that fixture with a product requirement, + one architecture decision, and one known constraint. Label all records as + synthetic demo knowledge, not production guidance. - Start a Claude Code Buzz agent and a Codex Buzz agent through [run-agent.sh](run-agent.sh), each with a distinct Buzz identity and the same ContextStream project. diff --git a/demo/fixture/README.md b/demo/fixture/README.md new file mode 100644 index 0000000..ed072a2 --- /dev/null +++ b/demo/fixture/README.md @@ -0,0 +1,19 @@ +# Invitation continuity fixture + +This is the synthetic repository used by the flagship ContextStream × Buzz +demonstration. Copy it to a disposable directory before each run; do not let a +recording mutate this baseline. + +The fixture issues HMAC-signed invitations and verifies their signatures. It +intentionally omits expiry enforcement so Claude can start the implementation +from approved ContextStream requirements. After a human approves a changed +admin-invitation policy, Codex must retrieve that durable decision in another +Buzz room and finish the implementation without being rebriefed. + +Run the baseline tests with: + +```bash +npm test +``` + +Everything here is demo-only and must not be treated as production guidance. diff --git a/demo/fixture/package.json b/demo/fixture/package.json new file mode 100644 index 0000000..10d824d --- /dev/null +++ b/demo/fixture/package.json @@ -0,0 +1,8 @@ +{ + "name": "contextstream-buzz-invitation-fixture", + "private": true, + "type": "module", + "scripts": { + "test": "node --test" + } +} diff --git a/demo/fixture/src/invitations.mjs b/demo/fixture/src/invitations.mjs new file mode 100644 index 0000000..dec932d --- /dev/null +++ b/demo/fixture/src/invitations.mjs @@ -0,0 +1,43 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +function sign(encodedPayload, secret) { + return createHmac("sha256", secret).update(encodedPayload).digest("base64url"); +} + +export function issueInvitation({ + role = "member", + issuedAtMs, + nonce, + secret, +}) { + const payload = Buffer.from( + JSON.stringify({ role, issuedAtMs, nonce }), + "utf8", + ).toString("base64url"); + + return `${payload}.${sign(payload, secret)}`; +} + +export function verifyInvitation(token, { secret, nowMs }) { + const [payload, suppliedSignature] = token.split("."); + if (!payload || !suppliedSignature) { + return { valid: false, reason: "malformed" }; + } + + const expectedSignature = sign(payload, secret); + const supplied = Buffer.from(suppliedSignature); + const expected = Buffer.from(expectedSignature); + if ( + supplied.length !== expected.length || + !timingSafeEqual(supplied, expected) + ) { + return { valid: false, reason: "invalid_signature" }; + } + + const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")); + + // Deliberately incomplete. Demo agents must retrieve and implement the + // approved expiry policy from the shared ContextStream project. + void nowMs; + return { valid: true, claims }; +} diff --git a/demo/fixture/test/invitations.test.mjs b/demo/fixture/test/invitations.test.mjs new file mode 100644 index 0000000..e576b1f --- /dev/null +++ b/demo/fixture/test/invitations.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { issueInvitation, verifyInvitation } from "../src/invitations.mjs"; + +const secret = "demo-secret"; +const issuedAtMs = Date.UTC(2026, 7, 5, 12, 0, 0); + +test("accepts an untampered signed invitation", () => { + const token = issueInvitation({ + role: "member", + issuedAtMs, + nonce: "invite-1", + secret, + }); + + const result = verifyInvitation(token, { + secret, + nowMs: issuedAtMs + 1_000, + }); + + assert.equal(result.valid, true); + assert.equal(result.claims.role, "member"); +}); + +test("rejects a tampered invitation", () => { + const token = issueInvitation({ + role: "member", + issuedAtMs, + nonce: "invite-2", + secret, + }); + const tampered = `${token.slice(0, -1)}x`; + + assert.deepEqual( + verifyInvitation(tampered, { secret, nowMs: issuedAtMs + 1_000 }), + { valid: false, reason: "invalid_signature" }, + ); +}); diff --git a/package.json b/package.json index e506415..8bedf7f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "Shared ContextStream project context for agents in Buzz", "scripts": { - "test": "node --test reference.test.mjs", + "test": "node --test reference.test.mjs demo/fixture/test/invitations.test.mjs", "smoke": "node smoke-contextstream.mjs" }, "engines": {