@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
@@ -64,6 +64,48 @@ export async function runResetOperatorApplication(input, admission, adapters) {
64
64
  await opened.decide('decline');
65
65
  return { status: EXIT_CODES.cancelled, diagnostics: [], result: null };
66
66
  }
67
+ function pruneUnconfirmed(message) {
68
+ return { status: 2, result: null, diagnostics: [{ severity: 'error', target: 'stacks',
69
+ code: 'STACKS_PRUNE_UNCONFIRMED',
70
+ message, fix: 'a person runs npx --no cockpit stacks prune --config <your cockpit.toml> --interactive in a terminal outside CI, reads the preview and types its digest' }] };
71
+ }
72
+ /**
73
+ * `stacks prune`: a preview by default. Removing an orphan whose checkout is gone is housekeeping
74
+ * anyone can undo by bringing the stack up again, so `--apply` passes for an agent; a prune that
75
+ * deletes volumes deletes data, and passes only where `[autonomy] reset` lets this run reset. That
76
+ * policy is this cockpit's, so an agent's `--apply` covers only this cockpit's stacks: another
77
+ * cockpit may require a person for the same removal. `--interactive` is a person's approval of the
78
+ * exact preview by its digest, and covers every stack on the machine.
79
+ */
80
+ export async function runStacksPruneOperatorApplication(input, admission, adapters) {
81
+ if (admission.interactive && (admission.json || admission.ci && admission.ci.length > 0)) {
82
+ return pruneUnconfirmed('interactive prune requires a human terminal outside CI and JSON modes');
83
+ }
84
+ if (admission.interactive && !admission.terminal)
85
+ return pruneUnconfirmed('interactive prune requires terminal input');
86
+ const opened = await adapters.begin({ configPath: input.configPath, scope: !admission.interactive && admission.apply ? 'this-cockpit' : 'machine' });
87
+ if (!('apply' in opened))
88
+ return opened;
89
+ if (!admission.interactive && !admission.apply || opened.empty)
90
+ return opened.preview;
91
+ if (admission.interactive) {
92
+ if (await adapters.confirm(opened.preview) !== opened.digest)
93
+ return pruneUnconfirmed('the typed digest did not match the prune preview; nothing was removed');
94
+ return opened.apply();
95
+ }
96
+ if (opened.deletesData) {
97
+ const policy = await adapters.policy(input.configPath);
98
+ if (!isPolicy(policy))
99
+ return policy;
100
+ const weakened = policy.recorded ? policyWeakening(policy.recorded, policy.current).filter(row => row.field === 'autonomy.reset') : [];
101
+ if (weakened.length)
102
+ return weakeningRefusal(weakened, 'nothing was pruned');
103
+ const refused = autonomyRefusal('reset', policy.current.autonomy.reset, admission);
104
+ if (refused)
105
+ return refused;
106
+ }
107
+ return opened.apply();
108
+ }
67
109
  function staleRemovalRefusal(message) {
68
110
  return refusal('GENERATED_STALE_APPROVAL_REQUIRED', message, 'generated-environment');
69
111
  }
@@ -0,0 +1,4 @@
1
+ import { type Stats } from 'node:fs';
2
+ /** The path's own status, not following a final symbolic link, or undefined when nothing is there. */
3
+ export declare function lstatIfPresent(path: string): Promise<Stats | undefined>;
4
+ export declare function lstatIfPresentSync(path: string): Stats | undefined;
@@ -0,0 +1,26 @@
1
+ // What is on disk at a path, where "nothing there" (a missing entry or a missing parent) is an answer
2
+ // and any other failure, such as a permission error, is raised for its caller to name.
3
+ import { lstatSync } from 'node:fs';
4
+ import { lstat } from 'node:fs/promises';
5
+ import { isMissing } from './contract.js';
6
+ /** The path's own status, not following a final symbolic link, or undefined when nothing is there. */
7
+ export async function lstatIfPresent(path) {
8
+ try {
9
+ return await lstat(path);
10
+ }
11
+ catch (error) {
12
+ if (isMissing(error))
13
+ return undefined;
14
+ throw error;
15
+ }
16
+ }
17
+ export function lstatIfPresentSync(path) {
18
+ try {
19
+ return lstatSync(path);
20
+ }
21
+ catch (error) {
22
+ if (isMissing(error))
23
+ return undefined;
24
+ throw error;
25
+ }
26
+ }
@@ -1,9 +1,11 @@
1
1
  import type { ExecuteProbeOutcome } from '../environment-exec/execute.js';
