@darkhunt-security/endpoint-codex 0.9.8 → 0.9.10
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 +145 -21
- package/dist/bin/forwarder.mjs +145 -21
- package/dist/bin/status.mjs +56 -10
- package/package.json +3 -3
package/dist/bin/backfill.mjs
CHANGED
|
@@ -18303,6 +18303,10 @@ function summarize(output) {
|
|
|
18303
18303
|
return text;
|
|
18304
18304
|
return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
|
|
18305
18305
|
}
|
|
18306
|
+
function attachmentLabel(record) {
|
|
18307
|
+
const size = record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : "";
|
|
18308
|
+
return `[${record.mediaType ?? "attachment"}${size}]`;
|
|
18309
|
+
}
|
|
18306
18310
|
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18307
18311
|
var SessionEmitter = class {
|
|
18308
18312
|
client;
|
|
@@ -18324,8 +18328,12 @@ var SessionEmitter = class {
|
|
|
18324
18328
|
/** True while consecutive user records still belong to the same turn. */
|
|
18325
18329
|
userTurnOpen = false;
|
|
18326
18330
|
pendingAttachments = [];
|
|
18331
|
+
/** Attachments seen before the trace could be opened knowing the prompt. */
|
|
18332
|
+
deferred = [];
|
|
18327
18333
|
pendingUsage;
|
|
18328
18334
|
outputText = [];
|
|
18335
|
+
/** The last thing the assistant said in this pass — the trace's result. */
|
|
18336
|
+
lastAssistantText;
|
|
18329
18337
|
count = 0;
|
|
18330
18338
|
constructor(client, options) {
|
|
18331
18339
|
this.client = client;
|
|
@@ -18376,6 +18384,28 @@ var SessionEmitter = class {
|
|
|
18376
18384
|
return;
|
|
18377
18385
|
}
|
|
18378
18386
|
this.count++;
|
|
18387
|
+
if (record.kind === "attachment")
|
|
18388
|
+
this.pendingAttachments.push(attachmentLabel(record));
|
|
18389
|
+
if (record.kind === "user_message")
|
|
18390
|
+
this.absorbUserTurn(record);
|
|
18391
|
+
if (!this.trace && record.kind === "attachment") {
|
|
18392
|
+
this.deferred.push(record);
|
|
18393
|
+
return;
|
|
18394
|
+
}
|
|
18395
|
+
this.flushDeferred();
|
|
18396
|
+
this.place(record);
|
|
18397
|
+
}
|
|
18398
|
+
/** Open the trace on the turn held back, then replay it. No-op once the trace exists. */
|
|
18399
|
+
flushDeferred() {
|
|
18400
|
+
if (this.deferred.length === 0)
|
|
18401
|
+
return;
|
|
18402
|
+
const held = this.deferred.splice(0);
|
|
18403
|
+
this.ensureTrace(held[0].ts);
|
|
18404
|
+
for (const record of held)
|
|
18405
|
+
this.place(record);
|
|
18406
|
+
}
|
|
18407
|
+
/** Route one record onto its owning trace and emit whatever it is worth. */
|
|
18408
|
+
place(record) {
|
|
18379
18409
|
const root = this.ensureTrace(record.ts);
|
|
18380
18410
|
const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
|
|
18381
18411
|
let owner = parentTrace ?? root;
|
|
@@ -18385,28 +18415,21 @@ var SessionEmitter = class {
|
|
|
18385
18415
|
sessionId: this.options.sessionId,
|
|
18386
18416
|
handoffFrom: [root.handoffToken()],
|
|
18387
18417
|
startTime: record.ts,
|
|
18418
|
+
// A subagent's first record is the task it was given, and it has already been
|
|
18419
|
+
// absorbed by the time this fires — so the child trace opens with its brief
|
|
18420
|
+
// rather than empty.
|
|
18421
|
+
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18388
18422
|
...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
|
|
18389
18423
|
}));
|
|
18390
18424
|
this.sidechains.push(owner);
|
|
18391
18425
|
}
|
|
18392
18426
|
this.owners.set(record.uuid, owner);
|
|
18393
18427
|
switch (record.kind) {
|
|
18394
|
-
case "user_message":
|
|
18428
|
+
case "user_message":
|
|
18395
18429
|
this.endGeneration();
|
|
18396
|
-
const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
|
|
18397
|
-
this.pendingAttachments = [];
|
|
18398
|
-
if (this.userTurnOpen) {
|
|
18399
|
-
this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
|
|
18400
|
-
} else {
|
|
18401
|
-
this.lastUserText = part;
|
|
18402
|
-
this.userTurnOpen = true;
|
|
18403
|
-
this.pendingToolResults = [];
|
|
18404
|
-
}
|
|
18405
18430
|
break;
|
|
18406
|
-
}
|
|
18407
18431
|
case "attachment": {
|
|
18408
|
-
const label =
|
|
18409
|
-
this.pendingAttachments.push(label);
|
|
18432
|
+
const label = attachmentLabel(record);
|
|
18410
18433
|
this.seeded(["attachment", record.uuid], () => owner.span("attachment", {
|
|
18411
18434
|
startTime: record.ts,
|
|
18412
18435
|
metadata: {
|
|
@@ -18463,8 +18486,26 @@ var SessionEmitter = class {
|
|
|
18463
18486
|
break;
|
|
18464
18487
|
}
|
|
18465
18488
|
}
|
|
18489
|
+
/**
|
|
18490
|
+
* Fold a user record into the turn in flight.
|
|
18491
|
+
*
|
|
18492
|
+
* Split out of the `user_message` case so it can run before the trace is opened —
|
|
18493
|
+
* see the call site in {@link ingest}.
|
|
18494
|
+
*/
|
|
18495
|
+
absorbUserTurn(record) {
|
|
18496
|
+
const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
|
|
18497
|
+
this.pendingAttachments = [];
|
|
18498
|
+
if (this.userTurnOpen) {
|
|
18499
|
+
this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
|
|
18500
|
+
} else {
|
|
18501
|
+
this.lastUserText = part;
|
|
18502
|
+
this.userTurnOpen = true;
|
|
18503
|
+
this.pendingToolResults = [];
|
|
18504
|
+
}
|
|
18505
|
+
}
|
|
18466
18506
|
/** Ends everything still open and flushes. Safe to call twice. */
|
|
18467
18507
|
async finish() {
|
|
18508
|
+
this.flushDeferred();
|
|
18468
18509
|
this.endGeneration();
|
|
18469
18510
|
for (const span of this.toolSpans.values())
|
|
18470
18511
|
span.end();
|
|
@@ -18475,7 +18516,10 @@ var SessionEmitter = class {
|
|
|
18475
18516
|
if (this.trace) {
|
|
18476
18517
|
this.trace.update({
|
|
18477
18518
|
metadata: this.meta,
|
|
18478
|
-
...this.title !== void 0 ? { name: this.title } : {}
|
|
18519
|
+
...this.title !== void 0 ? { name: this.title } : {},
|
|
18520
|
+
// The pass's result. Paired with the `input` set when the trace opened, this is
|
|
18521
|
+
// what stops the consumer inferring the trace's I/O from a descendant.
|
|
18522
|
+
...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
|
|
18479
18523
|
});
|
|
18480
18524
|
this.trace.end();
|
|
18481
18525
|
this.trace = void 0;
|
|
@@ -18513,6 +18557,9 @@ var SessionEmitter = class {
|
|
|
18513
18557
|
return this.options.parentAgentId ?? this.meta["parentSessionId"];
|
|
18514
18558
|
}
|
|
18515
18559
|
defaultTraceName() {
|
|
18560
|
+
const label = this.options.workflow?.label;
|
|
18561
|
+
if (label)
|
|
18562
|
+
return label;
|
|
18516
18563
|
const agentType = this.agentType();
|
|
18517
18564
|
if (agentType)
|
|
18518
18565
|
return `subagent: ${agentType}`;
|
|
@@ -18521,6 +18568,27 @@ var SessionEmitter = class {
|
|
|
18521
18568
|
}
|
|
18522
18569
|
return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
|
|
18523
18570
|
}
|
|
18571
|
+
/**
|
|
18572
|
+
* The run this agent belongs to, flattened onto the trace.
|
|
18573
|
+
*
|
|
18574
|
+
* Flat scalars rather than a nested object: metadata is lifted onto the span one
|
|
18575
|
+
* attribute at a time downstream, so a nested value would arrive as a JSON blob that
|
|
18576
|
+
* nothing can group or filter on. `workflow_id` is the one that matters — it is what
|
|
18577
|
+
* lets a consumer collect the agents of one run out of a session that ran several.
|
|
18578
|
+
*/
|
|
18579
|
+
workflowMetadata() {
|
|
18580
|
+
const workflow = this.options.workflow;
|
|
18581
|
+
if (!workflow)
|
|
18582
|
+
return {};
|
|
18583
|
+
return {
|
|
18584
|
+
workflow_id: workflow.id,
|
|
18585
|
+
...workflow.name !== void 0 ? { workflow_name: workflow.name } : {},
|
|
18586
|
+
...workflow.label !== void 0 ? { workflow_label: workflow.label } : {},
|
|
18587
|
+
...workflow.phase !== void 0 ? { workflow_phase: workflow.phase } : {},
|
|
18588
|
+
...workflow.phaseIndex !== void 0 ? { workflow_phase_index: workflow.phaseIndex } : {},
|
|
18589
|
+
...workflow.attempt !== void 0 ? { workflow_attempt: workflow.attempt } : {}
|
|
18590
|
+
};
|
|
18591
|
+
}
|
|
18524
18592
|
/** The session traces group under. Set from the path, or from `session_meta`. */
|
|
18525
18593
|
grouping;
|
|
18526
18594
|
ensureTrace(ts) {
|
|
@@ -18531,6 +18599,10 @@ var SessionEmitter = class {
|
|
|
18531
18599
|
name: this.title ?? this.defaultTraceName(),
|
|
18532
18600
|
sessionId: this.grouping ?? this.options.sessionId,
|
|
18533
18601
|
startTime: ts,
|
|
18602
|
+
// What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
|
|
18603
|
+
// `update()` carries output alone — which is why `ingest` absorbs the user turn
|
|
18604
|
+
// before it calls this.
|
|
18605
|
+
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18534
18606
|
tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
|
|
18535
18607
|
metadata: {
|
|
18536
18608
|
...this.meta,
|
|
@@ -18541,7 +18613,10 @@ var SessionEmitter = class {
|
|
|
18541
18613
|
...this.agentId() !== void 0 ? { agent_id: this.agentId() } : {},
|
|
18542
18614
|
...this.agentType() !== void 0 ? { agent_type: this.agentType() } : {},
|
|
18543
18615
|
...this.parentAgentId() !== void 0 ? { parent_agent_id: this.parentAgentId() } : {},
|
|
18544
|
-
...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {}
|
|
18616
|
+
...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {},
|
|
18617
|
+
// snake_case for the same reason as `agent_id` above: trace-hub lifts each
|
|
18618
|
+
// key through unchanged, and these are read alongside it.
|
|
18619
|
+
...this.workflowMetadata()
|
|
18545
18620
|
},
|
|
18546
18621
|
...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
|
|
18547
18622
|
...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
|
|
@@ -18596,6 +18671,8 @@ var SessionEmitter = class {
|
|
|
18596
18671
|
if (!this.generation)
|
|
18597
18672
|
return;
|
|
18598
18673
|
const output = this.outputText.join("\n");
|
|
18674
|
+
if (output)
|
|
18675
|
+
this.lastAssistantText = output;
|
|
18599
18676
|
this.generation.end({
|
|
18600
18677
|
...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
|
|
18601
18678
|
...this.model !== void 0 ? { model: this.model } : {},
|
|
@@ -21696,6 +21773,7 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21696
21773
|
...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
|
|
21697
21774
|
...sub?.parentAgentId !== void 0 ? { parentAgentId: sub.parentAgentId } : {},
|
|
21698
21775
|
...sub?.spawnDepth !== void 0 ? { spawnDepth: sub.spawnDepth } : {},
|
|
21776
|
+
...sub?.workflow !== void 0 ? { workflow: sub.workflow } : {},
|
|
21699
21777
|
...ids !== void 0 ? { ids } : {},
|
|
21700
21778
|
...userId !== void 0 ? { userId } : {},
|
|
21701
21779
|
...carried !== void 0 ? { resumeTurn: carried } : {}
|
|
@@ -21824,6 +21902,7 @@ import { join as join9 } from "node:path";
|
|
|
21824
21902
|
var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
|
|
21825
21903
|
|
|
21826
21904
|
// adapters/codex/dist/transcript.js
|
|
21905
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
21827
21906
|
import { basename, join as join10 } from "node:path";
|
|
21828
21907
|
import { homedir as homedir4 } from "node:os";
|
|
21829
21908
|
function str(value) {
|
|
@@ -21860,17 +21939,62 @@ function textOf(content) {
|
|
|
21860
21939
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
21861
21940
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
21862
21941
|
}
|
|
21942
|
+
function jwtClaims(token) {
|
|
21943
|
+
const parts = token.split(".");
|
|
21944
|
+
if (parts.length !== 3 || !parts[1])
|
|
21945
|
+
return void 0;
|
|
21946
|
+
try {
|
|
21947
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
21948
|
+
const claims = JSON.parse(json);
|
|
21949
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
21950
|
+
} catch {
|
|
21951
|
+
return void 0;
|
|
21952
|
+
}
|
|
21953
|
+
}
|
|
21954
|
+
function emailFromAuth(raw) {
|
|
21955
|
+
let parsed;
|
|
21956
|
+
try {
|
|
21957
|
+
parsed = JSON.parse(raw);
|
|
21958
|
+
} catch {
|
|
21959
|
+
return void 0;
|
|
21960
|
+
}
|
|
21961
|
+
const tokens = parsed?.tokens;
|
|
21962
|
+
const token = tokens?.id_token;
|
|
21963
|
+
if (typeof token !== "string" || token === "")
|
|
21964
|
+
return void 0;
|
|
21965
|
+
const email = jwtClaims(token)?.["email"];
|
|
21966
|
+
return typeof email === "string" && email !== "" ? email : void 0;
|
|
21967
|
+
}
|
|
21863
21968
|
var codexTranscript = {
|
|
21864
21969
|
vendor: "codex",
|
|
21865
21970
|
sessionRoots() {
|
|
21866
21971
|
return [join10(homedir4(), ".codex", "sessions")];
|
|
21867
21972
|
},
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21973
|
+
/**
|
|
21974
|
+
* `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
|
|
21975
|
+
*
|
|
21976
|
+
* Codex records no identity in the rollout itself and none in plain text anywhere
|
|
21977
|
+
* else: `tokens.account_id` is a UUID, not a person. The address is a claim inside the
|
|
21978
|
+
* OAuth id_token, so reading it means decoding that token — hence {@link jwtClaims}.
|
|
21979
|
+
*
|
|
21980
|
+
* The token's own `exp` is deliberately not enforced. It is an hour long and the file
|
|
21981
|
+
* is only rewritten on sign-in and on refresh, so gating on expiry would leave most
|
|
21982
|
+
* sessions unattributed while answering a question nobody asked: an expired id_token
|
|
21983
|
+
* is evidence that a token needs refreshing, never evidence that a different person is
|
|
21984
|
+
* now signed in. Signing in as someone else rewrites this file, which is what makes
|
|
21985
|
+
* the stale-address worry unfounded — the claim always names the last account to
|
|
21986
|
+
* authenticate on this machine, which is the account that wrote these rollouts.
|
|
21987
|
+
*
|
|
21988
|
+
* `undefined` for API-key auth (`OPENAI_API_KEY` with no tokens), for a signed-out
|
|
21989
|
+
* agent, and for anything unreadable; the caller then falls back to configuration.
|
|
21990
|
+
*/
|
|
21991
|
+
resolveUserId() {
|
|
21992
|
+
try {
|
|
21993
|
+
return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
|
|
21994
|
+
} catch {
|
|
21995
|
+
return void 0;
|
|
21996
|
+
}
|
|
21997
|
+
},
|
|
21874
21998
|
sessionIdFor(path) {
|
|
21875
21999
|
const name = basename(path, ".jsonl");
|
|
21876
22000
|
const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(name);
|
package/dist/bin/forwarder.mjs
CHANGED
|
@@ -18303,6 +18303,10 @@ function summarize(output) {
|
|
|
18303
18303
|
return text;
|
|
18304
18304
|
return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
|
|
18305
18305
|
}
|
|
18306
|
+
function attachmentLabel(record) {
|
|
18307
|
+
const size = record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : "";
|
|
18308
|
+
return `[${record.mediaType ?? "attachment"}${size}]`;
|
|
18309
|
+
}
|
|
18306
18310
|
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18307
18311
|
var SessionEmitter = class {
|
|
18308
18312
|
client;
|
|
@@ -18324,8 +18328,12 @@ var SessionEmitter = class {
|
|
|
18324
18328
|
/** True while consecutive user records still belong to the same turn. */
|
|
18325
18329
|
userTurnOpen = false;
|
|
18326
18330
|
pendingAttachments = [];
|
|
18331
|
+
/** Attachments seen before the trace could be opened knowing the prompt. */
|
|
18332
|
+
deferred = [];
|
|
18327
18333
|
pendingUsage;
|
|
18328
18334
|
outputText = [];
|
|
18335
|
+
/** The last thing the assistant said in this pass — the trace's result. */
|
|
18336
|
+
lastAssistantText;
|
|
18329
18337
|
count = 0;
|
|
18330
18338
|
constructor(client, options) {
|
|
18331
18339
|
this.client = client;
|
|
@@ -18376,6 +18384,28 @@ var SessionEmitter = class {
|
|
|
18376
18384
|
return;
|
|
18377
18385
|
}
|
|
18378
18386
|
this.count++;
|
|
18387
|
+
if (record.kind === "attachment")
|
|
18388
|
+
this.pendingAttachments.push(attachmentLabel(record));
|
|
18389
|
+
if (record.kind === "user_message")
|
|
18390
|
+
this.absorbUserTurn(record);
|
|
18391
|
+
if (!this.trace && record.kind === "attachment") {
|
|
18392
|
+
this.deferred.push(record);
|
|
18393
|
+
return;
|
|
18394
|
+
}
|
|
18395
|
+
this.flushDeferred();
|
|
18396
|
+
this.place(record);
|
|
18397
|
+
}
|
|
18398
|
+
/** Open the trace on the turn held back, then replay it. No-op once the trace exists. */
|
|
18399
|
+
flushDeferred() {
|
|
18400
|
+
if (this.deferred.length === 0)
|
|
18401
|
+
return;
|
|
18402
|
+
const held = this.deferred.splice(0);
|
|
18403
|
+
this.ensureTrace(held[0].ts);
|
|
18404
|
+
for (const record of held)
|
|
18405
|
+
this.place(record);
|
|
18406
|
+
}
|
|
18407
|
+
/** Route one record onto its owning trace and emit whatever it is worth. */
|
|
18408
|
+
place(record) {
|
|
18379
18409
|
const root = this.ensureTrace(record.ts);
|
|
18380
18410
|
const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
|
|
18381
18411
|
let owner = parentTrace ?? root;
|
|
@@ -18385,28 +18415,21 @@ var SessionEmitter = class {
|
|
|
18385
18415
|
sessionId: this.options.sessionId,
|
|
18386
18416
|
handoffFrom: [root.handoffToken()],
|
|
18387
18417
|
startTime: record.ts,
|
|
18418
|
+
// A subagent's first record is the task it was given, and it has already been
|
|
18419
|
+
// absorbed by the time this fires — so the child trace opens with its brief
|
|
18420
|
+
// rather than empty.
|
|
18421
|
+
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18388
18422
|
...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
|
|
18389
18423
|
}));
|
|
18390
18424
|
this.sidechains.push(owner);
|
|
18391
18425
|
}
|
|
18392
18426
|
this.owners.set(record.uuid, owner);
|
|
18393
18427
|
switch (record.kind) {
|
|
18394
|
-
case "user_message":
|
|
18428
|
+
case "user_message":
|
|
18395
18429
|
this.endGeneration();
|
|
18396
|
-
const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
|
|
18397
|
-
this.pendingAttachments = [];
|
|
18398
|
-
if (this.userTurnOpen) {
|
|
18399
|
-
this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
|
|
18400
|
-
} else {
|
|
18401
|
-
this.lastUserText = part;
|
|
18402
|
-
this.userTurnOpen = true;
|
|
18403
|
-
this.pendingToolResults = [];
|
|
18404
|
-
}
|
|
18405
18430
|
break;
|
|
18406
|
-
}
|
|
18407
18431
|
case "attachment": {
|
|
18408
|
-
const label =
|
|
18409
|
-
this.pendingAttachments.push(label);
|
|
18432
|
+
const label = attachmentLabel(record);
|
|
18410
18433
|
this.seeded(["attachment", record.uuid], () => owner.span("attachment", {
|
|
18411
18434
|
startTime: record.ts,
|
|
18412
18435
|
metadata: {
|
|
@@ -18463,8 +18486,26 @@ var SessionEmitter = class {
|
|
|
18463
18486
|
break;
|
|
18464
18487
|
}
|
|
18465
18488
|
}
|
|
18489
|
+
/**
|
|
18490
|
+
* Fold a user record into the turn in flight.
|
|
18491
|
+
*
|
|
18492
|
+
* Split out of the `user_message` case so it can run before the trace is opened —
|
|
18493
|
+
* see the call site in {@link ingest}.
|
|
18494
|
+
*/
|
|
18495
|
+
absorbUserTurn(record) {
|
|
18496
|
+
const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
|
|
18497
|
+
this.pendingAttachments = [];
|
|
18498
|
+
if (this.userTurnOpen) {
|
|
18499
|
+
this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
|
|
18500
|
+
} else {
|
|
18501
|
+
this.lastUserText = part;
|
|
18502
|
+
this.userTurnOpen = true;
|
|
18503
|
+
this.pendingToolResults = [];
|
|
18504
|
+
}
|
|
18505
|
+
}
|
|
18466
18506
|
/** Ends everything still open and flushes. Safe to call twice. */
|
|
18467
18507
|
async finish() {
|
|
18508
|
+
this.flushDeferred();
|
|
18468
18509
|
this.endGeneration();
|
|
18469
18510
|
for (const span of this.toolSpans.values())
|
|
18470
18511
|
span.end();
|
|
@@ -18475,7 +18516,10 @@ var SessionEmitter = class {
|
|
|
18475
18516
|
if (this.trace) {
|
|
18476
18517
|
this.trace.update({
|
|
18477
18518
|
metadata: this.meta,
|
|
18478
|
-
...this.title !== void 0 ? { name: this.title } : {}
|
|
18519
|
+
...this.title !== void 0 ? { name: this.title } : {},
|
|
18520
|
+
// The pass's result. Paired with the `input` set when the trace opened, this is
|
|
18521
|
+
// what stops the consumer inferring the trace's I/O from a descendant.
|
|
18522
|
+
...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
|
|
18479
18523
|
});
|
|
18480
18524
|
this.trace.end();
|
|
18481
18525
|
this.trace = void 0;
|
|
@@ -18513,6 +18557,9 @@ var SessionEmitter = class {
|
|
|
18513
18557
|
return this.options.parentAgentId ?? this.meta["parentSessionId"];
|
|
18514
18558
|
}
|
|
18515
18559
|
defaultTraceName() {
|
|
18560
|
+
const label = this.options.workflow?.label;
|
|
18561
|
+
if (label)
|
|
18562
|
+
return label;
|
|
18516
18563
|
const agentType = this.agentType();
|
|
18517
18564
|
if (agentType)
|
|
18518
18565
|
return `subagent: ${agentType}`;
|
|
@@ -18521,6 +18568,27 @@ var SessionEmitter = class {
|
|
|
18521
18568
|
}
|
|
18522
18569
|
return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
|
|
18523
18570
|
}
|
|
18571
|
+
/**
|
|
18572
|
+
* The run this agent belongs to, flattened onto the trace.
|
|
18573
|
+
*
|
|
18574
|
+
* Flat scalars rather than a nested object: metadata is lifted onto the span one
|
|
18575
|
+
* attribute at a time downstream, so a nested value would arrive as a JSON blob that
|
|
18576
|
+
* nothing can group or filter on. `workflow_id` is the one that matters — it is what
|
|
18577
|
+
* lets a consumer collect the agents of one run out of a session that ran several.
|
|
18578
|
+
*/
|
|
18579
|
+
workflowMetadata() {
|
|
18580
|
+
const workflow = this.options.workflow;
|
|
18581
|
+
if (!workflow)
|
|
18582
|
+
return {};
|
|
18583
|
+
return {
|
|
18584
|
+
workflow_id: workflow.id,
|
|
18585
|
+
...workflow.name !== void 0 ? { workflow_name: workflow.name } : {},
|
|
18586
|
+
...workflow.label !== void 0 ? { workflow_label: workflow.label } : {},
|
|
18587
|
+
...workflow.phase !== void 0 ? { workflow_phase: workflow.phase } : {},
|
|
18588
|
+
...workflow.phaseIndex !== void 0 ? { workflow_phase_index: workflow.phaseIndex } : {},
|
|
18589
|
+
...workflow.attempt !== void 0 ? { workflow_attempt: workflow.attempt } : {}
|
|
18590
|
+
};
|
|
18591
|
+
}
|
|
18524
18592
|
/** The session traces group under. Set from the path, or from `session_meta`. */
|
|
18525
18593
|
grouping;
|
|
18526
18594
|
ensureTrace(ts) {
|
|
@@ -18531,6 +18599,10 @@ var SessionEmitter = class {
|
|
|
18531
18599
|
name: this.title ?? this.defaultTraceName(),
|
|
18532
18600
|
sessionId: this.grouping ?? this.options.sessionId,
|
|
18533
18601
|
startTime: ts,
|
|
18602
|
+
// What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
|
|
18603
|
+
// `update()` carries output alone — which is why `ingest` absorbs the user turn
|
|
18604
|
+
// before it calls this.
|
|
18605
|
+
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18534
18606
|
tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
|
|
18535
18607
|
metadata: {
|
|
18536
18608
|
...this.meta,
|
|
@@ -18541,7 +18613,10 @@ var SessionEmitter = class {
|
|
|
18541
18613
|
...this.agentId() !== void 0 ? { agent_id: this.agentId() } : {},
|
|
18542
18614
|
...this.agentType() !== void 0 ? { agent_type: this.agentType() } : {},
|
|
18543
18615
|
...this.parentAgentId() !== void 0 ? { parent_agent_id: this.parentAgentId() } : {},
|
|
18544
|
-
...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {}
|
|
18616
|
+
...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {},
|
|
18617
|
+
// snake_case for the same reason as `agent_id` above: trace-hub lifts each
|
|
18618
|
+
// key through unchanged, and these are read alongside it.
|
|
18619
|
+
...this.workflowMetadata()
|
|
18545
18620
|
},
|
|
18546
18621
|
...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
|
|
18547
18622
|
...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
|
|
@@ -18596,6 +18671,8 @@ var SessionEmitter = class {
|
|
|
18596
18671
|
if (!this.generation)
|
|
18597
18672
|
return;
|
|
18598
18673
|
const output = this.outputText.join("\n");
|
|
18674
|
+
if (output)
|
|
18675
|
+
this.lastAssistantText = output;
|
|
18599
18676
|
this.generation.end({
|
|
18600
18677
|
...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
|
|
18601
18678
|
...this.model !== void 0 ? { model: this.model } : {},
|
|
@@ -21696,6 +21773,7 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21696
21773
|
...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
|
|
21697
21774
|
...sub?.parentAgentId !== void 0 ? { parentAgentId: sub.parentAgentId } : {},
|
|
21698
21775
|
...sub?.spawnDepth !== void 0 ? { spawnDepth: sub.spawnDepth } : {},
|
|
21776
|
+
...sub?.workflow !== void 0 ? { workflow: sub.workflow } : {},
|
|
21699
21777
|
...ids !== void 0 ? { ids } : {},
|
|
21700
21778
|
...userId !== void 0 ? { userId } : {},
|
|
21701
21779
|
...carried !== void 0 ? { resumeTurn: carried } : {}
|
|
@@ -21811,6 +21889,7 @@ import { join as join9 } from "node:path";
|
|
|
21811
21889
|
var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
|
|
21812
21890
|
|
|
21813
21891
|
// adapters/codex/dist/transcript.js
|
|
21892
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
21814
21893
|
import { basename, join as join10 } from "node:path";
|
|
21815
21894
|
import { homedir as homedir4 } from "node:os";
|
|
21816
21895
|
function str(value) {
|
|
@@ -21847,17 +21926,62 @@ function textOf(content) {
|
|
|
21847
21926
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
21848
21927
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
21849
21928
|
}
|
|
21929
|
+
function jwtClaims(token) {
|
|
21930
|
+
const parts = token.split(".");
|
|
21931
|
+
if (parts.length !== 3 || !parts[1])
|
|
21932
|
+
return void 0;
|
|
21933
|
+
try {
|
|
21934
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
21935
|
+
const claims = JSON.parse(json);
|
|
21936
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
21937
|
+
} catch {
|
|
21938
|
+
return void 0;
|
|
21939
|
+
}
|
|
21940
|
+
}
|
|
21941
|
+
function emailFromAuth(raw) {
|
|
21942
|
+
let parsed;
|
|
21943
|
+
try {
|
|
21944
|
+
parsed = JSON.parse(raw);
|
|
21945
|
+
} catch {
|
|
21946
|
+
return void 0;
|
|
21947
|
+
}
|
|
21948
|
+
const tokens = parsed?.tokens;
|
|
21949
|
+
const token = tokens?.id_token;
|
|
21950
|
+
if (typeof token !== "string" || token === "")
|
|
21951
|
+
return void 0;
|
|
21952
|
+
const email = jwtClaims(token)?.["email"];
|
|
21953
|
+
return typeof email === "string" && email !== "" ? email : void 0;
|
|
21954
|
+
}
|
|
21850
21955
|
var codexTranscript = {
|
|
21851
21956
|
vendor: "codex",
|
|
21852
21957
|
sessionRoots() {
|
|
21853
21958
|
return [join10(homedir4(), ".codex", "sessions")];
|
|
21854
21959
|
},
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21960
|
+
/**
|
|
21961
|
+
* `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
|
|
21962
|
+
*
|
|
21963
|
+
* Codex records no identity in the rollout itself and none in plain text anywhere
|
|
21964
|
+
* else: `tokens.account_id` is a UUID, not a person. The address is a claim inside the
|
|
21965
|
+
* OAuth id_token, so reading it means decoding that token — hence {@link jwtClaims}.
|
|
21966
|
+
*
|
|
21967
|
+
* The token's own `exp` is deliberately not enforced. It is an hour long and the file
|
|
21968
|
+
* is only rewritten on sign-in and on refresh, so gating on expiry would leave most
|
|
21969
|
+
* sessions unattributed while answering a question nobody asked: an expired id_token
|
|
21970
|
+
* is evidence that a token needs refreshing, never evidence that a different person is
|
|
21971
|
+
* now signed in. Signing in as someone else rewrites this file, which is what makes
|
|
21972
|
+
* the stale-address worry unfounded — the claim always names the last account to
|
|
21973
|
+
* authenticate on this machine, which is the account that wrote these rollouts.
|
|
21974
|
+
*
|
|
21975
|
+
* `undefined` for API-key auth (`OPENAI_API_KEY` with no tokens), for a signed-out
|
|
21976
|
+
* agent, and for anything unreadable; the caller then falls back to configuration.
|
|
21977
|
+
*/
|
|
21978
|
+
resolveUserId() {
|
|
21979
|
+
try {
|
|
21980
|
+
return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
|
|
21981
|
+
} catch {
|
|
21982
|
+
return void 0;
|
|
21983
|
+
}
|
|
21984
|
+
},
|
|
21861
21985
|
sessionIdFor(path) {
|
|
21862
21986
|
const name = basename(path, ".jsonl");
|
|
21863
21987
|
const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(name);
|
package/dist/bin/status.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire as __dhCreateRequire } from 'node:module';
|
|
|
3
3
|
const require = __dhCreateRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// adapters/codex/bin/status.mjs
|
|
6
|
-
import { readFileSync as
|
|
6
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
7
7
|
import { dirname, join as join12 } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
@@ -1228,6 +1228,7 @@ import { join as join9 } from "node:path";
|
|
|
1228
1228
|
var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
|
|
1229
1229
|
|
|
1230
1230
|
// adapters/codex/dist/transcript.js
|
|
1231
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1231
1232
|
import { basename, join as join10 } from "node:path";
|
|
1232
1233
|
import { homedir as homedir4 } from "node:os";
|
|
1233
1234
|
function str(value) {
|
|
@@ -1264,17 +1265,62 @@ function textOf(content) {
|
|
|
1264
1265
|
const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
|
|
1265
1266
|
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
1266
1267
|
}
|
|
1268
|
+
function jwtClaims(token) {
|
|
1269
|
+
const parts = token.split(".");
|
|
1270
|
+
if (parts.length !== 3 || !parts[1])
|
|
1271
|
+
return void 0;
|
|
1272
|
+
try {
|
|
1273
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
1274
|
+
const claims = JSON.parse(json);
|
|
1275
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
1276
|
+
} catch {
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function emailFromAuth(raw) {
|
|
1281
|
+
let parsed;
|
|
1282
|
+
try {
|
|
1283
|
+
parsed = JSON.parse(raw);
|
|
1284
|
+
} catch {
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
const tokens = parsed?.tokens;
|
|
1288
|
+
const token = tokens?.id_token;
|
|
1289
|
+
if (typeof token !== "string" || token === "")
|
|
1290
|
+
return void 0;
|
|
1291
|
+
const email = jwtClaims(token)?.["email"];
|
|
1292
|
+
return typeof email === "string" && email !== "" ? email : void 0;
|
|
1293
|
+
}
|
|
1267
1294
|
var codexTranscript = {
|
|
1268
1295
|
vendor: "codex",
|
|
1269
1296
|
sessionRoots() {
|
|
1270
1297
|
return [join10(homedir4(), ".codex", "sessions")];
|
|
1271
1298
|
},
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1299
|
+
/**
|
|
1300
|
+
* `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
|
|
1301
|
+
*
|
|
1302
|
+
* Codex records no identity in the rollout itself and none in plain text anywhere
|
|
1303
|
+
* else: `tokens.account_id` is a UUID, not a person. The address is a claim inside the
|
|
1304
|
+
* OAuth id_token, so reading it means decoding that token — hence {@link jwtClaims}.
|
|
1305
|
+
*
|
|
1306
|
+
* The token's own `exp` is deliberately not enforced. It is an hour long and the file
|
|
1307
|
+
* is only rewritten on sign-in and on refresh, so gating on expiry would leave most
|
|
1308
|
+
* sessions unattributed while answering a question nobody asked: an expired id_token
|
|
1309
|
+
* is evidence that a token needs refreshing, never evidence that a different person is
|
|
1310
|
+
* now signed in. Signing in as someone else rewrites this file, which is what makes
|
|
1311
|
+
* the stale-address worry unfounded — the claim always names the last account to
|
|
1312
|
+
* authenticate on this machine, which is the account that wrote these rollouts.
|
|
1313
|
+
*
|
|
1314
|
+
* `undefined` for API-key auth (`OPENAI_API_KEY` with no tokens), for a signed-out
|
|
1315
|
+
* agent, and for anything unreadable; the caller then falls back to configuration.
|
|
1316
|
+
*/
|
|
1317
|
+
resolveUserId() {
|
|
1318
|
+
try {
|
|
1319
|
+
return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
|
|
1320
|
+
} catch {
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1278
1324
|
sessionIdFor(path) {
|
|
1279
1325
|
const name = basename(path, ".jsonl");
|
|
1280
1326
|
const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(name);
|
|
@@ -1373,7 +1419,7 @@ var codexTranscript = {
|
|
|
1373
1419
|
};
|
|
1374
1420
|
|
|
1375
1421
|
// adapters/codex/bin/lib/launchers.mjs
|
|
1376
|
-
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as
|
|
1422
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1377
1423
|
import { homedir as homedir5 } from "node:os";
|
|
1378
1424
|
import { join as join11 } from "node:path";
|
|
1379
1425
|
var LAUNCHERS = {
|
|
@@ -1399,7 +1445,7 @@ function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
|
1399
1445
|
}
|
|
1400
1446
|
let body = "";
|
|
1401
1447
|
try {
|
|
1402
|
-
body =
|
|
1448
|
+
body = readFileSync7(file, "utf8");
|
|
1403
1449
|
} catch {
|
|
1404
1450
|
}
|
|
1405
1451
|
if (body !== script(target)) stale.push(name);
|
|
@@ -1437,7 +1483,7 @@ try {
|
|
|
1437
1483
|
const root = join12(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
1438
1484
|
let installed = "unknown";
|
|
1439
1485
|
try {
|
|
1440
|
-
installed = JSON.parse(
|
|
1486
|
+
installed = JSON.parse(readFileSync8(join12(root, "package.json"), "utf8")).version;
|
|
1441
1487
|
} catch {
|
|
1442
1488
|
}
|
|
1443
1489
|
console.log(` plugin ${installed} ${root}`);
|
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.10",
|
|
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.2",
|
|
17
|
+
"@darkhunt-security/endpoint-core": "0.9.2"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public",
|