@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
@@ -5,6 +5,7 @@ import { type GuardSurfaceTarget } from '../../surfaces/harness-registry.js';
5
5
  import { type FixContext } from '../../fix-catalogue.js';
6
6
  import { observeSetupCheckout, type SetupCheckoutObservation } from '../setup-observation.js';
7
7
  import { loadCockpitConfig, type LoadedConfigProjection } from '../../config/load.js';
8
+ import { type SiblingCheckoutRecordRead } from '../sibling-checkouts.js';
8
9
  export interface GuardEvaluationApplicationInput {
9
10
  readonly identity: unknown;
10
11
  /** The checkout the invoked guard script resolved from its own location. */
@@ -17,7 +18,7 @@ export interface GuardEvaluationApplicationInput {
17
18
  /** The harness's command search path, whose directories inside the allowed roots the guard protects. */
18
19
  readonly searchPath?: string;
19
20
  }
20
- export interface GuardEvaluationApplicationAdapters {
21
+ interface GuardEvaluationApplicationAdapters {
21
22
  readonly loadConfig: typeof loadCockpitConfig;
22
23
  readonly observeCheckout: typeof observeSetupCheckout;
23
24
  readonly resolveGit: () => Promise<import('../executable.js').ExecutableResolution>;
@@ -25,19 +26,24 @@ export interface GuardEvaluationApplicationAdapters {
25
26
  readonly resolveDocker?: () => Promise<import('../executable.js').ExecutableResolution>;
26
27
  readonly execute?: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
27
28
  }
28
- export interface GuardEvaluationApplicationResult {
29
+ interface GuardEvaluationApplicationResult {
29
30
  readonly decision: GuardDecision;
30
31
  readonly diagnostics: readonly import('../contract.js').Diagnostic[];
31
32
  readonly fixContext?: FixContext;
32
33
  }
33
34
  /** Installed bytes are authority only while their source, roots and publication still match. */
34
35
  export declare function runGuardEvaluationApplication(input: GuardEvaluationApplicationInput, adapters: GuardEvaluationApplicationAdapters): Promise<GuardEvaluationApplicationResult>;
35
- export interface PreparedGuardEvaluationPolicy {
36
+ interface PreparedGuardEvaluationPolicy {
36
37
  readonly policy: GuardEvaluationPolicy;
37
38
  }
38
- export interface GuardEvaluationPolicyInput {
39
+ interface GuardEvaluationPolicyInput {
39
40
  readonly configuration: LoadedConfigProjection;
40
41
  readonly checkout: SetupCheckoutObservation;
41
42
  }
42
- /** Source projections share authenticated authority; installed consumption is admitted separately. */
43
- export declare function prepareGuardEvaluationPolicy(input: GuardEvaluationPolicyInput): Promise<EnvironmentCommandOutcome<PreparedGuardEvaluationPolicy>>;
43
+ /**
44
+ * Source projections share authenticated authority; installed consumption is admitted separately.
45
+ * `siblingCheckouts` is the configuration directory's sibling checkout record, when the caller
46
+ * started reading it already; it is read here otherwise.
47
+ */
48
+ export declare function prepareGuardEvaluationPolicy(input: GuardEvaluationPolicyInput, siblingCheckouts?: Promise<SiblingCheckoutRecordRead>): Promise<EnvironmentCommandOutcome<PreparedGuardEvaluationPolicy>>;
49
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { lstatIfPresentSync } from '../presence.js';
2
3
  import { lstatSync, realpathSync } from 'node:fs';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -8,13 +9,15 @@ import { hasErrorCode, RefusalError, ENVIRONMENT_CODES, errorCause, snapshotJson
8
9
  import { GUARD_EXECUTABLE_SEARCH_PATH } from '../executable.js';
9
10
  import { CockpitConfig } from '../../schema.js';
10
11
  import { pinnedPolicyDenial } from '../../guards/policy-pin.js';
12
+ import { guardConfigChange, guardConfigHeldReason } from '../../guards/policy-change.js';
11
13
  import { evaluateGuard, foldGuardSpelling, GUARD_LOCATION_LIMITS, GUARD_ROOT_PROTECTIONS, guardIdentity, } from '../../guards/evaluator.js';
12
- import { ordinarySessionCommand, resolveOrdinaryInvocation } from '../../command-table.js';
14
+ import { commandSpelling, ordinarySessionCommand, resolveOrdinaryInvocation, UNSYNCED_ADMITTED_COMMANDS, UNSYNCED_ADMITTED_PROSE } from '../../command-table.js';
13
15
  import { effectivePolicy } from '../../autonomy.js';
14
16
  import { GUARD_INPUT_LIMITS, parseGuardInput } from '../../guards/invocation-parser.js';
15
17
  import { readAnchoredFile, readAnchoredBatches, nodeAuthority } from '../../generated-environment/transaction.js';
16
18
  import { parseOwnershipManifest } from '../../surfaces/ownership.js';
17
19
  import { installedPackageMetadata } from '../../package-metadata.js';
20
+ import { ANSWER_CONFIRMATION_PATH } from '../../guards/answer-confirmation.js';
18
21
  import { GUARD_LOCAL_PATHS, parseGuardLocalIdentity } from '../../guards/local-identity.js';
19
22
  import { guardInstalledDirectories, guardLauncherEvidence, guardSearchDirectories, guardSearchPathRelativeEntry, guardSecretFiles, guardSensitivePathsBelow, guardTrackedSensitivePaths, guardUnignoredSensitivePaths, } from '../../guards/protected-locations.js';
20
23
  import { classifyReadOnlyInvocation, cockpitLauncherPrefix } from '../../guards/read-only.js';
@@ -27,9 +30,10 @@ import { observeInstallPolicyLockfiles, withInstallPolicyProjection } from '../i
27
30
  import { resolveInstallPolicies, renderGuardProjectCommand } from '../install-policy.js';
28
31
  import { checkoutId as deriveCheckoutId, publicationCockpitId } from '../identity.js';
29
32
  import { consumeSetupCheckoutProjection, observeSetupCheckout, withSetupCheckoutProjection, } from '../setup-observation.js';
30
- import { loadCockpitConfig, consumeLoadedConfigProjection } from '../../config/load.js';
33
+ import { browserPreapprovalRequested, loadCockpitConfig, consumeLoadedConfigProjection, STRUCTURED_INPUT_BYTE_LIMIT } from '../../config/load.js';
31
34
  import { deriveRegistry } from '../../registry-derivation.js';
32
35
  import { confinedPath, lifecycleRefusal, ownDataValues, sameSource } from './shared.js';
36
+ import { readSiblingCheckouts, servedSiblings } from '../sibling-checkouts.js';
33
37
  // `npx --no cockpit logs --service <id>` is an ordinary invocation only for a declared service. A
34
38
  // cockpit's services are whatever its own Compose files resolve to, so they are read from Compose
35
39
  // rather than guessed, and an unreadable topology refuses.
@@ -50,6 +54,15 @@ function guardHeld(identity, what, why, fix) {
50
54
  : `a human must restore it with \`${formatShellCommand(['git', '-C', identity.root, 'restore', '--', fix.path])}\` (it is committed), or run ${sync}`;
51
55
  return `${what}; ${why}, so every tool call is held, including a sync run through this agent. Fix: ${action}.`;
52
56
  }
57
+ // An edit that does not widen what agents may do needs only a sync, which the agent may run itself;
58
+ // every other call waits for it, so no call is judged under a policy sync has not published.
59
+ function unsyncedHold(identity, detail) {
60
+ const sync = cockpitCommand('sync', { config: identity.configPath });
61
+ const reason = `${identity.configPath} changed since the guards were generated without widening what agents may do${detail}; every tool call except ${UNSYNCED_ADMITTED_PROSE} is held until it is synced. Fix: run \`${sync}\` (this agent may run it where [autonomy] sync = "agent"), then commit any regenerated files.`;
62
+ return { decision: { decision: 'unavailable', reason }, diagnostics: [{ severity: 'error', target: 'guard', message: reason,
63
+ code: 'GUARD_CONFIG_UNSYNCED',
64
+ fix: `run ${sync}; an agent may run it where [autonomy] sync = "agent"` }] };
65
+ }
53
66
  const GUARD_RESTORABLE = new Set(['.cockpit/generated-environment-ownership.json', '.cockpit/guards/bash-guard.sh',
54
67
  '.claude/settings.json', '.codex/hooks.json', '.opencode/plugin/cockpit-guards.js']);
55
68
  /** Installed bytes are authority only while their source, roots and publication still match. */
@@ -148,13 +161,25 @@ export async function runGuardEvaluationApplication(input, adapters) {
148
161
  const modulePending = settle(read(dirname(identity.module), basename(identity.module), 16_777_216));
149
162
  const configPending = settle(adapters.loadConfig(identity.configPath));
150
163
  const publicationPending = settle(readPublication());
164
+ const checkoutsPending = settle(readSiblingCheckouts(identity.root));
151
165
  const module = unwrap(await modulePending);
152
166
  if (module.sha256 !== identity.moduleSha256)
153
167
  return changed('the installed cockpit version changed (its handler bytes differ)');
154
168
  const loaded = unwrap(await configPending);
155
- if (loaded.kind !== 'complete' || loaded.result.source.path !== identity.configPath || loaded.result.source.sha256 !== identity.configSha256) {
169
+ if (loaded.kind !== 'complete' || loaded.result.source.path !== identity.configPath) {
156
170
  return unavailable(guardHeld(identity, `${identity.configPath} changed since the guards were generated`, 'guard policy comes from it and an edit takes effect only once a human approves it', { kind: 'sync' }));
157
171
  }
172
+ // Every check below runs against the configuration on disk; an edit since sync is admitted
173
+ // only when it does not widen the one the guards were generated from, and then only for sync.
174
+ const configSha256 = loaded.result.source.sha256;
175
+ const unsynced = configSha256 !== identity.configSha256;
176
+ if (unsynced) {
177
+ const local = (path) => readAnchoredFile(identity.root, path, STRUCTURED_INPUT_BYTE_LIMIT, node, { execute: adapters.execute ?? executeProbe });
178
+ const [recorded, confirmations] = await Promise.all([local(GUARD_LOCAL_PATHS.generatedConfig), local(ANSWER_CONFIRMATION_PATH)]);
179
+ const change = guardConfigChange(identity.configSha256, recorded.kind === 'complete' ? recorded.bytes : undefined, { sha256: configSha256, config: loaded.result.config }, confirmations.kind === 'complete' ? confirmations.bytes : undefined);
180
+ if (change.kind === 'widening' || change.kind === 'unrecorded')
181
+ return unavailable(guardHeld(identity, `${identity.configPath} changed since the guards were generated and ${guardConfigHeldReason(change)}`, 'guard policy comes from it and an edit that may widen it takes effect only once a human approves it', { kind: 'sync' }));
182
+ }
158
183
  const config = loaded.result.config;
159
184
  let publication;
160
185
  try {
@@ -200,9 +225,11 @@ export async function runGuardEvaluationApplication(input, adapters) {
200
225
  return unavailable(parsed.reason);
201
226
  if (parsed.cwd !== data.cwd)
202
227
  return unavailable('Guard request cwd does not match its invocation authority.');
203
- const prepared = await prepareGuardEvaluationPolicy({ configuration: loaded.result, checkout });
228
+ const prepared = await prepareGuardEvaluationPolicy({ configuration: loaded.result, checkout }, checkoutsPending.then(unwrap));
204
229
  if (prepared.kind !== 'complete')
205
- return unavailable('Guard policy authority could not be authenticated.');
230
+ return unsynced
231
+ ? unsyncedHold(identity, `, but the guard cannot build its policy from it (${prepared.diagnostics[0]?.message ?? 'no reason given'})`)
232
+ : unavailable('Guard policy authority could not be authenticated.');
206
233
  // The inspector answers synchronously, so what only Git can say is asked first: which protected
207
234
  // paths a default ripgrep search reads, and which a repository's index holds for a Git command
208
235
  // that prints committed content. A request not asked here gets the walk, or no answer.
@@ -333,7 +360,7 @@ export async function runGuardEvaluationApplication(input, adapters) {
333
360
  launched.push(proven);
334
361
  return proven;
335
362
  };
336
- const configured = { canonicalRoot: identity.root, configPath: identity.configPath };
363
+ const configured = { canonicalRoot: identity.root, configPath: identity.configPath, profiles: agentProfiles(config) };
337
364
  for (const { executable, cwd, argv } of parsed.invocations) {
338
365
  const proven = prove(executable, cwd, argv, true);
339
366
  // The evaluator judges a session's command as one more invocation, so its spelling needs proof too.
@@ -341,6 +368,9 @@ export async function runGuardEvaluationApplication(input, adapters) {
341
368
  if (inner)
342
369
  prove(inner[0], cwd, inner.slice(1), false);
343
370
  }
371
+ if (unsynced && !(parsed.tool === 'Bash' && parsed.invocations.length > 0 && parsed.invocations.every(invocation => launched.some(entry => entry.session && entry.executable === invocation.executable && entry.cwd === invocation.cwd
372
+ && isDeepStrictEqual(entry.argv, invocation.argv) && UNSYNCED_ADMITTED_COMMANDS.some(name => name === commandSpelling(entry.argv.slice(entry.prefix)).name)))))
373
+ return unsyncedHold(identity, '');
344
374
  let launcherSha256;
345
375
  if (launched.length) {
346
376
  const status = lstatSync(launcher);
@@ -370,24 +400,17 @@ export async function runGuardEvaluationApplication(input, adapters) {
370
400
  }
371
401
  if (cockpitInvocations.length + prepared.result.policy.projectCommands.length > GUARD_INPUT_LIMITS.invocations)
372
402
  return unavailable('Guard candidates exceed the invocation bound.');
373
- // The policy pin needs the text an edit applies to, which must be the text the guard was generated for.
403
+ // The policy pin needs the text an edit applies to, which must be the text the policy was built from.
374
404
  const configRead = publication.get(configName);
375
- if (configRead.sha256 !== identity.configSha256)
405
+ if (configRead.sha256 !== configSha256)
376
406
  return unavailable('Guard configuration changed during evaluation.');
377
407
  const configStatus = lstatSync(identity.configPath);
378
408
  // A hard link or another spelling of the configuration is the configuration.
379
409
  const isConfig = (path) => {
380
410
  if (foldGuardSpelling(path) === foldGuardSpelling(identity.configPath))
381
411
  return true;
382
- try {
383
- const status = lstatSync(path);
384
- return status.dev === configStatus.dev && status.ino === configStatus.ino;
385
- }
386
- catch (error) {
387
- if (hasErrorCode(error, 'ENOENT', 'ENOTDIR'))
388
- return false;
389
- throw error;
390
- }
412
+ const status = lstatIfPresentSync(path);
413
+ return status !== undefined && status.dev === configStatus.dev && status.ino === configStatus.ino;
391
414
  };
392
415
  const pinned = pinnedPolicyDenial(parsed, configRead.bytes.toString('utf8'), isConfig);
393
416
  if (pinned)
@@ -398,7 +421,7 @@ export async function runGuardEvaluationApplication(input, adapters) {
398
421
  searchPathRelativeEntry: guardSearchPathRelativeEntry(searchPath),
399
422
  ripgrepConfigActive: data.ripgrepConfigActive === true }, inspect);
400
423
  const current = await adapters.loadConfig(identity.configPath);
401
- if (current.kind !== 'complete' || current.result.source.sha256 !== identity.configSha256)
424
+ if (current.kind !== 'complete' || current.result.source.sha256 !== configSha256)
402
425
  return unavailable('Guard configuration changed during evaluation.');
403
426
  for (const [path, before] of [...roots, ...inspected]) {
404
427
  const after = lstatSync(path);
@@ -422,13 +445,41 @@ export async function runGuardEvaluationApplication(input, adapters) {
422
445
  return unavailable(`Guard authority could not be read or revalidated: ${errorCause(error)}`);
423
446
  }
424
447
  }
425
- // The guard runs only against the configuration sync generated it from, so the policy read here is the
426
- // one the last sync accepted.
448
+ // The guard runs against the configuration sync generated it from, or one that does not widen it and
449
+ // admits only sync and check, so the policy read here grants nothing the last sync did not.
427
450
  function ordinaryGrants(config) {
428
451
  const policy = effectivePolicy(config);
429
- return [...config.browser?.request_preapproval === true ? ['browser-preapproval'] : [],
452
+ return [...config.browser && browserPreapprovalRequested(config.browser) ? ['browser-preapproval'] : [],
430
453
  ...policy.autonomy.sync === 'agent' ? ['autonomy-sync'] : [], ...policy.autonomy.reset === 'agent' ? ['autonomy-reset'] : []];
431
454
  }
455
+ // `up --profile` and `session --profile` start that profile's services for one run. Only the default
456
+ // set a person declared is theirs to name, and a profile that mounts an operator-gated sibling, which
457
+ // touches live data or local hardware, never starts without a person.
458
+ function agentProfiles(config) {
459
+ return config.environment.profiles.filter(profile => !config.sibling.some(row => row.gate === 'operator' && row.mount === `profile:${profile}`));
460
+ }
461
+ /**
462
+ * Every checkout the guard grants for a sibling: each declared directory, then each checkout `use`
463
+ * recorded, which is where the agent works on that sibling. A recorded checkout that is gone grants
464
+ * nothing. Allowed roots, instance protection and project commands all come from this one list.
465
+ */
466
+ function siblingRoots(config, siblingsRoot, served) {
467
+ const declared = config.sibling.map((sibling, index) => {
468
+ const root = resolve(siblingsRoot, sibling.dir);
469
+ if (!confinedPath(siblingsRoot, root))
470
+ throw new RefusalError('guard policy sibling escapes its declared root');
471
+ return { sibling, index, root };
472
+ });
473
+ const recorded = declared.flatMap(({ sibling, index }) => {
474
+ const entry = served.get(sibling.slug);
475
+ if (!entry?.overridden)
476
+ return [];
477
+ if (!lstatIfPresentSync(entry.served)?.isDirectory())
478
+ return [];
479
+ return [{ sibling, index, root: entry.served }];
480
+ });
481
+ return [...declared, ...recorded];
482
+ }
432
483
  class GuardFileMissing extends RefusalError {
433
484
  path;
434
485
  constructor(path) {
@@ -439,8 +490,12 @@ class GuardFileMissing extends RefusalError {
439
490
  function guardPolicyRefusal(message) {
440
491
  return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, message, 'guard-policy');
441
492
  }
442
- /** Source projections share authenticated authority; installed consumption is admitted separately. */
443
- export async function prepareGuardEvaluationPolicy(input) {
493
+ /**
494
+ * Source projections share authenticated authority; installed consumption is admitted separately.
495
+ * `siblingCheckouts` is the configuration directory's sibling checkout record, when the caller
496
+ * started reading it already; it is read here otherwise.
497
+ */
498
+ export async function prepareGuardEvaluationPolicy(input, siblingCheckouts) {
444
499
  const inputData = ownDataValues(input, ['configuration', 'checkout']);
445
500
  const data = inputData && Object.hasOwn(inputData, 'configuration') && Object.hasOwn(inputData, 'checkout') ? inputData : undefined;
446
501
  if (!data)
@@ -455,12 +510,16 @@ export async function prepareGuardEvaluationPolicy(input) {
455
510
  if (derived.kind !== 'complete')
456
511
  return guardPolicyRefusal(`guard policy registry refused (${derived.code}): ${derived.message}`);
457
512
  const guardConfig = CockpitConfig.parse(derived.result.config);
513
+ const recorded = await (siblingCheckouts ?? readSiblingCheckouts(dirname(loaded.source.path)));
514
+ if (recorded.kind === 'refusal')
515
+ return guardPolicyRefusal(recorded.diagnostic.message);
516
+ const served = servedSiblings(dirname(loaded.source.path), guardConfig, recorded.checkouts);
458
517
  const observed = snapshotJson(data.checkout);
459
518
  if (observed?.state !== 'available')
460
519
  return guardPolicyRefusal('guard policy checkout observation is unavailable');
461
520
  try {
462
521
  const identities = new Map();
463
- let siblingsRoot;
522
+ let checkoutRoots;
464
523
  const result = await withSetupCheckoutProjection(data.checkout, {
465
524
  source: loaded.source, cockpitName: loaded.config.cockpit.name, evaluatedAt: Date.parse(observed.observedAt),
466
525
  }, (projection) => {
@@ -509,14 +568,11 @@ export async function prepareGuardEvaluationPolicy(input) {
509
568
  return path;
510
569
  };
511
570
  const canonicalRoot = directory(dirname(loaded.source.path));
512
- siblingsRoot = directory(resolve(canonicalRoot, config.cockpit.siblings_root));
571
+ checkoutRoots = siblingRoots(config, directory(resolve(canonicalRoot, config.cockpit.siblings_root)), served);
513
572
  const allowedRoots = new Set([canonicalRoot]);
514
573
  const instances = [];
515
574
  const templates = [];
516
- for (const sibling of config.sibling) {
517
- const root = resolve(siblingsRoot, sibling.dir);
518
- if (!confinedPath(siblingsRoot, root))
519
- throw new RefusalError('guard policy sibling escapes its declared root');
575
+ for (const { sibling, root } of checkoutRoots) {
520
576
  allowedRoots.add(directory(root));
521
577
  for (const file of derived.result.secretPaths.filter(row => row.slug === sibling.slug)) {
522
578
  const path = resolve(root, file.path);
@@ -571,16 +627,15 @@ export async function prepareGuardEvaluationPolicy(input) {
571
627
  return { kind: 'complete', status: 0, diagnostics: [],
572
628
  result: { policy } };
573
629
  });
574
- if (!result || result.kind !== 'complete' || !siblingsRoot) {
630
+ if (!result || result.kind !== 'complete' || !checkoutRoots) {
575
631
  return result ?? guardPolicyRefusal('guard policy checkout authority is copied, changed, expired or already consumed');
576
632
  }
577
633
  const boundaries = result.result.policy;
578
634
  const projectCommands = [];
579
- for (const [siblingIndex, sibling] of guardConfig.sibling.entries()) {
635
+ for (const { index: siblingIndex, sibling, root: checkoutRoot } of checkoutRoots) {
580
636
  const policies = guardConfig.environment.install_policies;
581
637
  if (!policies.some(policy => policy.toolchain === sibling.toolchain && (policy.build || policy.test)))
582
638
  continue;
583
- const checkoutRoot = resolve(siblingsRoot, sibling.dir);
584
639
  const checkoutId = deriveCheckoutId({ checkoutPath: checkoutRoot });
585
640
  const evaluatedAt = Date.now();
586
641
  const observation = await observeInstallPolicyLockfiles({ source: loaded.source, siblingIndex, siblingSlug: sibling.slug,
@@ -2,12 +2,12 @@ import type { ExecuteProbeOutcome } from '../../environment-exec/execute.js';
2
2
  import { RefusalError, type CheckoutId, type EnvironmentDiagnostic, type EnvironmentObservation, type ExecutionStep, type StackId } from '../contract.js';
3
3
  import { type ResolvedExecutable } from '../executable.js';
4
4
  import { type SetupCheckoutProjection } from '../setup-observation.js';
5
- import type { LoadedConfigProjection } from '../../config/load.js';
5
+ import { loadCockpitConfig, type LoadedConfigProjection } from '../../config/load.js';
6
6
  import { type EvaluatedObservation, type PreflightContext, type PreflightEvaluation } from '../observations.js';
7
7
  import { type ComparisonRemoteResult } from '../git-observation.js';
8
8
  import type { CapturedRuntimeEvidence, EnvironmentSnapshot } from '../evidence.js';
9
9
  import { type ReadinessRefusal } from './lifecycle-context.js';
10
- export interface ComparisonRemoteInput {
10
+ interface ComparisonRemoteInput {
11
11
  readonly executable: ResolvedExecutable;
12
12
  readonly checkoutRoot: string;
13
13
  readonly env: Readonly<Record<string, string>>;
@@ -29,8 +29,12 @@ export interface HealthObservationProducers {
29
29
  readonly ports: () => Promise<readonly EnvironmentObservation[]>;
30
30
  readonly runtime: () => Promise<readonly EnvironmentObservation[]>;
31
31
  readonly state: () => Promise<readonly EnvironmentObservation[]>;
32
+ readonly siblings: () => Promise<readonly EnvironmentObservation[]>;
32
33
  readonly templates: () => Promise<readonly EnvironmentObservation[]>;
33
34
  }
35
+ export type LoadedCockpitConfigResult = Extract<Awaited<ReturnType<typeof loadCockpitConfig>>, {
36
+ readonly kind: 'complete';
37
+ }>;
34
38
  export interface HealthObservationSourceInput {
35
39
  readonly checkout: SetupCheckoutProjection;
36
40
  readonly configuration: LoadedConfigProjection;
@@ -55,6 +59,11 @@ export interface ObservationCommandAdapters {
55
59
  readonly execute: (step: ExecutionStep, options: {
56
60
  readonly signal?: AbortSignal;
57
61
  }) => Promise<ExecuteProbeOutcome>;
62
+ /**
63
+ * Which checkout serves each sibling, what a worktree lacks, and whether the running stack is stale.
64
+ * Reads cockpit.toml again unless handed the loaded configuration or called after `load`.
65
+ */
66
+ readonly inspectServedSiblings?: (loaded?: LoadedCockpitConfigResult) => Promise<readonly EnvironmentDiagnostic[]>;
58
67
  }
59
68
  export interface PreflightCommandInput {
60
69
  readonly configPath: string;
@@ -65,13 +74,13 @@ export interface ComparisonRemoteEvaluation {
65
74
  readonly sibling: string;
66
75
  readonly result: ComparisonRemoteResult;
67
76
  }
68
- export interface ApplicationPreflightEvaluation {
77
+ interface ApplicationPreflightEvaluation {
69
78
  readonly verdict: PreflightEvaluation['verdict'];
70
79
  readonly revision: string;
71
80
  readonly checks: readonly EvaluatedObservation[];
72
81
  readonly comparisons: readonly ComparisonRemoteEvaluation[];
73
82
  }
74
- export type PreflightCommandResult = Readonly<{
83
+ type PreflightCommandResult = Readonly<{
75
84
  kind: 'complete';
76
85
  status: 0;
77
86
  diagnostics: readonly EnvironmentDiagnostic[];
@@ -122,7 +131,7 @@ interface PreparedHealthObservation {
122
131
  readonly notices: () => readonly EnvironmentDiagnostic[];
123
132
  readonly unobservable: ReadonlyMap<string, string>;
124
133
  }
125
- declare const healthProducerNames: readonly ["claims", "git", "host", "lifecycle", "ports", "runtime", "state", "templates", "envSchema", "surfaces"];
134
+ declare const healthProducerNames: readonly ["claims", "git", "host", "lifecycle", "ports", "runtime", "state", "templates", "envSchema", "siblings", "surfaces"];
126
135
  export declare function applicationDiagnostic(code: EnvironmentDiagnostic['code'], message: string, target: string, fix?: string): EnvironmentDiagnostic;
127
136
  export declare function issueHealthObservationSource(rawSource: HealthObservationSourceInput): HealthObservationSource | undefined;
128
137
  export declare function prepareHealthObservation(input: {
@@ -5,6 +5,7 @@ import { resolvedGitExecutable } from '../executable.js';
5
5
  import { declaresNothingToRun } from '../declaration.js';
6
6
  import { checkoutId as deriveCheckoutId, stackId as deriveStackId } from '../identity.js';
7
7
  import { consumeSetupCheckoutProjection } from '../setup-observation.js';
8
+ import { loadCockpitConfig } from '../../config/load.js';
8
9
  import { aggregateObservations, stoppedStackApplicability, } from '../observations.js';
9
10
  import { envSchemaToolMissing, VARLOCK_INSTALL_URL } from '../env-schema-observation.js';
10
11
  import { SURFACE_OBSERVATION_CODES } from '../generated-application/surface-observation.js';
@@ -25,7 +26,7 @@ export class ObservationProducerRefusal extends RefusalError {
25
26
  }
26
27
  const issuedHealthObservationSources = new WeakMap();
27
28
  const healthProducerNames = Object.freeze([
28
- 'claims', 'git', 'host', 'lifecycle', 'ports', 'runtime', 'state', 'templates', 'envSchema', 'surfaces',
29
+ 'claims', 'git', 'host', 'lifecycle', 'ports', 'runtime', 'state', 'templates', 'envSchema', 'siblings', 'surfaces',
29
30
  ]);
30
31
  export function applicationDiagnostic(code, message, target, fix) {
31
32
  return { severity: 'error', code, message, target, ...(fix === undefined ? {} : { fix }) };
@@ -348,6 +349,7 @@ function unavailablePreflight(message, forwarded) {
348
349
  export async function runPreflight(input, adapters) {
349
350
  let health;
350
351
  let refused;
352
+ let servedFindings = Promise.resolve([]);
351
353
  try {
352
354
  const source = await adapters.load(input, issueHealthObservationSource, (diagnostics) => { refused = diagnostics; return undefined; }, 'preflight');
353
355
  if (!source)
@@ -355,6 +357,8 @@ export async function runPreflight(input, adapters) {
355
357
  const observed = prepareHealthObservation(input, source);
356
358
  if (!observed)
357
359
  return unavailablePreflight('preflight requires authenticated steady lifecycle evidence');
360
+ // Reports only and catches its own errors, so it runs beside the health evaluation.
361
+ servedFindings = adapters.inspectServedSiblings?.() ?? Promise.resolve([]);
358
362
  health = await evaluateApplicationHealth(observed, adapters);
359
363
  }
360
364
  catch (error) {
@@ -370,10 +374,12 @@ export async function runPreflight(input, adapters) {
370
374
  };
371
375
  // A stopped stack never passes the gate, whatever the rows observable without it say.
372
376
  const passed = health.verdict === 'pass' && health.stopped === null;
377
+ // Warnings only: a stale stack still answers, and whether a changed lockfile matters is the product's call.
378
+ const served = await servedFindings;
373
379
  return {
374
380
  kind: passed ? 'complete' : 'blocked',
375
381
  status: passed ? 0 : 2,
376
- diagnostics: healthDiagnostics(health),
382
+ diagnostics: [...healthDiagnostics(health), ...served],
377
383
  result,
378
384
  };
379
385
  }
@@ -52,6 +52,8 @@ export interface RuntimeReadiness {
52
52
  readonly runtimeObservations: readonly EnvironmentObservation[];
53
53
  /** The digest of each file this runtime is started from, by the name a person edits. */
54
54
  readonly inputs: RuntimeInputs;
55
+ /** The Compose profiles this runtime activates, sorted. */
56
+ readonly activeProfiles: readonly string[];
55
57
  }
56
58
  export interface AcquiredPorts {
57
59
  readonly leaseResult: LeasePortFamilyResult;
@@ -65,7 +67,7 @@ interface LifecycleCommandAdapters {
65
67
  execute(step: ExecutionStep): Promise<ExecuteProbeOutcome>;
66
68
  withRuntimeReadiness<T>(context: LifecycleRunContext, use: (runtime: RuntimeReadiness) => Promise<T>, ports?: PortLeaseProjection, refuse?: ReadinessRefusal<T>, purpose?: RuntimeReadinessPurpose): Promise<T | undefined>;
67
69
  }
68
- export type SeedOutcome = Readonly<{
70
+ type SeedOutcome = Readonly<{
69
71
  kind: 'none' | 'current' | 'ran';
70
72
  }> | Readonly<{
71
73
  kind: 'refusal';
@@ -76,6 +78,11 @@ export interface UpCommandAdapters extends LifecycleCommandAdapters {
76
78
  seed?(context: LifecycleRunContext, up: Extract<UpCommandResult, {
77
79
  kind: 'complete';
78
80
  }>['result']): Promise<SeedOutcome>;
81
+ /**
82
+ * Records the siblings' lockfiles and rebuild inputs the stack runs on; warnings when it cannot.
83
+ * A resumed stack keeps the record of its start, because resuming restarts nothing.
84
+ */
85
+ recordStackInputs?(context: LifecycleRunContext, change: 'started' | 'resumed' | 'unchanged'): Promise<readonly EnvironmentDiagnostic[]>;
79
86
  withLifecycleStage<T>(context: LifecycleRunContext, use: (stage: LifecycleStageProjection) => Promise<T>, refuse?: ReadinessRefusal<T>): Promise<T | undefined>;
80
87
  withAcquiredPorts<T>(context: LifecycleRunContext, use: (ports: AcquiredPorts) => Promise<T>, refuse?: ReadinessRefusal<T>): Promise<T | undefined>;
81
88
  }
@@ -126,7 +133,7 @@ export type DownCommandResult = EnvironmentCommandOutcome<Readonly<{
126
133
  stackId: StackId;
127
134
  change: 'removed' | 'unchanged';
128
135
  }>>;
129
- export interface ResetPreviewResult {
136
+ interface ResetPreviewResult {
130
137
  readonly stackId: StackId;
131
138
  readonly planDigest: string;
132
139
  readonly targets: readonly ResetTargetInput[];
@@ -155,7 +162,7 @@ export interface OperatorResetSession {
155
162
  }>;
156
163
  decide(decision: 'approve' | 'decline'): Promise<OperatorResetResult>;
157
164
  }
158
- export interface DestructiveRunContextInput {
165
+ interface DestructiveRunContextInput {
159
166
  readonly lifecycle: LifecycleRunContext;
160
167
  readonly instance: SelectDestructiveInstanceInput;
161
168
  /** The holder claim the load took on a stack no session held, so this run could read it. */
@@ -6,7 +6,7 @@ import { abortStateTransaction, readState, transactState } from '../state.js';
6
6
  import { decodeLifecycleRecord, resolveLifecycleStage } from '../lifecycle-stage.js';
7
7
  import { inspectPortLeaseTransfer, revalidatePortLeaseProjection } from '../ports.js';
8
8
  import { commitDrainOutcome, decodeCoordinationRecord } from '../claims.js';
9
- import { lifecycleRefusal, ownerDiagnostic, shellQuote } from './shared.js';
9
+ import { lifecycleRefusal, ownerDiagnostic } from './shared.js';
10
10
  import { withLifecycleConfiguration } from './lifecycle-context.js';
11
11
  export function lifecycleSuggestion(context, command) {
12
12
  return cockpitCommand(command, { config: context.source.path });
@@ -82,7 +82,7 @@ export async function publishLifecycleRecord(context, expected, value, transfer,
82
82
  // Port leases are machine-wide, so up reads every cockpit's state. Another cockpit's namespace this
83
83
  // version cannot read is skipped there rather than refused; this cockpit's own unreadable state still
84
84
  // refuses in the command itself. The skipped ones are named in a single warning with one cleanup
85
- // command, and only by doctor: the folders belong to other cockpits, usually ones an earlier version
85
+ // command (`stacks prune`), and only by doctor: the folders belong to other cockpits, usually ones an earlier version
86
86
  // left behind, and repeating them on every up (and so every session) buried each result under lines
87
87
  // about cockpits the person was not working on. A port they still hold fails up on its own.
88
88
  export function unreadableStateWarning(skipped) {
@@ -96,7 +96,7 @@ export function unreadableStateWarning(skipped) {
96
96
  code: ENVIRONMENT_CODES.STATE_RECORD_MALFORMED,
97
97
  message: `${count} under ${directory} cannot be read by this cockpit version (${reasons}), so their claims and port leases were ignored`,
98
98
  target: directory,
99
- fix: `ask the person who owns this machine whether any cockpit still uses them; if none does, stop any stack they started (docker compose ls lists them), then delete them: rm -rf ${skipped.map(({ path }) => shellQuote(path)).join(' ')}`,
99
+ fix: `preview their removal with ${cockpitCommand('stacks prune')}, which removes those an earlier cockpit build left once none of their stacks is kept, then apply it with --apply`,
100
100
  };
101
101
  }
102
102
  // `up` keeps a holder whenever containers may exist, so a stack no session holds has nothing running.
@@ -147,6 +147,7 @@ export async function invalidateCompletedLifecycle(context, workBasisSha256, ope
147
147
  completedServices: [],
148
148
  ...(purpose === 'down' ? { teardownComplete: true } : {
149
149
  ...(stored.value.runtimeInputs ? { runtimeInputs: stored.value.runtimeInputs } : {}),
150
+ ...(stored.value.activeProfiles ? { activeProfiles: stored.value.activeProfiles } : {}),
150
151
  }),
151
152
  };
152
153
  const publication = await publishLifecycleRecord(context, stored.record, value);
@@ -1,4 +1,4 @@
1
- import { type LifecycleProgress } from '../contract.js';
1
+ import { type EnvironmentDiagnostic, type LifecycleProgress } from '../contract.js';
2
2
  import { type RuntimeServiceEvidence, type RuntimeServiceFailureEvidence, type RuntimeServiceObservation, type RuntimeTopologyEvidence } from '../runtime.js';
3
3
  import { type SessionCommandAdapters, type SessionCommandInput, type SessionCommandResult, type SleepCommandAdapters, type SleepCommandInput, type SleepCommandResult, type UpCommandAdapters, type UpCommandInput, type UpCommandResult } from './lifecycle-context.js';
4
4
  export { decodeLifecycleRecord } from '../lifecycle-stage.js';
@@ -12,6 +12,7 @@ type ServiceAdmission = {
12
12
  readonly logs?: RuntimeServiceFailureEvidence | RuntimeServiceObservation;
13
13
  };
14
14
  export declare function revalidateService(topology: RuntimeTopologyEvidence, completed: LifecycleProgress['completedServices'][number], adapters: Pick<UpCommandAdapters, 'execute'>): Promise<ServiceAdmission>;
15
+ export declare function collectFailureLogs(evidence: RuntimeServiceFailureEvidence | RuntimeServiceObservation | undefined, service: string, adapters: Pick<UpCommandAdapters, 'execute'>): Promise<readonly EnvironmentDiagnostic[]>;
15
16
  export declare function runUp(input: UpCommandInput, adapters: UpCommandAdapters): Promise<UpCommandResult>;
16
17
  export declare function runSession(input: SessionCommandInput, adapters: SessionCommandAdapters): Promise<SessionCommandResult>;
17
18
  export declare function runSleep(input: SleepCommandInput, adapters: SleepCommandAdapters): Promise<SleepCommandResult>;
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { constants as osConstants } from 'node:os';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
- import { basename, isAbsolute, join, resolve, sep } from 'node:path';
4
+ import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
5
5
  import { cleanupFencedExecution, repairFencedExecution } from '../../environment-exec/execute.js';
6
6
  import { ENVIRONMENT_CODES, forwardedRefusal, } from '../contract.js';
7
7
  import { cockpitCommand } from '../../fix-catalogue.js';
@@ -15,6 +15,7 @@ import { observationSatisfiesExpectation } from '../observations.js';
15
15
  import { issuedLifecycleRuns, lifecycleContextRefusal, withLifecycleConfiguration, withLifecycleRunNotices, } from './lifecycle-context.js';
16
16
  import { exactCommandConfigPath, invalidateCompletedLifecycle, lifecycleSuggestion, publishDrainOutcome, publishLifecycleRecord, readLifecycleRecord, repairRequired, unheldStack, } from './lifecycle-state.js';
17
17
  import { lifecycleRefusal, ownerDiagnostic, uniqueDiagnostics } from './shared.js';
18
+ import { forgetStackInputs } from '../stack-inputs.js';
18
19
  function seconds(millis) {
19
20
  return String(millis / 1_000);
20
21
  }
@@ -89,7 +90,7 @@ async function reportFailedService(evidence, service, context, adapters, stop) {
89
90
  ...(stopped === false ? [ownerDiagnostic(ENVIRONMENT_CODES.EXECUTION_COMPENSATION_STEP_FAILED, `the failed service ${service} could not be stopped, so it may keep restarting; run \`${lifecycleSuggestion(context, 'down')}\` to remove it`, service)] : []),
90
91
  ];
91
92
  }
92
- async function collectFailureLogs(evidence, service, adapters) {
93
+ export async function collectFailureLogs(evidence, service, adapters) {
93
94
  if (!evidence)
94
95
  return [ownerDiagnostic(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'bounded failure logs are unavailable', service)];
95
96
  const plan = planFailureLogs({ evidence, tail: 200, timeoutMs: 10_000, outputLimitBytes: 1_048_576 });
@@ -282,6 +283,7 @@ async function executeUpRound(context, adapters, forceReset, rememberAcquisition
282
283
  holderId: context.holderId,
283
284
  ...planned.plan.progress,
284
285
  runtimeInputs: runtime.inputs,
286
+ ...(runtime.activeProfiles.length > 0 ? { activeProfiles: runtime.activeProfiles } : {}),
285
287
  };
286
288
  if (!stored || resetProgress || stored.value.holderId !== context.holderId && !completedCoTenant) {
287
289
  // The first lifecycle data starts its durable history, as `setup` does (an existing
@@ -458,6 +460,9 @@ async function upWithContext(context, adapters) {
458
460
  diagnostics: uniqueDiagnostics([...result.diagnostics, ...data.diagnostics]) };
459
461
  if (data?.kind === 'ran' || data?.kind === 'current')
460
462
  result = { ...result, result: { ...result.result, seed: data.kind } };
463
+ const unrecorded = await adapters.recordStackInputs?.(context, result.result.change) ?? [];
464
+ if (unrecorded.length > 0)
465
+ result = { ...result, diagnostics: uniqueDiagnostics([...result.diagnostics, ...unrecorded]) };
461
466
  }
462
467
  if (result.kind === 'refusal' && releasable()) {
463
468
  const compensation = await compensateAcquiredPortHolder({
@@ -596,7 +601,10 @@ export async function runSleep(input, adapters) {
596
601
  return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle run authority is unavailable', configPath);
597
602
  }
598
603
  const context = loaded.result;
599
- return withLifecycleRunNotices(context, await sleepWithContext(context, adapters));
604
+ const outcome = await sleepWithContext(context, adapters);
605
+ // A stopped stack runs nothing a later doctor could call stale.
606
+ const forgotten = outcome.kind === 'complete' && outcome.result.change === 'stopped' ? await forgetStackInputs(dirname(context.source.path)) : [];
607
+ return withLifecycleRunNotices(context, forgotten.length === 0 ? outcome : { ...outcome, diagnostics: uniqueDiagnostics([...outcome.diagnostics, ...forgotten]) });
600
608
  }
601
609
  async function sleepWithContext(context, adapters) {
602
610
  let recovery = await inspectSleepDrainRecovery({
@@ -0,0 +1,60 @@
1
+ import type { ExecuteProbeOutcome, ForegroundProcessInput, ForegroundProcessOutcome } from '../../environment-exec/execute.js';
2
+ import { type EnvironmentCommandOutcome, type EnvironmentDiagnostic, type ExecutionStep, type LifecycleProgress } from '../contract.js';
3
+ import { type RuntimeServiceState, type RuntimeTopologyEvidence } from '../runtime.js';
4
+ export interface ServiceObservation {
5
+ readonly topology: RuntimeTopologyEvidence;
6
+ readonly completedServices: LifecycleProgress['completedServices'];
7
+ /** Records the container a rebuild put in place of the one the stack was started with. */
8
+ readonly recordContainer: (service: string, containerId: string) => Promise<readonly EnvironmentDiagnostic[]>;
9
+ }
10
+ export interface ServiceCommandAdapters {
11
+ readonly observe: (input: {
12
+ readonly configPath: string;
13
+ }) => Promise<ServiceObservation>;
14
+ readonly execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
15
+ readonly executeForeground: (input: ForegroundProcessInput) => Promise<ForegroundProcessOutcome>;
16
+ }
17
+ export declare function runServicePs(input: Readonly<{
18
+ configPath: string;
19
+ }>, adapters: ServiceCommandAdapters): Promise<EnvironmentCommandOutcome<Readonly<{
20
+ services: readonly RuntimeServiceState[];
21
+ }>>>;
22
+ /**
23
+ * `restart` restarts the container in place; `rebuild` builds the service's image and replaces its
24
+ * container, for a change baked into the image. Both then wait for the service as `up` does and
25
+ * report its logs when it does not become healthy.
26
+ */
27
+ export declare function runServiceRestart(input: Readonly<{
28
+ configPath: string;
29
+ service: string;
30
+ verb: 'restart' | 'rebuild';
31
+ }>, adapters: ServiceCommandAdapters): Promise<EnvironmentCommandOutcome<Readonly<{
32
+ service: string;
33
+ containerId: string;
34
+ change: 'restarted' | 'rebuilt';
35
+ }>>>;
36
+ type ServiceExecResult = Readonly<{
37
+ kind: 'complete';
38
+ status: number;
39
+ diagnostics: readonly EnvironmentDiagnostic[];
40
+ result: Readonly<{
41
+ exitCode: number;
42
+ signal: NodeJS.Signals | null;
43
+ }>;
44
+ }> | Readonly<{
45
+ kind: 'refusal';
46
+ status: number;
47
+ diagnostics: readonly EnvironmentDiagnostic[];
48
+ result: null;
49
+ }>;
50
+ /**
51
+ * Runs a command inside one service's container in the foreground. A command that exits nonzero
52
+ * is the command's answer, not a cockpit failure, so its status passes through as `session` does.
53
+ */
54
+ export declare function runServiceExec(input: Readonly<{
55
+ configPath: string;
56
+ service: string;
57
+ argv: readonly [string, ...string[]];
58
+ tty: boolean;
59
+ }>, adapters: ServiceCommandAdapters): Promise<ServiceExecResult>;
60
+ export {};