@darkhunt-security/endpoint-codex 0.9.8 → 0.9.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkhunt-guard",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "hooks": "./hooks/hooks.json",
5
5
  "description": "Darkhunt endpoint plugin for Codex CLI \u2014 full session trace capture and PreToolUse guardrails.",
6
6
  "author": {
@@ -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 = `[${record.mediaType ?? "attachment"}${record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : ""}]`;
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;
@@ -18531,6 +18575,10 @@ var SessionEmitter = class {
18531
18575
  name: this.title ?? this.defaultTraceName(),
18532
18576
  sessionId: this.grouping ?? this.options.sessionId,
18533
18577
  startTime: ts,
18578
+ // What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
18579
+ // `update()` carries output alone — which is why `ingest` absorbs the user turn
18580
+ // before it calls this.
18581
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18534
18582
  tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18535
18583
  metadata: {
18536
18584
  ...this.meta,
@@ -18596,6 +18644,8 @@ var SessionEmitter = class {
18596
18644
  if (!this.generation)
18597
18645
  return;
18598
18646
  const output = this.outputText.join("\n");
18647
+ if (output)
18648
+ this.lastAssistantText = output;
18599
18649
  this.generation.end({
18600
18650
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18601
18651
  ...this.model !== void 0 ? { model: this.model } : {},
@@ -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 = `[${record.mediaType ?? "attachment"}${record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : ""}]`;
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;
@@ -18531,6 +18575,10 @@ var SessionEmitter = class {
18531
18575
  name: this.title ?? this.defaultTraceName(),
18532
18576
  sessionId: this.grouping ?? this.options.sessionId,
18533
18577
  startTime: ts,
18578
+ // What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
18579
+ // `update()` carries output alone — which is why `ingest` absorbs the user turn
18580
+ // before it calls this.
18581
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18534
18582
  tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18535
18583
  metadata: {
18536
18584
  ...this.meta,
@@ -18596,6 +18644,8 @@ var SessionEmitter = class {
18596
18644
  if (!this.generation)
18597
18645
  return;
18598
18646
  const output = this.outputText.join("\n");
18647
+ if (output)
18648
+ this.lastAssistantText = output;
18599
18649
  this.generation.end({
18600
18650
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18601
18651
  ...this.model !== void 0 ? { model: this.model } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhunt-security/endpoint-codex",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "type": "module",
5
5
  "description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
6
6
  "bin": {