@darkhunt-security/endpoint-codex 0.9.9 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/dist/bin/backfill.mjs +326 -19
- package/dist/bin/forwarder.mjs +326 -19
- package/dist/bin/status.mjs +237 -15
- package/package.json +3 -3
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,128 @@ 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
|
+
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 unwrapExecOutput(value) {
|
|
1290
|
+
if (typeof value !== "string")
|
|
1291
|
+
return { output: value };
|
|
1292
|
+
let parsed;
|
|
1293
|
+
try {
|
|
1294
|
+
parsed = JSON.parse(value);
|
|
1295
|
+
} catch {
|
|
1296
|
+
return { output: value };
|
|
1297
|
+
}
|
|
1298
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1299
|
+
return { output: value };
|
|
1300
|
+
const bag = parsed;
|
|
1301
|
+
if (!Object.prototype.hasOwnProperty.call(bag, "output"))
|
|
1302
|
+
return { output: value };
|
|
1303
|
+
const metadata = bag["metadata"];
|
|
1304
|
+
if (!metadata || typeof metadata !== "object")
|
|
1305
|
+
return { output: value };
|
|
1306
|
+
const exitCode = num(metadata["exit_code"]);
|
|
1307
|
+
if (exitCode === void 0)
|
|
1308
|
+
return { output: value };
|
|
1309
|
+
return { output: bag["output"], exitCode };
|
|
1310
|
+
}
|
|
1311
|
+
function jwtClaims(token) {
|
|
1312
|
+
const parts = token.split(".");
|
|
1313
|
+
if (parts.length !== 3 || !parts[1])
|
|
1314
|
+
return void 0;
|
|
1315
|
+
try {
|
|
1316
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
1317
|
+
const claims = JSON.parse(json);
|
|
1318
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
1319
|
+
} catch {
|
|
1320
|
+
return void 0;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function emailFromAuth(raw) {
|
|
1324
|
+
let parsed;
|
|
1325
|
+
try {
|
|
1326
|
+
parsed = JSON.parse(raw);
|
|
1327
|
+
} catch {
|
|
1328
|
+
return void 0;
|
|
1329
|
+
}
|
|
1330
|
+
const tokens = parsed?.tokens;
|
|
1331
|
+
const token = tokens?.id_token;
|
|
1332
|
+
if (typeof token !== "string" || token === "")
|
|
1333
|
+
return void 0;
|
|
1334
|
+
const email = jwtClaims(token)?.["email"];
|
|
1335
|
+
return typeof email === "string" && email !== "" ? email : void 0;
|
|
1336
|
+
}
|
|
1337
|
+
function repositoryIdentity(url) {
|
|
1338
|
+
if (url === void 0)
|
|
1339
|
+
return void 0;
|
|
1340
|
+
const trimmed = url.trim();
|
|
1341
|
+
if (trimmed === "")
|
|
1342
|
+
return void 0;
|
|
1343
|
+
const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
|
|
1344
|
+
if (scp)
|
|
1345
|
+
return `${scp[1]}/${stripSuffix(scp[2])}`;
|
|
1346
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
|
|
1347
|
+
try {
|
|
1348
|
+
const parsed = new URL(withScheme);
|
|
1349
|
+
if (!parsed.hostname)
|
|
1350
|
+
return trimmed;
|
|
1351
|
+
const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
|
|
1352
|
+
return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
|
|
1353
|
+
} catch {
|
|
1354
|
+
return trimmed;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function stripSuffix(path) {
|
|
1358
|
+
return path.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
1359
|
+
}
|
|
1267
1360
|
var codexTranscript = {
|
|
1268
1361
|
vendor: "codex",
|
|
1269
1362
|
sessionRoots() {
|
|
1270
1363
|
return [join10(homedir4(), ".codex", "sessions")];
|
|
1271
1364
|
},
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1365
|
+
/**
|
|
1366
|
+
* `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
|
|
1367
|
+
*
|
|
1368
|
+
* Codex records no identity in the rollout itself and none in plain text anywhere
|
|
1369
|
+
* else: `tokens.account_id` is a UUID, not a person. The address is a claim inside the
|
|
1370
|
+
* OAuth id_token, so reading it means decoding that token — hence {@link jwtClaims}.
|
|
1371
|
+
*
|
|
1372
|
+
* The token's own `exp` is deliberately not enforced. It is an hour long and the file
|
|
1373
|
+
* is only rewritten on sign-in and on refresh, so gating on expiry would leave most
|
|
1374
|
+
* sessions unattributed while answering a question nobody asked: an expired id_token
|
|
1375
|
+
* is evidence that a token needs refreshing, never evidence that a different person is
|
|
1376
|
+
* now signed in. Signing in as someone else rewrites this file, which is what makes
|
|
1377
|
+
* the stale-address worry unfounded — the claim always names the last account to
|
|
1378
|
+
* authenticate on this machine, which is the account that wrote these rollouts.
|
|
1379
|
+
*
|
|
1380
|
+
* `undefined` for API-key auth (`OPENAI_API_KEY` with no tokens), for a signed-out
|
|
1381
|
+
* agent, and for anything unreadable; the caller then falls back to configuration.
|
|
1382
|
+
*/
|
|
1383
|
+
resolveUserId() {
|
|
1384
|
+
try {
|
|
1385
|
+
return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
|
|
1386
|
+
} catch {
|
|
1387
|
+
return void 0;
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1278
1390
|
sessionIdFor(path) {
|
|
1279
1391
|
const name = basename(path, ".jsonl");
|
|
1280
1392
|
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);
|
|
@@ -1297,12 +1409,20 @@ var codexTranscript = {
|
|
|
1297
1409
|
const uuid = `codex:${offset}`;
|
|
1298
1410
|
const base = { vendor: "codex", ts, uuid };
|
|
1299
1411
|
if (raw.type === "session_meta") {
|
|
1412
|
+
const rawGit = payload["git"];
|
|
1413
|
+
const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
|
|
1414
|
+
const gitBranch = str(git?.["branch"]);
|
|
1415
|
+
const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
|
|
1300
1416
|
return [
|
|
1301
1417
|
{
|
|
1302
1418
|
...base,
|
|
1303
1419
|
kind: "session_meta",
|
|
1304
1420
|
meta: {
|
|
1305
1421
|
...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
|
|
1422
|
+
// Conditional spread rather than a plain assignment: the 7 `{}` blocks and
|
|
1423
|
+
// the 92 absent ones must leave the key absent, not present-and-undefined.
|
|
1424
|
+
...gitBranch !== void 0 ? { gitBranch } : {},
|
|
1425
|
+
...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
|
|
1306
1426
|
...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
|
|
1307
1427
|
...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
|
|
1308
1428
|
// A Codex subagent is a rollout of its own, a peer of its parent in the
|
|
@@ -1318,15 +1438,61 @@ var codexTranscript = {
|
|
|
1318
1438
|
}
|
|
1319
1439
|
if (raw.type === "turn_context") {
|
|
1320
1440
|
const model = str(payload["model"]);
|
|
1321
|
-
|
|
1441
|
+
const approval = str(payload["approval_policy"]);
|
|
1442
|
+
const sandbox = payload["sandbox_policy"];
|
|
1443
|
+
const sandboxMode = (() => {
|
|
1444
|
+
if (!sandbox || typeof sandbox !== "object")
|
|
1445
|
+
return void 0;
|
|
1446
|
+
const bag = sandbox;
|
|
1447
|
+
return str(bag["mode"]) ?? str(bag["type"]);
|
|
1448
|
+
})();
|
|
1449
|
+
const meta = {
|
|
1450
|
+
...model !== void 0 ? { model } : {},
|
|
1451
|
+
...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
|
|
1452
|
+
...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
|
|
1453
|
+
};
|
|
1454
|
+
if (Object.keys(meta).length === 0)
|
|
1322
1455
|
return [];
|
|
1323
|
-
return [{ ...base, kind: "session_meta", meta
|
|
1456
|
+
return [{ ...base, kind: "session_meta", meta }];
|
|
1324
1457
|
}
|
|
1325
1458
|
const kind = str(payload["type"]);
|
|
1326
1459
|
if (raw.type === "event_msg" && kind === "token_count") {
|
|
1327
1460
|
const usage = mapTokenCount(payload["info"]);
|
|
1328
1461
|
return usage ? [{ ...base, kind: "usage", usage }] : [];
|
|
1329
1462
|
}
|
|
1463
|
+
if (raw.type === "compacted") {
|
|
1464
|
+
const note = str(payload["message"]);
|
|
1465
|
+
return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
|
|
1466
|
+
}
|
|
1467
|
+
if (raw.type === "world_state") {
|
|
1468
|
+
const state = payload["state"];
|
|
1469
|
+
const perms = state && typeof state === "object" ? state["permissions"] : void 0;
|
|
1470
|
+
if (!perms || typeof perms !== "object")
|
|
1471
|
+
return [];
|
|
1472
|
+
const list = perms["approved_command_prefixes"];
|
|
1473
|
+
const steps = Array.isArray(list) ? list : [];
|
|
1474
|
+
return [
|
|
1475
|
+
{
|
|
1476
|
+
...base,
|
|
1477
|
+
kind: "permission_grant",
|
|
1478
|
+
input: {
|
|
1479
|
+
steps: budgeted(steps),
|
|
1480
|
+
count: steps.length,
|
|
1481
|
+
...payload["full"] === true ? { full: true } : {}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
];
|
|
1485
|
+
}
|
|
1486
|
+
if (raw.type === "event_msg" && kind === "turn_aborted") {
|
|
1487
|
+
return [
|
|
1488
|
+
{
|
|
1489
|
+
...base,
|
|
1490
|
+
kind: "user_message",
|
|
1491
|
+
isError: true,
|
|
1492
|
+
errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
|
|
1493
|
+
}
|
|
1494
|
+
];
|
|
1495
|
+
}
|
|
1330
1496
|
if (raw.type !== "response_item")
|
|
1331
1497
|
return [];
|
|
1332
1498
|
switch (kind) {
|
|
@@ -1340,18 +1506,74 @@ var codexTranscript = {
|
|
|
1340
1506
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1341
1507
|
}
|
|
1342
1508
|
];
|
|
1343
|
-
case "function_call_output":
|
|
1509
|
+
case "function_call_output": {
|
|
1510
|
+
const flattened = textOf(payload["output"]) ?? payload["output"];
|
|
1511
|
+
const { output, exitCode } = unwrapExecOutput(flattened);
|
|
1344
1512
|
return [
|
|
1345
1513
|
{
|
|
1346
1514
|
...base,
|
|
1347
1515
|
kind: "tool_result",
|
|
1348
|
-
output
|
|
1516
|
+
output,
|
|
1517
|
+
// Only where an exit status was actually found. On the 993 string outputs
|
|
1518
|
+
// that are not envelopes and the 144 array ones the key stays ABSENT rather
|
|
1519
|
+
// than `false`: the emitter tests `record.isError`, and "this tool reported
|
|
1520
|
+
// success" is a different claim from "nothing here reported an outcome".
|
|
1521
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
1349
1522
|
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1350
1523
|
}
|
|
1351
1524
|
];
|
|
1525
|
+
}
|
|
1526
|
+
// Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
|
|
1527
|
+
// `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
|
|
1528
|
+
// across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
|
|
1529
|
+
// handling only `function_call` left every shell command and every file write
|
|
1530
|
+
// this endpoint made invisible to both the trace and anything reading it.
|
|
1531
|
+
case "custom_tool_call":
|
|
1532
|
+
return [
|
|
1533
|
+
{
|
|
1534
|
+
...base,
|
|
1535
|
+
kind: "tool_call",
|
|
1536
|
+
toolName: str(payload["name"]) ?? "tool",
|
|
1537
|
+
input: payload["input"],
|
|
1538
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1539
|
+
}
|
|
1540
|
+
];
|
|
1541
|
+
case "custom_tool_call_output": {
|
|
1542
|
+
const flattened = textOf(payload["output"]) ?? payload["output"];
|
|
1543
|
+
const { output, exitCode } = unwrapExecOutput(flattened);
|
|
1544
|
+
return [
|
|
1545
|
+
{
|
|
1546
|
+
...base,
|
|
1547
|
+
kind: "tool_result",
|
|
1548
|
+
output,
|
|
1549
|
+
...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
|
|
1550
|
+
...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
|
|
1551
|
+
}
|
|
1552
|
+
];
|
|
1553
|
+
}
|
|
1554
|
+
// The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
|
|
1555
|
+
// `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
|
|
1556
|
+
// session where the agent searched and opened pages rendered as reasoning
|
|
1557
|
+
// followed by an answer with no source anywhere in it. The queries and the URLs
|
|
1558
|
+
// are the whole point of the record: they are the evidence of what the agent
|
|
1559
|
+
// read from outside the machine.
|
|
1560
|
+
//
|
|
1561
|
+
// `action` is passed through whole. It is already a vendor-neutral argument bag
|
|
1562
|
+
// ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
|
|
1563
|
+
// worst, 121 on average across all 49.
|
|
1564
|
+
//
|
|
1565
|
+
// Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
|
|
1566
|
+
// `id`, `call_id` is the pairing key everywhere else in this stream, and no
|
|
1567
|
+
// response_item output line ever pairs with a search — so setting one would
|
|
1568
|
+
// register a span in the emitter's pairing table that nothing ever closes, and
|
|
1569
|
+
// `finish()` would end it at pass end and stamp a multi-minute duration on a
|
|
1570
|
+
// search that took seconds. Left unpaired, the emitter closes it at the call
|
|
1571
|
+
// timestamp, which is the truthful reading.
|
|
1572
|
+
case "web_search_call":
|
|
1573
|
+
return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
|
|
1352
1574
|
case "reasoning": {
|
|
1353
1575
|
const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
|
|
1354
|
-
return
|
|
1576
|
+
return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
|
|
1355
1577
|
}
|
|
1356
1578
|
case "message": {
|
|
1357
1579
|
const text = textOf(payload["content"]);
|
|
@@ -1373,7 +1595,7 @@ var codexTranscript = {
|
|
|
1373
1595
|
};
|
|
1374
1596
|
|
|
1375
1597
|
// adapters/codex/bin/lib/launchers.mjs
|
|
1376
|
-
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as
|
|
1598
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1377
1599
|
import { homedir as homedir5 } from "node:os";
|
|
1378
1600
|
import { join as join11 } from "node:path";
|
|
1379
1601
|
var LAUNCHERS = {
|
|
@@ -1399,7 +1621,7 @@ function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
|
1399
1621
|
}
|
|
1400
1622
|
let body = "";
|
|
1401
1623
|
try {
|
|
1402
|
-
body =
|
|
1624
|
+
body = readFileSync7(file, "utf8");
|
|
1403
1625
|
} catch {
|
|
1404
1626
|
}
|
|
1405
1627
|
if (body !== script(target)) stale.push(name);
|
|
@@ -1437,7 +1659,7 @@ try {
|
|
|
1437
1659
|
const root = join12(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
1438
1660
|
let installed = "unknown";
|
|
1439
1661
|
try {
|
|
1440
|
-
installed = JSON.parse(
|
|
1662
|
+
installed = JSON.parse(readFileSync8(join12(root, "package.json"), "utf8")).version;
|
|
1441
1663
|
} catch {
|
|
1442
1664
|
}
|
|
1443
1665
|
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.11",
|
|
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",
|