@bli-cockpit/cli 0.1.8 → 0.1.11

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
@@ -4,8 +4,9 @@ Public BLI Cockpit command-line interface for approved operators and interns.
4
4
 
5
5
  The npm package is public; the Cockpit backend and admin tooling are not. The
6
6
  CLI pairs a local laptop with the private Cockpit dashboard, records safe
7
- work context, uploads consented raw Codex/diff evidence to durable private
8
- storage, and uploads metadata refs after dashboard-approved
7
+ work context, uploads consented raw Codex/Claude/diff evidence and explicit
8
+ images attached into agent sessions to durable private storage, and uploads
9
+ metadata refs after dashboard-approved
9
10
  device pairing.
10
11
 
11
12
  ## One-paste install
@@ -109,11 +110,14 @@ Remote dashboard:
109
110
  - durable private Storage objects accepted by the chunked
110
111
  `/api/ambient/evidence/upload/begin|chunk|commit` endpoints, tracked in a
111
112
  durable per-object upload ledger;
113
+ - attached image artifact metadata accepted by `/api/ambient/agent-artifacts`
114
+ for screenshots/images explicitly attached into Codex or Claude sessions;
112
115
  - Codex session attribution records (session id, file hash, attribution state
113
116
  and reason labels, scores) accepted by `/api/ambient/codex-sessions`.
114
117
 
115
118
  Never provide Supabase service-role keys, raw DB URLs, root env files, cookies,
116
- or deployment tokens to this CLI. The collector must never read env files.
119
+ or deployment tokens to this CLI. The collector must never read env files and
120
+ does not collect random desktop screenshots or screen recordings.
117
121
 
118
122
  ## Public package boundary
119
123
 
