@kashscript/hudhud 0.1.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.
Files changed (138) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/prisma.d.ts +170 -0
  4. package/dist/adapters/prisma.d.ts.map +1 -0
  5. package/dist/adapters/prisma.js +185 -0
  6. package/dist/adapters/prisma.js.map +1 -0
  7. package/dist/anchor/index.d.ts +55 -0
  8. package/dist/anchor/index.d.ts.map +1 -0
  9. package/dist/anchor/index.js +97 -0
  10. package/dist/anchor/index.js.map +1 -0
  11. package/dist/certificate/certificate.d.ts +115 -0
  12. package/dist/certificate/certificate.d.ts.map +1 -0
  13. package/dist/certificate/certificate.js +221 -0
  14. package/dist/certificate/certificate.js.map +1 -0
  15. package/dist/certificate/html.d.ts +10 -0
  16. package/dist/certificate/html.d.ts.map +1 -0
  17. package/dist/certificate/html.js +141 -0
  18. package/dist/certificate/html.js.map +1 -0
  19. package/dist/certificate/index.d.ts +3 -0
  20. package/dist/certificate/index.d.ts.map +1 -0
  21. package/dist/certificate/index.js +5 -0
  22. package/dist/certificate/index.js.map +1 -0
  23. package/dist/did-registry/index.d.ts +3 -0
  24. package/dist/did-registry/index.d.ts.map +1 -0
  25. package/dist/did-registry/index.js +10 -0
  26. package/dist/did-registry/index.js.map +1 -0
  27. package/dist/errors.d.ts +35 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +65 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/exception/index.d.ts +97 -0
  32. package/dist/exception/index.d.ts.map +1 -0
  33. package/dist/exception/index.js +144 -0
  34. package/dist/exception/index.js.map +1 -0
  35. package/dist/identity/index.d.ts +33 -0
  36. package/dist/identity/index.d.ts.map +1 -0
  37. package/dist/identity/index.js +36 -0
  38. package/dist/identity/index.js.map +1 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +32 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/ledger/event-schema.d.ts +97 -0
  44. package/dist/ledger/event-schema.d.ts.map +1 -0
  45. package/dist/ledger/event-schema.js +54 -0
  46. package/dist/ledger/event-schema.js.map +1 -0
  47. package/dist/ledger/index.d.ts +7 -0
  48. package/dist/ledger/index.d.ts.map +1 -0
  49. package/dist/ledger/index.js +14 -0
  50. package/dist/ledger/index.js.map +1 -0
  51. package/dist/ledger/mutex.d.ts +9 -0
  52. package/dist/ledger/mutex.d.ts.map +1 -0
  53. package/dist/ledger/mutex.js +36 -0
  54. package/dist/ledger/mutex.js.map +1 -0
  55. package/dist/ledger/store.d.ts +31 -0
  56. package/dist/ledger/store.d.ts.map +1 -0
  57. package/dist/ledger/store.js +66 -0
  58. package/dist/ledger/store.js.map +1 -0
  59. package/dist/ledger/writer.d.ts +69 -0
  60. package/dist/ledger/writer.d.ts.map +1 -0
  61. package/dist/ledger/writer.js +167 -0
  62. package/dist/ledger/writer.js.map +1 -0
  63. package/dist/mcp/allowlist.d.ts +53 -0
  64. package/dist/mcp/allowlist.d.ts.map +1 -0
  65. package/dist/mcp/allowlist.js +65 -0
  66. package/dist/mcp/allowlist.js.map +1 -0
  67. package/dist/mcp/client.d.ts +68 -0
  68. package/dist/mcp/client.d.ts.map +1 -0
  69. package/dist/mcp/client.js +198 -0
  70. package/dist/mcp/client.js.map +1 -0
  71. package/dist/mcp/index.d.ts +3 -0
  72. package/dist/mcp/index.d.ts.map +1 -0
  73. package/dist/mcp/index.js +5 -0
  74. package/dist/mcp/index.js.map +1 -0
  75. package/dist/privacy/index.d.ts +21 -0
  76. package/dist/privacy/index.d.ts.map +1 -0
  77. package/dist/privacy/index.js +96 -0
  78. package/dist/privacy/index.js.map +1 -0
  79. package/dist/runtime/dispatcher.d.ts +27 -0
  80. package/dist/runtime/dispatcher.d.ts.map +1 -0
  81. package/dist/runtime/dispatcher.js +66 -0
  82. package/dist/runtime/dispatcher.js.map +1 -0
  83. package/dist/runtime/escalation.d.ts +98 -0
  84. package/dist/runtime/escalation.d.ts.map +1 -0
  85. package/dist/runtime/escalation.js +188 -0
  86. package/dist/runtime/escalation.js.map +1 -0
  87. package/dist/runtime/index.d.ts +7 -0
  88. package/dist/runtime/index.d.ts.map +1 -0
  89. package/dist/runtime/index.js +9 -0
  90. package/dist/runtime/index.js.map +1 -0
  91. package/dist/runtime/runner.d.ts +78 -0
  92. package/dist/runtime/runner.d.ts.map +1 -0
  93. package/dist/runtime/runner.js +145 -0
  94. package/dist/runtime/runner.js.map +1 -0
  95. package/dist/scope/index.d.ts +19 -0
  96. package/dist/scope/index.d.ts.map +1 -0
  97. package/dist/scope/index.js +44 -0
  98. package/dist/scope/index.js.map +1 -0
  99. package/dist/sop/compiler.d.ts +36 -0
  100. package/dist/sop/compiler.d.ts.map +1 -0
  101. package/dist/sop/compiler.js +54 -0
  102. package/dist/sop/compiler.js.map +1 -0
  103. package/dist/sop/index.d.ts +3 -0
  104. package/dist/sop/index.d.ts.map +1 -0
  105. package/dist/sop/index.js +5 -0
  106. package/dist/sop/index.js.map +1 -0
  107. package/dist/sop/service.d.ts +40 -0
  108. package/dist/sop/service.d.ts.map +1 -0
  109. package/dist/sop/service.js +140 -0
  110. package/dist/sop/service.js.map +1 -0
  111. package/package.json +112 -0
  112. package/src/adapters/prisma.ts +310 -0
  113. package/src/anchor/index.ts +149 -0
  114. package/src/certificate/certificate.ts +340 -0
  115. package/src/certificate/html.ts +156 -0
  116. package/src/certificate/index.ts +4 -0
  117. package/src/did-registry/index.ts +11 -0
  118. package/src/errors.ts +79 -0
  119. package/src/exception/index.ts +244 -0
  120. package/src/identity/index.ts +82 -0
  121. package/src/index.ts +33 -0
  122. package/src/ledger/event-schema.ts +61 -0
  123. package/src/ledger/index.ts +22 -0
  124. package/src/ledger/mutex.ts +37 -0
  125. package/src/ledger/store.ts +86 -0
  126. package/src/ledger/writer.ts +249 -0
  127. package/src/mcp/allowlist.ts +102 -0
  128. package/src/mcp/client.ts +284 -0
  129. package/src/mcp/index.ts +4 -0
  130. package/src/privacy/index.ts +123 -0
  131. package/src/runtime/dispatcher.ts +96 -0
  132. package/src/runtime/escalation.ts +292 -0
  133. package/src/runtime/index.ts +14 -0
  134. package/src/runtime/runner.ts +227 -0
  135. package/src/scope/index.ts +59 -0
  136. package/src/sop/compiler.ts +99 -0
  137. package/src/sop/index.ts +4 -0
  138. package/src/sop/service.ts +184 -0
