@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 +7 -3
- package/dist/adapters/agent-image-evidence.js +97 -0
- package/dist/adapters/agent-image-records.js +147 -0
- package/dist/adapters/agent-image-validation.js +88 -0
- package/dist/adapters/raw-evidence.js +145 -16
- package/dist/autostart.js +197 -0
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +135 -851
- package/dist/commands/session-sync.js +504 -0
- package/dist/evidence-upload-client.js +7 -0
- package/dist/local-state.js +1 -1
- package/dist/upload.js +152 -3
- package/package.json +2 -2
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
|
|
8
|
-
storage, and uploads
|
|
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:
|
|
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) {
|