@gyoungwe/pi-evidence 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @gyoungwe/pi-evidence
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Portable evidence contracts for native Pi and Drone Desktop. The package is a
4
+ read-only boundary: it validates content-addressed artifact manifests,
5
+ provenance and digest declarations, immutable checkpoint and branch metadata,
6
+ evidence capsules, and replay/import proofs.
7
+
8
+ The package does not read files, write a ledger, create workspaces, execute a
9
+ workflow, or inherit execution authorization. A host supplies bytes and
10
+ persists validated records in its composition root. Capsule blobs are passed as
11
+ base64 strings or `Uint8Array` values, so a host can choose its own storage
12
+ policy. Absolute paths, traversal, credential names, `.pi`/`.drone` state and
13
+ other private state labels are rejected at the contract boundary.
14
+
15
+ The native Pi extension registers three read-only tools:
16
+
17
+ - `evidence_validate_manifest`
18
+ - `evidence_validate_capsule`
19
+ - `evidence_verify_replay`
20
+
21
+ `npm run check` runs the typecheck, deterministic self-contained build,
22
+ registration tests and a clean tarball install smoke. It does not publish the
23
+ package or call a model.
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "generator": "packages/pi-evidence/scripts/build.mjs",
4
+ "inputsSha256": "d789c68b05f48ae651bfcd4a717b044ef0cdb6cb03a9cb60be0d83c80e52781e",
5
+ "outputs": {
6
+ "dist/index.mjs": "0e8bc22875efda4e4a2ff4641c4e04424089c37cef60c99ac5a5049a2b56e85e"
7
+ }
8
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,598 @@
1
+ // @ts-nocheck
2
+ // src/digest.ts
3
+ import { createHash } from "node:crypto";
4
+ function stableJson(value) {
5
+ if (value === null || typeof value !== "object") {
6
+ const encoded = JSON.stringify(value);
7
+ return encoded === void 0 ? "null" : encoded;
8
+ }
9
+ if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
10
+ const object = value;
11
+ return `{${Object.keys(object).filter((key) => object[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
12
+ }
13
+ function canonicalJson(value) {
14
+ return `${stableJson(value)}
15
+ `;
16
+ }
17
+ function sha256Hex(value) {
18
+ return createHash("sha256").update(value).digest("hex");
19
+ }
20
+ function digestJson(value) {
21
+ return sha256Hex(canonicalJson(value));
22
+ }
23
+ function isSha256(value) {
24
+ return typeof value === "string" && /^[a-f0-9]{64}$/iu.test(value);
25
+ }
26
+
27
+ // src/artifacts.ts
28
+ var EVIDENCE_CAPSULE_LIMIT = 64 * 1024 * 1024;
29
+ var PRIVATE_PATH_PART = /^(?:\.env(?:\.|$)|auth\.json$|credentials?(?:\.|$)|secrets?(?:\.|$)|tokens?(?:\.|$)|password(?:\.|$)|\.ssh$|\.git$|\.pi$|\.drone$|userData$|sessions?$)/iu;
30
+ function isSafeEvidencePath(path) {
31
+ if (typeof path !== "string" || !path || path.includes("\0") || path.includes("\\")) return false;
32
+ if (path.startsWith("/") || path.startsWith("~") || /^[A-Za-z]:/.test(path)) return false;
33
+ const parts = path.split("/");
34
+ return parts.every(
35
+ (part) => part.length > 0 && part !== "." && part !== ".." && !PRIVATE_PATH_PART.test(part)
36
+ );
37
+ }
38
+ function isSafeBranchId(value) {
39
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/.test(value);
40
+ }
41
+ function bytesOf(entry) {
42
+ return entry.bytes ?? entry.size ?? 0;
43
+ }
44
+ function normalizeEntry(entry) {
45
+ return {
46
+ artifactId: entry.artifactId ?? "",
47
+ versionId: entry.versionId ?? "",
48
+ path: entry.path,
49
+ sha256: entry.sha256 ?? "",
50
+ bytes: bytesOf(entry),
51
+ role: entry.role ?? "output",
52
+ ...entry.accessLevel ? { accessLevel: entry.accessLevel } : {},
53
+ parentVersionIds: [...entry.parentVersionIds ?? []],
54
+ ...entry.producedByJobId ? { producedByJobId: entry.producedByJobId } : {}
55
+ };
56
+ }
57
+ function sourceEntries(manifest) {
58
+ if (Array.isArray(manifest)) return manifest;
59
+ return manifest.entries;
60
+ }
61
+ function validateArtifactManifest(manifest, options = {}) {
62
+ const source = sourceEntries(manifest);
63
+ const errors = [];
64
+ const objectManifest = Array.isArray(manifest) ? void 0 : manifest;
65
+ if (objectManifest && objectManifest.version !== 1) errors.push("Unsupported artifact manifest version");
66
+ const entries = source.map(normalizeEntry);
67
+ const verifications = [];
68
+ const paths = /* @__PURE__ */ new Set();
69
+ const versions = /* @__PURE__ */ new Set();
70
+ const expectedByPath = new Map((options.expected ?? source).map((entry) => [entry.path, entry]));
71
+ let totalBytes = 0;
72
+ for (const entry of entries) {
73
+ const path = entry.path;
74
+ const pathKey = path.toLocaleLowerCase("en-US");
75
+ const bytes = entry.bytes;
76
+ totalBytes += Number.isFinite(bytes) ? bytes : 0;
77
+ let reason;
78
+ if (!isSafeEvidencePath(path)) {
79
+ reason = path.startsWith("/") || path.startsWith("~") || /^[A-Za-z]:/.test(path) ? "private-path" : "invalid-path";
80
+ errors.push(`Unsafe evidence path: ${path}`);
81
+ }
82
+ if (paths.has(pathKey) || [...paths].some((known) => known.startsWith(`${pathKey}/`) || pathKey.startsWith(`${known}/`))) {
83
+ reason = "invalid-path";
84
+ errors.push(`Duplicate or conflicting evidence path: ${path}`);
85
+ }
86
+ paths.add(pathKey);
87
+ if (options.requireIdentity && (!entry.artifactId || !entry.versionId || versions.has(entry.versionId))) {
88
+ errors.push(`Invalid or duplicate artifact identity: ${path}`);
89
+ }
90
+ versions.add(entry.versionId);
91
+ if (!Number.isSafeInteger(bytes) || bytes < 0) {
92
+ reason = "size-mismatch";
93
+ errors.push(`Invalid artifact size: ${path}`);
94
+ }
95
+ if (!isSha256(entry.sha256)) {
96
+ reason = "checksum-mismatch";
97
+ errors.push(`Invalid sha256: ${path}`);
98
+ }
99
+ if (!Array.isArray(entry.parentVersionIds)) errors.push(`Invalid parent version list: ${path}`);
100
+ if (!["input", "output", "code", "environment", "review"].includes(entry.role)) {
101
+ errors.push(`Invalid artifact role: ${path}`);
102
+ }
103
+ const expected = expectedByPath.get(path);
104
+ if (expected?.bytes !== void 0 && bytes > expected.bytes)
105
+ errors.push(`Artifact exceeds expected size: ${path}`);
106
+ verifications.push({ path, expectedSha256: entry.sha256, ok: !reason, ...reason ? { reason } : {} });
107
+ }
108
+ if (options.maxTotalBytes !== void 0 && totalBytes > options.maxTotalBytes)
109
+ errors.push(`Artifacts exceed ${options.maxTotalBytes} bytes`);
110
+ if (totalBytes > EVIDENCE_CAPSULE_LIMIT) errors.push("Evidence exceeds capsule limit");
111
+ if (objectManifest && objectManifest.totalBytes !== void 0 && objectManifest.totalBytes !== totalBytes)
112
+ errors.push("Artifact manifest totalBytes mismatch");
113
+ const result2 = verifications;
114
+ Object.assign(result2, { valid: errors.length === 0, errors, entries, totalBytes });
115
+ return result2;
116
+ }
117
+ function assertValidArtifactManifest(manifest, options = {}) {
118
+ const validation = validateArtifactManifest(manifest, {
119
+ ...options,
120
+ requireIdentity: options.requireIdentity ?? true
121
+ });
122
+ if (!validation.valid) throw new Error(`Invalid artifact manifest: ${validation.errors.join("; ")}`);
123
+ const entries = [...validation.entries].sort((a, b) => a.path.localeCompare(b.path));
124
+ const normalized = { version: 1, entries, totalBytes: validation.totalBytes };
125
+ return normalized;
126
+ }
127
+ function artifactManifestDigest(manifest) {
128
+ const normalized = assertValidArtifactManifest(manifest, { requireIdentity: false });
129
+ return digestJson(normalized);
130
+ }
131
+ var isSafeArtifactPath = isSafeEvidencePath;
132
+ function artifactManifestBytes(manifest) {
133
+ return manifest.entries.reduce((total, entry) => total + entry.bytes, 0);
134
+ }
135
+ function withinArtifactLimit(manifest, maxBytes) {
136
+ return Number.isFinite(maxBytes) && maxBytes >= 0 && artifactManifestBytes(manifest) <= maxBytes;
137
+ }
138
+ function validateArtifactEntry(entry) {
139
+ const result2 = validateArtifactManifest([entry]);
140
+ return result2[0]?.ok ? void 0 : result2[0]?.reason ?? "checksum-mismatch";
141
+ }
142
+ function artifactDigest(bytes) {
143
+ return sha256Hex(bytes);
144
+ }
145
+ function artifactFromBytes(input) {
146
+ if (!input.artifactId || !input.versionId || !isSafeEvidencePath(input.path))
147
+ throw new Error("Artifact identity or path is invalid");
148
+ const bytes = typeof input.bytes === "string" ? new TextEncoder().encode(input.bytes) : input.bytes;
149
+ return {
150
+ artifactId: input.artifactId,
151
+ versionId: input.versionId,
152
+ path: input.path,
153
+ sha256: artifactDigest(bytes),
154
+ bytes: bytes.byteLength,
155
+ role: input.role,
156
+ ...input.accessLevel ? { accessLevel: input.accessLevel } : {},
157
+ parentVersionIds: [...input.parentVersionIds ?? []],
158
+ ...input.producedByJobId ? { producedByJobId: input.producedByJobId } : {}
159
+ };
160
+ }
161
+
162
+ // src/provenance.ts
163
+ function validateProvenance(value) {
164
+ if (value.schemaVersion !== 1 || !Array.isArray(value.artifacts))
165
+ throw new Error("Invalid evidence provenance");
166
+ assertValidArtifactManifest({ version: 1, entries: value.artifacts }, { requireIdentity: true });
167
+ for (const [label, digest] of [
168
+ ["workflowDigest", value.workflowDigest],
169
+ ["methodDigest", value.methodDigest],
170
+ ["toolContractDigest", value.toolContractDigest],
171
+ ["containerDigest", value.containerDigest],
172
+ ["specSha256", value.specSha256]
173
+ ]) {
174
+ if (digest !== void 0 && !isSha256(digest)) throw new Error(`Invalid ${label}`);
175
+ }
176
+ if (value.sourceRefs?.some(
177
+ (ref) => typeof ref !== "string" || !ref.trim() || ref.startsWith("/") || ref.startsWith("~") || /^[A-Za-z]:[\\/]/.test(ref) || /(?:^|[/\\])(?:\.pi|\.drone|\.git|\.ssh|auth\.json|credentials?)(?:[/\\]|$)/iu.test(ref)
178
+ ))
179
+ throw new Error("Invalid provenance source reference");
180
+ }
181
+ function provenanceDigest(value) {
182
+ validateProvenance(value);
183
+ return digestJson({
184
+ ...value,
185
+ artifacts: [...value.artifacts].sort((a, b) => a.path.localeCompare(b.path))
186
+ });
187
+ }
188
+ function workflowDigest(value) {
189
+ return digestJson(value);
190
+ }
191
+ function methodDigest(value) {
192
+ return digestJson(value);
193
+ }
194
+ function toolContractDigest(value) {
195
+ return digestJson(value);
196
+ }
197
+ function containerDigest(value) {
198
+ return digestJson(value);
199
+ }
200
+ function createEvidenceProvenance(input) {
201
+ const value = {
202
+ schemaVersion: 1,
203
+ ...input,
204
+ artifacts: [...input.artifacts].sort((a, b) => a.path.localeCompare(b.path))
205
+ };
206
+ validateProvenance(value);
207
+ return value;
208
+ }
209
+
210
+ // src/checkpoints.ts
211
+ function checkpointBody(value) {
212
+ const { lineage: _lineage, analysis, ...rest } = value;
213
+ const { provenance, ...analysisRest } = analysis;
214
+ const checkpointProvenance = provenance ? (({ lineage: _lineage2, ...withoutLineage }) => withoutLineage)(
215
+ provenance
216
+ ) : void 0;
217
+ const contentAnalysis = {
218
+ version: analysisRest.version,
219
+ id: analysisRest.id,
220
+ question: analysisRest.question,
221
+ assay: analysisRest.assay,
222
+ mode: analysisRest.mode,
223
+ ...analysisRest.stage ? { stage: analysisRest.stage } : {},
224
+ ...analysisRest.status ? { status: analysisRest.status } : {},
225
+ ...analysisRest.capabilities ? { capabilities: [...analysisRest.capabilities] } : {},
226
+ ...analysisRest.adapter ? { adapter: { ...analysisRest.adapter } } : {},
227
+ ...checkpointProvenance ? { provenance: checkpointProvenance } : {},
228
+ ...analysisRest.review ? { review: { ...analysisRest.review, limitations: [...analysisRest.review.limitations] } } : {},
229
+ ...analysisRest.publicationRequested !== void 0 ? { publicationRequested: analysisRest.publicationRequested } : {},
230
+ ...analysisRest.events ? { events: analysisRest.events.map((event) => ({ ...event })) } : {}
231
+ };
232
+ const files = [...value.files].sort((left, right) => left.artifact.path.localeCompare(right.artifact.path));
233
+ return { ...rest, files, analysis: contentAnalysis };
234
+ }
235
+ function checkpointId(value) {
236
+ return digestJson(checkpointBody(value));
237
+ }
238
+ function validateAnalysis(value) {
239
+ if (value.version !== 1 || typeof value.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value.id) || typeof value.question !== "string" || !value.question.trim() || typeof value.assay !== "string" || !value.assay.trim() || value.mode !== "exploratory" && value.mode !== "inferential")
240
+ throw new Error("Invalid evidence analysis snapshot");
241
+ const rawAnalysis = value;
242
+ if ("branchOf" in rawAnalysis || "workspace" in rawAnalysis)
243
+ throw new Error("Evidence analysis snapshot cannot contain host workspace state");
244
+ if (value.provenance) validateProvenance(value.provenance);
245
+ if (value.events?.some((event) => typeof event.at !== "string" || typeof event.type !== "string"))
246
+ throw new Error("Invalid analysis event");
247
+ }
248
+ function validateCheckpoint(checkpoint) {
249
+ const { id, ...body } = checkpoint;
250
+ if (checkpoint.schemaVersion !== 1 || id !== checkpointId(body))
251
+ throw new Error("Checkpoint manifest checksum mismatch");
252
+ validateAnalysis(checkpoint.analysis);
253
+ if (Number.isNaN(Date.parse(checkpoint.createdAt))) throw new Error("Invalid checkpoint timestamp");
254
+ if (checkpoint.lineage && (!checkpoint.lineage.eventId || checkpoint.lineage.eventId.includes("/")))
255
+ throw new Error("Invalid checkpoint lineage");
256
+ if (checkpoint.environmentFingerprint !== void 0 && (typeof checkpoint.environmentFingerprint !== "string" || checkpoint.environmentFingerprint.length > 512))
257
+ throw new Error("Invalid environment fingerprint");
258
+ if (!Array.isArray(checkpoint.files) || checkpoint.files.length === 0 || checkpoint.files.length > 1e4)
259
+ throw new Error("Invalid evidence files");
260
+ const validation = assertValidArtifactManifest(
261
+ { version: 1, entries: checkpoint.files.map((file) => file.artifact) },
262
+ { requireIdentity: true }
263
+ );
264
+ if ((validation.totalBytes ?? 0) > EVIDENCE_CAPSULE_LIMIT)
265
+ throw new Error("Evidence exceeds capsule limit");
266
+ const kinds = /* @__PURE__ */ new Set(["input", "output", "code", "environment", "review"]);
267
+ for (const file of checkpoint.files) {
268
+ if (!kinds.has(file.kind) || file.kind !== file.artifact.role)
269
+ throw new Error("Evidence file kind does not match artifact role");
270
+ if (!isSafeEvidencePath(file.artifact.path) || !isSha256(file.artifact.sha256))
271
+ throw new Error("Invalid frozen artifact");
272
+ }
273
+ }
274
+ function createCheckpoint(input) {
275
+ const body = {
276
+ schemaVersion: 1,
277
+ ...input,
278
+ files: [...input.files].sort((left, right) => left.artifact.path.localeCompare(right.artifact.path))
279
+ };
280
+ const checkpoint = { ...body, id: checkpointId(body) };
281
+ validateCheckpoint(checkpoint);
282
+ return checkpoint;
283
+ }
284
+ function createBranch(input) {
285
+ if (!isSafeBranchId(input.id)) throw new Error("Unsafe evidence branch id");
286
+ validateCheckpoint(input.checkpoint);
287
+ if (Number.isNaN(Date.parse(input.createdAt))) throw new Error("Invalid branch timestamp");
288
+ const branch = {
289
+ schemaVersion: 1,
290
+ id: input.id,
291
+ checkpointId: input.checkpoint.id,
292
+ sourceAnalysisId: input.checkpoint.analysis.id,
293
+ status: input.status ?? "preparing",
294
+ createdAt: input.createdAt
295
+ };
296
+ validateBranch(branch);
297
+ return branch;
298
+ }
299
+ function validateBranch(branch) {
300
+ if (branch.schemaVersion !== 1 || !isSafeBranchId(branch.id) || !isSha256(branch.checkpointId) || !branch.sourceAnalysisId || !["preparing", "ready", "failed"].includes(branch.status) || Number.isNaN(Date.parse(branch.createdAt)))
301
+ throw new Error("Invalid evidence branch");
302
+ }
303
+ function toEvidenceAnalysisSnapshot(context) {
304
+ return {
305
+ version: 1,
306
+ id: context.id,
307
+ question: context.question,
308
+ assay: context.assay,
309
+ mode: context.mode,
310
+ stage: context.stage,
311
+ status: context.status,
312
+ capabilities: context.capabilities ? [...context.capabilities] : [],
313
+ ...context.adapter ? { adapter: { ...context.adapter } } : {},
314
+ ...context.provenance ? { provenance: context.provenance } : {},
315
+ ...context.review ? {
316
+ review: {
317
+ approved: context.review.approved,
318
+ detail: context.review.detail,
319
+ limitations: [...context.review.limitations]
320
+ }
321
+ } : {},
322
+ publicationRequested: context.publicationRequested,
323
+ events: context.events ? context.events.map((event) => ({ ...event })) : []
324
+ };
325
+ }
326
+
327
+ // src/capsule.ts
328
+ var CAPSULE_LIMIT = EVIDENCE_CAPSULE_LIMIT;
329
+ var audienceRank = (value) => value === "public" ? 0 : value === "restricted" ? 1 : 2;
330
+ function decodeBase64(value) {
331
+ if (!value || value.length > EVIDENCE_CAPSULE_LIMIT * 2 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
332
+ throw new Error("Invalid or oversized capsule blob");
333
+ const data = Uint8Array.from(Buffer.from(value, "base64"));
334
+ if (Buffer.from(data).toString("base64") !== value) throw new Error("Invalid capsule base64");
335
+ return data;
336
+ }
337
+ function encodeBlob(value) {
338
+ return typeof value === "string" ? value : Buffer.from(value).toString("base64");
339
+ }
340
+ function eligibleFiles(capsule) {
341
+ const audience = capsule.publication?.audience ?? "private";
342
+ return capsule.checkpoint.files.filter((file) => audienceRank(file.artifact.accessLevel ?? "private") <= audienceRank(audience)).map((file) => file.artifact);
343
+ }
344
+ function validateCapsule(capsule) {
345
+ if (capsule.format !== "drone-evidence-capsule" || capsule.version !== 1)
346
+ throw new Error("Unsupported capsule version");
347
+ validateCheckpoint(capsule.checkpoint);
348
+ const audience = capsule.publication?.audience ?? "private";
349
+ if (!["public", "restricted", "private"].includes(audience))
350
+ throw new Error("Invalid capsule publication audience");
351
+ const omittedPaths = capsule.checkpoint.files.filter((file) => audienceRank(file.artifact.accessLevel ?? "private") > audienceRank(audience)).map((file) => file.artifact.path).sort();
352
+ if (capsule.publication && canonicalJson([...capsule.publication.omittedPaths].sort()) !== canonicalJson(omittedPaths))
353
+ throw new Error("Capsule publication omission list mismatch");
354
+ const expected = new Set(eligibleFiles(capsule).map((artifact) => artifact.sha256));
355
+ if (!capsule.blobs || typeof capsule.blobs !== "object") throw new Error("Missing capsule blobs");
356
+ for (const [hash, encoded] of Object.entries(capsule.blobs)) {
357
+ if (!isSha256(hash) || !expected.has(hash)) throw new Error("Unexpected capsule blob");
358
+ const data = decodeBase64(encoded);
359
+ const artifact = capsule.checkpoint.files.find((file) => file.artifact.sha256 === hash)?.artifact;
360
+ if (!artifact || data.byteLength !== artifact.bytes || sha256Hex(data) !== hash)
361
+ throw new Error("Capsule blob checksum mismatch");
362
+ }
363
+ for (const artifact of eligibleFiles(capsule))
364
+ if (typeof capsule.blobs[artifact.sha256] !== "string") throw new Error("Missing capsule blob");
365
+ if (Object.keys(capsule.blobs).length !== expected.size) throw new Error("Unexpected capsule blobs");
366
+ }
367
+ function createEvidenceCapsule(checkpoint, blobs, audience = "private") {
368
+ validateCheckpoint(checkpoint);
369
+ const omittedPaths = checkpoint.files.filter((file) => audienceRank(file.artifact.accessLevel ?? "private") > audienceRank(audience)).map((file) => file.artifact.path).sort();
370
+ const selected = {};
371
+ for (const artifact of checkpoint.files.map((file) => file.artifact)) {
372
+ if (audienceRank(artifact.accessLevel ?? "private") > audienceRank(audience)) continue;
373
+ const value = blobs[artifact.sha256];
374
+ if (value === void 0) throw new Error(`Missing blob for ${artifact.path}`);
375
+ const encoded = encodeBlob(value);
376
+ const bytes = decodeBase64(encoded);
377
+ if (bytes.byteLength !== artifact.bytes || sha256Hex(bytes) !== artifact.sha256)
378
+ throw new Error(`Blob does not match ${artifact.path}`);
379
+ selected[artifact.sha256] = encoded;
380
+ }
381
+ const capsule = {
382
+ format: "drone-evidence-capsule",
383
+ version: 1,
384
+ checkpoint,
385
+ blobs: selected,
386
+ ...audience === "private" ? {} : { publication: { audience, omittedPaths } }
387
+ };
388
+ validateCapsule(capsule);
389
+ return capsule;
390
+ }
391
+ function serializeCapsule(capsule) {
392
+ validateCapsule(capsule);
393
+ return canonicalJson(canonicalCapsule(capsule));
394
+ }
395
+ function canonicalCapsule(capsule) {
396
+ if (!capsule.publication) return capsule;
397
+ return {
398
+ ...capsule,
399
+ publication: {
400
+ ...capsule.publication,
401
+ omittedPaths: [...capsule.publication.omittedPaths].sort()
402
+ }
403
+ };
404
+ }
405
+ function parseCapsule(serialized) {
406
+ if (serialized.length > EVIDENCE_CAPSULE_LIMIT * 2) throw new Error("Capsule exceeds import limit");
407
+ let parsed;
408
+ try {
409
+ parsed = JSON.parse(serialized);
410
+ } catch {
411
+ throw new Error("Invalid evidence capsule JSON");
412
+ }
413
+ validateCapsule(parsed);
414
+ return parsed;
415
+ }
416
+ function validateCapsuleImport(serialized) {
417
+ const capsule = typeof serialized === "string" ? parseCapsule(serialized) : serialized;
418
+ validateCapsule(capsule);
419
+ return capsule.checkpoint;
420
+ }
421
+ function capsuleDigest(capsule) {
422
+ validateCapsule(capsule);
423
+ return digestJson(canonicalCapsule(capsule));
424
+ }
425
+ function capsuleBytes(capsule) {
426
+ return new TextEncoder().encode(serializeCapsule(capsule));
427
+ }
428
+
429
+ // src/replay.ts
430
+ function outputFiles(checkpoint) {
431
+ return checkpoint.files.filter((file) => file.kind === "output").map((file) => file.artifact);
432
+ }
433
+ function validateOutput(value) {
434
+ if (!isSafeEvidencePath(value.path) || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || !isSha256(value.sha256))
435
+ throw new Error(`Invalid replay output: ${value.path}`);
436
+ }
437
+ function verifyReplay(checkpoint, input) {
438
+ validateCheckpoint(checkpoint);
439
+ const seenOutputs = /* @__PURE__ */ new Set();
440
+ for (const output of input.outputs) {
441
+ validateOutput(output);
442
+ if (seenOutputs.has(output.path)) throw new Error(`Duplicate replay output: ${output.path}`);
443
+ seenOutputs.add(output.path);
444
+ }
445
+ const expected = outputFiles(checkpoint);
446
+ const actualByPath = new Map(input.outputs.map((output) => [output.path, output]));
447
+ const checks = expected.map((artifact) => {
448
+ const actual = actualByPath.get(artifact.path);
449
+ return {
450
+ path: artifact.path,
451
+ matched: !!actual && actual.bytes === artifact.bytes && actual.sha256.toLowerCase() === artifact.sha256.toLowerCase()
452
+ };
453
+ });
454
+ const expectedPaths = new Set(expected.map((artifact) => artifact.path));
455
+ const unexpected = input.outputs.some((output) => !expectedPaths.has(output.path));
456
+ const environmentMatched = checkpoint.environmentFingerprint !== void 0 && input.environmentFingerprint === checkpoint.environmentFingerprint;
457
+ const reproduced = environmentMatched && checks.length === expected.length && checks.every((check) => check.matched) && !unexpected;
458
+ return {
459
+ status: reproduced ? "reproduced" : "failed",
460
+ detail: reproduced ? "Exact output bytes and environment matched" : "Output or environment differs from frozen evidence",
461
+ outputChecks: checks,
462
+ environmentMatched
463
+ };
464
+ }
465
+ function createReplayReceipt(input) {
466
+ const verification = verifyReplay(input.checkpoint, {
467
+ environmentFingerprint: input.environmentFingerprint,
468
+ outputs: input.outputs
469
+ });
470
+ if (!input.id || !input.completedAt || Number.isNaN(Date.parse(input.completedAt)))
471
+ throw new Error("Invalid replay receipt identity");
472
+ return { ...verification, id: input.id, checkpointId: input.checkpoint.id, completedAt: input.completedAt };
473
+ }
474
+ function validateReplayReceipt(receipt) {
475
+ if (!receipt.id || !isSha256(receipt.checkpointId) || Number.isNaN(Date.parse(receipt.completedAt)) || receipt.status === "reproduced" && (!receipt.environmentMatched || receipt.outputChecks.some((check) => !check.matched)))
476
+ throw new Error("Invalid replay receipt");
477
+ const seenPaths = /* @__PURE__ */ new Set();
478
+ for (const check of receipt.outputChecks) {
479
+ if (!isSafeEvidencePath(check.path) || seenPaths.has(check.path))
480
+ throw new Error("Invalid replay output check path");
481
+ seenPaths.add(check.path);
482
+ }
483
+ }
484
+ var replayProof = verifyReplay;
485
+
486
+ // src/index.ts
487
+ function result(details) {
488
+ return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], details };
489
+ }
490
+ function readObject(value, label) {
491
+ if (!value || typeof value !== "object" || Array.isArray(value))
492
+ throw new Error(`${label} must be an object`);
493
+ return value;
494
+ }
495
+ function registerPiEvidence(pi) {
496
+ pi.registerTool({
497
+ name: "evidence_validate_manifest",
498
+ label: "Evidence \xB7 validate manifest",
499
+ description: "Validate content-addressed artifact metadata without reading files or writing state.",
500
+ drone: { readOnly: true, recoverySafe: true, capabilities: ["evidence", "files"] },
501
+ parameters: { type: "object", properties: { manifest: { type: "object" } }, required: ["manifest"] },
502
+ async execute(_id, params) {
503
+ const input = readObject(params, "params");
504
+ return result(validateArtifactManifest(input.manifest, { requireIdentity: true }));
505
+ }
506
+ });
507
+ pi.registerTool({
508
+ name: "evidence_validate_capsule",
509
+ label: "Evidence \xB7 validate capsule",
510
+ description: "Validate an imported evidence capsule, omission list and every blob digest without importing it.",
511
+ drone: { readOnly: true, recoverySafe: true, capabilities: ["evidence", "files"] },
512
+ parameters: {
513
+ type: "object",
514
+ properties: { capsule: { type: "object" }, serialized: { type: "string" } }
515
+ },
516
+ async execute(_id, params) {
517
+ const input = readObject(params, "params");
518
+ const checkpoint = validateCapsuleImport(
519
+ input.serialized !== void 0 ? String(input.serialized) : input.capsule
520
+ );
521
+ return result({ valid: true, checkpointId: checkpoint.id, analysisId: checkpoint.analysis.id });
522
+ }
523
+ });
524
+ pi.registerTool({
525
+ name: "evidence_verify_replay",
526
+ label: "Evidence \xB7 verify replay",
527
+ description: "Compare host-provided output metadata and environment fingerprint with a frozen checkpoint.",
528
+ drone: { readOnly: true, recoverySafe: true, capabilities: ["evidence", "files"] },
529
+ parameters: {
530
+ type: "object",
531
+ properties: {
532
+ checkpoint: { type: "object" },
533
+ environmentFingerprint: { type: "string" },
534
+ outputs: { type: "array" }
535
+ },
536
+ required: ["checkpoint", "outputs"]
537
+ },
538
+ async execute(_id, params) {
539
+ const input = readObject(params, "params");
540
+ return result(
541
+ verifyReplay(input.checkpoint, {
542
+ environmentFingerprint: input.environmentFingerprint === void 0 ? void 0 : String(input.environmentFingerprint),
543
+ outputs: Array.isArray(input.outputs) ? input.outputs : []
544
+ })
545
+ );
546
+ }
547
+ });
548
+ return {
549
+ tools: ["evidence_validate_manifest", "evidence_validate_capsule", "evidence_verify_replay"]
550
+ };
551
+ }
552
+ var index_default = registerPiEvidence;
553
+ export {
554
+ CAPSULE_LIMIT,
555
+ EVIDENCE_CAPSULE_LIMIT,
556
+ artifactDigest,
557
+ artifactFromBytes,
558
+ artifactManifestBytes,
559
+ artifactManifestDigest,
560
+ assertValidArtifactManifest,
561
+ canonicalJson,
562
+ capsuleBytes,
563
+ capsuleDigest,
564
+ checkpointId,
565
+ containerDigest,
566
+ createBranch,
567
+ createCheckpoint,
568
+ createEvidenceCapsule,
569
+ createEvidenceProvenance,
570
+ createReplayReceipt,
571
+ index_default as default,
572
+ digestJson,
573
+ isSafeArtifactPath,
574
+ isSafeBranchId,
575
+ isSafeEvidencePath,
576
+ isSha256,
577
+ methodDigest,
578
+ parseCapsule,
579
+ provenanceDigest,
580
+ registerPiEvidence,
581
+ replayProof,
582
+ serializeCapsule,
583
+ sha256Hex,
584
+ stableJson,
585
+ toEvidenceAnalysisSnapshot,
586
+ toolContractDigest,
587
+ validateArtifactEntry,
588
+ validateArtifactManifest,
589
+ validateBranch,
590
+ validateCapsule,
591
+ validateCapsuleImport,
592
+ validateCheckpoint,
593
+ validateProvenance,
594
+ validateReplayReceipt,
595
+ verifyReplay,
596
+ withinArtifactLimit,
597
+ workflowDigest
598
+ };
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "@gyoungwe/pi-evidence",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Portable evidence manifests, provenance, checkpoints and replay proofs for native Pi",
5
+ "keywords": ["pi-package", "evidence", "provenance", "reproducibility"],
6
+ "license": "MIT",
7
+ "publishConfig": {"access": "public"},
8
+ "repository": {"type": "git", "url": "git+https://github.com/Gyoungwe/drone-plugins.git", "directory": "packages/pi-evidence"},
9
+ "type": "module",
10
+ "main": "./dist/index.mjs",
11
+ "files": ["dist", "src", "package.json", "README.md", "build-manifest.json", "plugin.manifest.json"],
12
+ "pi": {"extensions": ["./dist/index.mjs"]},
13
+ "exports": {".": {"types": "./src/index.ts", "import": "./dist/index.mjs", "default": "./dist/index.mjs"}},
14
+ "scripts": {"build": "node scripts/build.mjs", "build:check": "node scripts/build.mjs --check", "typecheck": "tsc --noEmit", "test": "npm run build && node --test test/registration.test.mjs test/contracts.test.mjs", "smoke": "node scripts/smoke.mjs", "check": "npm run typecheck && npm run build:check && npm test && npm run smoke"},
15
+ "peerDependencies": {"@earendil-works/pi-coding-agent": ">=1.1.0 <2.0.0"},
16
+ "dependencies": {"@gyoungwe/plugin-api": "^1.0.0"},
17
+ "devDependencies": {"typescript": "^5.9.0", "esbuild": "^0.25.12"}
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "drone-pi-evidence",
4
+ "version": "0.1.0",
5
+ "pi": {
6
+ "apiVersion": 1,
7
+ "minHostVersion": "1.1.0",
8
+ "testedHosts": ["1.1.0", "latest"]
9
+ },
10
+ "drone": {
11
+ "pluginApi": 1,
12
+ "resultContract": "drone-result/v3"
13
+ },
14
+ "entrypoints": { "extension": "./dist/index.mjs" },
15
+ "capabilities": ["evidence", "provenance", "replay", "files"],
16
+ "hosts": ["native-pi", "drone-desktop"],
17
+ "permissions": {
18
+ "filesystem": [],
19
+ "network": [],
20
+ "subprocess": [],
21
+ "externalWrites": false
22
+ }
23
+ }