@@ -0,0 +1,97 @@
1
+ import { SECRET_FILE_SEGMENT_PATTERN } from "@bli-cockpit/telemetry-core";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { AGENT_IMAGE_MAX_BYTES, validateAgentImageBytes, } from "./agent-image-validation.js";
6
+ import { imageCandidatesFromRecord, } from "./agent-image-records.js";
7
+ export async function collectAgentImageEvidenceFromJsonlFile(options) {
8
+ const stat = await fs.stat(options.filePath).catch(() => null);
9
+ if (!stat?.isFile())
10
+ return { images: [], skipped: [] };
11
+ if (stat.size > AGENT_IMAGE_MAX_BYTES) {
12
+ return {
13
+ images: [],
14
+ skipped: [{ label: "transcript", reason: "file_too_large" }],
15
+ };
16
+ }
17
+ const content = await fs.readFile(options.filePath, "utf8").catch(() => null);
18
+ if (content === null)
19
+ return { images: [], skipped: [] };
20
+ const images = [];
21
+ const skipped = [];
22
+ let lineIndex = 0;
23
+ for (const line of content.split("\n")) {
24
+ if (!line.trim())
25
+ continue;
26
+ lineIndex += 1;
27
+ let record;
28
+ try {
29
+ record = JSON.parse(line);
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ for (const candidate of imageCandidatesFromRecord(record, {
35
+ source: options.source,
36
+ sessionId: options.sessionId,
37
+ sidecarId: options.sidecarId,
38
+ fallbackOccurredAt: new Date(stat.mtimeMs).toISOString(),
39
+ lineIndex,
40
+ })) {
41
+ const result = await loadCandidate(candidate, path.dirname(options.filePath));
42
+ if ("reason" in result) {
43
+ skipped.push({ label: candidate.label, reason: result.reason });
44
+ }
45
+ else {
46
+ images.push(result);
47
+ }
48
+ }
49
+ }
50
+ return { images, skipped };
51
+ }
52
+ async function loadCandidate(candidate, transcriptDir) {
53
+ let bytes;
54
+ if (candidate.dataValue) {
55
+ bytes = Buffer.from(candidate.dataValue, "base64");
56
+ }
57
+ else if (candidate.pathValue) {
58
+ const imagePath = path.isAbsolute(candidate.pathValue)
59
+ ? candidate.pathValue
60
+ : path.resolve(transcriptDir, candidate.pathValue);
61
+ if (SECRET_FILE_SEGMENT_PATTERN.test(imagePath)) {
62
+ return { reason: "secret_like_file_name" };
63
+ }
64
+ bytes = await fs.readFile(imagePath).catch(() => Buffer.alloc(0));
65
+ if (bytes.byteLength === 0)
66
+ return { reason: "file_read_failed" };
67
+ }
68
+ else {
69
+ return { reason: "missing_image_bytes" };
70
+ }
71
+ const validation = validateAgentImageBytes(bytes, candidate.declaredMediaType);
72
+ if ("reason" in validation)
73
+ return validation;
74
+ return {
75
+ bytes,
76
+ extension: validation.extension,
77
+ label: candidate.label,
78
+ metadata: {
79
+ agent_source: candidate.source,
80
+ source_session_id: candidate.sessionId,
81
+ ...(candidate.sourceMessageId ? { source_message_id: candidate.sourceMessageId } : {}),
82
+ ...(candidate.sidecarId ? { source_sidecar_id: candidate.sidecarId } : {}),
83
+ ...(candidate.turnIndex !== undefined ? { turn_index: candidate.turnIndex } : {}),
84
+ occurred_at: candidate.occurredAt,
85
+ artifact_kind: candidate.artifactKind,
86
+ capture_origin: "agent_session_attachment",
87
+ width: validation.width,
88
+ height: validation.height,
89
+ media_type: validation.mediaType,
90
+ content_hash_sha256: sha256(bytes),
91
+ byte_size: bytes.byteLength,
92
+ },
93
+ };
94
+ }
95
+ function sha256(value) {
96
+ return crypto.createHash("sha256").update(value).digest("hex");
97
+ }
@@ -0,0 +1,147 @@
1
+ export function imageCandidatesFromRecord(record, context) {
2
+ if (!isRecord(record))
3
+ return [];
4
+ const payload = recordOf(record["payload"]);
5
+ const message = recordOf(record["message"]) ?? recordOf(payload?.["message"]);
6
+ const sourceMessageId = stringFrom(record, ["id", "message_id", "messageId", "uuid"]) ??
7
+ stringFrom(payload, ["id", "message_id", "messageId", "uuid"]);
8
+ const turnIndex = numberFrom(record, ["turn_index", "turnIndex"]) ??
9
+ numberFrom(payload, ["turn_index", "turnIndex"]);
10
+ const occurredAt = isoFrom(stringFrom(record, ["timestamp", "created_at", "createdAt", "time"]) ??
11
+ stringFrom(payload, ["timestamp", "created_at", "createdAt", "time"])) ?? context.fallbackOccurredAt;
12
+ const candidates = [];
13
+ const base = {
14
+ source: context.source,
15
+ sessionId: context.sessionId,
16
+ sidecarId: context.sidecarId,
17
+ sourceMessageId,
18
+ turnIndex,
19
+ occurredAt,
20
+ lineIndex: context.lineIndex,
21
+ };
22
+ for (const value of explicitAttachmentObjects(record, payload, message)) {
23
+ const candidate = candidateFromObject(value, base, true);
24
+ if (candidate)
25
+ candidates.push(candidate);
26
+ }
27
+ for (const value of explicitImageBlocks(record, payload, message)) {
28
+ const candidate = candidateFromObject(value, base, false);
29
+ if (candidate)
30
+ candidates.push(candidate);
31
+ }
32
+ return candidates;
33
+ }
34
+ function explicitAttachmentObjects(record, payload, message) {
35
+ const out = [];
36
+ if (isAttachmentRecord(record))
37
+ out.push(record);
38
+ if (payload && isAttachmentRecord(payload))
39
+ out.push(payload);
40
+ for (const holder of [record, payload, message]) {
41
+ if (!holder)
42
+ continue;
43
+ const single = holder["attachment"];
44
+ if (isRecord(single))
45
+ out.push(single);
46
+ const attachments = holder["attachments"];
47
+ if (Array.isArray(attachments))
48
+ out.push(...attachments);
49
+ }
50
+ return out;
51
+ }
52
+ function explicitImageBlocks(record, payload, message) {
53
+ const out = [];
54
+ for (const holder of [record, payload, message]) {
55
+ const content = holder?.["content"];
56
+ if (Array.isArray(content))
57
+ out.push(...content.filter((value) => isRecord(value)));
58
+ }
59
+ return out;
60
+ }
61
+ function candidateFromObject(value, base, attachmentEnvelope) {
62
+ if (!isRecord(value))
63
+ return null;
64
+ const nestedSource = recordOf(value["source"]);
65
+ const imageUrl = recordOf(value["image_url"]);
66
+ const typeValue = stringFrom(value, ["type", "kind", "artifact_kind"]);
67
+ const declaredMediaType = stringFrom(value, ["media_type", "mime_type", "mimeType"]) ??
68
+ stringFrom(nestedSource, ["media_type", "mime_type", "mimeType"]);
69
+ const parsedDataUrl = parseDataUrl(stringFrom(imageUrl, ["url"]));
70
+ const pathValue = stringFrom(value, ["path", "local_path", "file_path", "filePath"]) ??
71
+ stringFrom(nestedSource, ["path", "local_path", "file_path", "filePath"]);
72
+ const dataValue = parsedDataUrl?.data ??
73
+ stringFrom(value, ["data_base64", "base64", "content_base64", "data"]) ??
74
+ stringFrom(nestedSource, ["data_base64", "base64", "content_base64", "data"]);
75
+ const mediaType = parsedDataUrl?.mediaType ?? declaredMediaType;
76
+ if (!pathValue && !dataValue)
77
+ return null;
78
+ if (!attachmentEnvelope && !isImageBlockType(typeValue))
79
+ return null;
80
+ if (attachmentEnvelope && !isImageish(typeValue, mediaType))
81
+ return null;
82
+ const objectId = stringFrom(value, ["id", "attachment_id", "attachmentId", "uuid"]);
83
+ return {
84
+ source: base.source,
85
+ sessionId: base.sessionId,
86
+ sidecarId: base.sidecarId,
87
+ sourceMessageId: objectId ?? base.sourceMessageId,
88
+ turnIndex: base.turnIndex,
89
+ occurredAt: base.occurredAt,
90
+ artifactKind: isScreenshot(typeValue) ? "screenshot" : "image_attachment",
91
+ declaredMediaType: mediaType,
92
+ pathValue,
93
+ dataValue,
94
+ label: objectId ?? base.sourceMessageId ?? `line-${base.lineIndex}`,
95
+ };
96
+ }
97
+ function parseDataUrl(value) {
98
+ const match = value?.match(/^data:([^;,]+);base64,(.+)$/i);
99
+ return match ? { mediaType: match[1] ?? "", data: match[2] ?? "" } : null;
100
+ }
101
+ function isAttachmentRecord(value) {
102
+ return ["attachment", "image_attachment", "screenshot"].includes(String(value["type"] ?? "").toLowerCase());
103
+ }
104
+ function isImageBlockType(value) {
105
+ return ["image", "input_image", "image_attachment", "screenshot"].includes(value?.toLowerCase() ?? "");
106
+ }
107
+ function isImageish(typeValue, mediaType) {
108
+ return isImageBlockType(typeValue) || Boolean(normalizeMediaType(mediaType));
109
+ }
110
+ function isScreenshot(typeValue) {
111
+ return typeValue?.toLowerCase().includes("screenshot") ?? false;
112
+ }
113
+ function normalizeMediaType(value) {
114
+ return ["image/png", "image/jpeg", "image/jpg"].includes(value?.trim().toLowerCase() ?? "");
115
+ }
116
+ function isoFrom(value) {
117
+ if (!value)
118
+ return null;
119
+ const date = new Date(value);
120
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
121
+ }
122
+ function stringFrom(record, keys) {
123
+ if (!record)
124
+ return undefined;
125
+ for (const key of keys) {
126
+ const value = record[key];
127
+ if (typeof value === "string" && value.trim())
128
+ return value.trim();
129
+ }
130
+ return undefined;
131
+ }
132
+ function numberFrom(record, keys) {
133
+ if (!record)
134
+ return undefined;
135
+ for (const key of keys) {
136
+ const value = record[key];
137
+ if (Number.isInteger(value))
138
+ return value;
139
+ }
140
+ return undefined;
141
+ }
142
+ function recordOf(value) {
143
+ return isRecord(value) ? value : undefined;
144
+ }
145
+ function isRecord(value) {
146
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
147
+ }
@@ -0,0 +1,88 @@
1
+ export const AGENT_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
2
+ export const AGENT_IMAGE_MAX_DIMENSION_PX = 16_384;
3
+ export const AGENT_IMAGE_MAX_PIXELS = 50_000_000;
4
+ export function validateAgentImageBytes(bytes, declaredMediaType) {
5
+ if (bytes.byteLength > AGENT_IMAGE_MAX_BYTES)
6
+ return { reason: "file_too_large" };
7
+ if (declaredMediaType && !normalizeMediaType(declaredMediaType)) {
8
+ return { reason: "unsupported_media_type" };
9
+ }
10
+ const detected = detectImage(bytes);
11
+ if (!detected)
12
+ return { reason: "invalid_image_magic" };
13
+ if (detected.width <= 0 ||
14
+ detected.height <= 0 ||
15
+ detected.width > AGENT_IMAGE_MAX_DIMENSION_PX ||
16
+ detected.height > AGENT_IMAGE_MAX_DIMENSION_PX ||
17
+ detected.width * detected.height > AGENT_IMAGE_MAX_PIXELS) {
18
+ return { reason: "image_pixel_limit_exceeded" };
19
+ }
20
+ return {
21
+ ...detected,
22
+ extension: detected.mediaType === "image/png" ? "png" : "jpg",
23
+ };
24
+ }
25
+ function detectImage(bytes) {
26
+ const png = parsePngDimensions(bytes);
27
+ if (png)
28
+ return { mediaType: "image/png", ...png };
29
+ const jpeg = parseJpegDimensions(bytes);
30
+ if (jpeg)
31
+ return { mediaType: "image/jpeg", ...jpeg };
32
+ return null;
33
+ }
34
+ function parsePngDimensions(bytes) {
35
+ const signature = "89504e470d0a1a0a";
36
+ if (bytes.byteLength < 24 || bytes.subarray(0, 8).toString("hex") !== signature) {
37
+ return null;
38
+ }
39
+ if (bytes.subarray(12, 16).toString("ascii") !== "IHDR")
40
+ return null;
41
+ return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
42
+ }
43
+ function parseJpegDimensions(bytes) {
44
+ if (bytes.byteLength < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8)
45
+ return null;
46
+ let offset = 2;
47
+ while (offset + 3 < bytes.byteLength) {
48
+ while (offset < bytes.byteLength && bytes[offset] === 0xff)
49
+ offset += 1;
50
+ if (offset + 2 > bytes.byteLength)
51
+ return null;
52
+ const marker = bytes[offset];
53
+ if (marker === undefined)
54
+ return null;
55
+ offset += 1;
56
+ if (marker === 0xd9 || marker === 0xda)
57
+ break;
58
+ if (marker !== undefined && marker >= 0xd0 && marker <= 0xd7)
59
+ continue;
60
+ if (offset + 2 > bytes.byteLength)
61
+ return null;
62
+ const length = bytes.readUInt16BE(offset);
63
+ if (length < 2 || offset + length > bytes.byteLength)
64
+ return null;
65
+ if (isJpegSofMarker(marker) && offset + 7 <= bytes.byteLength) {
66
+ return {
67
+ height: bytes.readUInt16BE(offset + 3),
68
+ width: bytes.readUInt16BE(offset + 5),
69
+ };
70
+ }
71
+ offset += length;
72
+ }
73
+ return null;
74
+ }
75
+ function isJpegSofMarker(marker) {
76
+ return (marker !== undefined &&
77
+ marker >= 0xc0 &&
78
+ marker <= 0xcf &&
79
+ ![0xc4, 0xc8, 0xcc].includes(marker));
80
+ }
81
+ function normalizeMediaType(value) {
82
+ const normalized = value?.trim().toLowerCase();
83
+ if (normalized === "image/png")
84
+ return "image/png";
85
+ if (normalized === "image/jpeg" || normalized === "image/jpg")
86
+ return "image/jpeg";
87
+ return null;
88
+ }
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  import { makeSourceAdapterIdentity, } from "./common.js";
9
+ import { collectAgentImageEvidenceFromJsonlFile, } from "./agent-image-evidence.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  const DEFAULT_SINCE_MINUTES = 24 * 60;
11
12
  const DEFAULT_SESSION_LIMIT = 50;
@@ -127,6 +128,9 @@ export async function collectRawEvidencePack(context, options) {
127
128
  local_path: entry.local_path,
128
129
  kind: entry.kind,
129
130
  codex_session_id: entry.codex_session_id ?? null,
131
+ ...(entry.artifact_metadata
132
+ ? { artifact_metadata: entry.artifact_metadata }
133
+ : {}),
130
134
  })),
