evolcore 0.0.22 → 0.0.24
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/CHANGELOG.md +74 -14
- package/README.md +4 -0
- package/bin/codex-managed-acl.ps1 +160 -0
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +384 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +30 -2
- package/dist/agents/claude-runner.js +179 -240
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +369 -0
- package/dist/agents/codex-runner.js +839 -192
- package/dist/agents/ecagent-runner.js +27 -34
- package/dist/agents/gemini-runner.js +62 -8
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +43 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +57 -5
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +7 -1
- package/dist/channels/aun.js +178 -77
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wechat.js +81 -10
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +108 -24
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +289 -61
- package/dist/cli/index.js +22 -2
- package/dist/cli/init.js +2 -10
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +75 -18
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +14 -2
- package/dist/core/auth/remote-access-store.js +136 -0
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +128 -30
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +6 -1
- package/dist/core/command/menu-handler.js +410 -98
- package/dist/core/command/menu-protocol.js +123 -0
- package/dist/core/command/slash-gate.js +4 -6
- package/dist/core/command/slash-handler.js +199 -70
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +147 -94
- package/dist/core/model/catalog-service.js +310 -0
- package/dist/core/model/model-catalog.js +134 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +5 -2
- package/dist/core/permission/approval-gateway.js +183 -32
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +257 -31
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +214 -57
- package/dist/ipc.js +191 -102
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +16 -2
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +70 -15
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +16 -4
- package/dist/utils/codex-cli.js +0 -43
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import readline from 'readline';
|
|
3
|
-
import { logger } from '
|
|
4
|
-
import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '
|
|
5
|
-
import { resolveCodexLaunchCommand } from '../
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../../core/permission/sandbox-runtime.js';
|
|
5
|
+
import { resolveCodexLaunchCommand } from '../baseagent.js';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
|
-
import { sanitizeShellExecutionEnvironment } from '
|
|
7
|
+
import { sanitizeShellExecutionEnvironment } from '../../core/permission/shell-environment.js';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { ensureProcessManagedTempDir } from '
|
|
11
|
-
import { getPackageRoot, resolvePaths } from '
|
|
12
|
-
import { killProcess } from '
|
|
13
|
-
import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '
|
|
10
|
+
import { ensureProcessManagedTempDir } from '../../cli/task-context.js';
|
|
11
|
+
import { getPackageRoot, resolvePaths } from '../../paths.js';
|
|
12
|
+
import { killProcess } from '../../utils/cross-platform.js';
|
|
13
|
+
import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from './app-server-registry.js';
|
|
14
14
|
const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
|
|
15
|
+
export const CODEX_THREAD_ACTIVE_WRITER = 'THREAD_ACTIVE_WRITER';
|
|
15
16
|
function tomlProviderKey(value) {
|
|
16
17
|
if (!value || /[\0\r\n]/.test(value)) {
|
|
17
18
|
throw new Error(`Invalid Codex provider name: ${value}`);
|
|
@@ -64,6 +65,7 @@ export class CodexAppServerClient {
|
|
|
64
65
|
initialized = false;
|
|
65
66
|
initializing = null;
|
|
66
67
|
stderrBuffer = [];
|
|
68
|
+
stderrSequence = 0;
|
|
67
69
|
managedHookRuntimeDir = null;
|
|
68
70
|
processRegistrationPath = null;
|
|
69
71
|
constructor(options) {
|
|
@@ -115,6 +117,61 @@ export class CodexAppServerClient {
|
|
|
115
117
|
...(options?.selectedCapabilityRoots ? { selectedCapabilityRoots: options.selectedCapabilityRoots } : {}),
|
|
116
118
|
});
|
|
117
119
|
}
|
|
120
|
+
async threadArchive(threadId) {
|
|
121
|
+
await this.request('thread/archive', { threadId });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
async threadUnarchive(threadId) {
|
|
125
|
+
return this.request('thread/unarchive', { threadId });
|
|
126
|
+
}
|
|
127
|
+
async threadUnsubscribe(threadId) {
|
|
128
|
+
return this.request('thread/unsubscribe', { threadId });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Drop this connection's subscription and wait until the server has released
|
|
132
|
+
* the in-memory thread. This is deliberately scoped to one thread; closing
|
|
133
|
+
* the app-server process would also disrupt unrelated concurrent sessions.
|
|
134
|
+
*/
|
|
135
|
+
async threadUnsubscribeAndWait(threadId, timeoutMs = 2_000) {
|
|
136
|
+
let closed = false;
|
|
137
|
+
let resolveClosed;
|
|
138
|
+
const closedPromise = new Promise(resolve => {
|
|
139
|
+
resolveClosed = resolve;
|
|
140
|
+
});
|
|
141
|
+
const removeNotificationHandler = this.onNotification(notification => {
|
|
142
|
+
if (notification.method !== 'thread/closed')
|
|
143
|
+
return;
|
|
144
|
+
const params = (notification.params || {});
|
|
145
|
+
const closedThreadId = params.threadId ?? params.thread_id;
|
|
146
|
+
if (closedThreadId !== threadId || closed)
|
|
147
|
+
return;
|
|
148
|
+
closed = true;
|
|
149
|
+
resolveClosed();
|
|
150
|
+
});
|
|
151
|
+
let timer;
|
|
152
|
+
try {
|
|
153
|
+
const response = await this.threadUnsubscribe(threadId);
|
|
154
|
+
if (response.status !== 'unsubscribed')
|
|
155
|
+
return response;
|
|
156
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
157
|
+
timer = setTimeout(() => {
|
|
158
|
+
if (closed)
|
|
159
|
+
return;
|
|
160
|
+
const error = new Error(`thread/closed timed out for thread ${threadId}`);
|
|
161
|
+
error.code = 'CODEX_THREAD_CLOSE_TIMEOUT';
|
|
162
|
+
reject(error);
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
timer.unref?.();
|
|
165
|
+
});
|
|
166
|
+
await Promise.race([closedPromise, timeoutPromise]);
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (timer)
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
removeNotificationHandler();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
118
175
|
async turnStart(threadId, input, options) {
|
|
119
176
|
const sandboxPolicy = this.toTurnSandboxPolicy(options?.sandbox);
|
|
120
177
|
return this.request('turn/start', {
|
|
@@ -295,8 +352,8 @@ export class CodexAppServerClient {
|
|
|
295
352
|
await this.request('turn/interrupt', { threadId, turnId });
|
|
296
353
|
return true;
|
|
297
354
|
}
|
|
298
|
-
async modelList(includeHidden = false) {
|
|
299
|
-
return this.request('model/list', { includeHidden });
|
|
355
|
+
async modelList(includeHidden = false, cursor) {
|
|
356
|
+
return this.request('model/list', { includeHidden, ...(cursor ? { cursor } : {}) });
|
|
300
357
|
}
|
|
301
358
|
async modelProviderCapabilitiesRead() {
|
|
302
359
|
return this.request('modelProvider/capabilities/read', {});
|
|
@@ -431,6 +488,7 @@ export class CodexAppServerClient {
|
|
|
431
488
|
const managedRequirements = this.ensureManagedHookRuntime();
|
|
432
489
|
const guardedCommand = buildHClassGuardCommand(command, commandArgs, guardRoot, {
|
|
433
490
|
managedCodexRequirements: managedRequirements,
|
|
491
|
+
protectEvolcoreFilesystem: this.options.protectEvolcoreFilesystem !== false,
|
|
434
492
|
});
|
|
435
493
|
if (!guardedCommand) {
|
|
436
494
|
throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
|
|
@@ -482,7 +540,7 @@ export class CodexAppServerClient {
|
|
|
482
540
|
const text = String(chunk).trim();
|
|
483
541
|
if (!text)
|
|
484
542
|
return;
|
|
485
|
-
this.stderrBuffer.push(text);
|
|
543
|
+
this.stderrBuffer.push({ sequence: ++this.stderrSequence, text });
|
|
486
544
|
if (this.stderrBuffer.length > 20)
|
|
487
545
|
this.stderrBuffer.shift();
|
|
488
546
|
logger.debug(`[CodexAppServer] ${text}`);
|
|
@@ -555,9 +613,8 @@ export class CodexAppServerClient {
|
|
|
555
613
|
...process.env,
|
|
556
614
|
...this.options.env,
|
|
557
615
|
});
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
// per-thread session roots are injected later through thread config.
|
|
616
|
+
// Use the normal user CODEX_HOME so EvolCore and Codex Desktop share
|
|
617
|
+
// threads, SQLite state, writer locks, and sandbox state.
|
|
561
618
|
const managedTmpDir = ensureProcessManagedTempDir();
|
|
562
619
|
env.TMPDIR = managedTmpDir;
|
|
563
620
|
delete env.EVOLCORE_RUNTIME_LOCK_DIR;
|
|
@@ -576,8 +633,8 @@ export class CodexAppServerClient {
|
|
|
576
633
|
}
|
|
577
634
|
const stat = fs.lstatSync(runtimeLockDir);
|
|
578
635
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
579
|
-
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
580
|
-
|| (stat.mode & 0o077) !== 0) {
|
|
636
|
+
|| (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
637
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
581
638
|
throw new Error('runtime lock directory is not private');
|
|
582
639
|
}
|
|
583
640
|
env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
|
|
@@ -663,7 +720,8 @@ export class CodexAppServerClient {
|
|
|
663
720
|
if (typeof managedDir !== 'string' || !path.isAbsolute(managedDir)
|
|
664
721
|
|| !fs.existsSync(managedDir) || !managedHookLoaded || !lockdownEnabled || !hooksEnabled) {
|
|
665
722
|
throw new Error('Codex Windows proactive requires an installed and loaded EvolCore managed PreToolUse hook. '
|
|
666
|
-
+ '
|
|
723
|
+
+ 'ec start/restart automatically checks and installs missing hooks with Windows administrator approval. '
|
|
724
|
+
+ 'Restart EvolCore to install missing hooks and reload the Codex policy.');
|
|
667
725
|
}
|
|
668
726
|
}
|
|
669
727
|
buildProcessArgs() {
|
|
@@ -677,7 +735,7 @@ export class CodexAppServerClient {
|
|
|
677
735
|
const headerEnvNames = Object.fromEntries(Object.keys(this.options.headers ?? {}).map(name => [name, codexHeaderEnvName(name)]));
|
|
678
736
|
const hasHeaders = Object.keys(headerEnvNames).length > 0;
|
|
679
737
|
const hasQueryParams = Object.keys(this.options.queryParams ?? {}).length > 0;
|
|
680
|
-
if (hasHeaders || hasQueryParams) {
|
|
738
|
+
if (hasHeaders || hasQueryParams || this.options.baseUrl || this.options.provider) {
|
|
681
739
|
const configuredProvider = this.options.provider?.trim();
|
|
682
740
|
const useRuntimeProvider = !configuredProvider || BUILT_IN_CODEX_PROVIDER_IDS.has(configuredProvider.toLowerCase());
|
|
683
741
|
const providerName = !useRuntimeProvider
|
|
@@ -712,7 +770,13 @@ export class CodexAppServerClient {
|
|
|
712
770
|
if (params !== undefined && params !== null)
|
|
713
771
|
payload.params = params;
|
|
714
772
|
return new Promise((resolve, reject) => {
|
|
715
|
-
this.pending.set(id, {
|
|
773
|
+
this.pending.set(id, {
|
|
774
|
+
resolve,
|
|
775
|
+
reject,
|
|
776
|
+
method,
|
|
777
|
+
params,
|
|
778
|
+
stderrStartSequence: this.stderrSequence,
|
|
779
|
+
});
|
|
716
780
|
proc.stdin.write(`${JSON.stringify(payload)}\n`, error => {
|
|
717
781
|
if (!error)
|
|
718
782
|
return;
|
|
@@ -786,12 +850,18 @@ export class CodexAppServerClient {
|
|
|
786
850
|
// The CLI's one-time bubblewrap configuration warning is process-wide
|
|
787
851
|
// startup noise, not evidence about this RPC. Do not append it to an
|
|
788
852
|
// unrelated request failure such as thread/resume.
|
|
789
|
-
const stderrLines = this.stderrBuffer
|
|
853
|
+
const stderrLines = this.stderrBuffer
|
|
854
|
+
.filter(entry => entry.sequence > pending.stderrStartSequence)
|
|
855
|
+
.map(entry => entry.text)
|
|
856
|
+
.filter(line => !/could not find bubblewrap on PATH/i.test(line));
|
|
790
857
|
const stderr = stderrLines.length ? `\n${stderrLines.join('\n')}` : '';
|
|
791
858
|
const error = new Error(`${pending.method} failed: ${detail}${stderr}`);
|
|
792
859
|
if (/no rollout found for thread id/i.test(detail)) {
|
|
793
860
|
error.code = 'CODEX_ROLLOUT_NOT_FOUND';
|
|
794
861
|
}
|
|
862
|
+
else if (/already has an active writer/i.test(detail)) {
|
|
863
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
864
|
+
}
|
|
795
865
|
pending.reject(error);
|
|
796
866
|
return;
|
|
797
867
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolvePaths } from '
|
|
4
|
-
import { getProcessStartTime } from '
|
|
3
|
+
import { resolvePaths } from '../../paths.js';
|
|
4
|
+
import { getProcessStartTime } from '../../utils/process-introspect.js';
|
|
5
5
|
const CODEX_APP_SERVER_RECORD_PREFIX = 'codex-app-server-';
|
|
6
6
|
const CODEX_APP_SERVER_RECORD_SUFFIX = '.json';
|
|
7
7
|
function recordFilePath(pid) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { CodexAppServerClient } from './app-server-client.js';
|
|
3
|
+
import { resolveCodexLaunchCommand } from '../baseagent.js';
|
|
4
|
+
import { parseModelList } from '../../core/model/catalog-service.js';
|
|
5
|
+
async function cliModels() {
|
|
6
|
+
const launch = resolveCodexLaunchCommand(['debug', 'models']);
|
|
7
|
+
if (!launch)
|
|
8
|
+
return null;
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
execFile(launch.command, launch.args, { timeout: 3000, windowsHide: true, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8' }, (error, stdout) => {
|
|
11
|
+
try {
|
|
12
|
+
if (error)
|
|
13
|
+
return resolve(null);
|
|
14
|
+
const json = JSON.parse(stdout);
|
|
15
|
+
if (!Array.isArray(json?.models))
|
|
16
|
+
return resolve(null);
|
|
17
|
+
resolve(parseModelList(json.models.filter((m) => m.visibility === 'list'))
|
|
18
|
+
.map(m => ({ ...m, verification: 'unverified' })));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
resolve(null);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** A catalog-only client uses the same transport configuration as inference. */
|
|
27
|
+
export async function codexNativeCatalog(context) {
|
|
28
|
+
if (context.baseagent !== 'codex')
|
|
29
|
+
return null;
|
|
30
|
+
const client = new CodexAppServerClient({
|
|
31
|
+
apiKey: context.apiKey ?? '', baseUrl: context.baseUrl, provider: context.provider,
|
|
32
|
+
headers: context.headers, queryParams: context.queryParams,
|
|
33
|
+
});
|
|
34
|
+
let timer;
|
|
35
|
+
let expired = false;
|
|
36
|
+
try {
|
|
37
|
+
return await Promise.race([
|
|
38
|
+
(async () => {
|
|
39
|
+
const models = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
let cursor;
|
|
42
|
+
do {
|
|
43
|
+
const page = await client.modelList(false, cursor);
|
|
44
|
+
if (expired)
|
|
45
|
+
throw new Error('Model catalog timeout');
|
|
46
|
+
if (!Array.isArray(page.data))
|
|
47
|
+
throw new Error('Invalid native model list');
|
|
48
|
+
models.push(...parseModelList(page.data.filter(m => !m.hidden)));
|
|
49
|
+
cursor = page.nextCursor || undefined;
|
|
50
|
+
if (cursor && (seen.has(cursor) || seen.size >= 100))
|
|
51
|
+
throw new Error('Invalid native model pagination');
|
|
52
|
+
if (cursor)
|
|
53
|
+
seen.add(cursor);
|
|
54
|
+
} while (cursor);
|
|
55
|
+
return [...new Map(models.map(m => [m.id, m])).values()];
|
|
56
|
+
})(),
|
|
57
|
+
new Promise((_, reject) => { timer = setTimeout(() => { expired = true; reject(new Error('Model catalog timeout')); }, 5000); }),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// The host CLI catalog has no guarantee of matching this Agent connection.
|
|
62
|
+
// It is useful metadata/suggestions and always remains unverified.
|
|
63
|
+
return await cliModels();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (timer)
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
await client.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { parse as parseToml } from 'smol-toml';
|
|
6
|
+
export function checkCodexProjectPath(value, options = {}) {
|
|
7
|
+
const configured = typeof value === 'string' ? value.trim() : '';
|
|
8
|
+
if (!configured) {
|
|
9
|
+
return { status: 'invalid', path: configured, warnings: [], reason: 'projects.defaultPath is not configured' };
|
|
10
|
+
}
|
|
11
|
+
const projectPath = path.resolve(configured);
|
|
12
|
+
const warnings = [];
|
|
13
|
+
if ((options.platform ?? process.platform) === 'win32') {
|
|
14
|
+
if (projectPath.length >= 240) {
|
|
15
|
+
warnings.push(`Windows path is long (${projectPath.length} characters); Codex or a child tool may reject it.`);
|
|
16
|
+
}
|
|
17
|
+
if (projectPath.startsWith('\\\\')) {
|
|
18
|
+
warnings.push('Windows UNC/network project path may be unavailable to Codex sandboxed processes.');
|
|
19
|
+
}
|
|
20
|
+
const systemRoot = process.env.SystemRoot ? path.resolve(process.env.SystemRoot).toLowerCase() : '';
|
|
21
|
+
if (systemRoot && projectPath.toLowerCase().startsWith(`${systemRoot}${path.sep}`)) {
|
|
22
|
+
warnings.push('Project path is under the Windows system directory; use a dedicated workspace when possible.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const root = path.parse(projectPath).root;
|
|
26
|
+
if (projectPath === root)
|
|
27
|
+
warnings.push('Project path is a filesystem root; use a dedicated project directory when possible.');
|
|
28
|
+
try {
|
|
29
|
+
const stat = fs.statSync(projectPath);
|
|
30
|
+
if (!stat.isDirectory())
|
|
31
|
+
return { status: 'not-directory', path: projectPath, warnings, reason: 'path exists but is not a directory' };
|
|
32
|
+
try {
|
|
33
|
+
fs.accessSync(projectPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return { status: 'inaccessible', path: projectPath, warnings, reason: error instanceof Error ? error.message : String(error) };
|
|
37
|
+
}
|
|
38
|
+
return { status: 'ok', path: projectPath, warnings };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error?.code === 'ENOENT') {
|
|
42
|
+
return { status: 'missing', path: projectPath, warnings, reason: 'directory does not exist' };
|
|
43
|
+
}
|
|
44
|
+
return { status: 'inaccessible', path: projectPath, warnings, reason: error instanceof Error ? error.message : String(error) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function codexManagedHookPaths(options = {}) {
|
|
48
|
+
if ((options.platform ?? process.platform) !== 'win32')
|
|
49
|
+
return null;
|
|
50
|
+
const programData = options.programData || process.env.ProgramData || path.join(options.homeDir || os.homedir(), 'ProgramData');
|
|
51
|
+
const requirements = path.resolve(programData, 'OpenAI', 'Codex', 'requirements.toml');
|
|
52
|
+
const managedDir = path.join(path.dirname(requirements), 'EvolCore', 'hooks');
|
|
53
|
+
return { requirements, managedDir, hookPath: path.join(managedDir, 'pre-tool-use.mjs') };
|
|
54
|
+
}
|
|
55
|
+
function decodeProbeOutput(buffer) {
|
|
56
|
+
if (!buffer?.length)
|
|
57
|
+
return '';
|
|
58
|
+
try {
|
|
59
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return new TextDecoder('gb18030').decode(buffer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function windowsCommandArg(value) {
|
|
66
|
+
return '"' + value.replaceAll('"', '\\"') + '"';
|
|
67
|
+
}
|
|
68
|
+
function expectedWindowsHookCommand(nodePath, hookPath) {
|
|
69
|
+
return windowsCommandArg(nodePath) + ' ' + windowsCommandArg(hookPath) + ' %EVOLCORE_INSTANCE_SOCKET%';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run synthetic Windows shell probes for the managed Codex hook.
|
|
73
|
+
*
|
|
74
|
+
* This is intentionally called by ec diagnose, never by the hot hook path.
|
|
75
|
+
* The probes pass {} and therefore stop before contacting a live daemon.
|
|
76
|
+
*/
|
|
77
|
+
export function runCodexHookLaunchDiagnostic(options = {}) {
|
|
78
|
+
if ((options.platform ?? process.platform) !== 'win32') {
|
|
79
|
+
return {
|
|
80
|
+
status: 'skipped',
|
|
81
|
+
reason: 'Windows managed-hook launch probes are not applicable on this platform',
|
|
82
|
+
results: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const paths = codexManagedHookPaths(options);
|
|
86
|
+
if (!paths) {
|
|
87
|
+
return {
|
|
88
|
+
status: 'skipped',
|
|
89
|
+
reason: 'Windows managed-hook paths are unavailable on this platform',
|
|
90
|
+
results: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const requirements = path.resolve(options.requirements || paths.requirements);
|
|
94
|
+
const managedDir = path.resolve(options.managedDir || paths.managedDir);
|
|
95
|
+
const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
|
|
96
|
+
const output = path.resolve(options.output
|
|
97
|
+
|| path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'log', 'evolcore-hook-diagnostic.json'));
|
|
98
|
+
const runtimeLog = output + '.runtime.jsonl';
|
|
99
|
+
const nodePath = path.resolve(options.nodePath || process.execPath);
|
|
100
|
+
const config = parseToml(fs.readFileSync(requirements, 'utf8'));
|
|
101
|
+
const configuredDir = config.hooks?.windows_managed_dir ?? config.hooks?.managed_dir;
|
|
102
|
+
if (typeof configuredDir !== 'string')
|
|
103
|
+
throw new Error('No Windows managed hook directory configured.');
|
|
104
|
+
const expected = expectedWindowsHookCommand(nodePath, hookPath);
|
|
105
|
+
const handlers = Array.isArray(config.hooks?.PreToolUse)
|
|
106
|
+
? config.hooks.PreToolUse.flatMap((group) => group?.hooks || [])
|
|
107
|
+
: [];
|
|
108
|
+
const handler = handlers.find((item) => (item?.type === 'command'
|
|
109
|
+
&& (item.command_windows ?? item.commandWindows) === expected));
|
|
110
|
+
if (!handler || /[\r\n%;&|<>]/.test(nodePath + hookPath)) {
|
|
111
|
+
throw new Error('Managed command differs from the expected EvolCore command for this Node path; no hooks executed.');
|
|
112
|
+
}
|
|
113
|
+
fs.mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 });
|
|
114
|
+
const env = { ...process.env, EVOLCORE_CODEX_HOOK_LOG: runtimeLog };
|
|
115
|
+
delete env.EVOLCORE_INSTANCE_SOCKET;
|
|
116
|
+
const comSpec = options.comSpec || process.env.ComSpec || 'C:/Windows/System32/cmd.exe';
|
|
117
|
+
const probes = [
|
|
118
|
+
{ label: 'direct_node', executable: nodePath, args: [hookPath] },
|
|
119
|
+
{ label: 'cmd_raw_command', executable: comSpec, args: ['/d', '/s', '/c', expected], windowsVerbatimArguments: true },
|
|
120
|
+
{ label: 'cmd_outer_quotes', executable: comSpec, args: ['/d', '/s', '/c', '"' + expected + '"'], windowsVerbatimArguments: true },
|
|
121
|
+
];
|
|
122
|
+
const results = probes.map(probe => {
|
|
123
|
+
const result = spawnSync(probe.executable, probe.args, {
|
|
124
|
+
input: '{}',
|
|
125
|
+
env,
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
timeout: 5000,
|
|
128
|
+
maxBuffer: 64 * 1024,
|
|
129
|
+
windowsVerbatimArguments: probe.windowsVerbatimArguments,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
label: probe.label,
|
|
133
|
+
pid: result.pid,
|
|
134
|
+
exitCode: result.status,
|
|
135
|
+
signal: result.signal,
|
|
136
|
+
error: result.error?.message,
|
|
137
|
+
stdout: decodeProbeOutput(result.stdout),
|
|
138
|
+
stderr: decodeProbeOutput(result.stderr),
|
|
139
|
+
stderrBase64: result.stderr?.toString('base64'),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
fs.writeFileSync(output, JSON.stringify({
|
|
143
|
+
time: new Date().toISOString(),
|
|
144
|
+
requirements,
|
|
145
|
+
hookPath,
|
|
146
|
+
command: expected,
|
|
147
|
+
nodeVersion: process.version,
|
|
148
|
+
nodePath,
|
|
149
|
+
socketEnvironmentPresent: Boolean(process.env.EVOLCORE_INSTANCE_SOCKET),
|
|
150
|
+
runtimeLog,
|
|
151
|
+
note: 'Synthetic launch probes; shell variants do not establish how Codex invoked the hook.',
|
|
152
|
+
results,
|
|
153
|
+
}, null, 2) + '\n', { mode: 0o600 });
|
|
154
|
+
return { status: 'ok', output, runtimeLog, results };
|
|
155
|
+
}
|
|
156
|
+
function regularFileState(file) {
|
|
157
|
+
try {
|
|
158
|
+
const stat = fs.lstatSync(file);
|
|
159
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
160
|
+
return 'unsafe';
|
|
161
|
+
return 'ok';
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error?.code === 'ENOENT')
|
|
165
|
+
return 'missing';
|
|
166
|
+
return 'unavailable';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function realDirectoryState(directory) {
|
|
170
|
+
try {
|
|
171
|
+
const stat = fs.lstatSync(directory);
|
|
172
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
173
|
+
return 'unsafe';
|
|
174
|
+
return 'ok';
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error?.code === 'ENOENT')
|
|
178
|
+
return 'missing';
|
|
179
|
+
return 'unavailable';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Read-only inspection of the machine-wide Windows Codex managed hook. */
|
|
183
|
+
export function inspectCodexManagedHook(options = {}) {
|
|
184
|
+
const paths = codexManagedHookPaths(options);
|
|
185
|
+
if (!paths)
|
|
186
|
+
return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
|
|
187
|
+
const requirements = path.resolve(options.requirements || paths.requirements);
|
|
188
|
+
const managedDir = path.resolve(options.managedDir || paths.managedDir);
|
|
189
|
+
const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
|
|
190
|
+
const configDirState = realDirectoryState(path.dirname(requirements));
|
|
191
|
+
const managedDirState = realDirectoryState(managedDir);
|
|
192
|
+
if ([configDirState, managedDirState].includes('unsafe')) {
|
|
193
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements or managed-hook directory is not a real directory' };
|
|
194
|
+
}
|
|
195
|
+
if ([configDirState, managedDirState].includes('unavailable')) {
|
|
196
|
+
return { status: 'unavailable', requirements, managedDir, hookPath, reason: 'unable to inspect requirements or managed-hook directory' };
|
|
197
|
+
}
|
|
198
|
+
const requirementState = regularFileState(requirements);
|
|
199
|
+
const hookState = regularFileState(hookPath);
|
|
200
|
+
if ([requirementState, hookState].includes('unsafe')) {
|
|
201
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements.toml or launcher is not a regular file' };
|
|
202
|
+
}
|
|
203
|
+
if ([requirementState, hookState].includes('unavailable')) {
|
|
204
|
+
return { status: 'unavailable', requirements, managedDir, hookPath, reason: 'unable to read requirements.toml or launcher' };
|
|
205
|
+
}
|
|
206
|
+
if (requirementState === 'missing' && hookState === 'ok') {
|
|
207
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
|
|
208
|
+
}
|
|
209
|
+
if (requirementState === 'ok' && hookState === 'missing') {
|
|
210
|
+
// Match the installer's repair contract: a partial managed installation
|
|
211
|
+
// is drift; only an untouched user requirements file can be provisioned.
|
|
212
|
+
try {
|
|
213
|
+
const source = fs.readFileSync(requirements, 'utf8');
|
|
214
|
+
const partial = parseToml(source);
|
|
215
|
+
const hooks = partial.hooks;
|
|
216
|
+
if (source.includes('# BEGIN EVOLCORE MANAGED CODEX HOOK')
|
|
217
|
+
|| source.includes('# END EVOLCORE MANAGED CODEX HOOK')
|
|
218
|
+
|| partial.allow_managed_hooks_only !== undefined
|
|
219
|
+
|| hooks?.windows_managed_dir !== undefined
|
|
220
|
+
|| hooks?.managed_dir !== undefined) {
|
|
221
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements contains managed-hook policy but the canonical launcher is missing' };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: `requirements.toml cannot be safely repaired: ${error instanceof Error ? error.message : String(error)}` };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if ([requirementState, hookState].includes('missing')) {
|
|
229
|
+
return { status: 'missing', requirements, managedDir, hookPath, reason: 'requirements.toml or launcher is missing' };
|
|
230
|
+
}
|
|
231
|
+
let config;
|
|
232
|
+
let launcher;
|
|
233
|
+
let source;
|
|
234
|
+
try {
|
|
235
|
+
source = fs.readFileSync(requirements, 'utf8');
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
config = parseToml(source);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return { status: 'unsafe', requirements, managedDir, hookPath, reason: `requirements.toml is invalid: ${error instanceof Error ? error.message : String(error)}` };
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
launcher = fs.readFileSync(hookPath, 'utf8');
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
|
|
251
|
+
}
|
|
252
|
+
const normalized = (value) => path.normalize(String(value || '')).toLowerCase();
|
|
253
|
+
const expectedPackageRoot = path.resolve(options.packageRoot || process.cwd());
|
|
254
|
+
const launcherValid = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
|
|
255
|
+
&& launcher.toLowerCase().includes(JSON.stringify(expectedPackageRoot).toLowerCase());
|
|
256
|
+
const configuredDir = config.hooks?.windows_managed_dir ?? config.hooks?.managed_dir;
|
|
257
|
+
const groups = config.hooks?.PreToolUse;
|
|
258
|
+
const canonicalCommand = (value) => {
|
|
259
|
+
if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value))
|
|
260
|
+
return false;
|
|
261
|
+
const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
|
|
262
|
+
return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
|
|
263
|
+
};
|
|
264
|
+
const hookLoaded = Array.isArray(groups) && groups.some((group) => group?.matcher === '*'
|
|
265
|
+
&& Array.isArray(group.hooks) && group.hooks.some((handler) => {
|
|
266
|
+
const command = typeof handler?.command === 'string' ? handler.command : '';
|
|
267
|
+
const windowsCommand = typeof handler?.command_windows === 'string' ? handler.command_windows : '';
|
|
268
|
+
return handler?.type === 'command' && handler.async !== true
|
|
269
|
+
&& canonicalCommand(command) && canonicalCommand(windowsCommand);
|
|
270
|
+
}));
|
|
271
|
+
const valid = config.allow_managed_hooks_only === true
|
|
272
|
+
&& config.features?.hooks === true
|
|
273
|
+
&& normalized(configuredDir) === normalized(managedDir)
|
|
274
|
+
&& launcherValid && hookLoaded;
|
|
275
|
+
return valid
|
|
276
|
+
? { status: 'installed', requirements, managedDir, hookPath }
|
|
277
|
+
: { status: 'unsafe', requirements, managedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
|
|
278
|
+
}
|
|
279
|
+
function powershellLiteral(value) {
|
|
280
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
281
|
+
}
|
|
282
|
+
/** Start-Process joins ArgumentList into a native command line, even for arrays. */
|
|
283
|
+
function windowsArgument(value) {
|
|
284
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
285
|
+
}
|
|
286
|
+
export function buildCodexManagedHookElevationCommand(options) {
|
|
287
|
+
const argumentLine = [options.installer, '--package-root', options.packageRoot,
|
|
288
|
+
'--requirements', options.requirements, '--managed-dir', options.managedDir, '--repair']
|
|
289
|
+
.map(windowsArgument).join(' ');
|
|
290
|
+
return [
|
|
291
|
+
"$ErrorActionPreference = 'Stop'",
|
|
292
|
+
"$ProgressPreference = 'SilentlyContinue'",
|
|
293
|
+
'try {',
|
|
294
|
+
` $p = Start-Process -FilePath ${powershellLiteral(options.nodePath)} -ArgumentList ${powershellLiteral(argumentLine)} -WorkingDirectory ${powershellLiteral(options.packageRoot)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru -ErrorAction Stop`,
|
|
295
|
+
' exit $p.ExitCode',
|
|
296
|
+
'} catch {',
|
|
297
|
+
' [Console]::Error.WriteLine($_.Exception.Message)',
|
|
298
|
+
' exit 1',
|
|
299
|
+
'}',
|
|
300
|
+
].join('\n');
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Ensure the machine-wide Windows hook before starting the daemon. Missing
|
|
304
|
+
* files are repaired through one UAC elevation; altered policy remains
|
|
305
|
+
* fail-closed and is never overwritten automatically.
|
|
306
|
+
*/
|
|
307
|
+
export function ensureCodexManagedHook(options) {
|
|
308
|
+
const platformName = options.platform ?? process.platform;
|
|
309
|
+
const paths = codexManagedHookPaths({
|
|
310
|
+
platform: platformName,
|
|
311
|
+
programData: options.programData,
|
|
312
|
+
homeDir: options.homeDir,
|
|
313
|
+
});
|
|
314
|
+
if (!paths)
|
|
315
|
+
return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
|
|
316
|
+
const inspectOptions = {
|
|
317
|
+
platform: platformName,
|
|
318
|
+
packageRoot: options.packageRoot,
|
|
319
|
+
requirements: options.requirements || paths.requirements,
|
|
320
|
+
managedDir: options.managedDir || paths.managedDir,
|
|
321
|
+
programData: options.programData,
|
|
322
|
+
homeDir: options.homeDir,
|
|
323
|
+
};
|
|
324
|
+
let state = inspectCodexManagedHook(inspectOptions);
|
|
325
|
+
if (state.status === 'installed')
|
|
326
|
+
return { ...state, repaired: false };
|
|
327
|
+
if (state.status !== 'missing')
|
|
328
|
+
return { ...state, repaired: false };
|
|
329
|
+
const installer = path.join(path.resolve(options.packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
|
|
330
|
+
if (!fs.existsSync(installer)) {
|
|
331
|
+
return { ...state, repaired: false, error: `managed-hook installer is missing: ${installer}` };
|
|
332
|
+
}
|
|
333
|
+
const runElevated = options.runElevated || ((script, packageRoot) => {
|
|
334
|
+
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
335
|
+
const command = buildCodexManagedHookElevationCommand({
|
|
336
|
+
nodePath: process.execPath, installer: script, packageRoot,
|
|
337
|
+
requirements: path.resolve(inspectOptions.requirements),
|
|
338
|
+
managedDir: path.resolve(inspectOptions.managedDir),
|
|
339
|
+
});
|
|
340
|
+
const result = spawnSync(powershell, [
|
|
341
|
+
// Do not use -NonInteractive here: Start-Process -Verb RunAs must be
|
|
342
|
+
// able to show the UAC consent dialog on the secure desktop.
|
|
343
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass',
|
|
344
|
+
'-EncodedCommand', Buffer.from(command, 'utf16le').toString('base64'),
|
|
345
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
346
|
+
if (result.error)
|
|
347
|
+
return { ok: false, detail: result.error.message };
|
|
348
|
+
if (result.status !== 0) {
|
|
349
|
+
const detail = (result.stderr || '').replace(/#<\s*CLIXML[\s\S]*?<\/Objs>\s*/gi, '').trim();
|
|
350
|
+
return { ok: false, detail: detail || `installer exited with code ${result.status}` };
|
|
351
|
+
}
|
|
352
|
+
return { ok: true };
|
|
353
|
+
});
|
|
354
|
+
options.onInstallRequired?.();
|
|
355
|
+
let elevated;
|
|
356
|
+
try {
|
|
357
|
+
elevated = runElevated(installer, path.resolve(options.packageRoot));
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
elevated = { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
361
|
+
}
|
|
362
|
+
if (!elevated.ok) {
|
|
363
|
+
return { ...state, repaired: false, error: elevated.detail || 'elevated managed-hook installation failed' };
|
|
364
|
+
}
|
|
365
|
+
state = inspectCodexManagedHook(inspectOptions);
|
|
366
|
+
return state.status === 'installed'
|
|
367
|
+
? { ...state, repaired: true }
|
|
368
|
+
: { ...state, repaired: false, error: state.reason || 'managed-hook installation did not pass verification' };
|
|
369
|
+
}
|