@skill-harness/adapters 0.11.0 → 0.12.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/dist/pi.js CHANGED
@@ -1,10 +1,72 @@
1
- import { existsSync, mkdtempSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir, homedir } from "node:os";
3
+ import { randomBytes } from "node:crypto";
3
4
  import { join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
  import { runPiJson } from "./pi-json.js";
5
7
  import { collectTrajectorySources, normalizePiTraces, resequence } from "./trajectory.js";
6
- import { exec, onPath, envNum, traceSha256, withProviderFailure } from "@skill-harness/core";
8
+ import { bindPromptObservation, observeProviderPayload, promptCaptureIsTrusted, verifyPromptSummary } from "./prompt-provenance.js";
9
+ import { exec, onPath, envNum, traceSha256, withProviderFailure, splitPromptDoc } from "@skill-harness/core";
7
10
  const PI_TIMEOUT_MS = envNum("PI_TIMEOUT_MS", 300_000);
11
+ const PROMPT_CAPTURE_EXTENSION = fileURLToPath(new URL("./prompt-capture-extension.js", import.meta.url));
12
+ function contractFor(req) {
13
+ if (req.systemPromptFile) {
14
+ const raw = readFileSync(req.systemPromptFile, "utf8");
15
+ return { text: raw, raw, mechanism: "system-prompt-file" };
16
+ }
17
+ const raw = readFileSync(join(requireSkillDir(req.skillDir, req.mode), "SKILL.md"), "utf8");
18
+ const body = splitPromptDoc(raw).body;
19
+ if (req.mode === "red")
20
+ return { text: body, raw, mechanism: "none" };
21
+ return { text: body, raw, mechanism: req.mode === "green" ? "pi-skill" : "append-system-prompt" };
22
+ }
23
+ const RUNTIME_INJECTION_ENV = ["NODE_OPTIONS", "NODE_PATH", "LD_PRELOAD", "DYLD_INSERT_LIBRARIES"];
24
+ function hasRuntimeInjection(req) {
25
+ return Boolean(req.armEnv && Object.keys(req.armEnv).length) || RUNTIME_INJECTION_ENV.some(key => Boolean(process.env[key]));
26
+ }
27
+ function captureSetup(req, env, contract, counter) {
28
+ if (!req.onPromptObservation)
29
+ return { env, finish: () => { } };
30
+ // Arbitrary scenario/arm extensions and runtime-injection env execute in Pi's
31
+ // process with full Node authority. Refuse a forgeable positive observation.
32
+ if (!promptCaptureIsTrusted(req.extensions?.length ?? 0, hasRuntimeInjection(req)))
33
+ return { env, finish: () => {
34
+ const empty = observeProviderPayload({}, contract.text, contract.mechanism, counter.value++);
35
+ req.onPromptObservation?.({ ...empty, status: "ERROR", error: "prompt delivery provenance is unauthenticated when subject extensions or runtime-injection env share Pi's process" });
36
+ } };
37
+ const dir = mkdtempSync(join(tmpdir(), "skill-harness-prompt-"));
38
+ const path = join(dir, "observations.jsonl"), contractPath = join(dir, "contract.json");
39
+ const authenticationKey = randomBytes(32).toString("hex");
40
+ writeFileSync(path, "", { mode: 0o600 });
41
+ writeFileSync(contractPath, JSON.stringify({ text: contract.text, mechanism: contract.mechanism, authentication_key: authenticationKey }), { mode: 0o600 });
42
+ const finish = () => {
43
+ try {
44
+ const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
45
+ const parsed = lines.map(line => { try {
46
+ return JSON.parse(line);
47
+ }
48
+ catch {
49
+ return null;
50
+ } });
51
+ const records = parsed.slice(0, -1), summary = parsed.at(-1);
52
+ if (!verifyPromptSummary(summary, records.length, authenticationKey)) {
53
+ const empty = observeProviderPayload({}, contract.text, contract.mechanism, counter.value++);
54
+ req.onPromptObservation?.({ ...empty, status: "ERROR", error: "Pi prompt observation log is missing, truncated, replayed, or unauthenticated" });
55
+ }
56
+ else
57
+ records.forEach((record, observerRequestIndex) => {
58
+ req.onPromptObservation?.(bindPromptObservation(record, contract.text, contract.mechanism, counter.value++, authenticationKey, observerRequestIndex));
59
+ });
60
+ }
61
+ finally {
62
+ rmSync(dir, { recursive: true, force: true });
63
+ }
64
+ };
65
+ return { env: { ...(env ?? process.env), SKILL_HARNESS_PROMPT_CAPTURE_FILE: path, SKILL_HARNESS_PROMPT_CONTRACT_FILE: contractPath }, finish };
66
+ }
67
+ function observerFlags(req) {
68
+ return req.onPromptObservation && promptCaptureIsTrusted(req.extensions?.length ?? 0, hasRuntimeInjection(req)) ? ["--extension", PROMPT_CAPTURE_EXTENSION] : [];
69
+ }
8
70
  /**
9
71
  * stderr fragments that mean the provider refused the request, so the run measured
10
72
  * nothing about the model. Substring matching on a message pi passes through from
@@ -57,14 +119,15 @@ function requireSkillDir(skillDir, mode) {
57
119
  * made conditional. Both are checked the same way, because the failure being
58
120
  * prevented — a skill dir that isn't there — costs a whole wave either way.
59
121
  */
60
- function skillFlags(mode, skillDir) {
122
+ function skillFlags(mode, skillDir, boundRaw) {
61
123
  switch (mode) {
62
124
  case "red":
63
125
  return ["--no-skills"];
64
126
  case "green":
65
127
  return ["--skill", requireSkillDir(skillDir, mode)];
66
128
  case "force": {
67
- const body = readFileSync(join(requireSkillDir(skillDir, mode), "SKILL.md"), "utf8");
129
+ requireSkillDir(skillDir, mode);
130
+ const body = boundRaw ?? readFileSync(join(resolve(skillDir), "SKILL.md"), "utf8");
68
131
  return ["--no-skills", "--append-system-prompt", body];
69
132
  }
70
133
  }
@@ -97,6 +160,7 @@ function header(turnNo, total, text) {
97
160
  }
98
161
  export const piAdapter = {
99
162
  name: "pi",
163
+ observesPrompts: true,
100
164
  available() {
101
165
  return Promise.resolve(onPath("pi"));
102
166
  },
@@ -132,15 +196,18 @@ export const piAdapter = {
132
196
  "--no-context-files",
133
197
  "--no-extensions",
134
198
  ...extensionFlags(req.extensions),
199
+ ...observerFlags(req),
135
200
  "--provider",
136
201
  req.model.provider,
137
202
  "--model",
138
203
  req.model.model,
139
204
  ];
140
- // An agent-file run IS the system prompt: no skill activation, whatever the mode.
205
+ // Bind bytes once: argv and provenance must describe the same contract even if a file changes mid-run.
206
+ const contract = contractFor(req);
141
207
  const flags = req.systemPromptFile
142
- ? ["--no-skills", "--append-system-prompt", readFileSync(req.systemPromptFile, "utf8")]
143
- : skillFlags(req.mode, req.skillDir);
208
+ ? ["--no-skills", "--append-system-prompt", contract.raw]
209
+ : skillFlags(req.mode, req.skillDir, contract.raw);
210
+ const requestCounter = { value: 0 };
144
211
  const total = req.turns.length;
145
212
  const parts = [];
146
213
  // The arm's env, merged over the harness's own — undefined (not `process.env`)
@@ -155,7 +222,14 @@ export const piAdapter = {
155
222
  let providerFailure = null;
156
223
  if (total === 1) {
157
224
  const args = [...flags, ...common, "--no-session", "-p", req.turns[0]];
158
- const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
225
+ const capture = captureSetup(req, env, contract, requestCounter);
226
+ let r;
227
+ try {
228
+ r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env: capture.env });
229
+ }
230
+ finally {
231
+ capture.finish();
232
+ }
159
233
  parts.push(header(1, 1, req.turns[0]));
160
234
  parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
161
235
  if (r.code !== 0) {
@@ -169,7 +243,14 @@ export const piAdapter = {
169
243
  for (let i = 0; i < total; i++) {
170
244
  const turnFlags = i === 0 ? ["--session-dir", session] : ["--session-dir", session, "-c"];
171
245
  const args = [...flags, ...common, ...turnFlags, "-p", req.turns[i]];
172
- const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
246
+ const capture = captureSetup(req, env, contract, requestCounter);
247
+ let r;
248
+ try {
249
+ r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env: capture.env });
250
+ }
251
+ finally {
252
+ capture.finish();
253
+ }
173
254
  parts.push(header(i + 1, total, req.turns[i]));
174
255
  parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
175
256
  if (r.code !== 0) {
@@ -201,14 +282,17 @@ export const piAdapter = {
201
282
  "--no-context-files",
202
283
  "--no-extensions",
203
284
  ...extensionFlags(req.extensions),
285
+ ...observerFlags(req),
204
286
  "--provider",
205
287
  req.model.provider,
206
288
  "--model",
207
289
  req.model.model,
208
290
  ];
291
+ const contract = contractFor(req);
209
292
  const flags = req.systemPromptFile
210
- ? ["--no-skills", "--append-system-prompt", readFileSync(req.systemPromptFile, "utf8")]
211
- : skillFlags(req.mode, req.skillDir);
293
+ ? ["--no-skills", "--append-system-prompt", contract.raw]
294
+ : skillFlags(req.mode, req.skillDir, contract.raw);
295
+ const requestCounter = { value: 0 };
212
296
  const piVersion = await this.version();
213
297
  const total = req.turns.length;
214
298
  const traces = [];
@@ -225,19 +309,26 @@ export const piAdapter = {
225
309
  ? ["--session-dir", session]
226
310
  : ["--session-dir", session, "-c"];
227
311
  const args = [...flags, ...common, "--mode", "json", ...turnFlags, "-p", req.turns[i]];
228
- const r = await runPiJson({
229
- args,
230
- cwd: req.cwd,
231
- timeoutMs: PI_TIMEOUT_MS,
232
- piVersion,
233
- subject: req.model,
234
- scenarioId: req.scenarioId ?? "(unknown)",
235
- mode: req.mode,
236
- rep: req.rep ?? 0,
237
- turn: i,
238
- homeDir: homedir(),
239
- env,
240
- });
312
+ const capture = captureSetup(req, env, contract, requestCounter);
313
+ let r;
314
+ try {
315
+ r = await runPiJson({
316
+ args,
317
+ cwd: req.cwd,
318
+ timeoutMs: PI_TIMEOUT_MS,
319
+ piVersion,
320
+ subject: req.model,
321
+ scenarioId: req.scenarioId ?? "(unknown)",
322
+ mode: req.mode,
323
+ rep: req.rep ?? 0,
324
+ turn: i,
325
+ homeDir: homedir(),
326
+ env: capture.env,
327
+ });
328
+ }
329
+ finally {
330
+ capture.finish();
331
+ }
241
332
  // A stream with no terminal events at all is not evidence of a clean run.
242
333
  // Fail loudly here rather than let an empty trace satisfy a `forbid_calls`
243
334
  // gate — "the model called nothing" and "we recorded nothing" must not
@@ -0,0 +1,10 @@
1
+ interface PromptEvent {
2
+ payload: unknown;
3
+ }
4
+ interface MinimalExtensionApi {
5
+ on(name: "before_provider_request", handler: (event: PromptEvent) => void): void;
6
+ on(name: "session_shutdown", handler: () => void): void;
7
+ }
8
+ /** Internal observation extension: hashes the final provider prompt in-process; never writes payload plaintext. */
9
+ export default function promptCapture(pi: MinimalExtensionApi): void;
10
+ export {};
@@ -0,0 +1,26 @@
1
+ import { appendFileSync, readFileSync, rmSync } from "node:fs";
2
+ import { authenticatePromptObservation, authenticatePromptSummary, observeProviderPayload } from "./prompt-provenance.js";
3
+ /** Internal observation extension: hashes the final provider prompt in-process; never writes payload plaintext. */
4
+ export default function promptCapture(pi) {
5
+ const target = process.env.SKILL_HARNESS_PROMPT_CAPTURE_FILE;
6
+ const contractPath = process.env.SKILL_HARNESS_PROMPT_CONTRACT_FILE;
7
+ if (!target || !contractPath)
8
+ throw new Error("prompt capture paths are required");
9
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
10
+ // Remove the capability before any subject/tool execution. The observer keeps
11
+ // only closed-over values; child processes cannot discover contract/evidence paths.
12
+ delete process.env.SKILL_HARNESS_PROMPT_CAPTURE_FILE;
13
+ delete process.env.SKILL_HARNESS_PROMPT_CONTRACT_FILE;
14
+ rmSync(contractPath, { force: true });
15
+ let requestIndex = 0;
16
+ pi.on("before_provider_request", (event) => {
17
+ const observation = observeProviderPayload(event.payload, contract.text, contract.mechanism, requestIndex++);
18
+ appendFileSync(target, JSON.stringify(authenticatePromptObservation(observation, contract.authentication_key)) + "\n", { encoding: "utf8", mode: 0o600 });
19
+ });
20
+ // Written after the agent loop, so a tool subprocess can at most corrupt the
21
+ // log into ERROR; it cannot truncate/replay a valid prefix into trusted PASS.
22
+ pi.on("session_shutdown", () => {
23
+ appendFileSync(target, JSON.stringify(authenticatePromptSummary(requestIndex, contract.authentication_key)) + "\n", { encoding: "utf8", mode: 0o600 });
24
+ });
25
+ }
26
+ //# sourceMappingURL=prompt-capture-extension.js.map
@@ -0,0 +1,22 @@
1
+ import { PROMPT_NORMALIZATION_RULE, type PromptMechanism, type PromptProvenance } from "@skill-harness/core";
2
+ export { PROMPT_NORMALIZATION_RULE } from "@skill-harness/core";
3
+ /** Registry entry 1: replace exactly Pi's dynamic cwd line, preserving every other byte. */
4
+ export declare function normalizePromptPayload(value: unknown, rule: typeof PROMPT_NORMALIZATION_RULE): unknown;
5
+ export declare function promptCaptureIsTrusted(extensionCount: number, hasRuntimeInjection?: boolean): boolean;
6
+ export interface AuthenticatedPromptObservation {
7
+ observation: PromptProvenance;
8
+ mac: string;
9
+ }
10
+ export interface AuthenticatedPromptSummary {
11
+ summary: {
12
+ count: number;
13
+ };
14
+ mac: string;
15
+ }
16
+ export declare function authenticatePromptObservation(observation: PromptProvenance, authenticationKey: string): AuthenticatedPromptObservation;
17
+ export declare function authenticatePromptSummary(count: number, authenticationKey: string): AuthenticatedPromptSummary;
18
+ export declare function verifyPromptSummary(value: unknown, expectedCount: number, authenticationKey: string): boolean;
19
+ /** Compute observation from final provider payload prompt bytes; caller supplies content, never status. */
20
+ export declare function observeProviderPayload(payload: unknown, contract: string, mechanism: PromptMechanism, requestIndex: number): PromptProvenance;
21
+ /** Parent-side binding: never accept contract identity or status from the child observer. */
22
+ export declare function bindPromptObservation(value: unknown, contract: string, mechanism: PromptMechanism, requestIndex: number, authenticationKey: string, observerRequestIndex?: number): PromptProvenance;
@@ -0,0 +1,146 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+ import { PROMPT_NORMALIZATION_FLAGS, PROMPT_NORMALIZATION_PATTERN, PROMPT_NORMALIZATION_REPLACEMENT, PROMPT_NORMALIZATION_RULE } from "@skill-harness/core";
3
+ export { PROMPT_NORMALIZATION_RULE } from "@skill-harness/core";
4
+ function sha(bytes) {
5
+ return createHash("sha256").update(bytes, "utf8").digest("hex");
6
+ }
7
+ /** Registry entry 1: replace exactly Pi's dynamic cwd line, preserving every other byte. */
8
+ export function normalizePromptPayload(value, rule) {
9
+ if (rule !== PROMPT_NORMALIZATION_RULE)
10
+ throw new Error(`unknown prompt normalization rule ${String(rule)}`);
11
+ if (typeof value === "string")
12
+ return value.replace(new RegExp(PROMPT_NORMALIZATION_PATTERN, PROMPT_NORMALIZATION_FLAGS), PROMPT_NORMALIZATION_REPLACEMENT);
13
+ if (Array.isArray(value))
14
+ return value.map((entry) => normalizePromptPayload(entry, rule));
15
+ if (value && typeof value === "object")
16
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizePromptPayload(entry, rule)]));
17
+ return value;
18
+ }
19
+ function countInStrings(value, needle) {
20
+ if (typeof value === "string") {
21
+ if (!needle)
22
+ return 0;
23
+ let count = 0, at = 0;
24
+ while ((at = value.indexOf(needle, at)) !== -1) {
25
+ count++;
26
+ at += needle.length;
27
+ }
28
+ return count;
29
+ }
30
+ if (Array.isArray(value))
31
+ return value.reduce((sum, entry) => sum + countInStrings(entry, needle), 0);
32
+ if (value && typeof value === "object") {
33
+ const record = value;
34
+ // A scenario may quote the contract. User-authored text is stimulus, never
35
+ // delivery, but providers also place tool results inside role=user messages.
36
+ if (record.role === "user") {
37
+ const content = Array.isArray(record.content) ? record.content : Array.isArray(record.parts) ? record.parts : [];
38
+ return content
39
+ .filter(block => block && typeof block === "object" && (["tool_result", "tool_response", "function_response"].includes(String(block.type ?? "")) ||
40
+ "functionResponse" in block || "function_response" in block))
41
+ .reduce((sum, block) => sum + countInStrings(block, needle), 0);
42
+ }
43
+ return Object.values(record).reduce((sum, entry) => sum + countInStrings(entry, needle), 0);
44
+ }
45
+ return 0;
46
+ }
47
+ const PROMPT_FIELDS = new Set(["instructions", "input", "system", "messages", "prompt", "contents"]);
48
+ function promptProjection(payload) {
49
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
50
+ return null;
51
+ const record = payload;
52
+ const projected = Object.fromEntries(Object.entries(record).filter(([key]) => PROMPT_FIELDS.has(key)));
53
+ const config = record.config && typeof record.config === "object" ? record.config : null;
54
+ if (config?.systemInstruction !== undefined)
55
+ projected.systemInstruction = config.systemInstruction;
56
+ return Object.keys(projected).length ? projected : null;
57
+ }
58
+ export function promptCaptureIsTrusted(extensionCount, hasRuntimeInjection = false) {
59
+ return extensionCount === 0 && !hasRuntimeInjection;
60
+ }
61
+ function observationMac(observation, authenticationKey) {
62
+ return createHmac("sha256", authenticationKey).update(JSON.stringify(observation)).digest("hex");
63
+ }
64
+ export function authenticatePromptObservation(observation, authenticationKey) {
65
+ return { observation, mac: observationMac(observation, authenticationKey) };
66
+ }
67
+ export function authenticatePromptSummary(count, authenticationKey) {
68
+ const summary = { count };
69
+ return { summary, mac: createHmac("sha256", authenticationKey).update(JSON.stringify(summary)).digest("hex") };
70
+ }
71
+ function validMac(supplied, expected) {
72
+ if (typeof supplied !== "string" || !/^[a-f0-9]{64}$/i.test(supplied))
73
+ return false;
74
+ const suppliedBytes = Buffer.from(supplied, "hex"), expectedBytes = Buffer.from(expected, "hex");
75
+ return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
76
+ }
77
+ export function verifyPromptSummary(value, expectedCount, authenticationKey) {
78
+ if (!value || typeof value !== "object")
79
+ return false;
80
+ const envelope = value;
81
+ if (!envelope.summary || envelope.summary.count !== expectedCount)
82
+ return false;
83
+ const expected = createHmac("sha256", authenticationKey).update(JSON.stringify(envelope.summary)).digest("hex");
84
+ return validMac(envelope.mac, expected);
85
+ }
86
+ function statusFor(occurrences, mechanism, observable) {
87
+ if (!observable)
88
+ return "ERROR";
89
+ return occurrences === (mechanism === "none" ? 0 : 1) ? "PASS" : "NOT-MEASURED";
90
+ }
91
+ /** Compute observation from final provider payload prompt bytes; caller supplies content, never status. */
92
+ export function observeProviderPayload(payload, contract, mechanism, requestIndex) {
93
+ const projection = promptProjection(payload);
94
+ const raw = JSON.stringify(projection ?? {});
95
+ const normalized = JSON.stringify(normalizePromptPayload(projection ?? {}, PROMPT_NORMALIZATION_RULE));
96
+ const occurrences = countInStrings(projection ?? {}, contract);
97
+ const observable = projection !== null;
98
+ return {
99
+ capture_version: "prompt-provenance-v1",
100
+ request_index: requestIndex,
101
+ raw_sha256: sha(raw),
102
+ normalized_sha256: sha(normalized),
103
+ normalization_rule: PROMPT_NORMALIZATION_RULE,
104
+ bytes: Buffer.byteLength(raw),
105
+ contract_sha256: sha(contract),
106
+ contract_bytes: Buffer.byteLength(contract),
107
+ contract_occurrences: occurrences,
108
+ mechanism,
109
+ status: statusFor(occurrences, mechanism, observable),
110
+ ...(observable ? {} : { error: "provider payload has no supported model-visible prompt field" }),
111
+ };
112
+ }
113
+ /** Parent-side binding: never accept contract identity or status from the child observer. */
114
+ export function bindPromptObservation(value, contract, mechanism, requestIndex, authenticationKey, observerRequestIndex = requestIndex) {
115
+ const fallback = { ...observeProviderPayload({}, contract, mechanism, requestIndex), status: "ERROR" };
116
+ if (!value || typeof value !== "object")
117
+ return { ...fallback, error: "prompt observer emitted a non-object record" };
118
+ const envelope = value;
119
+ if (!envelope.observation)
120
+ return { ...fallback, error: "prompt observation authentication missing" };
121
+ const expectedMac = observationMac(envelope.observation, authenticationKey);
122
+ if (!validMac(envelope.mac, expectedMac))
123
+ return { ...fallback, error: "prompt observation authentication failed" };
124
+ const record = envelope.observation;
125
+ if (record.request_index !== observerRequestIndex)
126
+ return { ...fallback, error: "prompt observation replay or ordering mismatch" };
127
+ const expectedDigest = sha(contract), expectedBytes = Buffer.byteLength(contract);
128
+ const valid = record.capture_version === "prompt-provenance-v1" &&
129
+ record.normalization_rule === PROMPT_NORMALIZATION_RULE && record.mechanism === mechanism &&
130
+ record.contract_sha256 === expectedDigest && record.contract_bytes === expectedBytes &&
131
+ typeof record.raw_sha256 === "string" && /^[a-f0-9]{64}$/i.test(record.raw_sha256) &&
132
+ typeof record.normalized_sha256 === "string" && /^[a-f0-9]{64}$/i.test(record.normalized_sha256) &&
133
+ Number.isInteger(record.bytes) && record.bytes >= 0 &&
134
+ Number.isInteger(record.contract_occurrences) && record.contract_occurrences >= 0;
135
+ if (!valid)
136
+ return { ...fallback, error: "prompt observation failed parent contract/provenance binding" };
137
+ const occurrences = record.contract_occurrences;
138
+ return {
139
+ capture_version: "prompt-provenance-v1", request_index: requestIndex,
140
+ raw_sha256: record.raw_sha256, normalized_sha256: record.normalized_sha256,
141
+ normalization_rule: PROMPT_NORMALIZATION_RULE, bytes: record.bytes,
142
+ contract_sha256: expectedDigest, contract_bytes: expectedBytes, contract_occurrences: occurrences,
143
+ mechanism, status: statusFor(occurrences, mechanism, true),
144
+ };
145
+ }
146
+ //# sourceMappingURL=prompt-provenance.js.map
@@ -12,20 +12,26 @@ export declare function normalizePiTraces(traces: ExecutionTraceV1[]): Trajector
12
12
  /** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
13
13
  export declare function normalizePrincipalAssuranceLedger(text: string): TrajectoryEventV1[];
14
14
  /**
15
- * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines and
16
- * ledgerVersion 2 runtime events emitted by 0.18.0. Version detection precedes
17
- * the legacy fallback so a new event can never be misdiagnosed as an old grant.
15
+ * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines,
16
+ * frozen ledgerVersion 2 events, and separately pinned production ledgerVersion
17
+ * 3 events. Explicit version dispatch precedes legacy fallback, so an unknown or
18
+ * malformed new record is never reinterpreted as an old grant.
18
19
  */
20
+ /** Historical selector behavior: unversioned 0.17 and frozen ledger v2 only. */
21
+ export declare function normalizePiDaddyLegacyLedger(text: string): TrajectoryEventV1[];
22
+ /** Separately versioned selector: every line must be an explicit ledgerVersion 3 event. */
23
+ export declare function normalizePiDaddyLedgerV3(text: string): TrajectoryEventV1[];
24
+ /** Programmatic mixed-version dispatcher; source declarations use explicit selectors above. */
19
25
  export declare function normalizePiDaddyLedger(text: string): TrajectoryEventV1[];
20
26
  /**
21
- * pi-daddy's canonical refusal vocabulary, in the pinned contract's own order.
22
- *
23
- * This is a copy of `#/$defs/refusalCode` from the pinned schema and is exported
24
- * so `pi-daddy-contract.test.ts` can assert set equality against the producer
25
- * artifact — a hand-maintained second vocabulary without that drift assertion is
26
- * exactly how `GRANT_ID_MALFORMED` came to be rejected as "unsupported".
27
+ * pi-daddy's canonical refusal vocabulary, derived directly from the immutable
28
+ * pinned schema. The producer's `src/refusals.ts` is vendored and digest-checked
29
+ * separately, while the real-builder verifier proves its production export is
30
+ * set-equal to this schema-derived runtime set. There is no second list here to
31
+ * forget when pi-daddy adds a contract-valid code.
27
32
  */
28
33
  export declare const V2_REFUSAL_CODES: Set<string>;
34
+ export declare const V3_REFUSAL_CODES: Set<string>;
29
35
  /**
30
36
  * Every vocabulary the adapter restates from the pinned contract, paired with the
31
37
  * place in the schema it must equal.