131
135
  reused,
132
136
  };
@@ -197,7 +201,7 @@ async function collectCodexJsonlFiles(collection, options) {
197
201
  .map((filePath) => ({ filePath, codexSessionId: null }));
198
202
  for (const candidate of candidates) {
199
203
  const codexSessionId = candidate.codexSessionId ?? shortHash(candidate.filePath);
200
- await collectOneEvidenceFile(collection, {
204
+ const transcriptAccepted = await collectOneEvidenceFile(collection, {
201
205
  filePath: candidate.filePath,
202
206
  kind: "codex_jsonl",
203
207
  sessionId: codexSessionId,
@@ -205,6 +209,15 @@ async function collectCodexJsonlFiles(collection, options) {
205
209
  redactedSummary: "Raw Codex JSONL transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
206
210
  contentAddress: (hash16) => `codex/${safeKeySegment(codexSessionId)}/${hash16}.jsonl`,
207
211
  });
212
+ if (transcriptAccepted) {
213
+ await collectAgentImagesFromTranscript(collection, {
214
+ filePath: candidate.filePath,
215
+ source: "codex",
216
+ sessionId: codexSessionId,
217
+ kind: "codex_image_attachment",
218
+ contentAddress: (hash16, extension) => `codex/${safeKeySegment(codexSessionId)}/images/${hash16}.${extension}`,
219
+ });
220
+ }
208
221
  }
209
222
  }
210
223
  async function collectClaudeJsonlFiles(collection, sessions) {
@@ -226,7 +239,7 @@ async function collectClaudeJsonlFiles(collection, sessions) {
226
239
  // skipped entry is recorded here.
227
240
  }
228
241
  else {
229
- await collectOneEvidenceFile(collection, {
242
+ const mainAccepted = await collectOneEvidenceFile(collection, {
230
243
  filePath: session.local_path,
231
244
  kind: "claude_jsonl",
232
245
  sessionId,
@@ -238,10 +251,28 @@ async function collectClaudeJsonlFiles(collection, sessions) {
238
251
  redactedSummary: "Raw Claude Code JSONL transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
239
252
  contentAddress: (hash16) => `claude/${safeKeySegment(sessionId)}/${hash16}.jsonl`,
240
253
  });
254
+ if (mainAccepted) {
255
+ await collectAgentImagesFromTranscript(collection, {
256
+ filePath: session.local_path,
257
+ source: "claude_code",
258
+ sessionId,
259
+ kind: "claude_image_attachment",
260
+ contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/images/${hash16}.${extension}`,
261
+ });
262
+ }
263
+ }
264
+ if (session.skip_main && !session.main_file_oversized) {
265
+ await collectAgentImagesFromTranscript(collection, {
266
+ filePath: session.local_path,
267
+ source: "claude_code",
268
+ sessionId,
269
+ kind: "claude_image_attachment",
270
+ contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/images/${hash16}.${extension}`,
271
+ });
241
272
  }
242
273
  for (const sidecar of session.sidecar_files) {
243
274
  const stem = path.basename(sidecar.local_path).replace(/\.jsonl$/i, "");
244
- await collectOneEvidenceFile(collection, {
275
+ const sidecarAccepted = await collectOneEvidenceFile(collection, {
245
276
  filePath: sidecar.local_path,
246
277
  kind: "claude_jsonl_sidecar",
247
278
  sessionId,
@@ -250,9 +281,89 @@ async function collectClaudeJsonlFiles(collection, sessions) {
250
281
  redactedSummary: "Raw Claude Code subagent transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
251
282
  contentAddress: (hash16) => `claude/${safeKeySegment(sessionId)}/subagents/${safeKeySegment(stem)}-${hash16}.jsonl`,
252
283
  });
284
+ if (sidecarAccepted) {
285
+ await collectAgentImagesFromTranscript(collection, {
286
+ filePath: sidecar.local_path,
287
+ source: "claude_code",
288
+ sessionId,
289
+ sidecarId: stem,
290
+ kind: "claude_image_attachment",
291
+ contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/subagents/${safeKeySegment(stem)}/images/${hash16}.${extension}`,
292
+ });
293
+ }
253
294
  }
254
295
  }
255
296
  }
297
+ async function collectAgentImagesFromTranscript(collection, options) {
298
+ const result = await collectAgentImageEvidenceFromJsonlFile({
299
+ filePath: options.filePath,
300
+ source: options.source,
301
+ sessionId: options.sessionId,
302
+ sidecarId: options.sidecarId,
303
+ });
304
+ for (const skipped of result.skipped) {
305
+ collection.skipped.push({
306
+ kind: options.kind,
307
+ label: skipped.label,
308
+ reason: skipped.reason,
309
+ });
310
+ }
311
+ for (const image of result.images) {
312
+ await collectOneAgentImageFile(collection, {
313
+ image,
314
+ kind: options.kind,
315
+ sessionId: options.sessionId,
316
+ contentAddress: options.contentAddress,
317
+ });
318
+ }
319
+ }
320
+ async function collectOneAgentImageFile(collection, options) {
321
+ const raw = options.image.bytes;
322
+ const contentHash = sha256(raw);
323
+ const metadata = {
324
+ ...options.image.metadata,
325
+ content_hash_sha256: contentHash,
326
+ byte_size: raw.byteLength,
327
+ };
328
+ if (collection.skipContentHashes.has(contentHash)) {
329
+ collection.reused.push({
330
+ kind: options.kind,
331
+ label: options.image.label,
332
+ content_hash_sha256: contentHash,
333
+ codex_session_id: options.sessionId,
334
+ artifact_metadata: metadata,
335
+ });
336
+ return;
337
+ }
338
+ const deferReason = admitToBudget(collection.budget, raw.byteLength);
339
+ if (deferReason) {
340
+ collection.skipped.push({
341
+ kind: options.kind,
342
+ label: options.image.label,
343
+ reason: deferReason,
344
+ });
345
+ return;
346
+ }
347
+ collection.index.value += 1;
348
+ const relativePath = path.join("files", `${String(collection.index.value).padStart(3, "0")}-agent-image-${contentHash.slice(0, 16)}.${options.image.extension}`);
349
+ const destination = path.join(collection.filesDir, path.basename(relativePath));
350
+ await fs.writeFile(destination, raw, { mode: 0o600 });
351
+ await chmodPrivate(destination, 0o600);
352
+ collection.entries.push(evidenceEntry({
353
+ kind: options.kind,
354
+ packId: collection.packId,
355
+ operatorId: collection.context.operatorId,
356
+ workContextId: collection.context.workContextId,
357
+ localPath: destination,
358
+ relativePath,
359
+ mediaType: metadata.media_type,
360
+ redactedSummary: "Raw image explicitly attached to an agent session, preserved in private durable storage.",
361
+ bytes: raw,
362
+ codexSessionId: options.sessionId,
363
+ contentAddress: options.contentAddress(contentHash.slice(0, 16), options.image.extension),
364
+ artifactMetadata: metadata,
365
+ }));
366
+ }
256
367
  /**
257
368
  * Reads, secret-guards, content-addresses, budget-checks, and copies one
258
369
  * attributed transcript into the pack. The secret guard runs again here even
@@ -268,7 +379,7 @@ async function collectOneEvidenceFile(collection, options) {
268
379
  label: fileName,
269
380
  reason: "secret_like_file_name",
270
381
  });
271
- return;
382
+ return false;
272
383
  }
273
384
  let raw;
274
385
  try {
@@ -280,7 +391,7 @@ async function collectOneEvidenceFile(collection, options) {
280
391
  label: fileName,
281
392
  reason: "file_read_failed",
282
393
  });
283
- return;
394
+ return false;
284
395
  }
285
396
  if (options.maxFileBytes && raw.byteLength > options.maxFileBytes) {
286
397
  collection.skipped.push({
@@ -288,7 +399,7 @@ async function collectOneEvidenceFile(collection, options) {
288
399
  label: fileName,
289
400
  reason: "file_too_large",
290
401
  });
291
- return;
402
+ return false;
292
403
  }
293
404
  if (containsSecretLikeContent(raw.toString("utf8"))) {
294
405
  collection.skipped.push({
@@ -296,7 +407,7 @@ async function collectOneEvidenceFile(collection, options) {
296
407
  label: fileName,
297
408
  reason: "secret_like_content_guard",
298
409
  });
299
- return;
410
+ return false;
300
411
  }
301
412
  const contentHash = sha256(raw);
302
413
  if (collection.skipContentHashes.has(contentHash)) {
@@ -306,7 +417,7 @@ async function collectOneEvidenceFile(collection, options) {
306
417
  content_hash_sha256: contentHash,
307
418
  codex_session_id: options.sessionId,
308
419
  });
309
- return;
420
+ return true;
310
421
  }
311
422
  const deferReason = admitToBudget(collection.budget, raw.byteLength);
312
423
  if (deferReason) {
@@ -315,7 +426,7 @@ async function collectOneEvidenceFile(collection, options) {
315
426
  label: fileName,
316
427
  reason: deferReason,
317
428
  });
318
- return;
429
+ return false;
319
430
  }
320
431
  collection.index.value += 1;
321
432
  const relativePath = path.join("files", `${String(collection.index.value).padStart(3, "0")}-${shortHash(options.filePath)}-${fileName}`);
@@ -335,6 +446,7 @@ async function collectOneEvidenceFile(collection, options) {
335
446
  codexSessionId: options.sessionId,
336
447
  contentAddress: options.contentAddress(contentHash.slice(0, 16)),
337
448
  }));
449
+ return true;
338
450
  }
339
451
  /**
340
452
  * Decrements the per-sync budget when a file fits, or returns a deferred-skip
@@ -475,6 +587,7 @@ function makeManifest(options) {
475
587
  claude_transcripts: "preserved_private_durable_remote",
476
588
  tool_payloads: "preserved_private_durable_remote",
477
589
  git_diffs: "preserved_private_durable_remote_env_secret_paths_excluded",
590
+ agent_image_attachments: "preserved_private_durable_remote_explicit_agent_session_attachment_only",
478
591
  env_files: "never_read",
479
592
  stdout: "manifest_only_no_raw_content",
480
593
  },
@@ -485,22 +598,35 @@ function makeManifest(options) {
485
598
  }
486
599
  function evidenceEntry(options) {
487
600
  const digest = sha256(options.bytes);
601
+ const objectKey = remoteObjectKey({
602
+ operatorId: options.operatorId,
603
+ workContextId: options.workContextId,
604
+ packId: options.packId,
605
+ relativePath: options.relativePath,
606
+ contentAddress: options.contentAddress,
607
+ });
488
608
  return {
489
609
  kind: options.kind,
490
610
  local_path: options.localPath,
491
611
  relative_path: options.relativePath,
492
- object_key: remoteObjectKey({
493
- operatorId: options.operatorId,
494
- workContextId: options.workContextId,
495
- packId: options.packId,
496
- relativePath: options.relativePath,
497
- contentAddress: options.contentAddress,
498
- }),
612
+ object_key: objectKey,
499
613
  content_hash_sha256: digest,
500
614
  byte_size: options.bytes.byteLength,
501
615
  media_type: options.mediaType,
502
616
  redacted_summary: options.redactedSummary,
503
617
  codex_session_id: options.codexSessionId ?? null,
618
+ ...(options.artifactMetadata
619
+ ? {
620
+ artifact_metadata: {
621
+ ...options.artifactMetadata,
622
+ raw_evidence_pointer_id: objectKey,
623
+ storage_bucket: RAW_EVIDENCE_BUCKET,
624
+ object_key: objectKey,
625
+ content_hash_sha256: digest,
626
+ byte_size: options.bytes.byteLength,
627
+ },
628
+ }
629
+ : {}),
504
630
  };
505
631
  }
506
632
  function redactManifestEntry(entry) {
@@ -512,6 +638,9 @@ function redactManifestEntry(entry) {
512
638
  byte_size: entry.byte_size,
513
639
  media_type: entry.media_type,
514
640
  redacted_summary: entry.redacted_summary,
641
+ ...(entry.artifact_metadata
642
+ ? { artifact_metadata: entry.artifact_metadata }
643
+ : {}),
515
644
  };
516
645
  }
517
646
  function pointerFromEntry(entry) {