2
2
  import { type RuntimeEvidenceObservation } from './application/health.js';
3
3
  import { type LogsCommandAdapters } from './application/logs.js';
4
+ import { type ServiceCommandAdapters } from './application/services.js';
5
+ import type { ForegroundProcessInput, ForegroundProcessOutcome } from '../environment-exec/execute.js';
4
6
  import { type ExecutionStep } from './contract.js';
5
7
  import { type RuntimeMeasurementProbeAuthority } from './runtime.js';
6
- export interface ReadCommandCompositionInput {
8
+ interface ReadCommandCompositionInput {
7
9
  readonly configPath: string;
8
10
  readonly env: Readonly<Record<string, string | undefined>>;
9
11
  readonly sessionId: string;
@@ -12,8 +14,9 @@ export interface ReadCommandCompositionInput {
12
14
  readonly signal?: AbortSignal;
13
15
  readonly runtimeMeasurementProbeAuthority?: RuntimeMeasurementProbeAuthority;
14
16
  }
15
- export interface ReadCommandAdapterComposition {
17
+ interface ReadCommandAdapterComposition {
16
18
  readonly logs: LogsCommandAdapters;
19
+ readonly services: ServiceCommandAdapters;
17
20
  readonly environment: Readonly<{
18
21
  observe: (input: {
19
22
  readonly configPath: string;
@@ -23,5 +26,7 @@ export interface ReadCommandAdapterComposition {
23
26
  type Executor = (step: ExecutionStep, options?: {
24
27
  readonly signal?: AbortSignal;
25
28
  }) => Promise<ExecuteProbeOutcome>;
26
- export declare function createReadCommandAdapters(rawInput: ReadCommandCompositionInput, execute: Executor): ReadCommandAdapterComposition;
29
+ export declare function createReadCommandAdapters(rawInput: ReadCommandCompositionInput, execute: Executor, executeForeground?: (input: ForegroundProcessInput, options: {
30
+ readonly signal?: AbortSignal;
31
+ }) => Promise<ForegroundProcessOutcome>): ReadCommandAdapterComposition;
27
32
  export {};
@@ -6,9 +6,12 @@ import {} from './application/health.js';
6
6
  import { decodeLifecycleRecord } from './application/lifecycle.js';
7
7
  import {} from './application/lifecycle-context.js';
8
8
  import {} from './application/logs.js';
9
+ import { publishLifecycleRecord } from './application/lifecycle-state.js';
10
+ import {} from './application/services.js';
9
11
  import { createLifecycleCommandAdapters } from './lifecycle-composition.js';
10
12
  import { deriveEntrypoints, observeRetainedPortFamily, projectRetainedPortFamilyResult } from './ports.js';
11
13
  import { DOCKER_CLIENT_ENVIRONMENT, ENVIRONMENT_CODES, RefusalError } from './contract.js';
14
+ import { loadServedSiblings, servedSiblingVariables } from './sibling-checkouts.js';
12
15
  import { DeclarationRefusalError, runtimeNotDeclared, serviceNotDeclared } from './declaration.js';
13
16
  import { admitRuntimeServiceHealth, revalidateRuntimeService, } from './runtime.js';
14
17
  import { resolveDockerExecutable } from './executable.js';
@@ -133,11 +136,13 @@ function exactCompletedTopology(context) {
133
136
  return topologyServices.length === completedServices.length
134
137
  && topologyServices.every((service, index) => service === completedServices[index]);
135
138
  }
136
- export function createReadCommandAdapters(rawInput, execute) {
139
+ export function createReadCommandAdapters(rawInput, execute, executeForeground) {
137
140
  const input = captureInput(rawInput);
138
141
  // `refuse` receives the lifecycle's own refusal, such as an unmeasured runtime, so a caller can
139
142
  // report it instead of a generic unavailable context.
140
- const prepare = async (signal, refuse) => {
143
+ // A service verb may act on a stack whose start stopped part-way: rebuilding the service that
144
+ // failed is how its fixed code gets running. Every other reader needs a started service.
145
+ const prepare = async (signal, refuse, partial = false) => {
141
146
  if (!input || typeof execute !== 'function')
142
147
  throw new RefusalError('read command composition is unavailable');
143
148
  const lifecycle = createLifecycleCommandAdapters({
@@ -191,7 +196,11 @@ export function createReadCommandAdapters(rawInput, execute) {
191
196
  if (lifecycleRead.kind !== 'complete')
192
197
  throw new RefusalError('lifecycle state is unavailable');
193
198
  const lifecycleValue = decodeLifecycleRecord(lifecycleRead.value, context)?.value;
194
- if (!lifecycleValue || lifecycleValue.completedServices.length === 0)
199
+ if (!lifecycleValue)
200
+ throw new RefusalError('lifecycle state is malformed');
201
+ if (lifecycleValue.teardownComplete)
202
+ throw new RefusalError('down removed the stack');
203
+ if (lifecycleValue.completedServices.length === 0 && !partial)
195
204
  throw new RefusalError('lifecycle state is malformed');
196
205
  if (lifecycleValue.holderId !== context.holderId)
197
206
  throw new RefusalError('lifecycle holder authority changed');
@@ -207,17 +216,19 @@ export function createReadCommandAdapters(rawInput, execute) {
207
216
  if (!confirmedComposeFiles || JSON.stringify(confirmedComposeFiles) !== JSON.stringify(composeFiles)) {
208
217
  throw new RefusalError('compose artifacts changed during runtime observation');
209
218
  }
219
+ // The stack interpolates the checkout `use` recorded, so reading it must name the same one.
220
+ const served = await loadServedSiblings(checkoutRoot, configuration.config);
221
+ if (served.kind === 'refusal')
222
+ throw new RefusalError(served.diagnostic.message);
210
223
  const ownComposeAuthority = {
211
224
  ownComposeFiles: true,
212
225
  cockpitId: context.cockpitId,
213
226
  stackId: context.stackId,
214
227
  portLeaseProjection: ports,
215
- siblingDirectories: Object.fromEntries(configuration.config.sibling.map((sibling) => [
216
- sibling.var,
217
- resolve(checkoutRoot, configuration.config.cockpit.siblings_root, sibling.dir),
218
- ])),
228
+ siblingDirectories: servedSiblingVariables(configuration.config, served.siblings),
219
229
  };
220
230
  return {
231
+ run: context,
221
232
  ownComposeAuthority,
222
233
  configuration,
223
234
  namespace: context.namespace,
@@ -343,10 +354,44 @@ export function createReadCommandAdapters(rawInput, execute) {
343
354
  };
344
355
  return { snapshot: ownedSnapshot, evidence: ownedEvidence };
345
356
  };
357
+ const observeServices = async (request) => {
358
+ if (!input || resolve(request.configPath) !== input.configPath)
359
+ throw new RefusalError('service configuration path does not match');
360
+ const context = await prepare(input.signal, (diagnostics) => { throw new DeclarationRefusalError(diagnostics[0]); }, true);
361
+ return {
362
+ topology: context.topology,
363
+ completedServices: context.lifecycle.completedServices,
364
+ // The recorded container is what `up`, `preflight` and `logs` revalidate, so a rebuild that
365
+ // replaced it updates the record rather than leaving the stack looking broken.
366
+ recordContainer: async (service, containerId) => {
367
+ const value = { ...context.lifecycle, completedServices: context.lifecycle.completedServices
368
+ .map((row) => row.service === service ? { service, containerId } : row) };
369
+ const published = await publishLifecycleRecord(context.run, context.lifecycleRecord, value);
370
+ if ('status' in published)
371
+ return published.diagnostics;
372
+ if (published.kind === 'complete')
373
+ return [];
374
+ return [{ severity: 'error',
375
+ code: published.kind === 'missing' ? ENVIRONMENT_CODES.STATE_RECORD_MALFORMED : published.code,
376
+ target: context.namespace.paths.lifecycle(context.stackId).path,
377
+ message: `the rebuilt container of service ${service} runs, and the stack record could not be updated to it${published.kind === 'missing' ? '' : `: ${published.message}`}`,
378
+ fix: 'run npx --no cockpit up --config <your cockpit.toml> to record the running containers again' }];
379
+ },
380
+ };
381
+ };
382
+ const services = {
383
+ observe: observeServices,
384
+ execute: (step) => execute(step, input?.signal ? { signal: input.signal } : undefined),
385
+ executeForeground: async (planned) => {
386
+ if (!executeForeground)
387
+ throw new RefusalError('foreground execution is not bound');
388
+ return executeForeground(planned, input?.signal ? { signal: input.signal } : {});
389
+ },
390
+ };
346
391
  const logs = {
347
392
  observe: observeLogs,
348
393
  execute: (step, options) => execute(step, options),
349
394
  ...(input?.signal ? { signal: input.signal } : {}),
350
395
  };
351
- return { logs, environment: { observe: observeEnvironment } };
396
+ return { logs, services, environment: { observe: observeEnvironment } };
352
397
  }
@@ -2,7 +2,7 @@ import { loadCockpitConfig } from '../config/load.js';
2
2
  import type { ExecuteProbeOutcome } from '../environment-exec/execute.js';
3
3
  import { type EnvironmentDiagnostic, type ExecutionStep } from './contract.js';
4
4
  import type { ReadinessReport } from '../readiness.js';
5
- export interface ProjectReadinessInput {
5
+ interface ProjectReadinessInput {
6
6
  /** The cockpit root, where cockpit.toml is or will be written. */
7
7
  readonly root: string;
8
8
  /** Present once cockpit.toml exists; its siblings are the repositories asked. */
@@ -35,4 +35,23 @@ export declare function inspectInitReadiness(input: Omit<ProjectReadinessInput,
35
35
  state: ReadinessReport['state'];
36
36
  findings: readonly EnvironmentDiagnostic[];
37
37
  }>>;
38
+ /**
39
+ * What the container runtime on this machine offers, measured by the same collector preflight
40
+ * compares the machine floor with, so init can propose a floor where no Compose limit records one.
41
+ */
42
+ export declare function measureInitMachine(input: Readonly<{
43
+ root: string;
44
+ }>, binding: Readonly<{
45
+ env: ProjectReadinessInput['env'];
46
+ timeoutMs: number;
47
+ execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
48
+ }>): Promise<Readonly<{
49
+ kind: 'complete';
50
+ memoryMb: number;
51
+ cpus: number;
52
+ observedAt: string;
53
+ }> | Readonly<{
54
+ kind: 'unavailable';
55
+ reason: string;
56
+ }>>;
38
57
  export {};
@@ -9,8 +9,9 @@ import { loadCockpitConfig } from '../config/load.js';
9
9
  import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
10
10
  import { inspectTeamReadiness } from '../team-metadata.js';
11
11
  import { guardSearchPathRelativeEntry } from '../guards/protected-locations.js';
12
- import { ENVIRONMENT_CODES, snapshotExecutionStep, RefusalError, isMissing } from './contract.js';
13
- import { resolveGitExecutable } from './executable.js';
12
+ import { DOCKER_CLIENT_ENVIRONMENT, ENVIRONMENT_CODES, snapshotExecutionStep, RefusalError, isMissing } from './contract.js';
13
+ import { resolveDiskCapacityExecutable, resolveDockerExecutable, resolveGitExecutable } from './executable.js';
14
+ import { measureMachineFloor } from './runtime.js';
14
15
  import { gitRepositoryRefusal, inspectTrackedSecretFiles } from './git-observation.js';
15
16
  import { declaresNothingToRun } from './declaration.js';
16
17
  import { inspectSiblingInstructions } from './sibling-instructions.js';
@@ -128,3 +129,23 @@ export async function inspectInitReadiness(input, binding) {
128
129
  : classification === undefined ? 'undetermined' : classification === 'no-runnable-product' ? 'surfaces-only' : 'runtime';
129
130
  return { state, findings };
130
131
  }
132
+ /**
133
+ * What the container runtime on this machine offers, measured by the same collector preflight
134
+ * compares the machine floor with, so init can propose a floor where no Compose limit records one.
135
+ */
136
+ export async function measureInitMachine(input, binding) {
137
+ const [docker, disk] = await Promise.all([resolveDockerExecutable(binding.env), resolveDiskCapacityExecutable()]);
138
+ if (docker.state !== 'available')
139
+ return { kind: 'unavailable', reason: docker.reason };
140
+ if (disk.state !== 'available')
141
+ return { kind: 'unavailable', reason: disk.reason };
142
+ const env = Object.fromEntries(Object.entries(binding.env)
143
+ .filter((entry) => DOCKER_CLIENT_ENVIRONMENT.has(entry[0]) && typeof entry[1] === 'string'));
144
+ const measured = await measureMachineFloor({ runtimeExecutable: docker.value, diskExecutable: disk.value, cwd: input.root,
145
+ env, timeoutMs: binding.timeoutMs }, binding.execute);
146
+ if (measured.kind !== 'complete')
147
+ return { kind: 'unavailable', reason: measured.kind === 'refusal'
148
+ ? measured.diagnostics.map(row => row.message).join('; ') : 'docker info did not report the runtime\'s memory and CPUs' };
149
+ return { kind: 'complete', memoryMb: Math.floor(measured.evidence.memoryBytes / 1024 ** 2),
150
+ cpus: measured.evidence.cpuMillis / 1_000, observedAt: measured.evidence.observedAt };
151
+ }
@@ -4,7 +4,7 @@ import { type CockpitId, type EnvironmentDiagnostic, type ExecutionFence, type E
4
4
  import type { ResolvedExecutable } from './executable.js';
5
5
  import { type StateAddress, type StateRecord } from './state.js';
6
6
  type Executor = (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
7
- export interface DockerSelection {
7
+ interface DockerSelection {
8
8
  readonly context: string;
9
9
  readonly endpoint: string;
10
10
  }
@@ -15,7 +15,7 @@ export interface ReclaimDiscovery extends ReclaimDiscoverySnapshot {
15
15
  readonly selection: DockerSelection;
16
16
  }
17
17
  /** What a person or the planner itself approved: the digest, and the discovery it was read from. */
18
- export interface ReclaimConsent {
18
+ interface ReclaimConsent {
19
19
  readonly digest: string;
20
20
  readonly observedAt: string;
21
21
  }
@@ -24,7 +24,7 @@ export interface ReclaimConsent {
24
24
  * to the next. `drain` is the attempt the action ran under, which the next plan matches against
25
25
  * the record its outcome published; null until the action runs.
26
26
  */
27
- export interface ReclaimContinuation {
27
+ interface ReclaimContinuation {
28
28
  readonly purpose: 'down' | 'reset';
29
29
  readonly digest: string;
30
30
  readonly completedTargetId: string;
@@ -37,7 +37,7 @@ export interface ReclaimContinuation {
37
37
  readonly drain: ReclaimDrainStep | null;
38
38
  }
39
39
  /** The one removal a plan permits, executed only by `executeReclaimAction` under a reserved attempt. */
40
- export interface PlannedReclaimAction {
40
+ interface PlannedReclaimAction {
41
41
  readonly stackId: StackId;
42
42
  readonly configSha256: string;
43
43
  /** Null when the interrupted target is already absent and only its removal is recorded. */
@@ -48,7 +48,7 @@ export interface PlannedReclaimAction {
48
48
  /** For an absent target: the actionable target ids a later discovery still shows. */
49
49
  readonly remainingTargets?: (discovery: ReclaimDiscovery) => Promise<readonly string[] | undefined>;
50
50
  }
51
- export type ReclaimTargetIdentity = ResetVolumeTargetIdentity | TeardownRuntimeTargetIdentity | PathTargetIdentity;
51
+ type ReclaimTargetIdentity = ResetVolumeTargetIdentity | TeardownRuntimeTargetIdentity | PathTargetIdentity;
52
52
  interface ResetVolumeTargetIdentity {
53
53
  readonly medium: 'runtime';
54
54
  readonly scope: 'reset-volume';
@@ -85,10 +85,10 @@ interface PathTargetIdentity {
85
85
  readonly birthtimeNs: string;
86
86
  readonly ancestors: readonly FilesystemAncestorIdentity[];
87
87
  }
88
- export type ReclaimResourceKind = 'container' | 'network' | 'volume' | 'image' | 'build-cache' | 'cache';
89
- export type OwnershipEvidence = 'current' | 'stale-config' | 'foreign' | 'unknown' | 'malformed' | 'partial' | 'unlabelled';
90
- export type Reversibility = 'runtime' | 'regenerable-cache' | 'data-bearing';
91
- export type ReclaimAction = 'teardown' | 'reclaim' | 'reset-required' | 'report-only';
88
+ type ReclaimResourceKind = 'container' | 'network' | 'volume' | 'image' | 'build-cache' | 'cache';
89
+ type OwnershipEvidence = 'current' | 'stale-config' | 'foreign' | 'unknown' | 'malformed' | 'partial' | 'unlabelled';
90
+ type Reversibility = 'runtime' | 'regenerable-cache' | 'data-bearing';
91
+ type ReclaimAction = 'teardown' | 'reclaim' | 'reset-required' | 'report-only';
92
92
  export interface ReclaimResource {
93
93
  readonly id: string;
94
94
  readonly name: string;
@@ -104,7 +104,7 @@ export interface ReclaimDiscoverySnapshot {
104
104
  readonly producer: 'runtime-resource-discovery';
105
105
  readonly resources: readonly ReclaimResource[];
106
106
  }
107
- export interface ClassifiedReclaimResource extends ReclaimResource {
107
+ interface ClassifiedReclaimResource extends ReclaimResource {
108
108
  readonly ownership: OwnershipEvidence;
109
109
  readonly reversibility: Reversibility;
110
110
  readonly action: ReclaimAction;
@@ -115,7 +115,7 @@ export interface ClassifiedReclaimResource extends ReclaimResource {
115
115
  readonly risk: string;
116
116
  };
117
117
  }
118
- export interface ClassifyReclaimInput {
118
+ interface ClassifyReclaimInput {
119
119
  readonly stackId: StackId;
120
120
  readonly cockpitId: CockpitId;
121
121
  readonly configSha256: string;
@@ -123,7 +123,7 @@ export interface ClassifyReclaimInput {
123
123
  readonly declaredCacheIds: readonly string[];
124
124
  readonly discovery: ReclaimDiscoverySnapshot;
125
125
  }
126
- export type ReclaimResult = {
126
+ type ReclaimResult = {
127
127
  readonly kind: 'complete';
128
128
  readonly noop: boolean;
129
129
  readonly resources: readonly ClassifiedReclaimResource[];
@@ -132,7 +132,7 @@ export type ReclaimResult = {
132
132
  readonly diagnostics: readonly EnvironmentDiagnostic[];
133
133
  };
134
134
  export declare function classifyReclaim(rawInput: ClassifyReclaimInput): ReclaimResult;
135
- export interface KeptTeardownResource {
135
+ interface KeptTeardownResource {
136
136
  readonly kind: ReclaimResourceKind;
137
137
  readonly id: string;
138
138
  readonly name: string;
@@ -149,7 +149,7 @@ export declare function keptTeardownResources(input: ClassifyReclaimInput): read
149
149
  * resources to look at instead of a generic unobservable precondition.
150
150
  */
151
151
  export declare function inspectionOverflow(step: ExecutionStep, outcome: ExecuteProbeOutcome): string | undefined;
152
- export interface ReclaimDiscoveryInput {
152
+ interface ReclaimDiscoveryInput {
153
153
  readonly executable: ResolvedExecutable;
154
154
  readonly cwd: string;
155
155
  readonly env: Readonly<Record<string, string>>;
@@ -172,6 +172,43 @@ type DiscoveryRefusal = {
172
172
  * complete round covers that race without admitting partial evidence.
173
173
  */
174
174
  export declare function discoverReclaim(input: ReclaimDiscoveryInput, execute: Executor): Promise<ReclaimDiscovery | DiscoveryRefusal | undefined>;
175
+ /** One container, network or volume that carries every cockpit ownership label, attributed by them alone. */
176
+ export interface MachineStackResource {
177
+ readonly kind: 'container' | 'network' | 'volume';
178
+ readonly id: string;
179
+ readonly name: string;
180
+ readonly cockpitId: CockpitId;
181
+ readonly stackId: StackId;
182
+ /** Containers only: Docker reports it running, restarting or paused. */
183
+ readonly running: boolean;
184
+ readonly composeProject: string | null;
185
+ /** Containers only: the directory Compose ran the project from, as Compose itself labelled it. */
186
+ readonly workingDir: string | null;
187
+ }
188
+ export interface MachineStackDiscovery {
189
+ readonly selection: DockerSelection;
190
+ readonly observedAt: string;
191
+ readonly resources: readonly MachineStackResource[];
192
+ /** Resources carrying the stack label whose other ownership labels are missing or malformed: never attributed. */
193
+ readonly unattributed: number;
194
+ }
195
+ type MachineStackDiscoveryInput = Omit<ReclaimDiscoveryInput, 'stackId' | 'cockpitId'>;
196
+ /**
197
+ * Lists every container, network and volume on the selected daemon that carries cockpit's stack
198
+ * label, whichever cockpit or checkout started it, for the machine-wide stack inventory. Ownership
199
+ * comes from the labels alone; nothing is attributed by its name. The selection is read before and
200
+ * after the listings, as `discoverReclaim` does, so the listings describe one daemon.
201
+ */
202
+ export declare function discoverMachineStacks(input: MachineStackDiscoveryInput, execute: Executor): Promise<MachineStackDiscovery | undefined>;
203
+ /**
204
+ * The Docker removals prune runs for one kind of one orphaned stack's resources, as the machine
205
+ * discovery attributed them: one client per chunk rather than one per resource. Undefined when the
206
+ * resources are not all of one kind and one stack, or a step cannot be built.
207
+ */
208
+ export declare function machineStackRemovalSteps(input: Pick<MachineStackDiscoveryInput, 'executable' | 'cwd' | 'env' | 'timeoutMs'>, selection: DockerSelection, resources: readonly MachineStackResource[]): readonly Readonly<{
209
+ step: ExecutionStep;
210
+ resources: readonly MachineStackResource[];
211
+ }>[] | undefined;
175
212
  export interface ResetTargetInput {
176
213
  readonly kind: 'container' | 'volume' | 'host-path';
177
214
  readonly identity: string;
@@ -183,7 +220,7 @@ interface HostPathDeclaration {
183
220
  readonly path: string;
184
221
  readonly declaration: string;
185
222
  }
186
- export interface DependencyCacheDeclaration {
223
+ interface DependencyCacheDeclaration {
187
224
  readonly cacheId: string;
188
225
  readonly root: string;
189
226
  readonly path: string;
@@ -269,7 +306,7 @@ export type TeardownPlanResult = {
269
306
  export declare function planReset(input: PlanResetInput): Promise<ResetPreview | ResetPlanResult>;
270
307
  type ByteClasses = Readonly<Record<'orphans' | 'images' | 'buildCache' | 'staleNetworks', number | null>>;
271
308
  export declare function planTeardown(input: PlanTeardownInput): Promise<TeardownPlanResult>;
272
- export interface ReclaimTargetCommitment extends DrainTargetCommitment {
309
+ interface ReclaimTargetCommitment extends DrainTargetCommitment {
273
310
  readonly targetId: string;
274
311
  }
275
312
  export interface ReclaimTargetEnvelope {
@@ -280,7 +317,7 @@ export interface ReclaimTargetEnvelope {
280
317
  readonly commitments: readonly ReclaimTargetCommitment[];
281
318
  }
282
319
  /** A removal the attempt admitted, with the attempt it ran under and what it observed. */
283
- export interface ExecutedReclaimAction {
320
+ interface ExecutedReclaimAction {
284
321
  readonly action: PlannedReclaimAction;
285
322
  readonly reservation: DrainReservation;
286
323
  readonly attempt: NonNullable<DrainReservation['attempt']>;
@@ -289,7 +326,7 @@ export interface ExecutedReclaimAction {
289
326
  readonly receipt: ExecutionReceipt | null;
290
327
  readonly fence: ExecutionFence | null;
291
328
  }
292
- export type ReclaimActionExecution = {
329
+ type ReclaimActionExecution = {
293
330
  readonly kind: 'executed';
294
331
  readonly executed: ExecutedReclaimAction;
295
332
  }
@@ -314,7 +351,7 @@ export declare function executeReclaimAction(input: {
314
351
  readonly attempt: ReservedDrainAttempt;
315
352
  readonly execute: Executor;
316
353
  }): Promise<ReclaimActionExecution>;
317
- export type ReconcileReclaimActionResult = {
354
+ type ReconcileReclaimActionResult = {
318
355
  readonly kind: 'complete';
319
356
  readonly outcome: DrainOutcome;
320
357
  readonly continuation: ReclaimContinuation | null;