@darkhunt-security/endpoint-codex 0.9.9 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkhunt-guard",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
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": {
@@ -18557,6 +18557,9 @@ var SessionEmitter = class {
18557
18557
  return this.options.parentAgentId ?? this.meta["parentSessionId"];
18558
18558
  }
18559
18559
  defaultTraceName() {
18560
+ const label = this.options.workflow?.label;
18561
+ if (label)
18562
+ return label;
18560
18563
  const agentType = this.agentType();
18561
18564
  if (agentType)
18562
18565
  return `subagent: ${agentType}`;
@@ -18565,6 +18568,27 @@ var SessionEmitter = class {
18565
18568
  }
18566
18569
  return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
18567
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
+ }
18568
18592
  /** The session traces group under. Set from the path, or from `session_meta`. */
18569
18593
  grouping;
18570
18594
  ensureTrace(ts) {
@@ -18589,7 +18613,10 @@ var SessionEmitter = class {
18589
18613
  ...this.agentId() !== void 0 ? { agent_id: this.agentId() } : {},
18590
18614
  ...this.agentType() !== void 0 ? { agent_type: this.agentType() } : {},
18591
18615
  ...this.parentAgentId() !== void 0 ? { parent_agent_id: this.parentAgentId() } : {},
18592
- ...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()
18593
18620
  },
18594
18621
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
18595
18622
  ...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
@@ -21746,6 +21773,7 @@ async function runForwarder(mapper, options = {}) {
21746
21773
  ...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
21747
21774
  ...sub?.parentAgentId !== void 0 ? { parentAgentId: sub.parentAgentId } : {},
21748
21775
  ...sub?.spawnDepth !== void 0 ? { spawnDepth: sub.spawnDepth } : {},
21776
+ ...sub?.workflow !== void 0 ? { workflow: sub.workflow } : {},
21749
21777
  ...ids !== void 0 ? { ids } : {},
21750
21778
  ...userId !== void 0 ? { userId } : {},
21751
21779
  ...carried !== void 0 ? { resumeTurn: carried } : {}
@@ -21874,6 +21902,7 @@ import { join as join9 } from "node:path";
21874
21902
  var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
21875
21903
 
21876
21904
  // adapters/codex/dist/transcript.js
21905
+ import { readFileSync as readFileSync6 } from "node:fs";
21877
21906
  import { basename, join as join10 } from "node:path";
21878
21907
  import { homedir as homedir4 } from "node:os";
21879
21908
  function str(value) {
@@ -21910,17 +21939,62 @@ function textOf(content) {
21910
21939
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
21911
21940
  return parts.length > 0 ? parts.join("\n") : void 0;
21912
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
+ }
21913
21968
  var codexTranscript = {
21914
21969
  vendor: "codex",
21915
21970
  sessionRoots() {
21916
21971
  return [join10(homedir4(), ".codex", "sessions")];
21917
21972
  },
21918
- // No `resolveUserId` yet, so Codex traces fall back to the configured `userId`.
21919
- // The identity exists but not as a plain field: `~/.codex/auth.json` holds
21920
- // `tokens.account_id` (a UUID, not a person) and an `email` claim inside the
21921
- // `id_token` JWT. Reading it means decoding an OAuth token and honouring its expiry
21922
- // a stale token would attribute sessions to a *wrong* address, which is worse than
21923
- // leaving them unattributed. Tracked separately.
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
+ },
21924
21998
  sessionIdFor(path) {
21925
21999
  const name = basename(path, ".jsonl");
21926
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);
@@ -18557,6 +18557,9 @@ var SessionEmitter = class {
18557
18557
  return this.options.parentAgentId ?? this.meta["parentSessionId"];
18558
18558
  }
18559
18559
  defaultTraceName() {
18560
+ const label = this.options.workflow?.label;
18561
+ if (label)
18562
+ return label;
18560
18563
  const agentType = this.agentType();
18561
18564
  if (agentType)
18562
18565
  return `subagent: ${agentType}`;
@@ -18565,6 +18568,27 @@ var SessionEmitter = class {
18565
18568
  }
18566
18569
  return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
18567
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
+ }
18568
18592
  /** The session traces group under. Set from the path, or from `session_meta`. */
18569
18593
  grouping;
18570
18594
  ensureTrace(ts) {
@@ -18589,7 +18613,10 @@ var SessionEmitter = class {
18589
18613
  ...this.agentId() !== void 0 ? { agent_id: this.agentId() } : {},
18590
18614
  ...this.agentType() !== void 0 ? { agent_type: this.agentType() } : {},
18591
18615
  ...this.parentAgentId() !== void 0 ? { parent_agent_id: this.parentAgentId() } : {},
18592
- ...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()
18593
18620
  },
18594
18621
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
18595
18622
  ...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
@@ -21746,6 +21773,7 @@ async function runForwarder(mapper, options = {}) {
21746
21773
  ...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
21747
21774
  ...sub?.parentAgentId !== void 0 ? { parentAgentId: sub.parentAgentId } : {},
21748
21775
  ...sub?.spawnDepth !== void 0 ? { spawnDepth: sub.spawnDepth } : {},
21776
+ ...sub?.workflow !== void 0 ? { workflow: sub.workflow } : {},
21749
21777
  ...ids !== void 0 ? { ids } : {},
21750
21778
  ...userId !== void 0 ? { userId } : {},
21751
21779
  ...carried !== void 0 ? { resumeTurn: carried } : {}
@@ -21861,6 +21889,7 @@ import { join as join9 } from "node:path";
21861
21889
  var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
21862
21890
 
21863
21891
  // adapters/codex/dist/transcript.js
21892
+ import { readFileSync as readFileSync6 } from "node:fs";
21864
21893
  import { basename, join as join10 } from "node:path";
21865
21894
  import { homedir as homedir4 } from "node:os";
21866
21895
  function str(value) {
@@ -21897,17 +21926,62 @@ function textOf(content) {
21897
21926
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
21898
21927
  return parts.length > 0 ? parts.join("\n") : void 0;
21899
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
+ }
21900
21955
  var codexTranscript = {
21901
21956
  vendor: "codex",
21902
21957
  sessionRoots() {
21903
21958
  return [join10(homedir4(), ".codex", "sessions")];
21904
21959
  },
21905
- // No `resolveUserId` yet, so Codex traces fall back to the configured `userId`.
21906
- // The identity exists but not as a plain field: `~/.codex/auth.json` holds
21907
- // `tokens.account_id` (a UUID, not a person) and an `email` claim inside the
21908
- // `id_token` JWT. Reading it means decoding an OAuth token and honouring its expiry
21909
- // a stale token would attribute sessions to a *wrong* address, which is worse than
21910
- // leaving them unattributed. Tracked separately.
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
+ },
21911
21985
  sessionIdFor(path) {
21912
21986
  const name = basename(path, ".jsonl");
21913
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);
@@ -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 readFileSync7 } from "node:fs";
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
- // No `resolveUserId` yet, so Codex traces fall back to the configured `userId`.
1273
- // The identity exists but not as a plain field: `~/.codex/auth.json` holds
1274
- // `tokens.account_id` (a UUID, not a person) and an `email` claim inside the
1275
- // `id_token` JWT. Reading it means decoding an OAuth token and honouring its expiry
1276
- // a stale token would attribute sessions to a *wrong* address, which is worse than
1277
- // leaving them unattributed. Tracked separately.
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 readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
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 = readFileSync6(file, "utf8");
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(readFileSync7(join12(root, "package.json"), "utf8")).version;
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.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.1",
17
- "@darkhunt-security/endpoint-core": "0.9.1"
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",