@iamdevlinph/codex-kit 1.0.4 → 1.0.5
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/README.md +28 -12
- package/assets/SUBAGENT_ROUTING.md +35 -34
- package/bin/codex-kit.js +129 -0
- package/bin/routing-hook.js +166 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Portable Codex setup for new devices and multiple projects. It provides:
|
|
4
4
|
|
|
5
5
|
- four custom subagents: explorer, quick implementer, implementer, and reviewer
|
|
6
|
-
- global
|
|
6
|
+
- automatic global role routing
|
|
7
7
|
- a reusable, stack-neutral `AGENTS.md` template
|
|
8
8
|
- safe commands for global setup, project synchronization, and reconciliation
|
|
9
9
|
|
|
@@ -20,17 +20,30 @@ codex-kit global install
|
|
|
20
20
|
codex-kit global configure
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Start a new Codex task or restart the client after installation. Codex may ask
|
|
24
|
+
you to review and trust the new command hooks once; approve them after confirming
|
|
25
|
+
that they run the installed `${CODEX_HOME:-~/.codex}/codex-kit/routing-hook.js`.
|
|
26
|
+
|
|
23
27
|
`npm install --global @iamdevlinph/codex-kit@latest` works as an alternative.
|
|
24
28
|
For one-off use without a global installation, prefix a command with
|
|
25
29
|
`pnpm dlx @iamdevlinph/codex-kit@latest`.
|
|
26
30
|
|
|
27
|
-
`global install` copies reusable agents to `${CODEX_HOME:-~/.codex}
|
|
28
|
-
|
|
31
|
+
`global install` copies reusable agents to `${CODEX_HOME:-~/.codex}`, maintains
|
|
32
|
+
a marked routing section in the global `AGENTS.md`, and adds package-owned
|
|
33
|
+
handlers to the global `hooks.json` without replacing existing hooks.
|
|
34
|
+
|
|
35
|
+
The Sol root plans, routes, coordinates, and validates. On every prompt, the
|
|
36
|
+
routing hook supplies the current `SUBAGENT_ROUTING.md`; the root classifies the
|
|
37
|
+
task and delegates to the exact matching role. The role's agent TOML—not the
|
|
38
|
+
routing policy—selects its model and reasoning effort. Planning, conversation,
|
|
39
|
+
status, and small read-only checks may stay on the root.
|
|
29
40
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
41
|
+
The hook rejects direct root writes through Codex edit tools and obvious
|
|
42
|
+
write-shaped shell commands. `SubagentStart` opens a 15-minute write allowance
|
|
43
|
+
for that worker's `session_id + turn_id`; `SubagentStop` closes it. This identifies
|
|
44
|
+
the delegated lane without hard-coding a model. Codex hooks do not intercept
|
|
45
|
+
every possible indirect shell-write path, so this is a workflow guardrail rather
|
|
46
|
+
than a security boundary.
|
|
34
47
|
|
|
35
48
|
`global configure` sets these defaults while preserving unrelated settings:
|
|
36
49
|
|
|
@@ -60,8 +73,8 @@ pnpm dlx @iamdevlinph/codex-kit@latest global list
|
|
|
60
73
|
```
|
|
61
74
|
|
|
62
75
|
The summary shows the Codex home, orchestrator, normal and Plan-mode reasoning
|
|
63
|
-
effort, routing status, and installed custom agents without
|
|
64
|
-
configuration.
|
|
76
|
+
effort, routing-file and routing-hook status, and installed custom agents without
|
|
77
|
+
dumping unrelated configuration.
|
|
65
78
|
|
|
66
79
|
Uninstall package-managed global files:
|
|
67
80
|
|
|
@@ -69,8 +82,8 @@ Uninstall package-managed global files:
|
|
|
69
82
|
pnpm dlx @iamdevlinph/codex-kit@latest global uninstall
|
|
70
83
|
```
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
Uninstall removes only codex-kit's handlers from `hooks.json` and preserves other
|
|
86
|
+
hooks. Modified managed files are preserved unless `--force` is supplied.
|
|
74
87
|
|
|
75
88
|
## Commands
|
|
76
89
|
|
|
@@ -180,7 +193,7 @@ commands do not add network latency or depend on registry availability.
|
|
|
180
193
|
## Requirements
|
|
181
194
|
|
|
182
195
|
- Node.js 20 or newer
|
|
183
|
-
- Codex with custom subagent support
|
|
196
|
+
- Codex with custom subagent and lifecycle-hook support
|
|
184
197
|
|
|
185
198
|
The published package has no runtime dependencies and uses only Node.js
|
|
186
199
|
standard-library modules.
|
|
@@ -194,3 +207,6 @@ redistribute the package beyond applicable law and npm's service terms.
|
|
|
194
207
|
|
|
195
208
|
- [Codex custom subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents)
|
|
196
209
|
- [Codex `AGENTS.md` discovery](https://learn.chatgpt.com/docs/agent-configuration/agents-md)
|
|
210
|
+
- [Codex hooks](https://learn.chatgpt.com/docs/hooks)
|
|
211
|
+
- [Delegate Mode](https://github.com/Raylinkh/delegate-mode), whose short-lived
|
|
212
|
+
subagent allowance pattern informed codex-kit's write guard
|
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
# Subagent Routing
|
|
2
2
|
|
|
3
|
-
The root parent is the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The root parent is the planner and orchestrator. Before acting on a substantive
|
|
4
|
+
task, classify the work using this file. Route by role and task shape, never by a
|
|
5
|
+
model name; each agent definition owns its model and reasoning effort.
|
|
6
6
|
|
|
7
|
-
The parent owns requirements, architecture, integration, and final
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
For any task that creates, edits, or deletes project files, delegate the work to
|
|
12
|
-
exactly one of `quick-implementer` or `implementer`. Include bookkeeping commands
|
|
13
|
-
that write project state in that assignment. The parent may perform read-only
|
|
14
|
-
inspection and final validation, but must not make the project-file changes itself.
|
|
15
|
-
|
|
16
|
-
These routing rules apply to the root parent. An implementation subagent performs
|
|
17
|
-
its assigned edits directly and must not delegate them again. Do not delegate
|
|
18
|
-
trivial conversation or straightforward read-only commands. A slow command alone
|
|
19
|
-
is not a reason to delegate.
|
|
20
|
-
|
|
21
|
-
For delegated work:
|
|
22
|
-
|
|
23
|
-
- Prefer one subagent. Add more only for independent, non-overlapping work.
|
|
24
|
-
- Give each agent a bounded task and request a concise, decision-ready report.
|
|
25
|
-
- For parallel implementation, assign explicit file or module ownership.
|
|
26
|
-
- Wait for relevant agents and synthesize their results in the parent task.
|
|
27
|
-
- Reuse prior exploration and evidence instead of repeating it.
|
|
7
|
+
The parent owns requirements, architecture, sequencing, integration, and final
|
|
8
|
+
validation. It may handle planning, conversation, status reporting, and small
|
|
9
|
+
read-only checks directly. When a route below matches, spawn that exact role
|
|
10
|
+
before performing the role's work. The user does not need to request delegation.
|
|
28
11
|
|
|
29
12
|
Select custom agents by exact name:
|
|
30
13
|
|
|
31
|
-
- Broad repository discovery or
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
- Broad repository discovery, contract tracing, or search across many files:
|
|
15
|
+
`code-explorer`
|
|
16
|
+
- Mechanical, well-specified implementation localized to one or two files:
|
|
17
|
+
`quick-implementer`
|
|
18
|
+
- Multi-file behavior changes, debugging, migrations, or substantial tests:
|
|
19
|
+
`implementer`
|
|
20
|
+
- Independent review of security-sensitive, architectural, public-API,
|
|
21
|
+
concurrency, migration, or otherwise difficult-to-validate changes:
|
|
22
|
+
`code-reviewer`
|
|
23
|
+
|
|
24
|
+
For tasks with multiple phases, sequence only the roles that add value. For
|
|
25
|
+
example, use `code-explorer` before implementation only when broad discovery is
|
|
26
|
+
actually needed, and use `code-reviewer` after implementation only when the
|
|
27
|
+
change meets its risk threshold. Avoid parallel write-heavy work and never assign
|
|
28
|
+
overlapping files to multiple agents.
|
|
29
|
+
|
|
30
|
+
An assigned implementation agent performs its edits directly and must not
|
|
31
|
+
delegate them again. Use either `quick-implementer` or `implementer` for one
|
|
32
|
+
implementation slice, not both. Read-only agents never modify files.
|
|
33
|
+
|
|
34
|
+
For every delegation:
|
|
35
|
+
|
|
36
|
+
- Give the agent a bounded task and the relevant requirements.
|
|
37
|
+
- Request a concise, decision-ready result.
|
|
38
|
+
- Wait for the result before integrating or validating dependent work.
|
|
39
|
+
- Reuse returned evidence instead of repeating the same exploration.
|
|
39
40
|
|
|
40
41
|
There is no commit-pusher role. Never delegate Git publishing. Do not stage,
|
|
41
|
-
commit, or push unless the user explicitly requests that operation in the
|
|
42
|
-
task; if requested, the parent handles it after review.
|
|
42
|
+
commit, tag, or push unless the user explicitly requests that operation in the
|
|
43
|
+
current task; if requested, the parent handles it after review.
|
package/bin/codex-kit.js
CHANGED
|
@@ -10,6 +10,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
10
10
|
const ASSETS = join(ROOT, "assets");
|
|
11
11
|
const AGENTS_DIR = join(ASSETS, "agents");
|
|
12
12
|
const ROUTING_FILE = join(ASSETS, "SUBAGENT_ROUTING.md");
|
|
13
|
+
const ROUTING_HOOK_FILE = join(ROOT, "bin", "routing-hook.js");
|
|
13
14
|
const TEMPLATE_FILE = join(ASSETS, "TEMPLATE_AGENTS.md");
|
|
14
15
|
const PACKAGE = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
|
15
16
|
const GLOBAL_BEGIN = "<!-- BEGIN codex-kit:subagent-routing -->";
|
|
@@ -44,6 +45,116 @@ function write(file, data) {
|
|
|
44
45
|
writeFileSync(temporary, data);
|
|
45
46
|
renameSync(temporary, file);
|
|
46
47
|
}
|
|
48
|
+
function readJsonObject(file) {
|
|
49
|
+
if (!existsSync(file))
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
const value = JSON.parse(readText(file));
|
|
53
|
+
if (isRecord(value))
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Use the actionable error below for invalid JSON and non-object roots.
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`${file} must contain a JSON object; fix or move it before installing.`);
|
|
60
|
+
}
|
|
61
|
+
function shellQuote(value) {
|
|
62
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
63
|
+
}
|
|
64
|
+
function hookCommands(file) {
|
|
65
|
+
return {
|
|
66
|
+
command: `/usr/bin/env node ${shellQuote(file)}`,
|
|
67
|
+
commandWindows: `node ${JSON.stringify(file)}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function removeHookHandlers(root, state) {
|
|
71
|
+
const hooks = root.hooks;
|
|
72
|
+
if (!isRecord(hooks))
|
|
73
|
+
return;
|
|
74
|
+
for (const [event, groupsValue] of Object.entries(hooks)) {
|
|
75
|
+
if (!Array.isArray(groupsValue))
|
|
76
|
+
continue;
|
|
77
|
+
const groups = groupsValue.flatMap((groupValue) => {
|
|
78
|
+
if (!isRecord(groupValue) || !Array.isArray(groupValue.hooks))
|
|
79
|
+
return [groupValue];
|
|
80
|
+
const handlers = groupValue.hooks.filter((handler) => {
|
|
81
|
+
if (!isRecord(handler))
|
|
82
|
+
return true;
|
|
83
|
+
return handler.command !== state.command && handler.commandWindows !== state.commandWindows;
|
|
84
|
+
});
|
|
85
|
+
return handlers.length ? [{ ...groupValue, hooks: handlers }] : [];
|
|
86
|
+
});
|
|
87
|
+
if (groups.length)
|
|
88
|
+
hooks[event] = groups;
|
|
89
|
+
else
|
|
90
|
+
delete hooks[event];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function installRoutingHooks(home, previous) {
|
|
94
|
+
const target = join(home, "hooks.json");
|
|
95
|
+
const hookFile = join(home, "codex-kit", "routing-hook.js");
|
|
96
|
+
const commands = hookCommands(hookFile);
|
|
97
|
+
const created = previous?.created ?? !existsSync(target);
|
|
98
|
+
const root = readJsonObject(target);
|
|
99
|
+
if (previous)
|
|
100
|
+
removeHookHandlers(root, previous);
|
|
101
|
+
const hooks = isRecord(root.hooks) ? root.hooks : {};
|
|
102
|
+
root.hooks = hooks;
|
|
103
|
+
const handler = {
|
|
104
|
+
type: "command",
|
|
105
|
+
command: commands.command,
|
|
106
|
+
commandWindows: commands.commandWindows,
|
|
107
|
+
timeout: 5,
|
|
108
|
+
};
|
|
109
|
+
const promptGroups = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
|
|
110
|
+
const toolGroups = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
111
|
+
hooks.UserPromptSubmit = [
|
|
112
|
+
...promptGroups,
|
|
113
|
+
{ hooks: [{ ...handler, statusMessage: "Loading subagent routing" }] },
|
|
114
|
+
];
|
|
115
|
+
const startGroups = Array.isArray(hooks.SubagentStart) ? hooks.SubagentStart : [];
|
|
116
|
+
hooks.SubagentStart = [
|
|
117
|
+
...startGroups,
|
|
118
|
+
{ hooks: [{ ...handler, statusMessage: "Opening delegated write lane" }] },
|
|
119
|
+
];
|
|
120
|
+
const stopGroups = Array.isArray(hooks.SubagentStop) ? hooks.SubagentStop : [];
|
|
121
|
+
hooks.SubagentStop = [
|
|
122
|
+
...stopGroups,
|
|
123
|
+
{ hooks: [{ ...handler, statusMessage: "Closing delegated write lane" }] },
|
|
124
|
+
];
|
|
125
|
+
hooks.PreToolUse = [
|
|
126
|
+
...toolGroups,
|
|
127
|
+
{
|
|
128
|
+
matcher: "Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions.apply_patch",
|
|
129
|
+
hooks: [{ ...handler, statusMessage: "Checking orchestrator routing" }],
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
133
|
+
const original = existsSync(target) ? readText(target) : "";
|
|
134
|
+
if (updated !== original) {
|
|
135
|
+
backup(target);
|
|
136
|
+
write(target, updated);
|
|
137
|
+
console.log(`updated: ${target}`);
|
|
138
|
+
}
|
|
139
|
+
return { target, ...commands, created };
|
|
140
|
+
}
|
|
141
|
+
function uninstallRoutingHooks(state) {
|
|
142
|
+
if (!existsSync(state.target)) {
|
|
143
|
+
console.warn(`preserved missing hooks file: ${state.target}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const root = readJsonObject(state.target);
|
|
147
|
+
removeHookHandlers(root, state);
|
|
148
|
+
const hooks = root.hooks;
|
|
149
|
+
if (isRecord(hooks) && !Object.keys(hooks).length)
|
|
150
|
+
delete root.hooks;
|
|
151
|
+
backup(state.target);
|
|
152
|
+
if (state.created && !Object.keys(root).length)
|
|
153
|
+
rmSync(state.target);
|
|
154
|
+
else
|
|
155
|
+
write(state.target, `${JSON.stringify(root, null, 2)}\n`);
|
|
156
|
+
console.log(`removed managed routing hooks from: ${state.target}`);
|
|
157
|
+
}
|
|
47
158
|
function loadState(home) {
|
|
48
159
|
const file = join(home, STATE_FILE);
|
|
49
160
|
if (!existsSync(file))
|
|
@@ -234,6 +345,7 @@ function installGlobal(options) {
|
|
|
234
345
|
version: PACKAGE.version,
|
|
235
346
|
files: {},
|
|
236
347
|
globalAgents: null,
|
|
348
|
+
hooks: null,
|
|
237
349
|
config: prior.config ?? null,
|
|
238
350
|
};
|
|
239
351
|
for (const name of readdirSync(AGENTS_DIR).filter((name) => name.endsWith(".toml")).sort()) {
|
|
@@ -245,6 +357,9 @@ function installGlobal(options) {
|
|
|
245
357
|
const routingRecord = installFile(ROUTING_FILE, join(home, "SUBAGENT_ROUTING.md"), "routing", prior, options.force);
|
|
246
358
|
if (routingRecord)
|
|
247
359
|
next.files.routing = routingRecord;
|
|
360
|
+
const hookRecord = installFile(ROUTING_HOOK_FILE, join(home, "codex-kit", "routing-hook.js"), "routing-hook", prior, options.force);
|
|
361
|
+
if (hookRecord)
|
|
362
|
+
next.files["routing-hook"] = hookRecord;
|
|
248
363
|
for (const [key, record] of Object.entries(prior.files)) {
|
|
249
364
|
if (key in next.files)
|
|
250
365
|
continue;
|
|
@@ -272,6 +387,7 @@ function installGlobal(options) {
|
|
|
272
387
|
console.log(`updated: ${globalAgents}`);
|
|
273
388
|
}
|
|
274
389
|
next.globalAgents = { target: globalAgents };
|
|
390
|
+
next.hooks = installRoutingHooks(home, prior.hooks);
|
|
275
391
|
saveState(home, next);
|
|
276
392
|
const commitPusher = join(home, "agents", "commit-pusher.toml");
|
|
277
393
|
if (existsSync(commitPusher)) {
|
|
@@ -360,6 +476,11 @@ function listGlobal(options) {
|
|
|
360
476
|
const hasRoutingBlock = existsSync(globalAgents) && readText(globalAgents).includes(GLOBAL_BEGIN);
|
|
361
477
|
console.log(`Global routing: ${hasRoutingBlock ? "installed" : "not installed"}`);
|
|
362
478
|
console.log(`Routing file: ${existsSync(routingFile) ? routingFile : "missing"}`);
|
|
479
|
+
const routingHook = state.hooks;
|
|
480
|
+
const hooksInstalled = Boolean(routingHook &&
|
|
481
|
+
existsSync(routingHook.target) &&
|
|
482
|
+
readText(routingHook.target).includes(routingHook.command));
|
|
483
|
+
console.log(`Routing hook: ${hooksInstalled ? "installed" : "not installed"}`);
|
|
363
484
|
console.log("Custom agents:");
|
|
364
485
|
const agents = existsSync(agentsDir)
|
|
365
486
|
? readdirSync(agentsDir).filter((name) => name.endsWith(".toml")).sort()
|
|
@@ -441,6 +562,14 @@ function uninstallGlobal(options) {
|
|
|
441
562
|
}
|
|
442
563
|
}
|
|
443
564
|
}
|
|
565
|
+
if (state.hooks)
|
|
566
|
+
uninstallRoutingHooks(state.hooks);
|
|
567
|
+
const allowancesDir = join(home, "codex-kit", "allowances");
|
|
568
|
+
if (existsSync(allowancesDir))
|
|
569
|
+
rmSync(allowancesDir, { recursive: true, force: true });
|
|
570
|
+
const kitDir = join(home, "codex-kit");
|
|
571
|
+
if (existsSync(kitDir) && !readdirSync(kitDir).length)
|
|
572
|
+
rmSync(kitDir, { recursive: true });
|
|
444
573
|
rmSync(statePath);
|
|
445
574
|
console.log(`Codex kit uninstalled from ${home}`);
|
|
446
575
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const home = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const routingFile = join(home, "SUBAGENT_ROUTING.md");
|
|
8
|
+
const allowancesDir = join(home, "codex-kit", "allowances");
|
|
9
|
+
const allowanceTtlMs = 15 * 60 * 1000;
|
|
10
|
+
const writeTools = new Set([
|
|
11
|
+
"Edit",
|
|
12
|
+
"Write",
|
|
13
|
+
"MultiEdit",
|
|
14
|
+
"NotebookEdit",
|
|
15
|
+
"apply_patch",
|
|
16
|
+
"ApplyPatch",
|
|
17
|
+
"functions.apply_patch",
|
|
18
|
+
]);
|
|
19
|
+
const writeCommands = new Set([
|
|
20
|
+
"chmod",
|
|
21
|
+
"chown",
|
|
22
|
+
"cp",
|
|
23
|
+
"dd",
|
|
24
|
+
"install",
|
|
25
|
+
"ln",
|
|
26
|
+
"mkdir",
|
|
27
|
+
"mv",
|
|
28
|
+
"rm",
|
|
29
|
+
"rmdir",
|
|
30
|
+
"rsync",
|
|
31
|
+
"tee",
|
|
32
|
+
"touch",
|
|
33
|
+
"truncate",
|
|
34
|
+
]);
|
|
35
|
+
const gitWriteCommands = new Set([
|
|
36
|
+
"add",
|
|
37
|
+
"am",
|
|
38
|
+
"apply",
|
|
39
|
+
"checkout",
|
|
40
|
+
"cherry-pick",
|
|
41
|
+
"clean",
|
|
42
|
+
"commit",
|
|
43
|
+
"merge",
|
|
44
|
+
"mv",
|
|
45
|
+
"rebase",
|
|
46
|
+
"reset",
|
|
47
|
+
"restore",
|
|
48
|
+
"revert",
|
|
49
|
+
"rm",
|
|
50
|
+
"stash",
|
|
51
|
+
"switch",
|
|
52
|
+
]);
|
|
53
|
+
const input = JSON.parse(readFileSync(0, "utf8"));
|
|
54
|
+
function allowanceFile(payload) {
|
|
55
|
+
if (!payload.session_id || !payload.turn_id)
|
|
56
|
+
return null;
|
|
57
|
+
const key = createHash("sha256")
|
|
58
|
+
.update(`${payload.session_id}\0${payload.turn_id}`)
|
|
59
|
+
.digest("hex");
|
|
60
|
+
return join(allowancesDir, `${key}.json`);
|
|
61
|
+
}
|
|
62
|
+
function writeAllowance(payload) {
|
|
63
|
+
const file = allowanceFile(payload);
|
|
64
|
+
if (!file)
|
|
65
|
+
return false;
|
|
66
|
+
mkdirSync(allowancesDir, { recursive: true });
|
|
67
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(temporary, JSON.stringify({
|
|
69
|
+
agentId: payload.agent_id,
|
|
70
|
+
agentType: payload.agent_type,
|
|
71
|
+
expiresAt: Date.now() + allowanceTtlMs,
|
|
72
|
+
}));
|
|
73
|
+
renameSync(temporary, file);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function removeAllowance(payload) {
|
|
77
|
+
const file = allowanceFile(payload);
|
|
78
|
+
if (file)
|
|
79
|
+
rmSync(file, { force: true });
|
|
80
|
+
}
|
|
81
|
+
function hasAllowance(payload) {
|
|
82
|
+
const file = allowanceFile(payload);
|
|
83
|
+
if (!file || !existsSync(file))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
87
|
+
if (typeof value.expiresAt === "number" && value.expiresAt > Date.now())
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Invalid allowance files fail closed.
|
|
92
|
+
}
|
|
93
|
+
rmSync(file, { force: true });
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function pruneAllowances() {
|
|
97
|
+
if (!existsSync(allowancesDir))
|
|
98
|
+
return;
|
|
99
|
+
for (const name of readdirSync(allowancesDir)) {
|
|
100
|
+
if (!name.endsWith(".json"))
|
|
101
|
+
continue;
|
|
102
|
+
const file = join(allowancesDir, name);
|
|
103
|
+
try {
|
|
104
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
105
|
+
if (typeof value.expiresAt === "number" && value.expiresAt > Date.now())
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Invalid allowance files are stale.
|
|
110
|
+
}
|
|
111
|
+
rmSync(file, { force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isWriteShapedBash(command) {
|
|
115
|
+
if (/(^|[^<])(?:>>?|&>>?|\d>>?)\s*[^&]/.test(command))
|
|
116
|
+
return true;
|
|
117
|
+
const commands = command.split(/(?:&&|\|\||;|\|)/).map((part) => part.trim()).filter(Boolean);
|
|
118
|
+
return commands.some((segment) => {
|
|
119
|
+
const tokens = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
120
|
+
const executable = tokens[0]?.split("/").at(-1);
|
|
121
|
+
if (!executable)
|
|
122
|
+
return false;
|
|
123
|
+
if (writeCommands.has(executable))
|
|
124
|
+
return true;
|
|
125
|
+
if ((executable === "sed" || executable === "perl") && tokens.slice(1, 5).some((token) => /^-[^-]*i/.test(token)))
|
|
126
|
+
return true;
|
|
127
|
+
return executable === "git" && Boolean(tokens[1] && gitWriteCommands.has(tokens[1]));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function hookContext(event, context) {
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
hookSpecificOutput: {
|
|
133
|
+
hookEventName: event,
|
|
134
|
+
additionalContext: context,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (input.hook_event_name === "UserPromptSubmit" && existsSync(routingFile)) {
|
|
139
|
+
const routing = readFileSync(routingFile, "utf8").trim();
|
|
140
|
+
process.stdout.write(`Codex-kit routing policy for this turn:\n\n${routing}\n\n` +
|
|
141
|
+
"Classify the task using this policy before acting. When it requires delegation, " +
|
|
142
|
+
"spawn the exact named role before doing that role's work. Agent definitions, not " +
|
|
143
|
+
"this policy, determine each role's model and reasoning effort.");
|
|
144
|
+
}
|
|
145
|
+
if (input.hook_event_name === "SubagentStart") {
|
|
146
|
+
pruneAllowances();
|
|
147
|
+
const recorded = writeAllowance(input);
|
|
148
|
+
process.stdout.write(hookContext("SubagentStart", `You are the delegated ${input.agent_type ?? "worker"}. ` +
|
|
149
|
+
`${recorded ? "A temporary write lane is active." : "No write lane could be recorded."} ` +
|
|
150
|
+
"Follow the assigned scope, perform the role's work directly without further delegation, validate it, and return concise evidence."));
|
|
151
|
+
}
|
|
152
|
+
if (input.hook_event_name === "SubagentStop")
|
|
153
|
+
removeAllowance(input);
|
|
154
|
+
if (input.hook_event_name === "PreToolUse") {
|
|
155
|
+
const writeAttempt = writeTools.has(input.tool_name ?? "") ||
|
|
156
|
+
(input.tool_name === "Bash" && isWriteShapedBash(input.tool_input?.command ?? ""));
|
|
157
|
+
if (writeAttempt && !input.agent_id && !hasAllowance(input)) {
|
|
158
|
+
process.stdout.write(JSON.stringify({
|
|
159
|
+
hookSpecificOutput: {
|
|
160
|
+
hookEventName: "PreToolUse",
|
|
161
|
+
permissionDecision: "deny",
|
|
162
|
+
permissionDecisionReason: "The root orchestrator may not write directly. Consult SUBAGENT_ROUTING.md and delegate to the exact role selected there.",
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|