@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,556 @@
1
+ import { sha256Hex } from '../sha256.js';
2
+ import { lstatSync, realpathSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { z } from 'zod';
6
+ import { errorCause, filesystemObjectIdentity, snapshotJson, RefusalError } from '../environment/contract.js';
7
+ import { authenticatedExecutionOutcome, executeProbe } from '../environment-exec/execute.js';
8
+ import { libexecProgram } from '../environment-exec/libexec-program.js';
9
+ import { abortStateTransaction, authenticateStateNamespace, issueStateScopedLock, readState, transactState, withStateScopedLock } from '../environment/state.js';
10
+ import { GeneratedHostPath } from '../schema.js';
11
+ import { GUARD_SURFACE_TARGETS } from '../surfaces/harness-registry.js';
12
+ import { collectMcpDeclarations, mcpApprovalDigest } from '../surfaces/mcp.js';
13
+ import { isPackageDescendant } from './package-paths.js';
14
+ import { assertBrowserStorageStateCurrent, browserRuntimeControlAuthorities, browserRuntimeControlPorts, browserRuntimeCoordination } from './runtime.js';
15
+ import { readBoundedFile } from '../bounded-read.js';
16
+ const issuedClients = new WeakMap();
17
+ const issuedOperationAuthorizations = new WeakMap();
18
+ // Historical measurements seed spelling only; every current invocation still needs its paired control.
19
+ const MEASURED = {
20
+ claudecode: { version: '2.1.220', date: '2026-08-25', tool: 'mcp__playwright__browser_navigate', cli: true },
21
+ codexcli: { version: '0.149.0', date: '2026-08-25', tool: 'mcp__playwright__browser_navigate', cli: false },
22
+ opencode: { version: '1.18.15', date: '2026-08-25', tool: 'playwright_browser_navigate', cli: true },
23
+ };
24
+ /** The harness version each target's browser transport spelling was measured at, for diagnostics. */
25
+ export function measuredBrowserHarnessVersions() {
26
+ return Object.fromEntries(Object.entries(MEASURED).map(([target, row]) => [target, row.version]));
27
+ }
28
+ const Sha256 = z.string().regex(/^[a-f0-9]{64}$/);
29
+ const Version = z.string().regex(/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/).max(128);
30
+ const Asset = z.object({ bytes: z.string().max(65_536), sha256: Sha256 }).strict();
31
+ const NativePackage = z.object({
32
+ directory: z.literal('node_modules/@playwright/test'),
33
+ manifest: Asset,
34
+ cli: Asset.extend({ path: GeneratedHostPath }).strict(),
35
+ }).strict();
36
+ const ProjectPackage = NativePackage.extend({
37
+ artifactRoot: GeneratedHostPath,
38
+ module: Asset.extend({ path: GeneratedHostPath }).strict(),
39
+ }).strict();
40
+ const Package = z.union([ProjectPackage, NativePackage]);
41
+ const PackageObservation = z.object({
42
+ cockpitDirectory: z.string().max(4096).refine(path => isAbsolute(path) && path !== '/' && resolve(path) === path && !/[\\\x00-\x1f\x7f]/.test(path)),
43
+ artifactRoot: GeneratedHostPath,
44
+ }).strict();
45
+ const ProjectOperation = z.object({
46
+ kind: z.enum(['drive', 'screenshot']),
47
+ surface: z.object({ name: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/), url: z.string().max(4_096) }).strict(),
48
+ deadline: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
49
+ role: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).optional(),
50
+ output: z.string().max(4_096).optional(),
51
+ }).strict();
52
+ const Input = z.object({
53
+ runtime: z.unknown(),
54
+ cockpitDirectory: z.string().max(4096).refine(path => isAbsolute(path) && path !== '/' && resolve(path) === path && !/[\\\x00-\x1f\x7f]/.test(path)),
55
+ harnesses: z.array(z.object({ target: z.enum(GUARD_SURFACE_TARGETS), version: Version }).strict()).min(1).max(GUARD_SURFACE_TARGETS.length)
56
+ .refine(rows => new Set(rows.map(row => row.target)).size === rows.length),
57
+ mcp: z.unknown(), approved: z.boolean(), testPackage: Package.nullable(), skillTemplate: z.string().max(16_384),
58
+ }).strict();
59
+ const Manifest = z.object({ name: z.literal('@playwright/test'), version: Version, bin: z.object({ playwright: GeneratedHostPath }) });
60
+ const MAX_SCREENSHOT_BYTES = 8_388_608;
61
+ const COMMAND_TIMEOUT_MS = 30_000;
62
+ // A filesystem error names the file itself; only the checks here need their own refusal.
63
+ function observedFile(path, maxBytes) {
64
+ if (realpathSync(path) !== path || lstatSync(path).nlink !== 1)
65
+ throw new RefusalError(`browser file ${path} is not a canonical single-link path`);
66
+ const read = readBoundedFile(path, maxBytes);
67
+ if (read.kind !== 'complete')
68
+ throw new RefusalError(`browser file ${path} exceeds its ${maxBytes}-byte read bound`);
69
+ return read.bytes;
70
+ }
71
+ function observedBytes(path, maxBytes) {
72
+ return new TextDecoder('utf-8', { fatal: true }).decode(observedFile(path, maxBytes));
73
+ }
74
+ function projectPackagePaths(root, runtime) {
75
+ const declaredManifest = runtime.project.manifest;
76
+ const projectManifestPath = resolve(root, declaredManifest ?? 'package.json');
77
+ if (!isPackageDescendant(root, projectManifestPath))
78
+ throw new RefusalError('browser project manifest escaped its cockpit root');
79
+ if (declaredManifest !== undefined) {
80
+ const manifestBytes = observedBytes(projectManifestPath, 1_048_576);
81
+ if (sha256Hex(manifestBytes) !== runtime.project.manifestSha256) {
82
+ throw new RefusalError('browser declared project manifest differs from its issued identity');
83
+ }
84
+ }
85
+ return { projectManifestPath, packageRoot: resolve(dirname(projectManifestPath), 'node_modules/@playwright/test') };
86
+ }
87
+ export function observeBrowserProjectPackage(input) {
88
+ const captured = snapshotJson({ cockpitDirectory: input.cockpitDirectory, artifactRoot: input.artifactRoot });
89
+ const parsed = PackageObservation.safeParse(captured);
90
+ if (!parsed.success)
91
+ throw new RefusalError('browser project package observation requires a canonical root and registered relative artifact root');
92
+ const root = parsed.data.cockpitDirectory;
93
+ if (realpathSync(root) !== root)
94
+ throw new RefusalError('browser project package root must be canonical');
95
+ const directory = 'node_modules/@playwright/test';
96
+ const { projectManifestPath, packageRoot } = projectPackagePaths(root, input.runtime);
97
+ const manifestBytes = observedBytes(resolve(packageRoot, 'package.json'), 65_536);
98
+ let manifest;
99
+ try {
100
+ manifest = JSON.parse(manifestBytes);
101
+ }
102
+ catch {
103
+ throw new RefusalError('browser test package manifest is invalid JSON');
104
+ }
105
+ const declared = Manifest.safeParse(manifest);
106
+ if (!declared.success)
107
+ throw new RefusalError('browser test package manifest is invalid');
108
+ const cliAbsolute = resolve(packageRoot, declared.data.bin.playwright);
109
+ if (!isPackageDescendant(packageRoot, cliAbsolute))
110
+ throw new RefusalError('browser test package CLI escaped its package root');
111
+ let moduleAbsolute;
112
+ try {
113
+ moduleAbsolute = realpathSync(createRequire(projectManifestPath).resolve('@playwright/test'));
114
+ }
115
+ catch {
116
+ throw new RefusalError('browser project module entry could not be resolved');
117
+ }
118
+ if (!isPackageDescendant(packageRoot, moduleAbsolute))
119
+ throw new RefusalError('browser project module escaped its package root');
120
+ const cliBytes = observedBytes(cliAbsolute, 65_536);
121
+ const moduleBytes = observedBytes(moduleAbsolute, 65_536);
122
+ return { directory, artifactRoot: parsed.data.artifactRoot,
123
+ manifest: { bytes: manifestBytes, sha256: sha256Hex(manifestBytes) },
124
+ cli: { path: relative(packageRoot, cliAbsolute), bytes: cliBytes, sha256: sha256Hex(cliBytes) },
125
+ module: { path: relative(packageRoot, moduleAbsolute), bytes: moduleBytes, sha256: sha256Hex(moduleBytes) } };
126
+ }
127
+ function file(target, path, bytes) {
128
+ return { target, path, bytes, sha256: sha256Hex(bytes) };
129
+ }
130
+ function pinnedCli(value, runtime, root) {
131
+ if (value === null)
132
+ return null;
133
+ let manifest;
134
+ try {
135
+ manifest = JSON.parse(value.manifest.bytes);
136
+ }
137
+ catch {
138
+ throw new RefusalError('browser test package manifest is invalid JSON');
139
+ }
140
+ const parsed = Manifest.safeParse(manifest);
141
+ if (!parsed.success || parsed.data.version !== runtime.project.driverVersion
142
+ || value.cli.path !== parsed.data.bin.playwright || value.manifest.sha256 !== sha256Hex(value.manifest.bytes)
143
+ || value.cli.sha256 !== sha256Hex(value.cli.bytes))
144
+ throw new RefusalError('browser test package does not match the pinned project driver and CLI bytes');
145
+ const { packageRoot } = projectPackagePaths(root, runtime);
146
+ if (observedBytes(resolve(packageRoot, 'package.json'), 65_536) !== value.manifest.bytes
147
+ || observedBytes(resolve(packageRoot, value.cli.path), 65_536) !== value.cli.bytes)
148
+ throw new RefusalError('browser test package differs from installed bytes');
149
+ const lockBytes = observedBytes(resolve(root, runtime.project.lockfile), 1_048_576);
150
+ let lock;
151
+ try {
152
+ lock = JSON.parse(lockBytes);
153
+ }
154
+ catch {
155
+ throw new RefusalError('browser test package needs an authenticated npm lockfile');
156
+ }
157
+ const locked = z.object({ packages: z.record(z.string(), z.unknown()) }).safeParse(lock);
158
+ const entry = locked.success ? z.object({ version: Version }).safeParse(locked.data.packages[value.directory]) : null;
159
+ if (sha256Hex(lockBytes) !== runtime.project.sha256 || !entry?.success || entry.data.version !== parsed.data.version) {
160
+ throw new RefusalError('browser test package differs from the issued project lock identity');
161
+ }
162
+ return { package: '@playwright/test', version: parsed.data.version,
163
+ argv: ['node', resolve(packageRoot, value.cli.path)], sha256: value.cli.sha256 };
164
+ }
165
+ function projectClient(value, row, runtime, root) {
166
+ if (value.artifactRoot !== runtime.cliArtifactRoot)
167
+ throw new RefusalError('browser project client artifact root must be the registered CLI artifact root');
168
+ const { projectManifestPath, packageRoot } = projectPackagePaths(root, runtime);
169
+ let resolvedModule;
170
+ try {
171
+ resolvedModule = realpathSync(createRequire(projectManifestPath).resolve('@playwright/test'));
172
+ }
173
+ catch {
174
+ throw new RefusalError('browser project module entry could not be resolved');
175
+ }
176
+ const declaredModule = resolve(packageRoot, value.module.path);
177
+ if (resolvedModule !== declaredModule || !isPackageDescendant(packageRoot, resolvedModule)
178
+ || value.module.sha256 !== sha256Hex(value.module.bytes)
179
+ || observedBytes(resolvedModule, 1_048_576) !== value.module.bytes) {
180
+ throw new RefusalError('browser project module entry differs from the installed resolved entry');
181
+ }
182
+ const manifestPath = resolve(packageRoot, 'package.json');
183
+ const lockPath = resolve(root, runtime.project.lockfile);
184
+ const nodePath = realpathSync(process.execPath);
185
+ const nodeBytes = observedFile(nodePath, 536_870_912);
186
+ const client = {
187
+ package: '@playwright/test', target: row.target, version: row.version, cwd: root,
188
+ artifactRoot: resolve(root, value.artifactRoot),
189
+ node: { path: nodePath, sha256: sha256Hex(nodeBytes), maxBytes: nodeBytes.byteLength },
190
+ module: { path: resolvedModule, sha256: value.module.sha256, maxBytes: 1_048_576 },
191
+ manifest: { path: manifestPath, sha256: value.manifest.sha256, maxBytes: 65_536 },
192
+ lock: { path: lockPath, sha256: runtime.project.sha256, maxBytes: 1_048_576 },
193
+ };
194
+ issuedClients.set(client, runtime);
195
+ return client;
196
+ }
197
+ export function authenticateBrowserProjectClient(value, runtime) {
198
+ return value !== null && typeof value === 'object' && issuedClients.get(value) === runtime
199
+ ? value : undefined;
200
+ }
201
+ // Returns undefined for a path that is not a confined directory; a filesystem error is thrown for the caller to name.
202
+ function artifactDirectoryIdentity(path) {
203
+ const directory = lstatSync(path, { bigint: true });
204
+ const parentPath = dirname(path);
205
+ const parent = lstatSync(parentPath, { bigint: true });
206
+ if (!directory.isDirectory() || directory.isSymbolicLink() || !parent.isDirectory() || parent.isSymbolicLink()
207
+ || realpathSync(path) !== path || realpathSync(parentPath) !== parentPath)
208
+ return undefined;
209
+ return { ...filesystemObjectIdentity(directory),
210
+ ancestors: [{ path: parentPath, ...filesystemObjectIdentity(parent) }] };
211
+ }
212
+ function operationStatePath(runtime, client, role) {
213
+ if (runtime.authentication.posture !== 'real')
214
+ return role === undefined ? undefined : null;
215
+ if (role === undefined || runtime.authentication.policy === 'none')
216
+ return null;
217
+ const state = runtime.authentication.states.find(candidate => candidate.worker === role);
218
+ if (!state)
219
+ return null;
220
+ const path = resolve(client.cwd, state.path);
221
+ return path.startsWith(`${client.cwd}${sep}`) ? path : null;
222
+ }
223
+ function operationStep(client, runtime, operation, remaining, origin, statePath, stateBytes, artifactIdentity) {
224
+ const controlAuthorities = browserRuntimeControlAuthorities(runtime);
225
+ const controlPorts = browserRuntimeControlPorts(runtime);
226
+ if (!controlAuthorities || !controlPorts)
227
+ throw new RefusalError('browser operation requires issued control authorities');
228
+ const program = libexecProgram('browser-operation.cjs');
229
+ // '--' ends Node's own options, so the program path can never be read as one.
230
+ const argv = [client.node.path, '--', program.path, '--screenshot-limit', String(MAX_SCREENSHOT_BYTES),
231
+ '--module-proof', client.module.path, '--module', client.module.path,
232
+ '--manifest-proof', client.manifest.path, '--lock-proof', client.lock.path, '--operation', operation.kind,
233
+ '--engine', runtime.server.engine, '--connection', runtime.connection.kind === 'remote' ? 'remote' : 'local',
234
+ ...(runtime.connection.kind === 'remote' ? ['--endpoint', runtime.connection.endpoint, '--expose-network', runtime.networkExposure] : []),
235
+ ...(runtime.connection.kind === 'local-fallback' && runtime.addressing.resolverRules
236
+ ? ['--resolver-rule', runtime.addressing.resolverRules.join(',')] : []),
237
+ '--control-authorities', JSON.stringify(controlAuthorities),
238
+ '--control-ports', JSON.stringify(controlPorts),
239
+ '--url', operation.surface.url, '--origin', origin, '--timeout', String(remaining),
240
+ ...(statePath ? ['--state-proof', statePath] : []),
241
+ ...(operation.output && artifactIdentity ? ['--output', operation.output, '--output-name', basename(operation.output),
242
+ '--artifact-root', client.artifactRoot, '--artifact-dev', String(artifactIdentity.dev), '--artifact-ino', String(artifactIdentity.ino),
243
+ '--artifact-birthtime-ns', artifactIdentity.birthtimeNs] : [])];
244
+ const index = (value) => argv.indexOf(value);
245
+ const env = statePath ? { COCKPIT_BROWSER_STORAGE_STATE: statePath } : {};
246
+ return {
247
+ id: `browser-${operation.kind}-${operation.surface.name}`, argv, argv0: client.node.path,
248
+ cwd: client.cwd, env,
249
+ timeoutMs: remaining, spawnNotAfter: new Date(operation.deadline).toISOString(), stdoutLimitBytes: 16_384, stderrLimitBytes: 16_384,
250
+ paths: operation.output ? [operation.output] : [], ports: [],
251
+ labels: { browser: operation.kind }, expected: [],
252
+ consumedFiles: [
253
+ { path: client.node.path, sha256: client.node.sha256, maxBytes: client.node.maxBytes, argvIndexes: [0], executable: true, executionMode: 'verified-path' },
254
+ { path: program.path, sha256: program.sha256, maxBytes: program.maxBytes, argvIndexes: [2], executable: false },
255
+ { path: client.module.path, sha256: client.module.sha256, maxBytes: client.module.maxBytes, argvIndexes: [index(client.module.path)], executable: false },
256
+ { path: client.manifest.path, sha256: client.manifest.sha256, maxBytes: client.manifest.maxBytes, argvIndexes: [index(client.manifest.path)], executable: false },
257
+ { path: client.lock.path, sha256: client.lock.sha256, maxBytes: client.lock.maxBytes, argvIndexes: [index(client.lock.path)], executable: false },
258
+ ...(statePath && stateBytes ? [{ path: statePath, sha256: sha256Hex(stateBytes), maxBytes: 1_048_576,
259
+ argvIndexes: [index(statePath)], executable: false }] : []),
260
+ ],
261
+ filesystemChecks: [{ path: client.artifactRoot, expected: 'directory',
262
+ ...(artifactIdentity ? { identity: artifactIdentity } : {}) }], compensation: [],
263
+ };
264
+ }
265
+ function notAdmitted(what, error) {
266
+ return { kind: 'unadmitted', cause: `${what}: ${errorCause(error)}` };
267
+ }
268
+ export async function executeBrowserProjectOperation(client, runtime, input, authorization) {
269
+ if (issuedClients.get(client) !== runtime)
270
+ return undefined;
271
+ const admitted = authorization && issuedOperationAuthorizations.get(authorization);
272
+ if (!admitted || admitted.client !== client || admitted.runtime !== runtime || admitted.active)
273
+ return undefined;
274
+ const captured = snapshotJson(input);
275
+ const parsed = ProjectOperation.safeParse(captured);
276
+ if (!parsed.success)
277
+ return undefined;
278
+ const operation = parsed.data;
279
+ const remaining = operation.deadline - Date.now();
280
+ if (remaining <= 0 || remaining > COMMAND_TIMEOUT_MS)
281
+ return undefined;
282
+ let url;
283
+ // The surface address was already validated by the schema; the URL error would quote it, so the
284
+ // caller's fixed not-admitted message stands.
285
+ try {
286
+ url = new URL(operation.surface.url);
287
+ }
288
+ catch {
289
+ return undefined;
290
+ }
291
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.origin !== admitted.origin
292
+ || operation.role !== admitted.role)
293
+ return undefined;
294
+ const statePath = admitted.statePath;
295
+ const screenshot = operation.kind === 'screenshot';
296
+ if (screenshot !== (operation.output !== undefined))
297
+ return undefined;
298
+ let artifactIdentity;
299
+ if (screenshot) {
300
+ try {
301
+ artifactIdentity = artifactDirectoryIdentity(client.artifactRoot);
302
+ }
303
+ catch (error) {
304
+ return notAdmitted('the screenshot artifact root could not be inspected', error);
305
+ }
306
+ }
307
+ if (screenshot && (!artifactIdentity || dirname(operation.output) !== client.artifactRoot
308
+ || !/^screenshot-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.png$/.test(basename(operation.output))))
309
+ return undefined;
310
+ // The revalidation throws only its own fixed-text TypeError, whose class says nothing the caller's
311
+ // refusal does not, so the admission is simply withheld.
312
+ try {
313
+ assertBrowserStorageStateCurrent(runtime);
314
+ }
315
+ catch {
316
+ return undefined;
317
+ }
318
+ let stateBytes;
319
+ try {
320
+ stateBytes = statePath ? observedFile(statePath, 1_048_576) : undefined;
321
+ }
322
+ catch (error) {
323
+ return notAdmitted('the seeded storage state could not be read', error);
324
+ }
325
+ const step = operationStep(client, runtime, operation, remaining, admitted.origin, statePath, stateBytes, artifactIdentity);
326
+ try {
327
+ assertBrowserStorageStateCurrent(runtime);
328
+ }
329
+ catch {
330
+ return undefined;
331
+ } // fixed-text TypeError, as above
332
+ admitted.active = true;
333
+ const pending = executeProbe(step).then(outcome => authenticatedExecutionOutcome(outcome, step));
334
+ admitted.pending.add(pending);
335
+ try {
336
+ return await pending;
337
+ }
338
+ finally {
339
+ admitted.pending.delete(pending);
340
+ admitted.active = false;
341
+ }
342
+ }
343
+ function currentCoordination(record, stackId, ownerId) {
344
+ if (!record || record.kind !== 'coordination' || record.subjectId !== stackId || !record.value || typeof record.value !== 'object')
345
+ return false;
346
+ const holders = record.value.holders;
347
+ return Array.isArray(holders) && holders.some(holder => holder && typeof holder === 'object' && holder.holderId === ownerId);
348
+ }
349
+ export async function withBrowserProjectOperationAdmission(client, runtime, input, operation) {
350
+ if (issuedClients.get(client) !== runtime || typeof operation !== 'function') {
351
+ return { kind: 'refusal', reason: 'Browser operation admission requires an issued project client and closed authority' };
352
+ }
353
+ const captured = input;
354
+ const state = input.state;
355
+ let origin;
356
+ // The URL error quotes the caller's origin; the refusal below names the expected shape instead.
357
+ try {
358
+ origin = new URL(captured.origin);
359
+ }
360
+ catch {
361
+ return { kind: 'refusal', reason: 'Browser operation admission requires an issued HTTP application origin' };
362
+ }
363
+ if (!['http:', 'https:'].includes(origin.protocol) || origin.username || origin.password || origin.pathname !== '/'
364
+ || origin.search || origin.hash || origin.origin !== captured.origin) {
365
+ return { kind: 'refusal', reason: 'Browser operation admission requires an issued HTTP application origin' };
366
+ }
367
+ const controlAuthorities = browserRuntimeControlAuthorities(runtime);
368
+ const controlPorts = browserRuntimeControlPorts(runtime);
369
+ const originPort = Number(origin.port || (origin.protocol === 'http:' ? '80' : '443'));
370
+ if (!controlAuthorities || !controlPorts || controlAuthorities.includes(origin.host) || controlPorts.includes(originPort)) {
371
+ return { kind: 'refusal', reason: 'Browser operation admission refuses the browser-control proxy authority' };
372
+ }
373
+ const auth = runtime.authentication;
374
+ const role = typeof captured.role === 'string' ? captured.role : undefined;
375
+ const statePath = operationStatePath(runtime, client, role);
376
+ if (statePath === null || captured.role !== undefined && role === undefined
377
+ || auth.posture !== 'real' && (role !== undefined || state !== undefined)
378
+ || auth.posture === 'real' && (role === undefined || auth.policy === 'none')
379
+ || auth.posture === 'real' && state === undefined) {
380
+ return { kind: 'refusal', reason: 'Browser operation admission requires current declared authentication authority' };
381
+ }
382
+ const invoke = async () => {
383
+ // Only the runtime's fixed-text TypeError escapes the revalidation; the refusal names the condition.
384
+ try {
385
+ assertBrowserStorageStateCurrent(runtime);
386
+ }
387
+ catch {
388
+ return { kind: 'refusal', reason: 'Browser seeded state changed before operation dispatch' };
389
+ }
390
+ const authorization = { kind: 'browser-project-operation-authorization' };
391
+ const authorizationState = { client, runtime, origin: origin.origin, ...(role ? { role } : {}),
392
+ ...(statePath ? { statePath } : {}), active: false, pending: new Set() };
393
+ issuedOperationAuthorizations.set(authorization, authorizationState);
394
+ try {
395
+ const value = await operation(authorization);
396
+ return { kind: 'admitted', value };
397
+ }
398
+ finally {
399
+ issuedOperationAuthorizations.delete(authorization);
400
+ await Promise.allSettled([...authorizationState.pending]);
401
+ }
402
+ };
403
+ if (auth.posture !== 'real')
404
+ return invoke();
405
+ const binding = browserRuntimeCoordination(runtime);
406
+ const namespace = authenticateStateNamespace(state?.namespace);
407
+ if (!binding || !namespace || !state || namespace !== binding.namespace || state.stackId !== binding.stackId) {
408
+ return { kind: 'refusal', reason: 'Browser authentication refuses coordination authority from a different runtime stack' };
409
+ }
410
+ const address = binding.namespace.paths.coordination(binding.stackId);
411
+ if (auth.policy === 'per-worker') {
412
+ const scope = issueStateScopedLock(namespace, { owner: 'browser-worker', identity: sha256Hex(`${binding.stackId}\0${role}`) });
413
+ if (!scope)
414
+ return { kind: 'refusal', reason: 'Per-worker browser authentication could not issue its canonical worker claim' };
415
+ const locked = await withStateScopedLock(namespace, scope, { ownerId: state.ownerId, contentionTimeoutMs: 0 }, async () => {
416
+ const before = await readState(address);
417
+ if (before.kind !== 'complete' || !currentCoordination(before.value, binding.stackId, state.ownerId)) {
418
+ return { kind: 'refusal', reason: 'Per-worker browser authentication requires the current coordination record and owner' };
419
+ }
420
+ const admitted = await invoke();
421
+ const after = await readState(address);
422
+ return after.kind === 'complete' && after.value.revision === before.value.revision
423
+ && currentCoordination(after.value, binding.stackId, state.ownerId)
424
+ ? admitted
425
+ : { kind: 'refusal', reason: 'Per-worker browser coordination changed during the operation' };
426
+ });
427
+ const detail = locked.kind === 'refusal' ? `: ${locked.message} [${locked.target}]${locked.recovery ? `; ${locked.recovery}` : ''}` : '';
428
+ return locked.kind === 'complete' ? locked.value : { kind: 'refusal', reason: (locked.kind === 'refusal' && locked.code.endsWith('LOCKED')
429
+ ? 'Per-worker browser authentication refused because its canonical worker claim is contended'
430
+ : 'Per-worker browser authentication could not authenticate its canonical worker claim') + detail };
431
+ }
432
+ const locked = await transactState(namespace, address, { ownerId: state.ownerId, contentionTimeoutMs: 0 }, async (current) => abortStateTransaction(currentCoordination(current, binding.stackId, state.ownerId)
433
+ ? await invoke()
434
+ : { kind: 'refusal', reason: 'Serialized browser authentication requires the current coordination record and owner' }));
435
+ if ('aborted' in locked)
436
+ return locked.value;
437
+ const detail = locked.kind === 'refusal' ? `: ${locked.message} [${locked.target}]${locked.recovery ? `; ${locked.recovery}` : ''}` : '';
438
+ return { kind: 'refusal', reason: (locked.kind === 'refusal' && locked.code.endsWith('LOCKED')
439
+ ? 'Serialized browser authentication refused because the canonical state lock is contended'
440
+ : 'Serialized browser authentication could not authenticate the canonical coordination state') + detail };
441
+ }
442
+ function nativeMcp(target, command, args) {
443
+ if (target === 'claudecode')
444
+ return file(target, '.mcp.json', JSON.stringify({ mcpServers: { playwright: { command, args } } }, null, 2) + '\n');
445
+ if (target === 'opencode')
446
+ return file(target, 'opencode.jsonc', JSON.stringify({ mcp: { playwright: { type: 'local', command: [command, ...args], enabled: true } } }, null, 2) + '\n');
447
+ return file(target, '.codex/config.toml', `[mcp_servers.playwright]\ncommand = ${JSON.stringify(command)}\nargs = ${JSON.stringify(args)}\nenabled = true\ndefault_tools_approval_mode = "approve"\n`);
448
+ }
449
+ function renderSkill(template, target, rows, runtime) {
450
+ if (template.split('{{browser}}').length !== 2 || /{{|}}/.test(template.replace('{{browser}}', ''))
451
+ || !template.startsWith('---\nname: cockpit-browser\n') || /^(?:allowed-tools|tools|disallowedTools):/m.test(template)) {
452
+ throw new RefusalError('browser skill template must contain one browser marker and retain browser tools');
453
+ }
454
+ const auth = runtime.authentication.posture !== 'real'
455
+ ? `Sign-in and persona selection are inapplicable: ${runtime.authentication.posture === 'none' ? 'the product has no sign-in' : 'authentication is bypassed'}.`
456
+ : `Use mode-600 stored state from seeded users with transcript-safe sign-in; never print credentials or state contents. The active state policy is ${runtime.authentication.policy}.`;
457
+ const addressing = runtime.addressing.posture === 'port' ? 'Browser resolver rules: inapplicable for port addressing.'
458
+ : runtime.connection.kind === 'remote'
459
+ ? 'The remote browser resolves application names on the host, so it opens the same URLs a person does.'
460
+ : `Apply browser resolver rules: ${runtime.addressing.resolverRules.join('; ')}.`;
461
+ const transports = rows.map(row => `${row.transport}: ${row.declaration}; callable: ${row.callable}; harness: ${target} ${row.version}; historical measurement: ${row.measuredVersion} on ${row.measuredOn}. ${row.reason}${row.tool ? ` Tool: ${row.tool}.` : ''}${row.cli ? ` Pinned CLI argv: ${JSON.stringify(row.cli.argv)}; package: ${row.cli.package} ${row.cli.version}.` : ''}`);
462
+ const guidance = [
463
+ '# Drive the application', ...transports, auth, addressing,
464
+ 'Use the declared shared headless browser connection.',
465
+ 'Prefer the eligible pinned CLI for identity-sensitive or concurrent work; keep the declared per-worker or serialized state policy.',
466
+ 'Retain browser access for the independent grader. File read-only controls do not restrain product state; product-state restraint remains instructed.',
467
+ 'Read the browser reference before sign-in, artifact cleanup, or a paired native probe. Current callable rows remain held for operator measurement.',
468
+ ].join('\n\n');
469
+ const prefix = target === 'claudecode' ? '.claude' : target === 'codexcli' ? '.agents' : '.opencode';
470
+ return file(target, `${prefix}/skills/cockpit-browser/SKILL.md`, template.replace('{{browser}}', () => guidance));
471
+ }
472
+ export function renderBrowserSurfaces(input) {
473
+ const captured = snapshotJson(input);
474
+ if (captured === undefined)
475
+ throw new RefusalError('browser surfaces require bounded plain data without accessors');
476
+ const parsed = Input.safeParse(captured);
477
+ if (!parsed.success)
478
+ throw new RefusalError('browser surfaces require canonical harness, test package and template data');
479
+ const data = parsed.data;
480
+ const runtime = input.runtime;
481
+ assertBrowserStorageStateCurrent(runtime);
482
+ const cli = pinnedCli(data.testPackage, runtime, data.cockpitDirectory);
483
+ // `approved` says the caller matched the declared server against [mcp.approved]; this one may carry a bound endpoint.
484
+ const approval = { playwright: mcpApprovalDigest(data.mcp, data.cockpitDirectory) };
485
+ const collected = collectMcpDeclarations({ cockpitDirectory: data.cockpitDirectory, declarations: [data.mcp],
486
+ approved: data.approved ? approval : {}, targets: data.harnesses.map(row => row.target) });
487
+ if (collected.kind !== 'complete')
488
+ throw new RefusalError(`browser MCP declaration refused: ${collected.diagnostic.code}`);
489
+ const server = collected.result.servers[0];
490
+ if (server.name !== 'playwright')
491
+ throw new RefusalError('browser MCP declaration must be the playwright server');
492
+ if (server.cwd !== data.cockpitDirectory) {
493
+ throw new RefusalError('the playwright MCP declaration must come from a sibling whose dir is the cockpit directory itself (dir = "."), so the server starts there');
494
+ }
495
+ if (!server.args.includes(runtime.mcp.image)) {
496
+ throw new RefusalError(`the playwright MCP server's arguments must include browser.images.mcp.reference (${runtime.mcp.image})`);
497
+ }
498
+ if (runtime.connection.kind === 'remote' && !server.args.includes(runtime.connection.endpoint)) {
499
+ throw new RefusalError('the playwright MCP server\'s arguments must include the bound browser endpoint');
500
+ }
501
+ const rows = [];
502
+ const files = [];
503
+ for (const harness of data.harnesses) {
504
+ const measured = MEASURED[harness.target];
505
+ for (const transport of ['mcp', 'cli']) {
506
+ let reason = 'Declaration only; current paired native invocation is required.';
507
+ let available = harness.version === measured.version;
508
+ if (!available)
509
+ reason = 'Harness version has no measured transport spelling.';
510
+ else if (runtime.connection.kind !== 'remote') {
511
+ available = false;
512
+ reason = 'Native transports require a declared remote browser endpoint; application commands use the local project client.';
513
+ }
514
+ else if (transport === 'cli' && !measured.cli) {
515
+ available = false;
516
+ reason = 'Browser CLI is unavailable under the measured workspace sandbox.';
517
+ }
518
+ else if (transport === 'cli' && cli === null) {
519
+ available = false;
520
+ reason = 'Pinned test package CLI identity is unavailable.';
521
+ }
522
+ else if (transport === 'mcp' && !server.enabled) {
523
+ available = false;
524
+ reason = 'Browser MCP server is absent from shared approval.';
525
+ }
526
+ else if (transport === 'mcp' && harness.target === 'codexcli' && runtime.preapproval !== 'eligible') {
527
+ available = false;
528
+ reason = 'Measured MCP transport requires approval that this data posture does not authorize.';
529
+ }
530
+ rows.push({ target: harness.target, version: harness.version, transport,
531
+ measuredOn: measured.date, measuredVersion: measured.version, declaration: available ? 'instructed' : 'unavailable',
532
+ callable: available ? 'human_needed' : 'unavailable', productState: 'instructed', reason,
533
+ tool: available && transport === 'mcp' ? measured.tool : null, cli: available && transport === 'cli' ? cli : null });
534
+ }
535
+ const ownRows = rows.filter(row => row.target === harness.target);
536
+ if (ownRows.every(row => row.declaration === 'unavailable'))
537
+ continue;
538
+ if (ownRows.some(row => row.transport === 'mcp' && row.declaration === 'instructed')) {
539
+ const native = collected.result.harnesses.find(row => row.target === harness.target).servers[0];
540
+ files.push(nativeMcp(harness.target, native.command, native.args));
541
+ }
542
+ files.push(renderSkill(data.skillTemplate, harness.target, ownRows, runtime));
543
+ }
544
+ const targets = rows.filter(row => row.transport === 'mcp' && row.declaration === 'instructed').map(row => row.target);
545
+ const mcp = collectMcpDeclarations({ cockpitDirectory: data.cockpitDirectory, declarations: targets.length ? [data.mcp] : [],
546
+ approved: targets.length ? approval : {}, targets: targets.length ? targets : data.harnesses.map(row => row.target) });
547
+ if (mcp.kind !== 'complete')
548
+ throw new RefusalError(`browser MCP collection refused: ${mcp.diagnostic.code}`);
549
+ const projectPackage = data.testPackage !== null && 'artifactRoot' in data.testPackage && 'module' in data.testPackage
550
+ ? data.testPackage : null;
551
+ const clients = projectPackage === null || cli === null ? [] : data.harnesses
552
+ .filter(harness => harness.version === MEASURED[harness.target].version)
553
+ .map(harness => projectClient(projectPackage, harness, runtime, data.cockpitDirectory));
554
+ return { rows, files, mcp,
555
+ clients, graderBrowserAccess: 'retained' };
556
+ }
@@ -0,0 +1,15 @@
1
+ import { type BrowserCommandKind, type BrowserCommandReceipt } from './commands.js';
2
+ export interface UiGateInput {
3
+ readonly activating?: unknown;
4
+ readonly receipts: readonly unknown[];
5
+ }
6
+ export type UiGateResult = Readonly<{
7
+ kind: 'satisfied';
8
+ activating: BrowserCommandKind;
9
+ receipt: BrowserCommandReceipt;
10
+ }> | Readonly<{
11
+ kind: 'refusal';
12
+ reason: string;
13
+ satisfyingCommand: string;
14
+ }>;
15
+ export declare function evaluateUiGate(input: UiGateInput): UiGateResult;
@@ -0,0 +1,18 @@
1
+ import { isIssuedBrowserCommandReceipt } from './commands.js';
2
+ const satisfyingCommand = 'npx --no cockpit browser drive --config cockpit.toml or npx --no cockpit browser screenshot --config cockpit.toml';
3
+ const activatingKinds = new Set(['drive', 'screenshot']);
4
+ const refusal = (reason) => ({ kind: 'refusal', reason, satisfyingCommand });
5
+ export function evaluateUiGate(input) {
6
+ if (!Object.hasOwn(input, 'activating') || !activatingKinds.has(input.activating)) {
7
+ return refusal('UI evidence requires an activating browser command');
8
+ }
9
+ if (!Array.isArray(input.receipts) || input.receipts.length === 0) {
10
+ return refusal('UI evidence requires an issued receipt from the activating browser command');
11
+ }
12
+ const activating = input.activating;
13
+ const receipt = input.receipts.find((candidate) => isIssuedBrowserCommandReceipt(candidate)
14
+ && candidate.activation === 'application' && candidate.kind === activating);
15
+ return receipt === undefined
16
+ ? refusal('UI evidence requires an issued receipt bound to the configured activating browser command')
17
+ : { kind: 'satisfied', activating, receipt };
18
+ }
@@ -0,0 +1,5 @@
1
+ export declare function runGuardCommand(commandArgs: readonly string[], io: {
2
+ stdout: Pick<NodeJS.WriteStream, 'write'>;
3
+ stderr: Pick<NodeJS.WriteStream, 'write'>;
4
+ stdin?: NodeJS.ReadStream;
5
+ }, env: Readonly<Record<string, string | undefined>>): Promise<number>;