@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
|
@@ -40,7 +40,8 @@ function result(stage, projection, ownerOutcome, greenfield) {
|
|
|
40
40
|
...(greenfield?.interview === undefined ? {} : { interview: greenfield.interview }),
|
|
41
41
|
...(greenfield?.cockpitRoot === undefined ? {} : { cockpitRoot: greenfield.cockpitRoot }),
|
|
42
42
|
...(greenfield?.resetAnswer === undefined ? {} : { resetAnswer: greenfield.resetAnswer }),
|
|
43
|
-
...(greenfield?.unconfirmed === undefined ? {} : { unconfirmed: greenfield.unconfirmed })
|
|
43
|
+
...(greenfield?.unconfirmed === undefined ? {} : { unconfirmed: greenfield.unconfirmed }),
|
|
44
|
+
...(greenfield?.derived === undefined ? {} : { derived: greenfield.derived }) };
|
|
44
45
|
}
|
|
45
46
|
// The repository's directory name becomes the first sibling's slug and the cockpit's name stem.
|
|
46
47
|
const SIBLING_DIRECTORY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -78,24 +79,52 @@ function documentEstate(input) {
|
|
|
78
79
|
}
|
|
79
80
|
async function interviewPlan(input, adapters, classification, answered) {
|
|
80
81
|
const planned = plannedGreenfieldQuestions({ root: input.root, classification: classification.classification, gaps: classification.gaps });
|
|
82
|
+
const outstanding = planned.slice(answered);
|
|
81
83
|
const observedRoot = estateInput(input).root;
|
|
82
84
|
const inference = await adapters.infer?.({ root: observedRoot, configurationRoot: input.root,
|
|
83
85
|
repositories: classification.runnable.map(row => ({ slug: row.slug, path: relative(observedRoot, resolve(observedRoot, row.path)) || '.' })) });
|
|
86
|
+
const skipped = [...inference?.skipped ?? []];
|
|
87
|
+
// Each proposal is listed under every field it helps answer.
|
|
88
|
+
const proposals = [...nameProposal(input.root).map(row => ({ ...row, field: 'cockpit.name' })),
|
|
89
|
+
...(inference?.facts ?? []).flatMap(({ fields, ...row }) => fields.map(field => ({ ...row, field })))];
|
|
90
|
+
if (adapters.measureMachine && outstanding.some(row => row.field === 'environment.machine')) {
|
|
91
|
+
const measured = await adapters.measureMachine({ root: input.root });
|
|
92
|
+
if (measured.kind === 'unavailable')
|
|
93
|
+
skipped.push({ source: 'container runtime', reason: `was not measured (${measured.reason}), so no machine floor is proposed from it` });
|
|
94
|
+
else
|
|
95
|
+
proposals.push(...machineProposals(measured, proposals).map(row => ({ ...row, field: 'environment.machine' })));
|
|
96
|
+
}
|
|
84
97
|
return {
|
|
85
98
|
answered: planned.slice(0, answered).map(row => row.field),
|
|
86
|
-
outstanding:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
outstanding: outstanding.map(({ field, why }) => {
|
|
100
|
+
const own = proposals.filter(row => row.field === field).map(({ target, value, provenance }) => ({ target, value, provenance }));
|
|
101
|
+
return { field, why, confirmedBy: own.some(row => row.target === field && row.provenance.kind === 'derived') ? 'agent' : 'person',
|
|
102
|
+
proposals: own };
|
|
103
|
+
}),
|
|
104
|
+
inferenceSkipped: skipped,
|
|
91
105
|
...(input.cockpitRoot === undefined ? {} : { cockpitRoot: input.cockpitRoot.kind }),
|
|
92
106
|
};
|
|
93
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* This machine's runtime memory and CPUs, for each that no Compose limit records. It is what the
|
|
110
|
+
* machine offers, not what the product needs, so the reason says so; free disk is never proposed
|
|
111
|
+
* from a measurement, because what is free now shrinks with every image pulled and would make
|
|
112
|
+
* preflight refuse the next start.
|
|
113
|
+
*/
|
|
114
|
+
function machineProposals(measured, proposals) {
|
|
115
|
+
const date = measured.observedAt.slice(0, 10);
|
|
116
|
+
return [['runtime_memory_mb', measured.memoryMb, `${measured.memoryMb} MB of memory`],
|
|
117
|
+
['runtime_cpus', measured.cpus, `${measured.cpus} CPUs`]]
|
|
118
|
+
.filter(([key]) => !proposals.some(row => row.target === `environment.machine.${key}`))
|
|
119
|
+
.map(([key, value, offered]) => ({ target: `environment.machine.${key}`, value,
|
|
120
|
+
provenance: { kind: 'measured', source: `container runtime, ${date}`,
|
|
121
|
+
reason: `the container runtime on this machine offered ${offered} on ${date}, and no Compose limit records what the product needs; a floor at this machine's capacity refuses a smaller machine until a person lowers it` } }));
|
|
122
|
+
}
|
|
94
123
|
// The cockpit root's directory name is the proposed cockpit name when it is a valid one.
|
|
95
124
|
function nameProposal(root) {
|
|
96
125
|
const name = basename(root);
|
|
97
126
|
return /^[a-z0-9][a-z0-9-]*$/.test(name) ? [{ target: 'cockpit.name', value: name,
|
|
98
|
-
provenance: { kind: '
|
|
127
|
+
provenance: { kind: 'derived', source: root, reason: 'the cockpit root directory name' } }] : [];
|
|
99
128
|
}
|
|
100
129
|
function commandDigest(input) {
|
|
101
130
|
return sha256(JSON.stringify({ root: input.root, target: input.target, tools: input.tools, registry: input.registry ?? null,
|
|
@@ -690,8 +719,8 @@ function resetUnavailableFix(input, current) {
|
|
|
690
719
|
return `no answer is recorded yet: run ${resume('--json')} to preview, then answer the interview`;
|
|
691
720
|
}
|
|
692
721
|
}
|
|
693
|
-
/**
|
|
722
|
+
/** How a finished interview's answers were established, beyond a person's word: chosen or unattributed, and derived. */
|
|
694
723
|
function unconfirmedOf(document) {
|
|
695
|
-
const
|
|
696
|
-
return
|
|
724
|
+
const { unconfirmed, derived } = document.kind === 'complete' ? document.result : {};
|
|
725
|
+
return { ...(unconfirmed?.length ? { unconfirmed } : {}), ...(derived?.length ? { derived } : {}) };
|
|
697
726
|
}
|
|
@@ -4,7 +4,7 @@ import { type HolderProcess } from './claims.js';
|
|
|
4
4
|
import { type ExecutionStep } from './contract.js';
|
|
5
5
|
import { type PortLeaseProjection } from './ports.js';
|
|
6
6
|
import { type RuntimeMeasurementProbeAuthority, type RuntimeLifecycleProjection, type RuntimeTopologyEvidence } from './runtime.js';
|
|
7
|
-
|
|
7
|
+
interface LifecycleCommandCompositionInput {
|
|
8
8
|
readonly configPath: string;
|
|
9
9
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
10
10
|
readonly sessionId: string;
|
|
@@ -13,8 +13,9 @@ export interface LifecycleCommandCompositionInput {
|
|
|
13
13
|
readonly runtimeMeasurementProbeAuthority?: RuntimeMeasurementProbeAuthority | (() => RuntimeMeasurementProbeAuthority);
|
|
14
14
|
readonly holderProcess?: HolderProcess;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
type LifecycleCommandAdapterComposition = Omit<UpCommandAdapters & SleepCommandAdapters, 'withRuntimeReadiness' | 'seed' | 'recordStackInputs'> & {
|
|
17
17
|
seed: NonNullable<UpCommandAdapters['seed']>;
|
|
18
|
+
recordStackInputs: NonNullable<UpCommandAdapters['recordStackInputs']>;
|
|
18
19
|
withMachineFloorReadiness<T>(context: LifecycleRunContext, use: (observations: readonly import('./contract.js').EnvironmentObservation[]) => Promise<T>, refuse?: ReadinessRefusal<T>): Promise<T | undefined>;
|
|
19
20
|
withRuntimeReadiness<T>(context: LifecycleRunContext, use: (runtime: LifecycleRuntimeReadiness) => Promise<T>, ports?: PortLeaseProjection, refuse?: ReadinessRefusal<T>, purpose?: RuntimeReadinessPurpose): Promise<T | undefined>;
|
|
20
21
|
};
|
|
@@ -24,6 +25,7 @@ interface LifecycleRuntimeReadiness {
|
|
|
24
25
|
readonly hostObservations: readonly import('./contract.js').EnvironmentObservation[];
|
|
25
26
|
readonly runtimeObservations: readonly import('./contract.js').EnvironmentObservation[];
|
|
26
27
|
readonly inputs: Readonly<Record<string, string>>;
|
|
28
|
+
readonly activeProfiles: readonly string[];
|
|
27
29
|
}
|
|
28
30
|
export declare function createLifecycleCommandAdapters(rawInput: LifecycleCommandCompositionInput, execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): LifecycleCommandAdapterComposition;
|
|
29
31
|
export {};
|
|
@@ -6,6 +6,7 @@ import { loadCockpitConfig, projectMachineFloorConfig } from '../config/load.js'
|
|
|
6
6
|
import { cockpitCommand } from '../fix-catalogue.js';
|
|
7
7
|
import { redirectedBindSources } from './compose-topology.js';
|
|
8
8
|
import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
|
|
9
|
+
import { mounted } from '../schema.js';
|
|
9
10
|
import { COMPOSE_OWNERSHIP_FILE, renderComposeOwnership } from './compose-ownership.js';
|
|
10
11
|
import { issueLifecycleRunContext, noticeLifecycleRun, } from './application/lifecycle-context.js';
|
|
11
12
|
import { isProcessStart, observeProcessStart, processHost, releaseDeadSessionHolders } from './claims.js';
|
|
@@ -15,13 +16,16 @@ import { resolveDiskCapacityExecutable, resolveDockerExecutable, resolveExecutab
|
|
|
15
16
|
import { readSeededAt, recordSeeded, seedDue } from './seed.js';
|
|
16
17
|
import { observeHostCapacity } from './host-observation.js';
|
|
17
18
|
import { composeProjectName, holderId, stackId } from './identity.js';
|
|
18
|
-
import { projectLifecycleStage, resolveLifecycleStage, } from './lifecycle-stage.js';
|
|
19
|
+
import { decodeLifecycleRecord, projectLifecycleStage, resolveLifecycleStage, } from './lifecycle-stage.js';
|
|
20
|
+
import { readState } from './state.js';
|
|
19
21
|
import { acquirePortProjection, compensateAcquiredPortHolder, } from './ports.js';
|
|
20
22
|
import { admitRuntimeMeasurement, measureMachineFloor, createRuntimeAdapter, observeRuntimeTopology, issueMachineFloorAuthority, OBSERVATION_WINDOW_HEADROOM_MS, observeDockerRuntime, observeRuntimeMeasurement, projectRuntimeLifecycle, runtimeMeasurementProbeObservation, } from './runtime.js';
|
|
21
23
|
import { readLocalRuntimeMeasurements } from './runtime-measurement-record.js';
|
|
22
24
|
import { consumeSetupCheckoutProjection, observeSetupCheckout, withSetupCheckoutProjection, } from './setup-observation.js';
|
|
23
25
|
import { selectStateRoot } from './state.js';
|
|
24
26
|
import { readBoundedFile } from '../bounded-read.js';
|
|
27
|
+
import { loadServedSiblings, missingServedCheckout, servedPath, servedSiblingVariables } from './sibling-checkouts.js';
|
|
28
|
+
import { recordStackInputs } from './stack-inputs.js';
|
|
25
29
|
const OWNER = 'lifecycle-adapter-composition';
|
|
26
30
|
const COMPOSE_BYTE_LIMIT = 1_048_576;
|
|
27
31
|
const LISTENER_CONCURRENCY = 16;
|
|
@@ -32,8 +36,19 @@ function runtimeUnavailable(reason, target) {
|
|
|
32
36
|
return ownerDiagnostics(ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, reason, target);
|
|
33
37
|
}
|
|
34
38
|
/** Names each mount difference by the row that declared it, so the refusal says what to edit. */
|
|
35
|
-
function mountMismatchDiagnostics(mismatch, config,
|
|
36
|
-
const bySource = new Map(config.sibling.map((sibling) => [
|
|
39
|
+
function mountMismatchDiagnostics(mismatch, config, served) {
|
|
40
|
+
const bySource = new Map(config.sibling.map((sibling) => [servedPath(served, sibling.slug), sibling]));
|
|
41
|
+
// After `use` the running stack still mounts the checkout it started with: the sibling's served
|
|
42
|
+
// directory is missing and another one is mounted in its place. Compose is fine; the stack is old.
|
|
43
|
+
const switched = mismatch.unexpected.length === 0 ? undefined
|
|
44
|
+
: config.sibling.find((sibling) => mismatch.missing.includes(servedPath(served, sibling.slug)));
|
|
45
|
+
if (switched)
|
|
46
|
+
return [{
|
|
47
|
+
severity: 'error', code: ENVIRONMENT_CODES.MOUNT_POSTURE_UNAVAILABLE, target: `sibling:${switched.slug}`,
|
|
48
|
+
message: `sibling ${switched.slug} is served from ${servedPath(served, switched.slug)}, but the running stack mounts ${mismatch.unexpected.join(', ')} instead`,
|
|
49
|
+
fix: `if the stack started before use changed the checkout, run ${cockpitCommand('down')} and then ${cockpitCommand('up')}; `
|
|
50
|
+
+ `otherwise bind-mount \${${switched.var}} in a Compose service and run ${cockpitCommand('check')}`,
|
|
51
|
+
}];
|
|
37
52
|
return [
|
|
38
53
|
...mismatch.missing.map((source) => {
|
|
39
54
|
const sibling = bySource.get(source);
|
|
@@ -56,8 +71,8 @@ function mountMismatchDiagnostics(mismatch, config, siblingsRoot) {
|
|
|
56
71
|
];
|
|
57
72
|
}
|
|
58
73
|
/** Names the sibling a redirected bind sits in, so the refusal says which checkout holds the link. */
|
|
59
|
-
function redirectedMountDiagnostics(redirected, config,
|
|
60
|
-
const bySource = new Map(config.sibling.map((sibling) => [
|
|
74
|
+
function redirectedMountDiagnostics(redirected, config, served) {
|
|
75
|
+
const bySource = new Map(config.sibling.map((sibling) => [servedPath(served, sibling.slug), sibling]));
|
|
61
76
|
return redirected.map(({ bind, holder }) => {
|
|
62
77
|
const sibling = bySource.get(holder);
|
|
63
78
|
return {
|
|
@@ -67,6 +82,23 @@ function redirectedMountDiagnostics(redirected, config, siblingsRoot) {
|
|
|
67
82
|
};
|
|
68
83
|
});
|
|
69
84
|
}
|
|
85
|
+
// A profile no Compose service declares enables nothing, and Compose accepts it silently, so the
|
|
86
|
+
// stack would start without the services the person asked for.
|
|
87
|
+
function undefinedProfileDiagnostics(profiles, config, requested, configPath) {
|
|
88
|
+
return profiles.map((profile) => {
|
|
89
|
+
const source = config.environment.profiles.includes(profile) ? `environment.profiles in ${basename(configPath)}`
|
|
90
|
+
: requested.includes(profile) ? 'up --profile' : 'the profile set the running stack was started with';
|
|
91
|
+
return {
|
|
92
|
+
severity: 'error', code: 'COMPOSE_PROFILE_UNDEFINED',
|
|
93
|
+
target: `profile:${profile}`,
|
|
94
|
+
message: `profile ${profile}, from ${source}, is declared by no service in environment.compose_files, so it would start nothing`,
|
|
95
|
+
fix: 'add the profile to the services it should start in the Compose file, or stop requesting it: remove it from environment.profiles or --profile, and for a stack started with it run npx --no cockpit down --config <your cockpit.toml>',
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// The runtime input naming the profile set, so `up` refuses to change the set under a running stack
|
|
100
|
+
// as it refuses any other change to what the stack was started from.
|
|
101
|
+
const ACTIVE_PROFILES_INPUT = 'active Compose profiles';
|
|
70
102
|
function refusal(target, message = 'lifecycle composition authority is unavailable', code = ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID) {
|
|
71
103
|
return {
|
|
72
104
|
kind: 'refusal',
|
|
@@ -258,6 +290,9 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
258
290
|
if (declaresNothingToRun(loaded.result.config.environment))
|
|
259
291
|
return { kind: 'refusal',
|
|
260
292
|
status: 2, result: null, diagnostics: [runtimeNotDeclared(loaded.result.source.path)] };
|
|
293
|
+
const served = await loadServedSiblings(dirname(loaded.result.source.path), loaded.result.config);
|
|
294
|
+
if (served.kind === 'refusal')
|
|
295
|
+
return { kind: 'refusal', status: 2, result: null, diagnostics: [served.diagnostic] };
|
|
261
296
|
const setupEnvironment = Object.fromEntries(Object.entries(input.env)
|
|
262
297
|
.filter((entry) => typeof entry[1] === 'string'));
|
|
263
298
|
const git = await resolveGitExecutable(input.env);
|
|
@@ -334,6 +369,7 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
334
369
|
config: loaded.result.config,
|
|
335
370
|
checkoutRoot: dirname(loaded.result.source.path),
|
|
336
371
|
siblingsRoot: resolve(dirname(loaded.result.source.path), loaded.result.config.cockpit.siblings_root),
|
|
372
|
+
served: served.siblings,
|
|
337
373
|
...(projected && 'authority' in projected ? { machineFloorAuthority: projected.authority } : {}),
|
|
338
374
|
});
|
|
339
375
|
return { ...issued, diagnostics: loaded.diagnostics };
|
|
@@ -417,7 +453,10 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
417
453
|
const withRuntimeReadiness = (context, use, ports, refuse, purpose) => runtimeReadiness(context, use, ports, refuse, purpose);
|
|
418
454
|
// `resolvedMounts` is set only on a teardown retry: stopping or removing a stack must not depend
|
|
419
455
|
// on its mounts matching cockpit.toml, or a stack whose declaration drifted could never be retired.
|
|
420
|
-
const runtimeReadiness = async (context, use, ports, refuse, purpose, resolvedMounts
|
|
456
|
+
const runtimeReadiness = async (context, use, ports, refuse, purpose, resolvedMounts,
|
|
457
|
+
// Set only on a teardown retry, for the same reason: a recorded profile the Compose files no
|
|
458
|
+
// longer declare must not keep down and sleep from retiring the stack.
|
|
459
|
+
droppedProfiles = []) => {
|
|
421
460
|
const binding = bindings.get(context);
|
|
422
461
|
if (!input || !binding)
|
|
423
462
|
return undefined;
|
|
@@ -428,6 +467,12 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
428
467
|
if (!machine) {
|
|
429
468
|
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.CONFIG_SCHEMA_INVALID, 'cockpit.toml declares no [environment.machine] table; runtime commands need its runtime_memory_mb, runtime_cpus, free_disk_mb and cold_start_seconds', 'environment.machine'));
|
|
430
469
|
}
|
|
470
|
+
// Teardown must reach a stack whatever it mounts; anything else would mount a directory that is gone.
|
|
471
|
+
if (purpose !== 'teardown') {
|
|
472
|
+
const gone = await missingServedCheckout(currentBinding.served);
|
|
473
|
+
if (gone)
|
|
474
|
+
return refuse?.([gone]);
|
|
475
|
+
}
|
|
431
476
|
const env = runtimeEnvironment(input.env);
|
|
432
477
|
const docker = await resolveDockerExecutable(input.env);
|
|
433
478
|
if (docker.state !== 'available')
|
|
@@ -461,7 +506,18 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
461
506
|
? observeRuntimeMeasurement(measured, context.stackId) : undefined;
|
|
462
507
|
let artifacts;
|
|
463
508
|
let inputs = { [basename(context.source.path)]: context.runtimeSha256 };
|
|
464
|
-
|
|
509
|
+
// A running stack keeps the profiles it started with, so every later command, down
|
|
510
|
+
// included, sees the services they enable; the run adds the declared default set and any
|
|
511
|
+
// profiles `up --profile` requested.
|
|
512
|
+
const recorded = await readState(context.namespace.paths.lifecycle(context.stackId));
|
|
513
|
+
if (recorded.kind !== 'complete' && recorded.kind !== 'missing')
|
|
514
|
+
return refuse?.(ownerDiagnostics(recorded.code, recorded.message, recorded.target));
|
|
515
|
+
const recordedValue = recorded.kind === 'complete' ? decodeLifecycleRecord(recorded.value, context)?.value : undefined;
|
|
516
|
+
if (recorded.kind === 'complete' && !recordedValue)
|
|
517
|
+
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'lifecycle progress record is malformed', context.namespace.paths.lifecycle(context.stackId).path));
|
|
518
|
+
const recordedProfiles = recordedValue?.activeProfiles ?? [];
|
|
519
|
+
const activeProfiles = [...new Set([...currentBinding.config.environment.profiles,
|
|
520
|
+
...input.activeProfiles ?? [], ...recordedProfiles])].filter((profile) => !droppedProfiles.includes(profile)).sort();
|
|
465
521
|
let expectedMountSources;
|
|
466
522
|
const declared = await composeArtifacts(currentBinding.checkoutRoot, currentBinding.config.environment.compose_files);
|
|
467
523
|
if (!declared)
|
|
@@ -479,11 +535,13 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
479
535
|
if (!override)
|
|
480
536
|
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.GENERATED_ENVIRONMENT_UNAVAILABLE, `${COMPOSE_OWNERSHIP_FILE} is missing or does not match the current Compose files and cockpit.toml; run ${cockpitCommand('sync')}`, COMPOSE_OWNERSHIP_FILE));
|
|
481
537
|
artifacts = [...declared, override];
|
|
482
|
-
inputs = { ...inputs, ...Object.fromEntries(declared.map(({ reference, sha256 }) => [reference, sha256]))
|
|
538
|
+
inputs = { ...inputs, ...Object.fromEntries(declared.map(({ reference, sha256 }) => [reference, sha256])),
|
|
539
|
+
...(activeProfiles.length === 0 ? {} : {
|
|
540
|
+
[ACTIVE_PROFILES_INPUT]: createHash('sha256').update(activeProfiles.join('\n')).digest('hex')
|
|
541
|
+
}) };
|
|
483
542
|
expectedMountSources = [...new Set([
|
|
484
|
-
...currentBinding.config
|
|
485
|
-
.
|
|
486
|
-
.map((sibling) => resolve(currentBinding.siblingsRoot, sibling.dir)),
|
|
543
|
+
...mounted(currentBinding.config, activeProfiles)
|
|
544
|
+
.map((sibling) => servedPath(currentBinding.served, sibling.slug)),
|
|
487
545
|
...currentBinding.config.environment.generated_host_paths
|
|
488
546
|
.map((path) => resolve(currentBinding.checkoutRoot, path)),
|
|
489
547
|
])].sort();
|
|
@@ -491,8 +549,7 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
491
549
|
return undefined;
|
|
492
550
|
if (resolvedMounts)
|
|
493
551
|
expectedMountSources = resolvedMounts;
|
|
494
|
-
const siblingDirectories =
|
|
495
|
-
.map((sibling) => [sibling.var, resolve(currentBinding.siblingsRoot, sibling.dir)]));
|
|
552
|
+
const siblingDirectories = servedSiblingVariables(currentBinding.config, currentBinding.served);
|
|
496
553
|
const batchId = randomBytes(32).toString('hex');
|
|
497
554
|
const runtime = createRuntimeAdapter({
|
|
498
555
|
runtime: observed.runtime,
|
|
@@ -530,19 +587,24 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
530
587
|
execute,
|
|
531
588
|
});
|
|
532
589
|
if (topology.kind !== 'complete') {
|
|
590
|
+
if (topology.undefinedProfiles && purpose === 'teardown') {
|
|
591
|
+
return runtimeReadiness(context, use, ports, refuse, purpose, resolvedMounts, [...droppedProfiles, ...topology.undefinedProfiles]);
|
|
592
|
+
}
|
|
593
|
+
if (topology.undefinedProfiles)
|
|
594
|
+
return refuse?.(undefinedProfileDiagnostics(topology.undefinedProfiles, currentBinding.config, input.activeProfiles ?? [], context.source.path));
|
|
533
595
|
const mismatch = topology.mounts;
|
|
534
596
|
if (!mismatch)
|
|
535
597
|
return undefined;
|
|
536
598
|
if (purpose === 'teardown' && !resolvedMounts) {
|
|
537
599
|
const resolved = [...expectedMountSources.filter((source) => !mismatch.missing.includes(source)), ...mismatch.unexpected].sort();
|
|
538
|
-
return runtimeReadiness(context, use, ports, refuse, purpose, resolved);
|
|
600
|
+
return runtimeReadiness(context, use, ports, refuse, purpose, resolved, droppedProfiles);
|
|
539
601
|
}
|
|
540
|
-
return refuse?.(mountMismatchDiagnostics(mismatch, currentBinding.config, currentBinding.
|
|
602
|
+
return refuse?.(mountMismatchDiagnostics(mismatch, currentBinding.config, currentBinding.served));
|
|
541
603
|
}
|
|
542
604
|
if (purpose !== 'teardown') {
|
|
543
605
|
const redirected = await redirectedBindSources(topology.topology.services.flatMap(({ bindMountSources }) => bindMountSources), expectedMountSources);
|
|
544
606
|
if (redirected.length > 0)
|
|
545
|
-
return refuse?.(redirectedMountDiagnostics(redirected, currentBinding.config, currentBinding.
|
|
607
|
+
return refuse?.(redirectedMountDiagnostics(redirected, currentBinding.config, currentBinding.served));
|
|
546
608
|
}
|
|
547
609
|
const disk = await resolveDiskCapacityExecutable();
|
|
548
610
|
if (disk.state !== 'available')
|
|
@@ -566,6 +628,7 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
566
628
|
runtimeObservations: measurementObservation
|
|
567
629
|
? [measurementObservation, ...topology.observations] : [...topology.observations],
|
|
568
630
|
inputs,
|
|
631
|
+
activeProfiles,
|
|
569
632
|
};
|
|
570
633
|
issuedRuntimeReadiness.add(readiness);
|
|
571
634
|
return use(readiness);
|
|
@@ -671,5 +734,9 @@ export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
|
671
734
|
throw error;
|
|
672
735
|
}
|
|
673
736
|
};
|
|
674
|
-
|
|
737
|
+
const recordInputs = async (context, change) => {
|
|
738
|
+
const binding = bindings.get(context);
|
|
739
|
+
return binding ? recordStackInputs(binding.checkoutRoot, binding.config, binding.served, { keepExisting: change !== 'started' }) : [];
|
|
740
|
+
};
|
|
741
|
+
return { load, execute, seed, recordStackInputs: recordInputs, withRuntimeReadiness, withMachineFloorReadiness, withLifecycleStage, withAcquiredPorts };
|
|
675
742
|
}
|
|
@@ -24,13 +24,18 @@ export interface LifecycleRecordValue extends LifecycleProgress {
|
|
|
24
24
|
* refused declaration change can say which one changed.
|
|
25
25
|
*/
|
|
26
26
|
readonly runtimeInputs?: RuntimeInputs;
|
|
27
|
+
/**
|
|
28
|
+
* The Compose profiles the stack was started with, sorted, when it runs with any: every later
|
|
29
|
+
* command on the stack activates the same set, so down removes the profile services too.
|
|
30
|
+
*/
|
|
31
|
+
readonly activeProfiles?: readonly string[];
|
|
27
32
|
}
|
|
28
33
|
export type RuntimeInputs = Readonly<Record<string, string>>;
|
|
29
34
|
export interface CurrentLifecycleRecord {
|
|
30
35
|
readonly record: StateRecord;
|
|
31
36
|
readonly value: LifecycleRecordValue;
|
|
32
37
|
}
|
|
33
|
-
|
|
38
|
+
type EarlierNameResult = Readonly<{
|
|
34
39
|
kind: 'complete';
|
|
35
40
|
name: string | undefined;
|
|
36
41
|
}> | Readonly<{
|
|
@@ -59,11 +64,11 @@ declare const lifecycleStageProjectionBrand: unique symbol;
|
|
|
59
64
|
export interface LifecycleStageProjection {
|
|
60
65
|
readonly [lifecycleStageProjectionBrand]: true;
|
|
61
66
|
}
|
|
62
|
-
|
|
67
|
+
interface LifecycleStageEvidence {
|
|
63
68
|
readonly stackId: StackId;
|
|
64
69
|
readonly stage: ObservationLifecycleStage;
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
type LifecycleStageResolution = {
|
|
67
72
|
readonly kind: 'complete';
|
|
68
73
|
readonly authority: LifecycleStageAuthority;
|
|
69
74
|
readonly observation: EnvironmentObservation;
|
|
@@ -69,7 +69,7 @@ export function earlierNameRefusalMessage(configPath, earlierName) {
|
|
|
69
69
|
export function decodeLifecycleRecord(record, context) {
|
|
70
70
|
const value = snapshotJson(record.value);
|
|
71
71
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
72
|
-
|| !exactKeys(value, ['completedServices', 'holderId', 'operationGeneration', 'workBasisSha256'], ['runtimeInputs', 'teardownComplete'])
|
|
72
|
+
|| !exactKeys(value, ['completedServices', 'holderId', 'operationGeneration', 'workBasisSha256'], ['activeProfiles', 'runtimeInputs', 'teardownComplete'])
|
|
73
73
|
|| record.format !== 'cockpit-state/v1' || record.kind !== 'lifecycle'
|
|
74
74
|
|| record.cockpitId !== context.cockpitId || record.subjectId !== context.stackId)
|
|
75
75
|
return undefined;
|
|
@@ -93,6 +93,15 @@ export function decodeLifecycleRecord(record, context) {
|
|
|
93
93
|
return undefined;
|
|
94
94
|
runtimeInputs = Object.fromEntries(entries);
|
|
95
95
|
}
|
|
96
|
+
let activeProfiles;
|
|
97
|
+
if (Object.hasOwn(candidate, 'activeProfiles')) {
|
|
98
|
+
const value = candidate.activeProfiles;
|
|
99
|
+
if (teardownComplete || !Array.isArray(value) || value.length === 0 || value.length > 64
|
|
100
|
+
|| value.some((profile, index) => typeof profile !== 'string' || !lifecycleService.test(profile)
|
|
101
|
+
|| index > 0 && value[index - 1] >= profile))
|
|
102
|
+
return undefined;
|
|
103
|
+
activeProfiles = value;
|
|
104
|
+
}
|
|
96
105
|
const completed = candidate.completedServices.map((item) => {
|
|
97
106
|
if (!item || typeof item !== 'object' || Array.isArray(item)
|
|
98
107
|
|| !exactKeys(item, ['containerId', 'service']))
|
|
@@ -112,6 +121,7 @@ export function decodeLifecycleRecord(record, context) {
|
|
|
112
121
|
holderId: candidate.holderId,
|
|
113
122
|
...(teardownComplete ? { teardownComplete: true } : {}),
|
|
114
123
|
...(runtimeInputs ? { runtimeInputs } : {}),
|
|
124
|
+
...(activeProfiles ? { activeProfiles } : {}),
|
|
115
125
|
workBasisSha256: candidate.workBasisSha256,
|
|
116
126
|
operationGeneration: candidate.operationGeneration,
|
|
117
127
|
completedServices: completed,
|
|
@@ -112,8 +112,8 @@ export declare function collectLogs(plan: Extract<LogPlan, {
|
|
|
112
112
|
records: never[];
|
|
113
113
|
truncated?: undefined;
|
|
114
114
|
} | {
|
|
115
|
-
reason?: undefined;
|
|
116
115
|
code?: undefined;
|
|
116
|
+
reason?: undefined;
|
|
117
117
|
kind: "cancelled" | "complete";
|
|
118
118
|
records: Readonly<{
|
|
119
119
|
stackId: StackId;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `Promise.all(items.map(run))` with at most `limit` runs in flight; results keep the items' order. */
|
|
2
|
+
export async function mapLimit(items, limit, run) {
|
|
3
|
+
const results = new Array(items.length);
|
|
4
|
+
let next = 0;
|
|
5
|
+
const worker = async () => {
|
|
6
|
+
while (next < items.length) {
|
|
7
|
+
const index = next++;
|
|
8
|
+
results[index] = await run(items[index]);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
12
|
+
return results;
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EnvironmentDiagnostic } from './contract.js';
|
|
2
|
+
import type { McpServer } from '../surfaces/mcp.js';
|
|
3
|
+
/**
|
|
4
|
+
* What each approved MCP server needs before a tool can start it, read without starting anything or
|
|
5
|
+
* touching the network: a local server's command and a remote server's headers helper resolve from
|
|
6
|
+
* where the wrapper runs them, and every environment name the server reads is set. A disabled server
|
|
7
|
+
* starts in no tool, so it is not checked. Diagnostics name variables, never their values.
|
|
8
|
+
*/
|
|
9
|
+
export declare function observeMcpStartup(servers: readonly McpServer[], env: Readonly<Record<string, string | undefined>>): Promise<readonly EnvironmentDiagnostic[]>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { locateExecutable } from './executable.js';
|
|
2
|
+
// Presence only: the doctor needs to know a command can be started, not its identity, so it
|
|
3
|
+
// does not hash the binary the way a verified spawn does.
|
|
4
|
+
async function commandReason(command, cwd, env) {
|
|
5
|
+
const located = await locateExecutable(command, env, cwd);
|
|
6
|
+
return located.state === 'present' ? undefined : located.reason;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What each approved MCP server needs before a tool can start it, read without starting anything or
|
|
10
|
+
* touching the network: a local server's command and a remote server's headers helper resolve from
|
|
11
|
+
* where the wrapper runs them, and every environment name the server reads is set. A disabled server
|
|
12
|
+
* starts in no tool, so it is not checked. Diagnostics name variables, never their values.
|
|
13
|
+
*/
|
|
14
|
+
export async function observeMcpStartup(servers, env) {
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
const launches = new Map();
|
|
17
|
+
for (const server of servers.filter(row => row.enabled)) {
|
|
18
|
+
const launch = 'transport' in server ? server.helper : server;
|
|
19
|
+
if (launch !== null)
|
|
20
|
+
launches.set(JSON.stringify([launch.command, launch.cwd]), commandReason(launch.command, launch.cwd, env));
|
|
21
|
+
}
|
|
22
|
+
for (const server of servers.filter(row => row.enabled)) {
|
|
23
|
+
const launch = 'transport' in server ? server.helper : server;
|
|
24
|
+
if (launch !== null) {
|
|
25
|
+
const reason = await launches.get(JSON.stringify([launch.command, launch.cwd]));
|
|
26
|
+
if (reason !== undefined)
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
severity: 'warning',
|
|
29
|
+
code: 'MCP_COMMAND_UNRESOLVED',
|
|
30
|
+
target: server.name,
|
|
31
|
+
message: `the ${'transport' in server ? 'headers helper' : 'command'} of MCP server ${server.name}, ${launch.command} in ${launch.cwd}, does not resolve to an executable file (${reason}), so no tool can start ${'transport' in server ? 'its connection' : 'it'}`,
|
|
32
|
+
fix: 'install the command or correct it in the sibling\'s MCP declaration, then run npx --no cockpit sync --config <your cockpit.toml>; a changed declaration needs a person to approve its new sha256 under [mcp.approved]',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const unset = server.envNames.filter(name => !env[name]);
|
|
36
|
+
if (unset.length > 0)
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
code: 'MCP_ENVIRONMENT_UNSET',
|
|
40
|
+
target: server.name,
|
|
41
|
+
message: `MCP server ${server.name} reads ${unset.join(', ')}, which ${unset.length === 1 ? 'is' : 'are'} unset or empty here, so a tool started from this environment ${'transport' in server ? 'cannot send the server its credential' : 'cannot start it'}`,
|
|
42
|
+
fix: `set each named variable in the environment the agent tool is started from${'transport' in server ? ', or give the server a headersHelper that reads the credential when the tool connects' : ''}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return diagnostics;
|
|
46
|
+
}
|
|
@@ -22,7 +22,7 @@ export interface PreflightEvaluation {
|
|
|
22
22
|
export interface DoctorCheck extends EvaluatedObservation {
|
|
23
23
|
readonly evidence: string;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
interface DoctorProjection {
|
|
26
26
|
readonly verdict: PreflightEvaluation['verdict'];
|
|
27
27
|
readonly checks: readonly DoctorCheck[];
|
|
28
28
|
}
|
|
@@ -7,6 +7,7 @@ import { observationDeclarations as envSchemaDeclarations } from './env-schema-o
|
|
|
7
7
|
import { observationDeclarations as gitDeclarations } from './git-observation.js';
|
|
8
8
|
import { observationDeclarations as hostDeclarations } from './host-observation.js';
|
|
9
9
|
import { observationDeclarations as lifecycleStageDeclarations } from './lifecycle-stage.js';
|
|
10
|
+
import { observationDeclarations as siblingDeclarations } from './sibling-observation.js';
|
|
10
11
|
import { observationDeclarations as runtimeDeclarations, runtimeHealthObservationDeclarations } from './runtime.js';
|
|
11
12
|
import { observationDeclarations as stateDeclarations } from './state.js';
|
|
12
13
|
import { observationDeclarations as templateDeclarations } from './templates.js';
|
|
@@ -33,6 +34,7 @@ export const allObservationDeclarations = Object.freeze([
|
|
|
33
34
|
...hostDeclarations,
|
|
34
35
|
...lifecycleStageDeclarations,
|
|
35
36
|
...runtimeDeclarations,
|
|
37
|
+
...siblingDeclarations,
|
|
36
38
|
...stateDeclarations,
|
|
37
39
|
...templateDeclarations,
|
|
38
40
|
...surfaceObservationDeclarations,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type EffectivePolicy, type PolicyWeakening } from '../autonomy.js';
|
|
2
2
|
import { type Diagnostic } from './contract.js';
|
|
3
3
|
import type { ResetCommandInput } from './application/lifecycle-context.js';
|
|
4
|
+
import type { StacksPruneScope } from './stack-inventory-prune.js';
|
|
4
5
|
import type { GeneratedEnvironmentCommandInput, GeneratedEnvironmentSyncInput, GeneratedStaleRemovalApproval, GeneratedStaleRemovalApprovalRequest } from './generated-application/shared.js';
|
|
5
|
-
|
|
6
|
+
interface OperatorAdmission {
|
|
6
7
|
readonly interactive: boolean;
|
|
7
8
|
readonly json: boolean;
|
|
8
9
|
readonly ci: string | undefined;
|
|
@@ -10,21 +11,21 @@ export interface OperatorAdmission {
|
|
|
10
11
|
readonly apply?: boolean;
|
|
11
12
|
}
|
|
12
13
|
/** The autonomy policy a gated command reads, or the refusal that it could not be read. */
|
|
13
|
-
|
|
14
|
+
type OperatorPolicy = Readonly<{
|
|
14
15
|
current: EffectivePolicy;
|
|
15
16
|
recorded?: EffectivePolicy;
|
|
16
17
|
record: () => Promise<void>;
|
|
17
18
|
}>;
|
|
18
|
-
|
|
19
|
+
interface OperatorCommandOutcome {
|
|
19
20
|
readonly status: number;
|
|
20
21
|
readonly diagnostics: readonly Diagnostic[];
|
|
21
22
|
readonly result: unknown;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
interface OperatorSession {
|
|
24
25
|
readonly preview: OperatorCommandOutcome;
|
|
25
26
|
decide(decision: 'approve' | 'decline'): Promise<OperatorCommandOutcome>;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
interface ResetOperatorApplicationAdapters {
|
|
28
29
|
readonly preview: (input: ResetCommandInput) => Promise<OperatorCommandOutcome>;
|
|
29
30
|
readonly begin: (input: ResetCommandInput) => Promise<OperatorSession | OperatorCommandOutcome>;
|
|
30
31
|
readonly confirm: (preview: OperatorCommandOutcome) => Promise<boolean>;
|
|
@@ -33,7 +34,7 @@ export interface ResetOperatorApplicationAdapters {
|
|
|
33
34
|
export type GeneratedEnvironmentSyncHandlerInput = GeneratedEnvironmentSyncInput & Readonly<{
|
|
34
35
|
authorizeStaleRemoval?: (request: GeneratedStaleRemovalApprovalRequest) => Promise<GeneratedStaleRemovalApproval | undefined>;
|
|
35
36
|
}>;
|
|
36
|
-
|
|
37
|
+
interface SyncOperatorApplicationAdapters {
|
|
37
38
|
readonly synchronize: (input: GeneratedEnvironmentSyncHandlerInput) => Promise<OperatorCommandOutcome>;
|
|
38
39
|
readonly confirmStaleRemoval: (request: GeneratedStaleRemovalApprovalRequest) => Promise<string | undefined>;
|
|
39
40
|
readonly policy: (configPath: string) => Promise<OperatorPolicy | OperatorCommandOutcome>;
|
|
@@ -45,6 +46,34 @@ export interface SyncOperatorApplicationAdapters {
|
|
|
45
46
|
/** The production policy adapter: cockpit.toml's policy now, the recorded one, and how to record the current one. */
|
|
46
47
|
export declare function loadOperatorPolicy(configPath: string): Promise<OperatorPolicy | OperatorCommandOutcome>;
|
|
47
48
|
export declare function runResetOperatorApplication(input: ResetCommandInput, admission: OperatorAdmission, adapters: ResetOperatorApplicationAdapters): Promise<OperatorCommandOutcome>;
|
|
49
|
+
export interface StacksPruneOperatorSession {
|
|
50
|
+
readonly preview: OperatorCommandOutcome;
|
|
51
|
+
readonly digest: string;
|
|
52
|
+
readonly empty: boolean;
|
|
53
|
+
readonly deletesData: boolean;
|
|
54
|
+
apply(): Promise<OperatorCommandOutcome>;
|
|
55
|
+
}
|
|
56
|
+
interface StacksPruneOperatorAdapters {
|
|
57
|
+
readonly begin: (input: Readonly<{
|
|
58
|
+
configPath: string;
|
|
59
|
+
scope: StacksPruneScope;
|
|
60
|
+
}>) => Promise<StacksPruneOperatorSession | OperatorCommandOutcome>;
|
|
61
|
+
/** Shows the preview to a person at a terminal and returns the digest they typed. */
|
|
62
|
+
readonly confirm: (preview: OperatorCommandOutcome) => Promise<string | undefined>;
|
|
63
|
+
readonly policy: (configPath: string) => Promise<OperatorPolicy | OperatorCommandOutcome>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `stacks prune`: a preview by default. Removing an orphan whose checkout is gone is housekeeping
|
|
67
|
+
* anyone can undo by bringing the stack up again, so `--apply` passes for an agent; a prune that
|
|
68
|
+
* deletes volumes deletes data, and passes only where `[autonomy] reset` lets this run reset. That
|
|
69
|
+
* policy is this cockpit's, so an agent's `--apply` covers only this cockpit's stacks: another
|
|
70
|
+
* cockpit may require a person for the same removal. `--interactive` is a person's approval of the
|
|
71
|
+
* exact preview by its digest, and covers every stack on the machine.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runStacksPruneOperatorApplication(input: Readonly<{
|
|
74
|
+
configPath: string;
|
|
75
|
+
}>, admission: OperatorAdmission, adapters: StacksPruneOperatorAdapters): Promise<OperatorCommandOutcome>;
|
|
48
76
|
export declare function runSyncOperatorApplication(input: GeneratedEnvironmentCommandInput & Readonly<{
|
|
49
77
|
dryRun?: boolean;
|
|
50
78
|
}>, admission: OperatorAdmission, adapters: SyncOperatorApplicationAdapters): Promise<OperatorCommandOutcome>;
|
|
79
|
+
export {};
|