@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.
- package/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
interface PreparedGuardEvaluationPolicy {
|
|
36
37
|
readonly policy: GuardEvaluationPolicy;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
interface GuardEvaluationPolicyInput {
|
|
39
40
|
readonly configuration: LoadedConfigProjection;
|
|
40
41
|
readonly checkout: SetupCheckoutObservation;
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
383
|
-
|
|
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 !==
|
|
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
|
|
426
|
-
//
|
|
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
|
|
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
|
-
/**
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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' || !
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: `
|
|
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
|
-
|
|
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 {};
|