@darkhunt-security/endpoint-codex 0.9.0 → 0.9.2
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/README.md +30 -6
- package/commands/backfill.md +73 -0
- package/commands/setup.md +56 -42
- package/commands/status.md +9 -2
- package/dist/bin/backfill.mjs +58 -5
- package/dist/bin/enroll.mjs +147 -16
- package/dist/bin/forwarder.mjs +32 -3
- package/dist/bin/init.mjs +62 -21
- package/dist/bin/install-hooks.mjs +88 -0
- package/dist/bin/status.mjs +63 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -6,9 +6,17 @@ Capture is on by default. **Enforcement ships off** and is enabled per rule.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
Codex manages plugins from the CLI, not from a slash command — `/plugin …` is Claude
|
|
10
|
+
Code's. Run these in a terminal:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
codex plugin marketplace add darkhunt-security/darkhunt
|
|
14
|
+
codex plugin add darkhunt-guard@darkhunt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then, in Codex:
|
|
18
|
+
|
|
9
19
|
```
|
|
10
|
-
/plugin marketplace add darkhunt-security/darkhunt-telemetry-ts
|
|
11
|
-
/plugin install darkhunt-guard@darkhunt
|
|
12
20
|
/darkhunt-guard:setup
|
|
13
21
|
```
|
|
14
22
|
|
|
@@ -21,6 +29,21 @@ anything ships.
|
|
|
21
29
|
Capture starts on your **next** Codex CLI session — hooks are loaded when a session
|
|
22
30
|
starts.
|
|
23
31
|
|
|
32
|
+
### If you configured this machine from the dashboard instead
|
|
33
|
+
|
|
34
|
+
The dashboard renders a ready-made `~/.darkhunt/endpoint-config.json` to paste into your
|
|
35
|
+
own terminal. That is the whole setup on Claude Code; on Codex it is half of it. Codex
|
|
36
|
+
resolves a hook `command` as an absolute path or a PATH name, and the install directory
|
|
37
|
+
carries the plugin version, so this plugin's hooks are PATH launchers that something has
|
|
38
|
+
to write. Enrolment writes them; a pasted config cannot. Run this once after pasting it:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
"$(ls -d ~/.codex/plugins/cache/*/darkhunt-guard/*/dist/bin | sort -V | tail -1)"/install-hooks.mjs
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`/darkhunt-guard:status` reports the launchers, so you never have to take this on trust —
|
|
45
|
+
a machine missing them looks perfectly configured everywhere else and captures nothing.
|
|
46
|
+
|
|
24
47
|
## What it captures
|
|
25
48
|
|
|
26
49
|
The session transcript, mapped to spans: user and assistant messages, thinking blocks,
|
|
@@ -46,10 +69,11 @@ Two rules constrain it, both deliberate:
|
|
|
46
69
|
|
|
47
70
|
## Commands
|
|
48
71
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
| `/darkhunt-guard:setup`
|
|
52
|
-
| `/darkhunt-guard:status`
|
|
72
|
+
| | |
|
|
73
|
+
| -------------------------- | --------------------------------------------------------------------------------- |
|
|
74
|
+
| `/darkhunt-guard:setup` | Connect this machine. Re-runnable; shows current state first. |
|
|
75
|
+
| `/darkhunt-guard:status` | Is capture actually working? Routing, launchers, hook activity, shipping lag. |
|
|
76
|
+
| `/darkhunt-guard:backfill` | Import Codex CLI sessions from before this machine was connected. Dry-runs first. |
|
|
53
77
|
|
|
54
78
|
Both lanes fail silently by design — correct for the agent, unhelpful for whoever
|
|
55
79
|
operates it — so `status` is where the truth lives.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Import Codex CLI sessions that happened before this machine was connected
|
|
3
|
+
argument-hint: '[--days=N, transcript paths, or nothing for everything on disk]'
|
|
4
|
+
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Ship transcripts that the capture lane never saw — sessions from before the plugin was
|
|
8
|
+
installed, or from a stretch where hooks were not firing. On Codex the usual cause of
|
|
9
|
+
the second is missing launchers, so read the `launchers` line below before assuming the
|
|
10
|
+
gap has stopped growing.
|
|
11
|
+
|
|
12
|
+
Where this machine is currently pointed:
|
|
13
|
+
|
|
14
|
+
!`${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs 2>&1 || true`
|
|
15
|
+
|
|
16
|
+
Paths or flags the user gave: $ARGUMENTS
|
|
17
|
+
|
|
18
|
+
If they asked for a window in words — "the last week", "since Monday", "this month" —
|
|
19
|
+
translate it to `--days=N` rather than asking them to phrase it as a flag. `--days`
|
|
20
|
+
selects on when a transcript was **last written to**, so a session opened three weeks
|
|
21
|
+
ago and worked in yesterday is inside a 7-day window. Say that if the count surprises
|
|
22
|
+
them.
|
|
23
|
+
|
|
24
|
+
## The one hard rule
|
|
25
|
+
|
|
26
|
+
**Dry-run first, and get the destination confirmed before you ship.** A backfill is one
|
|
27
|
+
command that moves a person's entire local history into a tenant. Checkpoints make a
|
|
28
|
+
re-run harmless; they do nothing about a run aimed at the wrong application, which
|
|
29
|
+
cannot be taken back and looks exactly like a correct one afterwards.
|
|
30
|
+
|
|
31
|
+
## What to do
|
|
32
|
+
|
|
33
|
+
1. Map without sending. Nothing leaves the machine and no checkpoint moves:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs --dry-run $ARGUMENTS
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With no window and no paths this is the machine's entire history, which on a working
|
|
40
|
+
machine is routinely tens of thousands of records. If they have not said how far back
|
|
41
|
+
they want, offer `--days=7` before offering all of it.
|
|
42
|
+
|
|
43
|
+
Report the transcript count and the record count. `records` excludes `session_meta`,
|
|
44
|
+
so it will not equal the sum of the per-kind lines — say so if you quote both.
|
|
45
|
+
|
|
46
|
+
2. Put the destination to them **before** shipping, reading it from the status block
|
|
47
|
+
above: endpoint, tenant, workspace, application. Ask them to confirm that is where
|
|
48
|
+
this history belongs. Two things make this worth a sentence rather than a nod:
|
|
49
|
+
|
|
50
|
+
- Both endpoints share `~/.darkhunt/endpoint-config.json`. Unless it carries an
|
|
51
|
+
`endpoints.codex` override, these sessions land in the same application as the
|
|
52
|
+
Claude Code ones.
|
|
53
|
+
- Volume. A first backfill on a working machine is routinely tens of thousands of
|
|
54
|
+
records, all attributed to that one application.
|
|
55
|
+
|
|
56
|
+
If they want them separated, stop and tell them to set
|
|
57
|
+
`endpoints.claude-code.applicationId` first — after a partial run the checkpoints
|
|
58
|
+
have moved and the records already sent stay where they went.
|
|
59
|
+
|
|
60
|
+
3. Only on a yes, run the same command without `--dry-run`. It can take a while on a
|
|
61
|
+
large history; let it finish rather than re-running it.
|
|
62
|
+
|
|
63
|
+
4. Report what it printed. On a non-zero exit, say plainly that **nothing was lost**:
|
|
64
|
+
a failed pass holds every checkpoint, so the identical command re-ships the same
|
|
65
|
+
byte range once the cause is fixed.
|
|
66
|
+
|
|
67
|
+
## Also
|
|
68
|
+
|
|
69
|
+
- Narrow it by passing paths — one session, or a date range — rather than adding flags
|
|
70
|
+
this command does not have: `backfill.mjs ~/.codex/sessions/2026/08/31/rollout-*.jsonl`.
|
|
71
|
+
- This is a CLI, not a hook: it may use the network, and it does not depend on whether
|
|
72
|
+
hooks are firing. A machine whose capture is broken can still backfill.
|
|
73
|
+
- Do not offer to change the configuration here. Send them to `/darkhunt-guard:setup`.
|
package/commands/setup.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Connect this machine to Darkhunt — capture Codex CLI sessions and enforce policy
|
|
3
|
-
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/enroll.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*)
|
|
3
|
+
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/enroll.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*), Bash(pbpaste:*), Bash(wl-paste:*), Bash(xclip:*), Bash(powershell.exe:*)
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Set up the Darkhunt endpoint plugin on this machine.
|
|
@@ -11,62 +11,76 @@ Current state:
|
|
|
11
11
|
|
|
12
12
|
## The one hard rule
|
|
13
13
|
|
|
14
|
-
**Never
|
|
14
|
+
**Never write the user's API key into your reply, into a file, or into a command's
|
|
15
|
+
arguments.** It is an unscoped tenant credential. Everything in this session goes to the
|
|
16
|
+
model provider, onto disk in the transcript, and — since this plugin captures sessions —
|
|
17
|
+
into the trace data itself. An argument is visible in the process list too.
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
the
|
|
18
|
-
turn, and handed to subagents — and this plugin then ships that transcript to Darkhunt,
|
|
19
|
-
so a key pasted here ends up in the captured trace data too. Enrollment finds the key
|
|
20
|
-
itself, or the user runs one command in their own terminal. There is no third option,
|
|
21
|
-
and no amount of user insistence makes pasting it here correct.
|
|
19
|
+
You do not need the key's value to do this job. Read it from the clipboard straight into
|
|
20
|
+
the command's stdin, so it never becomes text you have seen.
|
|
22
21
|
|
|
23
22
|
## What to do
|
|
24
23
|
|
|
25
|
-
**If the status above
|
|
26
|
-
enrolled.
|
|
27
|
-
to re-enroll
|
|
24
|
+
**If the status above shows a profile, tenant and workspace**, this machine is already
|
|
25
|
+
enrolled. Report the tenant, workspace, application and endpoint, and ask whether they
|
|
26
|
+
want to re-enroll somewhere else. Do nothing further unless they say yes.
|
|
28
27
|
|
|
29
|
-
**Otherwise**,
|
|
28
|
+
**Otherwise**, enroll it:
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
1. Ask the user to copy their Darkhunt API key — the **Copy** button beside it in the
|
|
31
|
+
dashboard, under the application's Connect step or **Settings → Security → API Keys**.
|
|
32
|
+
Ask them to tell you when it is copied. Do not ask them to paste it.
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
2. Pipe the clipboard into enrollment. Pick the reader for their platform — `pbpaste` on
|
|
35
|
+
macOS, `wl-paste` or `xclip -o -selection clipboard` on Linux,
|
|
36
|
+
`powershell.exe -NoProfile -Command Get-Clipboard` on Windows:
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
a correct one from everywhere else, and this summary is the only place the mistake is
|
|
42
|
-
visible.
|
|
38
|
+
```
|
|
39
|
+
pbpaste | ${CLAUDE_PLUGIN_ROOT}/dist/bin/enroll.mjs --apiKey=-
|
|
40
|
+
```
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
terminal, outside this session:
|
|
42
|
+
Add `--apiBaseUrl=<url>` if they are on a Darkhunt other than `api.darkhunt.ai`.
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
darkhunt-cli enroll --api-key <key>
|
|
49
|
-
```
|
|
44
|
+
3. React to the exit code:
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
- **0** — done. Show exactly what it printed: endpoint, tenant, workspace,
|
|
47
|
+
application. Ask them to confirm that is where they expect their sessions to go. A
|
|
48
|
+
config aimed at the wrong tenant looks identical to a correct one everywhere else,
|
|
49
|
+
and this summary is the only place the mistake is visible.
|
|
55
50
|
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
- **2** — the key reaches more than one tenant, workspace or application, and the
|
|
52
|
+
options are listed. Show them by name, ask which, then re-run the same piped command
|
|
53
|
+
with the chosen id added (`--tenantId=`, `--workspaceId=` or `--applicationId=`),
|
|
54
|
+
keeping any already settled. Up to three rounds.
|
|
60
55
|
|
|
61
|
-
- **
|
|
62
|
-
|
|
56
|
+
- **1 with "stdin was empty"** — the clipboard did not hold the key. Ask them to copy
|
|
57
|
+
it again, then retry. Do not ask them to paste it here instead.
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
- **1 with "rejected"** — the clipboard held something that is not a valid key. Same
|
|
60
|
+
remedy.
|
|
61
|
+
|
|
62
|
+
- **3** — no key was found at all, which should not happen on this path. Do not ask
|
|
63
|
+
for the key here. Send them to the dashboard instead: the application's Connect step
|
|
64
|
+
renders a ready-made block that writes `~/.darkhunt/endpoint-config.json` with every
|
|
65
|
+
value already filled in, to paste into their own terminal. That block writes
|
|
66
|
+
credentials only, so it must be followed by:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
${CLAUDE_PLUGIN_ROOT}/dist/bin/install-hooks.mjs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then re-run the status command above to confirm both halves took.
|
|
73
|
+
|
|
74
|
+
4. Tell them capture begins on their **next** Codex CLI session — hooks load when a
|
|
75
|
+
session starts — and that `/darkhunt-guard:status` shows whether it is working.
|
|
76
|
+
|
|
77
|
+
If the status output reports a launcher problem, run
|
|
78
|
+
`${CLAUDE_PLUGIN_ROOT}/dist/bin/install-hooks.mjs` and read what it prints. Codex
|
|
79
|
+
resolves this plugin's hook commands by PATH name, so a machine with correct
|
|
80
|
+
credentials and no launchers on `PATH` is configured and capturing nothing.
|
|
67
81
|
|
|
68
82
|
## Also
|
|
69
83
|
|
|
70
|
-
- Do not edit `~/.darkhunt/endpoint-config.json` by hand
|
|
71
|
-
and a
|
|
84
|
+
- Do not edit `~/.darkhunt/endpoint-config.json` by hand. Enrollment writes it at 0600
|
|
85
|
+
and a looser mode is refused at load.
|
|
72
86
|
- Enforcement ships **off**. Setup does not turn it on, and you should not offer to.
|
package/commands/status.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Is Darkhunt capture actually working? Shows routing, hook activity and shipping lag
|
|
3
|
-
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*)
|
|
2
|
+
description: Is Darkhunt capture actually working? Shows routing, launchers, hook activity and shipping lag
|
|
3
|
+
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/install-hooks.mjs:*)
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
!`${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs 2>&1 || true`
|
|
@@ -15,6 +15,13 @@ What the fields mean, and what to say:
|
|
|
15
15
|
`/darkhunt-guard:setup`. Nothing is being captured, and nothing is being blocked.
|
|
16
16
|
- **tenant / workspace / application** — where sessions are being sent. Worth reading
|
|
17
17
|
aloud: a wrong destination looks exactly like a right one everywhere else.
|
|
18
|
+
- **launchers PROBLEM** — the failure with no other symptom. Codex resolves this
|
|
19
|
+
plugin's hook commands by PATH name (`darkhunt-codex-spool`, `darkhunt-codex-guard`),
|
|
20
|
+
so a machine whose config is perfect but whose launchers are missing, stale, or in a
|
|
21
|
+
directory that is not on `PATH` captures nothing and reports nothing wrong anywhere
|
|
22
|
+
else. Missing or stale is fixed by running
|
|
23
|
+
`${CLAUDE_PLUGIN_ROOT}/dist/bin/install-hooks.mjs`; a `PATH` problem is theirs to fix
|
|
24
|
+
in their shell profile, and the line says which it is.
|
|
18
25
|
- **user … [from agent]** — traces are attributed to the signed-in Codex CLI account.
|
|
19
26
|
`[from config]` means a value someone typed in, which on a shared machine is likely to
|
|
20
27
|
be the wrong person.
|
package/dist/bin/backfill.mjs
CHANGED
|
@@ -18295,6 +18295,7 @@ function summarize(output) {
|
|
|
18295
18295
|
return text;
|
|
18296
18296
|
return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
|
|
18297
18297
|
}
|
|
18298
|
+
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18298
18299
|
var SessionEmitter = class {
|
|
18299
18300
|
client;
|
|
18300
18301
|
options;
|
|
@@ -18321,10 +18322,26 @@ var SessionEmitter = class {
|
|
|
18321
18322
|
constructor(client, options) {
|
|
18322
18323
|
this.client = client;
|
|
18323
18324
|
this.options = options;
|
|
18325
|
+
if (options.resumeTurn) {
|
|
18326
|
+
this.lastUserText = options.resumeTurn.userText;
|
|
18327
|
+
this.userTurnOpen = options.resumeTurn.open;
|
|
18328
|
+
}
|
|
18324
18329
|
}
|
|
18325
18330
|
get recordCount() {
|
|
18326
18331
|
return this.count;
|
|
18327
18332
|
}
|
|
18333
|
+
/**
|
|
18334
|
+
* The turn still in flight, for the forwarder to hand to the next pass. Read after
|
|
18335
|
+
* `finish()` — a turn is only settled once the pass has ended everything it opened.
|
|
18336
|
+
*/
|
|
18337
|
+
get turn() {
|
|
18338
|
+
const text = this.lastUserText;
|
|
18339
|
+
const carried = text !== void 0 && text.length > RESUMED_USER_TEXT_LIMIT ? `${text.slice(0, RESUMED_USER_TEXT_LIMIT)}\u2026 [truncated, ${text.length} chars]` : text;
|
|
18340
|
+
return {
|
|
18341
|
+
...carried !== void 0 ? { userText: carried } : {},
|
|
18342
|
+
open: this.userTurnOpen
|
|
18343
|
+
};
|
|
18344
|
+
}
|
|
18328
18345
|
/**
|
|
18329
18346
|
* Create a span under a seed derived from the record. Scoped by session id, so two
|
|
18330
18347
|
* sessions sharing a record uuid cannot collide, and by a discriminator, so the
|
|
@@ -18580,6 +18597,11 @@ function resumeOffset(checkpoints, path) {
|
|
|
18580
18597
|
return 0;
|
|
18581
18598
|
return previous.offset;
|
|
18582
18599
|
}
|
|
18600
|
+
function resumeTurn(checkpoints, path, from) {
|
|
18601
|
+
if (from <= 0)
|
|
18602
|
+
return void 0;
|
|
18603
|
+
return checkpoints[path]?.turn;
|
|
18604
|
+
}
|
|
18583
18605
|
|
|
18584
18606
|
// packages/core/dist/forwarder/tail.js
|
|
18585
18607
|
import { closeSync, openSync, readSync, statSync as statSync3 } from "node:fs";
|
|
@@ -18737,7 +18759,7 @@ function saveHealth(scope, health) {
|
|
|
18737
18759
|
}
|
|
18738
18760
|
|
|
18739
18761
|
// packages/core/dist/forwarder/run.js
|
|
18740
|
-
import { readdirSync as readdirSync2 } from "node:fs";
|
|
18762
|
+
import { readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
18741
18763
|
|
|
18742
18764
|
// node_modules/@darkhunt-security/telemetry/dist/client.js
|
|
18743
18765
|
init_esm();
|
|
@@ -21597,12 +21619,14 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21597
21619
|
for (const path of paths2) {
|
|
21598
21620
|
try {
|
|
21599
21621
|
const from = resumeOffset(checkpoints, path);
|
|
21622
|
+
const carried = resumeTurn(checkpoints, path, from);
|
|
21600
21623
|
const emitter = new SessionEmitter(client, {
|
|
21601
21624
|
sessionId: mapper.sessionIdFor(path),
|
|
21602
21625
|
vendor: mapper.vendor,
|
|
21603
21626
|
passKey: String(from),
|
|
21604
21627
|
...ids !== void 0 ? { ids } : {},
|
|
21605
|
-
...userId !== void 0 ? { userId } : {}
|
|
21628
|
+
...userId !== void 0 ? { userId } : {},
|
|
21629
|
+
...carried !== void 0 ? { resumeTurn: carried } : {}
|
|
21606
21630
|
});
|
|
21607
21631
|
let cursor = from;
|
|
21608
21632
|
let reached = from;
|
|
@@ -21628,7 +21652,12 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21628
21652
|
if (ingested === 0)
|
|
21629
21653
|
continue;
|
|
21630
21654
|
await emitter.finish();
|
|
21631
|
-
|
|
21655
|
+
const turn = emitter.turn;
|
|
21656
|
+
staged[path] = {
|
|
21657
|
+
offset: reached,
|
|
21658
|
+
size: fileSize,
|
|
21659
|
+
...turn.userText !== void 0 || turn.open ? { turn } : {}
|
|
21660
|
+
};
|
|
21632
21661
|
result2.transcripts++;
|
|
21633
21662
|
result2.records += emitter.recordCount;
|
|
21634
21663
|
if (watch.failures() > 0)
|
|
@@ -21665,6 +21694,19 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21665
21694
|
release();
|
|
21666
21695
|
}
|
|
21667
21696
|
}
|
|
21697
|
+
function withinDays(paths2, days) {
|
|
21698
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
21699
|
+
throw new RangeError(`--days must be a positive number of days, got ${days}`);
|
|
21700
|
+
}
|
|
21701
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
21702
|
+
return paths2.filter((path) => {
|
|
21703
|
+
try {
|
|
21704
|
+
return statSync4(path).mtimeMs >= cutoff;
|
|
21705
|
+
} catch {
|
|
21706
|
+
return true;
|
|
21707
|
+
}
|
|
21708
|
+
});
|
|
21709
|
+
}
|
|
21668
21710
|
function discoverTranscripts(mapper) {
|
|
21669
21711
|
const found = [];
|
|
21670
21712
|
const walk = (dir) => {
|
|
@@ -21843,10 +21885,21 @@ var flag = (n) => {
|
|
|
21843
21885
|
};
|
|
21844
21886
|
var profile = flag("profile");
|
|
21845
21887
|
var credentialsPath2 = flag("credentials");
|
|
21846
|
-
var
|
|
21888
|
+
var sinceDays;
|
|
21889
|
+
if (flag("days") !== void 0) {
|
|
21890
|
+
sinceDays = Number(flag("days"));
|
|
21891
|
+
if (!Number.isFinite(sinceDays) || sinceDays <= 0) {
|
|
21892
|
+
process.stderr.write(`darkhunt: --days must be a positive number, got '${flag("days")}'
|
|
21893
|
+
`);
|
|
21894
|
+
process.exit(1);
|
|
21895
|
+
}
|
|
21896
|
+
}
|
|
21897
|
+
var discovered = explicit.length > 0 ? explicit : discoverTranscripts(codexTranscript);
|
|
21898
|
+
var paths = sinceDays === void 0 ? discovered : withinDays(discovered, sinceDays);
|
|
21899
|
+
var window2 = sinceDays === void 0 ? "" : ` written to in the last ${sinceDays} day(s)`;
|
|
21847
21900
|
var say = (m) => process.stderr.write(`darkhunt: ${m}
|
|
21848
21901
|
`);
|
|
21849
|
-
say(`${dryRun ? "DRY RUN \u2014 nothing will be sent. " : ""}${paths.length} transcript(s)`);
|
|
21902
|
+
say(`${dryRun ? "DRY RUN \u2014 nothing will be sent. " : ""}${paths.length} transcript(s)${window2}`);
|
|
21850
21903
|
var result = await runForwarder(codexTranscript, { paths, dryRun, profile, credentialsPath: credentialsPath2 });
|
|
21851
21904
|
say(
|
|
21852
21905
|
`${dryRun ? "mapped" : "shipped"} ${result.records} record(s) from ${result.transcripts} transcript(s)`
|
package/dist/bin/enroll.mjs
CHANGED
|
@@ -52,6 +52,14 @@ var SPOOL_DIR = join4(CONFIG_DIR, "spool");
|
|
|
52
52
|
// packages/core/dist/runtime/beat.js
|
|
53
53
|
var BEAT_LIVE_MS = 30 * 60 * 1e3;
|
|
54
54
|
|
|
55
|
+
// packages/core/dist/runtime/guard.js
|
|
56
|
+
async function readStdin(stream) {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
for await (const chunk of stream)
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
// packages/core/dist/forwarder/tail.js
|
|
56
64
|
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
57
65
|
var LINE_CEILING = 64 * 1024 * 1024;
|
|
@@ -998,18 +1006,18 @@ function init(argv) {
|
|
|
998
1006
|
const cfgPath = configPath();
|
|
999
1007
|
const existingConfig = readJson(cfgPath) ?? {};
|
|
1000
1008
|
const existingCreds = separateFile ? readJson(credsPath) : existingConfig;
|
|
1001
|
-
const
|
|
1009
|
+
const apiKey2 = flags["apiKey"] ?? process.env["DARKHUNT_API_KEY"] ?? existingCreds?.apiKey;
|
|
1002
1010
|
const tenantId = flags["tenantId"] ?? process.env["DARKHUNT_TENANT_ID"] ?? existingCreds?.tenantId;
|
|
1003
1011
|
const apiBaseUrl = flags["apiBaseUrl"] ?? process.env["DARKHUNT_API_BASE_URL"] ?? existingCreds?.apiBaseUrl ?? "https://api.darkhunt.ai";
|
|
1004
1012
|
const workspaceId = flags["workspaceId"] ?? process.env["DARKHUNT_WORKSPACE_ID"] ?? existingCreds?.workspaceId;
|
|
1005
1013
|
const applicationId = flags["applicationId"] ?? process.env["DARKHUNT_APPLICATION_ID"] ?? existingCreds?.applicationId;
|
|
1006
1014
|
const userId = flags["userId"] ?? existingCreds?.userId;
|
|
1007
|
-
assertPresent("apiKey",
|
|
1015
|
+
assertPresent("apiKey", apiKey2, "DARKHUNT_API_KEY");
|
|
1008
1016
|
assertPresent("tenantId", tenantId, "DARKHUNT_TENANT_ID");
|
|
1009
1017
|
assertPresent("workspaceId", workspaceId, "DARKHUNT_WORKSPACE_ID");
|
|
1010
1018
|
assertPresent("applicationId", applicationId, "DARKHUNT_APPLICATION_ID");
|
|
1011
1019
|
const credentials = {
|
|
1012
|
-
apiKey,
|
|
1020
|
+
apiKey: apiKey2,
|
|
1013
1021
|
apiBaseUrl,
|
|
1014
1022
|
tenantId,
|
|
1015
1023
|
workspaceId,
|
|
@@ -1115,7 +1123,8 @@ function resolveKey(explicit, cliCredentialsPath) {
|
|
|
1115
1123
|
return {
|
|
1116
1124
|
apiKey: cli.apiKey,
|
|
1117
1125
|
source: "darkhunt-cli",
|
|
1118
|
-
...typeof cli.apiBaseUrl === "string" ? { apiBaseUrl: cli.apiBaseUrl } : {}
|
|
1126
|
+
...typeof cli.apiBaseUrl === "string" ? { apiBaseUrl: cli.apiBaseUrl } : {},
|
|
1127
|
+
...typeof cli.tenantId === "string" && cli.tenantId !== "" ? { tenantId: cli.tenantId } : {}
|
|
1119
1128
|
};
|
|
1120
1129
|
}
|
|
1121
1130
|
} catch {
|
|
@@ -1124,10 +1133,10 @@ function resolveKey(explicit, cliCredentialsPath) {
|
|
|
1124
1133
|
}
|
|
1125
1134
|
var MAX_LISTED = 15;
|
|
1126
1135
|
var DEFAULT_API_BASE_URL = "https://api.darkhunt.ai";
|
|
1127
|
-
async function get(base,
|
|
1136
|
+
async function get(base, apiKey2, path) {
|
|
1128
1137
|
let res;
|
|
1129
1138
|
try {
|
|
1130
|
-
res = await fetch(`${base}${path}`, { headers: { Authorization: `Bearer ${
|
|
1139
|
+
res = await fetch(`${base}${path}`, { headers: { Authorization: `Bearer ${apiKey2}` } });
|
|
1131
1140
|
} catch (err) {
|
|
1132
1141
|
throw new EnrollError(`cannot reach ${base}: ${err.message}`);
|
|
1133
1142
|
}
|
|
@@ -1164,18 +1173,54 @@ function resolve2(kind, options, chosen) {
|
|
|
1164
1173
|
throw new AmbiguousError(kind, options);
|
|
1165
1174
|
}
|
|
1166
1175
|
var CLI_CREDENTIALS_PATH = join5(homedir3(), ".darkhunt", "credentials.json");
|
|
1176
|
+
function idsFromEnv() {
|
|
1177
|
+
const read = (name) => {
|
|
1178
|
+
const value = process.env[name];
|
|
1179
|
+
return value !== void 0 && value.trim() !== "" ? value : void 0;
|
|
1180
|
+
};
|
|
1181
|
+
const tenantId = read("DARKHUNT_TENANT_ID");
|
|
1182
|
+
const workspaceId = read("DARKHUNT_WORKSPACE_ID");
|
|
1183
|
+
const applicationId = read("DARKHUNT_APPLICATION_ID");
|
|
1184
|
+
return {
|
|
1185
|
+
...tenantId !== void 0 ? { tenantId } : {},
|
|
1186
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
1187
|
+
...applicationId !== void 0 ? { applicationId } : {}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
async function accessibleTenants(base, apiKey2, candidates, recorded) {
|
|
1191
|
+
const known = candidates.find((c) => c.id === recorded);
|
|
1192
|
+
if (known)
|
|
1193
|
+
return [known];
|
|
1194
|
+
if (candidates.length <= 1)
|
|
1195
|
+
return candidates;
|
|
1196
|
+
const reachable = await Promise.all(candidates.map(async (c) => {
|
|
1197
|
+
try {
|
|
1198
|
+
await get(base, apiKey2, `/workflow-manager/api/t/${c.id}/workspaces`);
|
|
1199
|
+
return c;
|
|
1200
|
+
} catch {
|
|
1201
|
+
return void 0;
|
|
1202
|
+
}
|
|
1203
|
+
}));
|
|
1204
|
+
const usable = reachable.filter((c) => c !== void 0);
|
|
1205
|
+
return usable.length > 0 ? usable : candidates;
|
|
1206
|
+
}
|
|
1167
1207
|
async function enroll(options) {
|
|
1208
|
+
const env = idsFromEnv();
|
|
1209
|
+
const chosen = {
|
|
1210
|
+
...env,
|
|
1211
|
+
...Object.fromEntries(Object.entries(options).filter(([, v]) => v !== void 0))
|
|
1212
|
+
};
|
|
1168
1213
|
const key = resolveKey(options.apiKey, CLI_CREDENTIALS_PATH);
|
|
1169
|
-
const { apiKey } = key;
|
|
1214
|
+
const { apiKey: apiKey2 } = key;
|
|
1170
1215
|
const base = (options.apiBaseUrl ?? key.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
1171
|
-
const me = await get(base,
|
|
1172
|
-
const tenantId = resolve2("tenant", toChoices(list(me.tenants)),
|
|
1173
|
-
const workspaces = toChoices(list(await get(base,
|
|
1174
|
-
const workspaceId = resolve2("workspace", workspaces,
|
|
1175
|
-
const applications = toChoices(list(await get(base,
|
|
1176
|
-
const applicationId = resolve2("application", applications,
|
|
1216
|
+
const me = await get(base, apiKey2, "/user-management/api/me");
|
|
1217
|
+
const tenantId = resolve2("tenant", await accessibleTenants(base, apiKey2, toChoices(list(me.tenants)), key.tenantId), chosen.tenantId ?? key.tenantId);
|
|
1218
|
+
const workspaces = toChoices(list(await get(base, apiKey2, `/workflow-manager/api/t/${tenantId}/workspaces`)));
|
|
1219
|
+
const workspaceId = resolve2("workspace", workspaces, chosen.workspaceId);
|
|
1220
|
+
const applications = toChoices(list(await get(base, apiKey2, `/workflow-manager/api/t/${tenantId}/workspaces/${workspaceId}/applications`)));
|
|
1221
|
+
const applicationId = resolve2("application", applications, chosen.applicationId);
|
|
1177
1222
|
const result = init([
|
|
1178
|
-
`--apiKey=${
|
|
1223
|
+
`--apiKey=${apiKey2}`,
|
|
1179
1224
|
`--apiBaseUrl=${base}`,
|
|
1180
1225
|
`--tenantId=${tenantId}`,
|
|
1181
1226
|
`--workspaceId=${workspaceId}`,
|
|
@@ -1193,14 +1238,92 @@ async function enroll(options) {
|
|
|
1193
1238
|
};
|
|
1194
1239
|
}
|
|
1195
1240
|
|
|
1241
|
+
// adapters/codex/bin/lib/launchers.mjs
|
|
1242
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1243
|
+
import { homedir as homedir4 } from "node:os";
|
|
1244
|
+
import { join as join6 } from "node:path";
|
|
1245
|
+
var LAUNCHERS = {
|
|
1246
|
+
"darkhunt-codex-spool": "spool.mjs",
|
|
1247
|
+
"darkhunt-codex-guard": "guard.mjs"
|
|
1248
|
+
};
|
|
1249
|
+
var BIN_DIR = join6(homedir4(), ".local", "bin");
|
|
1250
|
+
var script = (target) => `#!/bin/sh
|
|
1251
|
+
# Installed by darkhunt endpoint-plugins. Resolves the newest installed
|
|
1252
|
+
# plugin version at run time; do not hard-code a version here.
|
|
1253
|
+
D=$(ls -d "$HOME"/.codex/plugins/cache/*/darkhunt-guard/*/dist/bin 2>/dev/null | sort -V | tail -1)
|
|
1254
|
+
[ -n "$D" ] || exit 0
|
|
1255
|
+
exec "$D/${target}" "$@"
|
|
1256
|
+
`;
|
|
1257
|
+
function installLaunchers(binDir = BIN_DIR) {
|
|
1258
|
+
mkdirSync2(binDir, { recursive: true });
|
|
1259
|
+
const installed = [];
|
|
1260
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1261
|
+
const path = join6(binDir, name);
|
|
1262
|
+
writeFileSync2(path, script(target), { mode: 493 });
|
|
1263
|
+
chmodSync2(path, 493);
|
|
1264
|
+
installed.push(path);
|
|
1265
|
+
}
|
|
1266
|
+
return installed;
|
|
1267
|
+
}
|
|
1268
|
+
function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
1269
|
+
const missing = [];
|
|
1270
|
+
const stale = [];
|
|
1271
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1272
|
+
const file = join6(binDir, name);
|
|
1273
|
+
if (!existsSync2(file)) {
|
|
1274
|
+
missing.push(name);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
let body = "";
|
|
1278
|
+
try {
|
|
1279
|
+
body = readFileSync3(file, "utf8");
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
if (body !== script(target)) stale.push(name);
|
|
1283
|
+
}
|
|
1284
|
+
return { binDir, missing, stale, onPath: path.split(":").includes(binDir) };
|
|
1285
|
+
}
|
|
1286
|
+
function launcherWarnings(state) {
|
|
1287
|
+
const lines = [];
|
|
1288
|
+
if (state.missing.length > 0) {
|
|
1289
|
+
lines.push(
|
|
1290
|
+
`hook launchers missing: ${state.missing.join(", ")} \u2014 hooks cannot run.`,
|
|
1291
|
+
` install them: install-hooks.mjs`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
if (state.stale.length > 0) {
|
|
1295
|
+
lines.push(
|
|
1296
|
+
`hook launchers are not the ones this version writes: ${state.stale.join(", ")}.`,
|
|
1297
|
+
` refresh them: install-hooks.mjs`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
if (!state.onPath) {
|
|
1301
|
+
lines.push(
|
|
1302
|
+
`${state.binDir} is not on PATH \u2014 hooks will not run.`,
|
|
1303
|
+
` add it: export PATH="${state.binDir}:$PATH" in your shell profile, then open a new terminal.`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return lines;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1196
1309
|
// adapters/codex/bin/enroll.mjs
|
|
1197
1310
|
var flag = (name) => {
|
|
1198
1311
|
const arg = process.argv.slice(2).find((a) => a.startsWith(`--${name}=`));
|
|
1199
1312
|
return arg ? arg.slice(name.length + 3) : void 0;
|
|
1200
1313
|
};
|
|
1314
|
+
var apiKey = flag("apiKey");
|
|
1315
|
+
if (apiKey === "-") {
|
|
1316
|
+
apiKey = (await readStdin(process.stdin)).trim();
|
|
1317
|
+
if (apiKey === "") {
|
|
1318
|
+
process.stderr.write(
|
|
1319
|
+
"darkhunt: --apiKey=- was given but stdin was empty.\n Copy your API key first, then pipe it in \u2014 e.g. `pbpaste | enroll.mjs --apiKey=-`.\n"
|
|
1320
|
+
);
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1201
1324
|
try {
|
|
1202
1325
|
const r = await enroll({
|
|
1203
|
-
...
|
|
1326
|
+
...apiKey !== void 0 ? { apiKey } : {},
|
|
1204
1327
|
...flag("apiBaseUrl") !== void 0 ? { apiBaseUrl: flag("apiBaseUrl") } : {},
|
|
1205
1328
|
...flag("tenantId") !== void 0 ? { tenantId: flag("tenantId") } : {},
|
|
1206
1329
|
...flag("workspaceId") !== void 0 ? { workspaceId: flag("workspaceId") } : {},
|
|
@@ -1211,6 +1334,7 @@ try {
|
|
|
1211
1334
|
environment: "$DARKHUNT_API_KEY",
|
|
1212
1335
|
"darkhunt-cli": "the key darkhunt-cli enrolled"
|
|
1213
1336
|
};
|
|
1337
|
+
const launchers = installLaunchers();
|
|
1214
1338
|
process.stdout.write(
|
|
1215
1339
|
`darkhunt: enrolled${r.email ? ` as ${r.email}` : ""} using ${KEY_SOURCE[r.keySource]}
|
|
1216
1340
|
endpoint ${r.apiBaseUrl}
|
|
@@ -1218,9 +1342,16 @@ try {
|
|
|
1218
1342
|
workspace ${r.workspaceId}
|
|
1219
1343
|
application ${r.applicationId}
|
|
1220
1344
|
config ${r.configPath} (0600)
|
|
1221
|
-
|
|
1345
|
+
` + launchers.map((p) => ` launcher ${p}
|
|
1346
|
+
`).join("") + ` capture: on enforce: off
|
|
1222
1347
|
`
|
|
1223
1348
|
);
|
|
1349
|
+
const warnings = launcherWarnings(launcherState());
|
|
1350
|
+
if (warnings.length > 0) {
|
|
1351
|
+
process.stdout.write(`
|
|
1352
|
+
${warnings.map((l) => `darkhunt: WARNING ${l}
|
|
1353
|
+
`).join("")}`);
|
|
1354
|
+
}
|
|
1224
1355
|
} catch (err) {
|
|
1225
1356
|
if (err instanceof MissingKeyError) {
|
|
1226
1357
|
process.stderr.write(
|
package/dist/bin/forwarder.mjs
CHANGED
|
@@ -18295,6 +18295,7 @@ function summarize(output) {
|
|
|
18295
18295
|
return text;
|
|
18296
18296
|
return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
|
|
18297
18297
|
}
|
|
18298
|
+
var RESUMED_USER_TEXT_LIMIT = 4e3;
|
|
18298
18299
|
var SessionEmitter = class {
|
|
18299
18300
|
client;
|
|
18300
18301
|
options;
|
|
@@ -18321,10 +18322,26 @@ var SessionEmitter = class {
|
|
|
18321
18322
|
constructor(client, options) {
|
|
18322
18323
|
this.client = client;
|
|
18323
18324
|
this.options = options;
|
|
18325
|
+
if (options.resumeTurn) {
|
|
18326
|
+
this.lastUserText = options.resumeTurn.userText;
|
|
18327
|
+
this.userTurnOpen = options.resumeTurn.open;
|
|
18328
|
+
}
|
|
18324
18329
|
}
|
|
18325
18330
|
get recordCount() {
|
|
18326
18331
|
return this.count;
|
|
18327
18332
|
}
|
|
18333
|
+
/**
|
|
18334
|
+
* The turn still in flight, for the forwarder to hand to the next pass. Read after
|
|
18335
|
+
* `finish()` — a turn is only settled once the pass has ended everything it opened.
|
|
18336
|
+
*/
|
|
18337
|
+
get turn() {
|
|
18338
|
+
const text = this.lastUserText;
|
|
18339
|
+
const carried = text !== void 0 && text.length > RESUMED_USER_TEXT_LIMIT ? `${text.slice(0, RESUMED_USER_TEXT_LIMIT)}\u2026 [truncated, ${text.length} chars]` : text;
|
|
18340
|
+
return {
|
|
18341
|
+
...carried !== void 0 ? { userText: carried } : {},
|
|
18342
|
+
open: this.userTurnOpen
|
|
18343
|
+
};
|
|
18344
|
+
}
|
|
18328
18345
|
/**
|
|
18329
18346
|
* Create a span under a seed derived from the record. Scoped by session id, so two
|
|
18330
18347
|
* sessions sharing a record uuid cannot collide, and by a discriminator, so the
|
|
@@ -18580,6 +18597,11 @@ function resumeOffset(checkpoints, path) {
|
|
|
18580
18597
|
return 0;
|
|
18581
18598
|
return previous.offset;
|
|
18582
18599
|
}
|
|
18600
|
+
function resumeTurn(checkpoints, path, from) {
|
|
18601
|
+
if (from <= 0)
|
|
18602
|
+
return void 0;
|
|
18603
|
+
return checkpoints[path]?.turn;
|
|
18604
|
+
}
|
|
18583
18605
|
|
|
18584
18606
|
// packages/core/dist/forwarder/tail.js
|
|
18585
18607
|
import { closeSync, openSync, readSync, statSync as statSync3 } from "node:fs";
|
|
@@ -18737,7 +18759,7 @@ function saveHealth(scope, health) {
|
|
|
18737
18759
|
}
|
|
18738
18760
|
|
|
18739
18761
|
// packages/core/dist/forwarder/run.js
|
|
18740
|
-
import { readdirSync as readdirSync2 } from "node:fs";
|
|
18762
|
+
import { readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
18741
18763
|
|
|
18742
18764
|
// node_modules/@darkhunt-security/telemetry/dist/client.js
|
|
18743
18765
|
init_esm();
|
|
@@ -21597,12 +21619,14 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21597
21619
|
for (const path of paths) {
|
|
21598
21620
|
try {
|
|
21599
21621
|
const from = resumeOffset(checkpoints, path);
|
|
21622
|
+
const carried = resumeTurn(checkpoints, path, from);
|
|
21600
21623
|
const emitter = new SessionEmitter(client, {
|
|
21601
21624
|
sessionId: mapper.sessionIdFor(path),
|
|
21602
21625
|
vendor: mapper.vendor,
|
|
21603
21626
|
passKey: String(from),
|
|
21604
21627
|
...ids !== void 0 ? { ids } : {},
|
|
21605
|
-
...userId !== void 0 ? { userId } : {}
|
|
21628
|
+
...userId !== void 0 ? { userId } : {},
|
|
21629
|
+
...carried !== void 0 ? { resumeTurn: carried } : {}
|
|
21606
21630
|
});
|
|
21607
21631
|
let cursor = from;
|
|
21608
21632
|
let reached = from;
|
|
@@ -21628,7 +21652,12 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21628
21652
|
if (ingested === 0)
|
|
21629
21653
|
continue;
|
|
21630
21654
|
await emitter.finish();
|
|
21631
|
-
|
|
21655
|
+
const turn = emitter.turn;
|
|
21656
|
+
staged[path] = {
|
|
21657
|
+
offset: reached,
|
|
21658
|
+
size: fileSize,
|
|
21659
|
+
...turn.userText !== void 0 || turn.open ? { turn } : {}
|
|
21660
|
+
};
|
|
21632
21661
|
result.transcripts++;
|
|
21633
21662
|
result.records += emitter.recordCount;
|
|
21634
21663
|
if (watch.failures() > 0)
|
package/dist/bin/init.mjs
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
import { createRequire as __dhCreateRequire } from 'node:module';
|
|
3
3
|
const require = __dhCreateRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
-
// adapters/codex/bin/init.mjs
|
|
6
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7
|
-
import { homedir as homedir4 } from "node:os";
|
|
8
|
-
import { join as join6 } from "node:path";
|
|
9
|
-
|
|
10
5
|
// packages/core/dist/config/local.js
|
|
11
6
|
import { join as join3 } from "node:path";
|
|
12
7
|
|
|
@@ -1083,30 +1078,75 @@ import { homedir as homedir3 } from "node:os";
|
|
|
1083
1078
|
import { join as join5 } from "node:path";
|
|
1084
1079
|
var CLI_CREDENTIALS_PATH = join5(homedir3(), ".darkhunt", "credentials.json");
|
|
1085
1080
|
|
|
1086
|
-
// adapters/codex/bin/
|
|
1087
|
-
|
|
1081
|
+
// adapters/codex/bin/lib/launchers.mjs
|
|
1082
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1083
|
+
import { homedir as homedir4 } from "node:os";
|
|
1084
|
+
import { join as join6 } from "node:path";
|
|
1085
|
+
var LAUNCHERS = {
|
|
1086
|
+
"darkhunt-codex-spool": "spool.mjs",
|
|
1087
|
+
"darkhunt-codex-guard": "guard.mjs"
|
|
1088
|
+
};
|
|
1088
1089
|
var BIN_DIR = join6(homedir4(), ".local", "bin");
|
|
1089
|
-
|
|
1090
|
-
mkdirSync2(BIN_DIR, { recursive: true });
|
|
1091
|
-
const installed = [];
|
|
1092
|
-
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1093
|
-
const path = join6(BIN_DIR, name);
|
|
1094
|
-
writeFileSync2(
|
|
1095
|
-
path,
|
|
1096
|
-
`#!/bin/sh
|
|
1090
|
+
var script = (target) => `#!/bin/sh
|
|
1097
1091
|
# Installed by darkhunt endpoint-plugins. Resolves the newest installed
|
|
1098
1092
|
# plugin version at run time; do not hard-code a version here.
|
|
1099
1093
|
D=$(ls -d "$HOME"/.codex/plugins/cache/*/darkhunt-guard/*/dist/bin 2>/dev/null | sort -V | tail -1)
|
|
1100
1094
|
[ -n "$D" ] || exit 0
|
|
1101
1095
|
exec "$D/${target}" "$@"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1096
|
+
`;
|
|
1097
|
+
function installLaunchers(binDir = BIN_DIR) {
|
|
1098
|
+
mkdirSync2(binDir, { recursive: true });
|
|
1099
|
+
const installed = [];
|
|
1100
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1101
|
+
const path = join6(binDir, name);
|
|
1102
|
+
writeFileSync2(path, script(target), { mode: 493 });
|
|
1105
1103
|
chmodSync2(path, 493);
|
|
1106
1104
|
installed.push(path);
|
|
1107
1105
|
}
|
|
1108
1106
|
return installed;
|
|
1109
1107
|
}
|
|
1108
|
+
function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
1109
|
+
const missing = [];
|
|
1110
|
+
const stale = [];
|
|
1111
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1112
|
+
const file = join6(binDir, name);
|
|
1113
|
+
if (!existsSync2(file)) {
|
|
1114
|
+
missing.push(name);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
let body = "";
|
|
1118
|
+
try {
|
|
1119
|
+
body = readFileSync2(file, "utf8");
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
if (body !== script(target)) stale.push(name);
|
|
1123
|
+
}
|
|
1124
|
+
return { binDir, missing, stale, onPath: path.split(":").includes(binDir) };
|
|
1125
|
+
}
|
|
1126
|
+
function launcherWarnings(state) {
|
|
1127
|
+
const lines = [];
|
|
1128
|
+
if (state.missing.length > 0) {
|
|
1129
|
+
lines.push(
|
|
1130
|
+
`hook launchers missing: ${state.missing.join(", ")} \u2014 hooks cannot run.`,
|
|
1131
|
+
` install them: install-hooks.mjs`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
if (state.stale.length > 0) {
|
|
1135
|
+
lines.push(
|
|
1136
|
+
`hook launchers are not the ones this version writes: ${state.stale.join(", ")}.`,
|
|
1137
|
+
` refresh them: install-hooks.mjs`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if (!state.onPath) {
|
|
1141
|
+
lines.push(
|
|
1142
|
+
`${state.binDir} is not on PATH \u2014 hooks will not run.`,
|
|
1143
|
+
` add it: export PATH="${state.binDir}:$PATH" in your shell profile, then open a new terminal.`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return lines;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// adapters/codex/bin/init.mjs
|
|
1110
1150
|
try {
|
|
1111
1151
|
const r = init(process.argv.slice(2));
|
|
1112
1152
|
const launchers = installLaunchers();
|
|
@@ -1118,10 +1158,11 @@ try {
|
|
|
1118
1158
|
`).join("") + ` capture: on enforce: off
|
|
1119
1159
|
`
|
|
1120
1160
|
);
|
|
1121
|
-
|
|
1161
|
+
const warnings = launcherWarnings(launcherState());
|
|
1162
|
+
if (warnings.length > 0) {
|
|
1122
1163
|
process.stdout.write(`
|
|
1123
|
-
darkhunt: WARNING ${
|
|
1124
|
-
`);
|
|
1164
|
+
${warnings.map((l) => `darkhunt: WARNING ${l}
|
|
1165
|
+
`).join("")}`);
|
|
1125
1166
|
}
|
|
1126
1167
|
} catch (err) {
|
|
1127
1168
|
process.stderr.write(`darkhunt: ${err.message}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __dhCreateRequire } from 'node:module';
|
|
3
|
+
const require = __dhCreateRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// adapters/codex/bin/lib/launchers.mjs
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
var LAUNCHERS = {
|
|
10
|
+
"darkhunt-codex-spool": "spool.mjs",
|
|
11
|
+
"darkhunt-codex-guard": "guard.mjs"
|
|
12
|
+
};
|
|
13
|
+
var BIN_DIR = join(homedir(), ".local", "bin");
|
|
14
|
+
var script = (target) => `#!/bin/sh
|
|
15
|
+
# Installed by darkhunt endpoint-plugins. Resolves the newest installed
|
|
16
|
+
# plugin version at run time; do not hard-code a version here.
|
|
17
|
+
D=$(ls -d "$HOME"/.codex/plugins/cache/*/darkhunt-guard/*/dist/bin 2>/dev/null | sort -V | tail -1)
|
|
18
|
+
[ -n "$D" ] || exit 0
|
|
19
|
+
exec "$D/${target}" "$@"
|
|
20
|
+
`;
|
|
21
|
+
function installLaunchers(binDir = BIN_DIR) {
|
|
22
|
+
mkdirSync(binDir, { recursive: true });
|
|
23
|
+
const installed2 = [];
|
|
24
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
25
|
+
const path = join(binDir, name);
|
|
26
|
+
writeFileSync(path, script(target), { mode: 493 });
|
|
27
|
+
chmodSync(path, 493);
|
|
28
|
+
installed2.push(path);
|
|
29
|
+
}
|
|
30
|
+
return installed2;
|
|
31
|
+
}
|
|
32
|
+
function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
33
|
+
const missing = [];
|
|
34
|
+
const stale = [];
|
|
35
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
36
|
+
const file = join(binDir, name);
|
|
37
|
+
if (!existsSync(file)) {
|
|
38
|
+
missing.push(name);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let body = "";
|
|
42
|
+
try {
|
|
43
|
+
body = readFileSync(file, "utf8");
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
if (body !== script(target)) stale.push(name);
|
|
47
|
+
}
|
|
48
|
+
return { binDir, missing, stale, onPath: path.split(":").includes(binDir) };
|
|
49
|
+
}
|
|
50
|
+
function launcherWarnings(state) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
if (state.missing.length > 0) {
|
|
53
|
+
lines.push(
|
|
54
|
+
`hook launchers missing: ${state.missing.join(", ")} \u2014 hooks cannot run.`,
|
|
55
|
+
` install them: install-hooks.mjs`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (state.stale.length > 0) {
|
|
59
|
+
lines.push(
|
|
60
|
+
`hook launchers are not the ones this version writes: ${state.stale.join(", ")}.`,
|
|
61
|
+
` refresh them: install-hooks.mjs`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!state.onPath) {
|
|
65
|
+
lines.push(
|
|
66
|
+
`${state.binDir} is not on PATH \u2014 hooks will not run.`,
|
|
67
|
+
` add it: export PATH="${state.binDir}:$PATH" in your shell profile, then open a new terminal.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// adapters/codex/bin/install-hooks.mjs
|
|
74
|
+
var installed = installLaunchers();
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
`darkhunt: hook launchers installed
|
|
77
|
+
` + installed.map((p) => ` ${p}
|
|
78
|
+
`).join("")
|
|
79
|
+
);
|
|
80
|
+
var warnings = launcherWarnings(launcherState());
|
|
81
|
+
if (warnings.length > 0) {
|
|
82
|
+
process.stdout.write(`
|
|
83
|
+
${warnings.map((l) => `darkhunt: ${l}
|
|
84
|
+
`).join("")}`);
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`
|
|
87
|
+
capture starts on your NEXT Codex CLI session.
|
|
88
|
+
`);
|
package/dist/bin/status.mjs
CHANGED
|
@@ -1318,6 +1318,63 @@ var codexTranscript = {
|
|
|
1318
1318
|
}
|
|
1319
1319
|
};
|
|
1320
1320
|
|
|
1321
|
+
// adapters/codex/bin/lib/launchers.mjs
|
|
1322
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1323
|
+
import { homedir as homedir5 } from "node:os";
|
|
1324
|
+
import { join as join10 } from "node:path";
|
|
1325
|
+
var LAUNCHERS = {
|
|
1326
|
+
"darkhunt-codex-spool": "spool.mjs",
|
|
1327
|
+
"darkhunt-codex-guard": "guard.mjs"
|
|
1328
|
+
};
|
|
1329
|
+
var BIN_DIR = join10(homedir5(), ".local", "bin");
|
|
1330
|
+
var script = (target) => `#!/bin/sh
|
|
1331
|
+
# Installed by darkhunt endpoint-plugins. Resolves the newest installed
|
|
1332
|
+
# plugin version at run time; do not hard-code a version here.
|
|
1333
|
+
D=$(ls -d "$HOME"/.codex/plugins/cache/*/darkhunt-guard/*/dist/bin 2>/dev/null | sort -V | tail -1)
|
|
1334
|
+
[ -n "$D" ] || exit 0
|
|
1335
|
+
exec "$D/${target}" "$@"
|
|
1336
|
+
`;
|
|
1337
|
+
function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
|
|
1338
|
+
const missing = [];
|
|
1339
|
+
const stale = [];
|
|
1340
|
+
for (const [name, target] of Object.entries(LAUNCHERS)) {
|
|
1341
|
+
const file = join10(binDir, name);
|
|
1342
|
+
if (!existsSync3(file)) {
|
|
1343
|
+
missing.push(name);
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
let body = "";
|
|
1347
|
+
try {
|
|
1348
|
+
body = readFileSync6(file, "utf8");
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
if (body !== script(target)) stale.push(name);
|
|
1352
|
+
}
|
|
1353
|
+
return { binDir, missing, stale, onPath: path.split(":").includes(binDir) };
|
|
1354
|
+
}
|
|
1355
|
+
function launcherWarnings(state) {
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
if (state.missing.length > 0) {
|
|
1358
|
+
lines.push(
|
|
1359
|
+
`hook launchers missing: ${state.missing.join(", ")} \u2014 hooks cannot run.`,
|
|
1360
|
+
` install them: install-hooks.mjs`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (state.stale.length > 0) {
|
|
1364
|
+
lines.push(
|
|
1365
|
+
`hook launchers are not the ones this version writes: ${state.stale.join(", ")}.`,
|
|
1366
|
+
` refresh them: install-hooks.mjs`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
if (!state.onPath) {
|
|
1370
|
+
lines.push(
|
|
1371
|
+
`${state.binDir} is not on PATH \u2014 hooks will not run.`,
|
|
1372
|
+
` add it: export PATH="${state.binDir}:$PATH" in your shell profile, then open a new terminal.`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return lines;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1321
1378
|
// adapters/codex/bin/status.mjs
|
|
1322
1379
|
try {
|
|
1323
1380
|
const s = status("codex", codexTranscript);
|
|
@@ -1329,6 +1386,12 @@ try {
|
|
|
1329
1386
|
` user ${s.userId ?? "(none \u2014 traces are unattributed)"}` + (s.userIdSource === "none" ? "" : ` [from ${s.userIdSource}]`)
|
|
1330
1387
|
);
|
|
1331
1388
|
console.log(` capture ${s.capture ? "ON" : "OFF"} enforce ${s.enforce}`);
|
|
1389
|
+
const launchers = launcherState();
|
|
1390
|
+
const launcherProblems = launcherWarnings(launchers);
|
|
1391
|
+
console.log(
|
|
1392
|
+
` launchers ${launcherProblems.length === 0 ? `OK (${launchers.binDir}, on PATH)` : "PROBLEM"}`
|
|
1393
|
+
);
|
|
1394
|
+
for (const line of launcherProblems) console.log(` ${line}`);
|
|
1332
1395
|
console.log(
|
|
1333
1396
|
` hook events ${s.spoolEvents}${s.spoolLastAt ? " last " + s.spoolLastAt : " (no session hook has ever fired)"}`
|
|
1334
1397
|
);
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darkhunt-security/endpoint-codex",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"endpoint-codex": "dist/bin/enroll.mjs"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"dist/bin",
|
|
8
11
|
"hooks",
|
|
@@ -10,8 +13,8 @@
|
|
|
10
13
|
".codex-plugin"
|
|
11
14
|
],
|
|
12
15
|
"devDependencies": {
|
|
13
|
-
"@darkhunt-security/endpoint-contracts": "0.9.
|
|
14
|
-
"@darkhunt-security/endpoint-core": "0.9.
|
|
16
|
+
"@darkhunt-security/endpoint-contracts": "0.9.1",
|
|
17
|
+
"@darkhunt-security/endpoint-core": "0.9.1"
|
|
15
18
|
},
|
|
16
19
|
"publishConfig": {
|
|
17
20
|
"access": "public",
|