@b00dea/cockpit 0.1.0 → 0.1.1

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 (196) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +3 -0
  3. package/cockpit.schema.json +73 -3
  4. package/dist/autonomy.d.ts +3 -2
  5. package/dist/autonomy.js +30 -13
  6. package/dist/browser/commands.d.ts +57 -2
  7. package/dist/browser/commands.js +114 -10
  8. package/dist/browser/runtime.d.ts +1 -0
  9. package/dist/browser/runtime.js +4 -1
  10. package/dist/browser/surfaces.d.ts +7 -7
  11. package/dist/browser/surfaces.js +5 -0
  12. package/dist/canonical-json.d.ts +2 -0
  13. package/dist/canonical-json.js +5 -0
  14. package/dist/cli-main.d.ts +40 -3
  15. package/dist/cli-main.js +241 -62
  16. package/dist/command-table.d.ts +19 -11
  17. package/dist/command-table.js +190 -38
  18. package/dist/config/load.d.ts +16 -4
  19. package/dist/config/load.js +25 -3
  20. package/dist/config/unconfirmed-toml.d.ts +9 -0
  21. package/dist/config/unconfirmed-toml.js +29 -0
  22. package/dist/diagnostics.js +10 -1
  23. package/dist/docs/check.d.ts +16 -0
  24. package/dist/docs/check.js +197 -0
  25. package/dist/docs/decision-record.d.ts +34 -0
  26. package/dist/docs/decision-record.js +135 -0
  27. package/dist/docs/decisions.d.ts +16 -0
  28. package/dist/docs/decisions.js +103 -0
  29. package/dist/docs/markdown.d.ts +36 -0
  30. package/dist/docs/markdown.js +88 -0
  31. package/dist/docs/references.d.ts +32 -0
  32. package/dist/docs/references.js +148 -0
  33. package/dist/environment/application/confirm.d.ts +35 -0
  34. package/dist/environment/application/confirm.js +122 -0
  35. package/dist/environment/application/destructive.js +6 -2
  36. package/dist/environment/application/doctor.d.ts +12 -7
  37. package/dist/environment/application/doctor.js +20 -8
  38. package/dist/environment/application/guard.d.ts +12 -6
  39. package/dist/environment/application/guard.js +87 -32
  40. package/dist/environment/application/health.d.ts +14 -5
  41. package/dist/environment/application/health.js +8 -2
  42. package/dist/environment/application/lifecycle-context.d.ts +10 -3
  43. package/dist/environment/application/lifecycle-state.js +4 -3
  44. package/dist/environment/application/lifecycle.d.ts +2 -1
  45. package/dist/environment/application/lifecycle.js +11 -3
  46. package/dist/environment/application/services.d.ts +60 -0
  47. package/dist/environment/application/services.js +89 -0
  48. package/dist/environment/application/siblings.d.ts +43 -0
  49. package/dist/environment/application/siblings.js +269 -0
  50. package/dist/environment/claims.d.ts +10 -10
  51. package/dist/environment/claims.js +3 -11
  52. package/dist/environment/compose-topology.d.ts +15 -7
  53. package/dist/environment/compose-topology.js +100 -23
  54. package/dist/environment/contract.d.ts +42 -9
  55. package/dist/environment/contract.js +33 -0
  56. package/dist/environment/executable.d.ts +12 -0
  57. package/dist/environment/executable.js +46 -23
  58. package/dist/environment/generated-application/check.js +2 -0
  59. package/dist/environment/generated-application/prepare.js +2 -0
  60. package/dist/environment/generated-application/shared.d.ts +10 -2
  61. package/dist/environment/generated-application/shared.js +6 -1
  62. package/dist/environment/generated-application/surface-outputs.js +23 -12
  63. package/dist/environment/generated-application/surfaces.js +16 -11
  64. package/dist/environment/generated-application/sync.js +19 -4
  65. package/dist/environment/git-observation.d.ts +25 -6
  66. package/dist/environment/git-observation.js +37 -28
  67. package/dist/environment/health-composition.d.ts +3 -2
  68. package/dist/environment/health-composition.js +19 -2
  69. package/dist/environment/init-application.d.ts +39 -14
  70. package/dist/environment/init-application.js +39 -10
  71. package/dist/environment/lifecycle-composition.d.ts +4 -2
  72. package/dist/environment/lifecycle-composition.js +84 -17
  73. package/dist/environment/lifecycle-stage.d.ts +8 -3
  74. package/dist/environment/lifecycle-stage.js +11 -1
  75. package/dist/environment/logs.d.ts +1 -1
  76. package/dist/environment/map-limit.d.ts +2 -0
  77. package/dist/environment/map-limit.js +13 -0
  78. package/dist/environment/mcp-observation.d.ts +9 -0
  79. package/dist/environment/mcp-observation.js +46 -0
  80. package/dist/environment/observations.d.ts +1 -1
  81. package/dist/environment/observations.js +2 -0
  82. package/dist/environment/operator-application.d.ts +35 -6
  83. package/dist/environment/operator-application.js +42 -0
  84. package/dist/environment/presence.d.ts +4 -0
  85. package/dist/environment/presence.js +26 -0
  86. package/dist/environment/read-composition.d.ts +8 -3
  87. package/dist/environment/read-composition.js +53 -8
  88. package/dist/environment/readiness-composition.d.ts +20 -1
  89. package/dist/environment/readiness-composition.js +23 -2
  90. package/dist/environment/reclaim.d.ts +56 -19
  91. package/dist/environment/reclaim.js +147 -42
  92. package/dist/environment/runtime.d.ts +64 -18
  93. package/dist/environment/runtime.js +73 -3
  94. package/dist/environment/served-checkout-grants.d.ts +53 -0
  95. package/dist/environment/served-checkout-grants.js +182 -0
  96. package/dist/environment/sibling-checkouts.d.ts +119 -0
  97. package/dist/environment/sibling-checkouts.js +254 -0
  98. package/dist/environment/sibling-observation.d.ts +27 -0
  99. package/dist/environment/sibling-observation.js +45 -0
  100. package/dist/environment/sibling-pull.d.ts +23 -0
  101. package/dist/environment/sibling-pull.js +95 -0
  102. package/dist/environment/stack-inputs.d.ts +18 -0
  103. package/dist/environment/stack-inputs.js +210 -0
  104. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  105. package/dist/environment/stack-inventory-prune.js +129 -0
  106. package/dist/environment/stack-inventory-report.d.ts +44 -0
  107. package/dist/environment/stack-inventory-report.js +78 -0
  108. package/dist/environment/stack-inventory.d.ts +94 -0
  109. package/dist/environment/stack-inventory.js +418 -0
  110. package/dist/environment/state.d.ts +48 -9
  111. package/dist/environment/state.js +142 -2
  112. package/dist/environment/tool-registration.js +3 -7
  113. package/dist/environment-exec/execute.d.ts +6 -3
  114. package/dist/environment-exec/execute.js +4 -0
  115. package/dist/fix-catalogue.d.ts +1 -1
  116. package/dist/generated-environment/adopt.js +4 -8
  117. package/dist/guards/answer-confirmation.d.ts +41 -0
  118. package/dist/guards/answer-confirmation.js +109 -0
  119. package/dist/guards/evaluator.d.ts +20 -3
  120. package/dist/guards/evaluator.js +39 -7
  121. package/dist/guards/local-identity.d.ts +6 -2
  122. package/dist/guards/local-identity.js +10 -4
  123. package/dist/guards/policy-change.d.ts +43 -0
  124. package/dist/guards/policy-change.js +159 -0
  125. package/dist/guards/policy-pin.d.ts +1 -1
  126. package/dist/guards/policy-pin.js +14 -12
  127. package/dist/init/command.d.ts +4 -3
  128. package/dist/init/command.js +36 -16
  129. package/dist/init/derive.d.ts +8 -6
  130. package/dist/init/derive.js +140 -36
  131. package/dist/init/draft.d.ts +13 -11
  132. package/dist/init/draft.js +54 -31
  133. package/dist/init/render.d.ts +18 -6
  134. package/dist/init/render.js +31 -9
  135. package/dist/repository-path.d.ts +9 -0
  136. package/dist/repository-path.js +16 -0
  137. package/dist/schema.d.ts +42 -75
  138. package/dist/schema.js +62 -11
  139. package/dist/surfaces/harness-registry.d.ts +80 -0
  140. package/dist/surfaces/harness-registry.js +29 -0
  141. package/dist/surfaces/mcp.d.ts +67 -17
  142. package/dist/surfaces/mcp.js +194 -31
  143. package/dist/surfaces/projection.d.ts +7 -7
  144. package/dist/surfaces/projection.js +41 -6
  145. package/dist/surfaces/render.d.ts +5 -44
  146. package/dist/surfaces/render.js +5 -2
  147. package/dist/team-metadata.js +2 -1
  148. package/dist/testing/test-protection.d.ts +34 -0
  149. package/dist/testing/test-protection.js +137 -0
  150. package/dist/testing/weakening.d.ts +15 -0
  151. package/dist/testing/weakening.js +30 -0
  152. package/dist/work-orders/application-closure.d.ts +7 -0
  153. package/dist/work-orders/application-closure.js +76 -0
  154. package/dist/work-orders/application-context.d.ts +162 -0
  155. package/dist/work-orders/application-context.js +137 -0
  156. package/dist/work-orders/application-decisions.d.ts +27 -0
  157. package/dist/work-orders/application-decisions.js +121 -0
  158. package/dist/work-orders/application-grading.d.ts +11 -0
  159. package/dist/work-orders/application-grading.js +212 -0
  160. package/dist/work-orders/application.d.ts +4 -97
  161. package/dist/work-orders/application.js +94 -254
  162. package/dist/work-orders/composition.d.ts +35 -58
  163. package/dist/work-orders/composition.js +216 -24
  164. package/dist/work-orders/contract.d.ts +22 -6
  165. package/dist/work-orders/contract.js +34 -16
  166. package/dist/work-orders/decisions.d.ts +23 -0
  167. package/dist/work-orders/decisions.js +55 -0
  168. package/dist/work-orders/store.d.ts +54 -5
  169. package/dist/work-orders/store.js +118 -24
  170. package/docs/README.md +6 -3
  171. package/docs/capability-matrix.md +17 -9
  172. package/docs/cli-reference.md +209 -1
  173. package/docs/command-list.md +12 -1
  174. package/docs/concepts.md +6 -4
  175. package/docs/configuration-essentials.md +7 -2
  176. package/docs/configuration-reference.md +18 -3
  177. package/docs/docs-check.md +76 -0
  178. package/docs/environment.md +194 -6
  179. package/docs/faq.md +5 -3
  180. package/docs/getting-started.md +109 -25
  181. package/docs/harness-surfaces.md +69 -2
  182. package/docs/safety-and-autonomy.md +56 -12
  183. package/docs/troubleshooting.md +29 -8
  184. package/docs/tutorial.md +3 -2
  185. package/docs/work-and-browser.md +180 -14
  186. package/libexec/browser-operation.cjs +86 -2
  187. package/package.json +2 -2
  188. package/surfaces/skills/cockpit-browser.skill.md +7 -2
  189. package/surfaces/skills/cockpit-explain.skill.md +1 -1
  190. package/surfaces/skills/cockpit-fix.skill.md +1 -1
  191. package/surfaces/skills/cockpit-setup.skill.md +3 -3
  192. package/surfaces/skills/cockpit-work.skill.md +1 -1
  193. package/surfaces/skills/procedures/explain.md +9 -4
  194. package/surfaces/skills/procedures/fix.md +25 -11
  195. package/surfaces/skills/procedures/setup.md +113 -40
  196. package/surfaces/skills/procedures/work.md +45 -16
