hivecode-mcp 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/substrate.js ADDED
@@ -0,0 +1,252 @@
1
+ // substrate.js — the Hivecode convergent semantic substrate.
2
+ //
3
+ // This is the primitive the whole project has been building toward. It is NOT a
4
+ // connection protocol (that is MCP / A2A — a wire that carries messages). It is a
5
+ // MEDIUM: a shared state that agents write into, where the medium itself enforces
6
+ // guarantees no channel can. Coordination is a property of the medium, not of a
7
+ // controller.
8
+ //
9
+ // The unit of exchange is not a message or a tool call — it is a CHANGE: a triple
10
+ // (intent, patch, provenance)
11
+ // published against the medium's current state. On every publish the medium enforces
12
+ // three invariants, and refuses the change if any fails:
13
+ //
14
+ // I1 PROVENANCE-VERIFIED — every accepted byte traces to a cryptographically
15
+ // verified author and a DECLARED intent, chained to the state it built on.
16
+ // A forged, unsigned, or mis-attributed change is rejected. (This is the new
17
+ // part — no CRDT, no git, no MCP carries verifiable per-change provenance.)
18
+ //
19
+ // I2 CONVERGENT — merging is deterministic and symmetric (ICR guarantees this),
20
+ // so any number of agents applying the same set of changes in ANY order reach
21
+ // the exact same state. No divergence, no "last writer wins" corruption.
22
+ //
23
+ // I3 NON-REGRESSING — the medium only ever moves to an equal-or-healthier state.
24
+ // A change that would make the code parse worse than it already does (or that
25
+ // silently clobbers another agent's edit) is refused, not landed. The shared
26
+ // state can never become more broken than it was.
27
+ //
28
+ // Everything language-specific (parse, structural merge, intent detection) is
29
+ // delegated to ICR (icr.js). Everything identity-specific (self-certifying keys,
30
+ // fingerprints) reuses the crypto model already proven in token.js. This file adds
31
+ // only the missing layer: provenance as a first-class, verifiable, chained TYPE on
32
+ // every change — the thing that turns "merge + coordinate" into a governed medium.
33
+
34
+ import crypto from 'crypto'
35
+ import { structuralMerge, supports, parses, languageFor } from './icr.js'
36
+ import { keyFingerprint } from './token.js'
37
+
38
+ // --- hashing / canonical encoding ----------------------------------------------
39
+ // A content hash names a state. Two identical texts hash identically on every peer,
40
+ // so a hash is a stable, portable pointer into the provenance DAG.
41
+ const sha = (s) => crypto.createHash('sha256').update(String(s), 'utf8').digest('base64url')
42
+
43
+ export const contentHash = (text) => sha(text == null ? '' : text)
44
+ export const GENESIS = 'genesis' // the parent of the very first change into an empty medium
45
+
46
+ // The exact bytes a change's signature commits to. Fixed field order (NOT JSON, whose
47
+ // key order is not guaranteed) so the signed message is identical on every peer. The
48
+ // signature binds WHO (author) + on-top-of-WHAT (parent) + WHY (intent) + WHAT
49
+ // (contentHash of the patch text) + WHEN (at). Change any one and the signature breaks.
50
+ function signingMessage(p) {
51
+ return [p.author, p.parent, p.intent || '', p.contentHash, p.at].join('\n')
52
+ }
53
+
54
+ // --- identity: self-certifying keypairs (Ed25519) ------------------------------
55
+ // An author IS the fingerprint of their public key (same anchoring token.js uses for
56
+ // secured rooms). You cannot claim to be another agent's id without their private key,
57
+ // and no key registry is needed — the key travels with the change and certifies itself.
58
+ export function genIdentity(name = '') {
59
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
60
+ const pk = publicKey.export({ type: 'spki', format: 'pem' }).toString()
61
+ const sk = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
62
+ return { name, pk, sk, id: keyFingerprint(pk) }
63
+ }
64
+
65
+ // --- authoring: turn a raw edit into a signed, provenance-typed CHANGE ----------
66
+ // `identity` is {pk, sk, id} from genIdentity (or a wrapped key). `base` is the text
67
+ // the agent edited FROM (its known ancestor); `text` is the result. `intent` is the
68
+ // declared reason (carried, verified, and used by ICR's semantic merge).
69
+ export function authorChange({ identity, filename, base, text, intent = '', at }) {
70
+ if (at == null) throw new Error('authorChange: `at` is required (pass a timestamp; the substrate never reads the clock itself so it stays deterministic/replayable)')
71
+ const cHash = contentHash(text)
72
+ const prov = {
73
+ author: identity.id,
74
+ pk: identity.pk,
75
+ parent: contentHash(base), // the state this change was made against
76
+ intent: String(intent || ''),
77
+ contentHash: cHash,
78
+ at,
79
+ }
80
+ const sig = crypto.sign(null, Buffer.from(signingMessage(prov)), crypto.createPrivateKey(identity.sk))
81
+ prov.sig = sig.toString('base64url')
82
+ return { filename, base, text, intent: prov.intent, prov }
83
+ }
84
+
85
+ // --- I1: verify a change's provenance ------------------------------------------
86
+ // Returns { ok, reason }. Checks, in order: the signature is valid for the carried
87
+ // public key; the author id is the fingerprint of that key (no impersonation); and
88
+ // the signed contentHash actually matches the patch text (no swapping the body after
89
+ // signing). A change that fails any of these is not trusted and is never applied.
90
+ export function verifyProvenance(change) {
91
+ const p = change && change.prov
92
+ if (!p || !p.author || !p.pk || !p.sig || !p.contentHash) return { ok: false, reason: 'missing provenance' }
93
+ if (keyFingerprint(p.pk) !== p.author) return { ok: false, reason: 'author id does not match public key (impersonation)' }
94
+ if (contentHash(change.text) !== p.contentHash) return { ok: false, reason: 'content hash does not match text (tampered body)' }
95
+ let valid = false
96
+ try { valid = crypto.verify(null, Buffer.from(signingMessage(p)), crypto.createPublicKey(p.pk), Buffer.from(p.sig, 'base64url')) }
97
+ catch { valid = false }
98
+ if (!valid) return { ok: false, reason: 'bad signature (forged or altered provenance)' }
99
+ return { ok: true }
100
+ }
101
+
102
+ // --- parse health: the metric non-regression is defined against -----------------
103
+ // 0 = parses; 1 = does not. (A coarse but honest, language-agnostic health score:
104
+ // the medium refuses to move from a parsing state to a non-parsing one.) For an
105
+ // unsupported language ICR can't judge structure, so we treat it as always-healthy
106
+ // and fall back to last-writer semantics — provenance still applies.
107
+ function health(text, filename) {
108
+ if (!supports(filename)) return 0
109
+ return parses(text, languageFor(filename)) ? 0 : 1
110
+ }
111
+ // Exported for the relay's content-authority check (non-regression on the head).
112
+ export function contentHealth(text, filename) { return health(text, filename) }
113
+
114
+ // --- authoritative head: validate the attested "current content" of a file ------
115
+ // A head is { text, hash, at, by?, receipt }. It is internally valid iff its receipt
116
+ // is a genuine signed provenance record AND it actually attests THIS text (hash ties
117
+ // the receipt to the content). This is the structural half of content authority; the
118
+ // relay adds non-regression (contentHealth) on top so the current content of a file is
119
+ // always both verified AND no more broken than it was.
120
+ export function headOk(head) {
121
+ if (!head || !head.receipt || typeof head.text !== 'string') return { ok: false, reason: 'malformed head' }
122
+ const h = contentHash(head.text)
123
+ if (head.hash !== h) return { ok: false, reason: 'head.hash does not match head.text' }
124
+ if (head.receipt.contentHash !== h) return { ok: false, reason: 'receipt does not attest this text' }
125
+ const v = verifyReceipt(head.receipt)
126
+ if (!v.ok) return { ok: false, reason: v.reason }
127
+ return { ok: true }
128
+ }
129
+
130
+ // --- an empty medium ------------------------------------------------------------
131
+ export function emptyState() {
132
+ return { text: null, hash: null, history: [] } // history = the provenance DAG (append-only)
133
+ }
134
+
135
+ // --- publish: the one operation, enforcing all three invariants -----------------
136
+ // publish(state, change) -> { accepted, state, receipt, reason }
137
+ // accepted:false -> the change is REFUSED; `state` is returned UNCHANGED (the medium
138
+ // never regresses), `reason` says which invariant stopped it.
139
+ // accepted:true -> `state` is the new state; `receipt` is the DAG node just linked.
140
+ //
141
+ // The function is PURE: same (state, change) -> same result on every peer. It reads no
142
+ // clock and no randomness (timestamps live in the signed change), so a peer can replay
143
+ // a change log and reach byte-identical state — the operational meaning of "convergent".
144
+ export function publish(state, change) {
145
+ state = state || emptyState()
146
+
147
+ // I1 — PROVENANCE. Refuse anything we can't attribute to a verified author + intent.
148
+ const v = verifyProvenance(change)
149
+ if (!v.ok) return { accepted: false, state, reason: 'I1 provenance: ' + v.reason }
150
+
151
+ const filename = change.filename || 'x.js'
152
+
153
+ // GENESIS — first change into an empty medium. Accept iff it is itself healthy; there
154
+ // is nothing to merge against, so convergence/non-regression are trivially satisfied.
155
+ if (state.text == null) {
156
+ if (health(change.text, filename) > 0) return { accepted: false, state, reason: 'I3 non-regression: genesis change does not parse' }
157
+ return commit(state, change, change.text, filename, 'genesis')
158
+ }
159
+
160
+ // FIXED POINT — the change reproduces the current state exactly. Nothing to do; the
161
+ // medium is already there. (Keeps re-publishes and echoes idempotent.)
162
+ if (change.text === state.text) return commit(state, change, state.text, filename, 'noop')
163
+
164
+ // I2 — CONVERGENCE via ICR's deterministic, symmetric 3-way merge:
165
+ // base = the ancestor the change was authored against; a = the medium's current
166
+ // text; b = the incoming change. ICR guarantees the merge is order-independent, so
167
+ // two peers merging the same changes converge on identical bytes.
168
+ const merged = structuralMerge(change.base, state.text, change.text, { filename, authors: { a: 'medium', b: change.prov.author } })
169
+
170
+ // I3 — NON-REGRESSION. Only 'auto' (a clean, parseable merge) may move the medium.
171
+ // 'semantic-conflict' — the change collides with the current state on the SAME unit;
172
+ // landing it would clobber another agent's edit. Refuse; the
173
+ // author must reconcile against the new parent and re-publish.
174
+ // 'fallback' — the merge would not parse / inputs unparseable. Refuse rather
175
+ // than let the shared state become more broken than it is.
176
+ if (merged.status !== 'auto') {
177
+ const reason = merged.status === 'semantic-conflict'
178
+ ? `I3 non-regression: semantic conflict on ${(merged.conflicts || []).join(', ') || 'a shared unit'} — reconcile against parent ${state.hash} and re-publish`
179
+ : `I3 non-regression: merge would not produce healthy code (${merged.reason || 'fallback'})`
180
+ return { accepted: false, state, reason, conflicts: merged.conflicts || [] }
181
+ }
182
+ // Defense in depth: the merged text must be at least as healthy as the current state.
183
+ if (health(merged.text, filename) > health(state.text, filename))
184
+ return { accepted: false, state, reason: 'I3 non-regression: merged result parses worse than current state' }
185
+
186
+ return commit(state, change, merged.text, filename, 'merge', merged)
187
+ }
188
+
189
+ // Link an accepted change into the provenance DAG and advance the state. The receipt is
190
+ // the audit record: it records the author/intent/parent from the signed change, the hash
191
+ // the medium actually arrived at, and how (genesis | noop | merge). The chain of receipts
192
+ // is independently verifiable end-to-end via verifyChain().
193
+ function commit(state, change, newText, filename, how, merged) {
194
+ const receipt = {
195
+ author: change.prov.author,
196
+ intent: change.prov.intent,
197
+ parent: change.prov.parent, // what the author built on
198
+ prevHash: state.hash || GENESIS, // the medium's state just before this commit
199
+ contentHash: change.prov.contentHash,
200
+ resultHash: contentHash(newText), // the hash the medium arrived at
201
+ at: change.prov.at,
202
+ sig: change.prov.sig,
203
+ pk: change.prov.pk,
204
+ how,
205
+ ...(merged && merged.renames && merged.renames.length ? { renames: merged.renames } : {}),
206
+ ...(merged && merged.provenance ? { units: merged.provenance } : {}),
207
+ }
208
+ return {
209
+ accepted: true,
210
+ receipt,
211
+ state: { text: newText, hash: receipt.resultHash, history: [...state.history, receipt] },
212
+ }
213
+ }
214
+
215
+ // --- audit: verify ONE receipt in isolation -------------------------------------
216
+ // A receipt is a flattened, self-contained provenance record (as stored in the live
217
+ // shared ledger). This proves its integrity WITHOUT the patch text: the signature
218
+ // commits to (author, parent, intent, contentHash, at), so a valid signature means
219
+ // "this verified author vouched for a change producing state <contentHash> with this
220
+ // intent at this time". Used to audit the live ledger, where concurrent writers make
221
+ // the history a signed DAG rather than a single line (so verifyChain's linear check
222
+ // doesn't apply, but per-receipt provenance still does — every entry is attributable).
223
+ export function verifyReceipt(r) {
224
+ if (!r || !r.pk || !r.author || !r.sig || !r.contentHash) return { ok: false, reason: 'missing provenance fields' }
225
+ if (keyFingerprint(r.pk) !== r.author) return { ok: false, reason: 'author id does not match public key' }
226
+ const msg = signingMessage({ author: r.author, parent: r.parent, intent: r.intent, contentHash: r.contentHash, at: r.at })
227
+ let valid = false
228
+ try { valid = crypto.verify(null, Buffer.from(msg), crypto.createPublicKey(r.pk), Buffer.from(r.sig, 'base64url')) } catch { valid = false }
229
+ return valid ? { ok: true } : { ok: false, reason: 'bad signature' }
230
+ }
231
+
232
+ // --- audit: verify an entire provenance DAG end-to-end --------------------------
233
+ // Re-checks every receipt's signature and identity, and that the chain is unbroken
234
+ // (each commit's prevHash equals the prior commit's resultHash). Returns
235
+ // { ok, length, broken? } — a full, independent audit of who authored every state the
236
+ // medium ever held. This is what makes "provenance-verified" true for HISTORY, not just
237
+ // the latest write: anyone can replay the chain and prove no state was ever unattributed.
238
+ export function verifyChain(history) {
239
+ if (!Array.isArray(history)) return { ok: false, reason: 'no history' }
240
+ let prev = GENESIS
241
+ for (let i = 0; i < history.length; i++) {
242
+ const r = history[i]
243
+ if (keyFingerprint(r.pk) !== r.author) return { ok: false, broken: i, reason: 'author id / key mismatch' }
244
+ const msg = signingMessage({ author: r.author, parent: r.parent, intent: r.intent, contentHash: r.contentHash, at: r.at })
245
+ let valid = false
246
+ try { valid = crypto.verify(null, Buffer.from(msg), crypto.createPublicKey(r.pk), Buffer.from(r.sig, 'base64url')) } catch { valid = false }
247
+ if (!valid) return { ok: false, broken: i, reason: 'bad signature' }
248
+ if (r.prevHash !== prev) return { ok: false, broken: i, reason: `chain break: prevHash ${r.prevHash} != ${prev}` }
249
+ prev = r.resultHash
250
+ }
251
+ return { ok: true, length: history.length }
252
+ }