@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0
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 +375 -0
- package/LICENSE +21 -0
- package/README.md +21 -1
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2574 -0
- package/dist/autonomy.d.ts +44 -0
- package/dist/autonomy.js +156 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +62 -0
- package/dist/browser/commands.js +349 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +117 -0
- package/dist/browser/runtime.js +183 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +556 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +139 -0
- package/dist/cli-main.js +1405 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +113 -0
- package/dist/command-table.js +512 -0
- package/dist/config/load.d.ts +171 -0
- package/dist/config/load.js +345 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +335 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +425 -0
- package/dist/environment/application/doctor.d.ts +89 -0
- package/dist/environment/application/doctor.js +235 -0
- package/dist/environment/application/guard.d.ts +43 -0
- package/dist/environment/application/guard.js +622 -0
- package/dist/environment/application/health.d.ts +134 -0
- package/dist/environment/application/health.js +379 -0
- package/dist/environment/application/lifecycle-context.d.ts +228 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +201 -0
- package/dist/environment/application/lifecycle.d.ts +17 -0
- package/dist/environment/application/lifecycle.js +729 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1709 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +117 -0
- package/dist/environment/compose-topology.js +413 -0
- package/dist/environment/contract.d.ts +489 -0
- package/dist/environment/contract.js +761 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +26 -0
- package/dist/environment/executable.js +167 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +189 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +276 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +484 -0
- package/dist/environment/generated-application/shared.js +349 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +389 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +699 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +109 -0
- package/dist/environment/git-observation.d.ts +183 -0
- package/dist/environment/git-observation.js +544 -0
- package/dist/environment/health-composition.d.ts +11 -0
- package/dist/environment/health-composition.js +628 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +308 -0
- package/dist/environment/init-application.js +697 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +29 -0
- package/dist/environment/lifecycle-composition.js +675 -0
- package/dist/environment/lifecycle-stage.d.ts +115 -0
- package/dist/environment/lifecycle-stage.js +398 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +344 -0
- package/dist/environment/operator-application.d.ts +50 -0
- package/dist/environment/operator-application.js +155 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/read-composition.d.ts +27 -0
- package/dist/environment/read-composition.js +352 -0
- package/dist/environment/readiness-composition.d.ts +38 -0
- package/dist/environment/readiness-composition.js +130 -0
- package/dist/environment/reclaim.d.ts +333 -0
- package/dist/environment/reclaim.js +1835 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +505 -0
- package/dist/environment/runtime.js +2335 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/state.d.ts +371 -0
- package/dist/environment/state.js +2013 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +96 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +102 -0
- package/dist/environment-exec/execute.js +1175 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +275 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/evaluator.d.ts +113 -0
- package/dist/guards/evaluator.js +1226 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +40 -0
- package/dist/guards/local-identity.js +123 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +175 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +40 -0
- package/dist/init/command.js +310 -0
- package/dist/init/derive.d.ts +51 -0
- package/dist/init/derive.js +715 -0
- package/dist/init/draft.d.ts +138 -0
- package/dist/init/draft.js +1205 -0
- package/dist/init/render.d.ts +35 -0
- package/dist/init/render.js +82 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1322 -0
- package/dist/schema.js +934 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +180 -0
- package/dist/surfaces/harness-registry.js +117 -0
- package/dist/surfaces/mcp.d.ts +95 -0
- package/dist/surfaces/mcp.js +186 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +681 -0
- package/dist/surfaces/render.d.ts +118 -0
- package/dist/surfaces/render.js +633 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +512 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/work-orders/application.d.ts +99 -0
- package/dist/work-orders/application.js +383 -0
- package/dist/work-orders/composition.d.ts +124 -0
- package/dist/work-orders/composition.js +1010 -0
- package/dist/work-orders/contract.d.ts +61 -0
- package/dist/work-orders/contract.js +122 -0
- package/dist/work-orders/store.d.ts +158 -0
- package/dist/work-orders/store.js +197 -0
- package/docs/README.md +65 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +50 -0
- package/docs/cli-reference.md +599 -0
- package/docs/command-list.md +51 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +171 -0
- package/docs/configuration-essentials.md +172 -0
- package/docs/configuration-reference.md +621 -0
- package/docs/environment.md +760 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +111 -0
- package/docs/getting-started.md +608 -0
- package/docs/harness-surfaces.md +433 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +419 -0
- package/docs/troubleshooting.md +361 -0
- package/docs/tutorial.md +170 -0
- package/docs/work-and-browser.md +254 -0
- package/libexec/browser-operation.cjs +186 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +76 -1
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +54 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +61 -0
- package/surfaces/skills/procedures/fix.md +81 -0
- package/surfaces/skills/procedures/setup.md +379 -0
- package/surfaces/skills/procedures/work.md +105 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface RepositoryIdentity {
|
|
2
|
+
readonly host: string;
|
|
3
|
+
readonly owner: string;
|
|
4
|
+
readonly repository: string;
|
|
5
|
+
}
|
|
6
|
+
export type RepositoryIdentityResult = Readonly<{
|
|
7
|
+
kind: 'known';
|
|
8
|
+
identity: Readonly<RepositoryIdentity>;
|
|
9
|
+
}> | Readonly<{
|
|
10
|
+
kind: 'unknown';
|
|
11
|
+
reason: 'credentialed' | 'malformed' | 'unbounded' | 'unsupported';
|
|
12
|
+
}>;
|
|
13
|
+
export declare function repositoryIdentity(value: unknown): RepositoryIdentityResult;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const INPUT_LIMIT_BYTES = 2_048;
|
|
2
|
+
const PATH_SEGMENT_LIMIT = 32;
|
|
3
|
+
const PATH_SEGMENT_LIMIT_BYTES = 255;
|
|
4
|
+
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
5
|
+
const SAFE_HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6
|
+
function unknown(reason) {
|
|
7
|
+
return { kind: 'unknown', reason };
|
|
8
|
+
}
|
|
9
|
+
function normalizedHost(value) {
|
|
10
|
+
const host = value.toLowerCase().replace(/\.$/, '');
|
|
11
|
+
if (host.length === 0 || Buffer.byteLength(host) > 253)
|
|
12
|
+
return undefined;
|
|
13
|
+
const labels = host.split('.');
|
|
14
|
+
if (labels.every((label) => /^(?:\d+|0x[0-9a-f]+)$/.test(label))) {
|
|
15
|
+
if (labels.length !== 4 || labels.some((label) => !/^(?:0|[1-9]\d{0,2})$/.test(label) || Number(label) > 255)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return host;
|
|
19
|
+
}
|
|
20
|
+
if (labels.some((label) => label.length === 0 || label.length > 63 || !SAFE_HOST_LABEL.test(label)))
|
|
21
|
+
return undefined;
|
|
22
|
+
return host;
|
|
23
|
+
}
|
|
24
|
+
function known(hostValue, pathValue) {
|
|
25
|
+
const host = normalizedHost(hostValue);
|
|
26
|
+
const path = pathValue.startsWith('/') ? pathValue.slice(1) : pathValue;
|
|
27
|
+
if (!host || path.length === 0 || path.startsWith('/') || path.endsWith('/') || path.includes('//')) {
|
|
28
|
+
return unknown('malformed');
|
|
29
|
+
}
|
|
30
|
+
const segments = path.split('/');
|
|
31
|
+
if (segments.length > PATH_SEGMENT_LIMIT)
|
|
32
|
+
return unknown('malformed');
|
|
33
|
+
if (segments.some((segment) => segment === '.' || segment === '..' || !SAFE_PATH_SEGMENT.test(segment))) {
|
|
34
|
+
return unknown('malformed');
|
|
35
|
+
}
|
|
36
|
+
let repository = segments.pop();
|
|
37
|
+
if (repository.endsWith('.git'))
|
|
38
|
+
repository = repository.slice(0, -4);
|
|
39
|
+
if (repository.length === 0 || repository === '.' || repository === '..')
|
|
40
|
+
return unknown('malformed');
|
|
41
|
+
if ([...segments, repository].some((segment) => Buffer.byteLength(segment) > PATH_SEGMENT_LIMIT_BYTES)) {
|
|
42
|
+
return unknown('unbounded');
|
|
43
|
+
}
|
|
44
|
+
const identity = { host, owner: segments.join('/'), repository };
|
|
45
|
+
return { kind: 'known', identity };
|
|
46
|
+
}
|
|
47
|
+
function urlIdentity(value, scheme) {
|
|
48
|
+
if (scheme !== 'https' && scheme !== 'ssh')
|
|
49
|
+
return unknown('unsupported');
|
|
50
|
+
const authorityStart = scheme.length + 3;
|
|
51
|
+
if (value.length === authorityStart || value[authorityStart] === '/')
|
|
52
|
+
return unknown('malformed');
|
|
53
|
+
const remainder = value.slice(authorityStart);
|
|
54
|
+
const pathStart = remainder.indexOf('/');
|
|
55
|
+
if (pathStart < 1)
|
|
56
|
+
return unknown('malformed');
|
|
57
|
+
const authority = remainder.slice(0, pathStart);
|
|
58
|
+
const path = remainder.slice(pathStart);
|
|
59
|
+
const at = authority.indexOf('@');
|
|
60
|
+
if (at === 0 || at !== authority.lastIndexOf('@'))
|
|
61
|
+
return unknown('malformed');
|
|
62
|
+
let hostAndPort = authority;
|
|
63
|
+
if (at > 0) {
|
|
64
|
+
const username = authority.slice(0, at);
|
|
65
|
+
if (scheme === 'https' || username !== 'git')
|
|
66
|
+
return unknown('credentialed');
|
|
67
|
+
hostAndPort = authority.slice(at + 1);
|
|
68
|
+
}
|
|
69
|
+
const colon = hostAndPort.indexOf(':');
|
|
70
|
+
let host = hostAndPort;
|
|
71
|
+
if (colon >= 0) {
|
|
72
|
+
if (colon !== hostAndPort.lastIndexOf(':'))
|
|
73
|
+
return unknown('unsupported');
|
|
74
|
+
host = hostAndPort.slice(0, colon);
|
|
75
|
+
const port = hostAndPort.slice(colon + 1);
|
|
76
|
+
if (host.length === 0 || port.length === 0)
|
|
77
|
+
return unknown('malformed');
|
|
78
|
+
if (port !== (scheme === 'https' ? '443' : '22'))
|
|
79
|
+
return unknown('unsupported');
|
|
80
|
+
}
|
|
81
|
+
return known(host, path);
|
|
82
|
+
}
|
|
83
|
+
export function repositoryIdentity(value) {
|
|
84
|
+
if (typeof value !== 'string')
|
|
85
|
+
return unknown('malformed');
|
|
86
|
+
if (Buffer.byteLength(value) > INPUT_LIMIT_BYTES)
|
|
87
|
+
return unknown('unbounded');
|
|
88
|
+
if (value.length === 0 || /[\s\x00-\x1f\x7f]/u.test(value) || /[%?#]/.test(value))
|
|
89
|
+
return unknown('malformed');
|
|
90
|
+
if (/^[A-Za-z]:/.test(value) || /^[./~\\]/.test(value))
|
|
91
|
+
return unknown('unsupported');
|
|
92
|
+
if (value.includes('\\'))
|
|
93
|
+
return unknown('malformed');
|
|
94
|
+
if (/(?:^|\/)\.{1,2}(?:\/|$)/.test(value))
|
|
95
|
+
return unknown('malformed');
|
|
96
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(value);
|
|
97
|
+
if (scheme)
|
|
98
|
+
return urlIdentity(value, scheme[1].toLowerCase());
|
|
99
|
+
const scp = /^(?:([^@/:]+)@)?([^@/:]+):(.+)$/.exec(value);
|
|
100
|
+
if (!scp)
|
|
101
|
+
return unknown('malformed');
|
|
102
|
+
const [, username, host, path] = scp;
|
|
103
|
+
if (username !== undefined && username !== 'git')
|
|
104
|
+
return unknown('credentialed');
|
|
105
|
+
if (path.startsWith('/'))
|
|
106
|
+
return unknown('malformed');
|
|
107
|
+
return known(host, path);
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const Row: z.ZodObject<{
|
|
3
|
+
runtime: z.ZodString;
|
|
4
|
+
daemon: z.ZodString;
|
|
5
|
+
engineVersion: z.ZodString;
|
|
6
|
+
composeFloor: z.ZodString;
|
|
7
|
+
measuredOn: z.ZodString;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
/** One passing measurement: the runtime product, the daemon `docker info` named, its engine version and the Compose floor. */
|
|
10
|
+
export type RuntimeMeasurementRow = Readonly<z.infer<typeof Row>>;
|
|
11
|
+
export type LocalRuntimeMeasurements = Readonly<{
|
|
12
|
+
kind: 'read';
|
|
13
|
+
path: string;
|
|
14
|
+
rows: readonly RuntimeMeasurementRow[];
|
|
15
|
+
}> | Readonly<{
|
|
16
|
+
kind: 'unreadable';
|
|
17
|
+
path: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** Read the rows; a missing file is an empty record, anything else unreadable names why. */
|
|
21
|
+
export declare function readLocalRuntimeMeasurements(stateRoot: string): Promise<LocalRuntimeMeasurements>;
|
|
22
|
+
export type RecordResult = Readonly<{
|
|
23
|
+
kind: 'complete';
|
|
24
|
+
path: string;
|
|
25
|
+
replaced: RuntimeMeasurementRow | null;
|
|
26
|
+
discardedUnreadable: string | null;
|
|
27
|
+
}> | Readonly<{
|
|
28
|
+
kind: 'refusal';
|
|
29
|
+
path: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Record one passing measurement, replacing the row for the same runtime and daemon. A record that
|
|
34
|
+
* cannot be read is replaced too, because every row in it is re-derivable by measuring, and the
|
|
35
|
+
* result says so; refusing would leave an agent no way forward.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recordLocalRuntimeMeasurement(stateRoot: string, row: RuntimeMeasurementRow): Promise<RecordResult>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { errorCause, hasErrorCode } from './contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Owns this machine's runtime measurements: `machine/runtime-measurements.json` under the per-user
|
|
4
|
+
* state root, one row per measured runtime. A row is keyed by the runtime product, the daemon
|
|
5
|
+
* `docker info` names and the engine version, and says nothing about a checkout, so every checkout
|
|
6
|
+
* and git worktree of every cockpit on this machine shares it; a different runtime, daemon or
|
|
7
|
+
* engine matches no row and is refused until it is measured. It is not something the team declares,
|
|
8
|
+
* so it is not in cockpit.toml: teammates on different runtimes would overwrite each other's rows,
|
|
9
|
+
* and recording one would change guarded configuration that only a human `sync` accepts.
|
|
10
|
+
*
|
|
11
|
+
* It reads and writes the file and nothing else: `runtime-measurement.ts` produces rows and
|
|
12
|
+
* `runtime.ts` decides whether a row admits the observed runtime.
|
|
13
|
+
*/
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
import { lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { readBoundedInput } from '../config/load.js';
|
|
19
|
+
const MEASUREMENTS_PATH = 'machine/runtime-measurements.json';
|
|
20
|
+
const FORMAT = 'cockpit-runtime-measurements/v1';
|
|
21
|
+
const ROW_LIMIT = 32;
|
|
22
|
+
const Text = z.string().min(1).max(128).regex(/^[^\u0000-\u001f\u007f]+$/);
|
|
23
|
+
const Row = z.object({
|
|
24
|
+
runtime: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
25
|
+
daemon: Text,
|
|
26
|
+
engineVersion: Text,
|
|
27
|
+
composeFloor: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
28
|
+
measuredOn: z.string().date(),
|
|
29
|
+
}).strict();
|
|
30
|
+
const MeasurementFile = z.object({ format: z.literal(FORMAT), measurements: z.array(Row).max(ROW_LIMIT) }).strict();
|
|
31
|
+
/** Read the rows; a missing file is an empty record, anything else unreadable names why. */
|
|
32
|
+
export async function readLocalRuntimeMeasurements(stateRoot) {
|
|
33
|
+
const path = join(stateRoot, MEASUREMENTS_PATH);
|
|
34
|
+
let bytes;
|
|
35
|
+
try {
|
|
36
|
+
const read = await readBoundedInput(path);
|
|
37
|
+
if (read.kind !== 'complete')
|
|
38
|
+
return { kind: 'unreadable', path, reason: 'the file is larger than cockpit reads' };
|
|
39
|
+
bytes = read.bytes;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (hasErrorCode(error, 'ENOENT'))
|
|
43
|
+
return { kind: 'read', path, rows: [] };
|
|
44
|
+
return { kind: 'unreadable', path, reason: `the file could not be read (${errorCause(error)})` };
|
|
45
|
+
}
|
|
46
|
+
let value;
|
|
47
|
+
try {
|
|
48
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
49
|
+
}
|
|
50
|
+
catch { // Parser text would quote the file; the reason names the two ways it fails.
|
|
51
|
+
return { kind: 'unreadable', path, reason: 'the file is not UTF-8 JSON' };
|
|
52
|
+
}
|
|
53
|
+
const parsed = MeasurementFile.safeParse(value);
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
const issue = parsed.error.issues[0];
|
|
56
|
+
return { kind: 'unreadable', path,
|
|
57
|
+
reason: `the file is not a ${FORMAT} record (${issue ? `${issue.path.join('.') || 'record'}: ${issue.message}` : 'invalid'})` };
|
|
58
|
+
}
|
|
59
|
+
return { kind: 'read', path, rows: parsed.data.measurements.map((row) => row) };
|
|
60
|
+
}
|
|
61
|
+
async function plainDirectory(path) {
|
|
62
|
+
try {
|
|
63
|
+
const status = await lstat(path);
|
|
64
|
+
return status.isDirectory() && !status.isSymbolicLink() ? 'present' : `${path} is not a plain directory`;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return hasErrorCode(error, 'ENOENT') ? 'absent' : `${path} is unavailable (${errorCause(error)})`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Rename replaces a planted symlink at the destination instead of writing through it.
|
|
71
|
+
async function replaceFile(directory, name, bytes) {
|
|
72
|
+
const temporary = join(directory, `.${name}.${randomBytes(8).toString('hex')}.tmp`);
|
|
73
|
+
try {
|
|
74
|
+
await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
|
|
75
|
+
await rename(temporary, join(directory, name));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await rm(temporary, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Record one passing measurement, replacing the row for the same runtime and daemon. A record that
|
|
83
|
+
* cannot be read is replaced too, because every row in it is re-derivable by measuring, and the
|
|
84
|
+
* result says so; refusing would leave an agent no way forward.
|
|
85
|
+
*/
|
|
86
|
+
export async function recordLocalRuntimeMeasurement(stateRoot, row) {
|
|
87
|
+
const path = join(stateRoot, MEASUREMENTS_PATH);
|
|
88
|
+
const checked = Row.safeParse(row);
|
|
89
|
+
if (!checked.success)
|
|
90
|
+
return { kind: 'refusal', path, reason: `the measurement row is malformed (${checked.error.issues[0]?.message ?? 'invalid'})` };
|
|
91
|
+
try {
|
|
92
|
+
const directory = join(stateRoot, 'machine');
|
|
93
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
94
|
+
const state = await plainDirectory(directory);
|
|
95
|
+
if (state !== 'present')
|
|
96
|
+
return { kind: 'refusal', path, reason: state };
|
|
97
|
+
const current = await readLocalRuntimeMeasurements(stateRoot);
|
|
98
|
+
const existing = current.kind === 'read' ? current.rows : [];
|
|
99
|
+
const replaced = existing.find((candidate) => candidate.runtime === row.runtime && candidate.daemon === row.daemon) ?? null;
|
|
100
|
+
const kept = existing.filter((candidate) => candidate !== replaced);
|
|
101
|
+
if (kept.length >= ROW_LIMIT) {
|
|
102
|
+
return { kind: 'refusal', path, reason: `the record already holds ${ROW_LIMIT} other runtimes; remove the ones this machine no longer runs` };
|
|
103
|
+
}
|
|
104
|
+
const record = { format: FORMAT, measurements: [...kept, checked.data] };
|
|
105
|
+
await replaceFile(directory, 'runtime-measurements.json', `${JSON.stringify(record, null, 2)}\n`);
|
|
106
|
+
return { kind: 'complete', path, replaced, discardedUnreadable: current.kind === 'unreadable' ? current.reason : null };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return { kind: 'refusal', path, reason: `the record could not be written (${errorCause(error)})` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ExecuteProbeOutcome } from '../environment-exec/execute.js';
|
|
2
|
+
import type { Diagnostic } from '../diagnostics.js';
|
|
3
|
+
import { type ExecutionStep } from './contract.js';
|
|
4
|
+
import { type RuntimeProbeRequest, type RuntimeProbeVerdict } from './runtime-probe.js';
|
|
5
|
+
export interface RuntimeMeasureInput {
|
|
6
|
+
readonly configPath: string;
|
|
7
|
+
readonly image: string;
|
|
8
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
9
|
+
}
|
|
10
|
+
export type RuntimeMeasureProbe = (request: Omit<RuntimeProbeRequest, 'installedModule'>) => Promise<RuntimeProbeVerdict>;
|
|
11
|
+
export interface RuntimeMeasureDependencies {
|
|
12
|
+
readonly execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
|
|
13
|
+
readonly probe: RuntimeMeasureProbe;
|
|
14
|
+
}
|
|
15
|
+
export interface RuntimeMeasureOutcome {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly result: unknown;
|
|
18
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
19
|
+
readonly artifact?: string | null;
|
|
20
|
+
readonly destination?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function runRuntimeMeasure(input: RuntimeMeasureInput, dependencies: RuntimeMeasureDependencies): Promise<RuntimeMeasureOutcome>;
|
|
23
|
+
/**
|
|
24
|
+
* Production probe binding. The probe only certifies itself when it runs from an installed package,
|
|
25
|
+
* so a source checkout is told that directly instead of failing deep inside the probe.
|
|
26
|
+
*/
|
|
27
|
+
export declare function probeInstalledRuntime(request: Omit<RuntimeProbeRequest, 'installedModule'>): Promise<RuntimeProbeVerdict>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns `cockpit runtime measure`: identifies the local container runtime, clears what an earlier
|
|
3
|
+
* interrupted measure left behind (`runtime-probe-residue.ts`), hands the runtime to the candidate
|
|
4
|
+
* and broken-control probe (`runtime-probe.ts`), and records a passing verdict as this machine's
|
|
5
|
+
* measurement through `runtime-measurement-record.ts`. cockpit.toml is only read: a measurement
|
|
6
|
+
* describes this machine, so recording one changes no guarded configuration.
|
|
7
|
+
*
|
|
8
|
+
* It does not admit runtimes (`runtime.ts` reads the rows) and it never spawns a process itself:
|
|
9
|
+
* every child process crosses the supplied `execute`, which production binds to
|
|
10
|
+
* `environment-exec/execute.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { mkdir, mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { selectLocalDockerDaemon } from './docker-daemon.js';
|
|
18
|
+
import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
|
|
19
|
+
import { loadCockpitConfig } from '../config/load.js';
|
|
20
|
+
import { cockpitCommand } from '../fix-catalogue.js';
|
|
21
|
+
import { ENVIRONMENT_CODES } from './contract.js';
|
|
22
|
+
import { declaresNothingToRun, runtimeNotDeclared } from './declaration.js';
|
|
23
|
+
import { resolveDockerExecutable, resolveGitExecutable } from './executable.js';
|
|
24
|
+
import { observeProcessStart, processHost } from './claims.js';
|
|
25
|
+
import { observeDockerRuntime, observedVersionCore } from './runtime.js';
|
|
26
|
+
import { recordLocalRuntimeMeasurement } from './runtime-measurement-record.js';
|
|
27
|
+
import { MEASURE_DIRECTORY_PREFIX, claimMeasureDirectory, sweepProbeResidue } from './runtime-probe-residue.js';
|
|
28
|
+
import { selectMachineStateRoot } from './state.js';
|
|
29
|
+
import { probeRuntimeSupport } from './runtime-probe.js';
|
|
30
|
+
import { refusalOutcome as refused } from '../refusal.js';
|
|
31
|
+
const IMAGE_ID = /^sha256:[a-f0-9]{64}$/;
|
|
32
|
+
const STEP_TIMEOUT_MS = 10_000;
|
|
33
|
+
const PROBE_STEP_TIMEOUT_MS = 30_000;
|
|
34
|
+
function dockerStep(executable, cwd, env, id, args, stdoutLimitBytes = 1_024) {
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
argv: [executable.canonicalPath, ...args],
|
|
38
|
+
argv0: executable.invocationPath,
|
|
39
|
+
cwd, env, timeoutMs: STEP_TIMEOUT_MS,
|
|
40
|
+
stdoutLimitBytes, stderrLimitBytes: 4_096,
|
|
41
|
+
paths: [], ports: [],
|
|
42
|
+
labels: { purpose: id },
|
|
43
|
+
expected: [],
|
|
44
|
+
consumedFiles: [{
|
|
45
|
+
path: executable.canonicalPath, sha256: executable.sha256,
|
|
46
|
+
maxBytes: 536_870_912, argvIndexes: [0], executable: true,
|
|
47
|
+
executionMode: executable.executionMode,
|
|
48
|
+
}],
|
|
49
|
+
filesystemChecks: [],
|
|
50
|
+
compensation: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function readDocker(execute, step) {
|
|
54
|
+
const outcome = authenticatedExecutionOutcome(await execute(step), step);
|
|
55
|
+
if (!outcome)
|
|
56
|
+
return { kind: 'failed', detail: 'the execution outcome could not be authenticated' };
|
|
57
|
+
let stdout = '';
|
|
58
|
+
let stderr = '';
|
|
59
|
+
try {
|
|
60
|
+
// Docker's final line ending is transport framing; trimming would rewrite endpoint identity.
|
|
61
|
+
stdout = new TextDecoder('utf-8', { fatal: true }).decode(outcome.stdout).replace(/\r?\n$/, '');
|
|
62
|
+
stderr = new TextDecoder('utf-8', { fatal: true }).decode(outcome.stderr).trim();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { kind: 'failed', detail: 'docker wrote output that is not UTF-8' };
|
|
66
|
+
} // Fatal decoding throws only for invalid UTF-8, which the detail names.
|
|
67
|
+
if (outcome.kind !== 'complete') {
|
|
68
|
+
return { kind: 'failed', detail: `${outcome.code}${stderr ? `: ${stderr.split('\n', 1)[0].slice(0, 256)}` : ''}` };
|
|
69
|
+
}
|
|
70
|
+
return { kind: 'complete', text: stdout };
|
|
71
|
+
}
|
|
72
|
+
export async function runRuntimeMeasure(input, dependencies) {
|
|
73
|
+
const loaded = await loadCockpitConfig(input.configPath);
|
|
74
|
+
if (loaded.kind !== 'complete')
|
|
75
|
+
return { status: 2, result: null, diagnostics: loaded.diagnostics };
|
|
76
|
+
const configPath = loaded.result.source.path;
|
|
77
|
+
const config = loaded.result.config;
|
|
78
|
+
if (declaresNothingToRun(config.environment))
|
|
79
|
+
return { status: 2, result: null,
|
|
80
|
+
diagnostics: [runtimeNotDeclared(configPath)] };
|
|
81
|
+
if (input.image.length === 0 || input.image.length > 256 || input.image.startsWith('-') || /[\s\u0000-\u001f\u007f]/.test(input.image)) {
|
|
82
|
+
return refused('CLI_USAGE_INVALID', '--image must name one local image reference or ID', '--image');
|
|
83
|
+
}
|
|
84
|
+
// Refuse a remote daemon before minutes of probing, and measure exactly the daemon `up` will
|
|
85
|
+
// reach: a local DOCKER_HOST as given, otherwise the context DOCKER_CONTEXT names or the active
|
|
86
|
+
// one, pinned by name for every later step so a `docker context use` mid-run cannot move the
|
|
87
|
+
// measurement to another daemon. cockpit's owners hand Docker no HOME, so a pinned name
|
|
88
|
+
// resolves through the operator's own context store, as it does for `up`. The context is pinned
|
|
89
|
+
// by name, not as its endpoint in DOCKER_HOST: that would leave down and reset inside the probe
|
|
90
|
+
// resolving the "default" context instead.
|
|
91
|
+
const daemon = await selectLocalDockerDaemon(input.env, STEP_TIMEOUT_MS, dependencies.execute, {
|
|
92
|
+
command: 'runtime measure', localOnly: 'a measurement describes this machine, which a remote daemon says nothing about'
|
|
93
|
+
});
|
|
94
|
+
if (daemon.kind === 'refusal')
|
|
95
|
+
return { status: 2, result: null, artifact: null, destination: null, diagnostics: [daemon.diagnostic] };
|
|
96
|
+
// Where the measurement is recorded: the per-user root every checkout of this machine shares.
|
|
97
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux')
|
|
98
|
+
return refused(ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, 'runtime measurements are recorded on macOS or Linux only', process.platform);
|
|
99
|
+
const stateRoot = await selectMachineStateRoot({ platform: process.platform, home: input.env.HOME, xdgStateHome: input.env.XDG_STATE_HOME });
|
|
100
|
+
if (stateRoot.kind !== 'complete')
|
|
101
|
+
return refused(stateRoot.kind === 'refusal' ? stateRoot.code : ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, stateRoot.kind === 'refusal' ? stateRoot.message : 'the state root is unavailable', stateRoot.kind === 'refusal' ? stateRoot.target : 'state root');
|
|
102
|
+
const path = input.env.PATH ?? '';
|
|
103
|
+
const [docker, git] = await Promise.all([resolveDockerExecutable(input.env), resolveGitExecutable(input.env)]);
|
|
104
|
+
if (docker.state !== 'available')
|
|
105
|
+
return refused(ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, docker.reason, 'docker');
|
|
106
|
+
if (git.state !== 'available')
|
|
107
|
+
return refused(ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, git.reason, 'git');
|
|
108
|
+
const cwd = dirname(configPath);
|
|
109
|
+
const runtimeEnv = { PATH: path,
|
|
110
|
+
...(input.env.DOCKER_HOST === undefined ? { DOCKER_CONTEXT: daemon.dockerContext } : { DOCKER_HOST: input.env.DOCKER_HOST }) };
|
|
111
|
+
const observed = await observeDockerRuntime({ executable: docker.value, cwd, env: runtimeEnv, timeoutMs: STEP_TIMEOUT_MS }, dependencies.execute);
|
|
112
|
+
if (observed.kind !== 'complete')
|
|
113
|
+
return refused(observed.code, observed.reason, 'docker', observed.fix);
|
|
114
|
+
const identity = observed.runtimeIdentity;
|
|
115
|
+
if (identity === null) {
|
|
116
|
+
return refused(ENVIRONMENT_CODES.RUNTIME_UNMEASURED, `Docker runtime is not a recognised product, so no measurement can apply (docker info: OperatingSystem ${JSON.stringify(observed.operatingSystem)}, `
|
|
117
|
+
+ `Name ${JSON.stringify(observed.name)}, OSType ${JSON.stringify(observed.osType)}; host ${observed.hostPlatform}). `
|
|
118
|
+
+ 'Report these docker info values to the cockpit maintainers so the runtime can be identified', 'docker info');
|
|
119
|
+
}
|
|
120
|
+
const image = await readDocker(dependencies.execute, dockerStep(docker.value, cwd, runtimeEnv, 'runtime:measure:image', ['image', 'inspect', input.image, '--format', '{{.Id}}']));
|
|
121
|
+
if (image.kind !== 'complete' || !IMAGE_ID.test(image.text)) {
|
|
122
|
+
// The runtime answered, so the remedy is the image, not the runtime.
|
|
123
|
+
return { status: 2, result: null, artifact: null, destination: null, diagnostics: [{
|
|
124
|
+
severity: 'error',
|
|
125
|
+
code: ENVIRONMENT_CODES.RUNTIME_IMAGE_UNAVAILABLE,
|
|
126
|
+
message: `--image ${JSON.stringify(input.image)} is not a local image (${image.kind === 'complete' ? 'unexpected image ID' : image.detail}); the probe never pulls`,
|
|
127
|
+
target: '--image',
|
|
128
|
+
fix: 'pull or build it first (docker pull <image>), then run npx --no cockpit runtime measure --config <your cockpit.toml> --image <image> again',
|
|
129
|
+
}] };
|
|
130
|
+
}
|
|
131
|
+
// The floor is the numeric core; a vendor suffix such as `-desktop.1` is evidence, not a floor.
|
|
132
|
+
// A complete observation has already refused a version without one.
|
|
133
|
+
const composeFloor = observedVersionCore(observed.composeVersion);
|
|
134
|
+
const measuredOn = new Date().toISOString().slice(0, 10);
|
|
135
|
+
// The probe requires an empty canonical directory and creates its isolated HOME beside it, so
|
|
136
|
+
// both live under one private parent that is removed whatever the verdict.
|
|
137
|
+
const parent = await realpath(await mkdtemp(join(tmpdir(), MEASURE_DIRECTORY_PREFIX)));
|
|
138
|
+
let verdict;
|
|
139
|
+
let swept;
|
|
140
|
+
try {
|
|
141
|
+
// The probe's resources and this directory name this process, so a later measure can tell
|
|
142
|
+
// whether it still runs. Without a readable start only the pid is named, and its resources
|
|
143
|
+
// are then removed only once that pid is gone.
|
|
144
|
+
const host = processHost(dependencies.execute);
|
|
145
|
+
const self = await observeProcessStart(process.pid, host);
|
|
146
|
+
const owner = self.state === 'present' ? `${process.pid}@${self.start}` : String(process.pid);
|
|
147
|
+
await claimMeasureDirectory(parent, owner);
|
|
148
|
+
// A measure killed mid-probe leaves containers, a network, a volume and this directory
|
|
149
|
+
// behind; clear this cockpit's own before adding another set to a shared daemon.
|
|
150
|
+
swept = await sweepProbeResidue({
|
|
151
|
+
observe: (pid) => observeProcessStart(pid, host),
|
|
152
|
+
projectPrefix: config.cockpit.name.slice(0, 40), temporaryDirectory: dirname(parent), ownDirectory: parent,
|
|
153
|
+
run: (id, args) => readDocker(dependencies.execute, dockerStep(docker.value, cwd, runtimeEnv, id, args, 65_536)),
|
|
154
|
+
...(process.getuid === undefined ? {} : { uid: process.getuid() }),
|
|
155
|
+
});
|
|
156
|
+
if (swept.kind === 'refusal')
|
|
157
|
+
return refused(ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, `${swept.reason}${swept.removed.length > 0 ? ` (already removed: ${swept.removed.join(', ')})` : ''}; nothing was probed`, 'docker');
|
|
158
|
+
const probeRoot = join(parent, 'probe');
|
|
159
|
+
await mkdir(probeRoot, { mode: 0o700 });
|
|
160
|
+
verdict = await dependencies.probe({
|
|
161
|
+
executable: docker.value, gitExecutable: git.value, cwd: probeRoot, projectPrefix: config.cockpit.name.slice(0, 40), owner,
|
|
162
|
+
env: { ...runtimeEnv, ...(input.env.TMPDIR === undefined ? {} : { TMPDIR: input.env.TMPDIR }) },
|
|
163
|
+
composeFloor, measuredOn, timeoutMs: PROBE_STEP_TIMEOUT_MS,
|
|
164
|
+
image: { reference: image.text, id: image.text },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await rm(parent, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
const evidence = verdict.evidence;
|
|
171
|
+
const summary = {
|
|
172
|
+
runtime: identity, dockerInfo: { operatingSystem: observed.operatingSystem, name: observed.name, osType: observed.osType },
|
|
173
|
+
endpoint: { value: daemon.endpoint, source: daemon.source },
|
|
174
|
+
image: image.text, candidate: verdict.candidate, brokenControl: verdict.brokenControl,
|
|
175
|
+
checks: evidence?.candidateChecks ?? null,
|
|
176
|
+
removedLeftovers: swept.removed, leftUntouched: swept.left,
|
|
177
|
+
};
|
|
178
|
+
if (verdict.status !== 'supported' || verdict.candidate !== 'passed' || verdict.brokenControl !== 'rejected') {
|
|
179
|
+
return { ...refused(verdict.operatorAction?.code ?? ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, `runtime ${identity} was not measured as supported (candidate ${verdict.candidate}, broken control ${verdict.brokenControl}): `
|
|
180
|
+
+ `${verdict.operatorAction?.reason ?? 'no reason was reported'}; nothing was recorded`, identity), result: summary };
|
|
181
|
+
}
|
|
182
|
+
// The probe observes the runtime again; a different daemon or Compose answering it would make
|
|
183
|
+
// this row describe something other than the identity read above.
|
|
184
|
+
if (evidence === null || evidence.runtimeVersion !== observed.runtime.version || evidence.composeVersion !== observed.composeVersion
|
|
185
|
+
|| evidence.runtimeExecutableSha256 !== docker.value.sha256) {
|
|
186
|
+
return { ...refused(ENVIRONMENT_CODES.RUNTIME_UNMEASURED, `the probe measured a different runtime than ${identity} ${observed.runtime.version} with Compose ${observed.composeVersion}; nothing was recorded`, identity), result: summary };
|
|
187
|
+
}
|
|
188
|
+
const row = { runtime: identity, daemon: observed.name, engineVersion: observed.runtime.version,
|
|
189
|
+
composeFloor, measuredOn };
|
|
190
|
+
const recorded = await recordLocalRuntimeMeasurement(stateRoot.value, row);
|
|
191
|
+
if (recorded.kind === 'refusal') {
|
|
192
|
+
return { ...refused(ENVIRONMENT_CODES.RUNTIME_UNMEASURED, `runtime ${identity} passed, but this machine's measurement could not be recorded in ${recorded.path}: ${recorded.reason}`, recorded.path),
|
|
193
|
+
result: { ...summary, row, recorded: false } };
|
|
194
|
+
}
|
|
195
|
+
const notes = [
|
|
196
|
+
...(swept.removed.length === 0 ? [] : [`removed what an interrupted measure left behind: ${swept.removed.join('; ')}`]),
|
|
197
|
+
...(swept.left.length === 0 ? [] : [`left in place, since a measure may still be running: ${swept.left.join('; ')}`]),
|
|
198
|
+
`runtime ${identity} on daemon ${JSON.stringify(observed.name)} passed with engine ${row.engineVersion}; recorded with Compose floor ${composeFloor} in ${recorded.path}, shared by every checkout on this machine`,
|
|
199
|
+
...(recorded.replaced === null ? [] : [`it replaces the measurement from ${recorded.replaced.measuredOn} (engine ${recorded.replaced.engineVersion})`]),
|
|
200
|
+
...(recorded.discardedUnreadable === null ? [] : [`the previous record was unreadable and was replaced: ${recorded.discardedUnreadable}`]),
|
|
201
|
+
`${basename(configPath)} is unchanged; next: ${cockpitCommand('up', { config: configPath })}`,
|
|
202
|
+
];
|
|
203
|
+
return { status: 0, result: { ...summary, row, recorded: true, path: recorded.path }, destination: null,
|
|
204
|
+
artifact: `${notes.join('\n')}\n`, diagnostics: [] };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Production probe binding. The probe only certifies itself when it runs from an installed package,
|
|
208
|
+
* so a source checkout is told that directly instead of failing deep inside the probe.
|
|
209
|
+
*/
|
|
210
|
+
export async function probeInstalledRuntime(request) {
|
|
211
|
+
const modulePath = fileURLToPath(new URL('./runtime-probe.js', import.meta.url));
|
|
212
|
+
const canonicalPath = await realpath(modulePath);
|
|
213
|
+
const installedSuffix = ['node_modules', '@b00dea', 'cockpit', 'dist', 'environment', 'runtime-probe.js'].join(sep);
|
|
214
|
+
if (!canonicalPath.endsWith(`${sep}${installedSuffix}`)) {
|
|
215
|
+
return {
|
|
216
|
+
status: 'unavailable', runtime: null, composeVersion: null, composeFloor: request.composeFloor, measuredOn: request.measuredOn,
|
|
217
|
+
candidate: 'failed', brokenControl: 'inconclusive', evidence: null,
|
|
218
|
+
operatorAction: { required: true, code: ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE,
|
|
219
|
+
reason: `runtime measurement runs only from an installed @b00dea/cockpit package, and this cockpit runs from ${dirname(canonicalPath)}; install the package and use its cockpit binary` },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const sha256 = createHash('sha256').update(await readFile(canonicalPath)).digest('hex');
|
|
223
|
+
return probeRuntimeSupport({ ...request, installedModule: { canonicalPath, sha256 } });
|
|
224
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ProcessStartObservation } from './claims.js';
|
|
2
|
+
export declare const MEASURE_DIRECTORY_PREFIX = "cockpit-runtime-measure-";
|
|
3
|
+
/** Names the measure process that created a probe resource, as `<pid>` or `<pid>@<start identity>`. */
|
|
4
|
+
export declare const PROBE_OWNER_LABEL = "dev.b0dea.cockpit.probe-owner";
|
|
5
|
+
/** Run one Docker read or removal; `failed` carries the first line of Docker's complaint. */
|
|
6
|
+
export type ResidueRunner = (id: string, args: readonly string[]) => Promise<Readonly<{
|
|
7
|
+
kind: 'complete';
|
|
8
|
+
text: string;
|
|
9
|
+
}> | Readonly<{
|
|
10
|
+
kind: 'failed';
|
|
11
|
+
detail: string;
|
|
12
|
+
}>>;
|
|
13
|
+
/** Observe a process's presence and start identity, as `observeProcessStart` does. */
|
|
14
|
+
export type ProcessObserver = (pid: number) => Promise<ProcessStartObservation>;
|
|
15
|
+
export type ProbeResidueOutcome = Readonly<{
|
|
16
|
+
kind: 'complete';
|
|
17
|
+
removed: readonly string[];
|
|
18
|
+
left: readonly string[];
|
|
19
|
+
}> | Readonly<{
|
|
20
|
+
kind: 'refusal';
|
|
21
|
+
reason: string;
|
|
22
|
+
removed: readonly string[];
|
|
23
|
+
}>;
|
|
24
|
+
/** Mark a freshly created measure directory as owned by `owner` (see `PROBE_OWNER_LABEL`), so a later measure can tell it is alive. */
|
|
25
|
+
export declare function claimMeasureDirectory(directory: string, owner: string): Promise<void>;
|
|
26
|
+
export declare function isProbeOwner(owner: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Remove this cockpit's probe resources left by a dead measure. A resource is the probe's only if
|
|
29
|
+
* it carries the probe label and a Compose project named `<projectPrefix>-probe-<batch>` (Compose
|
|
30
|
+
* appends the stack hash) whose batch matches the label, so a different cockpit's probe, or any other stack, is never touched.
|
|
31
|
+
* It is dead only when the measure its owner label names is provably gone. Liveness is read from
|
|
32
|
+
* the label on the shared daemon, not from the temp directory: agent tools give each session its
|
|
33
|
+
* own TMPDIR, so a concurrent measure's directory is invisible, and judging by it removed that
|
|
34
|
+
* measure's running probe. Anything not provably dead may be running now and is only reported.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sweepProbeResidue(input: Readonly<{
|
|
37
|
+
projectPrefix: string;
|
|
38
|
+
temporaryDirectory: string;
|
|
39
|
+
ownDirectory: string;
|
|
40
|
+
run: ResidueRunner;
|
|
41
|
+
observe: ProcessObserver;
|
|
42
|
+
uid?: number;
|
|
43
|
+
}>): Promise<ProbeResidueOutcome>;
|