engineering-behavior-observatory 0.2.1
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 +202 -0
- package/README.md +151 -0
- package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
- package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
- package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
- package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
- package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
- package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
- package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
- package/contracts/openhands-agent-server-v1.44.1.json +65 -0
- package/contracts/openhands-agent-server-v1.46.0.json +46 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
- package/dist/src/agent-sdk-normalizer.d.ts +22 -0
- package/dist/src/agent-sdk-normalizer.js +995 -0
- package/dist/src/agent-sdk-run.d.ts +31 -0
- package/dist/src/agent-sdk-run.js +242 -0
- package/dist/src/agent-sdk-runner.d.ts +96 -0
- package/dist/src/agent-sdk-runner.js +407 -0
- package/dist/src/agent-sdk.d.ts +294 -0
- package/dist/src/agent-sdk.js +644 -0
- package/dist/src/aggregation.d.ts +183 -0
- package/dist/src/aggregation.js +513 -0
- package/dist/src/artifacts.d.ts +36 -0
- package/dist/src/artifacts.js +1372 -0
- package/dist/src/atlas-grafana.d.ts +377 -0
- package/dist/src/atlas-grafana.js +47 -0
- package/dist/src/atlas-html.d.ts +2 -0
- package/dist/src/atlas-html.js +34 -0
- package/dist/src/atlas.d.ts +130 -0
- package/dist/src/atlas.js +235 -0
- package/dist/src/behavior-assertions.d.ts +95 -0
- package/dist/src/behavior-assertions.js +154 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.js +957 -0
- package/dist/src/codex-judge.d.ts +44 -0
- package/dist/src/codex-judge.js +214 -0
- package/dist/src/codex-run.d.ts +114 -0
- package/dist/src/codex-run.js +519 -0
- package/dist/src/codex.d.ts +187 -0
- package/dist/src/codex.js +1028 -0
- package/dist/src/contracts.d.ts +155 -0
- package/dist/src/contracts.js +866 -0
- package/dist/src/corpus.d.ts +67 -0
- package/dist/src/corpus.js +540 -0
- package/dist/src/cursor-sdk-runner.d.ts +81 -0
- package/dist/src/cursor-sdk-runner.js +362 -0
- package/dist/src/cursor-sdk.d.ts +81 -0
- package/dist/src/cursor-sdk.js +1078 -0
- package/dist/src/deepseek-adapter.d.ts +152 -0
- package/dist/src/deepseek-adapter.js +777 -0
- package/dist/src/exports.d.ts +69 -0
- package/dist/src/exports.js +966 -0
- package/dist/src/human-calibration.d.ts +167 -0
- package/dist/src/human-calibration.js +618 -0
- package/dist/src/index.d.ts +71 -0
- package/dist/src/index.js +38 -0
- package/dist/src/lifecycle.d.ts +196 -0
- package/dist/src/lifecycle.js +1889 -0
- package/dist/src/normalization-integrity.d.ts +128 -0
- package/dist/src/normalization-integrity.js +429 -0
- package/dist/src/openhands-run.d.ts +36 -0
- package/dist/src/openhands-run.js +415 -0
- package/dist/src/openhands.d.ts +130 -0
- package/dist/src/openhands.js +859 -0
- package/dist/src/pi.d.ts +203 -0
- package/dist/src/pi.js +1345 -0
- package/dist/src/process-protocol.d.ts +245 -0
- package/dist/src/process-protocol.js +1261 -0
- package/dist/src/retained-evidence.d.ts +12 -0
- package/dist/src/retained-evidence.js +201 -0
- package/dist/src/run-bundles.d.ts +193 -0
- package/dist/src/run-bundles.js +993 -0
- package/dist/src/scheduler.d.ts +109 -0
- package/dist/src/scheduler.js +1080 -0
- package/dist/src/semantic-judge.d.ts +194 -0
- package/dist/src/semantic-judge.js +875 -0
- package/dist/src/structural-observations.d.ts +124 -0
- package/dist/src/structural-observations.js +671 -0
- package/dist/src/task-packets.d.ts +115 -0
- package/dist/src/task-packets.js +683 -0
- package/dist/src/uniform-events.d.ts +120 -0
- package/dist/src/uniform-events.js +158 -0
- package/dist/src/verifiers.d.ts +116 -0
- package/dist/src/verifiers.js +819 -0
- package/dist/src/workspaces.d.ts +56 -0
- package/dist/src/workspaces.js +1314 -0
- package/docs/README.md +60 -0
- package/docs/development/README.md +71 -0
- package/docs/development/documentation-sync.md +58 -0
- package/docs/development/extension-contracts.md +307 -0
- package/docs/evaluation/README.md +48 -0
- package/docs/evaluation/aggregation.md +112 -0
- package/docs/evaluation/behavior-assertions.md +45 -0
- package/docs/evaluation/human-calibration.md +170 -0
- package/docs/evaluation/normalization-integrity.md +80 -0
- package/docs/evaluation/semantic-judge.md +188 -0
- package/docs/evaluation/structural-observations.md +84 -0
- package/docs/evaluation/uniform-events.md +131 -0
- package/docs/guides/atlas.md +144 -0
- package/docs/guides/evidence-and-sharing.md +100 -0
- package/docs/guides/operator-guide.md +424 -0
- package/docs/guides/quickstart.md +152 -0
- package/docs/guides/telemetry.md +66 -0
- package/docs/harnesses/README.md +31 -0
- package/docs/harnesses/claude-agent-sdk.md +61 -0
- package/docs/harnesses/codex-harness.md +174 -0
- package/docs/harnesses/cursor-sdk.md +149 -0
- package/docs/harnesses/deepseek-harness.md +134 -0
- package/docs/harnesses/openhands-agent-server.md +153 -0
- package/docs/harnesses/pi-sdk.md +66 -0
- package/docs/reference/README.md +19 -0
- package/docs/reference/agent-sdk-operational-runner.md +118 -0
- package/docs/reference/cli.md +114 -0
- package/docs/reference/contracts.md +222 -0
- package/docs/reference/run-bundle-contract.md +354 -0
- package/docs/reference/run-lifecycle.md +54 -0
- package/examples/cursor-sdk/README.md +11 -0
- package/examples/cursor-sdk/capture-profile.json +1 -0
- package/examples/cursor-sdk/harness.json +1 -0
- package/examples/cursor-sdk/model.json +1 -0
- package/examples/cursor-sdk/native-limits.json +1 -0
- package/examples/cursor-sdk/native-tool-policy.json +1 -0
- package/ontology/behavior-categories.v1.json +46 -0
- package/package.json +66 -0
- package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
- package/release/0.1.0/README.md +46 -0
- package/release/0.1.0/reproducibility.json +61 -0
- package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
- package/release/0.2.0/README.md +42 -0
- package/release/0.2.0/reproducibility.json +73 -0
- package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
- package/release/0.2.1/README.md +63 -0
- package/release/0.2.1/reproducibility.json +73 -0
- package/release/README.md +13 -0
- package/schemas/aggregation.v1.json +293 -0
- package/schemas/behavior-assertions.v1.json +220 -0
- package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
- package/schemas/experiment.v1.schema.json +188 -0
- package/schemas/human-calibration.v1.json +336 -0
- package/schemas/normalization-integrity.v1.json +302 -0
- package/schemas/run-bundles/v1.json +1040 -0
- package/schemas/run-queue.v1.schema.json +206 -0
- package/schemas/semantic-judge.v1.json +380 -0
- package/schemas/structural-observations.v1.json +131 -0
- package/schemas/task-packet-freeze.v1.schema.json +106 -0
- package/schemas/task-packet.v1.schema.json +234 -0
- package/schemas/uniform-events/v1.json +289 -0
- package/scripts/atlas-grafana.sh +18 -0
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir, userInfo } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { assertNoDuplicateJsonKeys, assertUniqueArtifactIdentities, canonicalizeMetadata, digestMetadata, inspectRetainedArtifact, readVerifiedArtifact, validateArtifact, validateRunManifestEvidence, writeArtifactAtomically, writeMetadataAtomically, } from "./artifacts.js";
|
|
6
|
+
/** Sanitize a caller-allowlisted derived document; this does not grant sharing approval. */
|
|
7
|
+
export function sanitizeDerivedExport(value, policy, correlations = []) {
|
|
8
|
+
validatePolicy(policy);
|
|
9
|
+
const sensitive = effectiveSensitiveValues(policy);
|
|
10
|
+
const replacements = new Map(correlations.map((source) => [source, `ref-${createHash("sha256").update(source).digest("hex").slice(0, 20)}`]));
|
|
11
|
+
// Derived views may embed cited session envelopes; apply native reasoning omission too.
|
|
12
|
+
const bytes = sanitizeArtifact(Buffer.from(canonicalizeMetadata(value)), "application/json", policy, replacements, sensitive, [homedir(), userInfo().username], new Map(), "session");
|
|
13
|
+
scanPortableTree([{ bytes, mediaType: "application/json" }], sensitive, correlations);
|
|
14
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
15
|
+
}
|
|
16
|
+
const MAX_SOURCE_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
const HIDDEN_FIELDS = new Set([
|
|
18
|
+
"chainofthought",
|
|
19
|
+
"extendedthinking",
|
|
20
|
+
"hiddenreasoning",
|
|
21
|
+
"encryptedreasoning",
|
|
22
|
+
"encryptedthinking",
|
|
23
|
+
"rawapibody",
|
|
24
|
+
"rawrequestbody",
|
|
25
|
+
"rawresponsebody",
|
|
26
|
+
"reasoning",
|
|
27
|
+
"reasoningcontent",
|
|
28
|
+
"reasoningdetails",
|
|
29
|
+
"reasoningsignature",
|
|
30
|
+
"thinking",
|
|
31
|
+
"thinkingcontent",
|
|
32
|
+
"thinkingsignature",
|
|
33
|
+
"textsignature",
|
|
34
|
+
"thoughtsignature",
|
|
35
|
+
]);
|
|
36
|
+
const CODEX_REASONING_DELTA_METHOD = "item/reasoning/textDelta";
|
|
37
|
+
const CODEX_REASONING_CONTENT_FIELDS = new Set(["content", "delta", "encryptedcontent", "summary", "text"]);
|
|
38
|
+
const CURSOR_REASONING_TYPES = new Set(["thinking", "thinkingdelta", "thinkingcompleted", "thinkingmessage"]);
|
|
39
|
+
const PI_PRIVATE_CONTENT_TYPES = new Set([
|
|
40
|
+
"thinking", "thinkingstart", "thinkingdelta", "thinkingend",
|
|
41
|
+
"reasoning", "reasoningstart", "reasoningdelta", "reasoningend",
|
|
42
|
+
]);
|
|
43
|
+
const PI_PRIVATE_CONTENT_FIELDS = new Set(["content", "delta", "encryptedcontent", "reasoning", "signature", "summary", "text", "thinking", "thinkingsignature"]);
|
|
44
|
+
const PI_PRIVATE_FIELDS = new Set(["encryptedreasoning", "encryptedthinking", "reasoningcontent", "reasoningdetails", "reasoningsignature", "thinkingcontent", "thinkingsignature", "textsignature", "thoughtsignature"]);
|
|
45
|
+
const SECRET_FIELDS = new Set([
|
|
46
|
+
"accesskey",
|
|
47
|
+
"accesstoken",
|
|
48
|
+
"apikey",
|
|
49
|
+
"authorization",
|
|
50
|
+
"blobencryptionkey",
|
|
51
|
+
"clientsecret",
|
|
52
|
+
"connectionstring",
|
|
53
|
+
"credential",
|
|
54
|
+
"credentials",
|
|
55
|
+
"credentialsjson",
|
|
56
|
+
"databaseurl",
|
|
57
|
+
"idtoken",
|
|
58
|
+
"oauthtoken",
|
|
59
|
+
"password",
|
|
60
|
+
"privatekey",
|
|
61
|
+
"refreshtoken",
|
|
62
|
+
"secret",
|
|
63
|
+
"secretaccesskey",
|
|
64
|
+
"token",
|
|
65
|
+
]);
|
|
66
|
+
// "eborunid"/"eboattemptid" are the normalized EBO OTel resource-attribute keys
|
|
67
|
+
// ("ebo.run.id"/"ebo.attempt.id") retained inside native telemetry evidence.
|
|
68
|
+
const CORRELATION_FIELDS = new Set(["attemptid", "bundleid", "id", "runid", "sessionid", "traceid", "eborunid", "eboattemptid"]);
|
|
69
|
+
const LOCAL_IDENTIFIER_FIELDS = new Set(["login", "owner", "user", "username"]);
|
|
70
|
+
const TRUNCATABLE_FIELDS = new Set([
|
|
71
|
+
"body",
|
|
72
|
+
"commandoutput",
|
|
73
|
+
"content",
|
|
74
|
+
"diff",
|
|
75
|
+
"filecontent",
|
|
76
|
+
"input",
|
|
77
|
+
"output",
|
|
78
|
+
"patch",
|
|
79
|
+
"prompt",
|
|
80
|
+
"stderr",
|
|
81
|
+
"stdout",
|
|
82
|
+
"text",
|
|
83
|
+
"toolinput",
|
|
84
|
+
"toolresult",
|
|
85
|
+
]);
|
|
86
|
+
const SECRET_PATTERNS = [
|
|
87
|
+
/()(?:-----BEGIN (?:(?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END (?:(?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----|$))()/gu,
|
|
88
|
+
/()(?:\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|sk-ant-[A-Za-z0-9_-]{12,}|AKIA[0-9A-Z]{16})\b)()/gu,
|
|
89
|
+
/((?:authorization)\s*[:=])(?!(?:\s*)\[REDACTED_)\s*[^\r\n]+()/giu,
|
|
90
|
+
/((?:authorization|api[_-]?key|(?:access|oauth|refresh|id)?[_-]?token|client[_-]?secret|private[_-]?key|secret[_-]?access[_-]?key|credentials?(?:[_-]?json)?|database[_-]?url|connection[_-]?string|password)\s*[:=]\s*")(?!\[REDACTED_SECRET\]")(?:\\.|[^"\\])*(")/giu,
|
|
91
|
+
/((?:authorization|api[_-]?key|(?:access|oauth|refresh|id)?[_-]?token|client[_-]?secret|private[_-]?key|secret[_-]?access[_-]?key|credentials?(?:[_-]?json)?|database[_-]?url|connection[_-]?string|password)\s*[:=]\s*')(?!\[REDACTED_SECRET\]')(?:\\.|[^'\\])*(')/giu,
|
|
92
|
+
/((?:authorization|api[_-]?key|(?:access|oauth|refresh|id)?[_-]?token|client[_-]?secret|private[_-]?key|secret[_-]?access[_-]?key|credentials?(?:[_-]?json)?|database[_-]?url|connection[_-]?string|password)\s*[:=])(?!(?:\s*(?:Bearer\s+)?)\[REDACTED_)\s*(?:Bearer\s+)?[^\s,"'}\]]{8,}()/giu,
|
|
93
|
+
];
|
|
94
|
+
const LOCAL_IDENTIFIER_PATTERNS = [
|
|
95
|
+
/((?:user(?:name)?|owner|login)\s*[:=]\s*")(?!\[LOCAL_USER\]")(?:\\.|[^"\\])*(")/giu,
|
|
96
|
+
/((?:user(?:name)?|owner|login)\s*[:=]\s*')(?!\[LOCAL_USER\]')(?:\\.|[^'\\])*(')/giu,
|
|
97
|
+
/((?:user(?:name)?|owner|login)\s*[:=])(?!(?:\s*)\[LOCAL_USER\])\s*[^\s,"'}\]]+()/giu,
|
|
98
|
+
];
|
|
99
|
+
const LOCAL_PATH = /(^|[\s"'=:(+\-])(?:[A-Za-z]:\\(?:[^\\\s"']+\\)*[^\\\s"']*|\/(?!\/)[^\s"']+)/gu;
|
|
100
|
+
const LOCAL_HOME_PATH = /(?:^|[\s`"'=:(+\-]|file:\/\/)(?:[A-Za-z]:\\+Users\\+[^\\\s`"']+(?=[\\\s`"',;:)}\]]|$)|\/(?:Users|home)\/[^/\s`"']+(?=[/\s`"',;:)}\]]|$)|\/root(?=[/\s`"',;:)}\]]|$))/giu;
|
|
101
|
+
/** Create one separately rooted, sanitized derivative of an M2 run bundle. */
|
|
102
|
+
export async function createPortableRunBundleExport(options) {
|
|
103
|
+
validatePolicy(options.policy);
|
|
104
|
+
const sourceRoot = resolve(options.sourceRoot);
|
|
105
|
+
const destinationRoot = resolve(options.destinationRoot);
|
|
106
|
+
const destinationWithinSource = relative(sourceRoot, destinationRoot);
|
|
107
|
+
if (destinationWithinSource === "" || (!destinationWithinSource.startsWith("..") && !isAbsolute(destinationWithinSource))) {
|
|
108
|
+
throw new Error("Portable export destination must be outside the restricted source bundle.");
|
|
109
|
+
}
|
|
110
|
+
const { manifest: sourceManifest, digest: sourceManifestDigest } = await readSourceManifest(sourceRoot);
|
|
111
|
+
const sourceErrors = [
|
|
112
|
+
...validateArtifact("manifest.json", sourceManifest),
|
|
113
|
+
...validateRunManifestEvidence("manifest.json", sourceManifest, sourceRoot),
|
|
114
|
+
];
|
|
115
|
+
if (sourceErrors.length > 0)
|
|
116
|
+
throw new Error(formatValidationErrors(sourceErrors));
|
|
117
|
+
for (const descriptor of sourceManifest.evidence) {
|
|
118
|
+
if (descriptor.sharingClass === "unknown") {
|
|
119
|
+
throw new Error(`Artifact ${descriptor.id} has an unknown sharing classification.`);
|
|
120
|
+
}
|
|
121
|
+
if (!["restricted", "internal"].includes(descriptor.sharingClass)) {
|
|
122
|
+
throw new Error(`Artifact ${descriptor.id} is not a restricted or internal M2 source artifact.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const policyDigest = digestString(digestMetadata(policyRecord(options.policy)));
|
|
126
|
+
const correlations = correlationProjection(sourceManifest, policyDigest);
|
|
127
|
+
const replacements = correlationReplacements(sourceManifest, correlations);
|
|
128
|
+
const sensitiveValues = effectiveSensitiveValues(options.policy);
|
|
129
|
+
const localIdentifiers = [homedir(), userInfo().username].filter((value) => value.length > 1);
|
|
130
|
+
const artifacts = [];
|
|
131
|
+
const transformations = [];
|
|
132
|
+
const excludedArtifacts = [];
|
|
133
|
+
await mkdir(dirname(destinationRoot), { recursive: true, mode: 0o700 });
|
|
134
|
+
await mkdir(destinationRoot, { mode: 0o700 });
|
|
135
|
+
try {
|
|
136
|
+
const portableDiagnostics = new Map();
|
|
137
|
+
let diagnosticIndex = sourceManifest.evidence.length;
|
|
138
|
+
for (const verifier of sourceManifest.evidence.filter(({ kind }) => kind === "verifier")) {
|
|
139
|
+
const verifierBytes = await readVerifiedArtifact(sourceRoot, verifier.relativePath, digestValue(verifier.digest), MAX_SOURCE_ARTIFACT_BYTES);
|
|
140
|
+
for (const [index, diagnostic] of sourceDiagnostics(parseJson(verifierBytes, "Verifier evidence")).entries()) {
|
|
141
|
+
if (portableDiagnostics.has(diagnostic.locator)) {
|
|
142
|
+
throw new Error(`Verifier diagnostic ${diagnostic.locator} is referenced more than once.`);
|
|
143
|
+
}
|
|
144
|
+
const sourceBytes = await readVerifiedArtifact(sourceRoot, diagnostic.locator, digestValue(diagnostic.digest), MAX_SOURCE_ARTIFACT_BYTES);
|
|
145
|
+
if (sourceBytes.length !== diagnostic.sizeBytes) {
|
|
146
|
+
throw new Error(`Verifier diagnostic ${diagnostic.locator} size does not match its source reference.`);
|
|
147
|
+
}
|
|
148
|
+
const id = `diagnostic-${createHash("sha256").update(`${verifier.id}\0${String(index)}\0${diagnostic.locator}`).digest("hex").slice(0, 24)}`;
|
|
149
|
+
const relativePath = `evidence/${String(diagnosticIndex++).padStart(4, "0")}.txt`;
|
|
150
|
+
const counts = new Map([["sanitized", 1]]);
|
|
151
|
+
const outputBytes = sanitizeArtifact(sourceBytes, "text/plain", options.policy, replacements, sensitiveValues, localIdentifiers, counts, "diagnostic");
|
|
152
|
+
const digest = await writeArtifactAtomically(destinationRoot, relativePath, outputBytes, undefined, { overwrite: false });
|
|
153
|
+
const artifact = {
|
|
154
|
+
id,
|
|
155
|
+
sourceArtifactId: id,
|
|
156
|
+
kind: "diagnostic",
|
|
157
|
+
mediaType: "text/plain",
|
|
158
|
+
digest: digestString(digest),
|
|
159
|
+
sizeBytes: outputBytes.length,
|
|
160
|
+
relativePath,
|
|
161
|
+
sourceDigest: diagnostic.digest,
|
|
162
|
+
diagnosticSource: {
|
|
163
|
+
verifierId: verifier.id,
|
|
164
|
+
stream: diagnostic.stream,
|
|
165
|
+
locatorDigest: digestText(diagnostic.locator),
|
|
166
|
+
sizeBytes: diagnostic.sizeBytes,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
artifacts.push(artifact);
|
|
170
|
+
transformations.push(...[...counts.entries()].map(([action, count]) => ({ artifactId: id, action, count })));
|
|
171
|
+
portableDiagnostics.set(diagnostic.locator, { artifact, truncated: diagnostic.truncated || counts.has("truncated") });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const [index, descriptor] of sourceManifest.evidence.entries()) {
|
|
175
|
+
if (descriptor.kind === "export-manifest") {
|
|
176
|
+
excludedArtifacts.push({ artifactId: descriptor.id, reason: "source-export-manifest" });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (descriptor.kind === "workspace" && descriptor.mediaType === "application/gzip") {
|
|
180
|
+
excludedArtifacts.push({ artifactId: descriptor.id, reason: "unsupported-workspace-snapshot" });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
assertSupportedPair(descriptor.kind, descriptor.mediaType);
|
|
184
|
+
const sourceBytes = await readVerifiedArtifact(sourceRoot, descriptor.relativePath, digestValue(descriptor.digest), MAX_SOURCE_ARTIFACT_BYTES);
|
|
185
|
+
if (sourceBytes.length !== descriptor.sizeBytes) {
|
|
186
|
+
throw new Error(`Artifact ${descriptor.id} size does not match its source descriptor.`);
|
|
187
|
+
}
|
|
188
|
+
const counts = new Map([["sanitized", 1]]);
|
|
189
|
+
const outputBytes = sanitizeArtifact(sourceBytes, descriptor.mediaType, options.policy, replacements, sensitiveValues, localIdentifiers, counts, descriptor.kind, portableDiagnostics);
|
|
190
|
+
const relativePath = `evidence/${String(index).padStart(4, "0")}${extensionFor(descriptor.mediaType)}`;
|
|
191
|
+
const digest = await writeArtifactAtomically(destinationRoot, relativePath, outputBytes, undefined, { overwrite: false });
|
|
192
|
+
artifacts.push({
|
|
193
|
+
id: descriptor.id,
|
|
194
|
+
sourceArtifactId: descriptor.id,
|
|
195
|
+
kind: descriptor.kind,
|
|
196
|
+
mediaType: descriptor.mediaType,
|
|
197
|
+
digest: digestString(digest),
|
|
198
|
+
sizeBytes: outputBytes.length,
|
|
199
|
+
relativePath,
|
|
200
|
+
sourceDigest: descriptor.digest,
|
|
201
|
+
});
|
|
202
|
+
transformations.push(...[...counts.entries()].map(([action, count]) => ({ artifactId: descriptor.id, action, count })));
|
|
203
|
+
}
|
|
204
|
+
const manifest = {
|
|
205
|
+
schemaVersion: "export-manifest/v1",
|
|
206
|
+
bundleId: correlations.bundleId,
|
|
207
|
+
status: "ready",
|
|
208
|
+
sharingClass: options.policy.sharingClass,
|
|
209
|
+
assessmentMode: sourceManifest.run.assessmentMode,
|
|
210
|
+
policyDigest,
|
|
211
|
+
artifactIds: artifacts.map(({ id }) => id),
|
|
212
|
+
sourceManifestDigest,
|
|
213
|
+
correlations,
|
|
214
|
+
artifacts,
|
|
215
|
+
transformations,
|
|
216
|
+
excludedArtifacts,
|
|
217
|
+
};
|
|
218
|
+
const schemaErrors = validateArtifact("manifest.json", manifest);
|
|
219
|
+
if (schemaErrors.length > 0)
|
|
220
|
+
throw new Error(formatValidationErrors(schemaErrors));
|
|
221
|
+
await writeMetadataAtomically(destinationRoot, "manifest.json", manifest, undefined, { overwrite: false });
|
|
222
|
+
const readback = await readPortableRunBundleExport(destinationRoot, options.policy, sourceManifest, sourceRoot);
|
|
223
|
+
if (readback.sourceManifestDigest !== sourceManifestDigest) {
|
|
224
|
+
throw new Error("Portable export source-manifest digest changed before readback.");
|
|
225
|
+
}
|
|
226
|
+
return structuredClone(manifest);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
await rm(destinationRoot, { recursive: true, force: true });
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Read back and revalidate a completed portable export tree. */
|
|
234
|
+
export async function readPortableRunBundleExport(exportRoot, policy, sourceManifest, sourceRoot) {
|
|
235
|
+
validatePolicy(policy);
|
|
236
|
+
const root = resolve(exportRoot);
|
|
237
|
+
const manifestBytes = await verifiedManifestBytes(root);
|
|
238
|
+
const manifest = parseJson(manifestBytes, "Portable export manifest");
|
|
239
|
+
const schemaErrors = validateArtifact("manifest.json", manifest);
|
|
240
|
+
if (schemaErrors.length > 0)
|
|
241
|
+
throw new Error(formatValidationErrors(schemaErrors));
|
|
242
|
+
if (manifest.status !== "ready" && manifest.status !== "exported")
|
|
243
|
+
throw new Error("Portable export is not ready.");
|
|
244
|
+
if (manifest.sharingClass !== policy.sharingClass || manifest.policyDigest !== digestString(digestMetadata(policyRecord(policy)))) {
|
|
245
|
+
throw new Error("Portable export policy does not match the requested policy.");
|
|
246
|
+
}
|
|
247
|
+
assertUniqueArtifactIdentities(manifest.artifacts.map(({ id, relativePath }) => ({ id, relativePath })));
|
|
248
|
+
if (JSON.stringify(manifest.artifactIds) !== JSON.stringify(manifest.artifacts.map(({ id }) => id))) {
|
|
249
|
+
throw new Error("Portable export artifact index does not match its descriptors.");
|
|
250
|
+
}
|
|
251
|
+
const included = new Set(manifest.artifactIds);
|
|
252
|
+
const includedSources = new Set(manifest.artifacts.map(({ sourceArtifactId }) => sourceArtifactId));
|
|
253
|
+
const excluded = new Set(manifest.excludedArtifacts.map(({ artifactId }) => artifactId));
|
|
254
|
+
if (includedSources.size !== manifest.artifacts.length || excluded.size !== manifest.excludedArtifacts.length
|
|
255
|
+
|| [...excluded].some((id) => included.has(id) || includedSources.has(id))) {
|
|
256
|
+
throw new Error("Portable export exclusions are duplicate or included artifacts.");
|
|
257
|
+
}
|
|
258
|
+
if (manifest.transformations.some(({ artifactId, count }) => !included.has(artifactId) || !Number.isSafeInteger(count) || count < 1)) {
|
|
259
|
+
throw new Error("Portable export transformation references are invalid.");
|
|
260
|
+
}
|
|
261
|
+
const transformationKeys = new Set(manifest.transformations.map(({ artifactId, action }) => `${artifactId}\0${action}`));
|
|
262
|
+
if (transformationKeys.size !== manifest.transformations.length) {
|
|
263
|
+
throw new Error("Portable export contains duplicate transformation records.");
|
|
264
|
+
}
|
|
265
|
+
if (manifest.artifacts.some(({ id }) => !manifest.transformations.some(({ artifactId }) => artifactId === id))) {
|
|
266
|
+
throw new Error("Portable export omits an artifact transformation record.");
|
|
267
|
+
}
|
|
268
|
+
const artifactBytes = [];
|
|
269
|
+
for (const artifact of manifest.artifacts) {
|
|
270
|
+
assertSupportedPair(artifact.kind, artifact.mediaType);
|
|
271
|
+
const bytes = await readVerifiedArtifact(root, artifact.relativePath, digestValue(artifact.digest), policy.maxArtifactBytes);
|
|
272
|
+
if (bytes.length !== artifact.sizeBytes)
|
|
273
|
+
throw new Error(`Portable artifact ${artifact.id} size does not match its descriptor.`);
|
|
274
|
+
parseSanitizedArtifact(bytes, artifact.mediaType);
|
|
275
|
+
artifactBytes.push(bytes);
|
|
276
|
+
}
|
|
277
|
+
validatePortableDiagnosticReferences(manifest, artifactBytes);
|
|
278
|
+
if (sourceManifest !== undefined)
|
|
279
|
+
await validateSourceReferences(manifest, sourceManifest, sourceRoot);
|
|
280
|
+
scanPortableTree([
|
|
281
|
+
{ bytes: manifestBytes, mediaType: "application/json" },
|
|
282
|
+
...artifactBytes.map((bytes, index) => ({
|
|
283
|
+
bytes,
|
|
284
|
+
mediaType: manifest.artifacts[index].mediaType,
|
|
285
|
+
})),
|
|
286
|
+
], effectiveSensitiveValues(policy), sourceManifest === undefined ? [] : sourceCorrelationValues(sourceManifest));
|
|
287
|
+
return structuredClone(manifest);
|
|
288
|
+
}
|
|
289
|
+
async function readSourceManifest(root) {
|
|
290
|
+
const inspected = await inspectRetainedArtifact(root, "manifest.json");
|
|
291
|
+
if (inspected.sizeBytes > 8 * 1024 * 1024)
|
|
292
|
+
throw new Error("Run manifest exceeds the export byte limit.");
|
|
293
|
+
const bytes = await readVerifiedArtifact(root, "manifest.json", inspected.digest, 8 * 1024 * 1024);
|
|
294
|
+
return { manifest: parseJson(bytes, "Run manifest"), digest: digestString(inspected.digest) };
|
|
295
|
+
}
|
|
296
|
+
async function verifiedManifestBytes(root) {
|
|
297
|
+
const inspected = await inspectRetainedArtifact(root, "manifest.json");
|
|
298
|
+
if (inspected.sizeBytes > 8 * 1024 * 1024)
|
|
299
|
+
throw new Error("Portable export manifest exceeds the readback byte limit.");
|
|
300
|
+
return readVerifiedArtifact(root, "manifest.json", inspected.digest, 8 * 1024 * 1024);
|
|
301
|
+
}
|
|
302
|
+
function sanitizeArtifact(bytes, mediaType, policy, replacements, sensitiveValues, localIdentifiers, counts, kind, portableDiagnostics = new Map()) {
|
|
303
|
+
if (mediaType === "application/json") {
|
|
304
|
+
increment(counts, "canonicalized");
|
|
305
|
+
const output = Buffer.from(canonicalizeMetadata(sanitizeValue(kind === "verifier"
|
|
306
|
+
? rewriteVerifierDiagnosticReferences(parseJson(bytes, "JSON evidence"), portableDiagnostics)
|
|
307
|
+
: stripNativeReasoning(parseJson(bytes, "JSON evidence"), kind, counts), policy, replacements, sensitiveValues, localIdentifiers, counts)));
|
|
308
|
+
if (output.length > policy.maxArtifactBytes)
|
|
309
|
+
throw new Error("Sanitized JSON evidence exceeds the configured artifact limit.");
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
if (mediaType === "application/x-ndjson") {
|
|
313
|
+
const lines = decode(bytes, "JSONL evidence").split(/\r?\n/gu).filter((line) => line.trim() !== "");
|
|
314
|
+
if (lines.length === 0)
|
|
315
|
+
throw new Error("JSONL evidence is empty.");
|
|
316
|
+
increment(counts, "canonicalized", lines.length);
|
|
317
|
+
const sanitized = lines.map((line) => canonicalizeMetadata(sanitizeValue(stripNativeReasoning(parseJson(Buffer.from(line), "JSONL evidence record"), kind, counts), policy, replacements, sensitiveValues, localIdentifiers, counts)));
|
|
318
|
+
const retained = [];
|
|
319
|
+
for (const line of sanitized) {
|
|
320
|
+
if (Buffer.byteLength(`${[...retained, line].join("\n")}\n`) > policy.maxArtifactBytes)
|
|
321
|
+
break;
|
|
322
|
+
retained.push(line);
|
|
323
|
+
}
|
|
324
|
+
if (retained.length === 0)
|
|
325
|
+
throw new Error("Sanitized JSONL record exceeds the configured artifact limit.");
|
|
326
|
+
if (retained.length !== sanitized.length)
|
|
327
|
+
increment(counts, "truncated", sanitized.length - retained.length);
|
|
328
|
+
return Buffer.from(`${retained.join("\n")}\n`);
|
|
329
|
+
}
|
|
330
|
+
const sanitized = rewriteString(decode(bytes, "text evidence"), policy, replacements, sensitiveValues, localIdentifiers, counts, false, true, true);
|
|
331
|
+
const bounded = truncateUtf8(sanitized, policy.maxArtifactBytes);
|
|
332
|
+
if (bounded !== sanitized)
|
|
333
|
+
increment(counts, "truncated");
|
|
334
|
+
return Buffer.from(bounded);
|
|
335
|
+
}
|
|
336
|
+
function sourceDiagnostics(value) {
|
|
337
|
+
if (!isRecord(value) || value.diagnostics === undefined)
|
|
338
|
+
return [];
|
|
339
|
+
if (!Array.isArray(value.diagnostics))
|
|
340
|
+
throw new Error("Verifier diagnostics must be an array.");
|
|
341
|
+
return value.diagnostics.map((entry) => {
|
|
342
|
+
if (!isRecord(entry) || !["stdout", "stderr"].includes(String(entry.stream))
|
|
343
|
+
|| typeof entry.locator !== "string" || typeof entry.digest !== "string"
|
|
344
|
+
|| !Number.isSafeInteger(entry.sizeBytes) || typeof entry.truncated !== "boolean") {
|
|
345
|
+
throw new Error("Verifier diagnostic reference is invalid.");
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
stream: entry.stream,
|
|
349
|
+
locator: entry.locator,
|
|
350
|
+
digest: entry.digest,
|
|
351
|
+
sizeBytes: entry.sizeBytes,
|
|
352
|
+
truncated: entry.truncated,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function rewriteVerifierDiagnosticReferences(value, diagnostics) {
|
|
357
|
+
if (diagnostics.size === 0 || !isRecord(value) || !Array.isArray(value.diagnostics))
|
|
358
|
+
return value;
|
|
359
|
+
const output = structuredClone(value);
|
|
360
|
+
for (const diagnostic of output.diagnostics) {
|
|
361
|
+
if (!isRecord(diagnostic) || typeof diagnostic.locator !== "string") {
|
|
362
|
+
throw new Error("Verifier diagnostic reference is invalid.");
|
|
363
|
+
}
|
|
364
|
+
const portable = diagnostics.get(diagnostic.locator);
|
|
365
|
+
if (portable === undefined)
|
|
366
|
+
throw new Error(`Verifier diagnostic ${diagnostic.locator} was not exported.`);
|
|
367
|
+
diagnostic.locator = portable.artifact.relativePath;
|
|
368
|
+
diagnostic.digest = portable.artifact.digest;
|
|
369
|
+
diagnostic.sizeBytes = portable.artifact.sizeBytes;
|
|
370
|
+
diagnostic.truncated = portable.truncated;
|
|
371
|
+
delete diagnostic.source;
|
|
372
|
+
}
|
|
373
|
+
return output;
|
|
374
|
+
}
|
|
375
|
+
function sanitizeValue(value, policy, replacements, sensitiveValues, localIdentifiers, counts, fieldName, truncatable = false) {
|
|
376
|
+
const normalizedField = fieldName === undefined ? undefined : normalizeFieldName(fieldName);
|
|
377
|
+
if (typeof value === "string") {
|
|
378
|
+
// Correlation values are also rewritten as substrings: native evidence
|
|
379
|
+
// embeds session and workspace identities inside encoded path strings that
|
|
380
|
+
// survive absolute-path redaction.
|
|
381
|
+
return rewriteString(value, policy, replacements, sensitiveValues, localIdentifiers, counts, normalizedField !== undefined && CORRELATION_FIELDS.has(normalizedField), truncatable, true, normalizedField !== undefined && LOCAL_IDENTIFIER_FIELDS.has(normalizedField));
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
return value.map((entry) => sanitizeValue(entry, policy, replacements, sensitiveValues, localIdentifiers, counts, fieldName, truncatable));
|
|
385
|
+
}
|
|
386
|
+
if (!isRecord(value))
|
|
387
|
+
return value;
|
|
388
|
+
const sourceFieldCount = Object.keys(value).length;
|
|
389
|
+
const output = {};
|
|
390
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
391
|
+
if (HIDDEN_FIELDS.has(normalizeFieldName(key))) {
|
|
392
|
+
increment(counts, "removed-field");
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (SECRET_FIELDS.has(normalizeFieldName(key))) {
|
|
396
|
+
output[key] = "[REDACTED_SECRET]";
|
|
397
|
+
increment(counts, "redacted-secret");
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
output[key] = sanitizeValue(entry, policy, replacements, sensitiveValues, localIdentifiers, counts, key, truncatable || TRUNCATABLE_FIELDS.has(normalizeFieldName(key)));
|
|
401
|
+
}
|
|
402
|
+
if (sourceFieldCount > 0 && Object.keys(output).length === 0) {
|
|
403
|
+
throw new Error("Sanitization removed every field from an evidence record.");
|
|
404
|
+
}
|
|
405
|
+
return output;
|
|
406
|
+
}
|
|
407
|
+
function stripCodexReasoning(value, kind, counts) {
|
|
408
|
+
if (kind !== "session")
|
|
409
|
+
return value;
|
|
410
|
+
if (Array.isArray(value))
|
|
411
|
+
return value.map((entry) => stripCodexReasoning(entry, kind, counts));
|
|
412
|
+
if (!isRecord(value))
|
|
413
|
+
return value;
|
|
414
|
+
const reasoningItem = value.type === "reasoning";
|
|
415
|
+
const reasoningDelta = value.method === CODEX_REASONING_DELTA_METHOD;
|
|
416
|
+
const cursorReasoning = typeof value.type === "string" && CURSOR_REASONING_TYPES.has(normalizeFieldName(value.type));
|
|
417
|
+
const cursorCheckpoint = typeof value.agentId === "string" && typeof value.blobId === "string" && typeof value.dataBase64 === "string";
|
|
418
|
+
const payloadContainsReasoning = containsCodexReasoning(value.payload);
|
|
419
|
+
const output = {};
|
|
420
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
421
|
+
const normalized = normalizeFieldName(key);
|
|
422
|
+
if (reasoningItem && key !== "type" && key !== "id") {
|
|
423
|
+
increment(counts, "removed-field");
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (cursorReasoning && (CODEX_REASONING_CONTENT_FIELDS.has(normalized) || normalized === "thinkingdurationms" || normalized === "message")) {
|
|
427
|
+
increment(counts, "removed-field");
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (cursorCheckpoint && normalized === "database64") {
|
|
431
|
+
increment(counts, "removed-field");
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (key === "raw" && (reasoningDelta || payloadContainsReasoning || rawContainsCodexReasoning(entry))) {
|
|
435
|
+
increment(counts, "removed-field");
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (reasoningDelta && CODEX_REASONING_CONTENT_FIELDS.has(normalized)) {
|
|
439
|
+
increment(counts, "removed-field");
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (reasoningDelta && (key === "params" || key === "payload")) {
|
|
443
|
+
output[key] = stripCodexReasoningEnvelope(entry, kind, counts);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
output[key] = stripCodexReasoning(entry, kind, counts);
|
|
447
|
+
}
|
|
448
|
+
return output;
|
|
449
|
+
}
|
|
450
|
+
function stripNativeReasoning(value, kind, counts) {
|
|
451
|
+
return stripPiReasoning(stripCodexReasoning(value, kind, counts), kind, counts);
|
|
452
|
+
}
|
|
453
|
+
function stripPiReasoning(value, kind, counts) {
|
|
454
|
+
if (kind !== "session")
|
|
455
|
+
return value;
|
|
456
|
+
if (Array.isArray(value))
|
|
457
|
+
return value.map((entry) => stripPiReasoning(entry, kind, counts));
|
|
458
|
+
if (!isRecord(value))
|
|
459
|
+
return value;
|
|
460
|
+
const privateContent = value.thought === true
|
|
461
|
+
|| typeof value.type === "string" && PI_PRIVATE_CONTENT_TYPES.has(normalizeFieldName(value.type));
|
|
462
|
+
const output = {};
|
|
463
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
464
|
+
const normalized = normalizeFieldName(key);
|
|
465
|
+
if (PI_PRIVATE_FIELDS.has(normalized) || privateContent && PI_PRIVATE_CONTENT_FIELDS.has(normalized)) {
|
|
466
|
+
increment(counts, "removed-field");
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
output[key] = stripPiReasoning(entry, kind, counts);
|
|
470
|
+
}
|
|
471
|
+
return output;
|
|
472
|
+
}
|
|
473
|
+
function stripCodexReasoningEnvelope(value, kind, counts) {
|
|
474
|
+
if (Array.isArray(value))
|
|
475
|
+
return value.map((entry) => stripCodexReasoningEnvelope(entry, kind, counts));
|
|
476
|
+
if (!isRecord(value))
|
|
477
|
+
return value;
|
|
478
|
+
const output = {};
|
|
479
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
480
|
+
if (CODEX_REASONING_CONTENT_FIELDS.has(normalizeFieldName(key))) {
|
|
481
|
+
increment(counts, "removed-field");
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
output[key] = stripCodexReasoning(entry, kind, counts);
|
|
485
|
+
}
|
|
486
|
+
return output;
|
|
487
|
+
}
|
|
488
|
+
function containsCodexReasoning(value) {
|
|
489
|
+
if (Array.isArray(value))
|
|
490
|
+
return value.some(containsCodexReasoning);
|
|
491
|
+
if (!isRecord(value))
|
|
492
|
+
return false;
|
|
493
|
+
return value.type === "reasoning" || value.method === CODEX_REASONING_DELTA_METHOD
|
|
494
|
+
|| Object.values(value).some(containsCodexReasoning);
|
|
495
|
+
}
|
|
496
|
+
function rawContainsCodexReasoning(value) {
|
|
497
|
+
if (typeof value !== "string")
|
|
498
|
+
return false;
|
|
499
|
+
try {
|
|
500
|
+
return containsCodexReasoning(JSON.parse(value));
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function rewriteString(input, policy, replacements, sensitiveValues, localIdentifiers, counts, rewriteCorrelation, truncateContent, rewriteTextCorrelations = false, rewriteLocalIdentifier = false) {
|
|
507
|
+
let output = input;
|
|
508
|
+
// Declared sensitive values are redacted before correlation rewriting: a
|
|
509
|
+
// secret that embeds a run, attempt, bundle, session, or trace ID must not
|
|
510
|
+
// be mutated into a form the exact-secret match and final scan cannot see.
|
|
511
|
+
for (const secret of sensitiveValues) {
|
|
512
|
+
const occurrences = countOccurrences(output, secret);
|
|
513
|
+
if (occurrences > 0) {
|
|
514
|
+
output = output.replaceAll(secret, "[REDACTED_SECRET]");
|
|
515
|
+
increment(counts, "redacted-secret", occurrences);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const correlation = rewriteCorrelation ? replacements.get(output) : undefined;
|
|
519
|
+
if (correlation !== undefined) {
|
|
520
|
+
output = correlation;
|
|
521
|
+
increment(counts, "rewritten-correlation");
|
|
522
|
+
}
|
|
523
|
+
if (rewriteTextCorrelations) {
|
|
524
|
+
for (const [source, replacement] of replacements) {
|
|
525
|
+
if (source.length < 8)
|
|
526
|
+
continue;
|
|
527
|
+
const occurrences = countOccurrences(output, source);
|
|
528
|
+
if (occurrences > 0) {
|
|
529
|
+
output = output.replaceAll(source, replacement);
|
|
530
|
+
increment(counts, "rewritten-correlation", occurrences);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
535
|
+
output = output.replace(pattern, (_match, prefix, suffix) => {
|
|
536
|
+
increment(counts, "redacted-secret");
|
|
537
|
+
return `${typeof prefix === "string" ? prefix : ""}[REDACTED_SECRET]${typeof suffix === "string" ? suffix : ""}`;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
output = output.replace(LOCAL_PATH, (_match, prefix) => {
|
|
541
|
+
increment(counts, "redacted-local-identifier");
|
|
542
|
+
return `${typeof prefix === "string" ? prefix : ""}[LOCAL_PATH]`;
|
|
543
|
+
});
|
|
544
|
+
for (const pattern of LOCAL_IDENTIFIER_PATTERNS) {
|
|
545
|
+
output = output.replace(pattern, (_match, prefix, suffix) => {
|
|
546
|
+
increment(counts, "redacted-local-identifier");
|
|
547
|
+
return `${typeof prefix === "string" ? prefix : ""}[LOCAL_USER]${typeof suffix === "string" ? suffix : ""}`;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (rewriteLocalIdentifier) {
|
|
551
|
+
for (const identifier of localIdentifiers.filter((value) => value !== homedir())) {
|
|
552
|
+
if (output === identifier) {
|
|
553
|
+
output = "[LOCAL_USER]";
|
|
554
|
+
increment(counts, "redacted-local-identifier");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!truncateContent)
|
|
559
|
+
return output;
|
|
560
|
+
const bounded = truncateUtf8(output, policy.maxStringBytes);
|
|
561
|
+
if (bounded !== output)
|
|
562
|
+
increment(counts, "truncated");
|
|
563
|
+
return bounded;
|
|
564
|
+
}
|
|
565
|
+
function scanPortableTree(buffers, sensitiveValues, sourceCorrelations) {
|
|
566
|
+
for (const [index, { bytes, mediaType }] of buffers.entries()) {
|
|
567
|
+
const text = decode(bytes, "portable export");
|
|
568
|
+
const failure = [
|
|
569
|
+
["known sensitive value", sensitiveValues.some((value) => text.includes(value))],
|
|
570
|
+
["secret pattern", containsPortableSecretPattern(text, mediaType)],
|
|
571
|
+
["absolute path", containsLocalPath(text, mediaType)],
|
|
572
|
+
["local identifier", LOCAL_IDENTIFIER_PATTERNS.some((pattern) => pattern.test(text))],
|
|
573
|
+
["source correlation", sourceCorrelations.filter((value) => value.length >= 8).some((value) => text.includes(value))],
|
|
574
|
+
["hidden content field", /"(?:chain[_-]?of[_-]?thought|extended[_-]?thinking|hidden[_-]?reasoning|encrypted[_-]?(?:reasoning|thinking)|reasoning(?:[_-]?(?:content|details|signature))?|thinking(?:[_-]?(?:content|signature))?|text[_-]?signature|thought[_-]?signature|raw[_-]?(?:api|request|response)[_-]?body)"\s*:/iu.test(text)],
|
|
575
|
+
["Codex reasoning content", containsCodexReasoningContent(text, mediaType)],
|
|
576
|
+
["Pi private reasoning content", containsPiReasoningContent(text, mediaType)],
|
|
577
|
+
].find(([, matched]) => matched);
|
|
578
|
+
if (failure !== undefined) {
|
|
579
|
+
resetPatterns();
|
|
580
|
+
throw new Error(`Portable export buffer ${index} failed the final secret scan: ${String(failure[0])}.`);
|
|
581
|
+
}
|
|
582
|
+
resetPatterns();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function containsPiReasoningContent(text, mediaType) {
|
|
586
|
+
if (mediaType === "application/json")
|
|
587
|
+
return valueContainsPiReasoningContent(parseJson(Buffer.from(text), "Portable JSON Pi reasoning scan"));
|
|
588
|
+
if (mediaType === "application/x-ndjson")
|
|
589
|
+
return text.split(/\r?\n/gu).filter(Boolean).some((line) => valueContainsPiReasoningContent(parseJson(Buffer.from(line), "Portable JSONL Pi reasoning scan")));
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
function valueContainsPiReasoningContent(value) {
|
|
593
|
+
if (Array.isArray(value))
|
|
594
|
+
return value.some(valueContainsPiReasoningContent);
|
|
595
|
+
if (!isRecord(value))
|
|
596
|
+
return false;
|
|
597
|
+
if (Object.keys(value).some((key) => PI_PRIVATE_FIELDS.has(normalizeFieldName(key))))
|
|
598
|
+
return true;
|
|
599
|
+
if ((value.thought === true || typeof value.type === "string" && PI_PRIVATE_CONTENT_TYPES.has(normalizeFieldName(value.type)))
|
|
600
|
+
&& Object.keys(value).some((key) => PI_PRIVATE_CONTENT_FIELDS.has(normalizeFieldName(key))))
|
|
601
|
+
return true;
|
|
602
|
+
return Object.values(value).some(valueContainsPiReasoningContent);
|
|
603
|
+
}
|
|
604
|
+
function containsCodexReasoningContent(text, mediaType) {
|
|
605
|
+
if (mediaType === "application/json") {
|
|
606
|
+
return valueContainsCodexReasoningContent(parseJson(Buffer.from(text), "Portable JSON reasoning scan"));
|
|
607
|
+
}
|
|
608
|
+
if (mediaType === "application/x-ndjson") {
|
|
609
|
+
return text.split(/\r?\n/gu).filter(Boolean).some((line) => valueContainsCodexReasoningContent(parseJson(Buffer.from(line), "Portable JSONL reasoning scan")));
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
function valueContainsCodexReasoningContent(value) {
|
|
614
|
+
if (typeof value === "string") {
|
|
615
|
+
const trimmed = value.trimStart();
|
|
616
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
617
|
+
return false;
|
|
618
|
+
try {
|
|
619
|
+
return valueContainsCodexReasoningContent(JSON.parse(trimmed));
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (Array.isArray(value))
|
|
626
|
+
return value.some(valueContainsCodexReasoningContent);
|
|
627
|
+
if (!isRecord(value))
|
|
628
|
+
return false;
|
|
629
|
+
if (value.type === "reasoning"
|
|
630
|
+
&& Object.keys(value).some((key) => CODEX_REASONING_CONTENT_FIELDS.has(normalizeFieldName(key))))
|
|
631
|
+
return true;
|
|
632
|
+
if (value.method === CODEX_REASONING_DELTA_METHOD) {
|
|
633
|
+
for (const container of [value.params, value.payload]) {
|
|
634
|
+
if (isRecord(container)
|
|
635
|
+
&& Object.keys(container).some((key) => CODEX_REASONING_CONTENT_FIELDS.has(normalizeFieldName(key))))
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return Object.values(value).some(valueContainsCodexReasoningContent);
|
|
640
|
+
}
|
|
641
|
+
/** Use the export pipeline's fail-closed credential patterns on release material. */
|
|
642
|
+
export function containsPortableSecretPattern(text, mediaType) {
|
|
643
|
+
if (mediaType === "application/json") {
|
|
644
|
+
return valueContainsSecretPattern(parseJson(Buffer.from(text), "Portable JSON final scan"));
|
|
645
|
+
}
|
|
646
|
+
if (mediaType === "application/x-ndjson") {
|
|
647
|
+
return text.split(/\r?\n/gu).filter(Boolean).some((line) => valueContainsSecretPattern(parseJson(Buffer.from(line), "Portable JSONL final scan")));
|
|
648
|
+
}
|
|
649
|
+
return stringContainsSecretPattern(text);
|
|
650
|
+
}
|
|
651
|
+
/** Detect user-identifying home paths without rejecting documented generic paths. */
|
|
652
|
+
export function containsPortableLocalHomePath(text, mediaType = "text/plain") {
|
|
653
|
+
if (mediaType === "application/json") {
|
|
654
|
+
return valueContainsLocalHomePath(parseJson(Buffer.from(text), "Portable JSON local-home scan"));
|
|
655
|
+
}
|
|
656
|
+
if (mediaType === "application/x-ndjson") {
|
|
657
|
+
return text.split(/\r?\n/gu).filter(Boolean).some((line) => valueContainsLocalHomePath(parseJson(Buffer.from(line), "Portable JSONL local-home scan")));
|
|
658
|
+
}
|
|
659
|
+
return stringContainsLocalHomePath(text);
|
|
660
|
+
}
|
|
661
|
+
function valueContainsLocalHomePath(value) {
|
|
662
|
+
if (typeof value === "string")
|
|
663
|
+
return stringContainsLocalHomePath(value);
|
|
664
|
+
if (Array.isArray(value))
|
|
665
|
+
return value.some(valueContainsLocalHomePath);
|
|
666
|
+
if (!isRecord(value))
|
|
667
|
+
return false;
|
|
668
|
+
return Object.entries(value).some(([key, entry]) => stringContainsLocalHomePath(key) || valueContainsLocalHomePath(entry));
|
|
669
|
+
}
|
|
670
|
+
function stringContainsLocalHomePath(text) {
|
|
671
|
+
const matched = LOCAL_HOME_PATH.test(text);
|
|
672
|
+
LOCAL_HOME_PATH.lastIndex = 0;
|
|
673
|
+
return matched;
|
|
674
|
+
}
|
|
675
|
+
function valueContainsSecretPattern(value) {
|
|
676
|
+
if (typeof value === "string")
|
|
677
|
+
return stringContainsSecretPattern(value);
|
|
678
|
+
if (Array.isArray(value))
|
|
679
|
+
return value.some(valueContainsSecretPattern);
|
|
680
|
+
if (!isRecord(value))
|
|
681
|
+
return false;
|
|
682
|
+
return Object.entries(value).some(([key, entry]) => SECRET_FIELDS.has(normalizeFieldName(key)) && entry !== "[REDACTED_SECRET]"
|
|
683
|
+
|| stringContainsSecretPattern(key)
|
|
684
|
+
|| valueContainsSecretPattern(entry));
|
|
685
|
+
}
|
|
686
|
+
function stringContainsSecretPattern(value) {
|
|
687
|
+
const matched = SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
688
|
+
resetPatterns();
|
|
689
|
+
return matched;
|
|
690
|
+
}
|
|
691
|
+
function containsLocalPath(text, mediaType) {
|
|
692
|
+
if (mediaType === "application/json") {
|
|
693
|
+
return valueContainsLocalPath(parseJson(Buffer.from(text), "Portable JSON final scan"));
|
|
694
|
+
}
|
|
695
|
+
if (mediaType === "application/x-ndjson") {
|
|
696
|
+
return text.split(/\r?\n/gu).filter(Boolean).some((line) => valueContainsLocalPath(parseJson(Buffer.from(line), "Portable JSONL final scan")));
|
|
697
|
+
}
|
|
698
|
+
return stringContainsLocalPath(text);
|
|
699
|
+
}
|
|
700
|
+
function valueContainsLocalPath(value) {
|
|
701
|
+
if (typeof value === "string")
|
|
702
|
+
return stringContainsLocalPath(value);
|
|
703
|
+
if (Array.isArray(value))
|
|
704
|
+
return value.some(valueContainsLocalPath);
|
|
705
|
+
if (!isRecord(value))
|
|
706
|
+
return false;
|
|
707
|
+
return Object.entries(value).some(([key, entry]) => stringContainsLocalPath(key) || valueContainsLocalPath(entry));
|
|
708
|
+
}
|
|
709
|
+
function stringContainsLocalPath(value) {
|
|
710
|
+
const matched = LOCAL_PATH.test(value);
|
|
711
|
+
LOCAL_PATH.lastIndex = 0;
|
|
712
|
+
return matched;
|
|
713
|
+
}
|
|
714
|
+
async function validateSourceReferences(manifest, source, sourceRoot) {
|
|
715
|
+
if ((manifest.assessmentMode ?? "verified") !== (source.run.assessmentMode ?? "verified")) {
|
|
716
|
+
throw new Error("Portable export assessment mode does not match its source run.");
|
|
717
|
+
}
|
|
718
|
+
const sourceById = new Map(source.evidence.map((descriptor) => [descriptor.id, descriptor]));
|
|
719
|
+
if (manifest.sourceManifestDigest === undefined)
|
|
720
|
+
throw new Error("Portable export omits its source-manifest digest.");
|
|
721
|
+
for (const artifact of manifest.artifacts) {
|
|
722
|
+
if (artifact.kind === "diagnostic") {
|
|
723
|
+
const verifier = artifact.diagnosticSource === undefined
|
|
724
|
+
? undefined
|
|
725
|
+
: sourceById.get(artifact.diagnosticSource.verifierId);
|
|
726
|
+
if (artifact.diagnosticSource === undefined || verifier?.kind !== "verifier") {
|
|
727
|
+
throw new Error(`Portable diagnostic ${artifact.id} does not reference a source verifier.`);
|
|
728
|
+
}
|
|
729
|
+
if (sourceRoot === undefined) {
|
|
730
|
+
throw new Error("Source root is required to validate portable diagnostic provenance.");
|
|
731
|
+
}
|
|
732
|
+
const verifierBytes = await readVerifiedArtifact(sourceRoot, verifier.relativePath, digestValue(verifier.digest), MAX_SOURCE_ARTIFACT_BYTES);
|
|
733
|
+
const sourceDiagnostic = sourceDiagnostics(parseJson(verifierBytes, "Source verifier evidence")).find((diagnostic) => diagnostic.stream === artifact.diagnosticSource?.stream
|
|
734
|
+
&& digestText(diagnostic.locator) === artifact.diagnosticSource.locatorDigest);
|
|
735
|
+
if (sourceDiagnostic === undefined || sourceDiagnostic.digest !== artifact.sourceDigest
|
|
736
|
+
|| sourceDiagnostic.sizeBytes !== artifact.diagnosticSource.sizeBytes) {
|
|
737
|
+
throw new Error(`Portable diagnostic ${artifact.id} does not match its source diagnostic reference.`);
|
|
738
|
+
}
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const descriptor = sourceById.get(artifact.sourceArtifactId);
|
|
742
|
+
if (descriptor === undefined || descriptor.digest !== artifact.sourceDigest
|
|
743
|
+
|| descriptor.kind !== artifact.kind || descriptor.mediaType !== artifact.mediaType) {
|
|
744
|
+
throw new Error(`Portable artifact ${artifact.id} does not match its source reference.`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const excluded of manifest.excludedArtifacts) {
|
|
748
|
+
const source = sourceById.get(excluded.artifactId);
|
|
749
|
+
const valid = excluded.reason === "source-export-manifest"
|
|
750
|
+
? source?.kind === "export-manifest"
|
|
751
|
+
: source?.kind === "workspace" && source.mediaType === "application/gzip";
|
|
752
|
+
if (!valid) {
|
|
753
|
+
throw new Error(`Portable exclusion ${excluded.artifactId} does not match its declared reason.`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const represented = new Set([
|
|
757
|
+
...manifest.artifacts.map(({ sourceArtifactId }) => sourceArtifactId),
|
|
758
|
+
...manifest.excludedArtifacts.map(({ artifactId }) => artifactId),
|
|
759
|
+
]);
|
|
760
|
+
if (source.evidence.some(({ id }) => !represented.has(id))) {
|
|
761
|
+
throw new Error("Portable export does not account for every source artifact.");
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function validatePortableDiagnosticReferences(manifest, artifactBytes) {
|
|
765
|
+
const artifactsByPath = new Map(manifest.artifacts.map((artifact) => [artifact.relativePath, artifact]));
|
|
766
|
+
const artifactsById = new Map(manifest.artifacts.map((artifact) => [artifact.id, artifact]));
|
|
767
|
+
const referencedDiagnostics = new Set();
|
|
768
|
+
for (const [index, verifier] of manifest.artifacts.entries()) {
|
|
769
|
+
if (verifier.kind !== "verifier")
|
|
770
|
+
continue;
|
|
771
|
+
const document = parseJson(artifactBytes[index], `Portable verifier ${verifier.id}`);
|
|
772
|
+
if (!isRecord(document) || document.diagnostics === undefined)
|
|
773
|
+
continue;
|
|
774
|
+
if (!Array.isArray(document.diagnostics))
|
|
775
|
+
throw new Error(`Portable verifier ${verifier.id} diagnostics are invalid.`);
|
|
776
|
+
for (const diagnostic of document.diagnostics) {
|
|
777
|
+
if (!isRecord(diagnostic) || typeof diagnostic.locator !== "string") {
|
|
778
|
+
throw new Error(`Portable verifier ${verifier.id} diagnostic reference is invalid.`);
|
|
779
|
+
}
|
|
780
|
+
const sidecar = artifactsByPath.get(diagnostic.locator);
|
|
781
|
+
if (sidecar?.kind !== "diagnostic" || sidecar.digest !== diagnostic.digest
|
|
782
|
+
|| sidecar.sizeBytes !== diagnostic.sizeBytes
|
|
783
|
+
|| sidecar.diagnosticSource?.verifierId !== verifier.id
|
|
784
|
+
|| sidecar.diagnosticSource?.stream !== diagnostic.stream) {
|
|
785
|
+
throw new Error(`Portable verifier ${verifier.id} diagnostic reference is not bound to its sidecar.`);
|
|
786
|
+
}
|
|
787
|
+
referencedDiagnostics.add(sidecar.id);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
for (const diagnostic of manifest.artifacts.filter(({ kind }) => kind === "diagnostic")) {
|
|
791
|
+
if (diagnostic.diagnosticSource === undefined
|
|
792
|
+
|| artifactsById.get(diagnostic.diagnosticSource.verifierId)?.kind !== "verifier"
|
|
793
|
+
|| !referencedDiagnostics.has(diagnostic.id)) {
|
|
794
|
+
throw new Error(`Portable diagnostic ${diagnostic.id} is not referenced by its verifier.`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function parseSanitizedArtifact(bytes, mediaType) {
|
|
799
|
+
if (mediaType === "application/json") {
|
|
800
|
+
parseJson(bytes, "Portable JSON evidence");
|
|
801
|
+
}
|
|
802
|
+
else if (mediaType === "application/x-ndjson") {
|
|
803
|
+
const lines = decode(bytes, "Portable JSONL evidence").split(/\r?\n/gu).filter((line) => line.trim() !== "");
|
|
804
|
+
if (lines.length === 0)
|
|
805
|
+
throw new Error("Portable JSONL evidence is empty.");
|
|
806
|
+
for (const line of lines)
|
|
807
|
+
parseJson(Buffer.from(line), "Portable JSONL evidence record");
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
decode(bytes, "Portable text evidence");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function parseJson(bytes, label) {
|
|
814
|
+
const text = decode(bytes, label);
|
|
815
|
+
assertNoDuplicateJsonKeys(text);
|
|
816
|
+
try {
|
|
817
|
+
return JSON.parse(text);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
throw new Error(`${label} is invalid JSON: ${errorMessage(error)}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function assertSupportedPair(kind, mediaType) {
|
|
824
|
+
const allowed = {
|
|
825
|
+
session: ["application/x-ndjson"],
|
|
826
|
+
hook: ["application/x-ndjson"],
|
|
827
|
+
telemetry: ["application/json"],
|
|
828
|
+
workspace: ["text/x-diff"],
|
|
829
|
+
verifier: ["application/json"],
|
|
830
|
+
diagnostic: ["text/plain"],
|
|
831
|
+
"capture-report": ["application/json"],
|
|
832
|
+
};
|
|
833
|
+
if (kind === "export-manifest")
|
|
834
|
+
return;
|
|
835
|
+
if (!allowed[kind]?.includes(mediaType))
|
|
836
|
+
throw new Error(`Artifact kind ${kind} has unsupported media type ${mediaType}.`);
|
|
837
|
+
}
|
|
838
|
+
function correlationProjection(manifest, salt) {
|
|
839
|
+
return {
|
|
840
|
+
bundleId: rewrittenId("bundle", manifest.bundleId, salt),
|
|
841
|
+
runId: rewrittenId("run", manifest.run.id, salt),
|
|
842
|
+
attemptId: rewrittenId("attempt", manifest.attempt.id, salt),
|
|
843
|
+
...(manifest.run.native?.sessionId === undefined ? {} : { sessionId: rewrittenId("session", manifest.run.native.sessionId, salt) }),
|
|
844
|
+
...(manifest.run.native?.traceId === undefined ? {} : { traceId: rewrittenId("trace", manifest.run.native.traceId, salt) }),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function correlationReplacements(manifest, projected) {
|
|
848
|
+
const replacements = [
|
|
849
|
+
[manifest.bundleId, projected.bundleId],
|
|
850
|
+
[manifest.run.id, projected.runId],
|
|
851
|
+
[manifest.attempt.id, projected.attemptId],
|
|
852
|
+
];
|
|
853
|
+
if (manifest.run.native?.sessionId !== undefined && projected.sessionId !== undefined) {
|
|
854
|
+
replacements.push([manifest.run.native.sessionId, projected.sessionId]);
|
|
855
|
+
}
|
|
856
|
+
if (manifest.run.native?.traceId !== undefined && projected.traceId !== undefined) {
|
|
857
|
+
replacements.push([manifest.run.native.traceId, projected.traceId]);
|
|
858
|
+
}
|
|
859
|
+
return new Map(replacements);
|
|
860
|
+
}
|
|
861
|
+
function sourceCorrelationValues(manifest) {
|
|
862
|
+
return [
|
|
863
|
+
manifest.bundleId,
|
|
864
|
+
manifest.run.id,
|
|
865
|
+
manifest.attempt.id,
|
|
866
|
+
...(manifest.run.native?.sessionId === undefined ? [] : [manifest.run.native.sessionId]),
|
|
867
|
+
...(manifest.run.native?.traceId === undefined ? [] : [manifest.run.native.traceId]),
|
|
868
|
+
];
|
|
869
|
+
}
|
|
870
|
+
function rewrittenId(kind, value, salt) {
|
|
871
|
+
return `${kind}-${createHash("sha256").update(`${salt}\0${value}`).digest("hex").slice(0, 24)}`;
|
|
872
|
+
}
|
|
873
|
+
function digestText(value) {
|
|
874
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
875
|
+
}
|
|
876
|
+
function policyRecord(policy) {
|
|
877
|
+
return {
|
|
878
|
+
sharingClass: policy.sharingClass,
|
|
879
|
+
maxArtifactBytes: policy.maxArtifactBytes,
|
|
880
|
+
maxStringBytes: policy.maxStringBytes,
|
|
881
|
+
sensitiveValueDigests: [...new Set(policy.sensitiveValues ?? [])]
|
|
882
|
+
.sort()
|
|
883
|
+
.map((value) => createHash("sha256").update(value).digest("hex")),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function effectiveSensitiveValues(policy) {
|
|
887
|
+
const environment = Object.entries(process.env)
|
|
888
|
+
.filter(([key, value]) => value !== undefined && value.length >= 8
|
|
889
|
+
&& !["logname", "user", "username"].includes(normalizeFieldName(key)))
|
|
890
|
+
.map(([, value]) => value);
|
|
891
|
+
return [...new Set([...(policy.sensitiveValues ?? []).filter((value) => value !== ""), ...environment])]
|
|
892
|
+
.sort((left, right) => right.length - left.length);
|
|
893
|
+
}
|
|
894
|
+
function validatePolicy(policy) {
|
|
895
|
+
if (!["partner", "public"].includes(policy.sharingClass))
|
|
896
|
+
throw new Error("Portable export sharing class is invalid.");
|
|
897
|
+
for (const [name, value] of [["artifact", policy.maxArtifactBytes], ["string", policy.maxStringBytes]]) {
|
|
898
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
899
|
+
throw new Error(`Portable export ${name} limit must be a positive safe integer.`);
|
|
900
|
+
}
|
|
901
|
+
if (policy.maxStringBytes > policy.maxArtifactBytes)
|
|
902
|
+
throw new Error("Portable export string limit cannot exceed the artifact limit.");
|
|
903
|
+
}
|
|
904
|
+
function extensionFor(mediaType) {
|
|
905
|
+
return mediaType === "application/json" ? ".json"
|
|
906
|
+
: mediaType === "application/x-ndjson" ? ".jsonl"
|
|
907
|
+
: mediaType === "text/x-diff" ? ".patch" : ".txt";
|
|
908
|
+
}
|
|
909
|
+
function truncateUtf8(value, maxBytes) {
|
|
910
|
+
if (Buffer.byteLength(value) <= maxBytes)
|
|
911
|
+
return value;
|
|
912
|
+
let low = 0;
|
|
913
|
+
let high = value.length;
|
|
914
|
+
while (low < high) {
|
|
915
|
+
const middle = Math.ceil((low + high) / 2);
|
|
916
|
+
if (Buffer.byteLength(value.slice(0, middle)) <= maxBytes)
|
|
917
|
+
low = middle;
|
|
918
|
+
else
|
|
919
|
+
high = middle - 1;
|
|
920
|
+
}
|
|
921
|
+
return value.slice(0, low);
|
|
922
|
+
}
|
|
923
|
+
function countOccurrences(value, search) {
|
|
924
|
+
if (search === "")
|
|
925
|
+
return 0;
|
|
926
|
+
let count = 0;
|
|
927
|
+
for (let index = value.indexOf(search); index !== -1; index = value.indexOf(search, index + search.length))
|
|
928
|
+
count += 1;
|
|
929
|
+
return count;
|
|
930
|
+
}
|
|
931
|
+
function increment(counts, action, count = 1) {
|
|
932
|
+
counts.set(action, (counts.get(action) ?? 0) + count);
|
|
933
|
+
}
|
|
934
|
+
function digestValue(value) {
|
|
935
|
+
return { algorithm: "sha256", value: value.slice("sha256:".length) };
|
|
936
|
+
}
|
|
937
|
+
function digestString(value) {
|
|
938
|
+
return `sha256:${value.value}`;
|
|
939
|
+
}
|
|
940
|
+
function decode(bytes, label) {
|
|
941
|
+
try {
|
|
942
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
throw new Error(`${label} is not valid UTF-8.`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function formatValidationErrors(errors) {
|
|
949
|
+
return errors.map((error) => `${error.artifact} ${error.field}: ${error.message}`).join("\n");
|
|
950
|
+
}
|
|
951
|
+
function normalizeFieldName(value) {
|
|
952
|
+
return value.replaceAll(/[^a-z0-9]/giu, "").toLowerCase();
|
|
953
|
+
}
|
|
954
|
+
function resetPatterns() {
|
|
955
|
+
LOCAL_PATH.lastIndex = 0;
|
|
956
|
+
for (const pattern of SECRET_PATTERNS)
|
|
957
|
+
pattern.lastIndex = 0;
|
|
958
|
+
for (const pattern of LOCAL_IDENTIFIER_PATTERNS)
|
|
959
|
+
pattern.lastIndex = 0;
|
|
960
|
+
}
|
|
961
|
+
function errorMessage(error) {
|
|
962
|
+
return error instanceof Error ? error.message : String(error);
|
|
963
|
+
}
|
|
964
|
+
function isRecord(value) {
|
|
965
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
966
|
+
}
|