@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.
- package/LICENSE +133 -0
- package/README.md +67 -0
- package/dist/adapters/prisma.d.ts +170 -0
- package/dist/adapters/prisma.d.ts.map +1 -0
- package/dist/adapters/prisma.js +185 -0
- package/dist/adapters/prisma.js.map +1 -0
- package/dist/anchor/index.d.ts +55 -0
- package/dist/anchor/index.d.ts.map +1 -0
- package/dist/anchor/index.js +97 -0
- package/dist/anchor/index.js.map +1 -0
- package/dist/certificate/certificate.d.ts +115 -0
- package/dist/certificate/certificate.d.ts.map +1 -0
- package/dist/certificate/certificate.js +221 -0
- package/dist/certificate/certificate.js.map +1 -0
- package/dist/certificate/html.d.ts +10 -0
- package/dist/certificate/html.d.ts.map +1 -0
- package/dist/certificate/html.js +141 -0
- package/dist/certificate/html.js.map +1 -0
- package/dist/certificate/index.d.ts +3 -0
- package/dist/certificate/index.d.ts.map +1 -0
- package/dist/certificate/index.js +5 -0
- package/dist/certificate/index.js.map +1 -0
- package/dist/did-registry/index.d.ts +3 -0
- package/dist/did-registry/index.d.ts.map +1 -0
- package/dist/did-registry/index.js +10 -0
- package/dist/did-registry/index.js.map +1 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +65 -0
- package/dist/errors.js.map +1 -0
- package/dist/exception/index.d.ts +97 -0
- package/dist/exception/index.d.ts.map +1 -0
- package/dist/exception/index.js +144 -0
- package/dist/exception/index.js.map +1 -0
- package/dist/identity/index.d.ts +33 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +36 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger/event-schema.d.ts +97 -0
- package/dist/ledger/event-schema.d.ts.map +1 -0
- package/dist/ledger/event-schema.js +54 -0
- package/dist/ledger/event-schema.js.map +1 -0
- package/dist/ledger/index.d.ts +7 -0
- package/dist/ledger/index.d.ts.map +1 -0
- package/dist/ledger/index.js +14 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/mutex.d.ts +9 -0
- package/dist/ledger/mutex.d.ts.map +1 -0
- package/dist/ledger/mutex.js +36 -0
- package/dist/ledger/mutex.js.map +1 -0
- package/dist/ledger/store.d.ts +31 -0
- package/dist/ledger/store.d.ts.map +1 -0
- package/dist/ledger/store.js +66 -0
- package/dist/ledger/store.js.map +1 -0
- package/dist/ledger/writer.d.ts +69 -0
- package/dist/ledger/writer.d.ts.map +1 -0
- package/dist/ledger/writer.js +167 -0
- package/dist/ledger/writer.js.map +1 -0
- package/dist/mcp/allowlist.d.ts +53 -0
- package/dist/mcp/allowlist.d.ts.map +1 -0
- package/dist/mcp/allowlist.js +65 -0
- package/dist/mcp/allowlist.js.map +1 -0
- package/dist/mcp/client.d.ts +68 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +198 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/privacy/index.d.ts +21 -0
- package/dist/privacy/index.d.ts.map +1 -0
- package/dist/privacy/index.js +96 -0
- package/dist/privacy/index.js.map +1 -0
- package/dist/runtime/dispatcher.d.ts +27 -0
- package/dist/runtime/dispatcher.d.ts.map +1 -0
- package/dist/runtime/dispatcher.js +66 -0
- package/dist/runtime/dispatcher.js.map +1 -0
- package/dist/runtime/escalation.d.ts +98 -0
- package/dist/runtime/escalation.d.ts.map +1 -0
- package/dist/runtime/escalation.js +188 -0
- package/dist/runtime/escalation.js.map +1 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/runner.d.ts +78 -0
- package/dist/runtime/runner.d.ts.map +1 -0
- package/dist/runtime/runner.js +145 -0
- package/dist/runtime/runner.js.map +1 -0
- package/dist/scope/index.d.ts +19 -0
- package/dist/scope/index.d.ts.map +1 -0
- package/dist/scope/index.js +44 -0
- package/dist/scope/index.js.map +1 -0
- package/dist/sop/compiler.d.ts +36 -0
- package/dist/sop/compiler.d.ts.map +1 -0
- package/dist/sop/compiler.js +54 -0
- package/dist/sop/compiler.js.map +1 -0
- package/dist/sop/index.d.ts +3 -0
- package/dist/sop/index.d.ts.map +1 -0
- package/dist/sop/index.js +5 -0
- package/dist/sop/index.js.map +1 -0
- package/dist/sop/service.d.ts +40 -0
- package/dist/sop/service.d.ts.map +1 -0
- package/dist/sop/service.js +140 -0
- package/dist/sop/service.js.map +1 -0
- package/package.json +112 -0
- package/src/adapters/prisma.ts +310 -0
- package/src/anchor/index.ts +149 -0
- package/src/certificate/certificate.ts +340 -0
- package/src/certificate/html.ts +156 -0
- package/src/certificate/index.ts +4 -0
- package/src/did-registry/index.ts +11 -0
- package/src/errors.ts +79 -0
- package/src/exception/index.ts +244 -0
- package/src/identity/index.ts +82 -0
- package/src/index.ts +33 -0
- package/src/ledger/event-schema.ts +61 -0
- package/src/ledger/index.ts +22 -0
- package/src/ledger/mutex.ts +37 -0
- package/src/ledger/store.ts +86 -0
- package/src/ledger/writer.ts +249 -0
- package/src/mcp/allowlist.ts +102 -0
- package/src/mcp/client.ts +284 -0
- package/src/mcp/index.ts +4 -0
- package/src/privacy/index.ts +123 -0
- package/src/runtime/dispatcher.ts +96 -0
- package/src/runtime/escalation.ts +292 -0
- package/src/runtime/index.ts +14 -0
- package/src/runtime/runner.ts +227 -0
- package/src/scope/index.ts +59 -0
- package/src/sop/compiler.ts +99 -0
- package/src/sop/index.ts +4 -0
- package/src/sop/service.ts +184 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kashscript/hudhud/certificate โ the Work Certificate (AIUC-1, DSSE-signed)
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// A monthly, per-client, SIGNED document rendered from the signed ledger โ the
|
|
5
|
+
// billing unit (it enumerates the warranted outcomes) and the retention
|
|
6
|
+
// artifact. Everything DERIVES from the ledger: each CertifiedWorkRecord carries
|
|
7
|
+
// its evidence (chain-tip hash, sign-off row, exception classification, anchor);
|
|
8
|
+
// the certificate aggregates the counts and a third party RE-DERIVES them
|
|
9
|
+
// (verifyWorkCertificate). Sections map onto the six AIUC-1 control domains
|
|
10
|
+
// (web-verified, not invented). DSSE via `@kashscript/attest`. Ported from the
|
|
11
|
+
// oreoasis-core-host engine (M5); format/payload identifiers neutralized.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
import { signEnvelope, verifyEnvelope } from "@kashscript/attest";
|
|
15
|
+
import type { DsseEnvelope, DsseSigner } from "@kashscript/attest";
|
|
16
|
+
|
|
17
|
+
import type { AnchorRecord } from "../anchor";
|
|
18
|
+
import { FAILURE_CLASSES } from "../exception";
|
|
19
|
+
import type { ExceptionClassificationRecord, FailureClass } from "../exception";
|
|
20
|
+
|
|
21
|
+
export const WORK_CERTIFICATE_FORMAT_VERSION = "kash-work-certificate/1" as const;
|
|
22
|
+
export const WORK_CERTIFICATE_PAYLOAD_TYPE = "application/vnd.kash.work-certificate+json" as const;
|
|
23
|
+
|
|
24
|
+
/** The AIUC-1 standard's canonical reference (web-verified). */
|
|
25
|
+
export const AIUC1_SOURCE = "https://www.aiuc-1.com/" as const;
|
|
26
|
+
|
|
27
|
+
export interface Aiuc1Domain {
|
|
28
|
+
readonly code: "A" | "B" | "C" | "D" | "E" | "F";
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly covers: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const AIUC1_DOMAINS: ReadonlyArray<Aiuc1Domain> = [
|
|
34
|
+
{ code: "A", name: "Data & Privacy", covers: "Customer-data policy, access controls, and safeguards against data leakage / PII exposure / IP infringement / unauthorized training." },
|
|
35
|
+
{ code: "B", name: "Security", covers: "Adversarial-robustness testing, input filtering, access-privilege enforcement, deployment security, and output-exposure limits." },
|
|
36
|
+
{ code: "C", name: "Safety", covers: "Risk-taxonomy definition, harmful/out-of-scope output prevention, human-review flagging, and third-party testing." },
|
|
37
|
+
{ code: "D", name: "Reliability", covers: "Preventing hallucinated outputs and unsafe tool calls." },
|
|
38
|
+
{ code: "E", name: "Accountability", covers: "Failure-response plans, access monitoring, regulatory compliance, quality management, activity logging, and transparency." },
|
|
39
|
+
{ code: "F", name: "Society", covers: "Preventing AI cyber-misuse and catastrophic-misuse scenarios." },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export type WorkOutcomeStatus = "delivered_autonomous" | "delivered_with_signoff" | "declined";
|
|
43
|
+
|
|
44
|
+
export interface CertifiedWorkRecord {
|
|
45
|
+
readonly workItemId: string;
|
|
46
|
+
readonly sessionId: string;
|
|
47
|
+
readonly workflowId: string;
|
|
48
|
+
readonly workflowLabel?: string;
|
|
49
|
+
readonly status: WorkOutcomeStatus;
|
|
50
|
+
readonly slaDeadlineIso: string;
|
|
51
|
+
readonly completedAtIso: string;
|
|
52
|
+
readonly slaMet: boolean;
|
|
53
|
+
readonly ledgerRowCount: number;
|
|
54
|
+
readonly ledgerTipHash: string;
|
|
55
|
+
readonly signoff: { readonly operatorDid: string; readonly ledgerStepIndex: number } | null;
|
|
56
|
+
readonly classification: ExceptionClassificationRecord | null;
|
|
57
|
+
readonly anchor: AnchorRecord | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WorkCertificatePeriod {
|
|
61
|
+
readonly clientDid: string;
|
|
62
|
+
readonly clientName: string;
|
|
63
|
+
readonly month: string;
|
|
64
|
+
readonly fromIso: string;
|
|
65
|
+
readonly toIso: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BuildWorkCertificateInput {
|
|
69
|
+
readonly period: WorkCertificatePeriod;
|
|
70
|
+
readonly issuerDid: string;
|
|
71
|
+
readonly records: ReadonlyArray<CertifiedWorkRecord>;
|
|
72
|
+
readonly slaTargetPct?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WorkCertificateSummary {
|
|
76
|
+
readonly workVolumeDelivered: number;
|
|
77
|
+
readonly workVolumeDeclined: number;
|
|
78
|
+
readonly automationRatePct: number;
|
|
79
|
+
readonly exceptionCount: number;
|
|
80
|
+
readonly exceptionsByClass: Record<FailureClass, number>;
|
|
81
|
+
readonly humanSignoffCount: number;
|
|
82
|
+
readonly slaAdherencePct: number;
|
|
83
|
+
readonly slaTargetPct: number | null;
|
|
84
|
+
readonly anchoredCount: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Aiuc1DomainCoverage extends Aiuc1Domain {
|
|
88
|
+
readonly evidence: ReadonlyArray<string>;
|
|
89
|
+
readonly status: "evidenced" | "partial";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CertifiedOutcome {
|
|
93
|
+
readonly workItemId: string;
|
|
94
|
+
readonly workflowId: string;
|
|
95
|
+
readonly status: WorkOutcomeStatus;
|
|
96
|
+
readonly slaMet: boolean;
|
|
97
|
+
readonly completedAtIso: string;
|
|
98
|
+
readonly ledgerTipHash: string;
|
|
99
|
+
readonly signoffLedgerStepIndex: number | null;
|
|
100
|
+
readonly classificationId: string | null;
|
|
101
|
+
readonly failureClass: FailureClass | null;
|
|
102
|
+
readonly anchorMerkleRoot: string | null;
|
|
103
|
+
readonly anchorWitnessBackend: string | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface WorkCertificateAnchorRef {
|
|
107
|
+
readonly merkleRoot: string;
|
|
108
|
+
readonly witnessBackend: string;
|
|
109
|
+
readonly witnessedAtIso: string | null;
|
|
110
|
+
readonly verifyWith: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface WorkCertificateBody {
|
|
114
|
+
readonly formatVersion: string;
|
|
115
|
+
readonly standard: { readonly name: "AIUC-1"; readonly source: string };
|
|
116
|
+
readonly period: WorkCertificatePeriod;
|
|
117
|
+
readonly issuerDid: string;
|
|
118
|
+
readonly issuedAtIso: string;
|
|
119
|
+
readonly summary: WorkCertificateSummary;
|
|
120
|
+
readonly aiuc1Coverage: ReadonlyArray<Aiuc1DomainCoverage>;
|
|
121
|
+
readonly outcomes: ReadonlyArray<CertifiedOutcome>;
|
|
122
|
+
readonly anchors: ReadonlyArray<WorkCertificateAnchorRef>;
|
|
123
|
+
readonly verification: { readonly instructions: string };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface WorkCertificate {
|
|
127
|
+
readonly body: WorkCertificateBody;
|
|
128
|
+
readonly envelope: DsseEnvelope;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function round1(n: number): number {
|
|
132
|
+
return Math.round(n * 10) / 10;
|
|
133
|
+
}
|
|
134
|
+
function emptyClassMix(): Record<FailureClass, number> {
|
|
135
|
+
const mix = {} as Record<FailureClass, number>;
|
|
136
|
+
for (const c of FAILURE_CLASSES) mix[c] = 0;
|
|
137
|
+
return mix;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function summarize(records: ReadonlyArray<CertifiedWorkRecord>, slaTargetPct: number | undefined): WorkCertificateSummary {
|
|
141
|
+
let autonomous = 0;
|
|
142
|
+
let withSignoff = 0;
|
|
143
|
+
let declined = 0;
|
|
144
|
+
let slaMet = 0;
|
|
145
|
+
let anchored = 0;
|
|
146
|
+
const exceptionsByClass = emptyClassMix();
|
|
147
|
+
for (const r of records) {
|
|
148
|
+
if (r.status === "delivered_autonomous") autonomous += 1;
|
|
149
|
+
else if (r.status === "delivered_with_signoff") withSignoff += 1;
|
|
150
|
+
else declined += 1;
|
|
151
|
+
if (r.slaMet) slaMet += 1;
|
|
152
|
+
if (r.anchor !== null) anchored += 1;
|
|
153
|
+
if (r.classification !== null) exceptionsByClass[r.classification.failureClass] += 1;
|
|
154
|
+
}
|
|
155
|
+
const delivered = autonomous + withSignoff;
|
|
156
|
+
const total = records.length;
|
|
157
|
+
return {
|
|
158
|
+
workVolumeDelivered: delivered,
|
|
159
|
+
workVolumeDeclined: declined,
|
|
160
|
+
automationRatePct: delivered === 0 ? 0 : round1((autonomous / delivered) * 100),
|
|
161
|
+
exceptionCount: records.filter((r) => r.classification !== null).length,
|
|
162
|
+
exceptionsByClass,
|
|
163
|
+
humanSignoffCount: records.filter((r) => r.signoff !== null).length,
|
|
164
|
+
slaAdherencePct: total === 0 ? 0 : round1((slaMet / total) * 100),
|
|
165
|
+
slaTargetPct: slaTargetPct ?? null,
|
|
166
|
+
anchoredCount: anchored,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mapAiuc1Coverage(records: ReadonlyArray<CertifiedWorkRecord>, summary: WorkCertificateSummary): ReadonlyArray<Aiuc1DomainCoverage> {
|
|
171
|
+
const anchoredEvidence =
|
|
172
|
+
summary.anchoredCount > 0
|
|
173
|
+
? `${summary.anchoredCount} of ${records.length} chain tips anchored to a public transparency log (Rekor / OpenTimestamps) โ existence provable to a third party.`
|
|
174
|
+
: "Ledger is tamper-evident and independently exportable; public-witness anchoring available (none attached this period).";
|
|
175
|
+
return AIUC1_DOMAINS.map((d): Aiuc1DomainCoverage => {
|
|
176
|
+
switch (d.code) {
|
|
177
|
+
case "A":
|
|
178
|
+
return { ...d, status: "evidenced", evidence: ["PII/secret redaction runs before every model call (privacy interceptor); category tags โ never raw values โ enter the signed ledger."] };
|
|
179
|
+
case "B":
|
|
180
|
+
return { ...d, status: "evidenced", evidence: [`Every action Ed25519-signed into an append-only, hash-chained ledger (${records.reduce((n, r) => n + r.ledgerRowCount, 0)} signed rows this period).`, anchoredEvidence] };
|
|
181
|
+
case "C":
|
|
182
|
+
return { ...d, status: "evidenced", evidence: [`${summary.exceptionCount} exception(s) flagged for human review and resolved this period; each classified under a fixed risk taxonomy (judgment / gap-filling / ambiguous).`, "Restricted actions suspend for human judgment rather than proceed autonomously."] };
|
|
183
|
+
case "D":
|
|
184
|
+
return { ...d, status: "evidenced", evidence: ["Tools restricted to a per-provider allowlist (scope-gating): an out-of-allowlist or restricted tool call cannot be dispatched by construction.", `Automation rate this period: ${summary.automationRatePct}% delivered without human intervention.`] };
|
|
185
|
+
case "E":
|
|
186
|
+
return { ...d, status: "evidenced", evidence: ["Complete signed activity log (the ledger) for every work item; a self-contained audit bundle is independently verifiable offline.", `${summary.humanSignoffCount} human sign-off(s), each a SECOND-key signed ledger row (the host cannot forge human judgment).`] };
|
|
187
|
+
case "F":
|
|
188
|
+
return { ...d, status: "partial", evidence: ["Money-moving / restricted tools require a two-key human sign-off; no autonomous execution of restricted actions in this period's ledger."] };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function toOutcome(r: CertifiedWorkRecord): CertifiedOutcome {
|
|
194
|
+
return {
|
|
195
|
+
workItemId: r.workItemId,
|
|
196
|
+
workflowId: r.workflowId,
|
|
197
|
+
status: r.status,
|
|
198
|
+
slaMet: r.slaMet,
|
|
199
|
+
completedAtIso: r.completedAtIso,
|
|
200
|
+
ledgerTipHash: r.ledgerTipHash,
|
|
201
|
+
signoffLedgerStepIndex: r.signoff?.ledgerStepIndex ?? null,
|
|
202
|
+
classificationId: r.classification?.id ?? null,
|
|
203
|
+
failureClass: r.classification?.failureClass ?? null,
|
|
204
|
+
anchorMerkleRoot: r.anchor?.merkleRoot ?? null,
|
|
205
|
+
anchorWitnessBackend: r.anchor?.witness?.backend ?? null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collectAnchors(records: ReadonlyArray<CertifiedWorkRecord>): ReadonlyArray<WorkCertificateAnchorRef> {
|
|
210
|
+
const seen = new Set<string>();
|
|
211
|
+
const out: WorkCertificateAnchorRef[] = [];
|
|
212
|
+
for (const r of records) {
|
|
213
|
+
const a = r.anchor;
|
|
214
|
+
if (a === null || seen.has(a.merkleRoot)) continue;
|
|
215
|
+
seen.add(a.merkleRoot);
|
|
216
|
+
const backend = a.witness?.backend ?? "none";
|
|
217
|
+
out.push({
|
|
218
|
+
merkleRoot: a.merkleRoot,
|
|
219
|
+
witnessBackend: backend,
|
|
220
|
+
witnessedAtIso: a.witness?.witnessedAt ?? null,
|
|
221
|
+
verifyWith:
|
|
222
|
+
backend === "rekor"
|
|
223
|
+
? "rekor-cli get --uuid <uuid in the anchor receipt>"
|
|
224
|
+
: backend === "opentimestamps"
|
|
225
|
+
? "ots upgrade <proof>.ots && ots verify <proof>.ots"
|
|
226
|
+
: "no public witness attached (locally tamper-evident only)",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const VERIFY_INSTRUCTIONS =
|
|
233
|
+
"This certificate is a DSSE envelope signed by the issuer's Ed25519 key. " +
|
|
234
|
+
"To verify: (1) check the envelope signature against the issuer DID's published key; " +
|
|
235
|
+
"(2) re-derive every summary count from the `outcomes` array (they must match `summary`); " +
|
|
236
|
+
"(3) for each outcome, re-verify the underlying audit bundle (chain + signatures) with the shipped verifier, " +
|
|
237
|
+
"and confirm the anchor with public tooling per each entry in `anchors`. Counts derive from the signed ledger โ " +
|
|
238
|
+
"there is no separate metering system to trust.";
|
|
239
|
+
|
|
240
|
+
/** Build + DSSE-sign a Work Certificate from a period's ledger-derived records. */
|
|
241
|
+
export async function buildWorkCertificate(
|
|
242
|
+
input: BuildWorkCertificateInput,
|
|
243
|
+
issuer: DsseSigner,
|
|
244
|
+
issuedAtIso: string = new Date().toISOString(),
|
|
245
|
+
): Promise<WorkCertificate> {
|
|
246
|
+
const summary = summarize(input.records, input.slaTargetPct);
|
|
247
|
+
const body: WorkCertificateBody = {
|
|
248
|
+
formatVersion: WORK_CERTIFICATE_FORMAT_VERSION,
|
|
249
|
+
standard: { name: "AIUC-1", source: AIUC1_SOURCE },
|
|
250
|
+
period: input.period,
|
|
251
|
+
issuerDid: input.issuerDid,
|
|
252
|
+
issuedAtIso,
|
|
253
|
+
summary,
|
|
254
|
+
aiuc1Coverage: mapAiuc1Coverage(input.records, summary),
|
|
255
|
+
outcomes: input.records.map(toOutcome),
|
|
256
|
+
anchors: collectAnchors(input.records),
|
|
257
|
+
verification: { instructions: VERIFY_INSTRUCTIONS },
|
|
258
|
+
};
|
|
259
|
+
const envelope = await signEnvelope(body, WORK_CERTIFICATE_PAYLOAD_TYPE, issuer);
|
|
260
|
+
return { body, envelope };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface WorkCertificateVerifyReport {
|
|
264
|
+
readonly ok: boolean;
|
|
265
|
+
readonly signatureOk: boolean;
|
|
266
|
+
readonly countsRecomputeOk: boolean;
|
|
267
|
+
readonly mismatches: ReadonlyArray<string>;
|
|
268
|
+
readonly body: WorkCertificateBody | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function recomputeCountsMatch(body: WorkCertificateBody): ReadonlyArray<string> {
|
|
272
|
+
const mismatches: string[] = [];
|
|
273
|
+
const o = body.outcomes;
|
|
274
|
+
const delivered = o.filter((x) => x.status !== "declined").length;
|
|
275
|
+
const declined = o.filter((x) => x.status === "declined").length;
|
|
276
|
+
const autonomous = o.filter((x) => x.status === "delivered_autonomous").length;
|
|
277
|
+
const slaMet = o.filter((x) => x.slaMet).length;
|
|
278
|
+
const exceptionCount = o.filter((x) => x.classificationId !== null).length;
|
|
279
|
+
const signoffCount = o.filter((x) => x.signoffLedgerStepIndex !== null).length;
|
|
280
|
+
const anchored = o.filter((x) => x.anchorMerkleRoot !== null).length;
|
|
281
|
+
const total = o.length;
|
|
282
|
+
const s = body.summary;
|
|
283
|
+
const check = (label: string, got: number, want: number): void => {
|
|
284
|
+
if (got !== want) mismatches.push(`${label}: outcomes=${got} vs summary=${want}`);
|
|
285
|
+
};
|
|
286
|
+
check("workVolumeDelivered", delivered, s.workVolumeDelivered);
|
|
287
|
+
check("workVolumeDeclined", declined, s.workVolumeDeclined);
|
|
288
|
+
check("exceptionCount", exceptionCount, s.exceptionCount);
|
|
289
|
+
check("humanSignoffCount", signoffCount, s.humanSignoffCount);
|
|
290
|
+
check("anchoredCount", anchored, s.anchoredCount);
|
|
291
|
+
const wantAutomation = delivered === 0 ? 0 : round1((autonomous / delivered) * 100);
|
|
292
|
+
if (wantAutomation !== s.automationRatePct) mismatches.push(`automationRatePct: recomputed=${wantAutomation} vs summary=${s.automationRatePct}`);
|
|
293
|
+
const wantSla = total === 0 ? 0 : round1((slaMet / total) * 100);
|
|
294
|
+
if (wantSla !== s.slaAdherencePct) mismatches.push(`slaAdherencePct: recomputed=${wantSla} vs summary=${s.slaAdherencePct}`);
|
|
295
|
+
const classMix = emptyClassMix();
|
|
296
|
+
for (const x of o) if (x.failureClass !== null) classMix[x.failureClass] += 1;
|
|
297
|
+
for (const c of FAILURE_CLASSES) {
|
|
298
|
+
if (classMix[c] !== s.exceptionsByClass[c]) mismatches.push(`exceptionsByClass.${c}: outcomes=${classMix[c]} vs summary=${s.exceptionsByClass[c]}`);
|
|
299
|
+
}
|
|
300
|
+
return mismatches;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isWorkCertificateBody(x: unknown): x is WorkCertificateBody {
|
|
304
|
+
if (x === null || typeof x !== "object") return false;
|
|
305
|
+
const b = x as Record<string, unknown>;
|
|
306
|
+
return (
|
|
307
|
+
typeof b.issuerDid === "string" &&
|
|
308
|
+
Array.isArray(b.outcomes) &&
|
|
309
|
+
b.summary !== null &&
|
|
310
|
+
typeof b.summary === "object"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Verify a Work Certificate: the DSSE signature is by the DECLARED issuer, the
|
|
315
|
+
* payload type matches, and every summary count re-derives from the enumerated
|
|
316
|
+
* outcomes. audit r1 F2: a valid signature from the wrong key, a different
|
|
317
|
+
* document type, or a malformed body must all fail โ not slip through or throw. */
|
|
318
|
+
export async function verifyWorkCertificate(
|
|
319
|
+
envelope: DsseEnvelope,
|
|
320
|
+
resolveIssuerKey: (keyid: string) => Uint8Array | null,
|
|
321
|
+
): Promise<WorkCertificateVerifyReport> {
|
|
322
|
+
const dsse = await verifyEnvelope(envelope, resolveIssuerKey);
|
|
323
|
+
|
|
324
|
+
if (dsse.payloadType !== WORK_CERTIFICATE_PAYLOAD_TYPE) {
|
|
325
|
+
return { ok: false, signatureOk: dsse.ok, countsRecomputeOk: false, mismatches: [`payloadType is "${dsse.payloadType}", expected "${WORK_CERTIFICATE_PAYLOAD_TYPE}"`], body: null };
|
|
326
|
+
}
|
|
327
|
+
const body = isWorkCertificateBody(dsse.payloadJson) ? dsse.payloadJson : null;
|
|
328
|
+
if (!dsse.ok || body === null) {
|
|
329
|
+
return { ok: false, signatureOk: dsse.ok, countsRecomputeOk: false, mismatches: body === null ? ["malformed work-certificate body"] : [], body };
|
|
330
|
+
}
|
|
331
|
+
// The signer must be the issuer the certificate NAMES. A valid signature from
|
|
332
|
+
// any OTHER resolvable key is not a valid certificate from `issuerDid`.
|
|
333
|
+
if (!dsse.verifiedKeyids.includes(body.issuerDid)) {
|
|
334
|
+
return { ok: false, signatureOk: true, countsRecomputeOk: false, mismatches: [`signer ${JSON.stringify(dsse.verifiedKeyids)} is not the declared issuer "${body.issuerDid}"`], body };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const mismatches = recomputeCountsMatch(body);
|
|
338
|
+
const countsRecomputeOk = mismatches.length === 0;
|
|
339
|
+
return { ok: countsRecomputeOk, signatureOk: true, countsRecomputeOk, mismatches, body };
|
|
340
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kashscript/hudhud/certificate โ the human-readable Work Certificate (HTML)
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// A self-contained, client-branded HTML document a CFO can read and a browser
|
|
5
|
+
// can "Print โ Save as PDF" (no headless-browser dependency). Renders a
|
|
6
|
+
// WorkCertificateBody; all styling inline so the file is portable. Ported from
|
|
7
|
+
// the oreoasis-core-host engine (M5); the footer is neutralized/brandable.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
import type { Aiuc1DomainCoverage, CertifiedOutcome, WorkCertificateBody } from "./certificate";
|
|
11
|
+
|
|
12
|
+
export interface WorkCertificateBrand {
|
|
13
|
+
readonly accentColor?: string;
|
|
14
|
+
readonly logoText?: string;
|
|
15
|
+
/** Footer attribution line (default neutral). */
|
|
16
|
+
readonly footerText?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function esc(s: string): string {
|
|
20
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
21
|
+
}
|
|
22
|
+
/** audit r1 F10: `accentColor` is interpolated into a CSS declaration (not an
|
|
23
|
+
* HTML text node), where `esc()` is insufficient โ a `;`/`{`/`}` would break out
|
|
24
|
+
* of the rule and rewrite the document's styles (content-hiding / banner spoof).
|
|
25
|
+
* Accept ONLY a CSS colour literal, else fall back to the default. */
|
|
26
|
+
function safeAccent(s: string | undefined, fallback: string): string {
|
|
27
|
+
if (s === undefined) return fallback;
|
|
28
|
+
return /^(#[0-9a-fA-F]{3,8}|[a-z]{3,20}|rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\))$/.test(s) ? s : fallback;
|
|
29
|
+
}
|
|
30
|
+
/** audit r1 F10: the body can arrive from a signature-valid-but-untrusted
|
|
31
|
+
* envelope, so only hyperlink an `http(s)` source (no `javascript:` URIs). */
|
|
32
|
+
function safeUrl(s: string): string {
|
|
33
|
+
return /^https?:\/\//i.test(s) ? s : "#";
|
|
34
|
+
}
|
|
35
|
+
function short(hash: string, n = 16): string {
|
|
36
|
+
return hash.length > n ? `${hash.slice(0, n)}โฆ` : hash;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATUS_LABEL: Record<CertifiedOutcome["status"], string> = {
|
|
40
|
+
delivered_autonomous: "Delivered (autonomous)",
|
|
41
|
+
delivered_with_signoff: "Delivered (human sign-off)",
|
|
42
|
+
declined: "Declined at sign-off",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function metricCard(label: string, value: string, sub: string): string {
|
|
46
|
+
return `<div class="card"><div class="card-v">${esc(value)}</div><div class="card-l">${esc(label)}</div><div class="card-s">${esc(sub)}</div></div>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function aiuc1Row(d: Aiuc1DomainCoverage): string {
|
|
50
|
+
const badge = d.status === "evidenced" ? `<span class="pill pill-ok">Evidenced</span>` : `<span class="pill pill-part">Partial</span>`;
|
|
51
|
+
const items = d.evidence.map((e) => `<li>${esc(e)}</li>`).join("");
|
|
52
|
+
return `<tr><td class="dom"><span class="dom-code">${esc(d.code)}</span> ${esc(d.name)} ${badge}<div class="dom-covers">${esc(d.covers)}</div></td><td><ul class="ev">${items}</ul></td></tr>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function outcomeRow(o: CertifiedOutcome): string {
|
|
56
|
+
const cls = o.failureClass ? esc(o.failureClass) : "โ";
|
|
57
|
+
const anchor = o.anchorMerkleRoot ? `${esc(short(o.anchorMerkleRoot, 12))} <span class="mut">(${esc(o.anchorWitnessBackend ?? "none")})</span>` : "โ";
|
|
58
|
+
const sla = o.slaMet ? `<span class="pill pill-ok">met</span>` : `<span class="pill pill-bad">missed</span>`;
|
|
59
|
+
return `<tr><td class="mono">${esc(o.workItemId)}</td><td>${esc(STATUS_LABEL[o.status])}</td><td>${sla}</td><td class="mono">${esc(short(o.ledgerTipHash, 12))}</td><td>${o.signoffLedgerStepIndex === null ? "โ" : `#${o.signoffLedgerStepIndex}`}</td><td>${cls}</td><td class="mono">${anchor}</td></tr>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Render a self-contained, print-to-PDF-ready HTML certificate. */
|
|
63
|
+
export function renderWorkCertificateHtml(body: WorkCertificateBody, brand: WorkCertificateBrand = {}): string {
|
|
64
|
+
const accent = safeAccent(brand.accentColor, "#0f2a4a");
|
|
65
|
+
const logo = brand.logoText ?? body.period.clientName;
|
|
66
|
+
const footer = brand.footerText ?? "Accountable AI workforce";
|
|
67
|
+
const s = body.summary;
|
|
68
|
+
const classMix = Object.entries(s.exceptionsByClass).map(([k, v]) => `${esc(k)}: <b>${v}</b>`).join(" ยท ");
|
|
69
|
+
const anchors =
|
|
70
|
+
body.anchors.length === 0
|
|
71
|
+
? `<p class="mut">No public-witness anchor attached to this period (ledger is tamper-evident and independently exportable).</p>`
|
|
72
|
+
: `<table class="tbl"><thead><tr><th>Merkle root</th><th>Witness</th><th>Witnessed</th><th>Verify with</th></tr></thead><tbody>${body.anchors
|
|
73
|
+
.map((a) => `<tr><td class="mono">${esc(short(a.merkleRoot, 20))}</td><td>${esc(a.witnessBackend)}</td><td>${esc(a.witnessedAtIso ?? "pending")}</td><td class="mono">${esc(a.verifyWith)}</td></tr>`)
|
|
74
|
+
.join("")}</tbody></table>`;
|
|
75
|
+
|
|
76
|
+
return `<!doctype html>
|
|
77
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
79
|
+
<title>Work Certificate โ ${esc(body.period.clientName)} โ ${esc(body.period.month)}</title>
|
|
80
|
+
<style>
|
|
81
|
+
:root { --accent: ${esc(accent)}; }
|
|
82
|
+
* { box-sizing: border-box; }
|
|
83
|
+
body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #1a1f26; margin: 0; background: #f4f6f8; }
|
|
84
|
+
.page { max-width: 900px; margin: 24px auto; background: #fff; box-shadow: 0 1px 6px rgba(0,0,0,.08); }
|
|
85
|
+
.band { background: var(--accent); color: #fff; padding: 28px 40px; }
|
|
86
|
+
.band .logo { font-size: 13px; letter-spacing: .18em; text-transform: uppercase; opacity: .85; }
|
|
87
|
+
.band h1 { margin: 6px 0 2px; font-size: 26px; font-weight: 650; }
|
|
88
|
+
.band .sub { opacity: .9; font-size: 14px; }
|
|
89
|
+
.band .verifiable { margin-top: 12px; display: inline-block; font-size: 12px; background: rgba(255,255,255,.16); padding: 4px 10px; border-radius: 3px; }
|
|
90
|
+
.body { padding: 28px 40px 40px; }
|
|
91
|
+
h2 { font-size: 15px; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); border-bottom: 2px solid #eceff2; padding-bottom: 6px; margin: 30px 0 14px; }
|
|
92
|
+
.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
93
|
+
.card { border: 1px solid #e6eaee; border-radius: 5px; padding: 14px; }
|
|
94
|
+
.card-v { font-size: 24px; font-weight: 680; color: var(--accent); }
|
|
95
|
+
.card-l { font-size: 12px; font-weight: 600; margin-top: 2px; }
|
|
96
|
+
.card-s { font-size: 11px; color: #6b7683; margin-top: 2px; }
|
|
97
|
+
table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
98
|
+
.aiuc td { border-top: 1px solid #eceff2; padding: 12px 8px; vertical-align: top; }
|
|
99
|
+
.aiuc .dom { width: 40%; }
|
|
100
|
+
.dom-code { display: inline-block; background: var(--accent); color: #fff; width: 20px; height: 20px; text-align: center; line-height: 20px; border-radius: 3px; font-size: 11px; font-weight: 700; margin-right: 4px; }
|
|
101
|
+
.dom-covers { color: #6b7683; font-size: 11.5px; margin-top: 4px; }
|
|
102
|
+
ul.ev { margin: 0; padding-left: 16px; } ul.ev li { margin: 3px 0; }
|
|
103
|
+
.tbl th { text-align: left; background: #f7f9fb; border-bottom: 1px solid #e6eaee; padding: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: #6b7683; }
|
|
104
|
+
.tbl td { border-bottom: 1px solid #f0f2f5; padding: 8px; }
|
|
105
|
+
.mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 11.5px; }
|
|
106
|
+
.mut { color: #94a0ac; }
|
|
107
|
+
.pill { font-size: 10.5px; padding: 1px 7px; border-radius: 10px; font-weight: 600; }
|
|
108
|
+
.pill-ok { background: #e3f5ea; color: #1a7a44; }
|
|
109
|
+
.pill-part { background: #fbf1dc; color: #8a6414; }
|
|
110
|
+
.pill-bad { background: #fbe4e4; color: #a52222; }
|
|
111
|
+
.verify { background: #f7f9fb; border: 1px solid #e6eaee; border-radius: 5px; padding: 14px 16px; font-size: 12.5px; }
|
|
112
|
+
.foot { color: #94a0ac; font-size: 11px; margin-top: 24px; border-top: 1px solid #eceff2; padding-top: 12px; }
|
|
113
|
+
a { color: var(--accent); }
|
|
114
|
+
@media print { body { background: #fff; } .page { box-shadow: none; margin: 0; max-width: none; } h2 { break-after: avoid; } tr { break-inside: avoid; } }
|
|
115
|
+
</style></head>
|
|
116
|
+
<body><div class="page">
|
|
117
|
+
<div class="band">
|
|
118
|
+
<div class="logo">${esc(logo)}</div>
|
|
119
|
+
<h1>Work Certificate</h1>
|
|
120
|
+
<div class="sub">${esc(body.period.clientName)} ยท ${esc(body.period.month)} ยท ${esc(body.period.fromIso.slice(0, 10))} โ ${esc(body.period.toIso.slice(0, 10))}</div>
|
|
121
|
+
<div class="verifiable">๐ Cryptographically signed & independently verifiable โ structured to AIUC-1</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="body">
|
|
124
|
+
<h2>Delivered work this period</h2>
|
|
125
|
+
<div class="cards">
|
|
126
|
+
${metricCard("Outcomes delivered", String(s.workVolumeDelivered), `${s.workVolumeDeclined} declined at sign-off`)}
|
|
127
|
+
${metricCard("Automation rate", `${s.automationRatePct}%`, "delivered without human touch")}
|
|
128
|
+
${metricCard("Exceptions", String(s.exceptionCount), `${s.humanSignoffCount} human sign-off(s)`)}
|
|
129
|
+
${metricCard("SLA adherence", `${s.slaAdherencePct}%`, s.slaTargetPct === null ? "no target set" : `target ${s.slaTargetPct}%`)}
|
|
130
|
+
</div>
|
|
131
|
+
<p style="font-size:12.5px;color:#6b7683;margin-top:12px">Exception classification mix โ ${classMix}. Every count on this certificate derives from the signed ledger; there is no separate metering system.</p>
|
|
132
|
+
|
|
133
|
+
<h2>AIUC-1 control coverage</h2>
|
|
134
|
+
<table class="aiuc"><tbody>
|
|
135
|
+
${body.aiuc1Coverage.map(aiuc1Row).join("\n")}
|
|
136
|
+
</tbody></table>
|
|
137
|
+
<p class="mut" style="font-size:11px;margin-top:8px">Categories per the AIUC-1 standard (<a href="${esc(safeUrl(body.standard.source))}">${esc(body.standard.source)}</a>). Evidence statements are factual records from this period's signed ledger.</p>
|
|
138
|
+
|
|
139
|
+
<h2>Warranted outcomes (${body.outcomes.length})</h2>
|
|
140
|
+
<table class="tbl"><thead><tr><th>Work item</th><th>Outcome</th><th>SLA</th><th>Chain tip</th><th>Sign-off</th><th>Class</th><th>Anchor</th></tr></thead>
|
|
141
|
+
<tbody>${body.outcomes.map(outcomeRow).join("\n")}</tbody></table>
|
|
142
|
+
|
|
143
|
+
<h2>Public-witness anchors</h2>
|
|
144
|
+
${anchors}
|
|
145
|
+
|
|
146
|
+
<h2>How to verify this certificate</h2>
|
|
147
|
+
<div class="verify">
|
|
148
|
+
<p style="margin:0 0 8px">${esc(body.verification.instructions)}</p>
|
|
149
|
+
<div class="mono">issuer: ${esc(body.issuerDid)}</div>
|
|
150
|
+
<div class="mono">format: ${esc(body.formatVersion)} ยท issued: ${esc(body.issuedAtIso)}</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="foot">${esc(footer)}. This certificate enumerates the warranted outcomes it is the billing unit for. AIUC-1 categories: ${esc(body.standard.source)}.</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div></body></html>`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kashscript/hudhud/did-registry โ DID โ Ed25519 public-key resolution
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// The DID registry the ledger verifies actor signatures against. The interface
|
|
5
|
+
// + in-memory reference live in `@kashscript/attest` (seeded from the same
|
|
6
|
+
// Oreoasis `did-registry.ts` in M1); hudhud re-exports them so ventures reach
|
|
7
|
+
// the registry through the runtime, and add a durable/PDS resolver at swap.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export { InMemoryDidRegistry } from "@kashscript/attest";
|
|
11
|
+
export type { DidRegistry, DidPublicKey } from "@kashscript/attest";
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kashscript/hudhud โ provider-neutral error vocabulary
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// Replaces the Oreoasis `@oreoasis/ks-bridge` exception classes with neutral
|
|
5
|
+
// `Hudhud*Error`s. A consuming venture maps them to its own HTTP envelope at
|
|
6
|
+
// its perimeter (ADR-0016 guard b); this package never emits a venture code.
|
|
7
|
+
//
|
|
8
|
+
// Ledger tamper is surfaced by `@kashscript/attest`'s `TamperError` (the ledger
|
|
9
|
+
// runs on attest) โ hudhud does not re-declare it.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/** Base class for every error this package throws. */
|
|
13
|
+
export class HudhudError extends Error {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
constructor(code: string, message: string) {
|
|
16
|
+
super(`[${code}] ${message}`);
|
|
17
|
+
this.name = "HudhudError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Thrown when an SOP manual cannot be compiled (cycle, malformed goto, empty). */
|
|
23
|
+
export class SopCompilationError extends HudhudError {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super("SOP_COMPILATION", message);
|
|
26
|
+
this.name = "SopCompilationError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Thrown when an MCP tool call exceeds the hard timeout. */
|
|
31
|
+
export class McpTimeoutError extends HudhudError {
|
|
32
|
+
readonly timeoutMs: number;
|
|
33
|
+
constructor(timeoutMs: number, message: string) {
|
|
34
|
+
super("MCP_TIMEOUT", message);
|
|
35
|
+
this.name = "McpTimeoutError";
|
|
36
|
+
this.timeoutMs = timeoutMs;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The security-violation discriminants raised at MCP dispatch time. */
|
|
41
|
+
export type McpSecurityViolationReason =
|
|
42
|
+
| "TOOL_NOT_ALLOWLISTED"
|
|
43
|
+
| "ARG_NOT_ALLOWED"
|
|
44
|
+
| "REQUIRED_ARG_MISSING"
|
|
45
|
+
| "TOOL_NAME_MALFORMED"
|
|
46
|
+
| "SCOPE_VIOLATION"
|
|
47
|
+
| "CIRCUIT_OPEN";
|
|
48
|
+
|
|
49
|
+
/** Thrown when a dispatch is refused by the allowlist / scope / breaker gates.
|
|
50
|
+
* `track` mirrors the Oreoasis four-track wire vocabulary. */
|
|
51
|
+
export class McpSecurityViolationError extends HudhudError {
|
|
52
|
+
readonly reason: McpSecurityViolationReason;
|
|
53
|
+
readonly track: "error" | "security";
|
|
54
|
+
constructor(reason: McpSecurityViolationReason, message: string, track: "error" | "security" = "security") {
|
|
55
|
+
super("MCP_SECURITY_VIOLATION", message);
|
|
56
|
+
this.name = "McpSecurityViolationError";
|
|
57
|
+
this.reason = reason;
|
|
58
|
+
this.track = track;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Thrown when a required exception classification is missing/invalid โ the
|
|
63
|
+
* mandatory `failureClass` gate (no "unclassified" state is representable). */
|
|
64
|
+
export class ExceptionClassificationError extends HudhudError {
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super("EXCEPTION_CLASSIFICATION", message);
|
|
67
|
+
this.name = "ExceptionClassificationError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Thrown by the ForensicLedgerService append pipeline (stepIndex mismatch,
|
|
72
|
+
* signature-verify failure, store write failure). Ledger *tamper* detected by
|
|
73
|
+
* the forensic scan surfaces as attest's `TamperError`, not this. */
|
|
74
|
+
export class LedgerAppendError extends HudhudError {
|
|
75
|
+
constructor(message: string) {
|
|
76
|
+
super("LEDGER_APPEND", message);
|
|
77
|
+
this.name = "LedgerAppendError";
|
|
78
|
+
}
|
|
79
|
+
}
|