@@ -0,0 +1,244 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/exception — signed exception-classification dataset
3
+ // ----------------------------------------------------------------------------
4
+ // The appreciating record of WHERE frontier models fail on real inputs and HOW
5
+ // a human resolved it. Two invariants make it an asset, not a log line:
6
+ // 1. MANDATORY — `failureClass` is required with NO "unclassified" member and
7
+ // NO default; `assertFailureClass` is the runtime belt (an untyped caller
8
+ // still cannot resolve without classifying).
9
+ // 2. SIGNED — the operator's Ed25519 key signs the record over its EXACT
10
+ // bytes (`id` + `timestampIso` minted INTO the signed payload).
11
+ // Ported from the oreoasis-core-host engine (M5); the DST is neutralized, the
12
+ // error reuses hudhud's `ExceptionClassificationError`, id generation inlined.
13
+ // ============================================================================
14
+
15
+ import { signEvent, verifyEvent } from "@kashscript/identity-core";
16
+ import { ExceptionClassificationError } from "../errors";
17
+
18
+ /** The failure classes. No "unclassified": classification is mandatory. */
19
+ export const FAILURE_CLASSES = ["judgment", "gap-filling", "ambiguous"] as const;
20
+ export type FailureClass = (typeof FAILURE_CLASSES)[number];
21
+
22
+ /** DST so a classification signature can't replay as a ledger/attestation sig.
23
+ * Neutral default; a venture passes its own DST at swap for wire-compat. */
24
+ export const HUDHUD_EXCEPTION_CLASSIFICATION_DST = "kash-exception-classification-v1\n" as const;
25
+ export const EXCEPTION_CLASSIFICATION_SCHEMA_VERSION = "exception-classification/1" as const;
26
+ const SIGN_HEADER = { kind: "exception-classification/1" } as const;
27
+
28
+ let lastIdMicros = 0;
29
+ function newRecordId(): string {
30
+ let micros = Date.now() * 1000;
31
+ if (micros <= lastIdMicros) micros = lastIdMicros + 1;
32
+ lastIdMicros = micros;
33
+ const rand = crypto.getRandomValues(new Uint8Array(6));
34
+ let hex = "";
35
+ for (const b of rand) hex += b.toString(16).padStart(2, "0");
36
+ return `exc_${micros.toString(36)}_${hex}`;
37
+ }
38
+
39
+ /** The mandatory gate — throws `ExceptionClassificationError` if `value` is
40
+ * absent/empty or not one of the classes. NOT merely the TS type. */
41
+ export function assertFailureClass(value: unknown): asserts value is FailureClass {
42
+ if (value === undefined || value === null || value === "") {
43
+ throw new ExceptionClassificationError(
44
+ `FAILURE_CLASS_REQUIRED — an exception cannot be resolved without a failureClass (one of ${FAILURE_CLASSES.join(" | ")})`,
45
+ );
46
+ }
47
+ if (!(FAILURE_CLASSES as ReadonlyArray<string>).includes(value as string)) {
48
+ throw new ExceptionClassificationError(
49
+ `FAILURE_CLASS_INVALID — failureClass must be one of ${FAILURE_CLASSES.join(" | ")} (got ${String(value)})`,
50
+ );
51
+ }
52
+ }
53
+
54
+ export interface ExceptionClassificationInput {
55
+ readonly failureClass: FailureClass;
56
+ readonly failureTag: string;
57
+ readonly workflowId: string;
58
+ readonly sopVersion: string;
59
+ readonly inputRef: string;
60
+ readonly contentHash: string;
61
+ readonly model: string;
62
+ readonly modelVersion: string;
63
+ readonly resolutionSummary: string;
64
+ readonly resolutionActionRefs: ReadonlyArray<string>;
65
+ }
66
+
67
+ export interface ClassificationSignedEnvelope {
68
+ readonly alg: "ed25519";
69
+ readonly sig: string;
70
+ readonly dst: string;
71
+ readonly kid: string;
72
+ }
73
+
74
+ export interface ExceptionClassificationRecord {
75
+ readonly id: string;
76
+ readonly schemaVersion: string;
77
+ readonly timestampIso: string;
78
+ readonly workflowId: string;
79
+ readonly sopVersion: string;
80
+ readonly inputRef: string;
81
+ readonly contentHash: string;
82
+ readonly model: string;
83
+ readonly modelVersion: string;
84
+ readonly failureClass: FailureClass;
85
+ readonly failureTag: string;
86
+ readonly operatorDid: string;
87
+ readonly resolutionSummary: string;
88
+ readonly resolutionActionRefs: ReadonlyArray<string>;
89
+ readonly escalationId: string | null;
90
+ readonly signoffLedgerStepIndex: number | null;
91
+ readonly signedEnvelope: ClassificationSignedEnvelope;
92
+ }
93
+
94
+ type UnsignedClassification = Omit<ExceptionClassificationRecord, "signedEnvelope">;
95
+ function unsignedOf(record: ExceptionClassificationRecord): UnsignedClassification {
96
+ const { signedEnvelope: _drop, ...rest } = record;
97
+ return rest;
98
+ }
99
+
100
+ /** Mint + sign a classification record (id/timestamp minted into the signed
101
+ * payload). Throws the mandatory gate if `failureClass` is missing/invalid. */
102
+ export async function buildSignedClassification(
103
+ input: ExceptionClassificationInput,
104
+ operator: { readonly did: string; readonly privateKey: Uint8Array },
105
+ link: { readonly escalationId?: string | null; readonly signoffLedgerStepIndex?: number | null } = {},
106
+ ): Promise<ExceptionClassificationRecord> {
107
+ assertFailureClass(input.failureClass);
108
+ const unsigned: UnsignedClassification = {
109
+ id: newRecordId(),
110
+ schemaVersion: EXCEPTION_CLASSIFICATION_SCHEMA_VERSION,
111
+ timestampIso: new Date().toISOString(),
112
+ workflowId: input.workflowId,
113
+ sopVersion: input.sopVersion,
114
+ inputRef: input.inputRef,
115
+ contentHash: input.contentHash,
116
+ model: input.model,
117
+ modelVersion: input.modelVersion,
118
+ failureClass: input.failureClass,
119
+ failureTag: input.failureTag,
120
+ operatorDid: operator.did,
121
+ resolutionSummary: input.resolutionSummary,
122
+ resolutionActionRefs: [...input.resolutionActionRefs],
123
+ escalationId: link.escalationId ?? null,
124
+ signoffLedgerStepIndex: link.signoffLedgerStepIndex ?? null,
125
+ };
126
+ const signed = await signEvent(SIGN_HEADER, unsigned, operator.privateKey, {
127
+ dst: HUDHUD_EXCEPTION_CLASSIFICATION_DST,
128
+ encoding: "hex",
129
+ kid: operator.did,
130
+ });
131
+ return {
132
+ ...unsigned,
133
+ signedEnvelope: { alg: "ed25519", sig: signed.sig, dst: HUDHUD_EXCEPTION_CLASSIFICATION_DST, kid: operator.did },
134
+ };
135
+ }
136
+
137
+ /** Verify a record's operator signature. Returns false on a well-formed mismatch. */
138
+ export async function verifyClassificationSignature(
139
+ record: ExceptionClassificationRecord,
140
+ operatorPublicKey: Uint8Array,
141
+ ): Promise<boolean> {
142
+ return verifyEvent(
143
+ {
144
+ header: SIGN_HEADER,
145
+ body: unsignedOf(record),
146
+ signature: {
147
+ alg: "ed25519",
148
+ sig: record.signedEnvelope.sig,
149
+ dst: record.signedEnvelope.dst,
150
+ encoding: "hex",
151
+ kid: record.signedEnvelope.kid,
152
+ },
153
+ },
154
+ operatorPublicKey,
155
+ );
156
+ }
157
+
158
+ // ── Store (append-only) ────────────────────────────────────────────────────
159
+
160
+ export interface ExceptionClassificationStore {
161
+ append(record: ExceptionClassificationRecord): Promise<void>;
162
+ history(): Promise<ReadonlyArray<ExceptionClassificationRecord>>;
163
+ latest(): Promise<ExceptionClassificationRecord | null>;
164
+ byWorkflow(workflowId: string): Promise<ReadonlyArray<ExceptionClassificationRecord>>;
165
+ }
166
+
167
+ export class InMemoryExceptionClassificationStore implements ExceptionClassificationStore {
168
+ private readonly rows: ExceptionClassificationRecord[] = [];
169
+ async append(record: ExceptionClassificationRecord): Promise<void> {
170
+ this.rows.push(record);
171
+ }
172
+ async history(): Promise<ReadonlyArray<ExceptionClassificationRecord>> {
173
+ return [...this.rows];
174
+ }
175
+ async latest(): Promise<ExceptionClassificationRecord | null> {
176
+ return this.rows.length === 0 ? null : this.rows[this.rows.length - 1]!;
177
+ }
178
+ async byWorkflow(workflowId: string): Promise<ReadonlyArray<ExceptionClassificationRecord>> {
179
+ return this.rows.filter((r) => r.workflowId === workflowId);
180
+ }
181
+ }
182
+
183
+ // ── Export + stats ─────────────────────────────────────────────────────────
184
+
185
+ export const EXCEPTION_DATASET_FORMAT_VERSION = "kash-exception-dataset/1" as const;
186
+
187
+ export interface ExceptionClassificationDatasetExport {
188
+ readonly formatVersion: string;
189
+ readonly exportedAtIso: string;
190
+ readonly count: number;
191
+ readonly records: ReadonlyArray<ExceptionClassificationRecord>;
192
+ }
193
+
194
+ /** Bundle the dataset for hand-off (records carry their own signatures). */
195
+ export function exportExceptionClassificationDataset(
196
+ records: ReadonlyArray<ExceptionClassificationRecord>,
197
+ exportedAtIso: string = new Date().toISOString(),
198
+ ): ExceptionClassificationDatasetExport {
199
+ return { formatVersion: EXCEPTION_DATASET_FORMAT_VERSION, exportedAtIso, count: records.length, records: [...records] };
200
+ }
201
+
202
+ export interface WorkflowMonthCount {
203
+ readonly workflowId: string;
204
+ readonly month: string;
205
+ readonly count: number;
206
+ }
207
+
208
+ export interface ExceptionClassificationStats {
209
+ readonly total: number;
210
+ readonly classMix: Record<FailureClass, number>;
211
+ readonly perWorkflow: ReadonlyArray<{ readonly workflowId: string; readonly count: number }>;
212
+ readonly perWorkflowOverTime: ReadonlyArray<WorkflowMonthCount>;
213
+ }
214
+
215
+ function emptyClassMix(): Record<FailureClass, number> {
216
+ const mix = {} as Record<FailureClass, number>;
217
+ for (const c of FAILURE_CLASSES) mix[c] = 0;
218
+ return mix;
219
+ }
220
+
221
+ /** Class mix + per-workflow (+ over-time) exception counts (the rate numerator). */
222
+ export function computeExceptionClassificationStats(
223
+ records: ReadonlyArray<ExceptionClassificationRecord>,
224
+ ): ExceptionClassificationStats {
225
+ const classMix = emptyClassMix();
226
+ const perWorkflow = new Map<string, number>();
227
+ const perWfMonth = new Map<string, number>();
228
+ for (const r of records) {
229
+ classMix[r.failureClass] += 1;
230
+ perWorkflow.set(r.workflowId, (perWorkflow.get(r.workflowId) ?? 0) + 1);
231
+ const key = `${r.workflowId} ${r.timestampIso.slice(0, 7)}`;
232
+ perWfMonth.set(key, (perWfMonth.get(key) ?? 0) + 1);
233
+ }
234
+ const perWorkflowArr = [...perWorkflow.entries()]
235
+ .map(([workflowId, count]) => ({ workflowId, count }))
236
+ .sort((a, b) => (a.workflowId < b.workflowId ? -1 : 1));
237
+ const perWorkflowOverTime = [...perWfMonth.entries()]
238
+ .map(([key, count]) => {
239
+ const [workflowId, month] = key.split(" ");
240
+ return { workflowId: workflowId!, month: month!, count };
241
+ })
242
+ .sort((a, b) => (a.workflowId === b.workflowId ? (a.month < b.month ? -1 : 1) : a.workflowId < b.workflowId ? -1 : 1));
243
+ return { total: records.length, classMix, perWorkflow: perWorkflowArr, perWorkflowOverTime };
244
+ }
@@ -0,0 +1,82 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/identity — the accountable-identity gate (MCP OAuth 2.1)
3
+ // ----------------------------------------------------------------------------
4
+ // Replaces Oreoasis's bespoke HS256 / sovereign-session auth with
5
+ // `@kashscript/identity-server` agent tokens (RFC 9068 + 8707 + 8693): the
6
+ // runtime accepts an audience-bound, agent→owner-delegated access token, and
7
+ // extracts the accountable actor `{ owner, agent }` for the ledger. The bespoke
8
+ // JWT + revocation paths are deleted; revocation, if a venture needs it, is a
9
+ // venture-side store. `AUDIENCE_MISMATCH` covers the closeout-aud-revocation
10
+ // behaviour.
11
+ // ============================================================================
12
+
13
+ import { verifyAgentToken } from "@kashscript/identity-server";
14
+ import type { Jwks } from "@kashscript/identity-server";
15
+
16
+ /** The delegation the accountable identity carries: the agent acting for the owner. */
17
+ export interface AccountableActor {
18
+ /** The resource owner — a did:kash root (the token `sub`). */
19
+ readonly owner: string;
20
+ /** The delegated agent — a did:kash (the token `act.sub`). */
21
+ readonly agent: string;
22
+ }
23
+
24
+ export type AccountableIdentityReason =
25
+ | "MALFORMED_TOKEN"
26
+ | "MALFORMED_CLAIMS"
27
+ | "UNSUPPORTED_ALG"
28
+ | "UNSUPPORTED_TOKEN_TYPE"
29
+ | "KEY_NOT_FOUND"
30
+ | "SIGNATURE_INVALID"
31
+ | "EXPIRED"
32
+ | "NOT_YET_VALID"
33
+ | "ISSUER_MISMATCH"
34
+ | "AUDIENCE_MISMATCH"
35
+ | "MISSING_OWNER_BINDING";
36
+
37
+ export interface AccountableIdentityResult {
38
+ readonly ok: boolean;
39
+ readonly reason: AccountableIdentityReason | null;
40
+ /** The `{ owner, agent }` binding — present whenever the signature verified. */
41
+ readonly actor: AccountableActor | null;
42
+ /** The token's granted scope, if any. */
43
+ readonly scope: string | null;
44
+ }
45
+
46
+ export interface VerifyAccountableIdentityOptions {
47
+ /** RFC 8707: this runtime's canonical resource URI. REQUIRED. */
48
+ readonly expectedAudience: string;
49
+ readonly expectedIssuer?: string;
50
+ /** Resolve the signer key by `kid` from this JWKS (else self-certifying did:kash). */
51
+ readonly jwks?: Jwks;
52
+ readonly nowSec?: number;
53
+ readonly clockSkewSeconds?: number;
54
+ }
55
+
56
+ /**
57
+ * Verify an agent access token and extract the accountable actor. The runtime
58
+ * uses `actor.agent` as the ledger actor DID for autonomous actions and
59
+ * `actor.owner` as the delegating principal.
60
+ */
61
+ export async function verifyAccountableIdentity(
62
+ token: string,
63
+ opts: VerifyAccountableIdentityOptions,
64
+ ): Promise<AccountableIdentityResult> {
65
+ const res = await verifyAgentToken(token, {
66
+ expectedAudience: opts.expectedAudience,
67
+ ...(opts.expectedIssuer !== undefined ? { expectedIssuer: opts.expectedIssuer } : {}),
68
+ ...(opts.jwks !== undefined ? { jwks: opts.jwks } : {}),
69
+ ...(opts.nowSec !== undefined ? { nowSec: opts.nowSec } : {}),
70
+ ...(opts.clockSkewSeconds !== undefined ? { clockSkewSeconds: opts.clockSkewSeconds } : {}),
71
+ });
72
+
73
+ const scope = typeof res.claims?.scope === "string" ? res.claims.scope : null;
74
+ if (!res.ok) {
75
+ // audit r1 F6: never hand back a consumable actor from a FAILED verification.
76
+ // identity-server still surfaces `ownerBinding` on post-signature failures
77
+ // (expired / wrong-audience) for logging, but the runtime must not attribute
78
+ // ledger rows to a binding whose token is invalid — make misuse unrepresentable.
79
+ return { ok: false, reason: res.reason, actor: null, scope };
80
+ }
81
+ return { ok: true, reason: null, actor: res.ownerBinding, scope };
82
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud — the Agent Protocol (accountable agent runtime)
3
+ // ----------------------------------------------------------------------------
4
+ // The generic, venture-neutral engine lifted from the oreoasis-core-host (M5 of
5
+ // the Manifestation): SOP compilation, MCP governance (allowlist + circuit
6
+ // breaker + PII redaction), scope gating, a signed action ledger + chain anchor
7
+ // (on @kashscript/attest), exception classification, work certificates
8
+ // (AIUC-1/DSSE), a DID registry, an identity gate (identity-server agent
9
+ // tokens), and the neuron-backed execution runtime. SOP CONTENT, fixtures,
10
+ // routes, storage schemas, and text UIs stay venture-side (guard b).
11
+ //
12
+ // Subpath entries (`./errors`, `./sop`, `./mcp`, `./privacy`, `./scope`,
13
+ // `./ledger`, `./anchor`, `./exception`, `./certificate`, `./did-registry`,
14
+ // `./identity`, `./runtime`, `./adapters/prisma`) mirror these modules; this
15
+ // root barrel re-exports the whole non-adapter surface. The durable Prisma
16
+ // adapters are import-gated behind `@kashscript/hudhud/adapters/prisma`.
17
+ // ============================================================================
18
+
19
+ export * from "./errors";
20
+ export * from "./sop";
21
+ export * from "./privacy";
22
+ export * from "./mcp";
23
+ export * from "./scope";
24
+ export * from "./ledger";
25
+ export * from "./anchor";
26
+ export * from "./exception";
27
+ export * from "./certificate";
28
+ export * from "./did-registry";
29
+ export * from "./identity";
30
+ export * from "./runtime";
31
+
32
+ /** The hudhud protocol/package version (M5 initial cut). */
33
+ export const HUDHUD_VERSION = "0.1.0" as const;
@@ -0,0 +1,61 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/ledger — closed-vocabulary payloadMutation schema
3
+ // ----------------------------------------------------------------------------
4
+ // The ledger's forensic value depends on a CLOSED event vocabulary: an
5
+ // unconstrained payload would chain-hash any JSON. Every accepted
6
+ // payloadMutation validates against this discriminated union before hashing.
7
+ // Extending is APPEND-ONLY — never repurpose an existing `event` discriminator
8
+ // (audit pipelines key on it). Ported from the oreoasis-core-host engine (M5);
9
+ // the venture-specific error-code regex is relaxed to a neutral non-empty string.
10
+ // ============================================================================
11
+
12
+ import { z } from "zod";
13
+
14
+ const NonNegBigInt = z.union([z.bigint(), z.string().regex(/^\d+$/)]);
15
+
16
+ const SessionBootstrap = z.object({ event: z.literal("session.bootstrap"), sopId: z.string().min(1) });
17
+ const ToolInvoked = z.object({ event: z.literal("tool.invoked"), tool: z.string().min(1), durationMs: z.number().int().nonnegative() });
18
+ const ToolFailed = z.object({ event: z.literal("tool.failed"), tool: z.string().min(1), errorCode: z.string().min(1), errorMessage: z.string().min(1).optional() });
19
+ const BudgetExhausted = z.object({ event: z.literal("budget.exhausted"), spentPaise: NonNegBigInt });
20
+ const SessionSettled = z.object({ event: z.literal("session.settled"), totalSpentPaise: NonNegBigInt });
21
+ const SessionFailedAudit = z.object({ event: z.literal("session.failed_audit"), reason: z.string().min(1) });
22
+ const SessionProcessCrashed = z.object({ event: z.literal("session.process_crashed"), signal: z.string().min(1).optional() });
23
+ const CacheCheckpoint = z.object({
24
+ event: z.literal("cache.checkpoint"),
25
+ flushedAtIso: z.string().min(1),
26
+ lifecycleState: z.enum(["NOT_STARTED", "RUNNING", "SETTLED", "FAILED_AUDIT", "BUDGET_EXHAUSTED", "PROCESS_CRASHED"]).optional(),
27
+ computeBudgetPaise: NonNegBigInt.optional(),
28
+ slotKeyPrefix: z.string().min(1),
29
+ });
30
+
31
+ /** Empty mutation — the bare `{}` object is a valid Empty-track value. */
32
+ const EmptyMutation = z.object({}).strict();
33
+
34
+ /** Every accepted event type. Unknown `event` discriminators are rejected, as
35
+ * are well-formed objects missing `event` UNLESS they're the empty `{}`. */
36
+ export const LedgerEventSchema = z.union([
37
+ EmptyMutation,
38
+ z.discriminatedUnion("event", [
39
+ SessionBootstrap,
40
+ ToolInvoked,
41
+ ToolFailed,
42
+ BudgetExhausted,
43
+ SessionSettled,
44
+ SessionFailedAudit,
45
+ SessionProcessCrashed,
46
+ CacheCheckpoint,
47
+ ]),
48
+ ]);
49
+
50
+ export type LedgerEvent = z.infer<typeof LedgerEventSchema>;
51
+
52
+ export const KNOWN_LEDGER_EVENTS = [
53
+ "session.bootstrap",
54
+ "tool.invoked",
55
+ "tool.failed",
56
+ "budget.exhausted",
57
+ "session.settled",
58
+ "session.failed_audit",
59
+ "session.process_crashed",
60
+ "cache.checkpoint",
61
+ ] as const;
@@ -0,0 +1,22 @@
1
+ // @kashscript/hudhud/ledger — the signed action ledger (write side).
2
+ // The store + mutex + ForensicLedgerService live here; the row shapes, the
3
+ // Ed25519 sign/verify, the forensic scan, and the DID registry are attest's
4
+ // (M1 seed) and are re-exported so ventures reach the whole ledger surface
5
+ // through hudhud.
6
+
7
+ export * from "./store";
8
+ export * from "./mutex";
9
+ export * from "./writer";
10
+ export * from "./event-schema";
11
+
12
+ // Re-export attest's ledger crypto surface (seeded from the same Oreoasis files).
13
+ // The DID registry lives at `@kashscript/hudhud/did-registry` (not re-exported
14
+ // here, to avoid a same-name overlap in the top-level barrel).
15
+ export {
16
+ OREOASIS_LEDGER_DST,
17
+ LedgerSignatureError,
18
+ signLedgerBlock,
19
+ verifyLedgerSignature,
20
+ forensicConsistencyScan,
21
+ } from "@kashscript/attest";
22
+ export type { LedgerSignaturePayload, ForensicScanReport } from "@kashscript/attest";
@@ -0,0 +1,37 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/ledger — per-key async mutex
3
+ // ----------------------------------------------------------------------------
4
+ // Serializes async sections keyed on a string (the ledger sessionId), closing
5
+ // the resolvePreviousBlockHash → appendBlock race two concurrent appends on the
6
+ // same session would otherwise hit. Ported verbatim from the oreoasis-core-host
7
+ // engine (M5).
8
+ // ============================================================================
9
+
10
+ export class SessionMutex {
11
+ private readonly tails = new Map<string, Promise<unknown>>();
12
+
13
+ /** Run `fn` while holding the mutex for `key`. Other acquirers for the same
14
+ * key queue behind this call. */
15
+ public async run<T>(key: string, fn: () => Promise<T>): Promise<T> {
16
+ const prior = this.tails.get(key) ?? Promise.resolve();
17
+ let release: (value: void) => void = () => {};
18
+ const gate = new Promise<void>((resolve) => {
19
+ release = resolve;
20
+ });
21
+ this.tails.set(key, gate);
22
+ try {
23
+ await prior;
24
+ return await fn();
25
+ } finally {
26
+ if (this.tails.get(key) === gate) {
27
+ this.tails.delete(key);
28
+ }
29
+ release();
30
+ }
31
+ }
32
+
33
+ /** Number of distinct keys currently locked. */
34
+ public activeKeyCount(): number {
35
+ return this.tails.size;
36
+ }
37
+ }
@@ -0,0 +1,86 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/ledger — append-only ledger store (write side)
3
+ // ----------------------------------------------------------------------------
4
+ // The write-side store the ForensicLedgerService appends to. The row shapes +
5
+ // the sign/verify/forensic-scan primitives live in `@kashscript/attest` (M1
6
+ // seed); this module ships the store interface + an in-memory reference +
7
+ // `resolvePreviousBlockHash`. A Prisma adapter ships at `./adapters/prisma`.
8
+ // Ported from the oreoasis-core-host engine (M5).
9
+ // ============================================================================
10
+
11
+ import { GENESIS_PREV_HASH } from "@kashscript/attest";
12
+ import type { LedgerBlockInput, LedgerBlockRecord } from "@kashscript/attest";
13
+
14
+ export type { LedgerBlockInput, LedgerBlockRecord };
15
+
16
+ export interface LedgerStore {
17
+ appendBlock(input: LedgerBlockInput): Promise<LedgerBlockRecord>;
18
+ latestBlockForSession(sessionId: string): Promise<LedgerBlockRecord | null>;
19
+ countBlocksForSession(sessionId: string): Promise<number>;
20
+ /** Every row for `sessionId` ordered by stepIndex — MUST NOT skip rows (the
21
+ * forensic scan's whole job is to detect missing/forged entries). */
22
+ listBlocksForSession(sessionId: string): Promise<ReadonlyArray<LedgerBlockRecord>>;
23
+ }
24
+
25
+ /** In-memory store keyed by sessionId. Concurrent same-session appends are
26
+ * serialized by the JS event loop + the (sessionId, stepIndex) uniqueness check. */
27
+ export class InMemoryLedgerStore implements LedgerStore {
28
+ private readonly bySession = new Map<string, LedgerBlockRecord[]>();
29
+ private nextLocalId = 1;
30
+
31
+ async appendBlock(input: LedgerBlockInput): Promise<LedgerBlockRecord> {
32
+ const existing = this.bySession.get(input.sessionId) ?? [];
33
+ if (existing.some((b) => b.stepIndex === input.stepIndex)) {
34
+ throw new Error(`LEDGER_DUPLICATE_STEP: (sessionId=${input.sessionId}, stepIndex=${input.stepIndex})`);
35
+ }
36
+ const record: LedgerBlockRecord = {
37
+ ...input,
38
+ payloadMutation: { ...input.payloadMutation },
39
+ emittedAt: new Date().toISOString(),
40
+ localId: this.nextLocalId++,
41
+ };
42
+ existing.push(record);
43
+ existing.sort((a, b) => a.stepIndex - b.stepIndex);
44
+ this.bySession.set(input.sessionId, existing);
45
+ return record;
46
+ }
47
+
48
+ async latestBlockForSession(sessionId: string): Promise<LedgerBlockRecord | null> {
49
+ const list = this.bySession.get(sessionId);
50
+ if (!list || list.length === 0) return null;
51
+ return list[list.length - 1]!;
52
+ }
53
+
54
+ async countBlocksForSession(sessionId: string): Promise<number> {
55
+ return (this.bySession.get(sessionId) ?? []).length;
56
+ }
57
+
58
+ async listBlocksForSession(sessionId: string): Promise<ReadonlyArray<LedgerBlockRecord>> {
59
+ const list = this.bySession.get(sessionId);
60
+ if (list === undefined) return [];
61
+ return list.map((row) => ({ ...row, payloadMutation: { ...row.payloadMutation } }));
62
+ }
63
+
64
+ /** TEST-ONLY: splice a forged row bypassing the uniqueness + ordering
65
+ * invariants, so the forensic scan can be exercised against a corrupt chain.
66
+ * Production callers MUST NOT use this. */
67
+ unsafeInsertForgedRow(record: LedgerBlockRecord): void {
68
+ const existing = this.bySession.get(record.sessionId) ?? [];
69
+ existing.push({ ...record, payloadMutation: { ...record.payloadMutation } });
70
+ existing.sort((a, b) => a.stepIndex - b.stepIndex);
71
+ this.bySession.set(record.sessionId, existing);
72
+ }
73
+ }
74
+
75
+ /** Resolve the previous block hash for the next insert into `sessionId`.
76
+ * Genesis (stepIndex 0) → GENESIS_PREV_HASH; else the prior row's currentHash. */
77
+ export async function resolvePreviousBlockHash(
78
+ store: LedgerStore,
79
+ sessionId: string,
80
+ ): Promise<{ previousBlockHash: string; nextStepIndex: number }> {
81
+ const latest = await store.latestBlockForSession(sessionId);
82
+ if (latest === null) {
83
+ return { previousBlockHash: GENESIS_PREV_HASH, nextStepIndex: 0 };
84
+ }
85
+ return { previousBlockHash: latest.currentTransactionBlockHash, nextStepIndex: latest.stepIndex + 1 };
86
+ }