@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
@@ -265,39 +265,43 @@ function selectionRow(bytes) {
265
265
  // Ownership is label-based: a resource without this stack's label is never removed, so listing the
266
266
  // rest of a shared daemon only cost time. Listing and inspecting every container, image and volume
267
267
  // on a daemon with a hundred unrelated containers made each `down` observation take seconds, and
268
- // `container ls --size` alone ran past the step timeout. Volumes stay unfiltered because a reset
269
- // refuses when a declared volume name belongs to something this stack does not own, and an
268
+ // `container ls --size` alone ran past the step timeout. A stack's volumes stay unfiltered because a
269
+ // reset refuses when a declared volume name belongs to something this stack does not own, and an
270
270
  // unlabelled volume of that name would otherwise be invisible; their inspection is still scoped.
271
- function runtimeCommand(kind, selection, stackId) {
271
+ // The machine-wide listing has no such name to check, so it lists cockpit-labelled volumes alone.
272
+ function runtimeCommand(kind, selection, owner) {
272
273
  if (kind === 'build-cache')
273
274
  return ['--host', selection.endpoint, 'builder', 'du', '--format', 'json'];
274
- const prefix = ['--host', selection.endpoint];
275
- const owned = ['--filter', `label=${REQUIRED_LABELS.stack}=${stackId}`];
275
+ const owned = ['--filter', owner === null ? `label=${REQUIRED_LABELS.stack}` : `label=${REQUIRED_LABELS.stack}=${owner.stackId}`];
276
276
  const command = kind === 'container' ? ['container', 'ls', '--all', '--no-trunc', ...owned]
277
- : kind === 'volume' ? ['volume', 'ls'] : kind === 'image' ? ['image', 'ls', '--all', '--no-trunc', ...owned] : [kind, 'ls', '--no-trunc', ...owned];
278
- return [...prefix, ...command, '--format', discoveryFormat(kind)];
277
+ : kind === 'volume' ? ['volume', 'ls', ...owner === null ? owned : []]
278
+ : kind === 'image' ? ['image', 'ls', '--all', '--no-trunc', ...owned] : [kind, 'ls', '--no-trunc', ...owned];
279
+ return ['--host', selection.endpoint, ...command, '--format', discoveryFormat(kind, owner)];
279
280
  }
280
- function discoveryFormat(kind) {
281
+ function discoveryFormat(kind, owner) {
281
282
  const id = kind === 'volume' ? '{{json .Name}}' : '{{json .ID}}';
282
283
  const name = kind === 'container' ? '{{json .Names}}' : kind === 'image' ? '{{json .Repository}}' : '{{json .Name}}';
283
284
  const label = (key) => `{{json (.Label "${key}")}}`;
285
+ const labels = kind === 'image' ? '{}' : `{${(owner === null ? MACHINE_LABELS : listedLabels(kind)).map((key) => `"${key}":${label(key)}`).join(',')}}`;
286
+ // The machine-wide listing attributes each resource by its labels alone and reports whether a container runs.
287
+ if (owner === null)
288
+ return `{"id":${id},"name":${name},"state":${kind === 'container' ? '{{json .State}}' : '""'},"labels":${labels}}`;
284
289
  // Without `--size` a container prints a misleading "0B (virtual ...)", so its size is unmeasured.
285
290
  const bytes = kind === 'network' || kind === 'container' ? 'null' : '{{json .Size}}';
286
- const labels = kind === 'image' ? '{}' : `{${listedLabels(kind).map((key) => `"${key}":${label(key)}`).join(',')}}`;
287
291
  return `{"id":${id},"name":${name},"labels":${labels},"referenced":false,"bytes":${bytes}}`;
288
292
  }
289
293
  function listedLabels(kind) {
290
294
  return [...Object.values(REQUIRED_LABELS), ...kind === 'volume' ? Object.values(COMPOSE_LABELS) : []];
291
295
  }
292
- function dockerLabels(value, kind) {
296
+ /** The labels Docker printed, refused when any is not `allowed` or its value is too long or holds a NUL; empty values are dropped. */
297
+ function dockerLabels(value, allowed, maxLength = 512) {
293
298
  if (value === null || typeof value !== 'object' || Array.isArray(value))
294
299
  return undefined;
295
300
  const labels = {};
296
- const allowed = new Set(listedLabels(kind));
297
- if (Object.keys(value).some((key) => !allowed.has(key)))
301
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
298
302
  return undefined;
299
303
  for (const [key, labelValue] of Object.entries(value)) {
300
- if (typeof labelValue !== 'string' || labelValue.length > 512 || labelValue.includes('\0'))
304
+ if (typeof labelValue !== 'string' || labelValue.length > maxLength || labelValue.includes('\0'))
301
305
  return undefined;
302
306
  if (labelValue !== '')
303
307
  labels[key] = labelValue;
@@ -345,7 +349,7 @@ function dockerResource(value, kind) {
345
349
  const name = row.name;
346
350
  const labels = kind === 'image'
347
351
  ? row.labels !== null && typeof row.labels === 'object' && !Array.isArray(row.labels) && exactKeys(row.labels, []) ? {} : undefined
348
- : dockerLabels(row.labels, kind);
352
+ : dockerLabels(row.labels, listedLabels(kind));
349
353
  const bytes = dockerBytes(row.bytes, kind === 'container');
350
354
  const referenced = kind === 'image' ? typeof row.referenced === 'boolean' ? row.referenced : undefined : row.referenced;
351
355
  return typeof id === 'string' && typeof name === 'string' && labels !== undefined && bytes !== undefined && typeof referenced === 'boolean'
@@ -500,17 +504,8 @@ async function selectDocker(input, executable, env, execute) {
500
504
  return undefined;
501
505
  return { value: { context: input.context, endpoint: row.endpoint }, startedAt: outcome.receipt.startedAt, finishedAt: outcome.receipt.finishedAt };
502
506
  }
503
- async function listResources(runtime, kind, execute) {
504
- const step = exactStep(`runtime:discover:${kind}`, [runtime.executable.canonicalPath, ...runtimeCommand(kind, runtime.selection, runtime.stackId)], runtime.executable, runtime.cwd, runtime.env, runtime.timeoutMs, [], {
505
- stackId: runtime.stackId, cockpitId: runtime.cockpitId, resourceKind: kind,
506
- dockerContext: runtime.selection.context, dockerEndpoint: runtime.selection.endpoint,
507
- }, [], OUTPUT_LIMIT);
508
- if (step === undefined)
509
- return undefined;
510
- const outcome = cleanOutput(await execute(step));
511
- const text = outcome === undefined ? undefined : utf8(outcome.stdout);
512
- if (outcome === undefined || text === undefined)
513
- return undefined;
507
+ /** One stack's listed resources of one kind, or nothing when any row cannot be read. */
508
+ function stackRows(text, kind) {
514
509
  const resources = [];
515
510
  for (const line of text.split('\n')) {
516
511
  if (line.length === 0)
@@ -538,7 +533,37 @@ async function listResources(runtime, kind, execute) {
538
533
  }
539
534
  if (resources.length > MAX_RESOURCES || new Set(resources.map(({ id }) => id)).size !== resources.length)
540
535
  return undefined;
541
- return { value: resources.sort((a, b) => lexical(a.id, b.id)), startedAt: outcome.receipt.startedAt, finishedAt: outcome.receipt.finishedAt };
536
+ return resources.sort((a, b) => lexical(a.id, b.id));
537
+ }
538
+ /**
539
+ * Lists every kind at once on the context's daemon, reading the selection before and after so the
540
+ * listings describe one daemon. A step that fails, writes to stderr or prints a row `parse` rejects
541
+ * fails the whole listing closed.
542
+ */
543
+ async function listOnSelectedDaemon(input, executable, env, execute, owner, kinds, parse) {
544
+ const planned = await selectDocker(input, executable, env, execute);
545
+ if (planned === undefined)
546
+ return undefined;
547
+ const selection = planned.value;
548
+ const listed = await Promise.all(kinds.map(async (kind) => {
549
+ const step = exactStep(`${owner === null ? 'stacks' : 'runtime'}:discover:${kind}`, [executable.canonicalPath, ...runtimeCommand(kind, selection, owner)], executable, input.cwd, env, input.timeoutMs, [], {
550
+ ...owner === null ? {} : { stackId: owner.stackId, cockpitId: owner.cockpitId },
551
+ resourceKind: kind, dockerContext: selection.context, dockerEndpoint: selection.endpoint,
552
+ }, [], OUTPUT_LIMIT);
553
+ const outcome = step === undefined ? undefined : cleanOutput(await execute(step));
554
+ const text = outcome === undefined ? undefined : utf8(outcome.stdout);
555
+ const rows = text === undefined ? undefined : parse(text, kind);
556
+ return rows === undefined ? undefined : { value: rows, startedAt: outcome.receipt.startedAt, finishedAt: outcome.receipt.finishedAt };
557
+ }));
558
+ if (listed.some((value) => value === undefined))
559
+ return undefined;
560
+ const lists = listed;
561
+ const listedAt = lists.map(({ finishedAt }) => finishedAt).sort(lexical).at(-1);
562
+ const reselected = await selectDocker(input, executable, env, execute);
563
+ if (reselected === undefined || !sameSelection(reselected.value, selection)
564
+ || Date.parse(reselected.startedAt) < Date.parse(listedAt))
565
+ return undefined;
566
+ return { selection, lists, listedAt, reselected };
542
567
  }
543
568
  /** Runs one kind's inspection steps; an oversized object names its chunk instead of failing anonymously. */
544
569
  async function runInspections(steps, kind, execute) {
@@ -569,7 +594,7 @@ async function inspectImages(runtime, images, execute) {
569
594
  return undefined;
570
595
  for (const [position, record] of rows.entries()) {
571
596
  const resourceId = ids[position];
572
- const observedLabels = exactKeys(record, ['id', 'labels']) ? dockerLabels(record.labels) : undefined;
597
+ const observedLabels = exactKeys(record, ['id', 'labels']) ? dockerLabels(record.labels, listedLabels()) : undefined;
573
598
  if (record.id !== resourceId || observedLabels === undefined || labels.has(resourceId))
574
599
  return undefined;
575
600
  labels.set(resourceId, observedLabels);
@@ -678,23 +703,14 @@ function sameSelection(left, right) {
678
703
  async function discoverReclaimRound(input, executable, env, execute) {
679
704
  // The selection is read before and after the listings, and again after the inspections, so the
680
705
  // context's endpoint cannot have moved to another daemon while this stack was being read.
681
- const planned = await selectDocker(input, executable, env, execute);
682
- if (planned === undefined)
706
+ const listed = await listOnSelectedDaemon(input, executable, env, execute, { stackId: input.stackId, cockpitId: input.cockpitId }, DISCOVERY_KINDS, stackRows);
707
+ if (listed === undefined || Date.parse(listed.reselected.finishedAt) - Date.parse(listed.listedAt) > 30_000)
683
708
  return undefined;
709
+ const { lists, listedAt, selection } = listed;
684
710
  const runtime = {
685
711
  executable, cwd: input.cwd, env, timeoutMs: input.timeoutMs,
686
- stackId: input.stackId, cockpitId: input.cockpitId, selection: planned.value,
712
+ stackId: input.stackId, cockpitId: input.cockpitId, selection,
687
713
  };
688
- const listed = await Promise.all(DISCOVERY_KINDS.map((kind) => listResources(runtime, kind, execute)));
689
- if (listed.some((value) => value === undefined))
690
- return undefined;
691
- const lists = listed;
692
- const listedAt = lists.map(({ finishedAt }) => finishedAt).sort(lexical).at(-1);
693
- const current = await selectDocker(input, executable, env, execute);
694
- if (current === undefined || !sameSelection(current.value, planned.value)
695
- || Date.parse(current.startedAt) < Date.parse(listedAt)
696
- || Date.parse(current.finishedAt) - Date.parse(listedAt) > 30_000)
697
- return undefined;
698
714
  const [containers, networks, volumes, images, buildCache] = lists.map(({ value }) => value);
699
715
  const inspect = boundedExecutor(execute, MAX_CONCURRENT_INSPECTIONS);
700
716
  const inspected = await Promise.all([
@@ -718,10 +734,10 @@ async function discoverReclaimRound(input, executable, env, execute) {
718
734
  if (Date.parse(finishedAt) - Date.parse(startedAt) > 30_000)
719
735
  return undefined;
720
736
  const confirmed = await selectDocker(input, executable, env, execute);
721
- if (confirmed === undefined || !sameSelection(confirmed.value, planned.value)
737
+ if (confirmed === undefined || !sameSelection(confirmed.value, selection)
722
738
  || Date.parse(confirmed.startedAt) < Date.parse(finishedAt))
723
739
  return undefined;
724
- return { observedAt: finishedAt, producer: 'runtime-resource-discovery', resources, startedAt, stackId: input.stackId, selection: planned.value };
740
+ return { observedAt: finishedAt, producer: 'runtime-resource-discovery', resources, startedAt, stackId: input.stackId, selection };
725
741
  }
726
742
  /**
727
743
  * Lists and inspects this stack's runtime resources on the selected Docker context. Discovery
@@ -741,6 +757,95 @@ export async function discoverReclaim(input, execute) {
741
757
  return await discoverReclaimRound(input, executable, env, execute)
742
758
  ?? await discoverReclaimRound(input, executable, env, execute);
743
759
  }
760
+ const MACHINE_KINDS = ['container', 'network', 'volume'];
761
+ const COMPOSE_WORKING_DIR = 'com.docker.compose.project.working_dir';
762
+ const MACHINE_LABELS = [...Object.values(REQUIRED_LABELS), COMPOSE_LABELS.project, COMPOSE_WORKING_DIR];
763
+ const RUNNING_STATES = new Set(['running', 'restarting', 'paused']);
764
+ // Each removal names its resources in argv, which a step bounds; a chunk also keeps the ids Docker
765
+ // echoes on stdout within the step's default output cap.
766
+ const REMOVAL_CHUNK = 128;
767
+ function machineRows(text, kind) {
768
+ const resources = [];
769
+ let unattributed = 0;
770
+ for (const line of text.split('\n')) {
771
+ if (line.length === 0)
772
+ continue;
773
+ // Unparseable docker output is unproven; the discovery fails closed.
774
+ const row = tryParseJson(line);
775
+ if (row === null || typeof row !== 'object' || Array.isArray(row) || !exactKeys(row, ['id', 'name', 'state', 'labels']))
776
+ return undefined;
777
+ const { id, name, state, labels: listed } = row;
778
+ // A working directory is a path, so its label may run far past a stack's own label bound.
779
+ const labels = listed !== null && typeof listed === 'object' && exactKeys(listed, MACHINE_LABELS) ? dockerLabels(listed, MACHINE_LABELS, 4_096) : undefined;
780
+ if (typeof id !== 'string' || !RESOURCE_ID.test(id) || typeof name !== 'string' || name.length === 0 || name.length > 512
781
+ || typeof state !== 'string' || state.length > 64 || labels === undefined)
782
+ return undefined;
783
+ const stack = labels[REQUIRED_LABELS.stack] ?? '';
784
+ const owner = labels[REQUIRED_LABELS.owner] ?? '';
785
+ if (!STACK_ID.test(stack) || !COCKPIT_ID.test(owner) || !SHA256.test(labels[REQUIRED_LABELS.config] ?? '')) {
786
+ unattributed += 1;
787
+ continue;
788
+ }
789
+ const workingDir = labels[COMPOSE_WORKING_DIR];
790
+ resources.push({
791
+ kind, id, name, cockpitId: owner, stackId: stack,
792
+ running: kind === 'container' && RUNNING_STATES.has(state),
793
+ composeProject: labels[COMPOSE_LABELS.project] ?? null,
794
+ workingDir: kind === 'container' && workingDir !== undefined && isAbsolute(workingDir) ? normalize(workingDir) : null,
795
+ });
796
+ }
797
+ return { resources, unattributed };
798
+ }
799
+ /**
800
+ * Lists every container, network and volume on the selected daemon that carries cockpit's stack
801
+ * label, whichever cockpit or checkout started it, for the machine-wide stack inventory. Ownership
802
+ * comes from the labels alone; nothing is attributed by its name. The selection is read before and
803
+ * after the listings, as `discoverReclaim` does, so the listings describe one daemon.
804
+ */
805
+ export async function discoverMachineStacks(input, execute) {
806
+ const executable = resolvedDockerExecutable(input.executable);
807
+ const env = safeEnvironment(input.env);
808
+ if (executable === undefined || env === undefined || !DOCKER_SELECTOR.test(input.context)
809
+ || input.host !== undefined && (admitLocalDockerEndpoint(input.host) === undefined || input.context !== 'default')
810
+ || !validExecutionInput(executable, input.cwd, input.env, input.timeoutMs))
811
+ return undefined;
812
+ const listed = await listOnSelectedDaemon(input, executable, env, execute, null, MACHINE_KINDS, machineRows);
813
+ if (listed === undefined)
814
+ return undefined;
815
+ const resources = listed.lists.flatMap(({ value }) => value.resources);
816
+ if (resources.length > MAX_RESOURCES)
817
+ return undefined;
818
+ return {
819
+ selection: listed.selection, observedAt: listed.listedAt,
820
+ resources: resources.sort((left, right) => lexical(`${left.kind}:${left.id}`, `${right.kind}:${right.id}`)),
821
+ unattributed: listed.lists.reduce((sum, { value }) => sum + value.unattributed, 0),
822
+ };
823
+ }
824
+ /**
825
+ * The Docker removals prune runs for one kind of one orphaned stack's resources, as the machine
826
+ * discovery attributed them: one client per chunk rather than one per resource. Undefined when the
827
+ * resources are not all of one kind and one stack, or a step cannot be built.
828
+ */
829
+ export function machineStackRemovalSteps(input, selection, resources) {
830
+ const executable = resolvedDockerExecutable(input.executable);
831
+ const env = safeEnvironment(input.env);
832
+ const [first] = resources;
833
+ if (first === undefined || executable === undefined || env === undefined || admitLocalDockerEndpoint(selection.endpoint) === undefined
834
+ || resources.some(({ kind, stackId, cockpitId, id }) => kind !== first.kind || stackId !== first.stackId || cockpitId !== first.cockpitId || !RESOURCE_ID.test(id))
835
+ || !validExecutionInput(executable, input.cwd, input.env, input.timeoutMs))
836
+ return undefined;
837
+ const command = first.kind === 'container' ? ['container', 'rm', '--force'] : [first.kind, 'rm'];
838
+ const removals = [];
839
+ for (let start = 0; start < resources.length; start += REMOVAL_CHUNK) {
840
+ const chunk = resources.slice(start, start + REMOVAL_CHUNK);
841
+ const ids = chunk.map(({ id }) => id);
842
+ const step = exactStep(`stacks:prune:${first.kind}:${digestValue(ids).slice(0, 16)}`, [executable.canonicalPath, '--host', selection.endpoint, ...command, '--', ...ids], executable, input.cwd, env, input.timeoutMs, [], { stackId: first.stackId, cockpitId: first.cockpitId, resourceKind: first.kind, resourceCount: String(ids.length) });
843
+ if (step === undefined)
844
+ return undefined;
845
+ removals.push({ step, resources: chunk });
846
+ }
847
+ return removals;
848
+ }
744
849
  function refusal(code, message, target, fix) {
745
850
  return { kind: 'refusal', diagnostics: [diagnostic(code, message, target, fix)] };
746
851
  }
@@ -86,8 +86,8 @@ export declare function observeRuntimeExecution(input: {
86
86
  readonly plan: ExecutionPlan;
87
87
  readonly outcome: ExecuteProbeOutcome;
88
88
  }): readonly EnvironmentObservation[];
89
- export type RuntimeOperation = 'validate' | 'up' | 'down';
90
- export interface RuntimeAdapterInput {
89
+ type RuntimeOperation = 'validate' | 'up' | 'down';
90
+ interface RuntimeAdapterInput {
91
91
  runtime: {
92
92
  name: 'docker';
93
93
  version: string;
@@ -108,7 +108,7 @@ export interface RuntimeAdapterInput {
108
108
  coldStartLimitMillis?: number;
109
109
  expectedMountSources?: readonly string[];
110
110
  }
111
- export interface RuntimePlanResult {
111
+ interface RuntimePlanResult {
112
112
  runtime: 'docker';
113
113
  operation: RuntimeOperation;
114
114
  projectName: string;
@@ -120,7 +120,7 @@ export interface ComposeArtifact {
120
120
  sha256: string;
121
121
  }
122
122
  /** Each `[[sibling]] var` and the absolute sibling directory it names. */
123
- export type SiblingDirectoryVariables = Readonly<Record<string, string>>;
123
+ type SiblingDirectoryVariables = Readonly<Record<string, string>>;
124
124
  export type RuntimeAdapterAuthority =
125
125
  /** A stack's own Compose files, which interpolate the lease, the sibling directories and their own defaults. */
126
126
  Readonly<{
@@ -133,7 +133,7 @@ Readonly<{
133
133
  /** The numeric core of a version Docker or Compose printed, or undefined when it has none. */
134
134
  export declare function observedVersionCore(value: string): string | undefined;
135
135
  export declare function classifyResolvedRuntimeLiterals(values: readonly string[]): 'resolved' | 'unresolved' | 'unsafe';
136
- export type MachineFloorPlanInput = {
136
+ type MachineFloorPlanInput = {
137
137
  readonly runtimeExecutable: ResolvedExecutable;
138
138
  readonly diskExecutable: ResolvedExecutable;
139
139
  readonly cwd: string;
@@ -146,7 +146,7 @@ export type MachineFloorPlanInput = {
146
146
  readonly timeoutMs: number;
147
147
  readonly authority?: MachineFloorAuthority;
148
148
  };
149
- export interface MachineFloorThresholds {
149
+ interface MachineFloorThresholds {
150
150
  readonly memoryBytes: number;
151
151
  readonly cpuMillis: number;
152
152
  readonly freeDiskBytes: number;
@@ -172,14 +172,14 @@ export declare function issueMachineFloorAuthority(input: {
172
172
  checkout: ConsumedSetupCheckoutObservation;
173
173
  }> | undefined;
174
174
  export declare function isMachineFloorEvidence(value: unknown): value is MachineFloorEvidence;
175
- export interface DockerRuntimeObservationInput {
175
+ interface DockerRuntimeObservationInput {
176
176
  readonly executable: ResolvedExecutable;
177
177
  readonly cwd: string;
178
178
  readonly env: Readonly<Record<string, string>>;
179
179
  readonly timeoutMs: number;
180
180
  }
181
- export type RuntimeIdentity = 'docker-desktop' | 'orbstack' | 'colima' | 'rancher-desktop' | 'docker-engine';
182
- export interface DockerInfoIdentityInput {
181
+ type RuntimeIdentity = 'docker-desktop' | 'orbstack' | 'colima' | 'rancher-desktop' | 'docker-engine';
182
+ interface DockerInfoIdentityInput {
183
183
  readonly operatingSystem: string;
184
184
  readonly name: string;
185
185
  readonly osType: string;
@@ -196,7 +196,7 @@ export interface DockerInfoIdentityInput {
196
196
  * been probed on a host here.
197
197
  */
198
198
  export declare function runtimeIdentity(input: DockerInfoIdentityInput): RuntimeIdentity | null;
199
- export type DockerRuntimeObservation = Readonly<{
199
+ type DockerRuntimeObservation = Readonly<{
200
200
  kind: 'complete';
201
201
  runtime: {
202
202
  readonly name: 'docker';
@@ -223,7 +223,7 @@ export declare function issueRuntimeMeasurementProbeAuthority(observation: Docke
223
223
  export declare function runtimeMeasurementProbeObservation(authority: RuntimeMeasurementProbeAuthority, executableSha256: string): Extract<DockerRuntimeObservation, {
224
224
  readonly kind: 'complete';
225
225
  }> | undefined;
226
- export type RuntimeMeasurementAdmission = Readonly<{
226
+ type RuntimeMeasurementAdmission = Readonly<{
227
227
  kind: 'admitted';
228
228
  runtimeIdentity: string;
229
229
  measurement: string;
@@ -239,7 +239,7 @@ export declare function versionAtLeast(actual: string, floor: string): boolean;
239
239
  export declare function admitRuntimeMeasurement(observation: DockerRuntimeObservation, measurements: LocalRuntimeMeasurements): RuntimeMeasurementAdmission;
240
240
  export declare function observeRuntimeMeasurement(admission: RuntimeMeasurementAdmission, stackId: StackId): EnvironmentObservation;
241
241
  export declare function observeDockerRuntime(rawInput: DockerRuntimeObservationInput, execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): Promise<DockerRuntimeObservation>;
242
- export type MachineFloorMeasurement = {
242
+ type MachineFloorMeasurement = {
243
243
  readonly kind: 'complete';
244
244
  readonly evidence: MachineFloorEvidence;
245
245
  } | MachineFloorRefusal | {
@@ -255,7 +255,7 @@ export interface RuntimeObservationWindow {
255
255
  readonly startedAt: string;
256
256
  readonly expiresAt: string;
257
257
  }
258
- export interface RuntimeTopologyService {
258
+ interface RuntimeTopologyService {
259
259
  readonly id: string;
260
260
  readonly dependsOn: readonly string[];
261
261
  readonly bindMountSources: readonly string[];
@@ -278,7 +278,7 @@ export interface RuntimeTopologyEvidence {
278
278
  readonly observationWindow: RuntimeObservationWindow;
279
279
  readonly services: readonly RuntimeTopologyService[];
280
280
  }
281
- export type RuntimeTopologyObservation = {
281
+ type RuntimeTopologyObservation = {
282
282
  readonly kind: 'complete';
283
283
  readonly step: ExecutionStep;
284
284
  readonly topology: RuntimeTopologyEvidence;
@@ -287,6 +287,7 @@ export type RuntimeTopologyObservation = {
287
287
  readonly kind: 'unavailable';
288
288
  readonly reason: string;
289
289
  readonly mounts?: RuntimeMountMismatch;
290
+ readonly undefinedProfiles?: readonly string[];
290
291
  };
291
292
  export declare const OBSERVATION_WINDOW_HEADROOM_MS = 300000;
292
293
  /**
@@ -309,7 +310,7 @@ export type RuntimeMountMismatch = Readonly<{
309
310
  export declare function isRuntimeTopologyEvidence(value: unknown): value is RuntimeTopologyEvidence;
310
311
  export declare function resolveRuntimeServiceOrder(topology: RuntimeTopologyEvidence): readonly string[];
311
312
  export declare function projectRuntimeExecutionEnvironment(topology: RuntimeTopologyEvidence, namespace?: StateNamespace): Readonly<Record<string, string>> | undefined;
312
- export type RuntimeContainerIdSlot = Readonly<{
313
+ type RuntimeContainerIdSlot = Readonly<{
313
314
  kind: 'runtime-container-id';
314
315
  service: string;
315
316
  }>;
@@ -328,7 +329,7 @@ export interface RuntimeLifecycleExecutionSemantics {
328
329
  readonly filesystemChecks: ExecutionStep['filesystemChecks'];
329
330
  readonly compensation: readonly RuntimeLifecycleExecutionSemantics[];
330
331
  }
331
- export interface RuntimeLifecycleServiceSemantics {
332
+ interface RuntimeLifecycleServiceSemantics {
332
333
  readonly id: string;
333
334
  readonly start: RuntimeLifecycleExecutionSemantics;
334
335
  readonly status: RuntimeLifecycleExecutionSemantics;
@@ -416,6 +417,8 @@ export declare function startRuntimeService(input: {
416
417
  readonly topology: RuntimeTopologyEvidence;
417
418
  readonly service: string;
418
419
  readonly execute: Execute;
420
+ /** Build the service's image and replace its container, as `rebuild` does after its code changed. */
421
+ readonly rebuild?: true;
419
422
  }): Promise<RuntimeServiceOutcome>;
420
423
  /**
421
424
  * Re-reads a service an earlier run started: stale when Compose no longer has the recorded
@@ -427,6 +430,49 @@ export declare function revalidateRuntimeService(input: {
427
430
  readonly expectedContainerId: string;
428
431
  readonly execute: Execute;
429
432
  }): Promise<RuntimeServiceOutcome>;
433
+ /**
434
+ * Restarts one service's container in place with `compose restart`, then waits for it as `up` does
435
+ * and inspects it, so the outcome is the same healthy-or-failed evidence a start gives.
436
+ */
437
+ export declare function restartRuntimeService(input: {
438
+ readonly topology: RuntimeTopologyEvidence;
439
+ readonly service: string;
440
+ readonly execute: Execute;
441
+ }): Promise<RuntimeServiceOutcome>;
442
+ /** One service of the stack as Compose reports it; `state` is `absent` when it has no container. */
443
+ export interface RuntimeServiceState {
444
+ readonly service: string;
445
+ readonly state: string;
446
+ readonly health: string | null;
447
+ readonly status: string | null;
448
+ readonly exitCode: number | null;
449
+ }
450
+ /** Lists every service of the stack with its container's state and health through `compose ps`. */
451
+ export declare function observeRuntimeServiceStates(input: {
452
+ readonly topology: RuntimeTopologyEvidence;
453
+ readonly execute: Execute;
454
+ }): Promise<Readonly<{
455
+ kind: 'complete';
456
+ services: readonly RuntimeServiceState[];
457
+ }> | Readonly<{
458
+ kind: 'unavailable';
459
+ reason: string;
460
+ }>>;
461
+ /**
462
+ * The foreground command that runs `argv` inside one service's container, under the stack's own
463
+ * project, files and profiles, so the caller never names the derived project.
464
+ */
465
+ export declare function planRuntimeServiceExec(input: {
466
+ readonly topology: RuntimeTopologyEvidence;
467
+ readonly service: string;
468
+ readonly argv: readonly [string, ...string[]];
469
+ readonly tty: boolean;
470
+ }): Readonly<{
471
+ executable: ResolvedExecutable;
472
+ argv: readonly string[];
473
+ cwd: string;
474
+ env: Readonly<Record<string, string>>;
475
+ }> | undefined;
430
476
  /** The observation as healthy-service evidence, or undefined when the service is not running healthy. */
431
477
  export declare function admitRuntimeServiceHealth(observation: RuntimeServiceObservation): RuntimeServiceEvidence | undefined;
432
478
  export declare function isRuntimeServiceEvidence(value: unknown): value is RuntimeServiceEvidence;
@@ -467,14 +513,14 @@ export interface RuntimeServiceFailureLogContext {
467
513
  readonly filesystemChecks: ExecutionStep['filesystemChecks'];
468
514
  }
469
515
  export declare function consumeRuntimeServiceFailureForLogs(evidence: RuntimeServiceFailureEvidence): RuntimeServiceFailureLogContext | undefined;
470
- export interface RuntimeServiceFailureContext {
516
+ interface RuntimeServiceFailureContext {
471
517
  readonly observation: RuntimeServiceObservation;
472
518
  readonly executable: ResolvedExecutable;
473
519
  readonly cwd: string;
474
520
  readonly env: Readonly<Record<string, string>>;
475
521
  }
476
522
  export declare function consumeRuntimeServiceObservationForFailureLogs(value: RuntimeServiceObservation): RuntimeServiceFailureContext | undefined;
477
- export type RuntimeStopOutcome =
523
+ type RuntimeStopOutcome =
478
524
  /** Compose stopped the stack and a fresh `ps` found nothing running. */
479
525
  {
480
526
  readonly kind: 'stopped';
@@ -1278,6 +1278,12 @@ function parseTopology(stdout, stackId, configSha256, activeProfiles) {
1278
1278
  profiles.set(name, selected);
1279
1279
  }
1280
1280
  const active = new Set(activeProfiles);
1281
+ // Compose reports only the services of active profiles, so an active profile no reported
1282
+ // service carries is one no service declares; Compose itself accepts it silently.
1283
+ const declaredProfiles = new Set([...profiles.values()].flat());
1284
+ const undefinedProfiles = activeProfiles.filter((profile) => !declaredProfiles.has(profile));
1285
+ if (undefinedProfiles.length > 0)
1286
+ return { undefinedProfiles };
1281
1287
  const names = declaredNames.filter((name) => {
1282
1288
  const serviceProfiles = profiles.get(name) ?? [];
1283
1289
  return serviceProfiles.length === 0 || serviceProfiles.some((profile) => active.has(profile));
@@ -1371,6 +1377,12 @@ function admitRuntimeTopology(contract, step, outcome) {
1371
1377
  const projected = parseTopology(outcome.stdout, contract.stackId, contract.configSha256, contract.runtime.activeProfiles);
1372
1378
  if (!projected)
1373
1379
  return unavailable();
1380
+ if ('undefinedProfiles' in projected)
1381
+ return {
1382
+ kind: 'unavailable',
1383
+ reason: `no Compose service declares profile ${projected.undefinedProfiles.join(', ')}`,
1384
+ undefinedProfiles: projected.undefinedProfiles,
1385
+ };
1374
1386
  const mounts = compareBindSources(projected.services.flatMap(({ bindMountSources }) => bindMountSources), contract.runtime.expectedMountSources);
1375
1387
  // The caller can name the sibling or host path behind each difference; a bare refusal here
1376
1388
  // left `up` reporting only that readiness authority was unavailable.
@@ -1486,7 +1498,7 @@ function runtimeServiceLookupSteps(topologyContract, service, issuanceId) {
1486
1498
  const identity = derivedComposeStep(topologyContract.step, `runtime:docker-compose:identity:${service.id}`, 'ps', ['ps', '--quiet', '--all', service.id], 128, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
1487
1499
  return status && identity ? { status, identity } : undefined;
1488
1500
  }
1489
- function runtimeServiceInitialSteps(topologyContract, service, issuanceId) {
1501
+ function runtimeServiceInitialSteps(topologyContract, service, issuanceId, rebuild = false) {
1490
1502
  // `up --wait` returns only once the service runs and its healthcheck passes, after Compose has
1491
1503
  // built or pulled its image, and the declared cold_start_seconds bounds exactly that. The
1492
1504
  // ordinary command budget does not: a first `up` was killed and reported "did not become
@@ -1495,7 +1507,7 @@ function runtimeServiceInitialSteps(topologyContract, service, issuanceId) {
1495
1507
  // grace; a start without that grace was killed mid-recreate, leaving the replacement container
1496
1508
  // created but never started.
1497
1509
  const start = derivedComposeStep({ ...topologyContract.step, timeoutMs: COMPOSE_STOP_GRACE_MS
1498
- + Math.max(topologyContract.step.timeoutMs, topologyContract.runtime.coldStartLimitMillis) }, `runtime:docker-compose:start:${service.id}`, 'up', ['--progress', 'quiet', 'up', '--detach', '--wait', '--no-deps', service.id], OUTPUT_LIMIT_BYTES, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
1510
+ + Math.max(topologyContract.step.timeoutMs, topologyContract.runtime.coldStartLimitMillis) }, `runtime:docker-compose:start:${service.id}`, 'up', ['--progress', 'quiet', 'up', '--detach', '--wait', '--no-deps', ...rebuild ? ['--build', '--force-recreate'] : [], service.id], OUTPUT_LIMIT_BYTES, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
1499
1511
  // A service that failed its start under a restart policy restarts forever until `down`. The
1500
1512
  // failed `up` stops it and keeps the container, so its logs stay readable and the next `up`
1501
1513
  // starts it again.
@@ -1817,7 +1829,7 @@ function runtimeServiceFailure(contract, stage, outcome, previousFinishedAt, con
1817
1829
  */
1818
1830
  export async function startRuntimeService(input) {
1819
1831
  const contract = runtimeServiceContract(input.topology, input.service);
1820
- const steps = contract && runtimeServiceInitialSteps(contract.topologyContract, contract.service, randomBytes(32).toString('hex'));
1832
+ const steps = contract && runtimeServiceInitialSteps(contract.topologyContract, contract.service, randomBytes(32).toString('hex'), input.rebuild === true);
1821
1833
  if (!contract || !steps)
1822
1834
  return { kind: 'failure' };
1823
1835
  const { projectName } = contract.topologyContract.runtime;
@@ -1881,6 +1893,64 @@ export async function revalidateRuntimeService(input) {
1881
1893
  return { kind: 'stale' };
1882
1894
  return inspectRuntimeService({ ...contract, containerId: observed.containerId, identityFinishedAt: identity.receipt.finishedAt }, input.execute);
1883
1895
  }
1896
+ /**
1897
+ * Restarts one service's container in place with `compose restart`, then waits for it as `up` does
1898
+ * and inspects it, so the outcome is the same healthy-or-failed evidence a start gives.
1899
+ */
1900
+ export async function restartRuntimeService(input) {
1901
+ const contract = runtimeServiceContract(input.topology, input.service);
1902
+ const step = contract && derivedComposeStep({ ...contract.topologyContract.step, timeoutMs: COMPOSE_STOP_GRACE_MS + contract.topologyContract.step.timeoutMs }, `runtime:docker-compose:restart:${contract.service.id}`, 'restart', ['--progress', 'quiet', 'restart', '--no-deps', contract.service.id], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.topologyContract.runtime.interpolatedNames);
1903
+ if (!contract || !step)
1904
+ return { kind: 'failure' };
1905
+ const restarted = await input.execute(step);
1906
+ if (restarted.kind !== 'complete' || restarted.stderr.byteLength !== 0) {
1907
+ return runtimeServiceFailure(contract, 'start', restarted, undefined, null);
1908
+ }
1909
+ return startRuntimeService(input);
1910
+ }
1911
+ /** Lists every service of the stack with its container's state and health through `compose ps`. */
1912
+ export async function observeRuntimeServiceStates(input) {
1913
+ const contract = issuedTopologyEvidence.get(input.topology);
1914
+ const step = contract && derivedComposeStep(contract.step, `runtime:docker-compose:ps:${contract.runtime.projectName}`, 'ps', ['ps', '--format', 'json', '--all'], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.runtime.interpolatedNames);
1915
+ if (!contract || !step)
1916
+ return { kind: 'unavailable', reason: 'the stack topology is no longer current' };
1917
+ const outcome = await input.execute(step);
1918
+ if (outcome.kind !== 'complete')
1919
+ return { kind: 'unavailable', reason: `docker compose ps failed (${outcome.code})` };
1920
+ const reported = new Map();
1921
+ try {
1922
+ for (const line of new TextDecoder('utf-8', { fatal: true }).decode(outcome.stdout).split('\n')) {
1923
+ if (line.trim() === '')
1924
+ continue;
1925
+ const row = JSON.parse(line);
1926
+ if (row.Project !== contract.runtime.projectName || typeof row.Service !== 'string' || typeof row.State !== 'string') {
1927
+ return { kind: 'unavailable', reason: 'docker compose ps reported a container outside this stack or without a state' };
1928
+ }
1929
+ reported.set(row.Service, {
1930
+ service: row.Service, state: row.State,
1931
+ health: typeof row.Health === 'string' && row.Health !== '' ? row.Health : null,
1932
+ status: typeof row.Status === 'string' ? row.Status : null,
1933
+ exitCode: typeof row.ExitCode === 'number' ? row.ExitCode : null,
1934
+ });
1935
+ }
1936
+ }
1937
+ catch (error) {
1938
+ return { kind: 'unavailable', reason: unavailableReason('docker compose ps output is not JSON lines', error) };
1939
+ }
1940
+ return { kind: 'complete', services: [...contract.services.keys()].sort().map(service => reported.get(service)
1941
+ ?? { service, state: 'absent', health: null, status: null, exitCode: null }) };
1942
+ }
1943
+ /**
1944
+ * The foreground command that runs `argv` inside one service's container, under the stack's own
1945
+ * project, files and profiles, so the caller never names the derived project.
1946
+ */
1947
+ export function planRuntimeServiceExec(input) {
1948
+ const contract = runtimeServiceContract(input.topology, input.service);
1949
+ const step = contract && derivedComposeStep(contract.topologyContract.step, `runtime:docker-compose:exec:${contract.service.id}`, 'exec', ['exec', ...input.tty ? [] : ['--no-tty'], contract.service.id, ...input.argv], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.topologyContract.runtime.interpolatedNames);
1950
+ if (!contract || !step)
1951
+ return undefined;
1952
+ return { executable: contract.topologyContract.runtime.runtimeExecutable, argv: step.argv.slice(1), cwd: step.cwd, env: step.env };
1953
+ }
1884
1954
  /** The observation as healthy-service evidence, or undefined when the service is not running healthy. */
1885
1955
  export function admitRuntimeServiceHealth(observation) {
1886
1956
  const contract = issuedRuntimeServiceObservations.get(observation);
@@ -0,0 +1,53 @@
1
+ import type { SurfaceTarget } from '../surfaces/contract.js';
2
+ import { type EnvironmentDiagnostic } from './contract.js';
3
+ export declare function inside(path: string, root: string): boolean;
4
+ /**
5
+ * The directory the OpenCode grant file names: the served checkout when it lies outside the
6
+ * declared one, which the grant would not otherwise reach.
7
+ */
8
+ export declare function openCodeGrantDirectory(declared: string, served: string | undefined): string;
9
+ /** Why a directory cannot go into an OpenCode access pattern, or undefined when it can. */
10
+ export declare function openCodePatternUnsafe(directory: string): string | undefined;
11
+ export interface AgentGrantPlan {
12
+ readonly settings: Readonly<{
13
+ bytes: string | null;
14
+ }> | undefined;
15
+ readonly local: readonly Readonly<{
16
+ name: string;
17
+ bytes: string;
18
+ }>[];
19
+ /** What the tools can reach, for a served checkout outside the declared one. */
20
+ readonly lines: readonly string[];
21
+ readonly report: Readonly<{
22
+ granted: readonly SurfaceTarget[];
23
+ ungranted: readonly SurfaceTarget[];
24
+ }> | undefined;
25
+ }
26
+ type Planned = Readonly<{
27
+ kind: 'complete';
28
+ plan: AgentGrantPlan;
29
+ }> | Readonly<{
30
+ kind: 'refusal';
31
+ diagnostic: EnvironmentDiagnostic;
32
+ }>;
33
+ /**
34
+ * Plans the machine-local grants for one sibling. `served` is the checkout that serves it, or
35
+ * undefined when the sibling is no longer declared; `targets` are the listed tools that hold a
36
+ * committed grant for it. A grant cockpit added earlier for the sibling is taken back when the
37
+ * served checkout no longer needs it.
38
+ */
39
+ export declare function planAgentGrants(input: Readonly<{
40
+ root: string;
41
+ slug: string;
42
+ declared: string | undefined;
43
+ served: string | undefined;
44
+ targets: readonly SurfaceTarget[];
45
+ }>): Promise<Planned>;
46
+ /** Writes a plan: cockpit's own record first, so a grant it added is never one it forgets. */
47
+ export declare function applyAgentGrants(root: string, plan: AgentGrantPlan): Promise<Readonly<{
48
+ kind: 'complete';
49
+ } | {
50
+ kind: 'refusal';
51
+ diagnostic: EnvironmentDiagnostic;
52
+ }>>;
53
+ export {};