@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
@@ -0,0 +1,94 @@
1
+ import { type CockpitId, type EnvironmentDiagnostic, type StackId } from './contract.js';
2
+ import type { MachineStackDiscovery, MachineStackResource } from './reclaim.js';
3
+ import { type StackStateFile, type StateNamespace } from './state.js';
4
+ type CheckoutStatus = 'present' | 'replaced' | 'missing' | 'unknown';
5
+ type StackVerdict = 'current' | 'kept' | 'orphaned';
6
+ export interface StackCheckout {
7
+ readonly path: string | null;
8
+ readonly status: CheckoutStatus;
9
+ /** Where the path, or the proof that no checkout exists, was read from. */
10
+ readonly source: string;
11
+ }
12
+ export interface StackRow {
13
+ readonly cockpitId: CockpitId;
14
+ readonly stackId: StackId;
15
+ readonly composeProject: string;
16
+ readonly checkout: StackCheckout;
17
+ readonly state: Readonly<{
18
+ status: 'readable' | 'malformed' | 'absent';
19
+ path: string | null;
20
+ reason?: string;
21
+ holders: number;
22
+ draining: boolean;
23
+ }>;
24
+ readonly containers: Readonly<{
25
+ running: readonly string[];
26
+ stopped: readonly string[];
27
+ }>;
28
+ readonly networks: readonly string[];
29
+ readonly volumes: readonly string[];
30
+ /** The port lease the stack still holds, by `[ports.bases]` name; null once released. */
31
+ readonly ports: Readonly<Record<string, number>> | null;
32
+ readonly lastActivity: Readonly<{
33
+ at: string;
34
+ source: 'state record' | 'state file modification time';
35
+ }> | null;
36
+ readonly verdict: StackVerdict;
37
+ readonly reason: string;
38
+ }
39
+ /** Another cockpit's state folder this version cannot read. */
40
+ interface StateFolderRow {
41
+ readonly path: string;
42
+ readonly cockpitId: CockpitId | null;
43
+ readonly reason: string;
44
+ readonly verdict: 'kept' | 'orphaned';
45
+ readonly why: string;
46
+ }
47
+ interface MachineStackInventory {
48
+ readonly stacks: readonly StackRow[];
49
+ readonly stateFolders: readonly StateFolderRow[];
50
+ /** Docker resources with the stack label whose other ownership labels are missing or malformed. */
51
+ readonly unattributedResources: number;
52
+ }
53
+ export interface ObservedStack {
54
+ readonly row: StackRow;
55
+ readonly namespace: StateNamespace | undefined;
56
+ readonly files: readonly StackStateFile[];
57
+ readonly resources: readonly MachineStackResource[];
58
+ }
59
+ export interface Observation {
60
+ readonly selection: MachineStackDiscovery['selection'] | undefined;
61
+ readonly inventory: MachineStackInventory;
62
+ readonly stacks: readonly ObservedStack[];
63
+ readonly folders: readonly Readonly<{
64
+ row: StateFolderRow;
65
+ fingerprint: string | undefined;
66
+ }>[];
67
+ }
68
+ export interface OwnCheckout {
69
+ readonly cockpitId: CockpitId;
70
+ readonly stackId: StackId;
71
+ readonly mainCheckoutPath: string;
72
+ }
73
+ export type Refusal = Readonly<{
74
+ kind: 'refusal';
75
+ diagnostics: readonly EnvironmentDiagnostic[];
76
+ }>;
77
+ export declare function refuse(code: EnvironmentDiagnostic['code'], message: string, target: string, fix?: string): Refusal;
78
+ /**
79
+ * Joins the state root's records with what the daemon lists, and decides each stack's checkout and
80
+ * verdict. Without `discovery` it reads state and checkouts only, as doctor does, and calls no
81
+ * unreadable state folder orphaned.
82
+ */
83
+ export declare function observeMachineStacks(input: Readonly<{
84
+ namespace: StateNamespace;
85
+ own: OwnCheckout;
86
+ discovery?: MachineStackDiscovery;
87
+ }>): Promise<Observation | Refusal>;
88
+ /** The orphaned-stack warnings of `inspectMachineStateForDoctor`, with a state that cannot be read reported as a warning. */
89
+ export declare function inspectMachineStacksForDoctor(input: Readonly<{
90
+ namespace: StateNamespace;
91
+ own: OwnCheckout;
92
+ configPath: string;
93
+ }>): Promise<readonly EnvironmentDiagnostic[]>;
94
+ export {};
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Observes every stack this machine's cockpit state or the local Docker daemon knows about, whichever
3
+ * cockpit and checkout started it, and decides which ones lost their checkout. A deleted worktree
4
+ * leaves its stack, volumes, network and port lease behind with no checkout left to run `down` from;
5
+ * this is where they are found. `cockpit stacks` lists them (`stack-inventory-report.ts`), `cockpit
6
+ * stacks prune` removes them (`stack-inventory-prune.ts`), and doctor reads the summary at the end.
7
+ *
8
+ * A stack's checkout is never guessed from a name. It is proven one of two ways: Compose labels each
9
+ * container with the directory it ran from, and the ancestor whose checkout identity derives the
10
+ * stack's own identity is the checkout; or the cockpit's main checkout is known, and none of the
11
+ * checkouts Git records for it derives the stack. Anything not proven is kept.
12
+ *
13
+ * None of these modules spawns a process: Docker steps cross the supplied `execute`, which production
14
+ * binds to `environment-exec/execute.ts`. State files are read and removed only through `state.ts`.
15
+ */
16
+ import { lstat, readdir, realpath } from 'node:fs/promises';
17
+ import { basename, dirname, join, resolve } from 'node:path';
18
+ import { readBoundedFile } from '../bounded-read.js';
19
+ import { cockpitCommand } from '../fix-catalogue.js';
20
+ import { decodeCoordinationRecord } from './claims.js';
21
+ import { ENVIRONMENT_CODES, errorCause, hasErrorCode } from './contract.js';
22
+ import { checkoutId as deriveCheckoutId, cockpitId as deriveCockpitId, composeProjectName, stackId as deriveStackId } from './identity.js';
23
+ import { inspectStackRecords, inspectStateNamespaceFolder, withStateNamespaceInventory, } from './state.js';
24
+ const COCKPIT_ID = /^cockpit:([a-z0-9][a-z0-9-]*):[a-f0-9]{64}$/;
25
+ const STACK_FILE = /^stack_([a-f0-9]{64})\.json$/;
26
+ const RECORD_KINDS = ['coordination', 'lifecycle', 'lifecycle-history', 'data-outcome'];
27
+ const GIT_POINTER_BYTES = 4_096;
28
+ const WORKTREE_LIMIT = 4_096;
29
+ export function refuse(code, message, target, fix) {
30
+ return { kind: 'refusal', diagnostics: [{ severity: 'error', code, message, target, ...(fix ? { fix } : {}) }] };
31
+ }
32
+ // A path that cannot exist (ENOENT, or a parent that is now a file) is absent; anything else that
33
+ // stops a read is not proof of absence, and is left to the caller as unknown.
34
+ async function statOf(path) {
35
+ try {
36
+ return await lstat(path);
37
+ }
38
+ catch (error) {
39
+ if (hasErrorCode(error, 'ENOENT', 'ENOTDIR'))
40
+ return 'absent';
41
+ if (hasErrorCode(error, 'EACCES', 'EPERM', 'ELOOP'))
42
+ return 'unreadable';
43
+ throw error;
44
+ }
45
+ }
46
+ function readPointer(path) {
47
+ try {
48
+ const read = readBoundedFile(path, GIT_POINTER_BYTES);
49
+ return read.kind === 'complete' ? read.bytes.toString('utf8').trim() : undefined;
50
+ }
51
+ catch (error) {
52
+ // Absent, not a regular file, a symbolic link or unreadable: no pointer Git would follow.
53
+ if (hasErrorCode(error, 'ENOENT', 'ENOTDIR', 'EISDIR', 'ELOOP', 'EACCES', 'EPERM'))
54
+ return undefined;
55
+ throw error;
56
+ }
57
+ }
58
+ function settledValue(result) {
59
+ if (result.status === 'rejected')
60
+ throw result.reason;
61
+ return result.value;
62
+ }
63
+ async function canonical(path) {
64
+ try {
65
+ return await realpath(path);
66
+ }
67
+ catch (error) {
68
+ if (hasErrorCode(error, 'ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'ELOOP'))
69
+ return undefined;
70
+ throw error;
71
+ }
72
+ }
73
+ /** The main checkout a checkout belongs to, read from its `.git` entry as Git lays it out. */
74
+ async function mainCheckoutOf(checkout) {
75
+ const dotGit = await statOf(join(checkout, '.git'));
76
+ if (typeof dotGit === 'string')
77
+ return undefined;
78
+ if (dotGit.isDirectory())
79
+ return canonical(checkout);
80
+ const pointer = /^gitdir: (.+)$/m.exec(readPointer(join(checkout, '.git')) ?? '');
81
+ if (!pointer)
82
+ return undefined;
83
+ const gitDirectory = resolve(checkout, pointer[1].trim());
84
+ const common = readPointer(join(gitDirectory, 'commondir'));
85
+ if (common === undefined)
86
+ return undefined;
87
+ const commonDirectory = resolve(gitDirectory, common);
88
+ return basename(commonDirectory) === '.git' ? canonical(dirname(commonDirectory)) : undefined;
89
+ }
90
+ /**
91
+ * The main checkout and every worktree Git records for it, or undefined when any registration cannot
92
+ * be read: an incomplete list would make a live worktree's stack look orphaned.
93
+ */
94
+ async function checkoutsOf(main) {
95
+ const registry = join(main, '.git', 'worktrees');
96
+ let names;
97
+ try {
98
+ names = await readdir(registry);
99
+ }
100
+ catch (error) {
101
+ if (hasErrorCode(error, 'ENOENT'))
102
+ return [{ path: main }];
103
+ if (hasErrorCode(error, 'ENOTDIR', 'EACCES', 'EPERM'))
104
+ return undefined;
105
+ throw error;
106
+ }
107
+ if (names.length > WORKTREE_LIMIT)
108
+ return undefined;
109
+ // Every registration is read at once; walking the results in name order keeps the outcome a
110
+ // sequential walk would give, down to which failure, an unreadable entry or a throw, decides it.
111
+ const registered = await Promise.allSettled(names.sort().map(async (name) => {
112
+ const pointer = readPointer(join(registry, name, 'gitdir'));
113
+ if (pointer === undefined)
114
+ return undefined;
115
+ const target = resolve(registry, name, pointer);
116
+ if (basename(target) !== '.git')
117
+ return undefined;
118
+ const status = await statOf(dirname(target));
119
+ if (status === 'unreadable')
120
+ return undefined;
121
+ if (status !== 'absent') {
122
+ const path = await canonical(dirname(target));
123
+ return path === undefined ? undefined : { path };
124
+ }
125
+ const locked = await statOf(join(registry, name, 'locked'));
126
+ if (locked === 'unreadable')
127
+ return undefined;
128
+ return { path: dirname(target), missing: locked === 'absent' ? 'registered' : 'locked' };
129
+ }));
130
+ const checkouts = [{ path: main }];
131
+ for (const result of registered) {
132
+ const checkout = settledValue(result);
133
+ if (checkout === undefined)
134
+ return undefined;
135
+ checkouts.push(checkout);
136
+ }
137
+ return checkouts;
138
+ }
139
+ function derivesStack(cockpitId, stackId, path) {
140
+ return sharedStackOf(cockpitId, deriveCheckoutId({ checkoutPath: path })) === stackId;
141
+ }
142
+ function sharedStackOf(cockpitId, checkoutId) {
143
+ return deriveStackId({ cockpitId, checkoutId, variant: { kind: 'shared' } });
144
+ }
145
+ /** The ancestor of a Compose working directory whose checkout identity derives this stack's identity. */
146
+ function provenCheckout(cockpitId, stackId, workingDir) {
147
+ for (let cursor = workingDir;; cursor = dirname(cursor)) {
148
+ if (derivesStack(cockpitId, stackId, cursor))
149
+ return cursor;
150
+ if (dirname(cursor) === cursor)
151
+ return undefined;
152
+ }
153
+ }
154
+ function cockpitNameOf(cockpitId) {
155
+ return COCKPIT_ID.exec(cockpitId)[1];
156
+ }
157
+ async function statusOf(path, cockpitId) {
158
+ const status = await statOf(path);
159
+ if (status === 'absent')
160
+ return 'missing';
161
+ if (status === 'unreadable')
162
+ return 'unknown';
163
+ if (!status.isDirectory())
164
+ return 'replaced';
165
+ const main = await mainCheckoutOf(path);
166
+ return main !== undefined && deriveCockpitId({ name: cockpitNameOf(cockpitId), mainCheckoutPath: main }) === cockpitId ? 'present' : 'replaced';
167
+ }
168
+ function namespaceCockpitId(path) {
169
+ // Earlier builds spelled the folder with colons; both spellings name the same identity.
170
+ const id = basename(path).replaceAll('_', ':');
171
+ return COCKPIT_ID.test(id) ? id : null;
172
+ }
173
+ async function stackIdsIn(namespace) {
174
+ const found = new Set();
175
+ for (const kind of RECORD_KINDS) {
176
+ let names;
177
+ try {
178
+ names = await readdir(join(namespace.cockpitRoot, 'records', kind));
179
+ }
180
+ catch (error) {
181
+ if (hasErrorCode(error, 'ENOENT'))
182
+ continue;
183
+ throw error;
184
+ }
185
+ for (const name of names) {
186
+ const match = STACK_FILE.exec(name);
187
+ if (match)
188
+ found.add(`stack:${match[1]}`);
189
+ }
190
+ }
191
+ return [...found].sort();
192
+ }
193
+ async function readStateStacks(namespace) {
194
+ const read = await withStateNamespaceInventory(namespace, async (namespaces, skipped) => {
195
+ const readStack = async (candidate, stackId) => {
196
+ const cockpitId = candidate.paths.coordination(stackId).cockpitId;
197
+ const coordinationPath = candidate.paths.coordination(stackId).path;
198
+ const records = await inspectStackRecords(candidate, stackId);
199
+ const stored = records.kind === 'complete' ? records.value.coordination : undefined;
200
+ const record = stored?.kind === 'complete' ? decodeCoordinationRecord(stored.value, stackId) : undefined;
201
+ const malformed = records.kind !== 'complete' ? records.kind === 'missing' ? 'its state disappeared during observation' : records.message
202
+ : stored?.kind === 'refusal' || stored?.kind === 'indeterminate' ? stored.message
203
+ : stored?.kind === 'complete' && record === undefined ? 'the coordination record does not match the format this version reads' : undefined;
204
+ const status = malformed === undefined ? undefined : await statOf(coordinationPath);
205
+ return {
206
+ cockpitId, stackId, namespace: candidate, coordinationPath, record, malformed,
207
+ files: records.kind === 'complete' ? records.value.files : [],
208
+ updatedAt: stored?.kind === 'complete' && record !== undefined ? stored.value.updatedAt : undefined,
209
+ modifiedAt: status !== undefined && typeof status !== 'string' ? status.mtime.toISOString() : undefined,
210
+ };
211
+ };
212
+ const perNamespace = await Promise.all(namespaces.map(async (candidate) => Promise.all((await stackIdsIn(candidate)).map((stackId) => readStack(candidate, stackId)))));
213
+ return { stacks: perNamespace.flat(), skipped };
214
+ });
215
+ if (read.kind === 'complete')
216
+ return read.value;
217
+ if (read.kind === 'missing')
218
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'the state root disappeared during observation', read.path);
219
+ return refuse(read.code, read.message, read.target, read.recovery);
220
+ }
221
+ function verdictOf(input) {
222
+ if (input.current)
223
+ return { verdict: 'current', reason: 'this is the stack of the checkout running the command' };
224
+ if (input.draining)
225
+ return { verdict: 'kept', reason: 'a down or reset of this stack is in progress, and only its own checkout may finish it' };
226
+ if (input.registered === 'locked')
227
+ return { verdict: 'kept', reason: 'Git marks its worktree locked, as for one on a drive or share that is not mounted now; once the checkout is really gone, run git worktree unlock and git worktree prune in its main checkout' };
228
+ if (input.registered === 'registered')
229
+ return { verdict: 'kept', reason: 'its checkout path does not exist but Git still registers the worktree, as for one on a drive or share that is not mounted now or one moved without git worktree repair; once the checkout is really gone, run git worktree prune in its main checkout' };
230
+ if (input.checkout.status === 'present')
231
+ return { verdict: 'kept', reason: 'its checkout exists' };
232
+ if (input.checkout.status === 'replaced')
233
+ return { verdict: 'kept', reason: 'its checkout path exists but is no longer a checkout of this cockpit; cockpit cannot prove nothing uses the stack' };
234
+ if (input.checkout.status === 'missing')
235
+ return { verdict: 'orphaned', reason: 'its checkout no longer exists' };
236
+ // State this version cannot read may be a newer version's, for a checkout that still exists and
237
+ // a stack that is only down; nothing running proves nothing about its checkout.
238
+ return input.malformed
239
+ ? { verdict: 'kept', reason: 'its state cannot be read by this cockpit version and no checkout is proven gone; prune it from a cockpit version that reads it, or once its checkout is gone' }
240
+ : { verdict: 'kept', reason: 'cockpit cannot tell where its checkout is, so it may still be in use' };
241
+ }
242
+ /**
243
+ * Joins the state root's records with what the daemon lists, and decides each stack's checkout and
244
+ * verdict. Without `discovery` it reads state and checkouts only, as doctor does, and calls no
245
+ * unreadable state folder orphaned.
246
+ */
247
+ export async function observeMachineStacks(input) {
248
+ // A read that fails in an unexpected way leaves the inventory unknown: a named refusal, which
249
+ // doctor reports as a warning, rather than a throw that ends the command.
250
+ try {
251
+ return await observe(input);
252
+ }
253
+ catch (error) {
254
+ return refuse(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, `this machine's stacks could not be observed safely: ${errorCause(error)}`, dirname(input.namespace.cockpitRoot), 'inspect the named state entry or checkout, then retry');
255
+ }
256
+ }
257
+ async function observe(input) {
258
+ const state = await readStateStacks(input.namespace);
259
+ if ('kind' in state)
260
+ return state;
261
+ const key = (cockpitId, stackId) => `${cockpitId}\0${stackId}`;
262
+ const resources = new Map();
263
+ for (const resource of input.discovery?.resources ?? []) {
264
+ const id = key(resource.cockpitId, resource.stackId);
265
+ resources.set(id, [...resources.get(id) ?? [], resource]);
266
+ }
267
+ const stateByKey = new Map(state.stacks.map((stack) => [key(stack.cockpitId, stack.stackId), stack]));
268
+ const skippedByCockpit = new Map(state.skipped.flatMap((folder) => {
269
+ const cockpitId = namespaceCockpitId(folder.path);
270
+ return cockpitId === null ? [] : [[cockpitId, folder]];
271
+ }));
272
+ const keys = [...new Set([...stateByKey.keys(), ...resources.keys()])].sort();
273
+ const identities = keys.map((id) => {
274
+ const [cockpitId, stackId] = id.split('\0');
275
+ return { id, cockpitId, stackId };
276
+ });
277
+ // A main checkout is known for this cockpit, and for any cockpit with a proven present checkout.
278
+ const proofs = new Map();
279
+ for (const { id, cockpitId, stackId } of identities) {
280
+ for (const resource of resources.get(id) ?? []) {
281
+ const proven = resource.workingDir === null ? undefined : provenCheckout(cockpitId, stackId, resource.workingDir);
282
+ if (proven !== undefined) {
283
+ proofs.set(id, proven);
284
+ break;
285
+ }
286
+ }
287
+ }
288
+ const mains = new Map([[input.own.cockpitId, input.own.mainCheckoutPath]]);
289
+ // Each proven checkout's main checkout is read at once; the first, in identity order, that
290
+ // derives its cockpit decides, and a read a sequential walk would have skipped cannot fail it.
291
+ const candidates = identities.flatMap(({ id, cockpitId }) => {
292
+ const proven = proofs.get(id);
293
+ return proven === undefined || cockpitId === input.own.cockpitId ? [] : [{ cockpitId, proven }];
294
+ });
295
+ const candidateMains = await Promise.allSettled(candidates.map(({ proven }) => mainCheckoutOf(proven)));
296
+ for (const [index, { cockpitId }] of candidates.entries()) {
297
+ if (mains.has(cockpitId))
298
+ continue;
299
+ const main = settledValue(candidateMains[index]);
300
+ if (main !== undefined && deriveCockpitId({ name: cockpitNameOf(cockpitId), mainCheckoutPath: main }) === cockpitId)
301
+ mains.set(cockpitId, main);
302
+ }
303
+ // Every checkout a cockpit records, with the checkout and shared stack identity it derives, computed once per path.
304
+ const checkouts = new Map(await Promise.all([...mains].map(async ([cockpitId, main]) => {
305
+ const paths = await checkoutsOf(main);
306
+ return [cockpitId, paths?.map((recorded) => {
307
+ const checkoutId = deriveCheckoutId({ checkoutPath: recorded.path });
308
+ return { ...recorded, checkoutId, stackId: sharedStackOf(cockpitId, checkoutId) };
309
+ })];
310
+ })));
311
+ const stacks = [];
312
+ for (const { id, cockpitId, stackId } of identities) {
313
+ const stored = stateByKey.get(id);
314
+ const owned = resources.get(id) ?? [];
315
+ const known = checkouts.get(cockpitId);
316
+ // A holder names its checkout's identity outright; the derivation covers a stack no one holds.
317
+ const holders = new Set(stored?.record?.holders.map(({ checkoutId: id }) => id) ?? []);
318
+ const registered = known?.find((checkout) => checkout.stackId === stackId || holders.has(checkout.checkoutId));
319
+ const proven = proofs.get(id);
320
+ const checkout = registered !== undefined
321
+ ? registered.missing === undefined
322
+ ? { path: registered.path, status: 'present', source: `a checkout Git records for ${mains.get(cockpitId)}` }
323
+ : { path: registered.path, status: 'unknown',
324
+ source: `a worktree Git ${registered.missing === 'locked' ? 'marks locked' : 'still registers'} for ${mains.get(cockpitId)}, at a path that does not exist now` }
325
+ : proven !== undefined
326
+ ? { path: proven, status: await statusOf(proven, cockpitId), source: `the Compose working directory label of container ${owned.find((row) => row.workingDir !== null).name}` }
327
+ : known !== undefined
328
+ ? { path: null, status: 'missing', source: `none of the checkouts Git records for ${mains.get(cockpitId)} is this stack's` }
329
+ : { path: null, status: 'unknown', source: 'no container of this stack records where it ran, and no checkout of its cockpit was found' };
330
+ const folder = skippedByCockpit.get(cockpitId);
331
+ const malformed = stored?.malformed ?? (folder ? `its cockpit's state folder cannot be read: ${folder.reason}` : undefined);
332
+ const containers = owned.filter(({ kind }) => kind === 'container');
333
+ const record = stored?.record;
334
+ const draining = record?.drain != null;
335
+ const decided = verdictOf({ current: cockpitId === input.own.cockpitId && stackId === input.own.stackId, checkout,
336
+ malformed: malformed !== undefined, draining, registered: registered?.missing });
337
+ const lease = record?.lease && !record.lease.released ? record.lease.ports : null;
338
+ stacks.push({
339
+ namespace: stored?.namespace, files: stored?.files ?? [], resources: owned,
340
+ row: {
341
+ cockpitId, stackId,
342
+ composeProject: owned.find(({ composeProject }) => composeProject !== null)?.composeProject
343
+ ?? composeProjectName({ cockpitName: cockpitNameOf(cockpitId), stackId }),
344
+ checkout,
345
+ state: {
346
+ status: stored === undefined && folder === undefined ? 'absent' : malformed !== undefined ? 'malformed' : 'readable',
347
+ path: stored?.coordinationPath ?? folder?.path ?? null,
348
+ ...(malformed === undefined ? {} : { reason: malformed }),
349
+ holders: record?.holders.length ?? 0, draining,
350
+ },
351
+ containers: {
352
+ running: containers.filter(({ running }) => running).map(({ name }) => name),
353
+ stopped: containers.filter(({ running }) => !running).map(({ name }) => name),
354
+ },
355
+ networks: owned.filter(({ kind }) => kind === 'network').map(({ name }) => name),
356
+ volumes: owned.filter(({ kind }) => kind === 'volume').map(({ name }) => name),
357
+ ports: lease === null ? null : { ...lease },
358
+ lastActivity: stored?.updatedAt !== undefined ? { at: stored.updatedAt, source: 'state record' }
359
+ : stored?.modifiedAt !== undefined ? { at: stored.modifiedAt, source: 'state file modification time' } : null,
360
+ ...decided,
361
+ },
362
+ });
363
+ }
364
+ const folders = await Promise.all(state.skipped.map(async (folder) => {
365
+ const cockpitId = namespaceCockpitId(folder.path);
366
+ const live = stacks.find(({ row }) => row.cockpitId === cockpitId && row.verdict !== 'orphaned');
367
+ // Only a prune, which consults Docker, can remove a folder, so only it needs the folder's fingerprint.
368
+ const fingerprint = input.discovery === undefined ? undefined : await inspectStateNamespaceFolder(input.namespace, folder.path);
369
+ // Only the colon spelling an earlier build used is provably no current build's: a folder in the
370
+ // current spelling that this version cannot read may belong to a newer version still in use.
371
+ const legacy = basename(folder.path).includes(':');
372
+ const why = cockpitId === null ? 'its name is not a cockpit identity, so Docker cannot show which stacks it holds'
373
+ : !legacy ? 'it is named as current cockpit versions name their state, so a newer version may still use it; prune it from a version that reads it'
374
+ : fingerprint === undefined ? 'Docker was not consulted to show that none of its stacks is in use'
375
+ : live !== undefined ? `its stack ${live.row.stackId} is kept: ${live.row.reason}`
376
+ : fingerprint.kind !== 'complete' ? `it cannot be inspected safely: ${fingerprint.kind === 'refusal' ? fingerprint.message : 'it disappeared'}`
377
+ : 'an earlier cockpit build wrote it, no current version reads it, and none of its stacks is in use';
378
+ const orphaned = cockpitId !== null && legacy && live === undefined && fingerprint?.kind === 'complete';
379
+ return {
380
+ row: { path: folder.path, cockpitId, reason: folder.reason, verdict: orphaned ? 'orphaned' : 'kept', why },
381
+ fingerprint: fingerprint?.kind === 'complete' ? fingerprint.value : undefined,
382
+ };
383
+ }));
384
+ return {
385
+ selection: input.discovery?.selection,
386
+ inventory: { stacks: stacks.map(({ row }) => row), stateFolders: folders.map(({ row }) => row), unattributedResources: input.discovery?.unattributed ?? 0 },
387
+ stacks, folders,
388
+ };
389
+ }
390
+ /**
391
+ * Doctor's view of the machine's stacks without asking Docker: stacks whose checkout is gone and
392
+ * state this version cannot read. The full inventory is `cockpit stacks`.
393
+ */
394
+ async function inspectMachineStateForDoctor(input) {
395
+ const observed = await observeMachineStacks({ namespace: input.namespace, own: input.own });
396
+ if ('kind' in observed)
397
+ return observed;
398
+ const gone = observed.inventory.stacks.filter(({ checkout }) => checkout.status === 'missing');
399
+ // Other cockpits' unreadable state folders have doctor's own warning; these are unreadable records.
400
+ const unreadable = observed.inventory.stacks.filter(({ state }) => state.status === 'malformed' && state.path !== null
401
+ && !observed.inventory.stateFolders.some(({ path }) => path === state.path)).length;
402
+ if (gone.length + unreadable === 0)
403
+ return { kind: 'complete', orphanedStacks: [] };
404
+ const parts = [
405
+ ...gone.length > 0 ? [`${gone.length} whose checkout no longer exists`] : [],
406
+ ...unreadable > 0 ? [`${unreadable} whose state this cockpit version cannot read`] : [],
407
+ ];
408
+ return { kind: 'complete', orphanedStacks: [{ severity: 'warning', target: dirname(input.namespace.cockpitRoot),
409
+ code: 'STACKS_ORPHANED',
410
+ message: `this machine has stacks that no checkout can reach (${parts.join('; ')}); they keep their containers, volumes, port leases and state`,
411
+ fix: `list them with ${cockpitCommand('stacks', { config: input.configPath })}, preview the removal with ${cockpitCommand('stacks prune', { config: input.configPath })}, then apply it with --apply` }] };
412
+ }
413
+ /** The orphaned-stack warnings of `inspectMachineStateForDoctor`, with a state that cannot be read reported as a warning. */
414
+ export async function inspectMachineStacksForDoctor(input) {
415
+ const state = await inspectMachineStateForDoctor(input);
416
+ return state.kind === 'complete' ? state.orphanedStacks : state.diagnostics.map((diagnostic) => ({ ...diagnostic, severity: 'warning',
417
+ message: `doctor could not read this machine's cockpit state, so it did not look for orphaned stacks: ${diagnostic.message}` }));
418
+ }
@@ -56,7 +56,7 @@ declare const StateRecordSchema: z.ZodObject<{
56
56
  value: z.ZodJSONSchema;
57
57
  }, z.core.$strict>;
58
58
  export type StateRecord = z.infer<typeof StateRecordSchema>;
59
- export type StateRecordKind = typeof RECORD_KINDS[number];
59
+ type StateRecordKind = typeof RECORD_KINDS[number];
60
60
  declare const BootstrapStateRecordSchema: z.ZodObject<{
61
61
  format: z.ZodLiteral<"cockpit-state/v1">;
62
62
  kind: z.ZodLiteral<"bootstrap">;
@@ -130,6 +130,7 @@ declare const BootstrapStateRecordSchema: z.ZodObject<{
130
130
  sha256: z.ZodString;
131
131
  provenance: z.ZodEnum<{
132
132
  "agent-default": "agent-default";
133
+ derived: "derived";
133
134
  "operator-confirmed": "operator-confirmed";
134
135
  unattributed: "unattributed";
135
136
  }>;
@@ -198,7 +199,7 @@ export type StateResult<T> = {
198
199
  readonly target: string;
199
200
  readonly recovery?: string;
200
201
  };
201
- export interface SelectStateRootInput {
202
+ interface SelectStateRootInput {
202
203
  readonly platform: 'darwin' | 'linux';
203
204
  readonly home: string | undefined;
204
205
  readonly xdgStateHome?: string;
@@ -216,7 +217,7 @@ export interface SelectBootstrapStateRootInput {
216
217
  readonly uid?: number;
217
218
  }
218
219
  type LockHolderState = 'live' | 'dead' | 'unknown';
219
- export interface StateLockInspection {
220
+ interface StateLockInspection {
220
221
  readonly holderState: LockHolderState | 'invalid';
221
222
  readonly description: string;
222
223
  readonly recovery: string;
@@ -233,7 +234,7 @@ export interface StateTransactionOptions {
233
234
  readonly beforeLockMetadata?: (operation: 'owner-unlink' | 'tombstone-rmdir' | 'owner-restore-link' | 'backup-unlink') => Promise<void>;
234
235
  readonly contentionTimeoutMs?: number;
235
236
  }
236
- export interface StateScopedLock {
237
+ interface StateScopedLock {
237
238
  readonly kind: 'state-scoped-lock';
238
239
  }
239
240
  export interface StateTransactionAbort<T> {
@@ -250,7 +251,6 @@ export declare function authenticateStatePublication(input: {
250
251
  readonly record: AnyStateRecord;
251
252
  }> | undefined;
252
253
  export declare function authenticateStateNamespace(value: unknown): StateNamespace | undefined;
253
- export declare function authenticateBootstrapStateNamespace(value: unknown): BootstrapStateNamespace | undefined;
254
254
  export declare function issueStateScopedLock(namespace: StateNamespace, input: Readonly<{
255
255
  owner: string;
256
256
  identity: string;
@@ -290,13 +290,13 @@ export declare function selectStateRoot(input: SelectStateRootInput): Promise<St
290
290
  export declare function readState(address: BootstrapStateAddress): Promise<StateResult<BootstrapStateRecord>>;
291
291
  export declare function readState(address: StateAddress): Promise<StateResult<StateRecord>>;
292
292
  export declare function readState(address: StateAddress | BootstrapStateAddress): Promise<StateResult<AnyStateRecord>>;
293
- export interface BootstrapAnswerResetPreview {
293
+ interface BootstrapAnswerResetPreview {
294
294
  readonly revision: number;
295
295
  readonly field: string;
296
296
  readonly invalidatedFields: readonly string[];
297
297
  readonly previewSha256: string;
298
298
  }
299
- export type BootstrapAnswerResetResult = Readonly<{
299
+ type BootstrapAnswerResetResult = Readonly<{
300
300
  kind: 'pending';
301
301
  preview: BootstrapAnswerResetPreview;
302
302
  }> | Readonly<{
@@ -320,7 +320,7 @@ type BootstrapObservationRefreshRefusal = Readonly<{
320
320
  reason: 'invalid' | 'stale';
321
321
  message: string;
322
322
  }>;
323
- export type BootstrapObservationRefreshResult = StateResult<BootstrapStateRecord> | BootstrapObservationRefreshRefusal;
323
+ type BootstrapObservationRefreshResult = StateResult<BootstrapStateRecord> | BootstrapObservationRefreshRefusal;
324
324
  export declare function refreshBootstrapObservation(namespace: BootstrapStateNamespace, address: BootstrapStateAddress, input: Readonly<{
325
325
  ownerId: BootstrapLockOwnerId;
326
326
  expectedRevision: number;
@@ -333,7 +333,7 @@ type BootstrapPublicationRefreshRefusal = Readonly<{
333
333
  reason: 'invalid' | 'stale';
334
334
  message: string;
335
335
  }>;
336
- export type BootstrapPublicationRefreshResult = StateResult<BootstrapStateRecord> | BootstrapPublicationRefreshRefusal;
336
+ type BootstrapPublicationRefreshResult = StateResult<BootstrapStateRecord> | BootstrapPublicationRefreshRefusal;
337
337
  export declare function refreshBootstrapPublication(namespace: BootstrapStateNamespace, address: BootstrapStateAddress, input: Readonly<{
338
338
  ownerId: BootstrapLockOwnerId;
339
339
  expectedRevision: number;
@@ -368,4 +368,43 @@ export declare function transactState(namespace: StateNamespace, address: StateA
368
368
  export declare function transactState<T>(namespace: StateNamespace, address: StateAddress, options: StateTransactionOptions, update: (current: StateRecord | null) => StateRecord | StateTransactionAbort<T> | Promise<StateRecord | StateTransactionAbort<T>>): Promise<StateResult<StateRecord> | StateTransactionAbort<T>>;
369
369
  export declare function transactState(namespace: BootstrapStateNamespace, address: BootstrapStateAddress, options: StateTransactionOptions, update: (current: BootstrapStateRecord | null) => BootstrapStateRecord | Promise<BootstrapStateRecord>): Promise<StateResult<BootstrapStateRecord>>;
370
370
  export declare function transactState<T>(namespace: BootstrapStateNamespace, address: BootstrapStateAddress, options: StateTransactionOptions, update: (current: BootstrapStateRecord | null) => BootstrapStateRecord | StateTransactionAbort<T> | Promise<BootstrapStateRecord | StateTransactionAbort<T>>): Promise<StateResult<BootstrapStateRecord> | StateTransactionAbort<T>>;
371
+ /** One of a stack's own record files, by the SHA-256 of its exact bytes. */
372
+ export type StackStateFile = Readonly<{
373
+ kind: 'coordination' | 'lifecycle' | 'lifecycle-history' | 'data-outcome';
374
+ path: string;
375
+ sha256: string;
376
+ }>;
377
+ /** A stack's record files, and its coordination record decoded from the very bytes its file digest binds. */
378
+ type StackStateRead = Readonly<{
379
+ files: readonly StackStateFile[];
380
+ /** Undefined when the stack has no coordination file. */
381
+ coordination: StateResult<StateRecord> | undefined;
382
+ }>;
383
+ /** The record files a stack has under one cockpit's namespace, read as bytes so an unparseable record is listed too. */
384
+ export declare function inspectStackState(value: StateNamespace, stackId: StackId): Promise<StateResult<readonly StackStateFile[]>>;
385
+ /** `inspectStackState`, plus the coordination record decoded from the bytes it read rather than from a second read. */
386
+ export declare function inspectStackRecords(value: StateNamespace, stackId: StackId): Promise<StateResult<StackStateRead>>;
387
+ /** What a guarded removal did: removed the named paths, or found them changed since the preview and left them. */
388
+ type StateRemoval = Readonly<{
389
+ kind: 'removed';
390
+ paths: readonly string[];
391
+ }> | Readonly<{
392
+ kind: 'changed';
393
+ reason: string;
394
+ }>;
395
+ /**
396
+ * Deletes a stack's record files, which releases its port lease, only when each file still holds the
397
+ * exact bytes `expected` names and no other file has appeared. It runs under the root port lock, so
398
+ * no `up` leases ports from a view that still counts the lease, and under the cockpit's own state
399
+ * lock, so no transaction rewrites a record between the check and the removal.
400
+ */
401
+ export declare function removeStackState(value: StateNamespace, stackId: StackId, expected: readonly StackStateFile[], options: Readonly<{
402
+ ownerId: HolderId;
403
+ }>): Promise<StateResult<StateRemoval>>;
404
+ /** A digest of another cockpit's state folder that this version cannot read: every entry's path, mode, size and modification time. */
405
+ export declare function inspectStateNamespaceFolder(value: StateNamespace, path: string): Promise<StateResult<string>>;
406
+ /** Deletes another cockpit's unreadable state folder, under the root port lock, only while it still matches `fingerprint`. */
407
+ export declare function removeStateNamespaceFolder(value: StateNamespace, path: string, fingerprint: string, options: Readonly<{
408
+ ownerId: HolderId;
409
+ }>): Promise<StateResult<StateRemoval>>;
371
410
  export {};