@b00dea/cockpit 0.0.0-bootstrap.0 → 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 +21 -1
  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 +76 -1
  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,461 @@
1
+ // Owns the machine facts behind the guard protections: the directories holding the code the
2
+ // running guard loads, the command search path directories inside the allowed roots, the identity
3
+ // of every file in the root secrets directory, the environment and secrets names below a searched
4
+ // tree or tracked by a repository, and which launcher spellings start the installed cockpit. It reads
5
+ // the filesystem and asks Git, and decides nothing; the guard application filters these facts into the
6
+ // policy and the evaluator turns them into hard denials.
7
+ import { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
8
+ import { readBoundedFile } from '../bounded-read.js';
9
+ import { basename, dirname, join, posix, relative } from 'node:path';
10
+ import { ENVIRONMENT_CODES, snapshotExecutionStep, RefusalError } from '../environment/contract.js';
11
+ import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
12
+ import { foldGuardSpelling, GUARD_LOCATION_LIMITS, GUARD_SECRET_FILE_LIMIT, guardIdentity } from './evaluator.js';
13
+ function missing(error) { return error.code === 'ENOENT'; }
14
+ /** The directory Node loads a bare specifier from: the nearest `node_modules/<name>` above `from`. */
15
+ function installedPackage(from, name) {
16
+ for (let directory = from;; directory = dirname(directory)) {
17
+ const candidate = join(directory, 'node_modules', name);
18
+ try {
19
+ if (lstatSync(candidate))
20
+ return candidate;
21
+ }
22
+ catch (error) {
23
+ if (!missing(error))
24
+ throw error;
25
+ }
26
+ if (dirname(directory) === directory)
27
+ throw new RefusalError(`guard dependency ${name} is not installed above ${from}`);
28
+ }
29
+ }
30
+ /**
31
+ * The running cockpit package and every package it loads, each under the spelling it was found by
32
+ * and its real path, plus the consumer's `node_modules` spelling of the package when that is a link
33
+ * to it. The guard authenticates only its entry point, so all of these are controls.
34
+ */
35
+ export function guardInstalledDirectories(packageRoot, consumerRoot) {
36
+ const found = new Set();
37
+ const visited = new Set();
38
+ const pending = [packageRoot];
39
+ const { name } = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
40
+ const linked = join(consumerRoot, 'node_modules', name);
41
+ try {
42
+ if (realpathSync(linked) === realpathSync(packageRoot))
43
+ pending.push(linked);
44
+ }
45
+ catch (error) {
46
+ if (!missing(error))
47
+ throw error;
48
+ }
49
+ while (pending.length) {
50
+ const spelled = pending.pop();
51
+ const real = realpathSync(spelled);
52
+ found.add(spelled).add(real);
53
+ if (visited.has(real))
54
+ continue;
55
+ visited.add(real);
56
+ if (found.size > GUARD_LOCATION_LIMITS.packageDirectories)
57
+ throw new RefusalError(`guard package closure exceeds ${GUARD_LOCATION_LIMITS.packageDirectories} directories`);
58
+ const manifest = JSON.parse(readFileSync(join(real, 'package.json'), 'utf8'));
59
+ for (const name of Object.keys(manifest.dependencies ?? {}))
60
+ pending.push(installedPackage(real, name));
61
+ }
62
+ return [...found].sort();
63
+ }
64
+ /**
65
+ * The file whose identity proves that `executable` with the launcher `words` after it, run in `cwd`,
66
+ * starts `launcher`, the installed cockpit the guard runs from, or undefined when it may start
67
+ * something else. A path, or the script `node` is given, qualifies when it resolves to the launcher;
68
+ * the bare name when the first `cockpit` on the command search path does. `npx` qualifies only in
69
+ * `root`, where npm runs a `bin` the root package.json declares before any installed one and applies
70
+ * the root `.npmrc`, whose `script-shell` and `node-options` run other code first; with neither
71
+ * present it runs the root's `node_modules/.bin/cockpit`, which must resolve to the launcher.
72
+ */
73
+ export function guardLauncherEvidence(executable, words, cwd, root, launcher, searchPath) {
74
+ const resolving = (path) => {
75
+ try {
76
+ return realpathSync(path) === launcher ? path : undefined;
77
+ }
78
+ catch (error) {
79
+ if (missing(error) || error.code === 'ENOTDIR')
80
+ return undefined;
81
+ throw error;
82
+ }
83
+ };
84
+ if (executable.includes('/'))
85
+ return resolving(posix.resolve(cwd, executable));
86
+ if (executable === 'node')
87
+ return resolving(posix.resolve(cwd, words[0]));
88
+ if (executable === 'cockpit') {
89
+ // The shell runs the first executable file the search path names; an empty entry is `cwd`.
90
+ for (const entry of searchPath.split(':')) {
91
+ const candidate = posix.resolve(cwd, entry, 'cockpit');
92
+ let status;
93
+ try {
94
+ status = statSync(candidate);
95
+ }
96
+ catch (error) {
97
+ if (missing(error) || error.code === 'ENOTDIR')
98
+ continue;
99
+ throw error;
100
+ }
101
+ if (status.isFile() && status.mode & 0o111)
102
+ return resolving(candidate);
103
+ }
104
+ return undefined;
105
+ }
106
+ if (executable !== 'npx' || cwd !== root)
107
+ return undefined;
108
+ try {
109
+ lstatSync(join(root, '.npmrc'));
110
+ return undefined;
111
+ }
112
+ catch (error) {
113
+ if (!missing(error))
114
+ throw error;
115
+ }
116
+ let manifest = {};
117
+ try {
118
+ manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
119
+ }
120
+ catch (error) {
121
+ if (error instanceof SyntaxError)
122
+ return undefined;
123
+ if (!missing(error))
124
+ throw error;
125
+ }
126
+ // npm reads a bin from `bin`, or from every file in `directories.bin`. Its other `directories`
127
+ // entries name documentation, and `npm init` writes `directories.doc` beside the docs/ sync creates.
128
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
129
+ if (!record(manifest) || 'bin' in manifest || 'directories' in manifest && (!record(manifest.directories) || 'bin' in manifest.directories))
130
+ return undefined;
131
+ return resolving(join(root, 'node_modules/.bin/cockpit'));
132
+ }
133
+ /**
134
+ * Absolute search path entries, and their real paths, that lie inside an allowed root. A relative
135
+ * entry resolves against whatever directory a command runs in, so it names no fixed location.
136
+ */
137
+ export function guardSearchDirectories(searchPath, allowedRoots) {
138
+ const inside = (path) => allowedRoots.some((root) => path === root || path.startsWith(`${root}/`));
139
+ const found = new Set();
140
+ for (const entry of searchPath.split(':')) {
141
+ if (!entry.startsWith('/'))
142
+ continue;
143
+ const spelled = posix.normalize(entry).replace(/(?<=.)\/+$/, '');
144
+ const candidates = [spelled];
145
+ try {
146
+ candidates.push(realpathSync(spelled));
147
+ }
148
+ catch (error) {
149
+ if (!missing(error))
150
+ throw error;
151
+ }
152
+ for (const candidate of candidates)
153
+ if (inside(candidate))
154
+ found.add(candidate);
155
+ }
156
+ if (found.size > GUARD_LOCATION_LIMITS.searchPathDirectories)
157
+ throw new RefusalError(`guard search path exceeds ${GUARD_LOCATION_LIMITS.searchPathDirectories} directories inside the allowed roots`);
158
+ return [...found].sort();
159
+ }
160
+ /** The first entry of `searchPath` that is not absolute, an empty one included, or null when there is none. */
161
+ export function guardSearchPathRelativeEntry(searchPath) {
162
+ return searchPath.split(':').find((entry) => !entry.startsWith('/')) ?? null;
163
+ }
164
+ /** Directory entries a recursive search may walk before the guard stops proving what it reads. */
165
+ const GUARD_SEARCH_ENTRY_LIMIT = 20_000;
166
+ const sensitiveName = (name) => { const folded = foldGuardSpelling(name); return folded.startsWith('.env') || folded === '.secrets'; };
167
+ /**
168
+ * Every path at or below `path` whose case-folded name starts with `.env` or is `.secrets`, of any
169
+ * type, without following links, as a recursive search would reach it; null once the walk passes
170
+ * `entryLimit` entries or finds more than the sensitive path bound. A missing path has none.
171
+ */
172
+ export function guardSensitivePathsBelow(path, entryLimit = GUARD_SEARCH_ENTRY_LIMIT) {
173
+ let status;
174
+ try {
175
+ status = lstatSync(path);
176
+ }
177
+ catch (error) {
178
+ if (missing(error))
179
+ return [];
180
+ throw error;
181
+ }
182
+ const found = sensitiveName(basename(path)) ? [path] : [];
183
+ const pending = status.isDirectory() ? [path] : [];
184
+ let seen = 0;
185
+ while (pending.length) {
186
+ const directory = pending.pop();
187
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
188
+ if (++seen > entryLimit)
189
+ return null;
190
+ const child = join(directory, entry.name);
191
+ if (sensitiveName(entry.name) && found.push(child) > GUARD_LOCATION_LIMITS.sensitivePaths)
192
+ return null;
193
+ if (entry.isDirectory())
194
+ pending.push(child);
195
+ }
196
+ }
197
+ return found.sort();
198
+ }
199
+ // A repository's index and work tree can each list hundreds of thousands of paths.
200
+ const GIT_LIST_BYTES = 16_777_216;
201
+ // Ignored entries Git lists below a searched tree, each ignored directory once. Each costs at most a
202
+ // check of its not-yet-seen ancestors for a repository boundary, the work of one step of the walk,
203
+ // so the walk's budget bounds it.
204
+ const GUARD_IGNORED_ENTRY_LIMIT = GUARD_SEARCH_ENTRY_LIMIT;
205
+ /**
206
+ * The NUL-separated paths `git <args>` prints in `repository`, with an empty answer for exit 1, or
207
+ * null when Git did not answer. The configuration overrides keep Git from running the fsmonitor
208
+ * program or trusting its untracked cache, and match ignore rules case-sensitively, as ripgrep does.
209
+ */
210
+ async function gitPaths(git, repository, args, execute) {
211
+ const step = snapshotExecutionStep({
212
+ id: 'guard-git-paths',
213
+ argv: [git.canonicalPath, '-C', repository, `--work-tree=${repository}`, '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false', '-c', 'core.ignoreCase=false', ...args],
214
+ argv0: git.invocationPath, cwd: repository, env: {}, timeoutMs: 10_000,
215
+ stdoutLimitBytes: GIT_LIST_BYTES, stderrLimitBytes: 65_536, paths: [repository], ports: [], labels: { action: 'guard-git-paths' },
216
+ expected: [{ kind: 'path', target: repository, state: 'repository' }],
217
+ consumedFiles: [{ path: git.canonicalPath, sha256: git.sha256, maxBytes: 536_870_912, argvIndexes: [0], executable: true, executionMode: git.executionMode }],
218
+ filesystemChecks: [], compensation: [],
219
+ });
220
+ if (!step)
221
+ throw new RefusalError(`guard Git query could not be planned in ${repository}`);
222
+ const outcome = authenticatedExecutionOutcome(await execute(step), step);
223
+ if (outcome?.kind === 'complete')
224
+ return outcome.stdout.toString('utf8').split('\0').filter(Boolean);
225
+ return outcome?.kind === 'refusal' && outcome.code === ENVIRONMENT_CODES.EXECUTION_STEP_FAILED
226
+ && outcome.execution.exitCode === 1 && outcome.execution.signal === null ? [] : null;
227
+ }
228
+ // A `.gitignore` is a short list of patterns; one past this is not read.
229
+ const IGNORE_FILE_BYTES = 1_048_576;
230
+ /**
231
+ * Whether Git and ripgrep may disagree on what an ignore line matches. They compile globs with
232
+ * different engines: ripgrep reads `{a,b}` as alternatives where Git matches the braces literally,
233
+ * does not know POSIX classes such as `[[:alpha:]]`, and in older releases refuses a `**` that is not
234
+ * a whole path component, skipping the line. Git trims only trailing spaces where ripgrep trims any
235
+ * trailing whitespace. Each lets Git ignore a file ripgrep reads, so the walk decides instead of
236
+ * Git's list. A backslash escape counts too: no check here proves both engines read every escape alike.
237
+ */
238
+ function divergentIgnoreLine(line) {
239
+ if (line.startsWith('#'))
240
+ return false;
241
+ const pattern = line.replace(/^!/, '');
242
+ return /[{}\\]|\[[:=.]|\s$/u.test(pattern) || pattern.split('/').some(component => component.includes('**') && component !== '**');
243
+ }
244
+ function present(path) {
245
+ try {
246
+ lstatSync(path);
247
+ return true;
248
+ }
249
+ catch (error) {
250
+ if (missing(error))
251
+ return false;
252
+ throw error;
253
+ }
254
+ }
255
+ /** The nearest directory at or above `directory` holding a `.git` entry, or undefined. */
256
+ function repositoryAbove(directory) {
257
+ for (let cursor = directory;; cursor = dirname(cursor)) {
258
+ if (present(join(cursor, '.git')))
259
+ return cursor;
260
+ if (dirname(cursor) === cursor)
261
+ return undefined;
262
+ }
263
+ }
264
+ /**
265
+ * The paths a default ripgrep search of `path` reads that are named `.env*`, lie below a `.secrets`
266
+ * directory or are one of `watched`, each reported at its first such name, as Git lists what the
267
+ * enclosing repository does not ignore; null past the sensitive path bound. Undefined when Git
268
+ * did not answer or its list cannot stand for ripgrep's: outside a repository, through a
269
+ * nested repository or submodule, below an `.ignore` or `.rgignore` file, which ripgrep reads and Git
270
+ * does not, or when the search names an ignored directory, whose children ripgrep may still read.
271
+ *
272
+ * Only `.gitignore` files are applied: ripgrep reads those, the repository's exclude file and the
273
+ * global excludes file, but finds the global one by other rules than Git, so Git applying fewer
274
+ * lists more. Hidden paths are kept: a type such as `rg -t sh`, which matches `.env`, reads them.
275
+ * Undefined as well when a `.gitignore` Git reads holds a line the two may match differently.
276
+ */
277
+ export async function guardUnignoredSensitivePaths(path, watched, git, execute) {
278
+ // ripgrep reads a path it is named whatever ignores it, and one such name already decides.
279
+ if (sensitiveName(basename(path)))
280
+ return [path];
281
+ const status = lstatSync(path);
282
+ // A case-insensitive volume finds `SRC` where Git's pathspec and ignore rules see only `src`.
283
+ if (realpathSync.native(path) !== path)
284
+ return undefined;
285
+ const start = status.isDirectory() ? path : dirname(path);
286
+ for (let cursor = start;; cursor = dirname(cursor)) {
287
+ if (present(join(cursor, '.ignore')) || present(join(cursor, '.rgignore')))
288
+ return undefined;
289
+ if (dirname(cursor) === cursor)
290
+ break;
291
+ }
292
+ const repository = repositoryAbove(start);
293
+ if (repository === undefined)
294
+ return undefined;
295
+ const below = relative(repository, path);
296
+ // check-ignore takes no pathspec magic, so a name it would read as magic or a wildcard is walked.
297
+ if (/^:|[*?[\\]/.test(below))
298
+ return undefined;
299
+ const scope = below === '' ? [] : [`:(literal)${below}`];
300
+ // `--directory` names an ignored directory once instead of reading everything below it, which
301
+ // ripgrep skips as well; listing every ignored entry, not only protected names, keeps each
302
+ // directory Git collapsed in view whatever its name.
303
+ const [named, cached, others, ignored] = await Promise.all([
304
+ below === '' ? [] : gitPaths(git, repository, ['check-ignore', '--no-index', '--', below], execute),
305
+ gitPaths(git, repository, ['ls-files', '-z', '--stage', '--', ...scope], execute),
306
+ gitPaths(git, repository, ['ls-files', '-z', '--others', '--exclude-per-directory=.gitignore', '--', ...scope], execute),
307
+ gitPaths(git, repository, ['ls-files', '-z', '--others', '--ignored', '--directory', '--exclude-per-directory=.gitignore', '--', ...scope], execute),
308
+ ]);
309
+ if (named?.length !== 0 || cached === null || others === null || ignored === null || ignored.length > GUARD_IGNORED_ENTRY_LIMIT)
310
+ return undefined;
311
+ const folded = new Set(watched.map(foldGuardSpelling));
312
+ const boundaries = new Map();
313
+ // ripgrep stops applying the ignore files above a directory holding `.git` or `.jj`, which Git
314
+ // does not, so what Git ignores below such a directory may still be read.
315
+ const belowBoundary = (directory) => {
316
+ if (directory === repository)
317
+ return false;
318
+ let known = boundaries.get(directory);
319
+ if (known === undefined) {
320
+ known = ['.git', '.jj'].some(name => present(join(directory, name))) || belowBoundary(dirname(directory));
321
+ boundaries.set(directory, known);
322
+ }
323
+ return known;
324
+ };
325
+ for (const entry of ignored) {
326
+ const names = entry.split('/').filter(Boolean);
327
+ if (names.some(name => ['.ignore', '.rgignore'].includes(foldGuardSpelling(name))))
328
+ return undefined;
329
+ const absolute = join(repository, ...names);
330
+ if (absolute !== path && !absolute.startsWith(`${path}/`))
331
+ return undefined;
332
+ // A collapsed directory may hold anything; a file matters only under a protected name.
333
+ const protectedEntry = entry.endsWith('/') || names.some(sensitiveName) || folded.has(foldGuardSpelling(absolute));
334
+ if (protectedEntry && belowBoundary(dirname(absolute)))
335
+ return undefined;
336
+ }
337
+ const listed = [];
338
+ for (const line of cached) {
339
+ const tab = line.indexOf('\t');
340
+ // A submodule's work tree is ripgrep's to search and absent from this index.
341
+ if (tab < 0 || line.startsWith('160000 '))
342
+ return undefined;
343
+ listed.push(line.slice(tab + 1));
344
+ }
345
+ for (const entry of others) {
346
+ // Git names an untracked nested repository as a directory and lists nothing inside it.
347
+ if (entry.endsWith('/'))
348
+ return undefined;
349
+ listed.push(entry);
350
+ }
351
+ // Git reads the `.gitignore` in each directory it enters: those from the repository down to the
352
+ // search, and those it lists below it under any status. One that is not a plain file, such as a
353
+ // link Git declines to follow and ripgrep follows, is not proven to say the same to both.
354
+ const ignoreFiles = new Set();
355
+ for (let cursor = start;; cursor = dirname(cursor)) {
356
+ ignoreFiles.add(join(cursor, '.gitignore'));
357
+ if (cursor === repository)
358
+ break;
359
+ }
360
+ for (const entry of [...listed, ...ignored])
361
+ if (foldGuardSpelling(basename(entry)) === '.gitignore')
362
+ ignoreFiles.add(join(repository, entry));
363
+ for (const file of ignoreFiles) {
364
+ // A symbolic link or non-regular file is not proven to say the same to Git and ripgrep, and an oversized one is not read.
365
+ let read;
366
+ try {
367
+ read = readBoundedFile(file, IGNORE_FILE_BYTES);
368
+ }
369
+ catch (error) {
370
+ if (missing(error))
371
+ continue;
372
+ if (['ELOOP', 'EISDIR', 'ENXIO'].includes(error.code ?? ''))
373
+ return undefined;
374
+ throw error;
375
+ }
376
+ if (read.kind === 'too-large')
377
+ return undefined;
378
+ // ripgrep stops reading an ignore file at its first line that is not UTF-8; Git reads on.
379
+ let text;
380
+ try {
381
+ text = new TextDecoder('utf-8', { fatal: true }).decode(read.bytes);
382
+ }
383
+ catch (error) {
384
+ if (error instanceof TypeError && error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA')
385
+ return undefined;
386
+ throw error;
387
+ }
388
+ if (text.split('\n').some(divergentIgnoreLine))
389
+ return undefined;
390
+ }
391
+ const found = new Set();
392
+ for (const entry of listed) {
393
+ if (entry.split('/').some(name => ['.ignore', '.rgignore'].includes(foldGuardSpelling(name))))
394
+ return undefined;
395
+ const absolute = join(repository, entry);
396
+ // Every query is scoped to `path`, so an entry outside it is a spelling Git and the volume disagree on.
397
+ if (absolute !== path && !absolute.startsWith(`${path}/`))
398
+ return undefined;
399
+ const names = absolute === path ? [] : relative(path, absolute).split('/');
400
+ const first = names.findIndex(sensitiveName);
401
+ if (first >= 0)
402
+ found.add(join(path, ...names.slice(0, first + 1)));
403
+ else if (folded.has(foldGuardSpelling(absolute)))
404
+ found.add(absolute);
405
+ if (found.size > GUARD_LOCATION_LIMITS.sensitivePaths)
406
+ return null;
407
+ }
408
+ return [...found].sort();
409
+ }
410
+ /**
411
+ * Every path `repository`'s index holds that is named `.env*`, lies below a `.secrets` or `.env*`
412
+ * directory or is one of `watched`, or null past the sensitive path bound or when Git did not
413
+ * answer. Only the index is asked: a file removed from it stays in history, which this does not walk.
414
+ */
415
+ export async function guardTrackedSensitivePaths(repository, watched, git, execute) {
416
+ const inside = watched.map(path => relative(repository, path)).filter(path => path !== '' && !path.startsWith('..'));
417
+ const tracked = await gitPaths(git, repository, ['ls-files', '-z', '--', ':(icase,glob)**/.env*', ':(icase,glob)**/.env*/**', ':(icase,glob)**/.secrets/**',
418
+ ...inside.map(path => `:(icase,literal)${path}`)], execute);
419
+ return tracked === null || tracked.length > GUARD_LOCATION_LIMITS.sensitivePaths ? null : tracked.map(path => join(repository, path)).sort();
420
+ }
421
+ /**
422
+ * Identities of every file in each case-folded spelling of `env/.secrets` below `root`, and whether
423
+ * any has a second hard link; `identities` is null once more than the bound of entries was seen,
424
+ * which leaves every read unproven rather than silently uncovered.
425
+ */
426
+ export function guardSecretFiles(root) {
427
+ const directories = (parent, name) => {
428
+ let entries;
429
+ try {
430
+ entries = readdirSync(parent);
431
+ }
432
+ catch (error) {
433
+ if (missing(error))
434
+ return [];
435
+ throw error;
436
+ }
437
+ return entries.filter((entry) => foldGuardSpelling(entry) === name).map((entry) => join(parent, entry))
438
+ .filter((path) => { const status = lstatSync(path); return status.isDirectory() && !status.isSymbolicLink(); });
439
+ };
440
+ const pending = directories(root, 'env').flatMap((env) => directories(env, '.secrets'));
441
+ const identities = [];
442
+ let linked = false;
443
+ let seen = 0;
444
+ while (pending.length) {
445
+ const directory = pending.pop();
446
+ for (const entry of readdirSync(directory)) {
447
+ if (++seen > GUARD_SECRET_FILE_LIMIT)
448
+ return { identities: null, linked: true };
449
+ const path = join(directory, entry);
450
+ const status = lstatSync(path);
451
+ if (status.isDirectory())
452
+ pending.push(path);
453
+ else if (status.isFile()) {
454
+ identities.push(guardIdentity(status));
455
+ if (status.nlink > 1)
456
+ linked = true;
457
+ }
458
+ }
459
+ }
460
+ return { identities: identities.sort(), linked };
461
+ }
@@ -0,0 +1,37 @@
1
+ import type { ParsedInvocation } from './invocation-parser.js';
2
+ /** Paths are as written; `cwd` is where they resolve, after any `git -C`. */
3
+ export interface ReadOnlyAccess {
4
+ readonly cwd: string;
5
+ /** Files whose content the command prints or counts. */
6
+ readonly reads: readonly string[];
7
+ /** Directories whose files' content the command searches recursively. */
8
+ readonly trees: readonly string[];
9
+ /** Paths whose names or metadata the command reports, never their content. */
10
+ readonly listings: readonly string[];
11
+ /** Paths that must exist, relative to `cwd`, for the command to be read-only. */
12
+ readonly requires?: readonly string[];
13
+ /** The `trees` are searched as ripgrep searches by default: what Git ignores below them is not read. */
14
+ readonly skipsIgnored?: boolean;
15
+ /**
16
+ * The command prints the content of files in the repository at `cwd`, its index or history: only
17
+ * below these paths, relative to `cwd`, or anywhere when null.
18
+ */
19
+ readonly trackedContent?: {
20
+ readonly limitedTo: readonly string[] | null;
21
+ };
22
+ }
23
+ /**
24
+ * How many leading words of `argv` belong to the launcher when `executable` may start cockpit, or
25
+ * undefined when it cannot: the bare `cockpit` name, a path, `node <path>`, and `npx --no [--]
26
+ * cockpit`. Every spelling is a candidate only; the application proves which one starts the guard's
27
+ * own installed cockpit, so all of them are judged by that one identity.
28
+ */
29
+ export declare function cockpitLauncherPrefix(executable: string, argv: readonly string[]): number | undefined;
30
+ /**
31
+ * The paths an exact read-only invocation touches, or undefined when it is not one. `cockpit` holds
32
+ * the cockpit arguments only when the application proved the invocation starts the installed cockpit.
33
+ */
34
+ export declare function classifyReadOnlyInvocation(invocation: ParsedInvocation, environment?: {
35
+ readonly ripgrepConfigActive: boolean;
36
+ readonly cockpit?: readonly string[];
37
+ }): ReadOnlyAccess | undefined;