nterminal 1.3.5 → 1.3.6
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 +68 -37
- package/assets/screenshot/accounts.png +0 -0
- package/assets/screenshot/history.png +0 -0
- package/assets/screenshot/mobile-panes.png +0 -0
- package/assets/screenshot/mobile.png +0 -0
- package/assets/screenshot/scr.png +0 -0
- package/assets/screenshot/subagents.png +0 -0
- package/bin/nterminal.js +2 -0
- package/dist/client/assets/MarkdownPreview-B-LrTjTM.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-CI0ztvMX.js → TranscriptMarkdownBody-3aYVmqQz.js} +1 -1
- package/dist/client/assets/index-D3LN5iAH.js +58 -0
- package/dist/client/assets/{index-EN8ngMgv.js → index-Dd_o8KCL.js} +1 -1
- package/dist/client/assets/index-W-zGfY1a.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/scripts/hooks.js +47 -6
- package/dist/scripts/hooks.js.map +1 -1
- package/dist/server/hooks/codexAppServer.d.ts +30 -0
- package/dist/server/hooks/codexAppServer.js +170 -0
- package/dist/server/hooks/codexAppServer.js.map +1 -0
- package/dist/server/hooks/codexTrust.d.ts +72 -0
- package/dist/server/hooks/codexTrust.js +116 -0
- package/dist/server/hooks/codexTrust.js.map +1 -0
- package/dist/server/hooks/hookInstallation.d.ts +29 -0
- package/dist/server/hooks/hookInstallation.js +98 -2
- package/dist/server/hooks/hookInstallation.js.map +1 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hookRoutes.d.ts +1 -1
- package/dist/server/routes/hookRoutes.js +38 -6
- package/dist/server/routes/hookRoutes.js.map +1 -1
- package/dist/server/terminal/PtyAdapter.d.ts +1 -0
- package/dist/server/terminal/PtyAdapter.js.map +1 -1
- package/dist/server/terminal/TerminalManager.js +10 -3
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/TmuxPtyAdapter.js +3 -1
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
- package/dist/server/terminal/codexTranscriptSource.d.ts +1 -0
- package/dist/server/terminal/codexTranscriptSource.js +58 -2
- package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
- package/docs/features.md +11 -1
- package/docs/provider-hooks.md +64 -11
- package/package.json +1 -1
- package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +0 -1
- package/dist/client/assets/index-JQJ1Eet1.js +0 -58
- package/dist/client/assets/index-xkL4h3qt.css +0 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, stat } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { HookControlError } from './hookFiles.js';
|
|
7
|
+
const DEFAULT_MAX_OUTPUT = 8 * 1024 * 1024;
|
|
8
|
+
const EXIT_GRACE_MS = 1_000;
|
|
9
|
+
// Only daemon-internal or test-only variables that could attach this process to
|
|
10
|
+
// daemon lifecycle files or redirect its user config. The user's own settings
|
|
11
|
+
// (PATH, HOME, proxies, CODEX_* overrides) are otherwise inherited unchanged,
|
|
12
|
+
// exactly like a `codex` started from the NTerminal runtime.
|
|
13
|
+
const SCRUBBED_ENV = ['CODEX_DAEMON_SHUTDOWN_SOCKET', 'CODEX_DAEMON_SHUTDOWN_FILE', 'CODEX_DAEMON_TELEMETRY_HANDOFF',
|
|
14
|
+
'CODEX_APP_SERVER_TEST_USER_CONFIG_FILE', 'CODEX_THREAD_ID'];
|
|
15
|
+
export async function withCodexAppServer(options, run) {
|
|
16
|
+
const env = { ...(options.env ?? process.env) };
|
|
17
|
+
for (const key of SCRUBBED_ENV)
|
|
18
|
+
delete env[key];
|
|
19
|
+
env.CODEX_HOME = options.codexHome;
|
|
20
|
+
env.CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED = '1';
|
|
21
|
+
const remaining = () => options.deadline - Date.now();
|
|
22
|
+
if (remaining() <= 0)
|
|
23
|
+
throw new HookControlError('codex_timeout');
|
|
24
|
+
// Own process group: the npm launcher forwards SIGTERM but not SIGKILL, and
|
|
25
|
+
// Codex may start helpers (plugin sync, sandbox probe). Cleanup kills the group.
|
|
26
|
+
const group = process.platform !== 'win32';
|
|
27
|
+
const child = spawn(options.executable, ['app-server', '--listen', 'stdio://'], {
|
|
28
|
+
cwd: options.cwd, env, stdio: ['pipe', 'pipe', 'pipe'], detached: group, windowsHide: true
|
|
29
|
+
});
|
|
30
|
+
let exited = false;
|
|
31
|
+
const exitPromise = new Promise((resolve) => {
|
|
32
|
+
child.once('exit', () => { exited = true; resolve(); });
|
|
33
|
+
child.once('error', () => { exited = true; resolve(); });
|
|
34
|
+
});
|
|
35
|
+
const kill = () => {
|
|
36
|
+
if (group && typeof child.pid === 'number') {
|
|
37
|
+
try {
|
|
38
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch { /* group already gone */ }
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
child.kill('SIGKILL');
|
|
45
|
+
}
|
|
46
|
+
catch { /* already exited */ }
|
|
47
|
+
};
|
|
48
|
+
const pending = new Map();
|
|
49
|
+
let failure = null;
|
|
50
|
+
const fail = (code) => {
|
|
51
|
+
if (failure)
|
|
52
|
+
return;
|
|
53
|
+
failure = new HookControlError(code);
|
|
54
|
+
for (const entry of pending.values())
|
|
55
|
+
entry.reject(failure);
|
|
56
|
+
pending.clear();
|
|
57
|
+
};
|
|
58
|
+
const write = (message) => {
|
|
59
|
+
if (failure || !child.stdin || child.stdin.destroyed)
|
|
60
|
+
return;
|
|
61
|
+
child.stdin.write(JSON.stringify(message) + '\n');
|
|
62
|
+
};
|
|
63
|
+
child.once('error', (error) => fail(['ENOENT', 'EACCES', 'ENOEXEC'].includes(error.code ?? '') ? 'codex_unavailable' : 'codex_rpc_failed'));
|
|
64
|
+
child.once('exit', () => fail('codex_rpc_failed'));
|
|
65
|
+
child.stdin?.on('error', () => fail('codex_rpc_failed'));
|
|
66
|
+
// stderr carries Codex's logs; drain it without retaining or forwarding content.
|
|
67
|
+
child.stderr?.resume();
|
|
68
|
+
let received = 0;
|
|
69
|
+
let buffer = '';
|
|
70
|
+
child.stdout?.setEncoding('utf8');
|
|
71
|
+
child.stdout?.on('data', (chunk) => {
|
|
72
|
+
received += Buffer.byteLength(chunk);
|
|
73
|
+
if (received > (options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT)) {
|
|
74
|
+
fail('codex_rpc_failed');
|
|
75
|
+
kill();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
buffer += chunk;
|
|
79
|
+
for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
|
|
80
|
+
const line = buffer.slice(0, index).trim();
|
|
81
|
+
buffer = buffer.slice(index + 1);
|
|
82
|
+
if (line)
|
|
83
|
+
handle(line);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const handle = (line) => {
|
|
87
|
+
let message;
|
|
88
|
+
try {
|
|
89
|
+
message = JSON.parse(line);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
fail('codex_rpc_failed');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
96
|
+
return;
|
|
97
|
+
const value = message;
|
|
98
|
+
if (value.id !== undefined && typeof value.method === 'string') {
|
|
99
|
+
// A server-initiated request: this client offers no capabilities.
|
|
100
|
+
write({ id: value.id, error: { code: -32601, message: 'unsupported by this client' } });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value.id !== 'number')
|
|
104
|
+
return; // notification
|
|
105
|
+
const entry = pending.get(value.id);
|
|
106
|
+
if (!entry)
|
|
107
|
+
return;
|
|
108
|
+
pending.delete(value.id);
|
|
109
|
+
if (value.error !== undefined)
|
|
110
|
+
entry.reject(new HookControlError('codex_rpc_failed'));
|
|
111
|
+
else
|
|
112
|
+
entry.resolve(value.result);
|
|
113
|
+
};
|
|
114
|
+
let nextId = 0;
|
|
115
|
+
const request = (method, params) => {
|
|
116
|
+
if (failure)
|
|
117
|
+
return Promise.reject(failure);
|
|
118
|
+
const id = ++nextId;
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
pending.set(id, { resolve, reject });
|
|
121
|
+
write(params === undefined ? { id, method } : { id, method, params });
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
let timer;
|
|
125
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
126
|
+
timer = setTimeout(() => { fail('codex_timeout'); kill(); reject(failure); }, Math.max(0, remaining()));
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
// hooks/list and config/batchWrite are stable methods: no experimentalApi opt-in.
|
|
130
|
+
await Promise.race([request('initialize', { clientInfo: { name: 'nterminal', title: 'NTerminal', version: '1' } }), timeout]);
|
|
131
|
+
write({ method: 'initialized' });
|
|
132
|
+
return await Promise.race([run(request), timeout]);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
fail('codex_rpc_failed'); // reject stragglers; no-op when a failure is already recorded
|
|
137
|
+
try {
|
|
138
|
+
child.stdin?.end();
|
|
139
|
+
}
|
|
140
|
+
catch { /* already closed */ }
|
|
141
|
+
// A clean exit flushes Codex's own state; never wait past the grace period.
|
|
142
|
+
if (!exited)
|
|
143
|
+
await Promise.race([exitPromise, new Promise((resolve) => setTimeout(resolve, EXIT_GRACE_MS).unref())]);
|
|
144
|
+
kill(); // always: also removes helpers left in the group
|
|
145
|
+
child.stdout?.destroy();
|
|
146
|
+
child.stderr?.destroy();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The `codex` a user's shell would run: the first executable `codex` on PATH,
|
|
151
|
+
* then common per-user install locations a service PATH may lack.
|
|
152
|
+
*/
|
|
153
|
+
export async function resolveCodexExecutable(env = process.env, home = os.homedir()) {
|
|
154
|
+
const directories = (env.PATH ?? '').split(path.delimiter).filter((entry) => path.isAbsolute(entry));
|
|
155
|
+
directories.push(path.join(home, '.npm-global', 'bin'), path.join(home, '.local', 'bin'), '/usr/local/bin', '/opt/homebrew/bin');
|
|
156
|
+
const names = process.platform === 'win32' ? ['codex.cmd', 'codex.exe', 'codex'] : ['codex'];
|
|
157
|
+
for (const directory of new Set(directories))
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
const candidate = path.join(directory, name);
|
|
160
|
+
try {
|
|
161
|
+
if (!(await stat(candidate)).isFile())
|
|
162
|
+
continue;
|
|
163
|
+
await access(candidate, constants.X_OK);
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
catch { /* next candidate */ }
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=codexAppServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codexAppServer.js","sourceRoot":"","sources":["../../../src/server/hooks/codexAppServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Codex trust for NTerminal's own hooks, performed exactly like Codex's
|
|
3
|
+
* `/hooks` screen (codex-rs/tui/src/hooks_rpc.rs): `hooks/list` reports each
|
|
4
|
+
* hook's key, `currentHash`, and `trustStatus`; `config/batchWrite` upserts
|
|
5
|
+
* `hooks.state."<key>".trusted_hash = <currentHash>` into the user config.toml.
|
|
6
|
+
* NTerminal never computes a hash or edits a trust entry itself.
|
|
7
|
+
*/
|
|
8
|
+
export interface CodexHookTrust {
|
|
9
|
+
/** NTerminal hook definitions recorded in the installation manifest. */
|
|
10
|
+
total: number;
|
|
11
|
+
/** Hooks this operation marked as trusted. */
|
|
12
|
+
trusted: number;
|
|
13
|
+
alreadyTrusted: number;
|
|
14
|
+
/** NTerminal hooks still untrusted or modified after the operation. */
|
|
15
|
+
pending: number;
|
|
16
|
+
/** Other hooks Codex listed (including the same hooks.json); never changed. */
|
|
17
|
+
skipped: number;
|
|
18
|
+
/** Events whose NTerminal hook was missing, ambiguous, managed, or failed verification. */
|
|
19
|
+
mismatches?: string[];
|
|
20
|
+
reason?: string;
|
|
21
|
+
checkedAt: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ExpectedCodexHook {
|
|
24
|
+
/** hooks.json event name, e.g. `PreToolUse`. */
|
|
25
|
+
event: string;
|
|
26
|
+
command: string;
|
|
27
|
+
async: boolean;
|
|
28
|
+
timeoutSec: number;
|
|
29
|
+
matcher: string | null;
|
|
30
|
+
}
|
|
31
|
+
interface ListedHook {
|
|
32
|
+
key: string;
|
|
33
|
+
eventName: string;
|
|
34
|
+
handlerType: string;
|
|
35
|
+
command: string | null;
|
|
36
|
+
async: boolean;
|
|
37
|
+
matcher: string | null;
|
|
38
|
+
timeoutSec: number;
|
|
39
|
+
sourcePath: string;
|
|
40
|
+
source: string;
|
|
41
|
+
isManaged: boolean;
|
|
42
|
+
currentHash: string;
|
|
43
|
+
trustStatus: string;
|
|
44
|
+
}
|
|
45
|
+
export interface CodexTrustRun {
|
|
46
|
+
executable: string;
|
|
47
|
+
codexHome: string;
|
|
48
|
+
/** Symlink-resolved hooks.json NTerminal installed into. */
|
|
49
|
+
configPath: string;
|
|
50
|
+
expected: ExpectedCodexHook[];
|
|
51
|
+
write: boolean;
|
|
52
|
+
deadline: number;
|
|
53
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
54
|
+
}
|
|
55
|
+
export declare function runCodexTrust(run: CodexTrustRun): Promise<CodexHookTrust>;
|
|
56
|
+
export declare function parseHooksList(result: unknown): ListedHook[];
|
|
57
|
+
/**
|
|
58
|
+
* NTerminal's hooks are exactly: listed from NTerminal's hooks.json (realpath
|
|
59
|
+
* comparison), a user-layer command hook that is not managed, for an event
|
|
60
|
+
* NTerminal installed, whose command, async flag, timeout, and matcher equal
|
|
61
|
+
* the recorded definition, occurring exactly once. Everything else is skipped.
|
|
62
|
+
*/
|
|
63
|
+
export declare function selectOwnedHooks(listed: ListedHook[], expected: ExpectedCodexHook[], configPath: string): Promise<{
|
|
64
|
+
owned: ListedHook[];
|
|
65
|
+
mismatches: string[];
|
|
66
|
+
skipped: number;
|
|
67
|
+
}>;
|
|
68
|
+
export declare function expectedCodexHooks(entries: Array<{
|
|
69
|
+
event: string;
|
|
70
|
+
group: Record<string, unknown>;
|
|
71
|
+
}>): ExpectedCodexHook[];
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HookControlError } from './hookFiles.js';
|
|
4
|
+
import { withCodexAppServer } from './codexAppServer.js';
|
|
5
|
+
export async function runCodexTrust(run) {
|
|
6
|
+
// Any cwd sees the user layer; CODEX_HOME always exists (it holds hooks.json)
|
|
7
|
+
// and is not a project root, so no project hooks are mixed in.
|
|
8
|
+
const cwd = run.codexHome;
|
|
9
|
+
return withCodexAppServer({ executable: run.executable, codexHome: run.codexHome, cwd, deadline: run.deadline, env: run.env }, async (request) => {
|
|
10
|
+
const list = async () => parseHooksList(await request('hooks/list', { cwds: [cwd] }));
|
|
11
|
+
const selection = await selectOwnedHooks(await list(), run.expected, run.configPath);
|
|
12
|
+
const base = { total: run.expected.length, skipped: selection.skipped, checkedAt: new Date().toISOString() };
|
|
13
|
+
if (selection.mismatches.length) {
|
|
14
|
+
return { ...base, trusted: 0, alreadyTrusted: countStatus(selection.owned, 'trusted'),
|
|
15
|
+
pending: run.expected.length - countStatus(selection.owned, 'trusted'), mismatches: selection.mismatches, reason: 'hooks_not_listed' };
|
|
16
|
+
}
|
|
17
|
+
const needed = selection.owned.filter((hook) => hook.trustStatus === 'untrusted' || hook.trustStatus === 'modified');
|
|
18
|
+
const alreadyTrusted = countStatus(selection.owned, 'trusted');
|
|
19
|
+
if (!run.write || needed.length === 0) {
|
|
20
|
+
return { ...base, trusted: 0, alreadyTrusted, pending: needed.length };
|
|
21
|
+
}
|
|
22
|
+
await writeTrust(request, needed);
|
|
23
|
+
const after = await list();
|
|
24
|
+
const mismatches = needed.filter((hook) => {
|
|
25
|
+
const current = after.find((entry) => entry.key === hook.key);
|
|
26
|
+
return !current || current.trustStatus !== 'trusted' || current.currentHash !== hook.currentHash;
|
|
27
|
+
}).map((hook) => eventFromKey(hook));
|
|
28
|
+
const trusted = needed.length - mismatches.length;
|
|
29
|
+
return { ...base, trusted, alreadyTrusted, pending: mismatches.length,
|
|
30
|
+
...(mismatches.length ? { mismatches, reason: 'verification_failed' } : {}) };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Exactly the TUI's payload: one upsert of `hooks.state` into the user config.toml. */
|
|
34
|
+
async function writeTrust(request, hooks) {
|
|
35
|
+
const value = Object.fromEntries(hooks.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
|
|
36
|
+
await request('config/batchWrite', {
|
|
37
|
+
edits: [{ keyPath: 'hooks.state', value, mergeStrategy: 'upsert' }],
|
|
38
|
+
filePath: null, expectedVersion: null, reloadUserConfig: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function parseHooksList(result) {
|
|
42
|
+
const invalid = () => new HookControlError('codex_rpc_failed');
|
|
43
|
+
if (!record(result) || !Array.isArray(result.data))
|
|
44
|
+
throw invalid();
|
|
45
|
+
const hooks = [];
|
|
46
|
+
for (const entry of result.data) {
|
|
47
|
+
if (!record(entry) || !Array.isArray(entry.hooks))
|
|
48
|
+
throw invalid();
|
|
49
|
+
for (const hook of entry.hooks) {
|
|
50
|
+
if (!record(hook) || typeof hook.key !== 'string' || typeof hook.eventName !== 'string'
|
|
51
|
+
|| typeof hook.sourcePath !== 'string' || typeof hook.currentHash !== 'string' || typeof hook.trustStatus !== 'string')
|
|
52
|
+
throw invalid();
|
|
53
|
+
hooks.push({
|
|
54
|
+
key: hook.key, eventName: hook.eventName, handlerType: typeof hook.handlerType === 'string' ? hook.handlerType : '',
|
|
55
|
+
command: typeof hook.command === 'string' ? hook.command : null, async: hook.async === true,
|
|
56
|
+
matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
|
|
57
|
+
timeoutSec: typeof hook.timeoutSec === 'number' ? hook.timeoutSec : -1,
|
|
58
|
+
sourcePath: hook.sourcePath, source: typeof hook.source === 'string' ? hook.source : '',
|
|
59
|
+
isManaged: hook.isManaged !== false, currentHash: hook.currentHash, trustStatus: hook.trustStatus
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// hooks/list returns one entry per requested cwd; the same hook may repeat.
|
|
64
|
+
return hooks.filter((hook, index) => hooks.findIndex((other) => other.key === hook.key) === index);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* NTerminal's hooks are exactly: listed from NTerminal's hooks.json (realpath
|
|
68
|
+
* comparison), a user-layer command hook that is not managed, for an event
|
|
69
|
+
* NTerminal installed, whose command, async flag, timeout, and matcher equal
|
|
70
|
+
* the recorded definition, occurring exactly once. Everything else is skipped.
|
|
71
|
+
*/
|
|
72
|
+
export async function selectOwnedHooks(listed, expected, configPath) {
|
|
73
|
+
const target = await resolve(configPath);
|
|
74
|
+
const inFile = [];
|
|
75
|
+
for (const hook of listed)
|
|
76
|
+
if (path.isAbsolute(hook.sourcePath) && await resolve(hook.sourcePath) === target)
|
|
77
|
+
inFile.push(hook);
|
|
78
|
+
const owned = [];
|
|
79
|
+
const mismatches = [];
|
|
80
|
+
for (const definition of expected) {
|
|
81
|
+
const candidates = inFile.filter((hook) => hook.eventName === camelEvent(definition.event)
|
|
82
|
+
&& hook.handlerType === 'command' && hook.command === definition.command);
|
|
83
|
+
const exact = candidates.filter((hook) => hook.async === definition.async && hook.timeoutSec === definition.timeoutSec
|
|
84
|
+
&& hook.matcher === definition.matcher && hook.source === 'user' && !hook.isManaged && hook.trustStatus !== 'managed'
|
|
85
|
+
&& hook.key.startsWith(`${hook.sourcePath}:`) && hook.currentHash.length > 0);
|
|
86
|
+
if (candidates.length !== 1 || exact.length !== 1 || owned.includes(exact[0]))
|
|
87
|
+
mismatches.push(definition.event);
|
|
88
|
+
else
|
|
89
|
+
owned.push(exact[0]);
|
|
90
|
+
}
|
|
91
|
+
return { owned, mismatches, skipped: listed.length - owned.length };
|
|
92
|
+
}
|
|
93
|
+
/** `SessionStart` -> `sessionStart`, the app-server's camelCase HookEventName. */
|
|
94
|
+
function camelEvent(event) { return event.charAt(0).toLowerCase() + event.slice(1); }
|
|
95
|
+
function eventFromKey(hook) { return hook.eventName.charAt(0).toUpperCase() + hook.eventName.slice(1); }
|
|
96
|
+
function countStatus(hooks, status) { return hooks.filter((hook) => hook.trustStatus === status).length; }
|
|
97
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
98
|
+
async function resolve(file) {
|
|
99
|
+
try {
|
|
100
|
+
return await realpath(file);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return path.resolve(file);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function expectedCodexHooks(entries) {
|
|
107
|
+
return entries.flatMap(({ event, group }) => {
|
|
108
|
+
const matcher = typeof group.matcher === 'string' && group.matcher !== '' ? group.matcher : null;
|
|
109
|
+
const handlers = Array.isArray(group.hooks) ? group.hooks : [];
|
|
110
|
+
return handlers.filter((hook) => hook.type === 'command' && typeof hook.command === 'string').map((hook) => ({
|
|
111
|
+
event, command: hook.command, async: hook.async === true,
|
|
112
|
+
timeoutSec: typeof hook.timeout === 'number' ? hook.timeout : 600, matcher
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=codexTrust.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codexTrust.js","sourceRoot":"","sources":["../../../src/server/hooks/codexTrust.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type CodexHookTrust } from './codexTrust.js';
|
|
2
|
+
export type { CodexHookTrust } from './codexTrust.js';
|
|
1
3
|
export type HookProvider = 'codex' | 'claude';
|
|
2
4
|
export interface ProviderHookInstallation {
|
|
3
5
|
provider: HookProvider;
|
|
@@ -10,7 +12,15 @@ export interface ProviderHookInstallation {
|
|
|
10
12
|
unsafePath?: string;
|
|
11
13
|
/** Non-fatal, bounded reason codes that can explain why installed hooks may not run. */
|
|
12
14
|
warnings?: string[];
|
|
15
|
+
/** Installed Codex hooks: how many NTerminal definitions exist and the helper command they run. */
|
|
16
|
+
hookCount?: number;
|
|
17
|
+
hookCommand?: string;
|
|
18
|
+
/** Codex's own trust report, from the last trust operation or check while the files are unchanged. */
|
|
19
|
+
trust?: CodexHookTrust;
|
|
13
20
|
}
|
|
21
|
+
export type CodexHookTrustResult = ProviderHookInstallation & {
|
|
22
|
+
trust: CodexHookTrust;
|
|
23
|
+
};
|
|
14
24
|
export interface HookInstallationOptions {
|
|
15
25
|
stateDir: string;
|
|
16
26
|
homeDir?: string;
|
|
@@ -29,6 +39,12 @@ export interface HookInstallationOptions {
|
|
|
29
39
|
/** Read-only managed policy locations; isolated tests provide synthetic directories. */
|
|
30
40
|
codexSystemDir?: string;
|
|
31
41
|
claudeManagedDir?: string;
|
|
42
|
+
/** Codex CLI for trust operations; default: `codex` resolved like the user's shell PATH. */
|
|
43
|
+
codexExecutable?: string;
|
|
44
|
+
/** Environment for the short-lived Codex app-server (tests); default: process.env. */
|
|
45
|
+
codexEnv?: NodeJS.ProcessEnv;
|
|
46
|
+
/** Hard limit for one Codex app-server session, including startup. */
|
|
47
|
+
codexTimeoutMs?: number;
|
|
32
48
|
}
|
|
33
49
|
export interface ManagedHelperRefresh {
|
|
34
50
|
updated: boolean;
|
|
@@ -45,6 +61,9 @@ export declare class HookInstallation {
|
|
|
45
61
|
private readonly nodeExecutable;
|
|
46
62
|
private readonly homes;
|
|
47
63
|
private resolved;
|
|
64
|
+
/** Last successful Codex trust report, valid only while hooks.json, config.toml, and the manifest are unchanged. */
|
|
65
|
+
private codexTrustCache;
|
|
66
|
+
private codexTrustCheck;
|
|
48
67
|
constructor(options: HookInstallationOptions);
|
|
49
68
|
/**
|
|
50
69
|
* Resolve symlinks once per operation. The receiver authenticates the helper
|
|
@@ -56,6 +75,16 @@ export declare class HookInstallation {
|
|
|
56
75
|
status(provider: HookProvider): Promise<ProviderHookInstallation>;
|
|
57
76
|
install(provider: HookProvider): Promise<ProviderHookInstallation>;
|
|
58
77
|
remove(provider: HookProvider): Promise<ProviderHookInstallation>;
|
|
78
|
+
/**
|
|
79
|
+
* After explicit operator confirmation: ask Codex (a private, short-lived
|
|
80
|
+
* `codex app-server`) to trust exactly NTerminal's installed hooks, as
|
|
81
|
+
* pressing Trust in `/hooks` would. Serialized with install/remove.
|
|
82
|
+
*/
|
|
83
|
+
trustCodexHooks(): Promise<CodexHookTrustResult>;
|
|
84
|
+
/** Read-only `hooks/list` report, reused while the relevant files are unchanged. */
|
|
85
|
+
checkCodexTrust(): Promise<CodexHookTrustResult>;
|
|
86
|
+
private codexTrust;
|
|
87
|
+
private codexFingerprint;
|
|
59
88
|
refreshManagedHelper(): Promise<ManagedHelperRefresh>;
|
|
60
89
|
private helperReferenced;
|
|
61
90
|
private displayPaths;
|
|
@@ -6,6 +6,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { HookControlError as InstallationError, ensureDirectory, missing, snapshot, replace } from './hookFiles.js';
|
|
7
7
|
import { acquireVerifiedLock } from './verifiedLock.js';
|
|
8
8
|
import { HelperJournal } from './helperJournal.js';
|
|
9
|
+
import { resolveCodexExecutable } from './codexAppServer.js';
|
|
10
|
+
import { expectedCodexHooks, runCodexTrust } from './codexTrust.js';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
9
12
|
// Subagent events are not registered: the runtime ignores child lifecycle events.
|
|
10
13
|
const CODEX_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PermissionRequest', 'Stop', 'Interrupt', 'SessionEnd'];
|
|
11
14
|
const CLAUDE_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PermissionRequest', 'Stop', 'StopFailure', 'PostToolUseFailure', 'Notification', 'SessionEnd'];
|
|
@@ -32,6 +35,9 @@ export class HookInstallation {
|
|
|
32
35
|
nodeExecutable;
|
|
33
36
|
homes;
|
|
34
37
|
resolved = {};
|
|
38
|
+
/** Last successful Codex trust report, valid only while hooks.json, config.toml, and the manifest are unchanged. */
|
|
39
|
+
codexTrustCache = null;
|
|
40
|
+
codexTrustCheck = null;
|
|
35
41
|
constructor(options) {
|
|
36
42
|
this.options = options;
|
|
37
43
|
const home = options.homeDir ?? os.homedir();
|
|
@@ -128,8 +134,19 @@ export class HookInstallation {
|
|
|
128
134
|
catch {
|
|
129
135
|
return finish({ ...result, reason: 'node_unavailable' });
|
|
130
136
|
}
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
if (provider === 'claude')
|
|
138
|
+
return finish({ ...result, installed: true, ...(disabled ? { reason: 'hooks_disabled' } : {}) });
|
|
139
|
+
const expected = expectedCodexHooks(manifest.entries);
|
|
140
|
+
const commands = [...new Set(expected.map((hook) => hook.command))];
|
|
141
|
+
const codex = { ...result, installed: true, hookCount: expected.length, ...(commands.length === 1 ? { hookCommand: commands[0] } : {}) };
|
|
142
|
+
if (disabled)
|
|
143
|
+
return finish({ ...codex, reason: 'hooks_disabled' });
|
|
144
|
+
// Cheap: reuse Codex's last report only while every input file is byte-identical.
|
|
145
|
+
const trust = this.codexTrustCache && this.codexTrustCache.fingerprint === await this.codexFingerprint(context)
|
|
146
|
+
? this.codexTrustCache.trust : null;
|
|
147
|
+
if (!trust)
|
|
148
|
+
return finish({ ...codex, reason: 'approval_unknown' });
|
|
149
|
+
return finish({ ...codex, approvalRequired: trust.pending > 0, trust, ...(trust.pending > 0 ? { reason: 'approval_required' } : {}) });
|
|
133
150
|
}
|
|
134
151
|
catch (error) {
|
|
135
152
|
return { ...display, ...failure(error) };
|
|
@@ -141,6 +158,85 @@ export class HookInstallation {
|
|
|
141
158
|
async remove(provider) {
|
|
142
159
|
return this.change(provider, true);
|
|
143
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* After explicit operator confirmation: ask Codex (a private, short-lived
|
|
163
|
+
* `codex app-server`) to trust exactly NTerminal's installed hooks, as
|
|
164
|
+
* pressing Trust in `/hooks` would. Serialized with install/remove.
|
|
165
|
+
*/
|
|
166
|
+
async trustCodexHooks() {
|
|
167
|
+
return this.codexTrust(true);
|
|
168
|
+
}
|
|
169
|
+
/** Read-only `hooks/list` report, reused while the relevant files are unchanged. */
|
|
170
|
+
async checkCodexTrust() {
|
|
171
|
+
if (this.codexTrustCheck)
|
|
172
|
+
return this.codexTrustCheck;
|
|
173
|
+
this.codexTrustCheck = this.codexTrust(false).finally(() => { this.codexTrustCheck = null; });
|
|
174
|
+
return this.codexTrustCheck;
|
|
175
|
+
}
|
|
176
|
+
async codexTrust(write) {
|
|
177
|
+
const report = (reason, total = 0) => ({ total, trusted: 0, alreadyTrusted: 0, pending: total, skipped: 0, reason, checkedAt: new Date().toISOString() });
|
|
178
|
+
if (this.options.previewMode)
|
|
179
|
+
return { ...this.result('codex'), reason: 'preview_mode', trust: report('preview_mode') };
|
|
180
|
+
const releases = [];
|
|
181
|
+
let trust;
|
|
182
|
+
try {
|
|
183
|
+
await this.prepare();
|
|
184
|
+
const context = this.context('codex');
|
|
185
|
+
if (!await snapshot(context.manifestPath))
|
|
186
|
+
return { ...await this.status('codex'), trust: report('hooks_not_installed') };
|
|
187
|
+
if (write) {
|
|
188
|
+
releases.push((await acquireVerifiedLock(path.join(this.hooksDir, '.installation.lock'), this.options.procRoot, this.options.machineIdPath)).release);
|
|
189
|
+
releases.push((await acquireVerifiedLock(path.join(path.dirname(context.configPath), '.nterminal-hooks.lock'), this.options.procRoot, this.options.machineIdPath)).release);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const cached = this.codexTrustCache;
|
|
193
|
+
if (cached && cached.fingerprint === await this.codexFingerprint(context))
|
|
194
|
+
return { ...await this.status('codex'), trust: cached.trust };
|
|
195
|
+
}
|
|
196
|
+
const status = await this.status('codex');
|
|
197
|
+
const manifest = await this.manifest(context);
|
|
198
|
+
if (!status.installed || !manifest) {
|
|
199
|
+
trust = report(status.reason && status.reason !== 'approval_unknown' ? status.reason : 'hooks_not_installed');
|
|
200
|
+
}
|
|
201
|
+
else if (status.reason === 'hooks_disabled') {
|
|
202
|
+
trust = report('hooks_disabled', status.hookCount);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const expected = expectedCodexHooks(manifest.entries);
|
|
206
|
+
const env = this.options.codexEnv ?? process.env;
|
|
207
|
+
const executable = this.options.codexExecutable ?? await resolveCodexExecutable(env, this.options.homeDir ?? os.homedir());
|
|
208
|
+
if (!executable)
|
|
209
|
+
trust = report('codex_unavailable', expected.length);
|
|
210
|
+
else {
|
|
211
|
+
try {
|
|
212
|
+
// The same CODEX_HOME the installer used, as configured (Codex's trust keys embed this path).
|
|
213
|
+
trust = await runCodexTrust({ executable, codexHome: this.homes.codex, configPath: context.configPath, expected, write,
|
|
214
|
+
deadline: Date.now() + (this.options.codexTimeoutMs ?? 10_000), env });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
trust = report(errorCode(error), expected.length);
|
|
218
|
+
}
|
|
219
|
+
this.codexTrustCache = trust.reason ? null : { fingerprint: await this.codexFingerprint(context), trust };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ...await this.status('codex'), trust };
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return { ...await this.status('codex'), trust: report(errorCode(error)) };
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
for (const release of releases.reverse())
|
|
229
|
+
await release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async codexFingerprint(context) {
|
|
233
|
+
const hash = createHash('sha256').update(this.homes.codex);
|
|
234
|
+
for (const file of [context.configPath, context.notifyPath, context.manifestPath]) {
|
|
235
|
+
const saved = file ? await snapshot(file) : null;
|
|
236
|
+
hash.update('\0').update(saved ? saved.text : '<missing>');
|
|
237
|
+
}
|
|
238
|
+
return hash.digest('hex');
|
|
239
|
+
}
|
|
144
240
|
async refreshManagedHelper() {
|
|
145
241
|
if (this.options.previewMode)
|
|
146
242
|
return { updated: false, reason: 'preview_mode' };
|