@darkhunt-security/endpoint-codex 0.9.10 → 0.9.12
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/.codex-plugin/plugin.json +1 -1
- package/dist/bin/backfill.mjs +256 -12
- package/dist/bin/forwarder.mjs +256 -12
- package/dist/bin/guard.mjs +8 -0
- package/dist/bin/spool.mjs +8 -0
- package/dist/bin/status.mjs +192 -5
- package/package.json +3 -3
package/dist/bin/backfill.mjs
CHANGED
|
@@ -18308,6 +18308,7 @@ function attachmentLabel(record) {
|
|
|
18308
18308
|
return `[${record.mediaType ?? "attachment"}${size}]`;
|
|
18309
18309
|
}
|
|
18310
18310
|
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18311
|
+
var USAGE_WIRE_NAMES = { thinking_tokens: "reasoning" };
|
|
18311
18312
|
var SessionEmitter = class {
|
|
18312
18313
|
client;
|
|
18313
18314
|
options;
|
|
@@ -18331,6 +18332,8 @@ var SessionEmitter = class {
|
|
|
18331
18332
|
/** Attachments seen before the trace could be opened knowing the prompt. */
|
|
18332
18333
|
deferred = [];
|
|
18333
18334
|
pendingUsage;
|
|
18335
|
+
/** Set when the model call this generation stands for failed. Cleared with it. */
|
|
18336
|
+
generationError;
|
|
18334
18337
|
outputText = [];
|
|
18335
18338
|
/** The last thing the assistant said in this pass — the trace's result. */
|
|
18336
18339
|
lastAssistantText;
|
|
@@ -18386,9 +18389,10 @@ var SessionEmitter = class {
|
|
|
18386
18389
|
this.count++;
|
|
18387
18390
|
if (record.kind === "attachment")
|
|
18388
18391
|
this.pendingAttachments.push(attachmentLabel(record));
|
|
18389
|
-
if (record.kind === "user_message")
|
|
18392
|
+
if (record.kind === "user_message" && !record.isError && !record.synthetic) {
|
|
18390
18393
|
this.absorbUserTurn(record);
|
|
18391
|
-
|
|
18394
|
+
}
|
|
18395
|
+
if (!this.trace && (record.kind === "attachment" || record.synthetic === true)) {
|
|
18392
18396
|
this.deferred.push(record);
|
|
18393
18397
|
return;
|
|
18394
18398
|
}
|
|
@@ -18426,6 +18430,15 @@ var SessionEmitter = class {
|
|
|
18426
18430
|
this.owners.set(record.uuid, owner);
|
|
18427
18431
|
switch (record.kind) {
|
|
18428
18432
|
case "user_message":
|
|
18433
|
+
if (record.synthetic) {
|
|
18434
|
+
this.seeded(["injected", record.uuid], () => owner.span("context_injection", {
|
|
18435
|
+
startTime: record.ts,
|
|
18436
|
+
...record.text !== void 0 ? { output: summarize(record.text) } : {}
|
|
18437
|
+
})).end({ endTime: record.ts });
|
|
18438
|
+
break;
|
|
18439
|
+
}
|
|
18440
|
+
if (record.isError)
|
|
18441
|
+
this.generationError = record.errorKind ?? "error";
|
|
18429
18442
|
this.endGeneration();
|
|
18430
18443
|
break;
|
|
18431
18444
|
case "attachment": {
|
|
@@ -18442,7 +18455,9 @@ var SessionEmitter = class {
|
|
|
18442
18455
|
}
|
|
18443
18456
|
case "assistant_message":
|
|
18444
18457
|
this.openGeneration(record, owner);
|
|
18445
|
-
if (record.
|
|
18458
|
+
if (record.isError)
|
|
18459
|
+
this.generationError = record.errorKind ?? "error";
|
|
18460
|
+
else if (record.text)
|
|
18446
18461
|
this.outputText.push(record.text);
|
|
18447
18462
|
break;
|
|
18448
18463
|
case "thinking": {
|
|
@@ -18471,15 +18486,44 @@ var SessionEmitter = class {
|
|
|
18471
18486
|
case "tool_result": {
|
|
18472
18487
|
const span = record.toolCallId ? this.toolSpans.get(record.toolCallId) : void 0;
|
|
18473
18488
|
if (span) {
|
|
18474
|
-
span.end({
|
|
18489
|
+
span.end({
|
|
18490
|
+
output: record.output,
|
|
18491
|
+
endTime: record.ts,
|
|
18492
|
+
...record.isError ? { level: "ERROR" } : {},
|
|
18493
|
+
...record.errorKind !== void 0 ? { statusMessage: record.errorKind } : {}
|
|
18494
|
+
});
|
|
18475
18495
|
this.toolSpans.delete(record.toolCallId);
|
|
18476
18496
|
}
|
|
18477
18497
|
this.pendingToolResults.push(summarize(record.output));
|
|
18478
18498
|
break;
|
|
18479
18499
|
}
|
|
18500
|
+
case "compaction":
|
|
18501
|
+
this.endGeneration();
|
|
18502
|
+
this.seeded(["compaction", record.uuid], () => owner.span("compaction", {
|
|
18503
|
+
startTime: record.ts,
|
|
18504
|
+
...record.text ? { output: record.text } : {}
|
|
18505
|
+
})).end({ endTime: record.ts });
|
|
18506
|
+
break;
|
|
18507
|
+
case "permission_grant": {
|
|
18508
|
+
const snapshot = record.input && typeof record.input === "object" ? record.input : void 0;
|
|
18509
|
+
const steps = Array.isArray(snapshot?.steps) ? snapshot.steps : [];
|
|
18510
|
+
this.seeded(["permission", record.uuid], () => owner.span("permission_grant", {
|
|
18511
|
+
startTime: record.ts,
|
|
18512
|
+
// `input`, not `output`: the list is what this record IS, not a result it
|
|
18513
|
+
// produced.
|
|
18514
|
+
input: steps,
|
|
18515
|
+
metadata: {
|
|
18516
|
+
// The true count, which is larger than `steps.length` when the mapper
|
|
18517
|
+
// truncated the snapshot to its byte budget.
|
|
18518
|
+
count: String(snapshot?.count ?? steps.length),
|
|
18519
|
+
full: String(snapshot?.full === true)
|
|
18520
|
+
}
|
|
18521
|
+
})).end({ endTime: record.ts });
|
|
18522
|
+
break;
|
|
18523
|
+
}
|
|
18480
18524
|
case "usage":
|
|
18481
18525
|
if (record.usage) {
|
|
18482
|
-
this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number"));
|
|
18526
|
+
this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number").map(([key, value]) => [USAGE_WIRE_NAMES[key] ?? key, value]));
|
|
18483
18527
|
}
|
|
18484
18528
|
if (record.model)
|
|
18485
18529
|
this.model = record.model;
|
|
@@ -18636,7 +18680,18 @@ var SessionEmitter = class {
|
|
|
18636
18680
|
this.model = record.model;
|
|
18637
18681
|
this.generation = this.seeded(["generation", id], () => owner.generation("assistant", {
|
|
18638
18682
|
startTime: record.ts,
|
|
18639
|
-
...this.model !== void 0 ? { model: this.model } : {}
|
|
18683
|
+
...this.model !== void 0 ? { model: this.model } : {},
|
|
18684
|
+
// What drove this model call — an installed skill, an MCP server's output, a
|
|
18685
|
+
// plugin. A flat scalar like every other metadata value emitted here, because
|
|
18686
|
+
// trace-hub lifts each key onto the span unchanged and a nested value would
|
|
18687
|
+
// arrive as a blob nothing can group or filter on. One lowercase word, so unlike
|
|
18688
|
+
// `agent_id` there is no camelCase to convert.
|
|
18689
|
+
//
|
|
18690
|
+
// Set once, at open, and never through update(): this method early-returns when
|
|
18691
|
+
// the generation is already open, so the value has to be constant across a
|
|
18692
|
+
// message group. Measured over the claude-code corpus, 0 of 1,381 attributed
|
|
18693
|
+
// groups carry two different tokens.
|
|
18694
|
+
...record.attribution !== void 0 ? { metadata: { attribution: record.attribution } } : {}
|
|
18640
18695
|
}));
|
|
18641
18696
|
const input = this.buildInputMessages();
|
|
18642
18697
|
if (input.inputMessages)
|
|
@@ -18676,12 +18731,14 @@ var SessionEmitter = class {
|
|
|
18676
18731
|
this.generation.end({
|
|
18677
18732
|
...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
|
|
18678
18733
|
...this.model !== void 0 ? { model: this.model } : {},
|
|
18679
|
-
...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {}
|
|
18734
|
+
...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {},
|
|
18735
|
+
...this.generationError !== void 0 ? { level: "ERROR", statusMessage: this.generationError } : {}
|
|
18680
18736
|
});
|
|
18681
18737
|
this.generation = void 0;
|
|
18682
18738
|
this.generationId = void 0;
|
|
18683
18739
|
this.outputText = [];
|
|
18684
18740
|
this.pendingUsage = void 0;
|
|
18741
|
+
this.generationError = void 0;
|
|
18685
18742
|
}
|
|
18686
18743
|
};
|
|
18687
18744
|
|
|
@@ -21939,6 +21996,62 @@ function textOf(content) {
|
|
|
21939
21996
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
21940
21997
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
21941
21998
|
}
|
|
21999
|
+
var CODEX_APPROVAL_MODES = {
|
|
22000
|
+
never: "auto",
|
|
22001
|
+
"on-request": "ask"
|
|
22002
|
+
};
|
|
22003
|
+
var CODEX_SANDBOX_MODES = {
|
|
22004
|
+
"workspace-write": "workspace-write",
|
|
22005
|
+
"read-only": "read-only",
|
|
22006
|
+
"danger-full-access": "danger-full-access"
|
|
22007
|
+
};
|
|
22008
|
+
var PERMISSION_SNAPSHOT_LIMIT = 32e3;
|
|
22009
|
+
function budgeted(steps) {
|
|
22010
|
+
const kept = [];
|
|
22011
|
+
let bytes = 0;
|
|
22012
|
+
for (const step of steps) {
|
|
22013
|
+
bytes += JSON.stringify(step ?? null).length + 1;
|
|
22014
|
+
if (bytes > PERMISSION_SNAPSHOT_LIMIT)
|
|
22015
|
+
break;
|
|
22016
|
+
kept.push(step);
|
|
22017
|
+
}
|
|
22018
|
+
return kept;
|
|
22019
|
+
}
|
|
22020
|
+
function execEnvelope(text) {
|
|
22021
|
+
let parsed;
|
|
22022
|
+
try {
|
|
22023
|
+
parsed = JSON.parse(text);
|
|
22024
|
+
} catch {
|
|
22025
|
+
return void 0;
|
|
22026
|
+
}
|
|
22027
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
22028
|
+
return void 0;
|
|
22029
|
+
const bag = parsed;
|
|
22030
|
+
if (!Object.prototype.hasOwnProperty.call(bag, "output"))
|
|
22031
|
+
return void 0;
|
|
22032
|
+
const metadata = bag["metadata"];
|
|
22033
|
+
const exitCode = num(bag["exit_code"]) ?? (metadata && typeof metadata === "object" ? num(metadata["exit_code"]) : void 0);
|
|
22034
|
+
if (exitCode === void 0)
|
|
22035
|
+
return void 0;
|
|
22036
|
+
return { output: bag["output"], exitCode };
|
|
22037
|
+
}
|
|
22038
|
+
function execResult(value) {
|
|
22039
|
+
if (typeof value === "string")
|
|
22040
|
+
return execEnvelope(value) ?? { output: value };
|
|
22041
|
+
if (Array.isArray(value)) {
|
|
22042
|
+
for (const block of value) {
|
|
22043
|
+
if (!block || typeof block !== "object")
|
|
22044
|
+
continue;
|
|
22045
|
+
const text = block["text"];
|
|
22046
|
+
if (typeof text !== "string")
|
|
22047
|
+
continue;
|
|
22048
|
+
const envelope = execEnvelope(text);
|
|
22049
|
+
if (envelope)
|
|
22050
|
+
return envelope;
|
|
22051
|
+
}
|
|
22052
|
+
}
|
|
22053
|
+
return { output: textOf(value) ?? value };
|
|
22054
|
+
}
|
|
21942
22055
|
function jwtClaims(token) {
|
|
21943
22056
|
const parts = token.split(".");
|
|
21944
22057
|
if (parts.length !== 3 || !parts[1])
|
|
@@ -21965,6 +22078,29 @@ function emailFromAuth(raw) {
|
|
|
21965
22078
|
const email = jwtClaims(token)?.["email"];
|
|
21966
22079
|
return typeof email === "string" && email !== "" ? email : void 0;
|
|
21967
22080
|
}
|
|
22081
|
+
function repositoryIdentity(url) {
|
|
22082
|
+
if (url === void 0)
|
|
22083
|
+
return void 0;
|
|
22084
|
+
const trimmed = url.trim();
|
|
22085
|
+
if (trimmed === "")
|
|
22086
|
+
return void 0;
|
|
22087
|
+
const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
|
|
22088
|
+
if (scp)
|
|
22089
|
+
return `${scp[1]}/${stripSuffix(scp[2])}`;
|
|
22090
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
|
|
22091
|
+
try {
|
|
22092
|
+
const parsed = new URL(withScheme);
|
|
22093
|
+
if (!parsed.hostname)
|
|
22094
|
+
return trimmed;
|
|
22095
|
+
const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
|
|
22096
|
+
return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
|
|
22097
|
+
} catch {
|
|
22098
|
+
return trimmed;
|
|
22099
|
+
}
|
|
22100
|
+
}
|
|
22101
|
+
function stripSuffix(path) {
|
|
22102
|
+
return path.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
22103
|
+
}
|
|
21968
22104
|
var codexTranscript = {
|
|
21969
22105
|
vendor: "codex",
|
|
21970
22106
|
sessionRoots() {
|
|
@@ -22017,12 +22153,20 @@ var codexTranscript = {
|
|
|
22017
22153
|
const uuid = `codex:${offset}`;
|
|
22018
22154
|
const base = { vendor: "codex", ts, uuid };
|
|
22019
22155
|
if (raw.type === "session_meta") {
|
|
22156
|
+
const rawGit = payload["git"];
|
|
22157
|
+
const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
|
|
22158
|
+
const gitBranch = str(git?.["branch"]);
|
|
22159
|
+
const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
|
|
22020
22160
|
return [
|
|
22021
22161
|
{
|
|
22022
22162
|
...base,
|
|
22023
22163
|
kind: "session_meta",
|
|
22024
22164
|
meta: {
|
|
22025
22165
|
...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
|
|
22166
|
+
// Conditional spread rather than a plain assignment: the 7 `{}` blocks and
|
|
22167
|
+
// the 92 absent ones must leave the key absent, not present-and-undefined.
|
|
22168
|
+
...gitBranch !== void 0 ? { gitBranch } : {},
|
|
22169
|
+
...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
|
|
22026
22170
|
...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
|
|
22027
22171
|
...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
|
|
22028
22172
|
// A Codex subagent is a rollout of its own, a peer of its parent in the
|
|
@@ -22038,15 +22182,61 @@ var codexTranscript = {
|
|
|
22038
22182
|
}
|
|
22039
22183
|
if (raw.type === "turn_context") {
|
|
22040
22184
|
const model = str(payload["model"]);
|
|
22041
|
-
|
|
22185
|
+
const approval = str(payload["approval_policy"]);
|
|
22186
|
+
const sandbox = payload["sandbox_policy"];
|
|
22187
|
+
const sandboxMode = (() => {
|
|
22188
|
+
if (!sandbox || typeof sandbox !== "object")
|
|
22189
|
+
return void 0;
|
|
22190
|
+
const bag = sandbox;
|
|
22191
|
+
return str(bag["mode"]) ?? str(bag["type"]);
|
|
22192
|
+
})();
|
|
22193
|
+
const meta = {
|
|
22194
|
+
...model !== void 0 ? { model } : {},
|
|
22195
|
+
...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
|
|
22196
|
+
...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
|
|
22197
|
+
};
|
|
22198
|
+
if (Object.keys(meta).length === 0)
|
|
22042
22199
|
return [];
|
|
22043
|
-
return [{ ...base, kind: "session_meta", meta
|
|
22200
|
+
return [{ ...base, kind: "session_meta", meta }];
|
|
22044
22201
|
}
|
|
22045
22202
|
const kind = str(payload["type"]);
|
|
22046
22203
|
if (raw.type === "event_msg" && kind === "token_count") {
|
|
22047
22204
|
const usage = mapTokenCount(payload["info"]);
|
|
22048
22205
|
return usage ? [{ ...base, kind: "usage", usage }] : [];
|
|
22049
22206
|
}
|
|
22207
|
+
if (raw.type === "compacted") {
|
|
22208
|
+
const note = str(payload["message"]);
|
|
22209
|
+
return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
|
|
22210
|
+
}
|
|
22211
|
+
if (raw.type === "world_state") {
|
|
22212
|
+
const state = payload["state"];
|
|
22213
|
+
const perms = state && typeof state === "object" ? state["permissions"] : void 0;
|
|
22214
|
+
if (!perms || typeof perms !== "object")
|
|
22215
|
+
return [];
|
|
22216
|
+
const list = perms["approved_command_prefixes"];
|
|
22217
|
+
const steps = Array.isArray(list) ? list : [];
|
|
22218
|
+
return [
|
|
22219
|
+
{
|
|
22220
|
+
...base,
|
|
22221
|
+
kind: "permission_grant",
|
|
22222
|
+
input: {
|
|
22223
|
+
steps: budgeted(steps),
|
|
22224
|
+
count: steps.length,
|
|
22225
|
+
...payload["full"] === true ? { full: true } : {}
|
|
22226
|
+
}
|
|
22227
|
+
}
|
|
22228
|
+
];
|
|
22229
|
+
}
|
|
22230
|
+
if (raw.type === "event_msg" && kind === "turn_aborted") {
|
|
22231
|
+
return [
|
|
22232
|
+
{
|
|
22233
|
+
...base,
|
|
22234
|
+
kind: "user_message",
|
|
22235
|
+
isError: true,
|
|
22236
|
+
errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
|
|
22237
|
+
}
|
|
22238
|
+
];
|
|
22239
|
+
}
|
|
22050
22240
|
if (raw.type !== "response_item")
|
|
22051
22241
|
return [];
|
|
22052
22242
|
switch (kind) {
|
|
@@ -22060,18 +22250,72 @@ var codexTranscript = {
|
|
|
22060
22250
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22061
22251
|
}
|
|
22062
22252
|
];
|
|
22063
|
-
case "function_call_output":
|
|
22253
|
+
case "function_call_output": {
|
|
22254
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
22064
22255
|
return [
|
|
22065
22256
|
{
|
|
22066
22257
|
...base,
|
|
22067
22258
|
kind: "tool_result",
|
|
22068
|
-
output
|
|
22259
|
+
output,
|
|
22260
|
+
// Only where an exit status was actually found. On the 993 string outputs
|
|
22261
|
+
// that are not envelopes and the 144 array ones the key stays ABSENT rather
|
|
22262
|
+
// than `false`: the emitter tests `record.isError`, and "this tool reported
|
|
22263
|
+
// success" is a different claim from "nothing here reported an outcome".
|
|
22264
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
22069
22265
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22070
22266
|
}
|
|
22071
22267
|
];
|
|
22268
|
+
}
|
|
22269
|
+
// Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
|
|
22270
|
+
// `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
|
|
22271
|
+
// across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
|
|
22272
|
+
// handling only `function_call` left every shell command and every file write
|
|
22273
|
+
// this endpoint made invisible to both the trace and anything reading it.
|
|
22274
|
+
case "custom_tool_call":
|
|
22275
|
+
return [
|
|
22276
|
+
{
|
|
22277
|
+
...base,
|
|
22278
|
+
kind: "tool_call",
|
|
22279
|
+
toolName: str(payload["name"]) ?? "tool",
|
|
22280
|
+
input: payload["input"],
|
|
22281
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22282
|
+
}
|
|
22283
|
+
];
|
|
22284
|
+
case "custom_tool_call_output": {
|
|
22285
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
22286
|
+
return [
|
|
22287
|
+
{
|
|
22288
|
+
...base,
|
|
22289
|
+
kind: "tool_result",
|
|
22290
|
+
output,
|
|
22291
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
22292
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22293
|
+
}
|
|
22294
|
+
];
|
|
22295
|
+
}
|
|
22296
|
+
// The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
|
|
22297
|
+
// `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
|
|
22298
|
+
// session where the agent searched and opened pages rendered as reasoning
|
|
22299
|
+
// followed by an answer with no source anywhere in it. The queries and the URLs
|
|
22300
|
+
// are the whole point of the record: they are the evidence of what the agent
|
|
22301
|
+
// read from outside the machine.
|
|
22302
|
+
//
|
|
22303
|
+
// `action` is passed through whole. It is already a vendor-neutral argument bag
|
|
22304
|
+
// ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
|
|
22305
|
+
// worst, 121 on average across all 49.
|
|
22306
|
+
//
|
|
22307
|
+
// Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
|
|
22308
|
+
// `id`, `call_id` is the pairing key everywhere else in this stream, and no
|
|
22309
|
+
// response_item output line ever pairs with a search — so setting one would
|
|
22310
|
+
// register a span in the emitter's pairing table that nothing ever closes, and
|
|
22311
|
+
// `finish()` would end it at pass end and stamp a multi-minute duration on a
|
|
22312
|
+
// search that took seconds. Left unpaired, the emitter closes it at the call
|
|
22313
|
+
// timestamp, which is the truthful reading.
|
|
22314
|
+
case "web_search_call":
|
|
22315
|
+
return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
|
|
22072
22316
|
case "reasoning": {
|
|
22073
22317
|
const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
|
|
22074
|
-
return
|
|
22318
|
+
return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
|
|
22075
22319
|
}
|
|
22076
22320
|
case "message": {
|
|
22077
22321
|
const text = textOf(payload["content"]);
|
package/dist/bin/forwarder.mjs
CHANGED
|
@@ -18308,6 +18308,7 @@ function attachmentLabel(record) {
|
|
|
18308
18308
|
return `[${record.mediaType ?? "attachment"}${size}]`;
|
|
18309
18309
|
}
|
|
18310
18310
|
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18311
|
+
var USAGE_WIRE_NAMES = { thinking_tokens: "reasoning" };
|
|
18311
18312
|
var SessionEmitter = class {
|
|
18312
18313
|
client;
|
|
18313
18314
|
options;
|
|
@@ -18331,6 +18332,8 @@ var SessionEmitter = class {
|
|
|
18331
18332
|
/** Attachments seen before the trace could be opened knowing the prompt. */
|
|
18332
18333
|
deferred = [];
|
|
18333
18334
|
pendingUsage;
|
|
18335
|
+
/** Set when the model call this generation stands for failed. Cleared with it. */
|
|
18336
|
+
generationError;
|
|
18334
18337
|
outputText = [];
|
|
18335
18338
|
/** The last thing the assistant said in this pass — the trace's result. */
|
|
18336
18339
|
lastAssistantText;
|
|
@@ -18386,9 +18389,10 @@ var SessionEmitter = class {
|
|
|
18386
18389
|
this.count++;
|
|
18387
18390
|
if (record.kind === "attachment")
|
|
18388
18391
|
this.pendingAttachments.push(attachmentLabel(record));
|
|
18389
|
-
if (record.kind === "user_message")
|
|
18392
|
+
if (record.kind === "user_message" && !record.isError && !record.synthetic) {
|
|
18390
18393
|
this.absorbUserTurn(record);
|
|
18391
|
-
|
|
18394
|
+
}
|
|
18395
|
+
if (!this.trace && (record.kind === "attachment" || record.synthetic === true)) {
|
|
18392
18396
|
this.deferred.push(record);
|
|
18393
18397
|
return;
|
|
18394
18398
|
}
|
|
@@ -18426,6 +18430,15 @@ var SessionEmitter = class {
|
|
|
18426
18430
|
this.owners.set(record.uuid, owner);
|
|
18427
18431
|
switch (record.kind) {
|
|
18428
18432
|
case "user_message":
|
|
18433
|
+
if (record.synthetic) {
|
|
18434
|
+
this.seeded(["injected", record.uuid], () => owner.span("context_injection", {
|
|
18435
|
+
startTime: record.ts,
|
|
18436
|
+
...record.text !== void 0 ? { output: summarize(record.text) } : {}
|
|
18437
|
+
})).end({ endTime: record.ts });
|
|
18438
|
+
break;
|
|
18439
|
+
}
|
|
18440
|
+
if (record.isError)
|
|
18441
|
+
this.generationError = record.errorKind ?? "error";
|
|
18429
18442
|
this.endGeneration();
|
|
18430
18443
|
break;
|
|
18431
18444
|
case "attachment": {
|
|
@@ -18442,7 +18455,9 @@ var SessionEmitter = class {
|
|
|
18442
18455
|
}
|
|
18443
18456
|
case "assistant_message":
|
|
18444
18457
|
this.openGeneration(record, owner);
|
|
18445
|
-
if (record.
|
|
18458
|
+
if (record.isError)
|
|
18459
|
+
this.generationError = record.errorKind ?? "error";
|
|
18460
|
+
else if (record.text)
|
|
18446
18461
|
this.outputText.push(record.text);
|
|
18447
18462
|
break;
|
|
18448
18463
|
case "thinking": {
|
|
@@ -18471,15 +18486,44 @@ var SessionEmitter = class {
|
|
|
18471
18486
|
case "tool_result": {
|
|
18472
18487
|
const span = record.toolCallId ? this.toolSpans.get(record.toolCallId) : void 0;
|
|
18473
18488
|
if (span) {
|
|
18474
|
-
span.end({
|
|
18489
|
+
span.end({
|
|
18490
|
+
output: record.output,
|
|
18491
|
+
endTime: record.ts,
|
|
18492
|
+
...record.isError ? { level: "ERROR" } : {},
|
|
18493
|
+
...record.errorKind !== void 0 ? { statusMessage: record.errorKind } : {}
|
|
18494
|
+
});
|
|
18475
18495
|
this.toolSpans.delete(record.toolCallId);
|
|
18476
18496
|
}
|
|
18477
18497
|
this.pendingToolResults.push(summarize(record.output));
|
|
18478
18498
|
break;
|
|
18479
18499
|
}
|
|
18500
|
+
case "compaction":
|
|
18501
|
+
this.endGeneration();
|
|
18502
|
+
this.seeded(["compaction", record.uuid], () => owner.span("compaction", {
|
|
18503
|
+
startTime: record.ts,
|
|
18504
|
+
...record.text ? { output: record.text } : {}
|
|
18505
|
+
})).end({ endTime: record.ts });
|
|
18506
|
+
break;
|
|
18507
|
+
case "permission_grant": {
|
|
18508
|
+
const snapshot = record.input && typeof record.input === "object" ? record.input : void 0;
|
|
18509
|
+
const steps = Array.isArray(snapshot?.steps) ? snapshot.steps : [];
|
|
18510
|
+
this.seeded(["permission", record.uuid], () => owner.span("permission_grant", {
|
|
18511
|
+
startTime: record.ts,
|
|
18512
|
+
// `input`, not `output`: the list is what this record IS, not a result it
|
|
18513
|
+
// produced.
|
|
18514
|
+
input: steps,
|
|
18515
|
+
metadata: {
|
|
18516
|
+
// The true count, which is larger than `steps.length` when the mapper
|
|
18517
|
+
// truncated the snapshot to its byte budget.
|
|
18518
|
+
count: String(snapshot?.count ?? steps.length),
|
|
18519
|
+
full: String(snapshot?.full === true)
|
|
18520
|
+
}
|
|
18521
|
+
})).end({ endTime: record.ts });
|
|
18522
|
+
break;
|
|
18523
|
+
}
|
|
18480
18524
|
case "usage":
|
|
18481
18525
|
if (record.usage) {
|
|
18482
|
-
this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number"));
|
|
18526
|
+
this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number").map(([key, value]) => [USAGE_WIRE_NAMES[key] ?? key, value]));
|
|
18483
18527
|
}
|
|
18484
18528
|
if (record.model)
|
|
18485
18529
|
this.model = record.model;
|
|
@@ -18636,7 +18680,18 @@ var SessionEmitter = class {
|
|
|
18636
18680
|
this.model = record.model;
|
|
18637
18681
|
this.generation = this.seeded(["generation", id], () => owner.generation("assistant", {
|
|
18638
18682
|
startTime: record.ts,
|
|
18639
|
-
...this.model !== void 0 ? { model: this.model } : {}
|
|
18683
|
+
...this.model !== void 0 ? { model: this.model } : {},
|
|
18684
|
+
// What drove this model call — an installed skill, an MCP server's output, a
|
|
18685
|
+
// plugin. A flat scalar like every other metadata value emitted here, because
|
|
18686
|
+
// trace-hub lifts each key onto the span unchanged and a nested value would
|
|
18687
|
+
// arrive as a blob nothing can group or filter on. One lowercase word, so unlike
|
|
18688
|
+
// `agent_id` there is no camelCase to convert.
|
|
18689
|
+
//
|
|
18690
|
+
// Set once, at open, and never through update(): this method early-returns when
|
|
18691
|
+
// the generation is already open, so the value has to be constant across a
|
|
18692
|
+
// message group. Measured over the claude-code corpus, 0 of 1,381 attributed
|
|
18693
|
+
// groups carry two different tokens.
|
|
18694
|
+
...record.attribution !== void 0 ? { metadata: { attribution: record.attribution } } : {}
|
|
18640
18695
|
}));
|
|
18641
18696
|
const input = this.buildInputMessages();
|
|
18642
18697
|
if (input.inputMessages)
|
|
@@ -18676,12 +18731,14 @@ var SessionEmitter = class {
|
|
|
18676
18731
|
this.generation.end({
|
|
18677
18732
|
...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
|
|
18678
18733
|
...this.model !== void 0 ? { model: this.model } : {},
|
|
18679
|
-
...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {}
|
|
18734
|
+
...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {},
|
|
18735
|
+
...this.generationError !== void 0 ? { level: "ERROR", statusMessage: this.generationError } : {}
|
|
18680
18736
|
});
|
|
18681
18737
|
this.generation = void 0;
|
|
18682
18738
|
this.generationId = void 0;
|
|
18683
18739
|
this.outputText = [];
|
|
18684
18740
|
this.pendingUsage = void 0;
|
|
18741
|
+
this.generationError = void 0;
|
|
18685
18742
|
}
|
|
18686
18743
|
};
|
|
18687
18744
|
|
|
@@ -21926,6 +21983,62 @@ function textOf(content) {
|
|
|
21926
21983
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
21927
21984
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
21928
21985
|
}
|
|
21986
|
+
var CODEX_APPROVAL_MODES = {
|
|
21987
|
+
never: "auto",
|
|
21988
|
+
"on-request": "ask"
|
|
21989
|
+
};
|
|
21990
|
+
var CODEX_SANDBOX_MODES = {
|
|
21991
|
+
"workspace-write": "workspace-write",
|
|
21992
|
+
"read-only": "read-only",
|
|
21993
|
+
"danger-full-access": "danger-full-access"
|
|
21994
|
+
};
|
|
21995
|
+
var PERMISSION_SNAPSHOT_LIMIT = 32e3;
|
|
21996
|
+
function budgeted(steps) {
|
|
21997
|
+
const kept = [];
|
|
21998
|
+
let bytes = 0;
|
|
21999
|
+
for (const step of steps) {
|
|
22000
|
+
bytes += JSON.stringify(step ?? null).length + 1;
|
|
22001
|
+
if (bytes > PERMISSION_SNAPSHOT_LIMIT)
|
|
22002
|
+
break;
|
|
22003
|
+
kept.push(step);
|
|
22004
|
+
}
|
|
22005
|
+
return kept;
|
|
22006
|
+
}
|
|
22007
|
+
function execEnvelope(text) {
|
|
22008
|
+
let parsed;
|
|
22009
|
+
try {
|
|
22010
|
+
parsed = JSON.parse(text);
|
|
22011
|
+
} catch {
|
|
22012
|
+
return void 0;
|
|
22013
|
+
}
|
|
22014
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
22015
|
+
return void 0;
|
|
22016
|
+
const bag = parsed;
|
|
22017
|
+
if (!Object.prototype.hasOwnProperty.call(bag, "output"))
|
|
22018
|
+
return void 0;
|
|
22019
|
+
const metadata = bag["metadata"];
|
|
22020
|
+
const exitCode = num(bag["exit_code"]) ?? (metadata && typeof metadata === "object" ? num(metadata["exit_code"]) : void 0);
|
|
22021
|
+
if (exitCode === void 0)
|
|
22022
|
+
return void 0;
|
|
22023
|
+
return { output: bag["output"], exitCode };
|
|
22024
|
+
}
|
|
22025
|
+
function execResult(value) {
|
|
22026
|
+
if (typeof value === "string")
|
|
22027
|
+
return execEnvelope(value) ?? { output: value };
|
|
22028
|
+
if (Array.isArray(value)) {
|
|
22029
|
+
for (const block of value) {
|
|
22030
|
+
if (!block || typeof block !== "object")
|
|
22031
|
+
continue;
|
|
22032
|
+
const text = block["text"];
|
|
22033
|
+
if (typeof text !== "string")
|
|
22034
|
+
continue;
|
|
22035
|
+
const envelope = execEnvelope(text);
|
|
22036
|
+
if (envelope)
|
|
22037
|
+
return envelope;
|
|
22038
|
+
}
|
|
22039
|
+
}
|
|
22040
|
+
return { output: textOf(value) ?? value };
|
|
22041
|
+
}
|
|
21929
22042
|
function jwtClaims(token) {
|
|
21930
22043
|
const parts = token.split(".");
|
|
21931
22044
|
if (parts.length !== 3 || !parts[1])
|
|
@@ -21952,6 +22065,29 @@ function emailFromAuth(raw) {
|
|
|
21952
22065
|
const email = jwtClaims(token)?.["email"];
|
|
21953
22066
|
return typeof email === "string" && email !== "" ? email : void 0;
|
|
21954
22067
|
}
|
|
22068
|
+
function repositoryIdentity(url) {
|
|
22069
|
+
if (url === void 0)
|
|
22070
|
+
return void 0;
|
|
22071
|
+
const trimmed = url.trim();
|
|
22072
|
+
if (trimmed === "")
|
|
22073
|
+
return void 0;
|
|
22074
|
+
const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
|
|
22075
|
+
if (scp)
|
|
22076
|
+
return `${scp[1]}/${stripSuffix(scp[2])}`;
|
|
22077
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
|
|
22078
|
+
try {
|
|
22079
|
+
const parsed = new URL(withScheme);
|
|
22080
|
+
if (!parsed.hostname)
|
|
22081
|
+
return trimmed;
|
|
22082
|
+
const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
|
|
22083
|
+
return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
|
|
22084
|
+
} catch {
|
|
22085
|
+
return trimmed;
|
|
22086
|
+
}
|
|
22087
|
+
}
|
|
22088
|
+
function stripSuffix(path) {
|
|
22089
|
+
return path.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
22090
|
+
}
|
|
21955
22091
|
var codexTranscript = {
|
|
21956
22092
|
vendor: "codex",
|
|
21957
22093
|
sessionRoots() {
|
|
@@ -22004,12 +22140,20 @@ var codexTranscript = {
|
|
|
22004
22140
|
const uuid = `codex:${offset}`;
|
|
22005
22141
|
const base = { vendor: "codex", ts, uuid };
|
|
22006
22142
|
if (raw.type === "session_meta") {
|
|
22143
|
+
const rawGit = payload["git"];
|
|
22144
|
+
const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
|
|
22145
|
+
const gitBranch = str(git?.["branch"]);
|
|
22146
|
+
const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
|
|
22007
22147
|
return [
|
|
22008
22148
|
{
|
|
22009
22149
|
...base,
|
|
22010
22150
|
kind: "session_meta",
|
|
22011
22151
|
meta: {
|
|
22012
22152
|
...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
|
|
22153
|
+
// Conditional spread rather than a plain assignment: the 7 `{}` blocks and
|
|
22154
|
+
// the 92 absent ones must leave the key absent, not present-and-undefined.
|
|
22155
|
+
...gitBranch !== void 0 ? { gitBranch } : {},
|
|
22156
|
+
...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
|
|
22013
22157
|
...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
|
|
22014
22158
|
...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
|
|
22015
22159
|
// A Codex subagent is a rollout of its own, a peer of its parent in the
|
|
@@ -22025,15 +22169,61 @@ var codexTranscript = {
|
|
|
22025
22169
|
}
|
|
22026
22170
|
if (raw.type === "turn_context") {
|
|
22027
22171
|
const model = str(payload["model"]);
|
|
22028
|
-
|
|
22172
|
+
const approval = str(payload["approval_policy"]);
|
|
22173
|
+
const sandbox = payload["sandbox_policy"];
|
|
22174
|
+
const sandboxMode = (() => {
|
|
22175
|
+
if (!sandbox || typeof sandbox !== "object")
|
|
22176
|
+
return void 0;
|
|
22177
|
+
const bag = sandbox;
|
|
22178
|
+
return str(bag["mode"]) ?? str(bag["type"]);
|
|
22179
|
+
})();
|
|
22180
|
+
const meta = {
|
|
22181
|
+
...model !== void 0 ? { model } : {},
|
|
22182
|
+
...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
|
|
22183
|
+
...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
|
|
22184
|
+
};
|
|
22185
|
+
if (Object.keys(meta).length === 0)
|
|
22029
22186
|
return [];
|
|
22030
|
-
return [{ ...base, kind: "session_meta", meta
|
|
22187
|
+
return [{ ...base, kind: "session_meta", meta }];
|
|
22031
22188
|
}
|
|
22032
22189
|
const kind = str(payload["type"]);
|
|
22033
22190
|
if (raw.type === "event_msg" && kind === "token_count") {
|
|
22034
22191
|
const usage = mapTokenCount(payload["info"]);
|
|
22035
22192
|
return usage ? [{ ...base, kind: "usage", usage }] : [];
|
|
22036
22193
|
}
|
|
22194
|
+
if (raw.type === "compacted") {
|
|
22195
|
+
const note = str(payload["message"]);
|
|
22196
|
+
return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
|
|
22197
|
+
}
|
|
22198
|
+
if (raw.type === "world_state") {
|
|
22199
|
+
const state = payload["state"];
|
|
22200
|
+
const perms = state && typeof state === "object" ? state["permissions"] : void 0;
|
|
22201
|
+
if (!perms || typeof perms !== "object")
|
|
22202
|
+
return [];
|
|
22203
|
+
const list = perms["approved_command_prefixes"];
|
|
22204
|
+
const steps = Array.isArray(list) ? list : [];
|
|
22205
|
+
return [
|
|
22206
|
+
{
|
|
22207
|
+
...base,
|
|
22208
|
+
kind: "permission_grant",
|
|
22209
|
+
input: {
|
|
22210
|
+
steps: budgeted(steps),
|
|
22211
|
+
count: steps.length,
|
|
22212
|
+
...payload["full"] === true ? { full: true } : {}
|
|
22213
|
+
}
|
|
22214
|
+
}
|
|
22215
|
+
];
|
|
22216
|
+
}
|
|
22217
|
+
if (raw.type === "event_msg" && kind === "turn_aborted") {
|
|
22218
|
+
return [
|
|
22219
|
+
{
|
|
22220
|
+
...base,
|
|
22221
|
+
kind: "user_message",
|
|
22222
|
+
isError: true,
|
|
22223
|
+
errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
|
|
22224
|
+
}
|
|
22225
|
+
];
|
|
22226
|
+
}
|
|
22037
22227
|
if (raw.type !== "response_item")
|
|
22038
22228
|
return [];
|
|
22039
22229
|
switch (kind) {
|
|
@@ -22047,18 +22237,72 @@ var codexTranscript = {
|
|
|
22047
22237
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22048
22238
|
}
|
|
22049
22239
|
];
|
|
22050
|
-
case "function_call_output":
|
|
22240
|
+
case "function_call_output": {
|
|
22241
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
22051
22242
|
return [
|
|
22052
22243
|
{
|
|
22053
22244
|
...base,
|
|
22054
22245
|
kind: "tool_result",
|
|
22055
|
-
output
|
|
22246
|
+
output,
|
|
22247
|
+
// Only where an exit status was actually found. On the 993 string outputs
|
|
22248
|
+
// that are not envelopes and the 144 array ones the key stays ABSENT rather
|
|
22249
|
+
// than `false`: the emitter tests `record.isError`, and "this tool reported
|
|
22250
|
+
// success" is a different claim from "nothing here reported an outcome".
|
|
22251
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
22056
22252
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22057
22253
|
}
|
|
22058
22254
|
];
|
|
22255
|
+
}
|
|
22256
|
+
// Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
|
|
22257
|
+
// `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
|
|
22258
|
+
// across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
|
|
22259
|
+
// handling only `function_call` left every shell command and every file write
|
|
22260
|
+
// this endpoint made invisible to both the trace and anything reading it.
|
|
22261
|
+
case "custom_tool_call":
|
|
22262
|
+
return [
|
|
22263
|
+
{
|
|
22264
|
+
...base,
|
|
22265
|
+
kind: "tool_call",
|
|
22266
|
+
toolName: str(payload["name"]) ?? "tool",
|
|
22267
|
+
input: payload["input"],
|
|
22268
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22269
|
+
}
|
|
22270
|
+
];
|
|
22271
|
+
case "custom_tool_call_output": {
|
|
22272
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
22273
|
+
return [
|
|
22274
|
+
{
|
|
22275
|
+
...base,
|
|
22276
|
+
kind: "tool_result",
|
|
22277
|
+
output,
|
|
22278
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
22279
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
22280
|
+
}
|
|
22281
|
+
];
|
|
22282
|
+
}
|
|
22283
|
+
// The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
|
|
22284
|
+
// `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
|
|
22285
|
+
// session where the agent searched and opened pages rendered as reasoning
|
|
22286
|
+
// followed by an answer with no source anywhere in it. The queries and the URLs
|
|
22287
|
+
// are the whole point of the record: they are the evidence of what the agent
|
|
22288
|
+
// read from outside the machine.
|
|
22289
|
+
//
|
|
22290
|
+
// `action` is passed through whole. It is already a vendor-neutral argument bag
|
|
22291
|
+
// ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
|
|
22292
|
+
// worst, 121 on average across all 49.
|
|
22293
|
+
//
|
|
22294
|
+
// Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
|
|
22295
|
+
// `id`, `call_id` is the pairing key everywhere else in this stream, and no
|
|
22296
|
+
// response_item output line ever pairs with a search — so setting one would
|
|
22297
|
+
// register a span in the emitter's pairing table that nothing ever closes, and
|
|
22298
|
+
// `finish()` would end it at pass end and stamp a multi-minute duration on a
|
|
22299
|
+
// search that took seconds. Left unpaired, the emitter closes it at the call
|
|
22300
|
+
// timestamp, which is the truthful reading.
|
|
22301
|
+
case "web_search_call":
|
|
22302
|
+
return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
|
|
22059
22303
|
case "reasoning": {
|
|
22060
22304
|
const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
|
|
22061
|
-
return
|
|
22305
|
+
return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
|
|
22062
22306
|
}
|
|
22063
22307
|
case "message": {
|
|
22064
22308
|
const text = textOf(payload["content"]);
|
package/dist/bin/guard.mjs
CHANGED
|
@@ -1378,6 +1378,14 @@ var codexCodec = {
|
|
|
1378
1378
|
};
|
|
1379
1379
|
},
|
|
1380
1380
|
encodeDecision(decision) {
|
|
1381
|
+
const noOpinion = decision.decision === "allow" && decision.updatedInput === void 0 && (decision.ruleId === null || decision.enforcement === "advisory");
|
|
1382
|
+
if (noOpinion) {
|
|
1383
|
+
const message = [decision.reason, decision.remediation].filter(Boolean).join(" ");
|
|
1384
|
+
return {
|
|
1385
|
+
stdout: message ? JSON.stringify({ systemMessage: message }) : "",
|
|
1386
|
+
exitCode: 0
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1381
1389
|
const hookSpecificOutput = {
|
|
1382
1390
|
hookEventName: "PreToolUse",
|
|
1383
1391
|
permissionDecision: decision.decision
|
package/dist/bin/spool.mjs
CHANGED
|
@@ -1180,6 +1180,14 @@ var codexCodec = {
|
|
|
1180
1180
|
};
|
|
1181
1181
|
},
|
|
1182
1182
|
encodeDecision(decision) {
|
|
1183
|
+
const noOpinion = decision.decision === "allow" && decision.updatedInput === void 0 && (decision.ruleId === null || decision.enforcement === "advisory");
|
|
1184
|
+
if (noOpinion) {
|
|
1185
|
+
const message = [decision.reason, decision.remediation].filter(Boolean).join(" ");
|
|
1186
|
+
return {
|
|
1187
|
+
stdout: message ? JSON.stringify({ systemMessage: message }) : "",
|
|
1188
|
+
exitCode: 0
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1183
1191
|
const hookSpecificOutput = {
|
|
1184
1192
|
hookEventName: "PreToolUse",
|
|
1185
1193
|
permissionDecision: decision.decision
|
package/dist/bin/status.mjs
CHANGED
|
@@ -1265,6 +1265,62 @@ function textOf(content) {
|
|
|
1265
1265
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
1266
1266
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
1267
1267
|
}
|
|
1268
|
+
var CODEX_APPROVAL_MODES = {
|
|
1269
|
+
never: "auto",
|
|
1270
|
+
"on-request": "ask"
|
|
1271
|
+
};
|
|
1272
|
+
var CODEX_SANDBOX_MODES = {
|
|
1273
|
+
"workspace-write": "workspace-write",
|
|
1274
|
+
"read-only": "read-only",
|
|
1275
|
+
"danger-full-access": "danger-full-access"
|
|
1276
|
+
};
|
|
1277
|
+
var PERMISSION_SNAPSHOT_LIMIT = 32e3;
|
|
1278
|
+
function budgeted(steps) {
|
|
1279
|
+
const kept = [];
|
|
1280
|
+
let bytes = 0;
|
|
1281
|
+
for (const step of steps) {
|
|
1282
|
+
bytes += JSON.stringify(step ?? null).length + 1;
|
|
1283
|
+
if (bytes > PERMISSION_SNAPSHOT_LIMIT)
|
|
1284
|
+
break;
|
|
1285
|
+
kept.push(step);
|
|
1286
|
+
}
|
|
1287
|
+
return kept;
|
|
1288
|
+
}
|
|
1289
|
+
function execEnvelope(text) {
|
|
1290
|
+
let parsed;
|
|
1291
|
+
try {
|
|
1292
|
+
parsed = JSON.parse(text);
|
|
1293
|
+
} catch {
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1297
|
+
return void 0;
|
|
1298
|
+
const bag = parsed;
|
|
1299
|
+
if (!Object.prototype.hasOwnProperty.call(bag, "output"))
|
|
1300
|
+
return void 0;
|
|
1301
|
+
const metadata = bag["metadata"];
|
|
1302
|
+
const exitCode = num(bag["exit_code"]) ?? (metadata && typeof metadata === "object" ? num(metadata["exit_code"]) : void 0);
|
|
1303
|
+
if (exitCode === void 0)
|
|
1304
|
+
return void 0;
|
|
1305
|
+
return { output: bag["output"], exitCode };
|
|
1306
|
+
}
|
|
1307
|
+
function execResult(value) {
|
|
1308
|
+
if (typeof value === "string")
|
|
1309
|
+
return execEnvelope(value) ?? { output: value };
|
|
1310
|
+
if (Array.isArray(value)) {
|
|
1311
|
+
for (const block of value) {
|
|
1312
|
+
if (!block || typeof block !== "object")
|
|
1313
|
+
continue;
|
|
1314
|
+
const text = block["text"];
|
|
1315
|
+
if (typeof text !== "string")
|
|
1316
|
+
continue;
|
|
1317
|
+
const envelope = execEnvelope(text);
|
|
1318
|
+
if (envelope)
|
|
1319
|
+
return envelope;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return { output: textOf(value) ?? value };
|
|
1323
|
+
}
|
|
1268
1324
|
function jwtClaims(token) {
|
|
1269
1325
|
const parts = token.split(".");
|
|
1270
1326
|
if (parts.length !== 3 || !parts[1])
|
|
@@ -1291,6 +1347,29 @@ function emailFromAuth(raw) {
|
|
|
1291
1347
|
const email = jwtClaims(token)?.["email"];
|
|
1292
1348
|
return typeof email === "string" && email !== "" ? email : void 0;
|
|
1293
1349
|
}
|
|
1350
|
+
function repositoryIdentity(url) {
|
|
1351
|
+
if (url === void 0)
|
|
1352
|
+
return void 0;
|
|
1353
|
+
const trimmed = url.trim();
|
|
1354
|
+
if (trimmed === "")
|
|
1355
|
+
return void 0;
|
|
1356
|
+
const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
|
|
1357
|
+
if (scp)
|
|
1358
|
+
return `${scp[1]}/${stripSuffix(scp[2])}`;
|
|
1359
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
|
|
1360
|
+
try {
|
|
1361
|
+
const parsed = new URL(withScheme);
|
|
1362
|
+
if (!parsed.hostname)
|
|
1363
|
+
return trimmed;
|
|
1364
|
+
const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
|
|
1365
|
+
return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
|
|
1366
|
+
} catch {
|
|
1367
|
+
return trimmed;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function stripSuffix(path) {
|
|
1371
|
+
return path.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
1372
|
+
}
|
|
1294
1373
|
var codexTranscript = {
|
|
1295
1374
|
vendor: "codex",
|
|
1296
1375
|
sessionRoots() {
|
|
@@ -1343,12 +1422,20 @@ var codexTranscript = {
|
|
|
1343
1422
|
const uuid = `codex:${offset}`;
|
|
1344
1423
|
const base = { vendor: "codex", ts, uuid };
|
|
1345
1424
|
if (raw.type === "session_meta") {
|
|
1425
|
+
const rawGit = payload["git"];
|
|
1426
|
+
const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
|
|
1427
|
+
const gitBranch = str(git?.["branch"]);
|
|
1428
|
+
const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
|
|
1346
1429
|
return [
|
|
1347
1430
|
{
|
|
1348
1431
|
...base,
|
|
1349
1432
|
kind: "session_meta",
|
|
1350
1433
|
meta: {
|
|
1351
1434
|
...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
|
|
1435
|
+
// Conditional spread rather than a plain assignment: the 7 `{}` blocks and
|
|
1436
|
+
// the 92 absent ones must leave the key absent, not present-and-undefined.
|
|
1437
|
+
...gitBranch !== void 0 ? { gitBranch } : {},
|
|
1438
|
+
...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
|
|
1352
1439
|
...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
|
|
1353
1440
|
...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
|
|
1354
1441
|
// A Codex subagent is a rollout of its own, a peer of its parent in the
|
|
@@ -1364,15 +1451,61 @@ var codexTranscript = {
|
|
|
1364
1451
|
}
|
|
1365
1452
|
if (raw.type === "turn_context") {
|
|
1366
1453
|
const model = str(payload["model"]);
|
|
1367
|
-
|
|
1454
|
+
const approval = str(payload["approval_policy"]);
|
|
1455
|
+
const sandbox = payload["sandbox_policy"];
|
|
1456
|
+
const sandboxMode = (() => {
|
|
1457
|
+
if (!sandbox || typeof sandbox !== "object")
|
|
1458
|
+
return void 0;
|
|
1459
|
+
const bag = sandbox;
|
|
1460
|
+
return str(bag["mode"]) ?? str(bag["type"]);
|
|
1461
|
+
})();
|
|
1462
|
+
const meta = {
|
|
1463
|
+
...model !== void 0 ? { model } : {},
|
|
1464
|
+
...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
|
|
1465
|
+
...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
|
|
1466
|
+
};
|
|
1467
|
+
if (Object.keys(meta).length === 0)
|
|
1368
1468
|
return [];
|
|
1369
|
-
return [{ ...base, kind: "session_meta", meta
|
|
1469
|
+
return [{ ...base, kind: "session_meta", meta }];
|
|
1370
1470
|
}
|
|
1371
1471
|
const kind = str(payload["type"]);
|
|
1372
1472
|
if (raw.type === "event_msg" && kind === "token_count") {
|
|
1373
1473
|
const usage = mapTokenCount(payload["info"]);
|
|
1374
1474
|
return usage ? [{ ...base, kind: "usage", usage }] : [];
|
|
1375
1475
|
}
|
|
1476
|
+
if (raw.type === "compacted") {
|
|
1477
|
+
const note = str(payload["message"]);
|
|
1478
|
+
return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
|
|
1479
|
+
}
|
|
1480
|
+
if (raw.type === "world_state") {
|
|
1481
|
+
const state = payload["state"];
|
|
1482
|
+
const perms = state && typeof state === "object" ? state["permissions"] : void 0;
|
|
1483
|
+
if (!perms || typeof perms !== "object")
|
|
1484
|
+
return [];
|
|
1485
|
+
const list = perms["approved_command_prefixes"];
|
|
1486
|
+
const steps = Array.isArray(list) ? list : [];
|
|
1487
|
+
return [
|
|
1488
|
+
{
|
|
1489
|
+
...base,
|
|
1490
|
+
kind: "permission_grant",
|
|
1491
|
+
input: {
|
|
1492
|
+
steps: budgeted(steps),
|
|
1493
|
+
count: steps.length,
|
|
1494
|
+
...payload["full"] === true ? { full: true } : {}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
];
|
|
1498
|
+
}
|
|
1499
|
+
if (raw.type === "event_msg" && kind === "turn_aborted") {
|
|
1500
|
+
return [
|
|
1501
|
+
{
|
|
1502
|
+
...base,
|
|
1503
|
+
kind: "user_message",
|
|
1504
|
+
isError: true,
|
|
1505
|
+
errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
|
|
1506
|
+
}
|
|
1507
|
+
];
|
|
1508
|
+
}
|
|
1376
1509
|
if (raw.type !== "response_item")
|
|
1377
1510
|
return [];
|
|
1378
1511
|
switch (kind) {
|
|
@@ -1386,18 +1519,72 @@ var codexTranscript = {
|
|
|
1386
1519
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1387
1520
|
}
|
|
1388
1521
|
];
|
|
1389
|
-
case "function_call_output":
|
|
1522
|
+
case "function_call_output": {
|
|
1523
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
1390
1524
|
return [
|
|
1391
1525
|
{
|
|
1392
1526
|
...base,
|
|
1393
1527
|
kind: "tool_result",
|
|
1394
|
-
output
|
|
1528
|
+
output,
|
|
1529
|
+
// Only where an exit status was actually found. On the 993 string outputs
|
|
1530
|
+
// that are not envelopes and the 144 array ones the key stays ABSENT rather
|
|
1531
|
+
// than `false`: the emitter tests `record.isError`, and "this tool reported
|
|
1532
|
+
// success" is a different claim from "nothing here reported an outcome".
|
|
1533
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
1395
1534
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1396
1535
|
}
|
|
1397
1536
|
];
|
|
1537
|
+
}
|
|
1538
|
+
// Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
|
|
1539
|
+
// `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
|
|
1540
|
+
// across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
|
|
1541
|
+
// handling only `function_call` left every shell command and every file write
|
|
1542
|
+
// this endpoint made invisible to both the trace and anything reading it.
|
|
1543
|
+
case "custom_tool_call":
|
|
1544
|
+
return [
|
|
1545
|
+
{
|
|
1546
|
+
...base,
|
|
1547
|
+
kind: "tool_call",
|
|
1548
|
+
toolName: str(payload["name"]) ?? "tool",
|
|
1549
|
+
input: payload["input"],
|
|
1550
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1551
|
+
}
|
|
1552
|
+
];
|
|
1553
|
+
case "custom_tool_call_output": {
|
|
1554
|
+
const { output, exitCode } = execResult(payload["output"]);
|
|
1555
|
+
return [
|
|
1556
|
+
{
|
|
1557
|
+
...base,
|
|
1558
|
+
kind: "tool_result",
|
|
1559
|
+
output,
|
|
1560
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
1561
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1562
|
+
}
|
|
1563
|
+
];
|
|
1564
|
+
}
|
|
1565
|
+
// The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
|
|
1566
|
+
// `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
|
|
1567
|
+
// session where the agent searched and opened pages rendered as reasoning
|
|
1568
|
+
// followed by an answer with no source anywhere in it. The queries and the URLs
|
|
1569
|
+
// are the whole point of the record: they are the evidence of what the agent
|
|
1570
|
+
// read from outside the machine.
|
|
1571
|
+
//
|
|
1572
|
+
// `action` is passed through whole. It is already a vendor-neutral argument bag
|
|
1573
|
+
// ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
|
|
1574
|
+
// worst, 121 on average across all 49.
|
|
1575
|
+
//
|
|
1576
|
+
// Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
|
|
1577
|
+
// `id`, `call_id` is the pairing key everywhere else in this stream, and no
|
|
1578
|
+
// response_item output line ever pairs with a search — so setting one would
|
|
1579
|
+
// register a span in the emitter's pairing table that nothing ever closes, and
|
|
1580
|
+
// `finish()` would end it at pass end and stamp a multi-minute duration on a
|
|
1581
|
+
// search that took seconds. Left unpaired, the emitter closes it at the call
|
|
1582
|
+
// timestamp, which is the truthful reading.
|
|
1583
|
+
case "web_search_call":
|
|
1584
|
+
return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
|
|
1398
1585
|
case "reasoning": {
|
|
1399
1586
|
const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
|
|
1400
|
-
return
|
|
1587
|
+
return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
|
|
1401
1588
|
}
|
|
1402
1589
|
case "message": {
|
|
1403
1590
|
const text = textOf(payload["content"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darkhunt-security/endpoint-codex",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
|
|
6
6
|
"bin": {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
".codex-plugin"
|
|
14
14
|
],
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@darkhunt-security/endpoint-contracts": "0.9.
|
|
17
|
-
"@darkhunt-security/endpoint-core": "0.9.
|
|
16
|
+
"@darkhunt-security/endpoint-contracts": "0.9.3",
|
|
17
|
+
"@darkhunt-security/endpoint-core": "0.9.3"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public",
|