nterminal 1.3.5 → 1.3.7

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.
Files changed (47) hide show
  1. package/README.md +68 -37
  2. package/assets/screenshot/accounts.png +0 -0
  3. package/assets/screenshot/history.png +0 -0
  4. package/assets/screenshot/mobile-panes.png +0 -0
  5. package/assets/screenshot/mobile.png +0 -0
  6. package/assets/screenshot/scr.png +0 -0
  7. package/assets/screenshot/subagents.png +0 -0
  8. package/bin/nterminal.js +2 -0
  9. package/dist/client/assets/MarkdownPreview-Bx3a_UAM.js +1 -0
  10. package/dist/client/assets/{TranscriptMarkdownBody-CI0ztvMX.js → TranscriptMarkdownBody-CWvFo7OT.js} +1 -1
  11. package/dist/client/assets/index-BdHPdvab.js +58 -0
  12. package/dist/client/assets/{index-EN8ngMgv.js → index-Cc-QI7Ho.js} +1 -1
  13. package/dist/client/assets/index-W-zGfY1a.css +1 -0
  14. package/dist/client/index.html +2 -2
  15. package/dist/scripts/hooks.js +47 -6
  16. package/dist/scripts/hooks.js.map +1 -1
  17. package/dist/server/hooks/codexAppServer.d.ts +30 -0
  18. package/dist/server/hooks/codexAppServer.js +170 -0
  19. package/dist/server/hooks/codexAppServer.js.map +1 -0
  20. package/dist/server/hooks/codexTrust.d.ts +72 -0
  21. package/dist/server/hooks/codexTrust.js +116 -0
  22. package/dist/server/hooks/codexTrust.js.map +1 -0
  23. package/dist/server/hooks/hookInstallation.d.ts +29 -0
  24. package/dist/server/hooks/hookInstallation.js +98 -2
  25. package/dist/server/hooks/hookInstallation.js.map +1 -1
  26. package/dist/server/index.js +3 -1
  27. package/dist/server/index.js.map +1 -1
  28. package/dist/server/notifications/agentCompletionMonitor.js +2 -1
  29. package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
  30. package/dist/server/routes/hookRoutes.d.ts +1 -1
  31. package/dist/server/routes/hookRoutes.js +38 -6
  32. package/dist/server/routes/hookRoutes.js.map +1 -1
  33. package/dist/server/terminal/PtyAdapter.d.ts +1 -0
  34. package/dist/server/terminal/PtyAdapter.js.map +1 -1
  35. package/dist/server/terminal/TerminalManager.js +14 -5
  36. package/dist/server/terminal/TerminalManager.js.map +1 -1
  37. package/dist/server/terminal/TmuxPtyAdapter.js +3 -1
  38. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
  39. package/dist/server/terminal/codexTranscriptSource.d.ts +1 -0
  40. package/dist/server/terminal/codexTranscriptSource.js +58 -2
  41. package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
  42. package/docs/features.md +12 -2
  43. package/docs/provider-hooks.md +64 -11
  44. package/package.json +1 -1
  45. package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +0 -1
  46. package/dist/client/assets/index-JQJ1Eet1.js +0 -58
  47. 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
- return finish({ ...result, installed: true, ...(disabled ? { reason: 'hooks_disabled' }
132
- : provider === 'codex' ? { reason: 'approval_unknown' } : {}) });
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' };