evolcore 0.0.21 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +376 -96
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +44 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
- 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 +154 -0
- package/dist/agents/codex-runner.js +1292 -219
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -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 +51 -4
- package/dist/aun/outbox.js +7 -0
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +240 -36
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +169 -51
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +16 -2
- package/dist/cli/trigger-command.js +22 -5
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +15 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +70 -6
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +62 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +150 -33
- package/dist/core/command/menu-protocol.js +120 -1
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +83 -40
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +41 -15
- package/dist/core/message/response-engine.js +246 -114
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -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 +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +74 -14
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +290 -30
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +104 -34
- package/dist/ipc.js +218 -96
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +86 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +9 -4
- package/dist/utils/npm-ops.js +47 -15
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +159 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +256 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- 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/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +10 -2
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +11 -3
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
10
|
+
import { parse as parseToml } from 'smol-toml';
|
|
7
11
|
|
|
8
12
|
const markerStart = '# BEGIN EVOLCORE MANAGED CODEX HOOK';
|
|
9
13
|
const markerEnd = '# END EVOLCORE MANAGED CODEX HOOK';
|
|
14
|
+
const aclScript = fileURLToPath(new URL('./codex-managed-acl.ps1', import.meta.url));
|
|
10
15
|
|
|
11
16
|
function argument(name) {
|
|
12
17
|
const index = process.argv.indexOf(name);
|
|
@@ -22,6 +27,11 @@ function isGlobalInstall() {
|
|
|
22
27
|
return ['1', 'true'].includes(String(process.env.npm_config_global || '').toLowerCase());
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function lifecycleEvent() {
|
|
31
|
+
const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === 'npm_lifecycle_event');
|
|
32
|
+
return String(entry?.[1] || '').toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
function ensureRealDirectory(directory) {
|
|
26
36
|
let stat;
|
|
27
37
|
try {
|
|
@@ -82,14 +92,38 @@ function windowsCommandArg(value) {
|
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
function removeManagedBlock(source) {
|
|
85
|
-
const
|
|
86
|
-
|
|
95
|
+
const original = parseToml(source);
|
|
96
|
+
if (!source.includes(markerStart) && !source.includes(markerEnd)) return source;
|
|
97
|
+
const unsafe = () => new Error('Ambiguous EvolCore managed Hook markers in requirements.toml. Existing configuration was not modified.');
|
|
98
|
+
const expression = new RegExp(`^${markerStart}\\r?\\n[\\s\\S]*?^${markerEnd}(?:\\r?\\n|$)`, 'gm');
|
|
99
|
+
const matches = [...source.matchAll(expression)];
|
|
100
|
+
if (matches.length !== 1 || source.split(markerStart).length !== 2 || source.split(markerEnd).length !== 2) throw unsafe();
|
|
101
|
+
const block = parseToml(matches[0][0]);
|
|
102
|
+
const groups = block.hooks?.PreToolUse;
|
|
103
|
+
if (!Array.isArray(groups) || groups.length !== 1
|
|
104
|
+
|| !isDeepStrictEqual(block, { hooks: { PreToolUse: groups } })) throw unsafe();
|
|
105
|
+
const content = source.replace(matches[0][0], '');
|
|
106
|
+
const retained = parseToml(content);
|
|
107
|
+
const originalGroups = original.hooks?.PreToolUse;
|
|
108
|
+
if (!Array.isArray(originalGroups)) throw unsafe();
|
|
109
|
+
// Markers inside a TOML string are not comments. Prove that removing the
|
|
110
|
+
// block removes only its Hook group, with every other policy unchanged.
|
|
111
|
+
const safe = originalGroups.some((group, index) => {
|
|
112
|
+
if (!isDeepStrictEqual(group, groups[0])) return false;
|
|
113
|
+
const expected = structuredClone(original);
|
|
114
|
+
expected.hooks.PreToolUse.splice(index, 1);
|
|
115
|
+
if (!expected.hooks.PreToolUse.length && !retained.hooks?.PreToolUse) delete expected.hooks.PreToolUse;
|
|
116
|
+
if (!Object.keys(expected.hooks).length && !retained.hooks) delete expected.hooks;
|
|
117
|
+
return isDeepStrictEqual(expected, retained);
|
|
118
|
+
});
|
|
119
|
+
if (!safe) throw unsafe();
|
|
120
|
+
return content;
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
function tableBounds(lines, table) {
|
|
90
124
|
const header = table ? `[${table}]` : null;
|
|
91
125
|
const start = header
|
|
92
|
-
? lines.findIndex(line => line.trim() === header)
|
|
126
|
+
? lines.findIndex(line => line.trim() === header || line.trim().startsWith(`${header} #`))
|
|
93
127
|
: -1;
|
|
94
128
|
if (table && start < 0) return null;
|
|
95
129
|
const from = table ? start + 1 : 0;
|
|
@@ -127,51 +161,9 @@ function setTomlKey(source, table, key, value) {
|
|
|
127
161
|
return lines.join('\n');
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (error?.code !== 'EEXIST' && error?.code !== 'EPERM') throw error;
|
|
136
|
-
}
|
|
137
|
-
const backup = `${file}.evolcore-backup-${process.pid}-${Date.now()}`;
|
|
138
|
-
fs.renameSync(file, backup);
|
|
139
|
-
try {
|
|
140
|
-
fs.renameSync(temporary, file);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
try { fs.renameSync(backup, file); } catch {}
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
try { fs.rmSync(backup, { force: true }); } catch {}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function atomicWrite(file, content) {
|
|
149
|
-
const temporary = `${file}.evolcore-${process.pid}-${Date.now()}.tmp`;
|
|
150
|
-
try {
|
|
151
|
-
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o440, flag: 'wx' });
|
|
152
|
-
atomicReplace(file, temporary);
|
|
153
|
-
} finally {
|
|
154
|
-
try { fs.rmSync(temporary, { force: true }); } catch {}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function restrictWindowsAcl(target) {
|
|
159
|
-
if (process.platform !== 'win32') return;
|
|
160
|
-
// Keep the hook and requirements administrator-owned while allowing the
|
|
161
|
-
// daemon's normal user token to read and execute them.
|
|
162
|
-
execFileSync('icacls.exe', [
|
|
163
|
-
target,
|
|
164
|
-
'/inheritance:r',
|
|
165
|
-
'/grant:r',
|
|
166
|
-
'*S-1-5-18:(OI)(CI)F',
|
|
167
|
-
'*S-1-5-32-544:(OI)(CI)F',
|
|
168
|
-
'*S-1-5-32-545:(OI)(CI)RX',
|
|
169
|
-
], { stdio: 'ignore' });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function managedHookLauncher(packageRoot) {
|
|
173
|
-
// Keep the machine-wide entrypoint stable across npm upgrades. The app
|
|
174
|
-
// npm upgrades replace the package contents at this stable global path, so
|
|
164
|
+
export function managedHookLauncher(packageRoot) {
|
|
165
|
+
// Keep the machine-wide entrypoint stable across npm upgrades. Those
|
|
166
|
+
// upgrades replace the package contents at this stable global path, so
|
|
175
167
|
// this file never needs to be rewritten just because the hook changed.
|
|
176
168
|
const trustedPackageRoot = path.resolve(packageRoot);
|
|
177
169
|
return `#!/usr/bin/env node
|
|
@@ -191,47 +183,18 @@ try {
|
|
|
191
183
|
`;
|
|
192
184
|
}
|
|
193
185
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (process.platform === 'linux' && argument('--mountpoint')) {
|
|
200
|
-
runLinuxPostinstall();
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (process.platform !== 'win32' && !argument('--requirements')) {
|
|
204
|
-
fail('Codex managed hook provisioning is only supported on Windows.');
|
|
205
|
-
}
|
|
206
|
-
const packageRoot = argument('--package-root');
|
|
207
|
-
const nodePath = argument('--node-path') || process.execPath;
|
|
208
|
-
const requirements = argument('--requirements')
|
|
209
|
-
|| path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml');
|
|
210
|
-
if (!packageRoot) fail('--package-root is required.');
|
|
211
|
-
const existingRequirements = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
|
|
212
|
-
const targetDirectory = argument('--managed-dir')
|
|
213
|
-
|| path.join(path.dirname(requirements), 'EvolCore', 'hooks');
|
|
214
|
-
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
215
|
-
if (!fs.existsSync(sourceHook) || !fs.statSync(sourceHook).isFile()) fail(`Codex PreToolUse hook source was not found: ${sourceHook}`);
|
|
216
|
-
|
|
217
|
-
fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
|
|
218
|
-
const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
|
|
219
|
-
const temporaryHook = `${hookPath}.evolcore-${process.pid}-${Date.now()}.tmp`;
|
|
220
|
-
try {
|
|
221
|
-
fs.writeFileSync(temporaryHook, managedHookLauncher(packageRoot), { encoding: 'utf8', mode: 0o500, flag: 'wx' });
|
|
222
|
-
fs.chmodSync(temporaryHook, 0o500);
|
|
223
|
-
atomicReplace(hookPath, temporaryHook);
|
|
224
|
-
} finally {
|
|
225
|
-
try { fs.rmSync(temporaryHook, { force: true }); } catch {}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let content = existingRequirements;
|
|
229
|
-
content = removeManagedBlock(content);
|
|
186
|
+
export function buildManagedRequirements(source, { targetDirectory, nodePath, hookPath }) {
|
|
187
|
+
// Keep the existing text editor, but reject unsupported TOML layouts before
|
|
188
|
+
// writing anything. Parsing also catches duplicate tables/keys on reinstall.
|
|
189
|
+
let content = removeManagedBlock(source);
|
|
190
|
+
const before = parseToml(content);
|
|
230
191
|
content = setTomlKey(content, null, 'allow_managed_hooks_only', 'true');
|
|
231
192
|
content = setTomlKey(content, 'features', 'hooks', 'true');
|
|
232
193
|
content = setTomlKey(content, 'hooks', 'windows_managed_dir', tomlString(path.resolve(targetDirectory)));
|
|
233
194
|
if (!content.endsWith('\n')) content += '\n';
|
|
234
|
-
|
|
195
|
+
// Windows hook command must include %EVOLCORE_INSTANCE_SOCKET% so Codex
|
|
196
|
+
// expands it at runtime with the per-daemon socket path
|
|
197
|
+
const command = `${windowsCommandArg(nodePath)} ${windowsCommandArg(hookPath)} %EVOLCORE_INSTANCE_SOCKET%`;
|
|
235
198
|
content += [
|
|
236
199
|
markerStart,
|
|
237
200
|
'[[hooks.PreToolUse]]',
|
|
@@ -248,17 +211,334 @@ function main() {
|
|
|
248
211
|
markerEnd,
|
|
249
212
|
'',
|
|
250
213
|
].join('\n');
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
214
|
+
const after = parseToml(content);
|
|
215
|
+
const expected = {
|
|
216
|
+
...before,
|
|
217
|
+
allow_managed_hooks_only: true,
|
|
218
|
+
features: { ...before.features, hooks: true },
|
|
219
|
+
hooks: {
|
|
220
|
+
...before.hooks,
|
|
221
|
+
windows_managed_dir: path.resolve(targetDirectory),
|
|
222
|
+
PreToolUse: [
|
|
223
|
+
...(before.hooks?.PreToolUse || []),
|
|
224
|
+
{ matcher: '*', hooks: [{ type: 'command', command, command_windows: command, statusMessage: 'Checking EvolCore session policy' }] },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
if (!isDeepStrictEqual(after, expected)) {
|
|
229
|
+
throw new Error('Could not safely update requirements.toml without changing unrelated policy. Existing configuration was not modified.');
|
|
230
|
+
}
|
|
231
|
+
return content;
|
|
258
232
|
}
|
|
259
233
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
234
|
+
function regularFile(file) {
|
|
235
|
+
try {
|
|
236
|
+
const stat = fs.lstatSync(file);
|
|
237
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return { ok: false, unsafe: true };
|
|
238
|
+
return { ok: true };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error?.code === 'ENOENT') return { ok: false, unsafe: false };
|
|
241
|
+
return { ok: false, unsafe: true, error };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sourceHasManagedMarker(source) {
|
|
246
|
+
return source.includes(markerStart) || source.includes(markerEnd);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Inspect only the canonical EvolCore managed-hook targets. */
|
|
250
|
+
export function inspectManagedHookInstallation({ requirements, managedDir, packageRoot }) {
|
|
251
|
+
const canonicalRequirements = path.resolve(requirements);
|
|
252
|
+
const canonicalManagedDir = path.resolve(managedDir);
|
|
253
|
+
const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
|
|
254
|
+
for (const directory of [path.dirname(canonicalRequirements), canonicalManagedDir]) {
|
|
255
|
+
try {
|
|
256
|
+
const stat = fs.lstatSync(directory);
|
|
257
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
258
|
+
return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `${directory} must be a real directory` };
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error?.code !== 'ENOENT') return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const req = regularFile(canonicalRequirements);
|
|
265
|
+
const hook = regularFile(hookPath);
|
|
266
|
+
if (req.unsafe || hook.unsafe) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is not a regular file' };
|
|
267
|
+
if (!req.ok && hook.ok) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
|
|
268
|
+
if (req.ok && !hook.ok) {
|
|
269
|
+
// A missing launcher can be repaired only when the requirements file is
|
|
270
|
+
// still an untouched user configuration. If it already contains any
|
|
271
|
+
// EvolCore-managed policy, treat the partial state as drift and fail
|
|
272
|
+
// closed instead of guessing which policy is authoritative.
|
|
273
|
+
try {
|
|
274
|
+
const source = fs.readFileSync(canonicalRequirements, 'utf8');
|
|
275
|
+
const partial = parseToml(source);
|
|
276
|
+
const managedPolicyPresent = sourceHasManagedMarker(source)
|
|
277
|
+
|| partial.allow_managed_hooks_only !== undefined
|
|
278
|
+
|| partial.hooks?.windows_managed_dir !== undefined
|
|
279
|
+
|| partial.hooks?.managed_dir !== undefined;
|
|
280
|
+
if (managedPolicyPresent) {
|
|
281
|
+
return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements contains managed-hook policy but the canonical launcher is missing' };
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!req.ok || !hook.ok) return { status: 'missing', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is missing' };
|
|
288
|
+
let parsed;
|
|
289
|
+
try { parsed = parseToml(fs.readFileSync(canonicalRequirements, 'utf8')); }
|
|
290
|
+
catch (error) { return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` }; }
|
|
291
|
+
const configured = parsed.hooks?.windows_managed_dir ?? parsed.hooks?.managed_dir;
|
|
292
|
+
const normalized = value => path.normalize(String(value || '')).toLowerCase();
|
|
293
|
+
const launcher = fs.readFileSync(hookPath, 'utf8');
|
|
294
|
+
const packageMarker = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
|
|
295
|
+
&& launcher.toLowerCase().includes(JSON.stringify(path.resolve(packageRoot || process.cwd())).toLowerCase());
|
|
296
|
+
const preToolUse = parsed.hooks?.PreToolUse;
|
|
297
|
+
const canonicalCommand = value => {
|
|
298
|
+
if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value)) return false;
|
|
299
|
+
const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
|
|
300
|
+
return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
|
|
301
|
+
};
|
|
302
|
+
const loaded = Array.isArray(preToolUse) && preToolUse.some(group => group?.matcher === '*'
|
|
303
|
+
&& Array.isArray(group.hooks) && group.hooks.some(handler => handler?.type === 'command'
|
|
304
|
+
&& handler.async !== true && typeof handler.command === 'string'
|
|
305
|
+
&& canonicalCommand(handler.command)
|
|
306
|
+
&& canonicalCommand(handler.command_windows ?? handler.commandWindows)));
|
|
307
|
+
const valid = parsed.allow_managed_hooks_only === true
|
|
308
|
+
&& parsed.features?.hooks === true
|
|
309
|
+
&& normalized(configured) === normalized(canonicalManagedDir)
|
|
310
|
+
&& packageMarker && loaded;
|
|
311
|
+
return valid
|
|
312
|
+
? { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath }
|
|
313
|
+
: { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Idempotently repair a missing canonical installation; unsafe edits fail closed. */
|
|
317
|
+
export function repairManagedHookInstallation(options, acl = windowsAcl) {
|
|
318
|
+
const { requirements, managedDir, packageRoot, nodePath = process.execPath } = options;
|
|
319
|
+
const canonicalRequirements = path.resolve(requirements);
|
|
320
|
+
const canonicalManagedDir = path.resolve(managedDir);
|
|
321
|
+
const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
|
|
322
|
+
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
323
|
+
const state = inspectManagedHookInstallation({ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot });
|
|
324
|
+
if (state.status === 'installed') return { ...state, repaired: false, warnings: [] };
|
|
325
|
+
if (state.status === 'unsafe') throw new Error(`Refusing automatic managed-hook repair: ${state.reason}`);
|
|
326
|
+
if (!fs.existsSync(sourceHook)) throw new Error(`Required Hook runtime file was not found: ${sourceHook}`);
|
|
327
|
+
const previousContent = fs.existsSync(canonicalRequirements) ? fs.readFileSync(canonicalRequirements, 'utf8') : '';
|
|
328
|
+
const content = buildManagedRequirements(previousContent, { targetDirectory: canonicalManagedDir, nodePath, hookPath });
|
|
329
|
+
const warnings = provisionManagedFiles({ requirements: canonicalRequirements, hookPath, sourceHook, nodePath, previousContent, content, launcher: managedHookLauncher(packageRoot) }, acl);
|
|
330
|
+
return { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, repaired: true, warnings };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function windowsAcl(action, payload = {}, run = execFileSync) {
|
|
334
|
+
const executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
335
|
+
try {
|
|
336
|
+
const output = run(executable, [
|
|
337
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', aclScript,
|
|
338
|
+
'-Request', Buffer.from(JSON.stringify({ action, ...payload })).toString('base64'),
|
|
339
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 30_000 });
|
|
340
|
+
return JSON.parse(output.replace(/^\uFEFF/, '').trim());
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const detail = error.stderr?.toString().trim() || error.message || String(error);
|
|
343
|
+
throw new Error(`Windows ACL ${action} failed (${payload.path || 'access verification'}, exit=${error.status ?? 'unknown'}): ${detail}`, { cause: error });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function existingFile(file, io) {
|
|
348
|
+
try {
|
|
349
|
+
const stat = io.lstatSync(file);
|
|
350
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Expected a regular file: ${file}`);
|
|
351
|
+
return stat;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error.code === 'ENOENT') return undefined;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function makeReplaceable(file, io) {
|
|
359
|
+
const stat = existingFile(file, io);
|
|
360
|
+
// Windows' readonly attribute prevents replacement even for Administrators.
|
|
361
|
+
// chmod affects that attribute, not the DACL. Restore it on the old inode.
|
|
362
|
+
if (process.platform === 'win32' && stat && !(stat.mode & 0o200)) io.chmodSync(file, 0o600);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Keep the original inodes (including ACLs) until both replacements validate. */
|
|
366
|
+
export function commitPreparedFiles(entries, verify, io = fs) {
|
|
367
|
+
const records = [];
|
|
368
|
+
try {
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const stat = existingFile(entry.file, io);
|
|
371
|
+
const record = { ...entry, mode: stat?.mode, backup: undefined, installed: false };
|
|
372
|
+
if (stat) {
|
|
373
|
+
const backup = `${entry.file}.evolcore-backup-${randomUUID()}`;
|
|
374
|
+
// A same-directory hard link retains the exact original security
|
|
375
|
+
// descriptor. If unsupported, stop before replacing any live file.
|
|
376
|
+
io.linkSync(entry.file, backup);
|
|
377
|
+
record.backup = backup;
|
|
378
|
+
}
|
|
379
|
+
records.push(record);
|
|
380
|
+
}
|
|
381
|
+
for (const record of records) {
|
|
382
|
+
makeReplaceable(record.file, io);
|
|
383
|
+
io.renameSync(record.temporary, record.file);
|
|
384
|
+
record.installed = true;
|
|
385
|
+
if (record.backup) io.chmodSync(record.backup, record.mode);
|
|
386
|
+
}
|
|
387
|
+
verify();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const recoveryErrors = [];
|
|
390
|
+
for (const record of records.toReversed()) {
|
|
391
|
+
try {
|
|
392
|
+
if (record.installed) {
|
|
393
|
+
makeReplaceable(record.file, io);
|
|
394
|
+
if (record.backup) io.renameSync(record.backup, record.file);
|
|
395
|
+
else io.unlinkSync(record.file);
|
|
396
|
+
}
|
|
397
|
+
if (record.backup && io.existsSync(record.backup)) {
|
|
398
|
+
makeReplaceable(record.backup, io);
|
|
399
|
+
io.unlinkSync(record.backup);
|
|
400
|
+
}
|
|
401
|
+
if (record.mode !== undefined) io.chmodSync(record.file, record.mode);
|
|
402
|
+
} catch (recoveryError) {
|
|
403
|
+
recoveryErrors.push(`${record.file}: ${recoveryError.message}; original retained at ${record.backup || '(new installation)'}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
throw new Error(`${error.message}${recoveryErrors.length
|
|
407
|
+
? `; rollback incomplete: ${recoveryErrors.join(' | ')}`
|
|
408
|
+
: '; previous files and permissions restored'}`, { cause: error });
|
|
409
|
+
}
|
|
410
|
+
const warnings = [];
|
|
411
|
+
for (const record of records) {
|
|
412
|
+
if (!record.backup) continue;
|
|
413
|
+
try {
|
|
414
|
+
makeReplaceable(record.backup, io);
|
|
415
|
+
io.unlinkSync(record.backup);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
warnings.push(`Installed successfully, but could not remove backup ${record.backup}: ${error.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return warnings;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** This transaction is used only by explicit provisioning, never npm upgrades. */
|
|
424
|
+
export function provisionManagedFiles(options, acl = windowsAcl) {
|
|
425
|
+
const { requirements, hookPath, sourceHook, nodePath, content, launcher } = options;
|
|
426
|
+
const directory = path.dirname(hookPath);
|
|
427
|
+
const configDirectory = path.dirname(requirements);
|
|
428
|
+
acl('preflight');
|
|
429
|
+
fs.mkdirSync(configDirectory, { recursive: true });
|
|
430
|
+
const lockPath = path.join(configDirectory, '.evolcore-hooks.install.lock');
|
|
431
|
+
let lock;
|
|
432
|
+
try { lock = fs.openSync(lockPath, 'wx', 0o600); } catch (error) {
|
|
433
|
+
throw new Error(`Cannot acquire installer lock ${lockPath}: ${error.message}. If an installer crashed, confirm it has stopped before removing the lock.`, { cause: error });
|
|
434
|
+
}
|
|
435
|
+
const staged = [];
|
|
436
|
+
let committed = false;
|
|
437
|
+
try {
|
|
438
|
+
fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }));
|
|
439
|
+
const existing = existingFile(requirements, fs);
|
|
440
|
+
// Detect edits made after configuration generation, including a second
|
|
441
|
+
// installer completing just before this process acquired the lock.
|
|
442
|
+
if ((existing ? fs.readFileSync(requirements, 'utf8') : '') !== options.previousContent) {
|
|
443
|
+
throw new Error(`Configuration changed while preparing installation: ${requirements}. Retry using the latest configuration.`);
|
|
444
|
+
}
|
|
445
|
+
if (fs.existsSync(directory)) {
|
|
446
|
+
const stat = fs.lstatSync(directory);
|
|
447
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Expected a real hook directory: ${directory}`);
|
|
448
|
+
} else {
|
|
449
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
450
|
+
acl('prepare', { path: directory, directory: true });
|
|
451
|
+
}
|
|
452
|
+
// Existing directory ACLs are checked below, never rewritten: changing
|
|
453
|
+
// inheritable ACEs could alter the live Hook before the transaction starts.
|
|
454
|
+
for (const [file, value, reference] of [
|
|
455
|
+
[hookPath, launcher, undefined],
|
|
456
|
+
[requirements, content, existing ? requirements : undefined],
|
|
457
|
+
]) {
|
|
458
|
+
const temporary = `${file}.evolcore-${randomUUID()}.tmp`;
|
|
459
|
+
staged.push({ file, temporary, value });
|
|
460
|
+
fs.writeFileSync(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
461
|
+
acl('prepare', { path: temporary, reference });
|
|
462
|
+
}
|
|
463
|
+
const readable = [
|
|
464
|
+
{ path: configDirectory, execute: true },
|
|
465
|
+
{ path: sourceHook },
|
|
466
|
+
{ path: nodePath, execute: true },
|
|
467
|
+
];
|
|
468
|
+
const verify = (temporary) => {
|
|
469
|
+
for (const entry of staged) {
|
|
470
|
+
const target = temporary ? entry.temporary : entry.file;
|
|
471
|
+
if (fs.readFileSync(target, 'utf8') !== entry.value) throw new Error(`Installed content differs: ${target}`);
|
|
472
|
+
}
|
|
473
|
+
return acl('verify', { entries: [
|
|
474
|
+
{ path: directory, protected: true, execute: true },
|
|
475
|
+
...staged.map(entry => ({ path: temporary ? entry.temporary : entry.file, protected: true })),
|
|
476
|
+
...readable,
|
|
477
|
+
] });
|
|
478
|
+
};
|
|
479
|
+
verify(true);
|
|
480
|
+
let accessReport;
|
|
481
|
+
const warnings = commitPreparedFiles(staged, () => { accessReport = verify(false); });
|
|
482
|
+
committed = true;
|
|
483
|
+
if ([accessReport].flat().some(entry => entry?.userProbe === 'unavailable')) {
|
|
484
|
+
warnings.push('ACLs verified, but this elevated account has no normal UAC linked token. Verify file reads and Hook execution from the intended non-elevated user before use.');
|
|
485
|
+
}
|
|
486
|
+
return warnings;
|
|
487
|
+
} finally {
|
|
488
|
+
for (const entry of staged) {
|
|
489
|
+
try { fs.rmSync(entry.temporary, { force: true }); } catch (error) {
|
|
490
|
+
process.stderr.write(`[evolcore] Could not clean staging file ${entry.temporary}: ${error.message}\n`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
fs.closeSync(lock);
|
|
494
|
+
try { fs.unlinkSync(lockPath); } catch (error) {
|
|
495
|
+
process.stderr.write(`[evolcore] ${committed ? 'Installed; ' : ''}could not remove installer lock ${lockPath}: ${error.message}\n`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function main() {
|
|
501
|
+
// npm may preserve its parent lifecycle variable on Windows when this
|
|
502
|
+
// script is spawned from a test runner. A global install without an
|
|
503
|
+
// explicit package root is still the postinstall shape; never enter the
|
|
504
|
+
// machine provisioning branch on an ordinary Windows lifecycle.
|
|
505
|
+
if (lifecycleEvent() === 'postinstall' || (isGlobalInstall() && !argument('--package-root'))) {
|
|
506
|
+
runLinuxPostinstall();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (process.platform === 'linux' && argument('--mountpoint')) {
|
|
510
|
+
runLinuxPostinstall();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (process.platform !== 'win32') fail('Codex managed hook provisioning is only supported on Windows.');
|
|
514
|
+
if (!argument('--package-root')) fail('--package-root is required.');
|
|
515
|
+
const packageRoot = path.resolve(argument('--package-root'));
|
|
516
|
+
const nodePath = path.resolve(argument('--node-path') || process.execPath);
|
|
517
|
+
const requirements = path.resolve(argument('--requirements')
|
|
518
|
+
|| path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml'));
|
|
519
|
+
const targetDirectory = path.resolve(argument('--managed-dir') || path.join(path.dirname(requirements), 'EvolCore', 'hooks'));
|
|
520
|
+
const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
|
|
521
|
+
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
522
|
+
for (const file of [sourceHook, nodePath]) {
|
|
523
|
+
if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
|
|
524
|
+
}
|
|
525
|
+
if (argument('--repair')) {
|
|
526
|
+
const result = repairManagedHookInstallation({ requirements, managedDir: targetDirectory, packageRoot, nodePath });
|
|
527
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const previousContent = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
|
|
531
|
+
const content = buildManagedRequirements(previousContent, { targetDirectory, nodePath, hookPath });
|
|
532
|
+
const warnings = provisionManagedFiles({
|
|
533
|
+
requirements, hookPath, sourceHook, nodePath, previousContent, content,
|
|
534
|
+
launcher: managedHookLauncher(packageRoot),
|
|
535
|
+
});
|
|
536
|
+
for (const warning of warnings) process.stderr.write(`[evolcore] ${warning}\n`);
|
|
537
|
+
process.stdout.write(`${JSON.stringify({ requirements, managedDir: targetDirectory, hookPath })}\n`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) {
|
|
541
|
+
try { main(); } catch (error) {
|
|
542
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
543
|
+
}
|
|
264
544
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveCommandPath } from '../utils/cross-platform.js';
|
|
2
|
+
import { getCodexAppServerAvailability } from './codex-runner.js';
|
|
3
|
+
export const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
4
|
+
export const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
5
|
+
/**
|
|
6
|
+
* Single source of truth for init and ECWeb base-agent probing.
|
|
7
|
+
* Claude/Gemini require an executable; Codex additionally requires a supported
|
|
8
|
+
* CLI with app-server; ecagent is bundled with EvolCore.
|
|
9
|
+
*/
|
|
10
|
+
export function detectAvailableBaseagents(codexAvailability = getCodexAppServerAvailability()) {
|
|
11
|
+
return BASEAGENT_CANDIDATES.filter((baseagent) => {
|
|
12
|
+
if (baseagent === 'ecagent')
|
|
13
|
+
return true;
|
|
14
|
+
if (baseagent === 'codex')
|
|
15
|
+
return codexAvailability.available;
|
|
16
|
+
return resolveCommandPath(baseagent) !== null;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function detectBaseagentDetails(codexAvailability = getCodexAppServerAvailability()) {
|
|
20
|
+
const claude = resolveCommandPath('claude') !== null;
|
|
21
|
+
const gemini = resolveCommandPath('gemini') !== null;
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
id: 'claude',
|
|
25
|
+
detected: claude,
|
|
26
|
+
detectionSource: claude ? ['PATH'] : [],
|
|
27
|
+
reason: claude ? undefined : '未检测到 Claude CLI。',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'codex',
|
|
31
|
+
detected: codexAvailability.available,
|
|
32
|
+
detectionSource: codexAvailability.available ? ['Codex CLI app-server'] : [],
|
|
33
|
+
reason: codexAvailability.available ? undefined : codexAvailability.reason,
|
|
34
|
+
version: codexAvailability.version,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'gemini',
|
|
38
|
+
detected: gemini,
|
|
39
|
+
detectionSource: gemini ? ['PATH'] : [],
|
|
40
|
+
reason: gemini ? undefined : '未检测到 Gemini CLI。',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'ecagent',
|
|
44
|
+
detected: true,
|
|
45
|
+
detectionSource: ['内置'],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
}
|
package/dist/agents/baseagent.js
CHANGED
|
@@ -157,7 +157,7 @@ export function resolveAnthropicConfig(config, override) {
|
|
|
157
157
|
|| pickExec(config.agents?.claude?.pathToClaudeCodeExecutable);
|
|
158
158
|
return { apiKey, baseUrl, headers, queryParams, model, effort, pathToClaudeCodeExecutable };
|
|
159
159
|
}
|
|
160
|
-
function loadCodexSettings() {
|
|
160
|
+
export function loadCodexSettings() {
|
|
161
161
|
try {
|
|
162
162
|
const configuredHome = process.env.CODEX_HOME?.trim();
|
|
163
163
|
const codexHome = configuredHome ? path.resolve(configuredHome) : path.join(os.homedir(), '.codex');
|