@@ -4,6 +4,7 @@ import { readBoundedFile } from '../bounded-read.js';
4
4
  import { isIP } from 'node:net';
5
5
  import { dirname, isAbsolute, relative, resolve } from 'node:path';
6
6
  import { errorCause, snapshotJson, RefusalError, hasErrorCode } from '../environment/contract.js';
7
+ import { redactSensitiveMessage } from '../environment/sensitive.js';
7
8
  import {} from '../environment/state.js';
8
9
  import { cockpitCommand } from '../fix-catalogue.js';
9
10
  import { assertBrowserStorageStateCurrent, browserRuntimeControlAuthorities, browserRuntimeControlPorts } from './runtime.js';
@@ -164,7 +165,81 @@ function publicAuthentication(value) {
164
165
  return value.posture !== 'real' ? { posture: value.posture }
165
166
  : { posture: 'real', role: value.role, policy: value.policy };
166
167
  }
167
- function parseObservation(value, kind, origin) {
168
+ const MAX_DIAGNOSTIC_ENTRIES = 20;
169
+ const MAX_EXCERPT_MESSAGES = 5;
170
+ const MAX_EXCERPT_MESSAGE_BYTES = 240;
171
+ const EXCERPT_OMITTED = 'browser.data is not isolated, non-production and non-restored, so page message text is never read; the counts and entries above still apply';
172
+ const isCount = (value) => Number.isSafeInteger(value) && value >= 0;
173
+ const exactKeys = (row, keys) => Object.keys(row).sort().join(',') === keys.sort().join(',');
174
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
175
+ const diagnosticPath = (value) => typeof value === 'string' && value.length <= 200 && /^\/[^\s?#\x00-\x1f\x7f]*$/.test(value);
176
+ function parseDiagnosticEntry(value) {
177
+ if (!isRecord(value))
178
+ return undefined;
179
+ if (value.kind === 'console') {
180
+ const keys = ['kind', ...value.path !== undefined ? ['path'] : [], ...value.line !== undefined ? ['line'] : []];
181
+ if (!exactKeys(value, keys) || value.path !== undefined && !diagnosticPath(value.path) || value.line !== undefined && !isCount(value.line))
182
+ return undefined;
183
+ return { kind: 'console', ...value.path !== undefined ? { path: value.path } : {}, ...value.line !== undefined ? { line: value.line } : {} };
184
+ }
185
+ if (value.kind === 'pageerror') {
186
+ return exactKeys(value, ['kind', 'name']) && typeof value.name === 'string' && /^[A-Za-z0-9_$.]{1,64}$/.test(value.name)
187
+ ? { kind: 'pageerror', name: value.name } : undefined;
188
+ }
189
+ if (value.kind !== 'request' || typeof value.method !== 'string' || !/^[A-Z]{1,16}$/.test(value.method) || !diagnosticPath(value.path))
190
+ return undefined;
191
+ if (exactKeys(value, ['kind', 'method', 'path', 'status'])) {
192
+ return Number.isInteger(value.status) && value.status >= 400 && value.status <= 599
193
+ ? { kind: 'request', method: value.method, path: value.path, status: value.status } : undefined;
194
+ }
195
+ return exactKeys(value, ['kind', 'method', 'path', 'failure']) && typeof value.failure === 'string' && /^ERR_[A-Z0-9_]{1,64}$/.test(value.failure)
196
+ ? { kind: 'request', method: value.method, path: value.path, failure: value.failure } : undefined;
197
+ }
198
+ // Page text is kept as work-order evidence, so a URL loses its query string and fragment and anything
199
+ // secret-shaped is redacted; text that cannot be redacted is withheld, never kept raw.
200
+ function sanitizeExcerpt(text) {
201
+ const redacted = redactSensitiveMessage(text.replace(/(\b[a-z][a-z0-9+.-]*:\/\/[^\s?#]*)[?#]\S*/gi, '$1')) ?? '[withheld]';
202
+ let bounded = redacted;
203
+ while (Buffer.byteLength(bounded) > MAX_EXCERPT_MESSAGE_BYTES)
204
+ bounded = bounded.slice(0, -1);
205
+ return bounded;
206
+ }
207
+ function parseMessages(value) {
208
+ if (!Array.isArray(value) || value.length > MAX_EXCERPT_MESSAGES)
209
+ return undefined;
210
+ const messages = [];
211
+ for (const row of value) {
212
+ if (!isRecord(row) || !exactKeys(row, ['kind', 'text']) || row.kind !== 'console' && row.kind !== 'pageerror' || typeof row.text !== 'string'
213
+ || row.text.length === 0 || Buffer.byteLength(row.text) > MAX_EXCERPT_MESSAGE_BYTES || /[\x00-\x1f\x7f]/.test(row.text))
214
+ return undefined;
215
+ messages.push({ kind: row.kind, text: sanitizeExcerpt(row.text) });
216
+ }
217
+ return { trust: 'untrusted-page-content', messages };
218
+ }
219
+ // The operation reads message text only when asked to, so its presence must match the posture exactly.
220
+ function parseDiagnostics(value, excerptAllowed) {
221
+ if (!isRecord(value) || !exactKeys(value, ['consoleErrors', 'consoleWarnings', 'pageErrors', 'failedRequests', 'abortedRequests', 'thirdParty', 'entries', 'omitted', ...excerptAllowed ? ['messages'] : []]))
222
+ return undefined;
223
+ const { consoleErrors, consoleWarnings, pageErrors, failedRequests, abortedRequests, thirdParty, entries, omitted } = value;
224
+ if (![consoleErrors, consoleWarnings, pageErrors, failedRequests, abortedRequests, omitted].every(isCount)
225
+ || !isRecord(thirdParty) || !exactKeys(thirdParty, ['requests', 'failed']) || !isCount(thirdParty.requests) || !isCount(thirdParty.failed)
226
+ || !Array.isArray(entries) || entries.length > MAX_DIAGNOSTIC_ENTRIES)
227
+ return undefined;
228
+ const parsed = entries.map(parseDiagnosticEntry);
229
+ if (parsed.some(entry => entry === undefined))
230
+ return undefined;
231
+ const excerpt = excerptAllowed ? parseMessages(value.messages) : { omitted: EXCERPT_OMITTED };
232
+ if (!excerpt)
233
+ return undefined;
234
+ // Every finding is either listed or counted as omitted, so the listing can never hide one.
235
+ if (parsed.length + omitted !== consoleErrors + pageErrors + failedRequests)
236
+ return undefined;
237
+ return { consoleErrors: consoleErrors, consoleWarnings: consoleWarnings, pageErrors: pageErrors,
238
+ failedRequests: failedRequests, abortedRequests: abortedRequests,
239
+ thirdParty: { requests: thirdParty.requests, failed: thirdParty.failed }, entries: parsed, omitted: omitted, excerpt };
240
+ }
241
+ const routeVerdict = (diagnostics) => diagnostics.consoleErrors + diagnostics.pageErrors + diagnostics.failedRequests > 0 ? 'findings' : 'clean';
242
+ function parseObservation(value, kind, origin, excerptAllowed) {
168
243
  let parsed;
169
244
  // Child stdout can be quoted by a parse error; the caller refuses with its own fixed message.
170
245
  try {
@@ -176,7 +251,7 @@ function parseObservation(value, kind, origin) {
176
251
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
177
252
  return undefined;
178
253
  const row = parsed;
179
- const expected = kind === 'drive' ? ['finalUrl', 'operation', 'schema'] : ['evidence', 'finalUrl', 'operation', 'schema'];
254
+ const expected = kind === 'drive' ? ['diagnostics', 'finalUrl', 'operation', 'schema'] : ['evidence', 'finalUrl', 'operation', 'schema'];
180
255
  if (Object.keys(row).sort().join(',') !== expected.sort().join(',') || row.schema !== 'cockpit-browser-observation/v1' || row.operation !== kind || typeof row.finalUrl !== 'string')
181
256
  return undefined;
182
257
  let final;
@@ -189,8 +264,10 @@ function parseObservation(value, kind, origin) {
189
264
  }
190
265
  if (final.origin !== origin || final.username || final.password || final.hash)
191
266
  return undefined;
192
- if (kind === 'drive')
193
- return { finalUrl: final.href };
267
+ if (kind === 'drive') {
268
+ const diagnostics = parseDiagnostics(row.diagnostics, excerptAllowed);
269
+ return diagnostics ? { finalUrl: final.href, diagnostics } : undefined;
270
+ }
194
271
  if (!row.evidence || typeof row.evidence !== 'object' || Array.isArray(row.evidence))
195
272
  return undefined;
196
273
  const evidence = row.evidence;
@@ -249,14 +326,14 @@ function ensureArtifactRoot(root) {
249
326
  }
250
327
  return undefined;
251
328
  }
252
- async function run(input, client, policy, auth, deadline, authorization) {
329
+ async function run(input, client, surfaces, policy, auth, deadline, authorization) {
253
330
  const receipts = [];
254
331
  if (input.kind === 'screenshot') {
255
332
  const unavailable = ensureArtifactRoot(client.artifactRoot);
256
333
  if (unavailable)
257
334
  return refusal(unavailable);
258
335
  }
259
- for (const surface of policy.surfaces) {
336
+ for (const surface of surfaces) {
260
337
  // Only the runtime's own fixed-text TypeError escapes the revalidation (it replaces filesystem
261
338
  // errors with that text), so the refusal names the condition instead.
262
339
  try {
@@ -285,7 +362,7 @@ async function run(input, client, policy, auth, deadline, authorization) {
285
362
  if (authenticated.execution.exitCode !== 0) {
286
363
  return refusal(`Browser ${input.kind} operation failed without a valid bounded diagnostic for ${surface.name}`);
287
364
  }
288
- const observation = parseObservation(authenticated.stdout, input.kind, policy.origin);
365
+ const observation = parseObservation(authenticated.stdout, input.kind, policy.origin, input.runtime.dataQualifies);
289
366
  if (!observation)
290
367
  return refusal(`Browser ${input.kind} returned no bound application-driving observation for ${surface.name}`);
291
368
  try {
@@ -309,13 +386,37 @@ async function run(input, client, policy, auth, deadline, authorization) {
309
386
  receipts.push({ ...base, screenshot: { path, ...observation.evidence } });
310
387
  }
311
388
  else
312
- receipts.push(base);
389
+ receipts.push({ ...base, diagnostics: observation.diagnostics, verdict: routeVerdict(observation.diagnostics) });
313
390
  }
314
391
  const receipt = { kind: input.kind, activation: 'application',
315
- authentication: publicAuthentication(auth), surfaces: receipts };
392
+ authentication: publicAuthentication(auth), surfaces: receipts,
393
+ ...input.kind === 'drive' ? { verdict: receipts.some(row => row.verdict === 'findings') ? 'findings' : 'clean' } : {} };
316
394
  issuedReceipts.add(receipt);
317
395
  return { kind: 'complete', receipt };
318
396
  }
397
+ // A route is chosen by its declared name or its declared path; anything else is refused with the choices.
398
+ function selectRoutes(input) {
399
+ if (input.route === undefined)
400
+ return input.policy.surfaces;
401
+ const route = input.route;
402
+ const origin = new URL(input.policy.origin);
403
+ let href;
404
+ if (route.startsWith('/') && !route.startsWith('//')) {
405
+ // `new URL` throws TypeError for an address it cannot parse; such a route can only match by name.
406
+ try {
407
+ href = new URL(route, origin).href;
408
+ }
409
+ catch (error) {
410
+ if (!(error instanceof TypeError))
411
+ throw error;
412
+ }
413
+ }
414
+ const surface = input.policy.surfaces.find(row => row.name === route || href !== undefined && row.url === href);
415
+ if (surface)
416
+ return [surface];
417
+ // The route is the caller's own input, so it is safe to echo back.
418
+ return `Browser route ${JSON.stringify(route)} is not declared; declared routes are ${input.policy.surfaces.map(row => `${row.name} (${new URL(row.url).pathname + new URL(row.url).search})`).join(', ')}`;
419
+ }
319
420
  export async function executeBrowserCommand(value) {
320
421
  const deadline = Date.now() + COMMAND_TIMEOUT_MS;
321
422
  const input = value;
@@ -338,12 +439,15 @@ export async function executeBrowserCommand(value) {
338
439
  + 'a remote browser reaches host addresses only through that exposure, so it would look for the application inside its own container. '
339
440
  + 'Use a 127.0.0.1, localhost or .localhost origin (an IPv6 literal is never tunnelled), or widen network_exposure with a network_exposure_reason');
340
441
  }
442
+ const selected = selectRoutes(input);
443
+ if (typeof selected === 'string')
444
+ return refusal(selected);
341
445
  const auth = authentication(input);
342
446
  if (!auth)
343
447
  return refusal(input.runtime.authentication.posture === 'real'
344
448
  ? 'Browser command requires a declared seeded role and matching coordination authority' : 'Bypassed browser authentication cannot accept role or state input');
345
449
  const admitted = await withBrowserProjectOperationAdmission(client, input.runtime, {
346
450
  origin: input.policy.origin, ...(auth.posture === 'real' ? { role: auth.role } : {}), ...(input.state ? { state: input.state } : {}),
347
- }, authorization => run(input, client, input.policy, auth, deadline, authorization));
451
+ }, authorization => run(input, client, selected, input.policy, auth, deadline, authorization));
348
452
  return admitted.kind === 'admitted' ? admitted.value : refusal(admitted.reason);
349
453
  }
@@ -100,6 +100,7 @@ export declare function resolveBrowserRuntime(input: BrowserRuntimeInput, coordi
100
100
  states: any[];
101
101
  claim: "exclusive" | "per-worker";
102
102
  };
103
+ dataQualifies: boolean;
103
104
  preapproval: "eligible" | "not-requested";
104
105
  networkExposure: string;
105
106
  networkExposureReason?: string | undefined;
@@ -123,7 +123,8 @@ export function resolveBrowserRuntime(input, coordinationNamespace) {
123
123
  || typeof input.data.reachesProduction !== 'boolean' || typeof input.requestPreapproval !== 'boolean') {
124
124
  throw new RefusalError('browser data posture must be explicit');
125
125
  }
126
- if (input.requestPreapproval && (input.data.mode === 'restored' || !input.data.isolated || input.data.reachesProduction)) {
126
+ const dataQualifies = input.data.mode !== 'restored' && input.data.isolated && !input.data.reachesProduction;
127
+ if (input.requestPreapproval && !dataQualifies) {
127
128
  throw new RefusalError('browser preapproval requires isolated empty or seeded data with no production reach');
128
129
  }
129
130
  const projection = {
@@ -141,6 +142,8 @@ export function resolveBrowserRuntime(input, coordinationNamespace) {
141
142
  authentication: signedOut
142
143
  ? { posture: signedOut, policy: 'none', states: [] }
143
144
  : { posture: 'real', policy: input.authentication.policy, states: states.map((state) => ({ ...state })), claim: input.authentication.policy === 'serialized' ? 'exclusive' : 'per-worker' },
145
+ // The posture alone, whatever request_preapproval says: it also gates message excerpts in drive receipts.
146
+ dataQualifies,
144
147
  preapproval: input.requestPreapproval ? 'eligible' : 'not-requested',
145
148
  networkExposure: networkExposure.value,
146
149
  ...(networkExposure.reason ? { networkExposureReason: networkExposure.reason } : {}),
@@ -3,9 +3,9 @@ import { type ExecuteProbeOutcome } from '../environment-exec/execute.js';
3
3
  import { type StateNamespace } from '../environment/state.js';
4
4
  import type { RenderedSurfaceFile } from '../surfaces/contract.js';
5
5
  import { type GuardSurfaceTarget } from '../surfaces/harness-registry.js';
6
- import { type McpDeclaration } from '../surfaces/mcp.js';
6
+ import { type McpStdioDeclaration } from '../surfaces/mcp.js';
7
7
  import { type BrowserRuntimeProjection } from './runtime.js';
8
- export type BrowserTestPackage = Readonly<{
8
+ type BrowserTestPackage = Readonly<{
9
9
  directory: string;
10
10
  manifest: {
11
11
  readonly bytes: string;
@@ -34,12 +34,12 @@ export interface BrowserSurfaceInput {
34
34
  readonly target: GuardSurfaceTarget;
35
35
  readonly version: string;
36
36
  }[];
37
- readonly mcp: McpDeclaration;
37
+ readonly mcp: McpStdioDeclaration;
38
38
  readonly approved: boolean;
39
39
  readonly testPackage: BrowserTestPackage | null;
40
40
  readonly skillTemplate: string;
41
41
  }
42
- export interface BrowserTransportRow {
42
+ interface BrowserTransportRow {
43
43
  readonly target: GuardSurfaceTarget;
44
44
  readonly version: string;
45
45
  readonly transport: 'mcp' | 'cli';
@@ -84,7 +84,7 @@ export interface BrowserProjectClient {
84
84
  maxBytes: number;
85
85
  }>;
86
86
  }
87
- export interface BrowserProjectOperation {
87
+ interface BrowserProjectOperation {
88
88
  readonly kind: 'drive' | 'screenshot';
89
89
  readonly surface: Readonly<{
90
90
  name: string;
@@ -97,7 +97,7 @@ export interface BrowserProjectOperation {
97
97
  export interface BrowserProjectOperationAuthorization {
98
98
  readonly kind: 'browser-project-operation-authorization';
99
99
  }
100
- export interface BrowserProjectOperationAdmissionInput {
100
+ interface BrowserProjectOperationAdmissionInput {
101
101
  readonly origin: string;
102
102
  readonly role?: string;
103
103
  readonly state?: Readonly<{
@@ -114,7 +114,7 @@ export declare function observeBrowserProjectPackage(input: Readonly<{
114
114
  artifactRoot: string;
115
115
  }>): BrowserTestPackage;
116
116
  export declare function authenticateBrowserProjectClient(value: unknown, runtime: BrowserRuntimeProjection): BrowserProjectClient | undefined;
117
- export type BrowserProjectOperationNotAdmitted = Readonly<{
117
+ type BrowserProjectOperationNotAdmitted = Readonly<{
118
118
  kind: 'unadmitted';
119
119
  cause: string;
120
120
  }>;
@@ -238,6 +238,7 @@ function operationStep(client, runtime, operation, remaining, origin, statePath,
238
238
  '--control-ports', JSON.stringify(controlPorts),
239
239
  '--url', operation.surface.url, '--origin', origin, '--timeout', String(remaining),
240
240
  ...(statePath ? ['--state-proof', statePath] : []),
241
+ ...(operation.kind === 'drive' && runtime.dataQualifies ? ['--messages'] : []),
241
242
  ...(operation.output && artifactIdentity ? ['--output', operation.output, '--output-name', basename(operation.output),
242
243
  '--artifact-root', client.artifactRoot, '--artifact-dev', String(artifactIdentity.dev), '--artifact-ino', String(artifactIdentity.ino),
243
244
  '--artifact-birthtime-ns', artifactIdentity.birthtimeNs] : [])];
@@ -489,6 +490,8 @@ export function renderBrowserSurfaces(input) {
489
490
  const server = collected.result.servers[0];
490
491
  if (server.name !== 'playwright')
491
492
  throw new RefusalError('browser MCP declaration must be the playwright server');
493
+ if (!('command' in server))
494
+ throw new RefusalError('the playwright MCP server must be a local command, not a remote URL');
492
495
  if (server.cwd !== data.cockpitDirectory) {
493
496
  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
497
  }
@@ -537,6 +540,8 @@ export function renderBrowserSurfaces(input) {
537
540
  continue;
538
541
  if (ownRows.some(row => row.transport === 'mcp' && row.declaration === 'instructed')) {
539
542
  const native = collected.result.harnesses.find(row => row.target === harness.target).servers[0];
543
+ if (!('command' in native))
544
+ throw new RefusalError('the playwright MCP server must be a local command, not a remote URL');
540
545
  files.push(nativeMcp(harness.target, native.command, native.args));
541
546
  }
542
547
  files.push(renderSkill(data.skillTemplate, harness.target, ownRows, runtime));
@@ -0,0 +1,2 @@
1
+ /** JSON with object keys sorted at every depth, so equal values serialise to identical bytes whatever order their keys were written in. */
2
+ export declare function canonicalJson(value: unknown): string;
@@ -0,0 +1,5 @@
1
+ /** JSON with object keys sorted at every depth, so equal values serialise to identical bytes whatever order their keys were written in. */
2
+ export function canonicalJson(value) {
3
+ return JSON.stringify(value, (_key, item) => item !== null && typeof item === 'object' && !Array.isArray(item)
4
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
5
+ }
@@ -2,7 +2,7 @@ import { type EvaluatorTool } from './schema.js';
2
2
  import { type ScaffoldBackingService } from './init/scaffold.js';
3
3
  import { type ReleaseMetadataTransport } from './freshness.js';
4
4
  import { type HandlerIdentifier, type WorkHandlerIdentifier } from './command-table.js';
5
- import type { FixContext } from './fix-catalogue.js';
5
+ import { type FixContext } from './fix-catalogue.js';
6
6
  import { type Diagnostic } from './diagnostics.js';
7
7
  import { type UnattendedInit } from './init/command.js';
8
8
  import { type InitApplicationInput, type InitApplicationOutcome, type InitResult } from './environment/init-application.js';
@@ -10,14 +10,17 @@ import { type RegistryResolveCommandInput } from './registry-resolve/command.js'
10
10
  import { type SetupCommandInput } from './setup/command.js';
11
11
  import { type DoctorCommandInput } from './environment/application/doctor.js';
12
12
  import { type PreflightCommandInput } from './environment/application/health.js';
13
+ import { type ConfirmCommandInput } from './environment/application/confirm.js';
13
14
  import { type DownCommandInput, type ResetCommandInput, type SleepCommandInput, type UpCommandInput } from './environment/application/lifecycle-context.js';
14
15
  import { type SiblingAddInput, type SiblingRemoveInput } from './environment/sibling-registration.js';
15
16
  import { type ToolChangeInput } from './environment/tool-registration.js';
17
+ import { type PullCommandInput, type UseCommandInput } from './environment/application/siblings.js';
16
18
  import { type SetupRefusalEvidence } from './environment/contract.js';
17
19
  import { type GeneratedEnvironmentCommandInput } from './generated-environment/command.js';
18
20
  import type { GeneratedStaleRemovalApproval, GeneratedStaleRemovalApprovalRequest } from './environment/generated-application/shared.js';
19
21
  import { type SurfaceSelftestDraftInput, type SurfaceSelftestExecutableCandidate, type SurfaceSelftestProbeSpecificationsCandidate } from './environment/selftest-application.js';
20
- import { loadOperatorPolicy } from './environment/operator-application.js';
22
+ import { loadOperatorPolicy, type StacksPruneOperatorSession } from './environment/operator-application.js';
23
+ import { type StacksPruneScope } from './environment/stack-inventory-prune.js';
21
24
  import type { WorkApplicationRequest } from './work-orders/application.js';
22
25
  export interface CliIo {
23
26
  stdout: Pick<NodeJS.WriteStream, 'write'>;
@@ -33,6 +36,7 @@ export interface CliIo {
33
36
  confirmSelftestExecutable?: (candidate: SurfaceSelftestExecutableCandidate) => Promise<string | undefined>;
34
37
  confirmSelftestProbeSpecifications?: (candidate: SurfaceSelftestProbeSpecificationsCandidate) => Promise<string | undefined>;
35
38
  confirmStaleRemoval?: (request: GeneratedStaleRemovalApprovalRequest) => Promise<string | undefined>;
39
+ confirmStacksPrune?: (preview: CommandOutcome) => Promise<string | undefined>;
36
40
  }
37
41
  export interface CliRuntime {
38
42
  readonly env?: Readonly<Record<string, string | undefined>>;
@@ -55,11 +59,31 @@ interface WorkCommandInput {
55
59
  }
56
60
  interface HandlerInputById extends Record<WorkHandlerIdentifier, WorkCommandInput> {
57
61
  check: GeneratedEnvironmentCommandInput;
62
+ 'docs-check': Readonly<{
63
+ configPath: string;
64
+ }>;
65
+ confirm: ConfirmCommandInput;
58
66
  init: InitApplicationInput & Readonly<{
59
67
  unattended?: UnattendedInit;
60
68
  }>;
61
69
  doctor: DoctorCommandInput;
62
70
  down: DownCommandInput;
71
+ exec: Readonly<{
72
+ configPath: string;
73
+ service: string;
74
+ argv: readonly [string, ...string[]];
75
+ }>;
76
+ ps: Readonly<{
77
+ configPath: string;
78
+ }>;
79
+ restart: Readonly<{
80
+ configPath: string;
81
+ service: string;
82
+ }>;
83
+ rebuild: Readonly<{
84
+ configPath: string;
85
+ service: string;
86
+ }>;
63
87
  logs: Readonly<{
64
88
  configPath: string;
65
89
  service: string;
@@ -95,19 +119,28 @@ interface HandlerInputById extends Record<WorkHandlerIdentifier, WorkCommandInpu
95
119
  configPath: string;
96
120
  argv: readonly [string, ...string[]];
97
121
  sessionId: string;
122
+ profiles: readonly string[];
98
123
  }>;
99
124
  sleep: SleepCommandInput;
125
+ stacks: Readonly<{
126
+ configPath: string;
127
+ operation: 'list' | 'prune';
128
+ }>;
100
129
  sync: GeneratedEnvironmentCommandInput & Readonly<{
101
130
  dryRun?: boolean;
102
131
  adopt?: readonly string[];
103
132
  authorizeStaleRemoval?: (request: GeneratedStaleRemovalApprovalRequest) => Promise<GeneratedStaleRemovalApproval | undefined>;
104
133
  }>;
105
- up: UpCommandInput;
134
+ up: UpCommandInput & Readonly<{
135
+ profiles: readonly string[];
136
+ }>;
106
137
  upgrade: Readonly<{
107
138
  configPath: string;
108
139
  version?: string;
109
140
  dryRun: boolean;
110
141
  }>;
142
+ use: Omit<UseCommandInput, 'env'>;
143
+ pull: Omit<PullCommandInput, 'env'>;
111
144
  }
112
145
  export type CommandHandlers = {
113
146
  [Identifier in HandlerIdentifier]: (input: HandlerInputById[Identifier]) => Promise<Identifier extends 'init' ? InitApplicationOutcome : CommandOutcome>;
@@ -124,6 +157,10 @@ interface OperatorWorkBinding {
124
157
  export type CliHandlerOverrides = Partial<CommandHandlers> & Readonly<{
125
158
  beginOperatorWorkApplication?: (input: WorkCommandInput) => Promise<OperatorWorkBinding | CommandOutcome>;
126
159
  beginOperatorReset?: (input: ResetCommandInput) => Promise<OperatorResetBinding | CommandOutcome>;
160
+ beginStacksPrune?: (input: Readonly<{
161
+ configPath: string;
162
+ scope: StacksPruneScope;
163
+ }>) => Promise<StacksPruneOperatorSession | CommandOutcome>;
127
164
  loadPolicy?: typeof loadOperatorPolicy;
128
165
  scaffoldCompose?: (input: ComposeScaffoldRequest) => Promise<CommandOutcome>;
129
166
  }>;