feat(governance): vote spine end-to-end — propose, open, vote, close over HTTP - #66
Merged
Merged
Conversation
…over HTTP S3 of the Solon v1 plan: the domain layer gets its public write surface, a reference agent signer, and a database-backed acceptance spec in CI. Routes (thin Zod-validated shells over lib/domain): - POST /api/proposals — file a signed proposal; the Bitcoin signature over proposalMessage() is the authorization, and AGENT proposers must also present their transport API key (Authorization: Bearer sk_solon_…) - POST /api/proposals/[id]/open — open the voting session (permissionless by design: the proposal is already signed and public, opening only starts the clock, and it can happen exactly once) - GET /api/sessions/[id] — session, snapshotted rules, live tally - POST /api/sessions/[id]/votes — cast a signed vote (replaces /api/voting/[id]/cryptographic-vote; UI + integration page updated) - POST /api/sessions/[id]/close — close and decide the outcome New domain code: - lib/domain/proposals.ts createProposal(): content binding via sha256 of canonical JSON (lib/domain/canonical.ts — the cross-repo contract OrangeCat will re-hash against, pinned by golden-hash tests) - closeRefusal() guard: a session cannot be closed while the voting window is open unless every eligible member has voted — nobody slams the door on a tally they like Scripts: - scripts/agent-vote.ts — reference agent signer (privkey from the agent's own env, signs and POSTs; the key never leaves the agent's box) - scripts/add-member.ts — documented operator bootstrap for the genesis roster, with MEMBER_ADDED audit events and one-time API-key minting CI: new `integration` job — postgres service container, `prisma migrate deploy` on a fresh database (proves baseline + seed replay), then the vote-spine integration spec: propose (agent transport auth enforced) → open (rules snapshotted) → three signed votes → early-close refused → full participation closes → APPROVED → policy v2 activated referencing the session, v1 superseded, every step in the audit trail; plus the humans-only electorate gate. Verified locally against a dockerized postgres 16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
S3 of the Solon v1 plan (S0 #64, S1+S2 #65). The domain layer gets its public write surface, a reference agent signer, an operator bootstrap for the genesis roster, and a database-backed acceptance spec in CI.
Routes (thin Zod shells over
lib/domain)POST /api/proposals— file a signed proposal. The Bitcoin signature overproposalMessage()is the authorization; AGENT proposers additionally presentAuthorization: Bearer sk_solon_…(transport auth and vote signature deliberately separate).POST /api/proposals/[id]/open— open the voting session. Permissionless by design: the proposal is already signed and public, opening only starts the clock, and it can happen exactly once.GET /api/sessions/[id]— session, snapshotted rules, live tally.POST /api/sessions/[id]/votes— cast a signed vote. Replaces/api/voting/[id]/cryptographic-vote(UI and integration page updated — one endpoint, not two).POST /api/sessions/[id]/close— close and decide the outcome.New domain code
createProposal()with content binding:contentHash = sha256(canonicalJson(proposedContent)).lib/domain/canonical.tsis the cross-repo contract OrangeCat will re-hash against — pinned by golden-hash unit tests so a drift breaks CI, not decision verification in prod.closeRefusal()guard: a session cannot close while the voting window is open unless every eligible member has voted — otherwise anyone could stack a tally and slam the door.Scripts
scripts/agent-vote.ts— the reference agent signer OC (Cat) and FC (Loki) will run: privkey from the agent's own env, signs and POSTs; the key never leaves the agent's box.scripts/add-member.ts— documented operator bootstrap for the genesis roster (membership changes after genesis are HUMANS_ONLY votes), withMEMBER_ADDEDaudit events and one-time API-key minting.CI: new
integrationjobPostgres 16 service container →
prisma migrate deployon a fresh database (proves the baseline + seed migrations replay) → the vote-spine spec: propose (agent transport auth enforced both ways) → open (rules snapshotted, 3 eligible) → three signed votes → early close refused → full participation closes early → APPROVED → policy v2 activated referencing the session, v1 superseded → every step present in the audit trail. Second spec: agents rejected from HUMANS_ONLY sessions, and excluded from the eligibility snapshot.Note: the plan named a separate
scripts/e2e-vote.ts; its coverage lives in the integration spec instead — one source of truth for the acceptance flow rather than a script duplicating it.Verification
npm run verifygreen (29 unit tests; integration spec skips withoutINTEGRATION=1).npm run buildgreen.add-member.tssmoke-tested: create + key mint + idempotent rerun.🤖 Generated with Claude Code