evolcore 0.0.25 → 0.0.27

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 (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,555 +1,582 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { randomUUID } from 'node:crypto';
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';
11
-
12
- const markerStart = '# BEGIN EVOLCORE MANAGED CODEX HOOK';
13
- const markerEnd = '# END EVOLCORE MANAGED CODEX HOOK';
14
- const aclScript = fileURLToPath(new URL('./codex-managed-acl.ps1', import.meta.url));
15
-
16
- function argument(name) {
17
- const index = process.argv.indexOf(name);
18
- return index >= 0 ? process.argv[index + 1] : undefined;
19
- }
20
-
21
- function hasArgument(name) {
22
- return process.argv.includes(name);
23
- }
24
-
25
- function fail(message) {
26
- process.stderr.write(`[evolcore] ${message}\n`);
27
- process.exit(1);
28
- }
29
-
30
- function isGlobalInstall() {
31
- return ['1', 'true'].includes(String(process.env.npm_config_global || '').toLowerCase());
32
- }
33
-
34
- function lifecycleEvent() {
35
- const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === 'npm_lifecycle_event');
36
- return String(entry?.[1] || '').toLowerCase();
37
- }
38
-
39
- function ensureRealDirectory(directory) {
40
- let stat;
41
- try {
42
- stat = fs.lstatSync(directory);
43
- } catch (error) {
44
- if (error?.code !== 'ENOENT') {
45
- fail(`could not inspect the Codex managed requirements mountpoint ${directory}: ${error?.message || String(error)}`);
46
- }
47
- }
48
-
49
- if (stat) {
50
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
51
- fail(`Codex managed requirements mountpoint is unsafe: ${directory} must be a real directory.`);
52
- }
53
- return;
54
- }
55
-
56
- try {
57
- fs.mkdirSync(directory, { mode: 0o755 });
58
- fs.chmodSync(directory, 0o755);
59
- } catch (error) {
60
- if (error?.code === 'EEXIST') {
61
- const raced = fs.lstatSync(directory);
62
- if (raced.isDirectory() && !raced.isSymbolicLink()) return;
63
- }
64
- fail(
65
- `Linux global installation requires the real directory ${directory}, but it could not be created: `
66
- + `${error?.message || String(error)}. `
67
- + `Create it first with: sudo install -d -m 0755 ${directory}`,
68
- );
69
- }
70
-
71
- const created = fs.lstatSync(directory);
72
- if (!created.isDirectory() || created.isSymbolicLink()) {
73
- fail(`Codex managed requirements mountpoint provisioning failed: ${directory} must be a real directory.`);
74
- }
75
- process.stdout.write(`[evolcore] Prepared Codex managed requirements mountpoint at ${directory}.\n`);
76
- }
77
-
78
- function runLinuxPostinstall() {
79
- const explicitMountpoint = argument('--mountpoint');
80
- if (process.platform !== 'linux') return;
81
- // Local source installs do not launch the Linux Codex guard and must not
82
- // mutate machine-wide configuration. An explicit path is accepted only for
83
- // isolated lifecycle behavior tests.
84
- if (!isGlobalInstall() && !explicitMountpoint) return;
85
- ensureRealDirectory(explicitMountpoint || '/etc/codex');
86
- }
87
-
88
- function tomlString(value) {
89
- return JSON.stringify(value);
90
- }
91
-
92
- function windowsCommandArg(value) {
93
- // Keep filesystem separators intact; tomlString() performs the required
94
- // TOML escaping after this Windows command-line quoting step.
95
- return `"${value.replaceAll('"', '\\"')}"`;
96
- }
97
-
98
- function windowsPostinstallNotice() {
99
- if (process.platform !== 'win32') return;
100
- process.stderr.write(
101
- '[evolcore] ec start/restart automatically checks and installs missing Codex managed hooks on Windows.\n'
102
- + '[evolcore] Accept the Windows administrator (UAC) prompt when installation is needed; no separate setup command is required.\n',
103
- );
104
- }
105
-
106
- function removeManagedBlock(source) {
107
- const original = parseToml(source);
108
- if (!source.includes(markerStart) && !source.includes(markerEnd)) return source;
109
- const unsafe = () => new Error('Ambiguous EvolCore managed Hook markers in requirements.toml. Existing configuration was not modified.');
110
- const expression = new RegExp(`^${markerStart}\\r?\\n[\\s\\S]*?^${markerEnd}(?:\\r?\\n|$)`, 'gm');
111
- const matches = [...source.matchAll(expression)];
112
- if (matches.length !== 1 || source.split(markerStart).length !== 2 || source.split(markerEnd).length !== 2) throw unsafe();
113
- const block = parseToml(matches[0][0]);
114
- const groups = block.hooks?.PreToolUse;
115
- if (!Array.isArray(groups) || groups.length !== 1
116
- || !isDeepStrictEqual(block, { hooks: { PreToolUse: groups } })) throw unsafe();
117
- const content = source.replace(matches[0][0], '');
118
- const retained = parseToml(content);
119
- const originalGroups = original.hooks?.PreToolUse;
120
- if (!Array.isArray(originalGroups)) throw unsafe();
121
- // Markers inside a TOML string are not comments. Prove that removing the
122
- // block removes only its Hook group, with every other policy unchanged.
123
- const safe = originalGroups.some((group, index) => {
124
- if (!isDeepStrictEqual(group, groups[0])) return false;
125
- const expected = structuredClone(original);
126
- expected.hooks.PreToolUse.splice(index, 1);
127
- if (!expected.hooks.PreToolUse.length && !retained.hooks?.PreToolUse) delete expected.hooks.PreToolUse;
128
- if (!Object.keys(expected.hooks).length && !retained.hooks) delete expected.hooks;
129
- return isDeepStrictEqual(expected, retained);
130
- });
131
- if (!safe) throw unsafe();
132
- return content;
133
- }
134
-
135
- function tableBounds(lines, table) {
136
- const header = table ? `[${table}]` : null;
137
- const start = header
138
- ? lines.findIndex(line => line.trim() === header || line.trim().startsWith(`${header} #`))
139
- : -1;
140
- if (table && start < 0) return null;
141
- const from = table ? start + 1 : 0;
142
- let to = lines.length;
143
- for (let index = from; index < lines.length; index += 1) {
144
- if (/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(lines[index])) {
145
- to = index;
146
- break;
147
- }
148
- }
149
- return { start, from, to };
150
- }
151
-
152
- function setTomlKey(source, table, key, value) {
153
- const lines = source.split('\n');
154
- let bounds = tableBounds(lines, table);
155
- if (!bounds) {
156
- if (table) {
157
- if (lines.length && lines.at(-1) !== '') lines.push('');
158
- lines.push(`[${table}]`, `${key} = ${value}`);
159
- } else {
160
- let insertAt = lines.findIndex(line => /^\s*\[/.test(line));
161
- if (insertAt < 0) insertAt = lines.length;
162
- lines.splice(insertAt, 0, `${key} = ${value}`);
163
- }
164
- return lines.join('\n');
165
- }
166
- const keyExpression = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*=`);
167
- const existing = lines.slice(bounds.from, bounds.to).findIndex(line => keyExpression.test(line));
168
- if (existing >= 0) {
169
- lines[bounds.from + existing] = `${key} = ${value}`;
170
- } else {
171
- lines.splice(bounds.from, 0, `${key} = ${value}`);
172
- }
173
- return lines.join('\n');
174
- }
175
-
176
- export function managedHookLauncher(packageRoot) {
177
- // Keep the machine-wide entrypoint stable across npm upgrades. Those
178
- // upgrades replace the package contents at this stable global path, so
179
- // this file never needs to be rewritten just because the hook changed.
180
- const trustedPackageRoot = path.resolve(packageRoot);
181
- return `#!/usr/bin/env node
182
- import path from 'node:path';
183
- import { pathToFileURL } from 'node:url';
184
-
185
- // EVOLCORE_MANAGED_HOOK_LAUNCHER
186
- const packageRoot = ${JSON.stringify(trustedPackageRoot)};
187
-
188
- try {
189
- await import(pathToFileURL(path.join(packageRoot, 'bin', 'codex-managed-hook.mjs')).href);
190
- } catch (error) {
191
- process.stderr.write('[evolcore] failed to load the current managed hook: '
192
- + (error instanceof Error ? error.message : String(error)) + '\\n');
193
- process.exit(2);
194
- }
195
- `;
196
- }
197
-
198
- export function buildManagedRequirements(source, { targetDirectory, nodePath, hookPath }) {
199
- // Keep the existing text editor, but reject unsupported TOML layouts before
200
- // writing anything. Parsing also catches duplicate tables/keys on reinstall.
201
- let content = removeManagedBlock(source);
202
- const before = parseToml(content);
203
- content = setTomlKey(content, null, 'allow_managed_hooks_only', 'true');
204
- content = setTomlKey(content, 'features', 'hooks', 'true');
205
- content = setTomlKey(content, 'hooks', 'windows_managed_dir', tomlString(path.resolve(targetDirectory)));
206
- if (!content.endsWith('\n')) content += '\n';
207
- // Windows hook command must include %EVOLCORE_INSTANCE_SOCKET% so Codex
208
- // expands it at runtime with the per-daemon socket path
209
- const command = `${windowsCommandArg(nodePath)} ${windowsCommandArg(hookPath)} %EVOLCORE_INSTANCE_SOCKET%`;
210
- content += [
211
- markerStart,
212
- '[[hooks.PreToolUse]]',
213
- 'matcher = "*"',
214
- '',
215
- '[[hooks.PreToolUse.hooks]]',
216
- 'type = "command"',
217
- `command = ${tomlString(command)}`,
218
- `command_windows = ${tomlString(command)}`,
219
- // Intentionally omit timeout: Codex applies its documented 600-second
220
- // default for synchronous PreToolUse hooks. The application pause gate
221
- // itself has no timeout; Codex remains the outer infrastructure ceiling.
222
- 'statusMessage = "Checking EvolCore session policy"',
223
- markerEnd,
224
- '',
225
- ].join('\n');
226
- const after = parseToml(content);
227
- const expected = {
228
- ...before,
229
- allow_managed_hooks_only: true,
230
- features: { ...before.features, hooks: true },
231
- hooks: {
232
- ...before.hooks,
233
- windows_managed_dir: path.resolve(targetDirectory),
234
- PreToolUse: [
235
- ...(before.hooks?.PreToolUse || []),
236
- { matcher: '*', hooks: [{ type: 'command', command, command_windows: command, statusMessage: 'Checking EvolCore session policy' }] },
237
- ],
238
- },
239
- };
240
- if (!isDeepStrictEqual(after, expected)) {
241
- throw new Error('Could not safely update requirements.toml without changing unrelated policy. Existing configuration was not modified.');
242
- }
243
- return content;
244
- }
245
-
246
- function regularFile(file) {
247
- try {
248
- const stat = fs.lstatSync(file);
249
- if (!stat.isFile() || stat.isSymbolicLink()) return { ok: false, unsafe: true };
250
- return { ok: true };
251
- } catch (error) {
252
- if (error?.code === 'ENOENT') return { ok: false, unsafe: false };
253
- return { ok: false, unsafe: true, error };
254
- }
255
- }
256
-
257
- function sourceHasManagedMarker(source) {
258
- return source.includes(markerStart) || source.includes(markerEnd);
259
- }
260
-
261
- /** Inspect only the canonical EvolCore managed-hook targets. */
262
- export function inspectManagedHookInstallation({ requirements, managedDir, packageRoot }) {
263
- const canonicalRequirements = path.resolve(requirements);
264
- const canonicalManagedDir = path.resolve(managedDir);
265
- const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
266
- for (const directory of [path.dirname(canonicalRequirements), canonicalManagedDir]) {
267
- try {
268
- const stat = fs.lstatSync(directory);
269
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
270
- return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `${directory} must be a real directory` };
271
- }
272
- } catch (error) {
273
- if (error?.code !== 'ENOENT') return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
274
- }
275
- }
276
- const req = regularFile(canonicalRequirements);
277
- const hook = regularFile(hookPath);
278
- if (req.unsafe || hook.unsafe) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is not a regular file' };
279
- if (!req.ok && hook.ok) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
280
- if (req.ok && !hook.ok) {
281
- // A missing launcher can be repaired only when the requirements file is
282
- // still an untouched user configuration. If it already contains any
283
- // EvolCore-managed policy, treat the partial state as drift and fail
284
- // closed instead of guessing which policy is authoritative.
285
- try {
286
- const source = fs.readFileSync(canonicalRequirements, 'utf8');
287
- const partial = parseToml(source);
288
- const managedPolicyPresent = sourceHasManagedMarker(source)
289
- || partial.allow_managed_hooks_only !== undefined
290
- || partial.hooks?.windows_managed_dir !== undefined
291
- || partial.hooks?.managed_dir !== undefined;
292
- if (managedPolicyPresent) {
293
- return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements contains managed-hook policy but the canonical launcher is missing' };
294
- }
295
- } catch (error) {
296
- return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` };
297
- }
298
- }
299
- if (!req.ok || !hook.ok) return { status: 'missing', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is missing' };
300
- let parsed;
301
- try { parsed = parseToml(fs.readFileSync(canonicalRequirements, 'utf8')); }
302
- catch (error) { return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` }; }
303
- const configured = parsed.hooks?.windows_managed_dir ?? parsed.hooks?.managed_dir;
304
- const normalized = value => path.normalize(String(value || '')).toLowerCase();
305
- const launcher = fs.readFileSync(hookPath, 'utf8');
306
- const packageMarker = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
307
- && launcher.toLowerCase().includes(JSON.stringify(path.resolve(packageRoot || process.cwd())).toLowerCase());
308
- const preToolUse = parsed.hooks?.PreToolUse;
309
- const canonicalCommand = value => {
310
- if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value)) return false;
311
- const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
312
- return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
313
- };
314
- const loaded = Array.isArray(preToolUse) && preToolUse.some(group => group?.matcher === '*'
315
- && Array.isArray(group.hooks) && group.hooks.some(handler => handler?.type === 'command'
316
- && handler.async !== true && typeof handler.command === 'string'
317
- && canonicalCommand(handler.command)
318
- && canonicalCommand(handler.command_windows ?? handler.commandWindows)));
319
- const valid = parsed.allow_managed_hooks_only === true
320
- && parsed.features?.hooks === true
321
- && normalized(configured) === normalized(canonicalManagedDir)
322
- && packageMarker && loaded;
323
- return valid
324
- ? { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath }
325
- : { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
326
- }
327
-
328
- /** Idempotently repair a missing canonical installation; unsafe edits fail closed. */
329
- export function repairManagedHookInstallation(options, acl = windowsAcl) {
330
- const { requirements, managedDir, packageRoot, nodePath = process.execPath } = options;
331
- const canonicalRequirements = path.resolve(requirements);
332
- const canonicalManagedDir = path.resolve(managedDir);
333
- const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
334
- const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
335
- const state = inspectManagedHookInstallation({ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot });
336
- if (state.status === 'installed') return { ...state, repaired: false, warnings: [] };
337
- if (state.status === 'unsafe') throw new Error(`Refusing automatic managed-hook repair: ${state.reason}`);
338
- if (!fs.existsSync(sourceHook)) throw new Error(`Required Hook runtime file was not found: ${sourceHook}`);
339
- const previousContent = fs.existsSync(canonicalRequirements) ? fs.readFileSync(canonicalRequirements, 'utf8') : '';
340
- const content = buildManagedRequirements(previousContent, { targetDirectory: canonicalManagedDir, nodePath, hookPath });
341
- const warnings = provisionManagedFiles({ requirements: canonicalRequirements, hookPath, sourceHook, nodePath, previousContent, content, launcher: managedHookLauncher(packageRoot) }, acl);
342
- return { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, repaired: true, warnings };
343
- }
344
-
345
- export function windowsAcl(action, payload = {}, run = execFileSync) {
346
- const executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
347
- try {
348
- const output = run(executable, [
349
- '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', aclScript,
350
- '-Request', Buffer.from(JSON.stringify({ action, ...payload })).toString('base64'),
351
- ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 30_000 });
352
- return JSON.parse(output.replace(/^\uFEFF/, '').trim());
353
- } catch (error) {
354
- const detail = error.stderr?.toString().trim() || error.message || String(error);
355
- throw new Error(`Windows ACL ${action} failed (${payload.path || 'access verification'}, exit=${error.status ?? 'unknown'}): ${detail}`, { cause: error });
356
- }
357
- }
358
-
359
- function existingFile(file, io) {
360
- try {
361
- const stat = io.lstatSync(file);
362
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Expected a regular file: ${file}`);
363
- return stat;
364
- } catch (error) {
365
- if (error.code === 'ENOENT') return undefined;
366
- throw error;
367
- }
368
- }
369
-
370
- function makeReplaceable(file, io) {
371
- const stat = existingFile(file, io);
372
- // Windows' readonly attribute prevents replacement even for Administrators.
373
- // chmod affects that attribute, not the DACL. Restore it on the old inode.
374
- if (process.platform === 'win32' && stat && !(stat.mode & 0o200)) io.chmodSync(file, 0o600);
375
- }
376
-
377
- /** Keep the original inodes (including ACLs) until both replacements validate. */
378
- export function commitPreparedFiles(entries, verify, io = fs) {
379
- const records = [];
380
- try {
381
- for (const entry of entries) {
382
- const stat = existingFile(entry.file, io);
383
- const record = { ...entry, mode: stat?.mode, backup: undefined, installed: false };
384
- if (stat) {
385
- const backup = `${entry.file}.evolcore-backup-${randomUUID()}`;
386
- // A same-directory hard link retains the exact original security
387
- // descriptor. If unsupported, stop before replacing any live file.
388
- io.linkSync(entry.file, backup);
389
- record.backup = backup;
390
- }
391
- records.push(record);
392
- }
393
- for (const record of records) {
394
- makeReplaceable(record.file, io);
395
- io.renameSync(record.temporary, record.file);
396
- record.installed = true;
397
- if (record.backup) io.chmodSync(record.backup, record.mode);
398
- }
399
- verify();
400
- } catch (error) {
401
- const recoveryErrors = [];
402
- for (const record of records.toReversed()) {
403
- try {
404
- if (record.installed) {
405
- makeReplaceable(record.file, io);
406
- if (record.backup) io.renameSync(record.backup, record.file);
407
- else io.unlinkSync(record.file);
408
- }
409
- if (record.backup && io.existsSync(record.backup)) {
410
- makeReplaceable(record.backup, io);
411
- io.unlinkSync(record.backup);
412
- }
413
- if (record.mode !== undefined) io.chmodSync(record.file, record.mode);
414
- } catch (recoveryError) {
415
- recoveryErrors.push(`${record.file}: ${recoveryError.message}; original retained at ${record.backup || '(new installation)'}`);
416
- }
417
- }
418
- throw new Error(`${error.message}${recoveryErrors.length
419
- ? `; rollback incomplete: ${recoveryErrors.join(' | ')}`
420
- : '; previous files and permissions restored'}`, { cause: error });
421
- }
422
- const warnings = [];
423
- for (const record of records) {
424
- if (!record.backup) continue;
425
- try {
426
- makeReplaceable(record.backup, io);
427
- io.unlinkSync(record.backup);
428
- } catch (error) {
429
- warnings.push(`Installed successfully, but could not remove backup ${record.backup}: ${error.message}`);
430
- }
431
- }
432
- return warnings;
433
- }
434
-
435
- /** This transaction is used only by explicit provisioning, never npm upgrades. */
436
- export function provisionManagedFiles(options, acl = windowsAcl) {
437
- const { requirements, hookPath, sourceHook, nodePath, content, launcher } = options;
438
- const directory = path.dirname(hookPath);
439
- const configDirectory = path.dirname(requirements);
440
- acl('preflight');
441
- fs.mkdirSync(configDirectory, { recursive: true });
442
- const lockPath = path.join(configDirectory, '.evolcore-hooks.install.lock');
443
- let lock;
444
- try { lock = fs.openSync(lockPath, 'wx', 0o600); } catch (error) {
445
- throw new Error(`Cannot acquire installer lock ${lockPath}: ${error.message}. If an installer crashed, confirm it has stopped before removing the lock.`, { cause: error });
446
- }
447
- const staged = [];
448
- let committed = false;
449
- try {
450
- fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }));
451
- const existing = existingFile(requirements, fs);
452
- // Detect edits made after configuration generation, including a second
453
- // installer completing just before this process acquired the lock.
454
- if ((existing ? fs.readFileSync(requirements, 'utf8') : '') !== options.previousContent) {
455
- throw new Error(`Configuration changed while preparing installation: ${requirements}. Retry using the latest configuration.`);
456
- }
457
- if (fs.existsSync(directory)) {
458
- const stat = fs.lstatSync(directory);
459
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Expected a real hook directory: ${directory}`);
460
- } else {
461
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
462
- acl('prepare', { path: directory, directory: true });
463
- }
464
- // Existing directory ACLs are checked below, never rewritten: changing
465
- // inheritable ACEs could alter the live Hook before the transaction starts.
466
- for (const [file, value, reference] of [
467
- [hookPath, launcher, undefined],
468
- [requirements, content, existing ? requirements : undefined],
469
- ]) {
470
- const temporary = `${file}.evolcore-${randomUUID()}.tmp`;
471
- staged.push({ file, temporary, value });
472
- fs.writeFileSync(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
473
- acl('prepare', { path: temporary, reference });
474
- }
475
- const readable = [
476
- { path: configDirectory, execute: true },
477
- { path: sourceHook },
478
- { path: nodePath, execute: true },
479
- ];
480
- const verify = (temporary) => {
481
- for (const entry of staged) {
482
- const target = temporary ? entry.temporary : entry.file;
483
- if (fs.readFileSync(target, 'utf8') !== entry.value) throw new Error(`Installed content differs: ${target}`);
484
- }
485
- return acl('verify', { entries: [
486
- { path: directory, protected: true, execute: true },
487
- ...staged.map(entry => ({ path: temporary ? entry.temporary : entry.file, protected: true })),
488
- ...readable,
489
- ] });
490
- };
491
- verify(true);
492
- let accessReport;
493
- const warnings = commitPreparedFiles(staged, () => { accessReport = verify(false); });
494
- committed = true;
495
- if ([accessReport].flat().some(entry => entry?.userProbe === 'unavailable')) {
496
- 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.');
497
- }
498
- return warnings;
499
- } finally {
500
- for (const entry of staged) {
501
- try { fs.rmSync(entry.temporary, { force: true }); } catch (error) {
502
- process.stderr.write(`[evolcore] Could not clean staging file ${entry.temporary}: ${error.message}\n`);
503
- }
504
- }
505
- fs.closeSync(lock);
506
- try { fs.unlinkSync(lockPath); } catch (error) {
507
- process.stderr.write(`[evolcore] ${committed ? 'Installed; ' : ''}could not remove installer lock ${lockPath}: ${error.message}\n`);
508
- }
509
- }
510
- }
511
-
512
- function main() {
513
- // An explicit package root selects provisioning, even if the elevated
514
- // child inherited npm lifecycle variables. Ordinary postinstall skips it.
515
- if (!argument('--package-root') && (lifecycleEvent() === 'postinstall' || isGlobalInstall())) {
516
- runLinuxPostinstall();
517
- windowsPostinstallNotice();
518
- return;
519
- }
520
- if (process.platform === 'linux' && argument('--mountpoint')) {
521
- runLinuxPostinstall();
522
- return;
523
- }
524
- if (process.platform !== 'win32') fail('Codex managed hook provisioning is only supported on Windows.');
525
- if (!argument('--package-root')) fail('--package-root is required.');
526
- const packageRoot = path.resolve(argument('--package-root'));
527
- const nodePath = path.resolve(argument('--node-path') || process.execPath);
528
- const requirements = path.resolve(argument('--requirements')
529
- || path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml'));
530
- const targetDirectory = path.resolve(argument('--managed-dir') || path.join(path.dirname(requirements), 'EvolCore', 'hooks'));
531
- const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
532
- const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
533
- for (const file of [sourceHook, nodePath]) {
534
- if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
535
- }
536
- if (hasArgument('--repair')) {
537
- const result = repairManagedHookInstallation({ requirements, managedDir: targetDirectory, packageRoot, nodePath });
538
- process.stdout.write(`${JSON.stringify(result)}\n`);
539
- return;
540
- }
541
- const previousContent = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
542
- const content = buildManagedRequirements(previousContent, { targetDirectory, nodePath, hookPath });
543
- const warnings = provisionManagedFiles({
544
- requirements, hookPath, sourceHook, nodePath, previousContent, content,
545
- launcher: managedHookLauncher(packageRoot),
546
- });
547
- for (const warning of warnings) process.stderr.write(`[evolcore] ${warning}\n`);
548
- process.stdout.write(`${JSON.stringify({ requirements, managedDir: targetDirectory, hookPath })}\n`);
549
- }
550
-
551
- if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) {
552
- try { main(); } catch (error) {
553
- fail(error instanceof Error ? error.message : String(error));
554
- }
555
- }
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
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';
11
+
12
+ const markerStart = '# BEGIN EVOLCORE MANAGED CODEX HOOK';
13
+ const markerEnd = '# END EVOLCORE MANAGED CODEX HOOK';
14
+ const aclScript = fileURLToPath(new URL('./codex-managed-acl.ps1', import.meta.url));
15
+
16
+ function argument(name) {
17
+ const index = process.argv.indexOf(name);
18
+ return index >= 0 ? process.argv[index + 1] : undefined;
19
+ }
20
+
21
+ function hasArgument(name) {
22
+ return process.argv.includes(name);
23
+ }
24
+
25
+ function fail(message) {
26
+ process.stderr.write(`[evolcore] ${message}\n`);
27
+ process.exit(1);
28
+ }
29
+
30
+ function isGlobalInstall() {
31
+ return ['1', 'true'].includes(String(process.env.npm_config_global || '').toLowerCase());
32
+ }
33
+
34
+ function lifecycleEvent() {
35
+ const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === 'npm_lifecycle_event');
36
+ return String(entry?.[1] || '').toLowerCase();
37
+ }
38
+
39
+ function ensureRealDirectory(directory) {
40
+ let stat;
41
+ try {
42
+ stat = fs.lstatSync(directory);
43
+ } catch (error) {
44
+ if (error?.code !== 'ENOENT') {
45
+ fail(`could not inspect the Codex managed requirements mountpoint ${directory}: ${error?.message || String(error)}`);
46
+ }
47
+ }
48
+
49
+ if (stat) {
50
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
51
+ fail(`Codex managed requirements mountpoint is unsafe: ${directory} must be a real directory.`);
52
+ }
53
+ return;
54
+ }
55
+
56
+ try {
57
+ fs.mkdirSync(directory, { mode: 0o755 });
58
+ fs.chmodSync(directory, 0o755);
59
+ } catch (error) {
60
+ if (error?.code === 'EEXIST') {
61
+ const raced = fs.lstatSync(directory);
62
+ if (raced.isDirectory() && !raced.isSymbolicLink()) return;
63
+ }
64
+ fail(
65
+ `Linux global installation requires the real directory ${directory}, but it could not be created: `
66
+ + `${error?.message || String(error)}. `
67
+ + `Create it first with: sudo install -d -m 0755 ${directory}`,
68
+ );
69
+ }
70
+
71
+ const created = fs.lstatSync(directory);
72
+ if (!created.isDirectory() || created.isSymbolicLink()) {
73
+ fail(`Codex managed requirements mountpoint provisioning failed: ${directory} must be a real directory.`);
74
+ }
75
+ process.stdout.write(`[evolcore] Prepared Codex managed requirements mountpoint at ${directory}.\n`);
76
+ }
77
+
78
+ function runLinuxPostinstall() {
79
+ const explicitMountpoint = argument('--mountpoint');
80
+ if (process.platform !== 'linux') return;
81
+ // Local source installs do not launch the Linux Codex guard and must not
82
+ // mutate machine-wide configuration. An explicit path is accepted only for
83
+ // isolated lifecycle behavior tests.
84
+ if (!isGlobalInstall() && !explicitMountpoint) return;
85
+ ensureRealDirectory(explicitMountpoint || '/etc/codex');
86
+ }
87
+
88
+ function tomlString(value) {
89
+ return JSON.stringify(value);
90
+ }
91
+
92
+ function windowsCommandArg(value) {
93
+ // Keep filesystem separators intact; tomlString() performs the required
94
+ // TOML escaping after this Windows command-line quoting step.
95
+ return `"${value.replaceAll('"', '\\"')}"`;
96
+ }
97
+
98
+ function windowsPostinstallNotice() {
99
+ if (process.platform !== 'win32') return;
100
+ process.stderr.write(
101
+ '[evolcore] ec start/restart automatically checks and installs missing Codex managed hooks on Windows.\n'
102
+ + '[evolcore] Accept the Windows administrator (UAC) prompt when installation is needed; no separate setup command is required.\n',
103
+ );
104
+ }
105
+
106
+ function removeManagedBlock(source) {
107
+ const original = parseToml(source);
108
+ if (!source.includes(markerStart) && !source.includes(markerEnd)) return source;
109
+ const unsafe = () => new Error('Ambiguous EvolCore managed Hook markers in requirements.toml. Existing configuration was not modified.');
110
+ const expression = new RegExp(`^${markerStart}\\r?\\n[\\s\\S]*?^${markerEnd}(?:\\r?\\n|$)`, 'gm');
111
+ const matches = [...source.matchAll(expression)];
112
+ if (matches.length !== 1 || source.split(markerStart).length !== 2 || source.split(markerEnd).length !== 2) throw unsafe();
113
+ const block = parseToml(matches[0][0]);
114
+ const groups = block.hooks?.PreToolUse;
115
+ if (!Array.isArray(groups) || groups.length !== 1
116
+ || !isDeepStrictEqual(block, { hooks: { PreToolUse: groups } })) throw unsafe();
117
+ const content = source.replace(matches[0][0], '');
118
+ const retained = parseToml(content);
119
+ const originalGroups = original.hooks?.PreToolUse;
120
+ if (!Array.isArray(originalGroups)) throw unsafe();
121
+ // Markers inside a TOML string are not comments. Prove that removing the
122
+ // block removes only its Hook group, with every other policy unchanged.
123
+ const safe = originalGroups.some((group, index) => {
124
+ if (!isDeepStrictEqual(group, groups[0])) return false;
125
+ const expected = structuredClone(original);
126
+ expected.hooks.PreToolUse.splice(index, 1);
127
+ if (!expected.hooks.PreToolUse.length && !retained.hooks?.PreToolUse) delete expected.hooks.PreToolUse;
128
+ if (!Object.keys(expected.hooks).length && !retained.hooks) delete expected.hooks;
129
+ return isDeepStrictEqual(expected, retained);
130
+ });
131
+ if (!safe) throw unsafe();
132
+ return content;
133
+ }
134
+
135
+ function tableBounds(lines, table) {
136
+ const header = table ? `[${table}]` : null;
137
+ const start = header
138
+ ? lines.findIndex(line => line.trim() === header || line.trim().startsWith(`${header} #`))
139
+ : -1;
140
+ if (table && start < 0) return null;
141
+ const from = table ? start + 1 : 0;
142
+ let to = lines.length;
143
+ for (let index = from; index < lines.length; index += 1) {
144
+ if (/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(lines[index])) {
145
+ to = index;
146
+ break;
147
+ }
148
+ }
149
+ return { start, from, to };
150
+ }
151
+
152
+ function setTomlKey(source, table, key, value) {
153
+ const lines = source.split('\n');
154
+ let bounds = tableBounds(lines, table);
155
+ if (!bounds) {
156
+ if (table) {
157
+ if (lines.length && lines.at(-1) !== '') lines.push('');
158
+ lines.push(`[${table}]`, `${key} = ${value}`);
159
+ } else {
160
+ let insertAt = lines.findIndex(line => /^\s*\[/.test(line));
161
+ if (insertAt < 0) insertAt = lines.length;
162
+ lines.splice(insertAt, 0, `${key} = ${value}`);
163
+ }
164
+ return lines.join('\n');
165
+ }
166
+ const keyExpression = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*=`);
167
+ const existing = lines.slice(bounds.from, bounds.to).findIndex(line => keyExpression.test(line));
168
+ if (existing >= 0) {
169
+ lines[bounds.from + existing] = `${key} = ${value}`;
170
+ } else {
171
+ lines.splice(bounds.from, 0, `${key} = ${value}`);
172
+ }
173
+ return lines.join('\n');
174
+ }
175
+
176
+ export function managedHookLauncher(packageRoot) {
177
+ // Keep the machine-wide entrypoint stable across npm upgrades. Those
178
+ // upgrades replace the package contents at this stable global path, so
179
+ // this file never needs to be rewritten just because the hook changed.
180
+ const trustedPackageRoot = path.resolve(packageRoot);
181
+ return `#!/usr/bin/env node
182
+ import path from 'node:path';
183
+ import { pathToFileURL } from 'node:url';
184
+
185
+ // EVOLCORE_MANAGED_HOOK_LAUNCHER
186
+ const packageRoot = ${JSON.stringify(trustedPackageRoot)};
187
+
188
+ try {
189
+ await import(pathToFileURL(path.join(packageRoot, 'bin', 'codex-managed-hook.mjs')).href);
190
+ } catch (error) {
191
+ process.stderr.write('[evolcore] failed to load the current managed hook: '
192
+ + (error instanceof Error ? error.message : String(error)) + '\\n');
193
+ process.exit(2);
194
+ }
195
+ `;
196
+ }
197
+
198
+ export function buildManagedRequirements(source, { targetDirectory, nodePath, hookPath }) {
199
+ // Keep the existing text editor, but reject unsupported TOML layouts before
200
+ // writing anything. Parsing also catches duplicate tables/keys on reinstall.
201
+ let content = removeManagedBlock(source);
202
+ const before = parseToml(content);
203
+ content = setTomlKey(content, null, 'allow_managed_hooks_only', 'true');
204
+ content = setTomlKey(content, 'features', 'hooks', 'true');
205
+ content = setTomlKey(content, 'hooks', 'windows_managed_dir', tomlString(path.resolve(targetDirectory)));
206
+ if (!content.endsWith('\n')) content += '\n';
207
+ // Windows hook command must include %EVOLCORE_INSTANCE_SOCKET% so Codex
208
+ // expands it at runtime with the per-daemon socket path
209
+ const command = `${windowsCommandArg(nodePath)} ${windowsCommandArg(hookPath)} %EVOLCORE_INSTANCE_SOCKET%`;
210
+ content += [
211
+ markerStart,
212
+ '[[hooks.PreToolUse]]',
213
+ 'matcher = "*"',
214
+ '',
215
+ '[[hooks.PreToolUse.hooks]]',
216
+ 'type = "command"',
217
+ `command = ${tomlString(command)}`,
218
+ `command_windows = ${tomlString(command)}`,
219
+ // Intentionally omit timeout: Codex applies its documented 600-second
220
+ // default for synchronous PreToolUse hooks. The application pause gate
221
+ // itself has no timeout; Codex remains the outer infrastructure ceiling.
222
+ 'statusMessage = "Checking EvolCore session policy"',
223
+ markerEnd,
224
+ '',
225
+ ].join('\n');
226
+ const after = parseToml(content);
227
+ const expected = {
228
+ ...before,
229
+ allow_managed_hooks_only: true,
230
+ features: { ...before.features, hooks: true },
231
+ hooks: {
232
+ ...before.hooks,
233
+ windows_managed_dir: path.resolve(targetDirectory),
234
+ PreToolUse: [
235
+ ...(before.hooks?.PreToolUse || []),
236
+ { matcher: '*', hooks: [{ type: 'command', command, command_windows: command, statusMessage: 'Checking EvolCore session policy' }] },
237
+ ],
238
+ },
239
+ };
240
+ if (!isDeepStrictEqual(after, expected)) {
241
+ throw new Error('Could not safely update requirements.toml without changing unrelated policy. Existing configuration was not modified.');
242
+ }
243
+ return content;
244
+ }
245
+
246
+ function regularFile(file) {
247
+ try {
248
+ const stat = fs.lstatSync(file);
249
+ if (!stat.isFile() || stat.isSymbolicLink()) return { ok: false, unsafe: true };
250
+ return { ok: true };
251
+ } catch (error) {
252
+ if (error?.code === 'ENOENT') return { ok: false, unsafe: false };
253
+ return { ok: false, unsafe: true, error };
254
+ }
255
+ }
256
+
257
+ function sourceHasManagedMarker(source) {
258
+ return source.includes(markerStart) || source.includes(markerEnd);
259
+ }
260
+
261
+ /** Read back the package root that managedHookLauncher() embedded. */
262
+ export function launcherPackageRoot(launcher) {
263
+ const match = /^\s*const packageRoot = ("(?:[^"\\]|\\.)*")\s*;/m.exec(launcher);
264
+ if (!match) return null;
265
+ try {
266
+ const value = JSON.parse(match[1]);
267
+ return typeof value === 'string' && value ? value : null;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * A `npm link` install reaches the package through a symlinked global path, so
275
+ * the launcher and the running CLI legitimately spell the same package root
276
+ * differently. Compare the resolved targets instead of the literal strings;
277
+ * a launcher pointing at a genuinely different package still fails closed.
278
+ */
279
+ export function samePackageRoot(embedded, expected) {
280
+ if (typeof embedded !== 'string' || !embedded) return false;
281
+ const normalize = value => path.normalize(String(value)).replace(/[\\/]+$/, '').toLowerCase();
282
+ const resolved = value => {
283
+ try { return normalize(fs.realpathSync(value)); } catch { return normalize(value); }
284
+ };
285
+ return normalize(embedded) === normalize(expected) || resolved(embedded) === resolved(expected);
286
+ }
287
+
288
+ /** Inspect only the canonical EvolCore managed-hook targets. */
289
+ export function inspectManagedHookInstallation({ requirements, managedDir, packageRoot }) {
290
+ const canonicalRequirements = path.resolve(requirements);
291
+ const canonicalManagedDir = path.resolve(managedDir);
292
+ const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
293
+ for (const directory of [path.dirname(canonicalRequirements), canonicalManagedDir]) {
294
+ try {
295
+ const stat = fs.lstatSync(directory);
296
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
297
+ return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `${directory} must be a real directory` };
298
+ }
299
+ } catch (error) {
300
+ if (error?.code !== 'ENOENT') return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
301
+ }
302
+ }
303
+ const req = regularFile(canonicalRequirements);
304
+ const hook = regularFile(hookPath);
305
+ if (req.unsafe || hook.unsafe) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is not a regular file' };
306
+ if (!req.ok && hook.ok) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
307
+ if (req.ok && !hook.ok) {
308
+ // A missing launcher can be repaired only when the requirements file is
309
+ // still an untouched user configuration. If it already contains any
310
+ // EvolCore-managed policy, treat the partial state as drift and fail
311
+ // closed instead of guessing which policy is authoritative.
312
+ try {
313
+ const source = fs.readFileSync(canonicalRequirements, 'utf8');
314
+ const partial = parseToml(source);
315
+ const managedPolicyPresent = sourceHasManagedMarker(source)
316
+ || partial.allow_managed_hooks_only !== undefined
317
+ || partial.hooks?.windows_managed_dir !== undefined
318
+ || partial.hooks?.managed_dir !== undefined;
319
+ if (managedPolicyPresent) {
320
+ return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements contains managed-hook policy but the canonical launcher is missing' };
321
+ }
322
+ } catch (error) {
323
+ return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` };
324
+ }
325
+ }
326
+ if (!req.ok || !hook.ok) return { status: 'missing', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is missing' };
327
+ let parsed;
328
+ try { parsed = parseToml(fs.readFileSync(canonicalRequirements, 'utf8')); }
329
+ catch (error) { return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` }; }
330
+ const configured = parsed.hooks?.windows_managed_dir ?? parsed.hooks?.managed_dir;
331
+ const normalized = value => path.normalize(String(value || '')).toLowerCase();
332
+ const launcher = fs.readFileSync(hookPath, 'utf8');
333
+ const packageMarker = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
334
+ && samePackageRoot(launcherPackageRoot(launcher), path.resolve(packageRoot || process.cwd()));
335
+ const preToolUse = parsed.hooks?.PreToolUse;
336
+ const canonicalCommand = value => {
337
+ if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value)) return false;
338
+ const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
339
+ return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
340
+ };
341
+ const loaded = Array.isArray(preToolUse) && preToolUse.some(group => group?.matcher === '*'
342
+ && Array.isArray(group.hooks) && group.hooks.some(handler => handler?.type === 'command'
343
+ && handler.async !== true && typeof handler.command === 'string'
344
+ && canonicalCommand(handler.command)
345
+ && canonicalCommand(handler.command_windows ?? handler.commandWindows)));
346
+ const valid = parsed.allow_managed_hooks_only === true
347
+ && parsed.features?.hooks === true
348
+ && normalized(configured) === normalized(canonicalManagedDir)
349
+ && packageMarker && loaded;
350
+ return valid
351
+ ? { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath }
352
+ : { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
353
+ }
354
+
355
+ /** Idempotently repair a missing canonical installation; unsafe edits fail closed. */
356
+ export function repairManagedHookInstallation(options, acl = windowsAcl) {
357
+ const { requirements, managedDir, packageRoot, nodePath = process.execPath } = options;
358
+ const canonicalRequirements = path.resolve(requirements);
359
+ const canonicalManagedDir = path.resolve(managedDir);
360
+ const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
361
+ const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
362
+ const state = inspectManagedHookInstallation({ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot });
363
+ if (state.status === 'installed') return { ...state, repaired: false, warnings: [] };
364
+ if (state.status === 'unsafe') throw new Error(`Refusing automatic managed-hook repair: ${state.reason}`);
365
+ if (!fs.existsSync(sourceHook)) throw new Error(`Required Hook runtime file was not found: ${sourceHook}`);
366
+ const previousContent = fs.existsSync(canonicalRequirements) ? fs.readFileSync(canonicalRequirements, 'utf8') : '';
367
+ const content = buildManagedRequirements(previousContent, { targetDirectory: canonicalManagedDir, nodePath, hookPath });
368
+ const warnings = provisionManagedFiles({ requirements: canonicalRequirements, hookPath, sourceHook, nodePath, previousContent, content, launcher: managedHookLauncher(packageRoot) }, acl);
369
+ return { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, repaired: true, warnings };
370
+ }
371
+
372
+ export function windowsAcl(action, payload = {}, run = execFileSync) {
373
+ const executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
374
+ try {
375
+ const output = run(executable, [
376
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', aclScript,
377
+ '-Request', Buffer.from(JSON.stringify({ action, ...payload })).toString('base64'),
378
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 30_000 });
379
+ return JSON.parse(output.replace(/^\uFEFF/, '').trim());
380
+ } catch (error) {
381
+ const detail = error.stderr?.toString().trim() || error.message || String(error);
382
+ throw new Error(`Windows ACL ${action} failed (${payload.path || 'access verification'}, exit=${error.status ?? 'unknown'}): ${detail}`, { cause: error });
383
+ }
384
+ }
385
+
386
+ function existingFile(file, io) {
387
+ try {
388
+ const stat = io.lstatSync(file);
389
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Expected a regular file: ${file}`);
390
+ return stat;
391
+ } catch (error) {
392
+ if (error.code === 'ENOENT') return undefined;
393
+ throw error;
394
+ }
395
+ }
396
+
397
+ function makeReplaceable(file, io) {
398
+ const stat = existingFile(file, io);
399
+ // Windows' readonly attribute prevents replacement even for Administrators.
400
+ // chmod affects that attribute, not the DACL. Restore it on the old inode.
401
+ if (process.platform === 'win32' && stat && !(stat.mode & 0o200)) io.chmodSync(file, 0o600);
402
+ }
403
+
404
+ /** Keep the original inodes (including ACLs) until both replacements validate. */
405
+ export function commitPreparedFiles(entries, verify, io = fs) {
406
+ const records = [];
407
+ try {
408
+ for (const entry of entries) {
409
+ const stat = existingFile(entry.file, io);
410
+ const record = { ...entry, mode: stat?.mode, backup: undefined, installed: false };
411
+ if (stat) {
412
+ const backup = `${entry.file}.evolcore-backup-${randomUUID()}`;
413
+ // A same-directory hard link retains the exact original security
414
+ // descriptor. If unsupported, stop before replacing any live file.
415
+ io.linkSync(entry.file, backup);
416
+ record.backup = backup;
417
+ }
418
+ records.push(record);
419
+ }
420
+ for (const record of records) {
421
+ makeReplaceable(record.file, io);
422
+ io.renameSync(record.temporary, record.file);
423
+ record.installed = true;
424
+ if (record.backup) io.chmodSync(record.backup, record.mode);
425
+ }
426
+ verify();
427
+ } catch (error) {
428
+ const recoveryErrors = [];
429
+ for (const record of records.toReversed()) {
430
+ try {
431
+ if (record.installed) {
432
+ makeReplaceable(record.file, io);
433
+ if (record.backup) io.renameSync(record.backup, record.file);
434
+ else io.unlinkSync(record.file);
435
+ }
436
+ if (record.backup && io.existsSync(record.backup)) {
437
+ makeReplaceable(record.backup, io);
438
+ io.unlinkSync(record.backup);
439
+ }
440
+ if (record.mode !== undefined) io.chmodSync(record.file, record.mode);
441
+ } catch (recoveryError) {
442
+ recoveryErrors.push(`${record.file}: ${recoveryError.message}; original retained at ${record.backup || '(new installation)'}`);
443
+ }
444
+ }
445
+ throw new Error(`${error.message}${recoveryErrors.length
446
+ ? `; rollback incomplete: ${recoveryErrors.join(' | ')}`
447
+ : '; previous files and permissions restored'}`, { cause: error });
448
+ }
449
+ const warnings = [];
450
+ for (const record of records) {
451
+ if (!record.backup) continue;
452
+ try {
453
+ makeReplaceable(record.backup, io);
454
+ io.unlinkSync(record.backup);
455
+ } catch (error) {
456
+ warnings.push(`Installed successfully, but could not remove backup ${record.backup}: ${error.message}`);
457
+ }
458
+ }
459
+ return warnings;
460
+ }
461
+
462
+ /** This transaction is used only by explicit provisioning, never npm upgrades. */
463
+ export function provisionManagedFiles(options, acl = windowsAcl) {
464
+ const { requirements, hookPath, sourceHook, nodePath, content, launcher } = options;
465
+ const directory = path.dirname(hookPath);
466
+ const configDirectory = path.dirname(requirements);
467
+ acl('preflight');
468
+ fs.mkdirSync(configDirectory, { recursive: true });
469
+ const lockPath = path.join(configDirectory, '.evolcore-hooks.install.lock');
470
+ let lock;
471
+ try { lock = fs.openSync(lockPath, 'wx', 0o600); } catch (error) {
472
+ throw new Error(`Cannot acquire installer lock ${lockPath}: ${error.message}. If an installer crashed, confirm it has stopped before removing the lock.`, { cause: error });
473
+ }
474
+ const staged = [];
475
+ let committed = false;
476
+ try {
477
+ fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }));
478
+ const existing = existingFile(requirements, fs);
479
+ // Detect edits made after configuration generation, including a second
480
+ // installer completing just before this process acquired the lock.
481
+ if ((existing ? fs.readFileSync(requirements, 'utf8') : '') !== options.previousContent) {
482
+ throw new Error(`Configuration changed while preparing installation: ${requirements}. Retry using the latest configuration.`);
483
+ }
484
+ if (fs.existsSync(directory)) {
485
+ const stat = fs.lstatSync(directory);
486
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Expected a real hook directory: ${directory}`);
487
+ } else {
488
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
489
+ acl('prepare', { path: directory, directory: true });
490
+ }
491
+ // Existing directory ACLs are checked below, never rewritten: changing
492
+ // inheritable ACEs could alter the live Hook before the transaction starts.
493
+ for (const [file, value, reference] of [
494
+ [hookPath, launcher, undefined],
495
+ [requirements, content, existing ? requirements : undefined],
496
+ ]) {
497
+ const temporary = `${file}.evolcore-${randomUUID()}.tmp`;
498
+ staged.push({ file, temporary, value });
499
+ fs.writeFileSync(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
500
+ acl('prepare', { path: temporary, reference });
501
+ }
502
+ const readable = [
503
+ { path: configDirectory, execute: true },
504
+ { path: sourceHook },
505
+ { path: nodePath, execute: true },
506
+ ];
507
+ const verify = (temporary) => {
508
+ for (const entry of staged) {
509
+ const target = temporary ? entry.temporary : entry.file;
510
+ if (fs.readFileSync(target, 'utf8') !== entry.value) throw new Error(`Installed content differs: ${target}`);
511
+ }
512
+ return acl('verify', { entries: [
513
+ { path: directory, protected: true, execute: true },
514
+ ...staged.map(entry => ({ path: temporary ? entry.temporary : entry.file, protected: true })),
515
+ ...readable,
516
+ ] });
517
+ };
518
+ verify(true);
519
+ let accessReport;
520
+ const warnings = commitPreparedFiles(staged, () => { accessReport = verify(false); });
521
+ committed = true;
522
+ if ([accessReport].flat().some(entry => entry?.userProbe === 'unavailable')) {
523
+ 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.');
524
+ }
525
+ return warnings;
526
+ } finally {
527
+ for (const entry of staged) {
528
+ try { fs.rmSync(entry.temporary, { force: true }); } catch (error) {
529
+ process.stderr.write(`[evolcore] Could not clean staging file ${entry.temporary}: ${error.message}\n`);
530
+ }
531
+ }
532
+ fs.closeSync(lock);
533
+ try { fs.unlinkSync(lockPath); } catch (error) {
534
+ process.stderr.write(`[evolcore] ${committed ? 'Installed; ' : ''}could not remove installer lock ${lockPath}: ${error.message}\n`);
535
+ }
536
+ }
537
+ }
538
+
539
+ function main() {
540
+ // An explicit package root selects provisioning, even if the elevated
541
+ // child inherited npm lifecycle variables. Ordinary postinstall skips it.
542
+ if (!argument('--package-root') && (lifecycleEvent() === 'postinstall' || isGlobalInstall())) {
543
+ runLinuxPostinstall();
544
+ windowsPostinstallNotice();
545
+ return;
546
+ }
547
+ if (process.platform === 'linux' && argument('--mountpoint')) {
548
+ runLinuxPostinstall();
549
+ return;
550
+ }
551
+ if (process.platform !== 'win32') fail('Codex managed hook provisioning is only supported on Windows.');
552
+ if (!argument('--package-root')) fail('--package-root is required.');
553
+ const packageRoot = path.resolve(argument('--package-root'));
554
+ const nodePath = path.resolve(argument('--node-path') || process.execPath);
555
+ const requirements = path.resolve(argument('--requirements')
556
+ || path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml'));
557
+ const targetDirectory = path.resolve(argument('--managed-dir') || path.join(path.dirname(requirements), 'EvolCore', 'hooks'));
558
+ const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
559
+ const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
560
+ for (const file of [sourceHook, nodePath]) {
561
+ if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
562
+ }
563
+ if (hasArgument('--repair')) {
564
+ const result = repairManagedHookInstallation({ requirements, managedDir: targetDirectory, packageRoot, nodePath });
565
+ process.stdout.write(`${JSON.stringify(result)}\n`);
566
+ return;
567
+ }
568
+ const previousContent = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
569
+ const content = buildManagedRequirements(previousContent, { targetDirectory, nodePath, hookPath });
570
+ const warnings = provisionManagedFiles({
571
+ requirements, hookPath, sourceHook, nodePath, previousContent, content,
572
+ launcher: managedHookLauncher(packageRoot),
573
+ });
574
+ for (const warning of warnings) process.stderr.write(`[evolcore] ${warning}\n`);
575
+ process.stdout.write(`${JSON.stringify({ requirements, managedDir: targetDirectory, hookPath })}\n`);
576
+ }
577
+
578
+ if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) {
579
+ try { main(); } catch (error) {
580
+ fail(error instanceof Error ? error.message : String(error));
581
+ }
582
+ }