@b00dea/cockpit 0.0.0-stage → 0.1.0

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 (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +22 -2
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +62 -0
  11. package/dist/browser/commands.js +349 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +74 -4
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,1226 @@
1
+ import { posix } from 'node:path';
2
+ import { errorCause, snapshotJson } from '../environment/contract.js';
3
+ import { GUARD_INPUT_LIMITS, isParsedGuardInput, shownGuardWord } from './invocation-parser.js';
4
+ import { GUARD_HOST_LIMITS, isGuardHost, normalizeGuardHost } from './host.js';
5
+ import { GuardCommandArgv } from '../schema.js';
6
+ import { renderGuardProjectCommand } from '../environment/install-policy.js';
7
+ import { GUARD_LOCAL_PATHS } from './local-identity.js';
8
+ import { classifyReadOnlyInvocation, cockpitLauncherPrefix } from './read-only.js';
9
+ import { SURFACE_EXECUTION_CONTROL_PATHS } from '../surfaces/harness-registry.js';
10
+ // An agent able to rewrite any of these can switch the guard off, grant itself commands or run code
11
+ // when a harness starts: the published guard transport, every harness registration that launches it,
12
+ // the manifest that authenticates both, Claude's local settings, where `disableAllHooks` would switch
13
+ // the hook off, and every registry-owned native MCP or permission output. These spellings match the
14
+ // restriction expansion, the guard application and the generated surface outputs. An adopted copy
15
+ // of a registration or configuration file is merged into it on the next sync, so it is a control too.
16
+ const GUARD_CONTROL_FILES = [
17
+ '.cockpit/guards/bash-guard.sh', '.cockpit/generated-environment-ownership.json',
18
+ ...SURFACE_EXECUTION_CONTROL_PATHS,
19
+ '.claude/settings.local.json', '.codex/hooks.json', 'opencode.json',
20
+ 'cockpit/adopted/.claude/settings.json', 'cockpit/adopted/.codex/hooks.json', 'cockpit/adopted/.codex/config.toml',
21
+ // The policy the last sync accepted: an agent that rewrote it could present its own weakening as accepted.
22
+ '.cockpit/policy.json',
23
+ ];
24
+ // OpenCode loads every module in its plugin directories at start, and the generated Claude plugin
25
+ // bundle carries its own MCP servers and hook registration, so no file below them is data. Every
26
+ // harness starts the generated MCP wrapper scripts as the server program, so overwriting one changes
27
+ // what runs before the next guarded call. The machine-local guard identity names the handler the
28
+ // guard launches, so it is a control as well. The work-order records hold who accepted, answered and
29
+ // signed off each order, unkeyed, so a record an agent wrote there would read as a person's or CI's
30
+ // to the next `cockpit work` command: only cockpit writes it.
31
+ const GUARD_CONTROL_DIRECTORIES = ['.opencode/plugin', '.opencode/plugins', '.cockpit/bundles/claudecode-plugin', '.cockpit/mcp', GUARD_LOCAL_PATHS.directory,
32
+ '.cockpit/work-orders'];
33
+ const SECRETS_ROOT = 'env/.secrets';
34
+ // Environment files conventionally named for the variables they document, never their values.
35
+ const DOTENV_DOCUMENTATION = new Set(['.env.example', '.env.sample', '.env.template', '.env.schema']);
36
+ /**
37
+ * Bounds on the locations the policy and the path inspector carry, one per quantity, each set above
38
+ * what an honest checkout holds so that only an unwalkable tree or a broken install reaches it.
39
+ */
40
+ export const GUARD_LOCATION_LIMITS = Object.freeze({
41
+ // The running cockpit and its dependency closure, a few dozen packages, under both spellings.
42
+ packageDirectories: 256,
43
+ // Command search path entries inside the allowed roots, under both spellings; a PATH holds tens.
44
+ searchPathDirectories: 256,
45
+ // Declared environment templates, one per environment file; the policy bounds instances alike.
46
+ templates: 256,
47
+ // Environment files and secrets directories one searched tree or repository index holds. Each is
48
+ // compared against every protected target, and a monorepo keeps one or more per package.
49
+ sensitivePaths: 1024,
50
+ });
51
+ export const GUARD_SECRET_FILE_LIMIT = 512;
52
+ /** Root-relative paths whose identity every policy carries beside each instance's. */
53
+ export const GUARD_ROOT_PROTECTIONS = Object.freeze([
54
+ ...GUARD_CONTROL_FILES.map((path) => ({ path, type: 'file' })),
55
+ ...[...GUARD_CONTROL_DIRECTORIES, SECRETS_ROOT].map((path) => ({ path, type: 'directory' })),
56
+ ]);
57
+ /** One spelling of a file's identity, shared by the policy build and the path inspector. */
58
+ export function guardIdentity(status) {
59
+ return `${status.dev}:${status.ino}`;
60
+ }
61
+ const IDENTITY = /^\d+:\d+$/;
62
+ // The one normalisation applied to both sides of every spelling comparison: NFC plus full case
63
+ // folding. A case-insensitive volume resolves `.ENV` or `.ſecrets` to the protected entry, and
64
+ // neither byte equality nor toLowerCase() equates those spellings.
65
+ export function foldGuardSpelling(path) {
66
+ return path.normalize('NFC').toUpperCase().toLowerCase().normalize('NFC');
67
+ }
68
+ const fold = foldGuardSpelling;
69
+ const unavailable = (reason) => ({ decision: 'unavailable', reason });
70
+ const ask = (reason) => ({ decision: 'ask', reason });
71
+ const UNPROVEN_LAUNCHER = 'This cockpit command requires human approval: the guard could not prove it starts the installed cockpit. '
72
+ + 'npx --no cockpit counts only from the cockpit root with no root .npmrc and no bin in the root package.json, and bare cockpit only when '
73
+ + 'the command search path finds the installed package; a path such as ./node_modules/.bin/cockpit counts whenever it resolves to the installed package.';
74
+ const deny = (reason) => ({ decision: 'deny', reason });
75
+ // A refusal names the command it judged, so an agent can tell which part of a chain was refused.
76
+ const named = (invocation) => {
77
+ const words = invocation.executable === null ? [] : [invocation.executable, ...invocation.argv];
78
+ const shown = [];
79
+ for (const word of words) {
80
+ if (!/^[\w./@+=:,%^~{}-]+$/.test(word) || [...shown, word].join(' ').length > 80)
81
+ break;
82
+ shown.push(word);
83
+ }
84
+ return shown.length ? `\`${shown.join(' ')}${shown.length < words.length ? ' …' : ''}\`` : 'This command';
85
+ };
86
+ const allowed = Object.freeze({ decision: 'allow', reason: 'Every invocation matches an exact permitted profile or bounded data operation.' });
87
+ function record(value) {
88
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
89
+ }
90
+ function keys(value, names) {
91
+ return Object.keys(value).length === names.length && names.every((name) => Object.hasOwn(value, name));
92
+ }
93
+ function scalar(value) {
94
+ return typeof value === 'string' && value.length > 0 && !/[\uD800-\uDFFF]/u.test(value) && !/[\x00-\x1f\x7f]/.test(value) && Buffer.byteLength(value) <= GUARD_INPUT_LIMITS.wordBytes;
95
+ }
96
+ function canonical(value) {
97
+ return scalar(value) && value.startsWith('/') && value !== '/' && posix.normalize(value) === value && !value.endsWith('/');
98
+ }
99
+ function within(path, root) { return path === root || path.startsWith(root + '/'); }
100
+ function identity(value) { return typeof value === 'string' && value.length <= 64 && IDENTITY.test(value); }
101
+ function strings(value) {
102
+ return Array.isArray(value) && value.every(scalar);
103
+ }
104
+ function argv(value) {
105
+ return Array.isArray(value) && value.length <= GUARD_INPUT_LIMITS.words && value.every((word) => typeof word === 'string' && !/[\uD800-\uDFFF]/u.test(word) && !/[\x00-\x1f\x7f]/.test(word)
106
+ && Buffer.byteLength(word) <= GUARD_INPUT_LIMITS.wordBytes && word !== '*' && word !== '**');
107
+ }
108
+ function ruleArgv(value) {
109
+ return Array.isArray(value) && GuardCommandArgv.safeParse(value).success;
110
+ }
111
+ function projectTuple(value) {
112
+ if (!Array.isArray(value.argv))
113
+ return false;
114
+ const tuple = GuardCommandArgv.safeParse([value.executable, ...value.argv]);
115
+ return tuple.success && renderGuardProjectCommand(tuple.data).kind === 'complete';
116
+ }
117
+ function capturePolicy(value) {
118
+ const p = snapshotJson(value);
119
+ if (!record(p) || !keys(p, ['canonicalRoot', 'configPath', 'allowedRoots', 'instances', 'templates', 'protectedIdentities', 'protectedBranches', 'hostedDatabaseHosts', 'vendorHosts', 'declaredAllows', 'declaredDenies', 'ripgrepConfigActive', 'executionDirectories', 'searchDirectories', 'searchPathRelativeEntry', 'secretFileIdentities', 'protectedFilesLinked', 'cockpitInvocations', 'cockpitLaunchers', 'projectCommands'])
120
+ || !canonical(p.canonicalRoot) || !canonical(p.configPath) || posix.dirname(p.configPath) !== p.canonicalRoot || !strings(p.allowedRoots) || !p.allowedRoots.includes(p.canonicalRoot) || !p.allowedRoots.every(canonical)
121
+ || !strings(p.protectedBranches) || !strings(p.hostedDatabaseHosts) || !strings(p.vendorHosts)
122
+ || !Array.isArray(p.instances) || !strings(p.templates) || p.templates.length > GUARD_LOCATION_LIMITS.templates || !Array.isArray(p.protectedIdentities) || !Array.isArray(p.declaredAllows) || !Array.isArray(p.declaredDenies)
123
+ || typeof p.ripgrepConfigActive !== 'boolean' || typeof p.protectedFilesLinked !== 'boolean' || !Array.isArray(p.cockpitInvocations) || !Array.isArray(p.cockpitLaunchers) || !Array.isArray(p.projectCommands)
124
+ || !strings(p.executionDirectories) || p.executionDirectories.length > GUARD_LOCATION_LIMITS.packageDirectories
125
+ || !strings(p.searchDirectories) || p.searchDirectories.length > GUARD_LOCATION_LIMITS.searchPathDirectories
126
+ || p.searchPathRelativeEntry !== null && (typeof p.searchPathRelativeEntry !== 'string' || p.searchPathRelativeEntry.startsWith('/')
127
+ || /[\x00-\x1f\x7f]/.test(p.searchPathRelativeEntry) || Buffer.byteLength(p.searchPathRelativeEntry) > GUARD_INPUT_LIMITS.wordBytes)
128
+ || p.secretFileIdentities !== null && (!Array.isArray(p.secretFileIdentities) || p.secretFileIdentities.length > GUARD_SECRET_FILE_LIMIT || !p.secretFileIdentities.every(identity)))
129
+ return undefined;
130
+ const roots = p.allowedRoots;
131
+ const confined = (value) => canonical(value) && roots.some((root) => within(value, root));
132
+ if (![...p.executionDirectories, ...p.searchDirectories].every(confined)
133
+ || [p.hostedDatabaseHosts, p.vendorHosts].some((hosts) => hosts.length > GUARD_HOST_LIMITS.rows
134
+ || !hosts.every(isGuardHost) || new Set(hosts.map(normalizeGuardHost)).size !== hosts.length)
135
+ || p.protectedBranches.some((branch) => /[\s:*?\[\\]/.test(branch))
136
+ || p.instances.some((item) => !record(item) || !keys(item, ['name', 'path']) || !scalar(item.name) || !confined(item.path))
137
+ || !p.templates.every(confined)
138
+ || p.declaredAllows.length > 64 || p.declaredAllows.some((item) => !record(item)
139
+ || !keys(item, item.cwd === undefined ? ['argv', 'reason'] : ['argv', 'reason', 'cwd'])
140
+ || !ruleArgv(item.argv) || !scalar(item.reason) || item.cwd !== undefined && !confined(item.cwd))
141
+ || p.declaredDenies.length > 64 || p.declaredDenies.some((item) => !record(item)
142
+ || !keys(item, item.fix === undefined ? ['match', 'argv', 'reason'] : ['match', 'argv', 'reason', 'fix'])
143
+ || !['prefix', 'contains'].includes(String(item.match)) || !ruleArgv(item.argv) || !scalar(item.reason)
144
+ || item.fix !== undefined && !scalar(item.fix))
145
+ || p.cockpitInvocations.some((item) => !record(item) || !keys(item, item.reads === undefined ? ['executable', 'cwd', 'argv'] : ['executable', 'cwd', 'argv', 'reads'])
146
+ || !scalar(item.executable) || item.executable.includes('*') || !confined(item.cwd) || !argv(item.argv)
147
+ || item.reads !== undefined && (!strings(item.reads) || item.reads.length > GUARD_INPUT_LIMITS.paths))
148
+ || p.cockpitLaunchers.length > GUARD_INPUT_LIMITS.invocations || p.cockpitLaunchers.some((item) => !record(item) || !keys(item, ['executable', 'cwd', 'argv'])
149
+ || !scalar(item.executable) || !argv(item.argv) || cockpitLauncherPrefix(item.executable, [...item.argv, 'check']) !== item.argv.length || !confined(item.cwd))
150
+ || p.projectCommands.some((item) => !record(item) || !keys(item, ['executable', 'checkoutRoot', 'kind', 'argv']) || !confined(item.checkoutRoot) || !['build', 'test'].includes(String(item.kind)) || !projectTuple(item))
151
+ || p.protectedIdentities.some((item) => !record(item) || !keys(item, ['path', 'identity', 'ancestors']) || !confined(item.path)
152
+ || item.identity !== null && !identity(item.identity) || !Array.isArray(item.ancestors) || !item.ancestors.every(identity)))
153
+ return undefined;
154
+ // A policy that omits an identity would silently fall back to spellings, so coverage is exact.
155
+ const expected = new Set([...p.instances.map((item) => item.path), p.configPath,
156
+ ...GUARD_ROOT_PROTECTIONS.map((item) => `${p.canonicalRoot}/${item.path}`)]);
157
+ const carried = new Set(p.protectedIdentities.map((item) => item.path));
158
+ if (carried.size !== p.protectedIdentities.length || carried.size !== expected.size || [...expected].some((path) => !carried.has(path)))
159
+ return undefined;
160
+ // Every field above is captured own data and checked before entering the typed policy core.
161
+ return p;
162
+ }
163
+ function hostUsed(value, configured) {
164
+ const host = normalizeGuardHost(configured);
165
+ // Resolvers apply compatibility normalisation before lookup, so fullwidth letters and the
166
+ // ideographic full stops name the same host as their ASCII spelling.
167
+ const lower = value.normalize('NFKC').replace(/[\u3002\uff0e\uff61]/g, '.').toLowerCase();
168
+ let offset = lower.indexOf(host);
169
+ while (offset >= 0) {
170
+ const before = lower[offset - 1];
171
+ const after = lower[offset + host.length];
172
+ const afterDot = after === '.' ? lower[offset + host.length + 1] : undefined;
173
+ if ((before === undefined || !/[a-z0-9_.-]/.test(before) || configured.startsWith('*.') && before === '.')
174
+ && (after === undefined || !/[a-z0-9_.-]/.test(after) || after === '.' && (afterDot === undefined || !/[a-z0-9_.-]/.test(afterDot))))
175
+ return true;
176
+ offset = lower.indexOf(host, offset + 1);
177
+ }
178
+ return false;
179
+ }
180
+ function ruleMatches(invocation, rule) {
181
+ if (invocation.executable === null)
182
+ return false;
183
+ const words = [invocation.executable, ...invocation.argv];
184
+ if ('cwd' in rule && rule.cwd !== undefined && rule.cwd !== invocation.cwd)
185
+ return false;
186
+ if (!('match' in rule) || rule.match === 'prefix')
187
+ return rule.argv.every((word, index) => words[index] === word);
188
+ return words.some((_, start) => rule.argv.every((word, index) => words[start + index] === word));
189
+ }
190
+ function gitInvocation(invocation) {
191
+ if (invocation.executable === null || posix.basename(invocation.executable) !== 'git')
192
+ return undefined;
193
+ const args = invocation.argv;
194
+ let cursor = 0;
195
+ let cwd = invocation.cwd;
196
+ let uncertain = false;
197
+ const directories = [];
198
+ while (args[cursor]?.startsWith('-')) {
199
+ const option = args[cursor++];
200
+ if (option === '--')
201
+ break;
202
+ if (option === '-C') {
203
+ const directory = args[cursor++];
204
+ if (directory === undefined)
205
+ return { verb: '', args: [], cwd, directories, uncertain: true };
206
+ directories.push({ path: directory, cwd });
207
+ cwd = posix.resolve(cwd, directory);
208
+ }
209
+ else if (['-c', '--git-dir', '--work-tree', '--namespace', '--config-env'].includes(option)) {
210
+ cursor++;
211
+ uncertain = true;
212
+ }
213
+ else if (['--no-pager', '--paginate', '--no-optional-locks'].includes(option)) {
214
+ continue;
215
+ }
216
+ else if (/^--(?:git-dir|work-tree|namespace|config-env)=/.test(option)) {
217
+ uncertain = true;
218
+ }
219
+ else
220
+ return { verb: '', args: [], cwd, directories, uncertain: true };
221
+ }
222
+ return { verb: args[cursor++] ?? '', args: args.slice(cursor), cwd, directories, uncertain };
223
+ }
224
+ const REMOVERS = new Set(['rm', 'unlink', 'rmdir', 'shred', 'trash']);
225
+ /** Visible source and destination paths for copy/install/move/link without inferring filesystem state. */
226
+ function transferOperands(command, args) {
227
+ const operands = [];
228
+ let targetDirectory;
229
+ let noTargetDirectory = false;
230
+ let installDirectories = false;
231
+ let uncertain = false;
232
+ let options = true;
233
+ const plainShort = command === 'cp' ? 'afHiLlnPpRrsvXx' : command === 'mv' ? 'fhinuv' : command === 'ln' ? 'bdFfhiLnPrsvw' : 'bCcpSsUv';
234
+ const valueShort = command === 'install' ? new Set(['f', 'g', 'm', 'o', 'M', 'h', 'B', 'l', 'N']) : command === 'ln' ? new Set(['S', 't']) : new Set();
235
+ const plainLong = new Set(command === 'cp' ? ['archive', 'force', 'interactive', 'link', 'no-clobber', 'no-dereference', 'preserve', 'recursive', 'reflink', 'remove-destination', 'sparse', 'symbolic-link', 'update', 'verbose']
236
+ : command === 'mv' ? ['force', 'interactive', 'no-clobber', 'no-target-directory', 'update', 'verbose']
237
+ : command === 'ln' ? ['backup', 'directory', 'force', 'interactive', 'logical', 'no-dereference', 'physical', 'relative', 'symbolic', 'verbose']
238
+ : ['backup', 'compare', 'directory', 'preserve-timestamps', 'strip', 'verbose']);
239
+ const valueLong = new Set(command === 'install' ? ['group', 'mode', 'owner', 'suffix'] : ['suffix']);
240
+ for (let index = 0; index < args.length; index++) {
241
+ const arg = args[index];
242
+ if (options && arg === '--') {
243
+ options = false;
244
+ continue;
245
+ }
246
+ if (options && (arg === '-t' || arg === '--target-directory')) {
247
+ targetDirectory = args[++index];
248
+ if (targetDirectory === undefined)
249
+ uncertain = true;
250
+ continue;
251
+ }
252
+ if (options && arg.startsWith('--target-directory=')) {
253
+ targetDirectory = arg.slice('--target-directory='.length);
254
+ if (!targetDirectory)
255
+ uncertain = true;
256
+ continue;
257
+ }
258
+ if (options && (arg === '-T' || arg === '--no-target-directory')) {
259
+ noTargetDirectory = true;
260
+ continue;
261
+ }
262
+ if (options && command === 'install' && (arg === '-d' || arg === '--directory')) {
263
+ installDirectories = true;
264
+ continue;
265
+ }
266
+ if (options && arg.startsWith('--')) {
267
+ const [option, attached] = arg.slice(2).split(/=(.*)/s);
268
+ if (valueLong.has(option)) {
269
+ if (attached === undefined && args[++index] === undefined)
270
+ uncertain = true;
271
+ }
272
+ else if (!plainLong.has(option))
273
+ uncertain = true;
274
+ if (command === 'cp' && ['recursive', 'archive'].includes(option))
275
+ uncertain = true;
276
+ continue;
277
+ }
278
+ if (options && arg.startsWith('-') && arg !== '-') {
279
+ const letters = arg.slice(1);
280
+ const valueAt = [...letters].findIndex(letter => valueShort.has(letter));
281
+ if (valueAt >= 0) {
282
+ const value = valueAt === letters.length - 1 ? args[++index] : undefined;
283
+ if (value === undefined)
284
+ uncertain = true;
285
+ // `ln -sft dir source` names the target directory inside a cluster.
286
+ else if (letters[valueAt] === 't')
287
+ targetDirectory = value;
288
+ }
289
+ else if ([...letters].some(letter => !plainShort.includes(letter)))
290
+ uncertain = true;
291
+ if (command === 'cp' && /[Rr]/.test(letters))
292
+ uncertain = true;
293
+ continue;
294
+ }
295
+ operands.push(arg);
296
+ }
297
+ if (installDirectories)
298
+ return { sources: [], writes: operands, uncertain: uncertain || targetDirectory !== undefined || noTargetDirectory };
299
+ if (targetDirectory !== undefined)
300
+ return {
301
+ sources: operands,
302
+ writes: [targetDirectory, ...operands.map(source => posix.join(targetDirectory, posix.basename(source)))],
303
+ uncertain: uncertain || noTargetDirectory,
304
+ };
305
+ // `ln source` links into the working directory under the source's own name.
306
+ if (command === 'ln' && operands.length === 1)
307
+ return { sources: operands, writes: [posix.basename(operands[0])], uncertain: uncertain || noTargetDirectory };
308
+ if (operands.length < 2)
309
+ return { sources: [], writes: [], uncertain: true };
310
+ const sources = operands.slice(0, -1);
311
+ const destination = operands.at(-1);
312
+ return {
313
+ sources,
314
+ writes: [destination, ...(!noTargetDirectory ? sources.map(source => posix.join(destination, posix.basename(source))) : [])],
315
+ uncertain,
316
+ };
317
+ }
318
+ /** Git abbreviates long options and bundles short ones, so output recognition must consume every known option value. */
319
+ function gitArchiveWrites(args) {
320
+ const longOptions = [
321
+ { name: 'format', valued: true },
322
+ { name: 'prefix', valued: true },
323
+ { name: 'add-file', valued: true },
324
+ { name: 'add-virtual-file', valued: true },
325
+ { name: 'output', valued: true },
326
+ { name: 'worktree-attributes', valued: false },
327
+ { name: 'verbose', valued: false },
328
+ { name: 'mtime', valued: true },
329
+ { name: 'list', valued: false },
330
+ { name: 'remote', valued: true },
331
+ { name: 'exec', valued: true },
332
+ ];
333
+ const paths = [];
334
+ let output = false;
335
+ let uncertain = false;
336
+ for (let index = 0; index < args.length; index++) {
337
+ const arg = args[index];
338
+ if (arg === '--')
339
+ break;
340
+ if (arg.startsWith('--')) {
341
+ const [spelling, attached] = arg.slice(2).split(/=(.*)/s);
342
+ if (spelling.startsWith('no-')) {
343
+ const name = spelling.slice(3);
344
+ const matches = longOptions.filter(option => option.name !== 'mtime' && option.name.startsWith(name));
345
+ if (matches.length !== 1 || attached !== undefined)
346
+ uncertain = true;
347
+ continue;
348
+ }
349
+ const matches = longOptions.filter(option => option.name.startsWith(spelling));
350
+ if (matches.length !== 1) {
351
+ uncertain = true;
352
+ continue;
353
+ }
354
+ const option = matches[0];
355
+ if (!option.valued) {
356
+ if (attached !== undefined)
357
+ uncertain = true;
358
+ continue;
359
+ }
360
+ const value = attached ?? args[++index];
361
+ if (!value)
362
+ uncertain = true;
363
+ else if (option.name === 'output') {
364
+ output = true;
365
+ paths.push(value);
366
+ }
367
+ continue;
368
+ }
369
+ if (/^-[0-9]+$/.test(arg))
370
+ continue;
371
+ if (arg.startsWith('-') && arg !== '-') {
372
+ const options = arg.slice(1);
373
+ for (let offset = 0; offset < options.length; offset++) {
374
+ const option = options[offset];
375
+ if (option === 'v' || option === 'l')
376
+ continue;
377
+ if (option === 'o') {
378
+ output = true;
379
+ const value = options.slice(offset + 1) || args[++index];
380
+ if (value)
381
+ paths.push(value);
382
+ else
383
+ uncertain = true;
384
+ break;
385
+ }
386
+ uncertain = true;
387
+ break;
388
+ }
389
+ }
390
+ }
391
+ return output || uncertain ? { paths, removes: true, uncertain } : undefined;
392
+ }
393
+ function tarExtractionWrites(args) {
394
+ let extraction = false;
395
+ let destination = '.';
396
+ let archive;
397
+ let uncertain = false;
398
+ let options = true;
399
+ for (let index = 0; index < args.length; index++) {
400
+ const arg = args[index];
401
+ if (options && arg === '--') {
402
+ options = false;
403
+ continue;
404
+ }
405
+ if (!options || !arg.startsWith('-') || arg === '-')
406
+ continue;
407
+ if (arg === '-x' || arg === '--extract' || arg === '--get') {
408
+ extraction = true;
409
+ continue;
410
+ }
411
+ if (arg === '-xf') {
412
+ extraction = true;
413
+ archive = args[++index];
414
+ if (archive === undefined)
415
+ uncertain = true;
416
+ continue;
417
+ }
418
+ if (arg === '-f' || arg === '--file') {
419
+ archive = args[++index];
420
+ if (archive === undefined)
421
+ uncertain = true;
422
+ continue;
423
+ }
424
+ if (arg.startsWith('--file=')) {
425
+ archive = arg.slice('--file='.length);
426
+ if (!archive)
427
+ uncertain = true;
428
+ continue;
429
+ }
430
+ if (arg === '-C' || arg === '--directory') {
431
+ const value = args[++index];
432
+ if (value === undefined)
433
+ uncertain = true;
434
+ else
435
+ destination = value;
436
+ continue;
437
+ }
438
+ if (arg.startsWith('--directory=')) {
439
+ const value = arg.slice('--directory='.length);
440
+ if (!value)
441
+ uncertain = true;
442
+ else
443
+ destination = value;
444
+ continue;
445
+ }
446
+ if (/^-[^-]*x/.test(arg) || arg.startsWith('--extract=') || arg.startsWith('--get='))
447
+ extraction = true;
448
+ uncertain = true;
449
+ }
450
+ return { paths: extraction ? [destination] : [], reads: archive === undefined ? [] : [archive], removes: false, uncertain: uncertain || !extraction };
451
+ }
452
+ const CHMOD_CLAUSE = '[ugoa]*(?:[-+=][rwxXstugo]*)+';
453
+ const CHMOD_MODE = new RegExp(`^(?:[0-7]{1,4}|${CHMOD_CLAUSE}(?:,${CHMOD_CLAUSE})*)$`);
454
+ function keepsOwnerRead(mode) {
455
+ if (/^[0-7]+$/.test(mode))
456
+ return (Number(mode.padStart(4, '0')[1]) & 4) !== 0;
457
+ return mode.split(',').every((clause) => {
458
+ const match = /^([ugoa]*)((?:[-+=][rwxXst]*)+)$/.exec(clause);
459
+ if (!match)
460
+ return false;
461
+ if (match[1] !== '' && !/[ua]/.test(match[1]))
462
+ return true;
463
+ return [...match[2].matchAll(/([-+=])([rwxXst]*)/g)].every(([, operator, permissions]) => operator === '+' || operator === '-' && !permissions.includes('r') || operator === '=' && permissions.includes('r'));
464
+ });
465
+ }
466
+ const GIT_CONFIG_READ_ACTIONS = new Set(['--get', '--get-all', '--get-regexp', '--get-urlmatch', '--get-color', '--get-colorbool', '--list', '-l']);
467
+ const GIT_CONFIG_READ_OPTIONS = new Set(['--global', '--system', '--local', '--worktree', '--includes', '--no-includes', '--show-origin',
468
+ '--show-scope', '--name-only', '-z', '--null', '--bool', '--int', '--bool-or-int', '--path', '--expiry-date', '--fixed-value', '--all']);
469
+ const GIT_CONFIG_VALUE_OPTIONS = new Set(['-f', '--file', '--blob', '--type', '--default']);
470
+ /** Whether a `git config` invocation is a proven read; every other form may write configuration. */
471
+ function gitConfigReads(args) {
472
+ const subcommand = args[0] === 'get' || args[0] === 'list';
473
+ let action = subcommand;
474
+ const positionals = [];
475
+ for (let index = subcommand ? 1 : 0; index < args.length; index++) {
476
+ const arg = args[index];
477
+ if (!arg.startsWith('-')) {
478
+ positionals.push(arg);
479
+ continue;
480
+ }
481
+ const name = arg.split('=')[0];
482
+ if (GIT_CONFIG_READ_ACTIONS.has(name))
483
+ action = true;
484
+ else if (GIT_CONFIG_VALUE_OPTIONS.has(name)) {
485
+ if (!arg.includes('='))
486
+ index++;
487
+ }
488
+ else if (!GIT_CONFIG_READ_OPTIONS.has(name))
489
+ return false;
490
+ }
491
+ // A lone `section.key` reads it; a lone word such as `edit` is a subcommand that writes.
492
+ return action || positionals.length === 1 && positionals[0].includes('.');
493
+ }
494
+ /** Destinations exposed directly by bounded filesystem writer grammars. */
495
+ function visibleWrites(invocation) {
496
+ if (invocation.executable === null)
497
+ return undefined;
498
+ const name = posix.basename(invocation.executable);
499
+ if (name === 'cp' || name === 'install' || name === 'mv') {
500
+ const transfer = transferOperands(name, invocation.argv);
501
+ return { paths: transfer.writes, reads: name === 'mv' ? [] : transfer.sources, removes: false, uncertain: transfer.uncertain };
502
+ }
503
+ if (name === 'ln') {
504
+ const transfer = transferOperands('ln', invocation.argv);
505
+ return { paths: transfer.writes, links: transfer.sources, removes: true, uncertain: transfer.uncertain };
506
+ }
507
+ if (name === 'dd') {
508
+ const paths = invocation.argv.filter(arg => arg.startsWith('of=')).map(arg => arg.slice(3)).filter(Boolean);
509
+ const reads = invocation.argv.filter(arg => arg.startsWith('if=')).map(arg => arg.slice(3)).filter(Boolean);
510
+ const operands = new Set(['if', 'of', 'bs', 'ibs', 'obs', 'cbs', 'count', 'skip', 'seek', 'iseek', 'oseek', 'files', 'fillchar', 'speed', 'status', 'conv', 'iflag', 'oflag']);
511
+ const conversions = new Set(['ascii', 'ebcdic', 'ibm', 'block', 'unblock', 'lcase', 'ucase', 'swab', 'noerror', 'notrunc', 'sync',
512
+ 'osync', 'sparse', 'fsync', 'fdatasync', 'excl', 'nocreat', 'oldascii', 'oldebcdic', 'oldibm', 'pareven', 'parnone', 'parodd', 'parset']);
513
+ const flags = new Set(['append', 'binary', 'cio', 'direct', 'directory', 'dsync', 'noatime', 'nocache', 'noctty', 'nofollow',
514
+ 'nolinks', 'nonblock', 'sync', 'text', 'fullblock', 'count_bytes', 'skip_bytes', 'seek_bytes', 'fsync']);
515
+ let removes = true;
516
+ let uncertain = false;
517
+ for (const arg of invocation.argv) {
518
+ const [operand, value] = arg.split(/=(.*)/s);
519
+ if (!operands.has(operand) || !value) {
520
+ uncertain = true;
521
+ continue;
522
+ }
523
+ if (operand === 'conv') {
524
+ const values = value.split(',');
525
+ if (values.some(value => !conversions.has(value)))
526
+ uncertain = true;
527
+ // GNU and BSD accumulate conversions across operands; later conv= cannot undo notrunc.
528
+ if (values.includes('notrunc'))
529
+ removes = false;
530
+ }
531
+ else if (operand === 'iflag' || operand === 'oflag') {
532
+ if (value.split(',').some(value => !flags.has(value)))
533
+ uncertain = true;
534
+ }
535
+ }
536
+ return { paths, reads, removes, uncertain };
537
+ }
538
+ if (name === 'tar')
539
+ return tarExtractionWrites(invocation.argv);
540
+ const scan = (plain, valued) => {
541
+ const operands = [];
542
+ const optionArguments = [];
543
+ let optionsAfterOperand = false;
544
+ let uncertain = false;
545
+ let options = true;
546
+ for (let index = 0; index < invocation.argv.length; index++) {
547
+ const arg = invocation.argv[index];
548
+ if (options && arg === '--') {
549
+ options = false;
550
+ continue;
551
+ }
552
+ if (options && arg.startsWith('-') && arg !== '-') {
553
+ optionArguments.push(arg);
554
+ if (operands.length)
555
+ optionsAfterOperand = true;
556
+ const option = arg.split('=')[0];
557
+ if (valued.has(option)) {
558
+ if (!arg.includes('=') && invocation.argv[++index] === undefined)
559
+ uncertain = true;
560
+ }
561
+ else if (option.startsWith('--') ? !plain.has(option) : [...option.slice(1)].some(letter => !plain.has(`-${letter}`)))
562
+ uncertain = true;
563
+ continue;
564
+ }
565
+ operands.push(arg);
566
+ }
567
+ return { operands, optionArguments, optionsAfterOperand, uncertain };
568
+ };
569
+ if (name === 'tee') {
570
+ const result = scan(new Set(['-a', '-i', '-p', '--append', '--ignore-interrupts', '--output-error']), new Set());
571
+ // BSD stops option parsing at the first filename; GNU may permute later options.
572
+ const append = !result.optionsAfterOperand && result.optionArguments.some(arg => arg === '--append' || /^-[aip]*a[aip]*$/.test(arg));
573
+ return { paths: result.operands, removes: !append, uncertain: result.uncertain || result.optionsAfterOperand
574
+ || result.optionArguments.some(arg => arg.includes('=') && !/^--output-error=(?:warn|warn-nopipe|exit|exit-nopipe)$/.test(arg)) };
575
+ }
576
+ if (name === 'touch') {
577
+ const result = scan(new Set(['-a', '-c', '-h', '-m', '--no-create']), new Set(['-A', '-d', '-r', '-t', '--date', '--reference', '--time']));
578
+ return { paths: result.operands, removes: false, uncertain: result.uncertain };
579
+ }
580
+ if (name === 'truncate') {
581
+ const result = scan(new Set(['-c', '--no-create']), new Set(['-r', '-s', '--reference', '--size']));
582
+ return { paths: result.operands, removes: true, uncertain: result.uncertain };
583
+ }
584
+ if (name === 'chmod') {
585
+ const result = scan(new Set(['-f', '-h', '-v', '-R', '-H', '-L', '-P', '-E', '-C', '-N', '-i', '-I']), new Set(['--reference']));
586
+ const reference = result.optionArguments.some(arg => arg.startsWith('--reference'));
587
+ const mode = !reference && result.operands.length > 1 && CHMOD_MODE.test(result.operands[0]) ? result.operands[0] : undefined;
588
+ // A mode taken from another file, or one that drops the owner's read, can leave a file unloadable.
589
+ return { paths: result.operands.slice(mode === undefined ? 0 : 1), removes: mode === undefined || !keepsOwnerRead(mode),
590
+ uncertain: result.uncertain || result.operands.length === 0 };
591
+ }
592
+ return undefined;
593
+ }
594
+ /** Paths an invocation deletes or renames away; undefined when it does neither. */
595
+ function removedOperands(invocation, git) {
596
+ if (invocation.executable === null)
597
+ return undefined;
598
+ const name = posix.basename(invocation.executable);
599
+ const positionals = (args) => {
600
+ const end = args.indexOf('--');
601
+ return [...(end < 0 ? args : args.slice(0, end)).filter((arg) => !arg.startsWith('-')), ...(end < 0 ? [] : args.slice(end + 1))];
602
+ };
603
+ if (REMOVERS.has(name))
604
+ return { cwd: invocation.cwd, paths: positionals(invocation.argv) };
605
+ if (name === 'mv')
606
+ return { cwd: invocation.cwd, paths: transferOperands('mv', invocation.argv).sources };
607
+ if (name === 'find' && invocation.argv.includes('-delete')) {
608
+ const roots = [];
609
+ for (const arg of invocation.argv) {
610
+ if (arg.startsWith('-') || arg === '!' || arg === '(')
611
+ break;
612
+ roots.push(arg);
613
+ }
614
+ return { cwd: invocation.cwd, paths: roots.length ? roots : ['.'] };
615
+ }
616
+ if (git?.verb === 'rm')
617
+ return { cwd: git.cwd, paths: positionals(git.args) };
618
+ if (git?.verb === 'mv')
619
+ return { cwd: git.cwd, paths: positionals(git.args).slice(0, -1) };
620
+ return undefined;
621
+ }
622
+ function pushDecision(args, branches) {
623
+ const positionals = [];
624
+ let options = true;
625
+ let uncertain = false;
626
+ let allBranches = false;
627
+ for (let i = 0; i < args.length; i++) {
628
+ const value = args[i];
629
+ if (options && value === '--') {
630
+ options = false;
631
+ continue;
632
+ }
633
+ if (options && value.startsWith('-')) {
634
+ if (['--all', '--mirror'].includes(value))
635
+ allBranches = true;
636
+ else if (['-o', '--push-option', '--repo', '--receive-pack', '--exec'].includes(value)) {
637
+ if (args[++i] === undefined) {
638
+ uncertain = true;
639
+ continue;
640
+ }
641
+ // --repo supplies the remote rather than a positional remote.
642
+ if (value === '--repo')
643
+ positionals.unshift(args[i]);
644
+ }
645
+ else if (/^--(?:push-option|receive-pack|exec)=/.test(value))
646
+ continue;
647
+ else if (value.startsWith('--repo='))
648
+ positionals.unshift(value.slice(7));
649
+ else if (!['-f', '--force', '--force-with-lease', '--force-if-includes', '-u', '--set-upstream', '-d', '--delete', '--tags', '--follow-tags', '--dry-run', '-n', '-v', '--verbose', '-q', '--quiet', '--atomic', '--porcelain', '--no-verify', '--prune'].includes(value)
650
+ && !value.startsWith('--force-with-lease='))
651
+ uncertain = true;
652
+ }
653
+ else
654
+ positionals.push(value);
655
+ }
656
+ if (allBranches && branches.length)
657
+ return deny('Refusing push to a declared protected branch.');
658
+ for (const ref of positionals.slice(1)) {
659
+ const withoutForce = ref.replace(/^\+/, '');
660
+ const colon = withoutForce.indexOf(':');
661
+ const destination = colon < 0 ? withoutForce : withoutForce.slice(colon + 1);
662
+ if (branches.some((branch) => destination === branch || destination === `refs/heads/${branch}`))
663
+ return deny('Refusing push to a declared protected branch.');
664
+ if (!destination || destination.includes('*'))
665
+ uncertain = true;
666
+ // Source-only revisions and symbolic aliases need repository state to name a destination;
667
+ // an exact argv grant cannot supply that evidence.
668
+ if (colon < 0 && (/(?:^|\/)[A-Z_]+$|[@~^{}]|\.\./.test(withoutForce)
669
+ || /^[a-f0-9]+$/i.test(withoutForce) || /-\d+-g[a-f0-9]+$/i.test(withoutForce)
670
+ || /^(?:heads|tags|remotes)\//.test(withoutForce)))
671
+ uncertain = true;
672
+ }
673
+ if (uncertain || positionals.length < 2)
674
+ return ask('Push destination requires human approval.');
675
+ return undefined;
676
+ }
677
+ /** Hard boundaries are independent of grants and are evaluated across the complete capture. */
678
+ export function evaluateGuard(input, policy, inspect) {
679
+ if (!isParsedGuardInput(input))
680
+ return unavailable('Guard input is unavailable or was not captured by the invocation parser.');
681
+ const resolved = capturePolicy(policy);
682
+ if (!resolved)
683
+ return unavailable('Guard policy is incomplete or malformed.');
684
+ const absolute = (path) => `${resolved.canonicalRoot}/${path}`;
685
+ const secretRoot = absolute(SECRETS_ROOT);
686
+ const controlFiles = GUARD_CONTROL_FILES.map(absolute);
687
+ const controlDirectories = GUARD_CONTROL_DIRECTORIES.map(absolute);
688
+ const identities = new Map(resolved.protectedIdentities.map((item) => [item.path, item]));
689
+ const identityOf = (path) => identities.get(path).identity;
690
+ const chainOf = (path) => { const item = identities.get(path); return item.identity === null ? item.ancestors : [...item.ancestors, item.identity]; };
691
+ const secretDenial = deny('Refusing mutation or staging below a secrets directory.');
692
+ const instanceDenial = deny('Refusing mutation or staging of a declared environment instance.');
693
+ // `git add -A`, `git add .` and a directory operand stage everything below them.
694
+ const coveringStageDenial = deny(`Refusing to stage a path that contains ${SECRETS_ROOT} or a declared environment instance: \`git add -A\`, \`git add .\` and a directory operand stage everything below them. Stage the changed files by name instead.`);
695
+ const controlDenial = deny('Refusing mutation of the guard, its hook registration or its ownership manifest.');
696
+ const gitMetadataDenial = deny('Refusing direct mutation of Git metadata: its configuration chooses programs that read-only Git commands run.');
697
+ const readDenial = deny('Refusing to read a declared environment instance or secrets directory into the agent context.');
698
+ const templates = new Set(resolved.templates.map(fold));
699
+ // Documentation by its name or by the configuration; the guard checks spellings it was given, folded.
700
+ const documentation = (folded) => templates.has(folded) || DOTENV_DOCUMENTATION.has(posix.basename(folded));
701
+ // An undeclared `.env` or `.env.*` file is where a product keeps the values its schema names, so
702
+ // no tool reads or writes one by default: the guard only asked for `cat .env` and allowed an edit.
703
+ const dotenvSecret = (folded) => { const name = posix.basename(folded); return (name === '.env' || name.startsWith('.env.')) && !documentation(folded); };
704
+ const dotenvDenial = deny('Refusing to read or write an environment file: `.env` and `.env.*` hold secrets unless named .env.example, .env.sample, .env.template or .env.schema, or declared as a template. '
705
+ + 'Fix: run what needs its values as `varlock run -- <cmd>`, which never prints them, or read the variable names from the .env.schema.');
706
+ const manifestPath = absolute(GUARD_CONTROL_FILES[1]);
707
+ const structural = [resolved.configPath, manifestPath];
708
+ const structuralDenial = deny(`Refusing to delete, rename, truncate, relink or make unreadable ${posix.basename(resolved.configPath)} or ${GUARD_CONTROL_FILES[1]}: every later guarded call reads them, so losing either holds the agent until a human restores it. Edit ${posix.basename(resolved.configPath)} in place instead.`);
709
+ const executionDenial = deny('Refusing to change the installed cockpit, a node_modules/.bin directory or a command search path directory: code there runs as the guard or in place of a permitted command.');
710
+ const linkDenial = deny('Refusing to link to a secrets directory, a declared environment instance, a guard control or the installed cockpit: the link would reach it under a name the guard does not protect.');
711
+ // The guard admits `npx --no cockpit` only from a root without an .npmrc, but checks that before
712
+ // the command runs: `printf … > .npmrc && npx --no cockpit …` planted one in between.
713
+ const npmrcDenial = deny('Refusing to write an .npmrc inside the configured roots: npm applies it to `npx --no cockpit`, and its script-shell or node-options would run other code as the cockpit launcher the guard trusts. A person edits .npmrc.');
714
+ // The guard authenticates only its entry point, so every module it loads is as much a control as
715
+ // its hook registration; an edited adapter once turned every hard denial into an allow.
716
+ const installedDirectories = resolved.executionDirectories.map(fold);
717
+ const installedCode = (folded, covering) => installedDirectories.some((directory) => within(folded, directory) || covering && within(directory, folded));
718
+ const binDirectories = resolved.allowedRoots.map((root) => fold(`${root}/node_modules/.bin`));
719
+ const searchDirectories = resolved.searchDirectories.map(fold);
720
+ // A file runs from a search path directory only as its direct child, which shadows a permitted command.
721
+ const executes = (folded, covering) => installedCode(folded, covering) || binDirectories.some((directory) => within(folded, directory))
722
+ || searchDirectories.some((directory) => folded === directory || posix.dirname(folded) === directory || covering && within(directory, folded));
723
+ const secretFiles = resolved.secretFileIdentities === null ? undefined : new Set(resolved.secretFileIdentities);
724
+ // A hard link to a secrets file shares its identity under a spelling no normalisation equates.
725
+ const secretFile = (evidence) => evidence.identity !== null && secretFiles?.has(evidence.identity) === true;
726
+ let authorityUnavailable;
727
+ let hardDeny;
728
+ const hold = (decision) => {
729
+ if (decision.decision === 'deny')
730
+ hardDeny = decision;
731
+ else if (decision.decision === 'unavailable' && !authorityUnavailable)
732
+ authorityUnavailable = decision;
733
+ };
734
+ const askDecisions = new Map();
735
+ const approvalBarriers = new Map();
736
+ const holdAsk = (invocation, decision) => {
737
+ if (!askDecisions.has(invocation))
738
+ askDecisions.set(invocation, decision);
739
+ };
740
+ // Identity facts for inspected evidence: the leaf itself, everything above it, and what it covers.
741
+ const evidenceFacts = (evidence) => {
742
+ const leaf = evidence.identity;
743
+ const chain = [...evidence.ancestors, ...(leaf === null ? [] : [leaf])];
744
+ return {
745
+ is: (path) => leaf !== null && identityOf(path) === leaf,
746
+ below: (path) => { const protectedIdentity = identityOf(path); return protectedIdentity !== null && chain.includes(protectedIdentity); },
747
+ covers: (path) => leaf !== null && chainOf(path).includes(leaf),
748
+ };
749
+ };
750
+ // A throwing inspector is missing evidence, which every caller treats as unsafe; the cause is
751
+ // kept so the refusal can name it.
752
+ let inspectionFailure;
753
+ const resolvedEvidence = (request) => {
754
+ let evidence;
755
+ inspectionFailure = undefined;
756
+ try {
757
+ evidence = snapshotJson(inspect(request));
758
+ }
759
+ catch (error) {
760
+ inspectionFailure = errorCause(error);
761
+ return undefined;
762
+ }
763
+ const lexical = posix.resolve(request.cwd, request.path);
764
+ const listed = request.operation === 'search' || request.operation === 'unignored-search' ? 'sensitive' : request.operation === 'repository' ? 'tracked' : undefined;
765
+ const paths = listed && evidence !== undefined && record(evidence) ? evidence[listed] : null;
766
+ return record(evidence) && keys(evidence, ['status', 'path', 'symlink', 'identity', 'ancestors', ...(listed ? [listed] : [])]) && evidence.status === 'resolved'
767
+ && evidence.path === lexical && evidence.symlink === false && (evidence.identity === null || identity(evidence.identity))
768
+ && Array.isArray(evidence.ancestors) && evidence.ancestors.every(identity)
769
+ && (paths === null || Array.isArray(paths) && paths.length <= GUARD_LOCATION_LIMITS.sensitivePaths
770
+ && paths.every((path) => canonical(path) && within(path, lexical))) ? evidence : undefined;
771
+ };
772
+ let pathCount = input.paths.length + input.invocations.reduce((sum, item) => sum + item.redirects.length, 0);
773
+ const inspectPath = (request, controlCoverage = false) => {
774
+ if (!scalar(request.path) || Buffer.byteLength(request.path) > GUARD_INPUT_LIMITS.pathBytes || !canonical(request.cwd)) {
775
+ hold(unavailable('Guard path or working directory is malformed.'));
776
+ return;
777
+ }
778
+ const lexical = posix.resolve(request.cwd, request.path);
779
+ const mutating = request.operation !== 'cwd';
780
+ const staging = request.operation === 'stage';
781
+ const removing = request.operation === 'remove';
782
+ // Spelling checks run before inspection so a hard denial survives missing evidence; they
783
+ // cover leaves that do not exist yet, which have no identity to compare, and folded
784
+ // spellings of a root that a case-insensitive volume resolves to it.
785
+ const folded = fold(lexical);
786
+ if (mutating && (folded.split('/').includes('.secrets') || (staging || removing) && within(fold(secretRoot), folded)))
787
+ hold(staging && !folded.split('/').includes('.secrets') ? coveringStageDenial : secretDenial);
788
+ // Held first, so a declared instance's own denial, held after it, is the one reported.
789
+ if (mutating && dotenvSecret(folded))
790
+ hold(dotenvDenial);
791
+ if (mutating && resolved.instances.some((item) => folded === fold(item.path) || (staging || removing) && within(fold(item.path), folded)))
792
+ hold(staging && !resolved.instances.some((item) => folded === fold(item.path)) ? coveringStageDenial : instanceDenial);
793
+ if (request.operation === 'write' && (controlFiles.some((path) => folded === fold(path) || controlCoverage && within(fold(path), folded))
794
+ || controlDirectories.some((path) => within(folded, fold(path)) || controlCoverage && within(fold(path), folded))))
795
+ hold(controlDenial);
796
+ if (request.operation === 'write' && executes(folded, controlCoverage))
797
+ hold(executionDenial);
798
+ if (request.operation === 'write' && posix.basename(folded) === '.npmrc')
799
+ hold(npmrcDenial);
800
+ if (mutating && folded.split('/').includes('.git'))
801
+ hold(gitMetadataDenial);
802
+ if (!resolved.allowedRoots.some((root) => within(lexical, root))) {
803
+ hold(unavailable('Guard path escapes configured roots. A path or working directory in this command is outside every root this cockpit manages, '
804
+ + 'which the guard does not judge: work inside the cockpit, or a person runs the command.'));
805
+ return;
806
+ }
807
+ const evidence = resolvedEvidence(request);
808
+ if (!evidence) {
809
+ hold(unavailable(`Guard path inspection failed, or its evidence is missing, symlinked or mismatched${inspectionFailure === undefined ? '' : ` (${inspectionFailure})`}.`));
810
+ return;
811
+ }
812
+ if (!mutating)
813
+ return evidence;
814
+ // Identity catches every spelling the volume resolves to a protected entry, including ones
815
+ // the shared normalisation does not equate, such as a hard link.
816
+ const { is, below, covers } = evidenceFacts(evidence);
817
+ if (below(secretRoot) || secretFile(evidence))
818
+ hold(secretDenial);
819
+ else if ((staging || removing) && covers(secretRoot))
820
+ hold(staging ? coveringStageDenial : secretDenial);
821
+ if (resolved.instances.some((item) => is(item.path)))
822
+ hold(instanceDenial);
823
+ else if ((staging || removing) && resolved.instances.some((item) => covers(item.path)))
824
+ hold(staging ? coveringStageDenial : instanceDenial);
825
+ if (request.operation === 'write' && (controlFiles.some(is) || controlDirectories.some(below)
826
+ || controlCoverage && (controlFiles.some(covers) || controlDirectories.some(covers))))
827
+ hold(controlDenial);
828
+ return evidence;
829
+ };
830
+ // Removing a control, anything below a control directory, or a directory containing one can
831
+ // disable the guard before its next invocation. Missing evidence never softens a spelling denial.
832
+ // The return value preserves a prompt for command removals beyond the inspector's authority.
833
+ const inspectRemoval = (path, cwd) => {
834
+ if (!scalar(path) || Buffer.byteLength(path) > GUARD_INPUT_LIMITS.pathBytes)
835
+ return false;
836
+ const lexical = posix.resolve(cwd, path);
837
+ if (structural.some((target) => within(fold(target), fold(lexical)))) {
838
+ hold(structuralDenial);
839
+ return false;
840
+ }
841
+ if (controlFiles.some((target) => within(fold(target), fold(lexical)))
842
+ || controlDirectories.some((target) => within(fold(target), fold(lexical)) || within(fold(lexical), fold(target)))) {
843
+ hold(controlDenial);
844
+ return false;
845
+ }
846
+ if (installedCode(fold(lexical), true)) {
847
+ hold(executionDenial);
848
+ return false;
849
+ }
850
+ if (!resolved.allowedRoots.some((root) => within(lexical, root)))
851
+ return true;
852
+ const evidence = inspectPath({ path, cwd, operation: 'remove' });
853
+ if (!evidence)
854
+ return false;
855
+ const { below, covers } = evidenceFacts(evidence);
856
+ if (structural.some(covers)) {
857
+ hold(structuralDenial);
858
+ return false;
859
+ }
860
+ if (controlFiles.some(covers) || controlDirectories.some((target) => covers(target) || below(target)))
861
+ hold(controlDenial);
862
+ return false;
863
+ };
864
+ const inspectPossibleControlOperand = (path, cwd) => {
865
+ if (!scalar(path) || Buffer.byteLength(path) > GUARD_INPUT_LIMITS.pathBytes)
866
+ return;
867
+ const lexical = posix.resolve(cwd, path);
868
+ if (!resolved.allowedRoots.some((root) => within(lexical, root)))
869
+ return;
870
+ const folded = fold(lexical);
871
+ if (controlFiles.some((target) => within(fold(target), folded))
872
+ || controlDirectories.some((target) => within(folded, fold(target)) || within(fold(target), folded))) {
873
+ hold(unavailable('Invocation names a guard control but its filesystem effect is not classified.'));
874
+ return;
875
+ }
876
+ const evidence = resolvedEvidence({ path, cwd, operation: 'write' });
877
+ if (!evidence)
878
+ return;
879
+ const { is, below, covers } = evidenceFacts(evidence);
880
+ if (controlFiles.some((target) => is(target) || covers(target))
881
+ || controlDirectories.some((target) => below(target) || covers(target))) {
882
+ hold(unavailable('Invocation names a guard control but its filesystem effect is not classified.'));
883
+ }
884
+ };
885
+ // A symbolic link's text resolves from the link's own directory and a hard link's from the
886
+ // working directory, so a source is checked from both.
887
+ const inspectLinkSource = (source, cwd, destinations) => {
888
+ if (!scalar(source) || Buffer.byteLength(source) > GUARD_INPUT_LIMITS.pathBytes) {
889
+ hold(unavailable('Guard path or working directory is malformed.'));
890
+ return;
891
+ }
892
+ const protectedTargets = [secretRoot, ...resolved.instances.map((item) => item.path), ...controlFiles, ...controlDirectories].map(fold);
893
+ const bases = new Set([cwd, ...destinations.flatMap((path) => { const target = posix.resolve(cwd, path); return [target, posix.dirname(target)]; })]);
894
+ for (const base of bases) {
895
+ const folded = fold(posix.resolve(base, source));
896
+ if (folded.split('/').includes('.secrets') || executes(folded, true)
897
+ || protectedTargets.some((target) => within(folded, target) || within(target, folded))) {
898
+ hold(linkDenial);
899
+ return;
900
+ }
901
+ }
902
+ if (!resolved.allowedRoots.some((root) => within(posix.resolve(cwd, source), root)))
903
+ return;
904
+ pathCount++;
905
+ const evidence = resolvedEvidence({ path: source, cwd, operation: 'read' });
906
+ if (!evidence)
907
+ return;
908
+ const { is, below, covers } = evidenceFacts(evidence);
909
+ if (secretFile(evidence) || [secretRoot, ...controlDirectories].some((path) => is(path) || below(path) || covers(path))
910
+ || [...resolved.instances.map((item) => item.path), ...controlFiles].some((path) => is(path) || covers(path)))
911
+ hold(linkDenial);
912
+ };
913
+ const searchedProtections = [secretRoot, ...resolved.instances.map((item) => item.path)];
914
+ // Why the last inspected search was not safe, when one protected path is the reason.
915
+ let searchBlocker;
916
+ // A read-only command is allowed only when every path it touches is evidenced inside the
917
+ // configured roots; reading a protected instance is a hard denial, anything doubtful is not safe.
918
+ // `chainReadOnly` says no other command in the call can create a protected path before the
919
+ // search runs, which is what lets an absent one be ignored.
920
+ const inspectReadOnly = (access, chainReadOnly = false) => {
921
+ let safe = true;
922
+ searchBlocker = undefined;
923
+ const visit = (path, kind) => {
924
+ if (!scalar(path) || Buffer.byteLength(path) > GUARD_INPUT_LIMITS.pathBytes) {
925
+ safe = false;
926
+ return;
927
+ }
928
+ const lexical = posix.resolve(access.cwd, path);
929
+ const folded = fold(lexical);
930
+ // A search rooted at or below a protected path reads it as surely as `cat` does; only a
931
+ // listing, which reports names, may name one.
932
+ const protectedRead = folded.split('/').includes('.secrets') || resolved.instances.some((item) => within(folded, fold(item.path)));
933
+ if (kind !== 'listing' && dotenvSecret(folded))
934
+ hold(dotenvDenial);
935
+ if (kind !== 'listing' && protectedRead)
936
+ hold(readDenial);
937
+ if (!resolved.allowedRoots.some((root) => within(lexical, root))) {
938
+ safe = false;
939
+ searchBlocker ??= ask(`${shownGuardWord(path)} is outside the configured roots, where the guard cannot prove it holds no secret. Read a copy inside the cockpit, or a person approves it.`);
940
+ return;
941
+ }
942
+ pathCount++;
943
+ // Another `.env*` name, such as `.envrc`, is not known to hold secrets, nor to be data.
944
+ if (kind === 'read' && /^\.env/.test(posix.basename(folded)) && !documentation(folded))
945
+ safe = false;
946
+ // Unenumerated secrets leave every content read unproven, and a second hard link to a
947
+ // protected file leaves every recursive search unproven: either may reach it unnamed.
948
+ if (kind !== 'listing' && !secretFiles || kind === 'tree' && resolved.protectedFilesLinked)
949
+ safe = false;
950
+ const evidence = resolvedEvidence({ path, cwd: access.cwd, operation: kind !== 'tree' ? 'read' : access.skipsIgnored ? 'unignored-search' : 'search' });
951
+ if (!evidence) {
952
+ safe = false;
953
+ return;
954
+ }
955
+ const { is, below, covers } = evidenceFacts(evidence);
956
+ if (kind !== 'listing' && (is(secretRoot) || below(secretRoot) || secretFile(evidence) || resolved.instances.some((item) => is(item.path))))
957
+ hold(readDenial);
958
+ if (kind !== 'tree')
959
+ return;
960
+ // The search cannot be told which files to skip: ripgrep's hidden-file filter yields to a
961
+ // whitelist in an agent-writable ignore file, and grep has none. So a tree that holds an
962
+ // existing protected path is not safe, and neither is one holding any `.env*` file or
963
+ // `.secrets` directory the inspector's walk found: `grep -r KEY .` once printed an
964
+ // undeclared sibling `.env.local` that `cat` would have stopped at. The walk sees only what
965
+ // exists now, so the search is safe only when nothing else in the call can write first, as
966
+ // `vercel env pull .env.local && rg KEY .` would.
967
+ const sensitive = evidence.sensitive;
968
+ // A search that skips what Git ignores reads a protected path only where it is listed.
969
+ const read = (target) => !access.skipsIgnored || sensitive === null
970
+ || sensitive.some((found) => within(fold(target), fold(found)) || within(fold(found), fold(target)));
971
+ const reached = searchedProtections.filter((target) => within(fold(target), folded) || covers(target));
972
+ const present = reached.find((target) => identityOf(target) !== null && read(target));
973
+ const unseen = sensitive?.find((found) => !documentation(fold(found)));
974
+ if (present === undefined && sensitive !== null && unseen === undefined && chainReadOnly)
975
+ return;
976
+ safe = false;
977
+ const shown = (target) => posix.relative(access.cwd, target) || '.';
978
+ searchBlocker = ask(present !== undefined
979
+ ? `A recursive search of ${path} would read ${shown(present)}, a declared environment instance or secrets directory. Search a path that does not contain it.`
980
+ : sensitive === null
981
+ ? `A recursive search of ${path} covers more than the guard walks to prove it reads no environment file or secrets directory. Search a narrower path.`
982
+ : unseen !== undefined
983
+ ? `A recursive search of ${path} would read ${shown(unseen)}, an environment file or secrets directory that is not a declared template. Search a path that does not contain it.`
984
+ : `A recursive search of ${path} could read an environment file or secrets directory that another command in this call creates first. Run the search on its own.`);
985
+ };
986
+ for (const path of access.requires ?? []) {
987
+ const evidence = resolvedEvidence({ path, cwd: access.cwd, operation: 'read' });
988
+ if (!evidence || evidence.identity === null)
989
+ safe = false;
990
+ }
991
+ // Diffs, patches and blobs print committed content, which the path inspection never sees. The
992
+ // index stands for the repository: a file once committed and since removed from it is not seen.
993
+ if (access.trackedContent) {
994
+ const tracked = resolvedEvidence({ path: '.', cwd: access.cwd, operation: 'repository' })?.tracked;
995
+ const limits = access.trackedContent.limitedTo?.map((spec) => fold(posix.resolve(access.cwd, spec)));
996
+ const exposed = tracked?.find((file) => !documentation(fold(file)) && (limits === undefined || limits.some((limit) => within(fold(file), limit))));
997
+ if (!tracked || exposed !== undefined || !chainReadOnly) {
998
+ safe = false;
999
+ searchBlocker = ask(!tracked ? 'The guard could not list the environment files and secrets this repository tracks, so it cannot prove this Git command prints none of them.'
1000
+ : exposed !== undefined ? `This Git command can print ${posix.relative(access.cwd, exposed)}, a tracked environment file, secrets file or declared instance. Limit it with \`-- <path>\` to paths that do not hold one.`
1001
+ : 'This Git command prints committed content, and another command in this call can change what the repository tracks first. Run it on its own.');
1002
+ }
1003
+ }
1004
+ for (const path of access.reads)
1005
+ visit(path, 'read');
1006
+ for (const path of access.trees)
1007
+ visit(path, 'tree');
1008
+ for (const path of access.listings)
1009
+ visit(path, 'listing');
1010
+ return safe;
1011
+ };
1012
+ const readOnly = new Set();
1013
+ const searchRefusals = new Map();
1014
+ // Launcher words name a package or the launcher, never an operand. Only a proven spelling runs
1015
+ // the installed cockpit, so only it is evaluated as cockpit; any other may run other code.
1016
+ const launcherPrefix = (invocation) => invocation.executable === null ? undefined : cockpitLauncherPrefix(invocation.executable, invocation.argv);
1017
+ const provenCockpit = (invocation) => {
1018
+ const prefix = launcherPrefix(invocation);
1019
+ return prefix !== undefined && resolved.cockpitLaunchers.some((item) => item.executable === invocation.executable && item.cwd === invocation.cwd
1020
+ && item.argv.length === prefix && item.argv.every((word, index) => word === invocation.argv[index])) ? invocation.argv.slice(prefix) : undefined;
1021
+ };
1022
+ const launchedCockpit = (invocation) => {
1023
+ const cockpit = provenCockpit(invocation);
1024
+ if (cockpit)
1025
+ return { cockpit, operands: cockpit };
1026
+ const prefix = launcherPrefix(invocation);
1027
+ if (prefix === undefined || !['npx', 'cockpit'].includes(invocation.executable))
1028
+ return { operands: invocation.argv };
1029
+ holdAsk(invocation, ask(UNPROVEN_LAUNCHER));
1030
+ return { operands: invocation.argv.slice(prefix) };
1031
+ };
1032
+ // A session runs the words after `--` as a command, with no shell, in its own directory, so
1033
+ // that command is judged as one more invocation: the session never admits more than it would.
1034
+ const invocations = input.invocations.flatMap((invocation) => {
1035
+ const cockpit = provenCockpit(invocation);
1036
+ const separator = cockpit?.[0] === 'session' ? cockpit.indexOf('--') : -1;
1037
+ const command = separator < 0 ? [] : cockpit.slice(separator + 1);
1038
+ if (!command.length)
1039
+ return [invocation];
1040
+ return [invocation, { executable: command[0], argv: command.slice(1), cwd: invocation.cwd, wrapped: false, redirects: [],
1041
+ words: command.map((value, index) => ({ role: index ? 'argument' : 'executable', value })) }];
1042
+ });
1043
+ const accesses = new Map(invocations.map((invocation) => {
1044
+ const cockpit = provenCockpit(invocation);
1045
+ return [invocation, classifyReadOnlyInvocation(invocation, { ripgrepConfigActive: resolved.ripgrepConfigActive, ...(cockpit ? { cockpit } : {}) })];
1046
+ }));
1047
+ // A definition or a `cd` runs nothing, so it cannot create a path before a search reads it; a bare redirect writes one.
1048
+ const chainReadOnly = [...accesses].every(([invocation, access]) => access !== undefined
1049
+ || invocation.executable === null && !invocation.redirects.some((redirect) => redirect.write));
1050
+ let possibleControlCount = 0;
1051
+ inspectPath({ path: input.cwd, cwd: input.cwd, operation: 'cwd' });
1052
+ for (const path of input.removed) {
1053
+ if (inspectRemoval(path, input.cwd))
1054
+ hold(unavailable('Removal path escapes configured roots.'));
1055
+ }
1056
+ for (const path of input.paths)
1057
+ inspectPath({ path, cwd: input.cwd, operation: 'write' });
1058
+ for (const invocation of invocations) {
1059
+ // Redirects, heredocs, comments and definitions are not executable host use.
1060
+ // A host passed to printf is still an argument and remains a hard denial.
1061
+ for (const word of invocation.words) {
1062
+ if (!['executable', 'argument', 'environment-assignment-value'].includes(word.role))
1063
+ continue;
1064
+ if (resolved.hostedDatabaseHosts.some((host) => hostUsed(word.value, host)))
1065
+ hold(deny('Refusing executable use of a declared hosted database host.'));
1066
+ if (resolved.vendorHosts.some((host) => hostUsed(word.value, host)))
1067
+ hold(deny('Refusing executable use of a declared vendor host.'));
1068
+ }
1069
+ inspectPath({ path: invocation.cwd, cwd: invocation.cwd, operation: 'cwd' });
1070
+ for (const redirect of invocation.redirects) {
1071
+ // A control file's own denial names the stronger boundary, so it is held last.
1072
+ if (redirect.truncate)
1073
+ inspectRemoval(redirect.target, invocation.cwd);
1074
+ if (redirect.write)
1075
+ inspectPath({ path: redirect.target, cwd: invocation.cwd, operation: 'write' });
1076
+ }
1077
+ const git = gitInvocation(invocation);
1078
+ const removed = removedOperands(invocation, git);
1079
+ for (const path of removed?.paths ?? []) {
1080
+ if (inspectRemoval(path, removed.cwd) && !approvalBarriers.has(invocation)) {
1081
+ approvalBarriers.set(invocation, ask('Removal outside configured roots requires human approval.'));
1082
+ }
1083
+ }
1084
+ const { operands } = launchedCockpit(invocation);
1085
+ const access = accesses.get(invocation);
1086
+ const writes = git?.verb === 'mv' ? (() => {
1087
+ const transfer = transferOperands('mv', git.args);
1088
+ return { paths: transfer.writes, reads: [], removes: false, uncertain: transfer.uncertain };
1089
+ })() : git?.verb === 'archive' ? gitArchiveWrites(git.args) : visibleWrites(invocation);
1090
+ if (writes) {
1091
+ const writeCwd = git && ['archive', 'mv'].includes(git.verb) ? git.cwd : invocation.cwd;
1092
+ for (const path of writes.paths) {
1093
+ pathCount++;
1094
+ inspectPath({ path, cwd: writeCwd, operation: 'write' }, true);
1095
+ if (writes.removes)
1096
+ inspectRemoval(path, writeCwd);
1097
+ }
1098
+ for (const source of writes.links ?? [])
1099
+ inspectLinkSource(source, writeCwd, writes.paths);
1100
+ if (writes.reads?.length && !inspectReadOnly({ cwd: invocation.cwd, reads: writes.reads, trees: [], listings: [] })) {
1101
+ hold(unavailable('Visible writer source is unavailable or unsafe to read.'));
1102
+ }
1103
+ if (writes.uncertain)
1104
+ hold(unavailable('Unsupported visible writer option.'));
1105
+ }
1106
+ else if (!access && git?.verb !== 'add') {
1107
+ // Staging writes the index, never the named file, and its operands are inspected as staged below.
1108
+ for (const arg of git?.args ?? operands) {
1109
+ if (++possibleControlCount <= GUARD_INPUT_LIMITS.paths) {
1110
+ inspectPossibleControlOperand(arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg, git?.cwd ?? invocation.cwd);
1111
+ }
1112
+ }
1113
+ if (possibleControlCount > GUARD_INPUT_LIMITS.paths)
1114
+ hold(unavailable('Potential guard control operand limit exceeded.'));
1115
+ }
1116
+ if (access && inspectReadOnly(access, chainReadOnly))
1117
+ readOnly.add(invocation);
1118
+ else if (searchBlocker)
1119
+ searchRefusals.set(invocation, searchBlocker);
1120
+ // A cockpit command that parses a named file can quote it in a refusal, so the file is read
1121
+ // into the agent context as surely as `cat` would read it.
1122
+ for (const profile of resolved.cockpitInvocations) {
1123
+ if (!profile.reads?.length || profile.executable !== invocation.executable || profile.cwd !== invocation.cwd
1124
+ || profile.argv.length !== invocation.argv.length || profile.argv.some((word, i) => word !== invocation.argv[i]))
1125
+ continue;
1126
+ if (!inspectReadOnly({ cwd: profile.cwd, reads: profile.reads, trees: [], listings: [] })) {
1127
+ holdAsk(invocation, ask('This cockpit command reads a file the guard cannot prove safe to read: a symlink, an undeclared .env file or one outside the configured roots.'));
1128
+ }
1129
+ }
1130
+ if (!git)
1131
+ continue;
1132
+ // Git changes directory as the system does, resolving `..` from where a link leads, so each
1133
+ // directory is inspected as spelled rather than by the path `cwd` names.
1134
+ for (const directory of git.directories)
1135
+ inspectPath({ ...directory, operation: 'cwd' });
1136
+ if (git.uncertain)
1137
+ holdAsk(invocation, ask('Git execution context is not an exact resolved profile.'));
1138
+ if (git.verb === 'push') {
1139
+ const decision = pushDecision(git.args, resolved.protectedBranches);
1140
+ if (decision?.decision === 'deny')
1141
+ hold(decision);
1142
+ else if (decision)
1143
+ holdAsk(invocation, decision);
1144
+ }
1145
+ if (git.verb === 'config' && !gitConfigReads(git.args))
1146
+ hold(gitMetadataDenial);
1147
+ if (git.verb === 'reset' && git.args.includes('--hard') || git.verb === 'clean' && git.args.some((arg) => /^-[^-]*[fdxX]/.test(arg) || arg === '--force'))
1148
+ holdAsk(invocation, ask('Destructive Git operation requires human approval.'));
1149
+ if (git.verb === 'add') {
1150
+ let options = true;
1151
+ let operands = 0;
1152
+ for (const arg of git.args) {
1153
+ if (options && arg === '--') {
1154
+ options = false;
1155
+ continue;
1156
+ }
1157
+ if (options && arg.startsWith('-')) {
1158
+ if (['-A', '--all', '-u', '--update'].includes(arg)) {
1159
+ // These forms can stage an instance even without a literal operand.
1160
+ inspectPath({ path: '.', cwd: git.cwd, operation: 'stage' });
1161
+ }
1162
+ else if (!['-f', '--force', '-v', '--verbose', '-n', '--dry-run'].includes(arg))
1163
+ hold(unavailable('Unsupported Git staging option. Stage with `git add <paths>`, `-A` or `-u`; a person runs any other staging form.'));
1164
+ }
1165
+ else {
1166
+ operands++;
1167
+ pathCount++;
1168
+ if (arg.startsWith(':') || /[*?\[]/.test(arg))
1169
+ hold(unavailable('Unproven Git staging pathspec grammar.'));
1170
+ else
1171
+ inspectPath({ path: arg, cwd: git.cwd, operation: 'stage' });
1172
+ }
1173
+ }
1174
+ if (!operands)
1175
+ holdAsk(invocation, ask('Staging without explicit paths requires human approval.'));
1176
+ }
1177
+ }
1178
+ if (pathCount > GUARD_INPUT_LIMITS.paths)
1179
+ hold(unavailable('Guard path limit exceeded.'));
1180
+ if (hardDeny)
1181
+ return hardDeny;
1182
+ for (const invocation of invocations) {
1183
+ const declared = resolved.declaredDenies.find(rule => ruleMatches(invocation, rule));
1184
+ if (declared)
1185
+ return deny(`${declared.reason}${declared.fix ? ` Fix: ${declared.fix}` : ''}`);
1186
+ }
1187
+ if (authorityUnavailable)
1188
+ return authorityUnavailable;
1189
+ const declaredReasons = [];
1190
+ for (const invocation of invocations) {
1191
+ const barrier = approvalBarriers.get(invocation);
1192
+ if (barrier)
1193
+ return barrier;
1194
+ // A relative entry resolves against whatever directory the command runs in, so no write
1195
+ // protection covers it: a file the agent wrote as packages/web/git runs as `git` there, and a
1196
+ // path-spelled launcher's `#!/usr/bin/env node` finds a planted `node` the same way.
1197
+ if (resolved.searchPathRelativeEntry !== null && invocation.executable !== null) {
1198
+ return ask(`The command search path the agent runs with holds the relative entry ${JSON.stringify(resolved.searchPathRelativeEntry)}${resolved.searchPathRelativeEntry === '' ? ' (an empty entry is the working directory)' : ''}, so ${invocation.executable}, or a program it starts by name, may run a file the agent wrote instead of the permitted program. Remove relative entries from the PATH the agent harness starts with.`);
1199
+ }
1200
+ // Only a proven read-only command may carry `$?`: as any other argument it could name a path the guard never saw.
1201
+ if (invocation.expandsStatus && !readOnly.has(invocation))
1202
+ return ask(`${named(invocation)} has an argument holding $?, whose digits the guard cannot see, so it cannot prove which path the argument names. Write the value out, or print it with echo on its own.`);
1203
+ const declared = !invocation.wrapped && !invocation.words.some((word) => word.role === 'environment-assignment-value')
1204
+ ? resolved.declaredAllows.find(rule => ruleMatches(invocation, rule)) : undefined;
1205
+ if (declared) {
1206
+ declaredReasons.push(declared.reason);
1207
+ continue;
1208
+ }
1209
+ const pending = askDecisions.get(invocation);
1210
+ if (pending)
1211
+ return pending;
1212
+ if (invocation.wrapped || invocation.words.some((word) => word.role === 'environment-assignment-value'))
1213
+ return ask('Wrapped or environment-modifying invocation requires human approval.');
1214
+ if (invocation.executable === null)
1215
+ continue;
1216
+ const matches = (profile, cwd) => profile.executable === invocation.executable
1217
+ && cwd === invocation.cwd && profile.argv.length === invocation.argv.length && profile.argv.every((word, i) => word === invocation.argv[i]);
1218
+ if (!readOnly.has(invocation) && !resolved.cockpitInvocations.some((profile) => matches(profile, profile.cwd))
1219
+ && !resolved.projectCommands.some((profile) => matches(profile, profile.checkoutRoot))) {
1220
+ const config = /^[\w./@%+=:,-]+$/.test(resolved.configPath) ? resolved.configPath : `'${resolved.configPath.replaceAll("'", "'\\''")}'`;
1221
+ return searchRefusals.get(invocation) ?? ask(`${named(invocation)} requires human approval: the guard admits only the read-only commands it classifies, a declared build or test command run from its checkout, and [guards] allow rules. `
1222
+ + `A person approves it or runs it in a terminal. To run tests, use \`npx --no cockpit test-run --config ${config} --gate <gate>\`; a person can admit a command for good under [guards] allow in ${posix.basename(resolved.configPath)}.`);
1223
+ }
1224
+ }
1225
+ return declaredReasons.length === 1 ? { decision: 'allow', reason: declaredReasons[0] } : allowed;
1226
+ }