@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,713 @@
|
|
|
1
|
+
import { exactKeys } from '../exact-keys.js';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { sha256Hex } from '../sha256.js';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { lstat, mkdir, open, realpath, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { types as utilTypes } from 'node:util';
|
|
9
|
+
import { PROBE_EVIDENCE_ROOT, validateSelftestEvidenceBundle, } from '../../libexec/validate-harness-evidence.mjs';
|
|
10
|
+
import { authenticatedExecutionOutcome, executeProbe } from '../environment-exec/execute.js';
|
|
11
|
+
import { generatedApplicationProjection } from './generated-application/authorities.js';
|
|
12
|
+
import { checkGeneratedEnvironment } from './generated-application/check.js';
|
|
13
|
+
import {} from './generated-application/shared.js';
|
|
14
|
+
import { redactSensitiveMessage } from './sensitive.js';
|
|
15
|
+
import { snapshotExecutionStep, snapshotGeneratedJson, ENVIRONMENT_CODES, errorCause, hasErrorCode, INHERITED_TOOL_ENVIRONMENT, } from './contract.js';
|
|
16
|
+
import { resolveExecutable } from './executable.js';
|
|
17
|
+
import { cockpitCommand, formatShellCommand } from '../fix-catalogue.js';
|
|
18
|
+
import { draftSurfaceSelftestSpecifications, planSurfaceSelftest, unfilledSurfaceSelftestSpecifications, surfaceSelftestCellLabel, surfaceSelftestHumanNeeded, surfaceSelftestReviews, } from '../surfaces/selftest.js';
|
|
19
|
+
const defaultAdapters = Object.freeze({ execute: executeProbe });
|
|
20
|
+
const MAX_EXECUTABLE_BYTES = 536_870_912;
|
|
21
|
+
const MAX_CONTROL_BYTES = 65_536;
|
|
22
|
+
const MAX_PROBE_SPECIFICATIONS_BYTES = 1_048_576;
|
|
23
|
+
const STREAM_BYTES = 65_536;
|
|
24
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
25
|
+
const EVIDENCE_EXPIRY_MS = 300_000;
|
|
26
|
+
const BUILTIN_EXECUTABLES = Object.freeze({ claudecode: 'claude', codexcli: 'codex', opencode: 'opencode' });
|
|
27
|
+
// Why a custom cell has no probe row; see SelftestProbeSpecification.
|
|
28
|
+
const UNPROBEABLE_REASON = 'selftest runs only built-in tools it found on PATH and you approved by digest, and a custom harness has no such executable';
|
|
29
|
+
const UNPROBEABLE_ACTION = 'measure the capability by hand with a candidate and a deliberately broken control and record it in cockpit.toml';
|
|
30
|
+
// A signed-in tool locates its stored sign-in through HOME, so a probe given PATH alone reports
|
|
31
|
+
// every cell as not signed in. The allowlist carries no credential; every other variable, tokens
|
|
32
|
+
// included, stays out of the probe.
|
|
33
|
+
const PROBE_ENVIRONMENT = new Set(['PATH', ...INHERITED_TOOL_ENVIRONMENT]);
|
|
34
|
+
function captureProbeEnvironment(value) {
|
|
35
|
+
try {
|
|
36
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || utilTypes.isProxy(value))
|
|
37
|
+
return undefined;
|
|
38
|
+
const prototype = Object.getPrototypeOf(value);
|
|
39
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
40
|
+
return undefined;
|
|
41
|
+
const keys = Reflect.ownKeys(value);
|
|
42
|
+
if (keys.length > 4_096)
|
|
43
|
+
return undefined;
|
|
44
|
+
const env = {};
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
if (typeof key !== 'string' || !PROBE_ENVIRONMENT.has(key))
|
|
47
|
+
continue;
|
|
48
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
49
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')
|
|
50
|
+
|| descriptor.value !== undefined && (typeof descriptor.value !== 'string'
|
|
51
|
+
|| Buffer.byteLength(descriptor.value) > 65_536))
|
|
52
|
+
return undefined;
|
|
53
|
+
env[key] = descriptor.value;
|
|
54
|
+
}
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
} // A throwing accessor or proxy trap makes the input malformed; the caller refuses it.
|
|
60
|
+
}
|
|
61
|
+
function commandInput(value) {
|
|
62
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
63
|
+
return undefined;
|
|
64
|
+
const { application, env: rawEnv, root, specifications } = value;
|
|
65
|
+
const env = captureProbeEnvironment(rawEnv);
|
|
66
|
+
if (typeof root !== 'string' || !env || !Array.isArray(specifications))
|
|
67
|
+
return undefined;
|
|
68
|
+
return { application, root, specifications, env };
|
|
69
|
+
}
|
|
70
|
+
function configuredCommandInput(value) {
|
|
71
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
72
|
+
return undefined;
|
|
73
|
+
const { configPath, env: rawEnv, probeSpecificationsPath } = value;
|
|
74
|
+
const env = captureProbeEnvironment(rawEnv);
|
|
75
|
+
if (typeof configPath !== 'string' || configPath.length === 0 || configPath.includes('\0')
|
|
76
|
+
|| typeof probeSpecificationsPath !== 'string' || probeSpecificationsPath.length === 0
|
|
77
|
+
|| probeSpecificationsPath.includes('\0') || !env)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { configPath, probeSpecificationsPath, env };
|
|
80
|
+
}
|
|
81
|
+
function refusal(code, reason) {
|
|
82
|
+
return { kind: 'refusal', status: 2, code, reason };
|
|
83
|
+
}
|
|
84
|
+
// A string is the file's text, `undefined` a file that is not a bounded regular file, and `{ cause }` an I/O or decoding failure.
|
|
85
|
+
async function readProbeSpecificationsArtifact(path) {
|
|
86
|
+
let handle;
|
|
87
|
+
try {
|
|
88
|
+
const before = await lstat(path);
|
|
89
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_PROBE_SPECIFICATIONS_BYTES)
|
|
90
|
+
return undefined;
|
|
91
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
92
|
+
const opened = await handle.stat();
|
|
93
|
+
if (!opened.isFile() || opened.size !== before.size || opened.dev !== before.dev || opened.ino !== before.ino)
|
|
94
|
+
return undefined;
|
|
95
|
+
const chunks = [];
|
|
96
|
+
const chunk = Buffer.allocUnsafe(STREAM_BYTES);
|
|
97
|
+
let total = 0;
|
|
98
|
+
while (true) {
|
|
99
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, MAX_PROBE_SPECIFICATIONS_BYTES - total + 1), null);
|
|
100
|
+
if (bytesRead === 0)
|
|
101
|
+
break;
|
|
102
|
+
total += bytesRead;
|
|
103
|
+
if (total > MAX_PROBE_SPECIFICATIONS_BYTES)
|
|
104
|
+
return undefined;
|
|
105
|
+
chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));
|
|
106
|
+
}
|
|
107
|
+
const after = await handle.stat();
|
|
108
|
+
if (total !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs)
|
|
109
|
+
return undefined;
|
|
110
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, total));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return { cause: errorCause(error) };
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await handle?.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const PROBE_SPECIFICATIONS_SCHEMA = 'cockpit-selftest-probe-specifications/v1';
|
|
120
|
+
// The file carries no digest of its own rows: a person edits them, and a digest anyone can
|
|
121
|
+
// recompute would prove nothing. What binds the rows is the approval of the whole file's SHA-256.
|
|
122
|
+
function probeSpecificationsArtifact(bytes, sourceSha256, generatedManifestSha256) {
|
|
123
|
+
let captured;
|
|
124
|
+
try {
|
|
125
|
+
captured = snapshotGeneratedJson(JSON.parse(bytes));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { kind: 'malformed', cause: errorCause(error) };
|
|
129
|
+
}
|
|
130
|
+
if (!captured || !exactKeys(captured, ['generatedManifestSha256', 'schema', 'sourceSha256', 'specifications'])
|
|
131
|
+
|| captured.schema !== PROBE_SPECIFICATIONS_SCHEMA || !Array.isArray(captured.specifications))
|
|
132
|
+
return { kind: 'malformed' };
|
|
133
|
+
if (captured.sourceSha256 !== sourceSha256 || captured.generatedManifestSha256 !== generatedManifestSha256) {
|
|
134
|
+
return { kind: 'stale', specifications: captured.specifications };
|
|
135
|
+
}
|
|
136
|
+
return { kind: 'complete', specifications: captured.specifications };
|
|
137
|
+
}
|
|
138
|
+
function causeSuffix(cause) {
|
|
139
|
+
return cause ? `: ${cause}` : '';
|
|
140
|
+
}
|
|
141
|
+
// Every refusal about the file itself names the command that writes a fresh one.
|
|
142
|
+
function probeSpecificationsRefusal(reason) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'refusal', status: 2, code: 'SELFTEST_SOURCE_INVALID', reason,
|
|
145
|
+
fix: 'write a new file with npx --no cockpit selftest --config <your cockpit.toml> --write-probe-specifications <new file>, fill the fields it lists, then pass that file to --probe-specifications',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function hasExecutableAuthority(adapters, approved, harness, executable) {
|
|
149
|
+
const operatorIdentity = approved.get(harness);
|
|
150
|
+
if (operatorIdentity?.canonicalPath === executable.canonicalPath && operatorIdentity.sha256 === executable.sha256)
|
|
151
|
+
return true;
|
|
152
|
+
if (adapters.requestExecutableApproval) {
|
|
153
|
+
const confirmation = await adapters.requestExecutableApproval({
|
|
154
|
+
harness, canonicalPath: executable.canonicalPath, sha256: executable.sha256,
|
|
155
|
+
});
|
|
156
|
+
if (confirmation === executable.sha256) {
|
|
157
|
+
approved.set(harness, { canonicalPath: executable.canonicalPath, sha256: executable.sha256 });
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (process.env.NODE_ENV === 'test') {
|
|
162
|
+
const identities = adapters.testOnlyExecutableIdentities?.[harness];
|
|
163
|
+
if (!Array.isArray(identities))
|
|
164
|
+
return false;
|
|
165
|
+
for (const identity of identities) {
|
|
166
|
+
if (typeof identity !== 'string' || !isAbsolute(identity) || identity.includes('\0'))
|
|
167
|
+
continue;
|
|
168
|
+
try {
|
|
169
|
+
if (await realpath(identity) === executable.canonicalPath)
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
} // An unresolvable test identity cannot match; the next one is tried and none matching refuses.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
function relativeEvidencePath(root, path) {
|
|
180
|
+
const offset = relative(root, path);
|
|
181
|
+
return offset !== '' && offset !== '..' && !offset.startsWith(`..${sep}`) && !isAbsolute(offset)
|
|
182
|
+
? offset.split(sep).join('/') : undefined;
|
|
183
|
+
}
|
|
184
|
+
function exactLine(value, code) {
|
|
185
|
+
return value.split(/\r?\n/).some((line) => line.trim() === code);
|
|
186
|
+
}
|
|
187
|
+
function boundedStream(value) {
|
|
188
|
+
let text;
|
|
189
|
+
try {
|
|
190
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(value);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
} // Fatal decoding throws only for invalid UTF-8, which the caller rejects as an unusable stream.
|
|
195
|
+
const redacted = redactSensitiveMessage(text);
|
|
196
|
+
if (redacted === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
text = redacted;
|
|
199
|
+
if (Buffer.byteLength(text) > STREAM_BYTES)
|
|
200
|
+
return undefined;
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
203
|
+
function step(id, root, argv, argv0, consumedFiles, deadline, labels, env) {
|
|
204
|
+
const remaining = deadline - Date.now();
|
|
205
|
+
if (remaining <= 0)
|
|
206
|
+
return undefined;
|
|
207
|
+
return snapshotExecutionStep({
|
|
208
|
+
id, argv, argv0, cwd: root, env, timeoutMs: Math.min(PROBE_TIMEOUT_MS, remaining),
|
|
209
|
+
spawnNotAfter: new Date(deadline).toISOString(), stdoutLimitBytes: STREAM_BYTES, stderrLimitBytes: STREAM_BYTES,
|
|
210
|
+
paths: [root], ports: [], labels, expected: [], consumedFiles, filesystemChecks: [], compensation: [],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async function run(adapters, value) {
|
|
214
|
+
if (!value)
|
|
215
|
+
return undefined;
|
|
216
|
+
const startedAt = new Date().toISOString();
|
|
217
|
+
const outcome = authenticatedExecutionOutcome(await adapters.execute(value), value);
|
|
218
|
+
const finishedAt = new Date().toISOString();
|
|
219
|
+
return outcome ? { outcome, startedAt, finishedAt } : undefined;
|
|
220
|
+
}
|
|
221
|
+
function publicCommand(command, executable, control) {
|
|
222
|
+
return command.map((value) => value === '{executable}' ? executable
|
|
223
|
+
: value === '{control}' && control !== undefined ? control : value);
|
|
224
|
+
}
|
|
225
|
+
function executionCommand(command, root, control) {
|
|
226
|
+
return command.map((value) => value === control && control !== undefined ? resolve(root, control) : value);
|
|
227
|
+
}
|
|
228
|
+
function event(command, cwd, execution, result, code) {
|
|
229
|
+
const stdout = boundedStream(execution.stdout);
|
|
230
|
+
const stderr = boundedStream(execution.stderr);
|
|
231
|
+
if (stdout === undefined || stderr === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
return {
|
|
234
|
+
cwd, command: [...command], exitCode: execution.execution.exitCode,
|
|
235
|
+
signal: execution.execution.signal, result, code, stdout, stderr,
|
|
236
|
+
stdoutSha256: sha256Hex(stdout), stderrSha256: sha256Hex(stderr),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function selectedEntryBytes(probe) {
|
|
240
|
+
const selected = probe.entries.find((entry) => {
|
|
241
|
+
if (probe.cell.capability === 'skill-parity')
|
|
242
|
+
return entry.path.includes(`/skills/${probe.cell.skill}/`);
|
|
243
|
+
if (probe.harness.kind !== 'builtin')
|
|
244
|
+
return false;
|
|
245
|
+
// Every built-in tool loads cockpit's instructions and the copied sibling guides from the root AGENTS.md.
|
|
246
|
+
if (probe.capability === 'instruction-loading')
|
|
247
|
+
return entry.path === 'AGENTS.md';
|
|
248
|
+
if (probe.harness.target === 'claudecode')
|
|
249
|
+
return entry.path === '.claude/settings.json';
|
|
250
|
+
if (probe.harness.target === 'codexcli')
|
|
251
|
+
return entry.path === '.codex/config.toml'
|
|
252
|
+
|| entry.path.startsWith('.codex/rules/');
|
|
253
|
+
return entry.path === 'opencode.jsonc';
|
|
254
|
+
});
|
|
255
|
+
if (!selected)
|
|
256
|
+
return undefined;
|
|
257
|
+
if (selected.kind === 'file')
|
|
258
|
+
return selected.bytes;
|
|
259
|
+
return selected.region.format === 'markdown-delimited'
|
|
260
|
+
? selected.region.bytes
|
|
261
|
+
: `${JSON.stringify(Object.fromEntries(selected.region.members.map((member) => [member.key, JSON.parse(member.valueBytes)])), null, 2)}\n`;
|
|
262
|
+
}
|
|
263
|
+
function authenticationRequired(outcome) {
|
|
264
|
+
const text = `${outcome.stdout.toString('utf8')}\n${outcome.stderr.toString('utf8')}`;
|
|
265
|
+
return /(?:not authenticated|not logged in|authentication required|unauthorized|\b401\b|\b403\b)/i.test(text);
|
|
266
|
+
}
|
|
267
|
+
// Returns undefined for a plain directory, otherwise why it is not one.
|
|
268
|
+
async function ensurePlainDirectory(path) {
|
|
269
|
+
const inspect = async () => {
|
|
270
|
+
const status = await lstat(path);
|
|
271
|
+
return status.isDirectory() && !status.isSymbolicLink() && await realpath(path) === path
|
|
272
|
+
? undefined : `${path} is not a plain directory`;
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
return await inspect();
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (!hasErrorCode(error, 'ENOENT'))
|
|
279
|
+
return `${path} could not be inspected: ${errorCause(error)}`;
|
|
280
|
+
try {
|
|
281
|
+
await mkdir(path, { mode: 0o700 });
|
|
282
|
+
}
|
|
283
|
+
catch (createError) {
|
|
284
|
+
if (!hasErrorCode(createError, 'EEXIST'))
|
|
285
|
+
return `${path} could not be created: ${errorCause(createError)}`;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
return await inspect();
|
|
289
|
+
}
|
|
290
|
+
catch (inspectError) {
|
|
291
|
+
return `${path} could not be inspected: ${errorCause(inspectError)}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function ensureEvidenceDirectory(root) {
|
|
296
|
+
try {
|
|
297
|
+
if (await realpath(root) !== root)
|
|
298
|
+
return { reason: `${root} is not canonical` };
|
|
299
|
+
let directory = root;
|
|
300
|
+
for (const segment of PROBE_EVIDENCE_ROOT.split('/')) {
|
|
301
|
+
directory = join(directory, segment);
|
|
302
|
+
const problem = await ensurePlainDirectory(directory);
|
|
303
|
+
if (problem)
|
|
304
|
+
return { reason: problem };
|
|
305
|
+
}
|
|
306
|
+
return directory;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
return { reason: `${root} could not be resolved: ${errorCause(error)}` };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function writeControlPair(directory, probe) {
|
|
313
|
+
const candidate = join(directory, 'candidate.json');
|
|
314
|
+
const broken = join(directory, 'broken.json');
|
|
315
|
+
const candidateBytes = selectedEntryBytes(probe);
|
|
316
|
+
if (!candidateBytes || Buffer.byteLength(candidateBytes) > MAX_CONTROL_BYTES)
|
|
317
|
+
return undefined;
|
|
318
|
+
const brokenBytes = '{\n';
|
|
319
|
+
await writeFile(candidate, candidateBytes, { flag: 'wx', mode: 0o600 });
|
|
320
|
+
await writeFile(broken, brokenBytes, { flag: 'wx', mode: 0o600 });
|
|
321
|
+
return { candidate, broken, candidateSha256: sha256Hex(candidateBytes), brokenSha256: sha256Hex(brokenBytes) };
|
|
322
|
+
}
|
|
323
|
+
function consumedExecutable(executable) {
|
|
324
|
+
return {
|
|
325
|
+
path: executable.canonicalPath, sha256: executable.sha256, maxBytes: MAX_EXECUTABLE_BYTES,
|
|
326
|
+
argvIndexes: [0], executable: true, executionMode: executable.executionMode,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function runEvidenceProbe(root, evidenceRoot, expectation, probe, env, adapters, approvedExecutables) {
|
|
330
|
+
const harnessName = probe.harness.name;
|
|
331
|
+
const observedAt = new Date().toISOString();
|
|
332
|
+
const resolution = await resolveExecutable(BUILTIN_EXECUTABLES[probe.harness.target], env);
|
|
333
|
+
if (resolution.state === 'unavailable')
|
|
334
|
+
return surfaceSelftestHumanNeeded(expectation, probe.cell, 'executable_missing', observedAt, `${resolution.reason}; install ${harnessName} on a supported host, sign in, and rerun its ${probe.capability} probe`);
|
|
335
|
+
const executable = resolution.value;
|
|
336
|
+
if (!await hasExecutableAuthority(adapters, approvedExecutables, probe.harness.name, executable)) {
|
|
337
|
+
return surfaceSelftestHumanNeeded(expectation, probe.cell, 'authentication_required', observedAt, `Provide an operator-approved absolute executable identity for ${harnessName} and rerun its ${probe.capability} probe`);
|
|
338
|
+
}
|
|
339
|
+
if (typeof evidenceRoot !== 'string')
|
|
340
|
+
return surfaceSelftestHumanNeeded(expectation, probe.cell, 'isolation_unavailable', observedAt, `${PROBE_EVIDENCE_ROOT} could not be created as a plain directory (${evidenceRoot.reason}), so probe controls cannot be kept apart from the checkout; make it a plain directory and rerun the ${harnessName} ${probe.capability} probe`);
|
|
341
|
+
let directory;
|
|
342
|
+
let controls;
|
|
343
|
+
try {
|
|
344
|
+
directory = join(evidenceRoot, `selftest-${randomBytes(12).toString('hex')}`);
|
|
345
|
+
await mkdir(directory, { mode: 0o700 });
|
|
346
|
+
controls = await writeControlPair(directory, probe);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return { probeFailure: `could not write probe controls: ${errorCause(error)}` };
|
|
350
|
+
}
|
|
351
|
+
if (!controls)
|
|
352
|
+
return undefined;
|
|
353
|
+
const candidatePath = relativeEvidencePath(root, controls.candidate);
|
|
354
|
+
const brokenPath = relativeEvidencePath(root, controls.broken);
|
|
355
|
+
const transcript = join(directory, 'transcript.json');
|
|
356
|
+
const transcriptPath = relativeEvidencePath(root, transcript);
|
|
357
|
+
if (!candidatePath || !brokenPath || !transcriptPath)
|
|
358
|
+
return undefined;
|
|
359
|
+
const deadline = Date.now() + EVIDENCE_EXPIRY_MS;
|
|
360
|
+
const invocation = publicCommand(probe.invocation, executable.canonicalPath);
|
|
361
|
+
const versionCommand = publicCommand(probe.versionCommand, executable.canonicalPath);
|
|
362
|
+
const helpCommand = publicCommand(probe.helpCommand, executable.canonicalPath);
|
|
363
|
+
const commonFiles = [consumedExecutable(executable)];
|
|
364
|
+
const probeEnvironment = Object.fromEntries(Object.entries(env).filter((row) => typeof row[1] === 'string' && row[1].length > 0 && !row[1].includes('\0')));
|
|
365
|
+
const controlIndex = invocation.indexOf('{control}');
|
|
366
|
+
if (controlIndex < 0)
|
|
367
|
+
return undefined;
|
|
368
|
+
const candidateCommand = publicCommand(invocation, executable.canonicalPath, candidatePath);
|
|
369
|
+
const brokenCommand = publicCommand(invocation, executable.canonicalPath, brokenPath);
|
|
370
|
+
const createStep = (role, command, control) => {
|
|
371
|
+
const execution = executionCommand(command, root, control ? relativeEvidencePath(root, control) : undefined);
|
|
372
|
+
const files = [...commonFiles];
|
|
373
|
+
if (control)
|
|
374
|
+
files.push({ path: control, sha256: control === controls.candidate ? controls.candidateSha256 : controls.brokenSha256,
|
|
375
|
+
maxBytes: MAX_CONTROL_BYTES, argvIndexes: [controlIndex], executable: false });
|
|
376
|
+
return step(`surface-selftest-${randomBytes(8).toString('hex')}-${role}`, root, execution, executable.invocationPath, files, deadline, { harness: harnessName, capability: probe.capability, role }, probeEnvironment);
|
|
377
|
+
};
|
|
378
|
+
const version = await run(adapters, createStep('version', versionCommand));
|
|
379
|
+
const help = await run(adapters, createStep('help', helpCommand));
|
|
380
|
+
const broken = await run(adapters, createStep('broken', brokenCommand, controls.broken));
|
|
381
|
+
const candidate = await run(adapters, createStep('candidate', candidateCommand, controls.candidate));
|
|
382
|
+
if (!version || !help || !broken || !candidate)
|
|
383
|
+
return undefined;
|
|
384
|
+
if ([version, help, broken, candidate].some((row) => authenticationRequired(row.outcome))) {
|
|
385
|
+
return surfaceSelftestHumanNeeded(expectation, probe.cell, 'authentication_required', observedAt, `Authenticate ${harnessName} and rerun its ${probe.capability} probe`);
|
|
386
|
+
}
|
|
387
|
+
if (version.outcome.kind !== 'complete' || help.outcome.kind !== 'complete')
|
|
388
|
+
return undefined;
|
|
389
|
+
const brokenOutput = `${broken.outcome.stdout.toString('utf8')}\n${broken.outcome.stderr.toString('utf8')}`;
|
|
390
|
+
if (broken.outcome.kind !== 'refusal' || !exactLine(brokenOutput, probe.brokenCode))
|
|
391
|
+
return undefined;
|
|
392
|
+
const candidateOutput = `${candidate.outcome.stdout.toString('utf8')}\n${candidate.outcome.stderr.toString('utf8')}`;
|
|
393
|
+
let verdict;
|
|
394
|
+
let candidateResult;
|
|
395
|
+
let candidateCode;
|
|
396
|
+
if (candidate.outcome.kind === 'complete' && exactLine(candidateOutput, probe.candidateCode)) {
|
|
397
|
+
verdict = 'instructed';
|
|
398
|
+
candidateResult = 'accepted';
|
|
399
|
+
candidateCode = probe.candidateCode;
|
|
400
|
+
}
|
|
401
|
+
else if (candidate.outcome.kind === 'refusal' && probe.capability === 'guard-firing' && exactLine(candidateOutput, probe.candidateCode)) {
|
|
402
|
+
verdict = 'enforced';
|
|
403
|
+
candidateResult = 'refused';
|
|
404
|
+
candidateCode = probe.candidateCode;
|
|
405
|
+
}
|
|
406
|
+
else if (exactLine(candidateOutput, probe.unavailableCode)) {
|
|
407
|
+
verdict = 'unavailable';
|
|
408
|
+
candidateResult = candidate.outcome.kind === 'complete' ? 'allowed' : 'unavailable';
|
|
409
|
+
candidateCode = probe.unavailableCode;
|
|
410
|
+
}
|
|
411
|
+
else
|
|
412
|
+
return undefined;
|
|
413
|
+
const startedAt = version.startedAt;
|
|
414
|
+
const finishedAt = candidate.finishedAt;
|
|
415
|
+
const expiresAt = new Date(Math.min(Date.parse(startedAt) + EVIDENCE_EXPIRY_MS, deadline)).toISOString();
|
|
416
|
+
const events = {
|
|
417
|
+
version: event(versionCommand, '.', version.outcome, 'completed', 'VERSION_OK'),
|
|
418
|
+
help: event(helpCommand, '.', help.outcome, 'completed', 'HELP_OK'),
|
|
419
|
+
brokenControl: event(brokenCommand, '.', broken.outcome, 'rejected', probe.brokenCode),
|
|
420
|
+
candidateControl: event(candidateCommand, '.', candidate.outcome, candidateResult, candidateCode),
|
|
421
|
+
};
|
|
422
|
+
if (!events.version || !events.help || !events.brokenControl || !events.candidateControl)
|
|
423
|
+
return undefined;
|
|
424
|
+
const versionText = `${events.version.stdout} ${events.version.stderr}`.trim();
|
|
425
|
+
const versionValue = versionText.split(/\s+/).find(Boolean);
|
|
426
|
+
if (!versionValue || Buffer.byteLength(versionValue) > 256)
|
|
427
|
+
return undefined;
|
|
428
|
+
const record = {
|
|
429
|
+
kind: 'evidence', harness: probe.harness, capability: probe.capability,
|
|
430
|
+
generatedManifestSha256: expectation.generatedManifestSha256, verdict, version: versionValue, measuredOn: startedAt.slice(0, 10),
|
|
431
|
+
runId: `selftest-${randomBytes(16).toString('hex')}`, startedAt, finishedAt, expiresAt,
|
|
432
|
+
candidatePath, brokenPath, transcriptPath, candidateSha256: controls.candidateSha256,
|
|
433
|
+
brokenSha256: controls.brokenSha256, transcriptSha256: '', invocation: [...invocation],
|
|
434
|
+
versionCommand: [...versionCommand], helpCommand: [...helpCommand],
|
|
435
|
+
candidateCode, brokenCode: probe.brokenCode, sample: probe.sample,
|
|
436
|
+
authority: { kind: 'native-executable', name: probe.harness.name, executable: executable.canonicalPath },
|
|
437
|
+
events: events,
|
|
438
|
+
};
|
|
439
|
+
const { transcriptSha256: _transcriptSha256, ...transcriptRecord } = record;
|
|
440
|
+
void _transcriptSha256;
|
|
441
|
+
const transcriptBytes = JSON.stringify(transcriptRecord);
|
|
442
|
+
await writeFile(transcript, transcriptBytes, { flag: 'wx', mode: 0o600 });
|
|
443
|
+
return { ...record, transcriptSha256: sha256Hex(transcriptBytes) };
|
|
444
|
+
}
|
|
445
|
+
export async function runSurfaceSelftestApplication(input, adapters = defaultAdapters) {
|
|
446
|
+
const capturedInput = commandInput(input);
|
|
447
|
+
if (!capturedInput)
|
|
448
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'selftest command input must contain only bounded own data');
|
|
449
|
+
const application = generatedApplicationProjection(capturedInput.application);
|
|
450
|
+
if (!application || application !== capturedInput.application)
|
|
451
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'generated application authority was not issued by its producer');
|
|
452
|
+
const plan = planSurfaceSelftest(application.surfaceSelftest, capturedInput.specifications);
|
|
453
|
+
if (plan.kind === 'refusal')
|
|
454
|
+
return refusal(plan.code, plan.reason);
|
|
455
|
+
const root = normalize(capturedInput.root);
|
|
456
|
+
if (!isAbsolute(root) || root !== capturedInput.root || basename(root).length === 0)
|
|
457
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'selftest root must be a canonical absolute directory');
|
|
458
|
+
const evidenceRoot = await ensureEvidenceDirectory(root);
|
|
459
|
+
const records = [];
|
|
460
|
+
const approvedExecutables = new Map();
|
|
461
|
+
for (const probe of plan.probes) {
|
|
462
|
+
const record = await runEvidenceProbe(root, evidenceRoot, plan.expectation, probe, capturedInput.env, adapters, approvedExecutables);
|
|
463
|
+
if (!record)
|
|
464
|
+
return refusal('SELFTEST_EXECUTION_INVALID', `probe execution could not establish ${probe.capability} for ${probe.harness.name}`);
|
|
465
|
+
if ('probeFailure' in record)
|
|
466
|
+
return refusal('SELFTEST_EXECUTION_INVALID', `probe execution could not establish ${probe.capability} for ${probe.harness.name}: ${record.probeFailure}`);
|
|
467
|
+
records.push(record);
|
|
468
|
+
}
|
|
469
|
+
for (const cell of plan.unprobeable) {
|
|
470
|
+
const record = surfaceSelftestHumanNeeded(plan.expectation, cell, 'enforcement_probe_required', new Date().toISOString(), `${UNPROBEABLE_REASON}; ${UNPROBEABLE_ACTION} for ${surfaceSelftestCellLabel(cell)}`);
|
|
471
|
+
if (!record)
|
|
472
|
+
return refusal('SELFTEST_EXECUTION_INVALID', `could not record ${surfaceSelftestCellLabel(cell)} as human_needed`);
|
|
473
|
+
records.push(record);
|
|
474
|
+
}
|
|
475
|
+
const reviews = surfaceSelftestReviews(plan.expectation, []);
|
|
476
|
+
if (!reviews)
|
|
477
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'skill review inventory could not be constructed');
|
|
478
|
+
const bundle = {
|
|
479
|
+
schema: 'cockpit-harness-selftest/v1', generatedManifestSha256: plan.expectation.generatedManifestSha256,
|
|
480
|
+
records, skillInventory: plan.expectation.shippedSkills, skillReviews: reviews,
|
|
481
|
+
};
|
|
482
|
+
const testOnlyExecutableIdentities = process.env.NODE_ENV === 'test'
|
|
483
|
+
? adapters.testOnlyExecutableIdentities : undefined;
|
|
484
|
+
const evidence = validateSelftestEvidenceBundle(bundle, plan.expectation, root, {
|
|
485
|
+
operatorExecutableIdentities: approvedExecutables,
|
|
486
|
+
testOnlyExecutableIdentities,
|
|
487
|
+
});
|
|
488
|
+
if (evidence.errors.length > 0)
|
|
489
|
+
return refusal('SELFTEST_EXECUTION_INVALID', evidence.errors.join('; '));
|
|
490
|
+
return { kind: 'complete', status: 0, bundle, evidence };
|
|
491
|
+
}
|
|
492
|
+
export async function runConfiguredSurfaceSelftestApplication(input, adapters = defaultAdapters) {
|
|
493
|
+
const capturedInput = configuredCommandInput(input);
|
|
494
|
+
if (!capturedInput)
|
|
495
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'configured selftest input must contain only bounded own data');
|
|
496
|
+
let configPath;
|
|
497
|
+
try {
|
|
498
|
+
configPath = await realpath(resolve(capturedInput.configPath));
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
return refusal('SELFTEST_SOURCE_INVALID', `configured selftest source could not be resolved: ${errorCause(error)}`);
|
|
502
|
+
}
|
|
503
|
+
const checked = await checkGeneratedEnvironment({ configPath });
|
|
504
|
+
if (checked.kind !== 'complete') {
|
|
505
|
+
const reason = checked.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join('; ');
|
|
506
|
+
return refusal('SELFTEST_SOURCE_INVALID', reason || 'generated environment check refused configured selftest source');
|
|
507
|
+
}
|
|
508
|
+
const requestedArtifactPath = resolve(capturedInput.probeSpecificationsPath);
|
|
509
|
+
if (!isAbsolute(requestedArtifactPath) || normalize(requestedArtifactPath) !== requestedArtifactPath) {
|
|
510
|
+
return refusal('SELFTEST_SOURCE_INVALID', 'probe specification artifact path must resolve to a canonical absolute path');
|
|
511
|
+
}
|
|
512
|
+
let artifactPath;
|
|
513
|
+
try {
|
|
514
|
+
artifactPath = await realpath(requestedArtifactPath);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
return probeSpecificationsRefusal(`probe specification file ${requestedArtifactPath} is missing or unreadable: ${errorCause(error)}`);
|
|
518
|
+
}
|
|
519
|
+
const bytes = await readProbeSpecificationsArtifact(artifactPath);
|
|
520
|
+
if (typeof bytes !== 'string')
|
|
521
|
+
return probeSpecificationsRefusal(`probe specification file ${artifactPath} is missing, unreadable, not a regular file or larger than ${MAX_PROBE_SPECIFICATIONS_BYTES} bytes${causeSuffix(bytes?.cause)}`);
|
|
522
|
+
const artifact = probeSpecificationsArtifact(bytes, checked.result.application.sourceSha256, checked.result.application.surfaceSelftest.manifestSha256);
|
|
523
|
+
if (artifact.kind === 'malformed') {
|
|
524
|
+
return probeSpecificationsRefusal(`probe specification file ${artifactPath} is not ${PROBE_SPECIFICATIONS_SCHEMA} JSON with exactly schema, sourceSha256, generatedManifestSha256 and specifications${causeSuffix(artifact.cause)}`);
|
|
525
|
+
}
|
|
526
|
+
if (artifact.kind === 'stale') {
|
|
527
|
+
return {
|
|
528
|
+
kind: 'refusal', status: 2, code: 'SELFTEST_SOURCE_INVALID',
|
|
529
|
+
reason: `probe specification file ${artifactPath} was written for a different cockpit.toml or generated manifest`,
|
|
530
|
+
fix: 'write a new file that keeps the rows you filled for unchanged cells with npx --no cockpit selftest --config <your cockpit.toml> --write-probe-specifications <new file> --probe-specifications <this file>, fill the fields it lists, then pass the new file to --probe-specifications',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const unfilled = unfilledSurfaceSelftestSpecifications(artifact.specifications);
|
|
534
|
+
if (unfilled.length > 0) {
|
|
535
|
+
return {
|
|
536
|
+
kind: 'refusal', status: 2, code: 'SELFTEST_SOURCE_INVALID',
|
|
537
|
+
reason: `probe specification file ${artifactPath} still has ${unfilled.length} row(s) with fields only a person can supply: ${unfilled.slice(0, 5).join('; ')}${unfilled.length > 5 ? `; and ${unfilled.length - 5} more` : ''}`,
|
|
538
|
+
fix: 'replace each null in the listed rows with the probe for that tool, then rerun selftest with the same file',
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const specifications = artifact.specifications;
|
|
542
|
+
const plan = planSurfaceSelftest(checked.result.application.surfaceSelftest, specifications);
|
|
543
|
+
if (plan.kind === 'refusal')
|
|
544
|
+
return refusal(plan.code, plan.reason);
|
|
545
|
+
const artifactSha256 = sha256Hex(bytes);
|
|
546
|
+
const candidate = {
|
|
547
|
+
canonicalPath: artifactPath,
|
|
548
|
+
sha256: artifactSha256,
|
|
549
|
+
sourceSha256: checked.result.application.sourceSha256,
|
|
550
|
+
generatedManifestSha256: checked.result.application.surfaceSelftest.manifestSha256,
|
|
551
|
+
commandVectors: plan.probes.map((probe) => ({
|
|
552
|
+
harness: probe.harness.name,
|
|
553
|
+
capability: probe.capability,
|
|
554
|
+
skill: probe.sample?.skill ?? null,
|
|
555
|
+
invocation: [...probe.invocation],
|
|
556
|
+
versionCommand: [...probe.versionCommand],
|
|
557
|
+
helpCommand: [...probe.helpCommand],
|
|
558
|
+
})),
|
|
559
|
+
};
|
|
560
|
+
const confirmation = await adapters.requestProbeSpecificationsApproval?.(candidate);
|
|
561
|
+
if (confirmation !== artifactSha256) {
|
|
562
|
+
// No `fix` field: the diagnostic index would print it as the remedy for every
|
|
563
|
+
// SELFTEST_EXECUTION_INVALID, including probes that ran and failed.
|
|
564
|
+
return refusal('SELFTEST_EXECUTION_INVALID', 'probe specification artifact requires independent approval of its exact digest and command vectors; run selftest with --interactive at a terminal outside CI and type the file SHA-256 it displays');
|
|
565
|
+
}
|
|
566
|
+
return runSurfaceSelftestApplication({
|
|
567
|
+
application: checked.result.application,
|
|
568
|
+
root: dirname(configPath),
|
|
569
|
+
specifications,
|
|
570
|
+
env: capturedInput.env,
|
|
571
|
+
}, adapters);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Writes the probe-specification file for the current cockpit.toml and generated manifest. The CLI
|
|
575
|
+
* creates `destination` and refuses an existing path, so a filled-in file is never overwritten.
|
|
576
|
+
*/
|
|
577
|
+
async function writeConfiguredSurfaceSelftestSpecifications(input) {
|
|
578
|
+
const refused = (message) => ({
|
|
579
|
+
status: 2, diagnostics: [{ severity: 'error', code: 'SELFTEST_SOURCE_INVALID', message }], result: null,
|
|
580
|
+
});
|
|
581
|
+
let configPath;
|
|
582
|
+
try {
|
|
583
|
+
configPath = await realpath(resolve(input.configPath));
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
return refused(`configured selftest source could not be resolved: ${errorCause(error)}`);
|
|
587
|
+
}
|
|
588
|
+
const checked = await checkGeneratedEnvironment({ configPath });
|
|
589
|
+
if (checked.kind !== 'complete') {
|
|
590
|
+
return refused(checked.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join('; ')
|
|
591
|
+
|| 'generated environment check refused configured selftest source');
|
|
592
|
+
}
|
|
593
|
+
let previous = [];
|
|
594
|
+
let previousPath;
|
|
595
|
+
if (input.previousProbeSpecificationsPath !== undefined) {
|
|
596
|
+
try {
|
|
597
|
+
previousPath = await realpath(resolve(input.previousProbeSpecificationsPath));
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
return refused(`earlier probe specification file ${resolve(input.previousProbeSpecificationsPath)} is missing or unreadable: ${errorCause(error)}`);
|
|
601
|
+
}
|
|
602
|
+
const bytes = await readProbeSpecificationsArtifact(previousPath);
|
|
603
|
+
if (typeof bytes !== 'string')
|
|
604
|
+
return refused(`earlier probe specification file ${previousPath} is unreadable, not a regular file or larger than ${MAX_PROBE_SPECIFICATIONS_BYTES} bytes${causeSuffix(bytes?.cause)}`);
|
|
605
|
+
// A stale file is the expected input: the digests changed, the rows may not have.
|
|
606
|
+
const earlier = probeSpecificationsArtifact(bytes, checked.result.application.sourceSha256, checked.result.application.surfaceSelftest.manifestSha256);
|
|
607
|
+
if (earlier.kind === 'malformed') {
|
|
608
|
+
return refused(`earlier probe specification file ${previousPath} is not ${PROBE_SPECIFICATIONS_SCHEMA} JSON with exactly schema, sourceSha256, generatedManifestSha256 and specifications${causeSuffix(earlier.cause)}`);
|
|
609
|
+
}
|
|
610
|
+
previous = earlier.specifications;
|
|
611
|
+
}
|
|
612
|
+
const draft = draftSurfaceSelftestSpecifications(checked.result.application.surfaceSelftest, previous);
|
|
613
|
+
if (!draft)
|
|
614
|
+
return refused('generated selftest projection is invalid');
|
|
615
|
+
const { specifications, unprobeable, carried } = draft;
|
|
616
|
+
const destination = resolve(input.writeProbeSpecificationsPath);
|
|
617
|
+
const unfilled = unfilledSurfaceSelftestSpecifications(specifications);
|
|
618
|
+
return {
|
|
619
|
+
status: 0,
|
|
620
|
+
diagnostics: unprobeable.map((cell) => ({
|
|
621
|
+
severity: 'warning',
|
|
622
|
+
code: 'SELFTEST_CUSTOM_PROBE_UNSUPPORTED',
|
|
623
|
+
message: `no row written for ${surfaceSelftestCellLabel(cell)}: ${UNPROBEABLE_REASON}, so selftest records this cell human_needed with enforcement_probe_required`,
|
|
624
|
+
target: surfaceSelftestCellLabel(cell),
|
|
625
|
+
fix: 'measure the custom harness capability by hand with a candidate and a deliberately broken control, then record what you saw as its state and measured_on in cockpit.toml (enforced needs an evidence file); do not add a row for it',
|
|
626
|
+
})),
|
|
627
|
+
artifact: `${JSON.stringify({
|
|
628
|
+
schema: PROBE_SPECIFICATIONS_SCHEMA,
|
|
629
|
+
sourceSha256: checked.result.application.sourceSha256,
|
|
630
|
+
generatedManifestSha256: checked.result.application.surfaceSelftest.manifestSha256,
|
|
631
|
+
specifications,
|
|
632
|
+
}, null, 2)}\n`,
|
|
633
|
+
destination,
|
|
634
|
+
result: {
|
|
635
|
+
probeSpecifications: destination,
|
|
636
|
+
rows: specifications.length,
|
|
637
|
+
fromGeneratedManifest: 'harness, capability, sample.skill and unavailableCode for every cell that needs evidence',
|
|
638
|
+
fromCockpit: 'versionCommand and helpCommand, the --version and --help identity probes; review them when you approve the file',
|
|
639
|
+
...(previousPath === undefined ? {} : {
|
|
640
|
+
carriedForward: `${carried} of ${specifications.length} row(s) kept unchanged from ${previousPath}; every other row is new or changed, or was not complete there`,
|
|
641
|
+
}),
|
|
642
|
+
fromYou: 'each null: invocation (argv starting with the placeholder already in versionCommand, with exactly one {control}), candidateCode and brokenCode (the exact lines the tool prints for an accepted and a broken control), and sample.features for skill-parity rows',
|
|
643
|
+
unfilled,
|
|
644
|
+
then: cockpitCommand('selftest', { config: configPath, args: `--interactive --probe-specifications ${formatShellCommand([destination])}` }),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
export async function runConfiguredSurfaceSelftestOutcome(input) {
|
|
649
|
+
if ('writeProbeSpecificationsPath' in input)
|
|
650
|
+
return writeConfiguredSurfaceSelftestSpecifications(input);
|
|
651
|
+
const { requestExecutableApproval, requestProbeSpecificationsApproval, ...commandInput } = input;
|
|
652
|
+
const outcome = await runConfiguredSurfaceSelftestApplication(commandInput, {
|
|
653
|
+
execute: executeProbe,
|
|
654
|
+
...(requestExecutableApproval ? { requestExecutableApproval } : {}),
|
|
655
|
+
...(requestProbeSpecificationsApproval ? { requestProbeSpecificationsApproval } : {}),
|
|
656
|
+
});
|
|
657
|
+
return outcome.kind === 'complete'
|
|
658
|
+
? { status: 0, diagnostics: [], result: { bundle: outcome.bundle, evidence: outcome.evidence } }
|
|
659
|
+
: { status: 2, diagnostics: [{ severity: 'error', code: outcome.code, message: outcome.reason,
|
|
660
|
+
...(outcome.fix ? { fix: outcome.fix } : {}) }], result: null };
|
|
661
|
+
}
|
|
662
|
+
function selftestOperatorRefusal(message) {
|
|
663
|
+
return {
|
|
664
|
+
artifact: null, destination: null,
|
|
665
|
+
status: 2, result: null,
|
|
666
|
+
diagnostics: [{ severity: 'error', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, message, target: 'selftest' }],
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function confirmSelftestExecutable(io, candidate) {
|
|
670
|
+
if (io.confirmSelftestExecutable)
|
|
671
|
+
return io.confirmSelftestExecutable(candidate);
|
|
672
|
+
if (!io.stdin)
|
|
673
|
+
return undefined;
|
|
674
|
+
io.stderr.write(`${JSON.stringify(candidate)}\n`);
|
|
675
|
+
const prompt = createInterface({ input: io.stdin, output: io.stderr, terminal: true });
|
|
676
|
+
try {
|
|
677
|
+
const answer = await prompt.question('Type the exact SHA-256 to approve this executable for this selftest run: ');
|
|
678
|
+
return answer.trim();
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
prompt.close();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function confirmSelftestProbeSpecifications(io, candidate) {
|
|
685
|
+
if (io.confirmSelftestProbeSpecifications)
|
|
686
|
+
return io.confirmSelftestProbeSpecifications(candidate);
|
|
687
|
+
if (!io.stdin)
|
|
688
|
+
return undefined;
|
|
689
|
+
io.stderr.write(`${JSON.stringify(candidate)}\n`);
|
|
690
|
+
const prompt = createInterface({ input: io.stdin, output: io.stderr, terminal: true });
|
|
691
|
+
try {
|
|
692
|
+
const answer = await prompt.question('Type the exact SHA-256 to approve this probe artifact and its command vectors: ');
|
|
693
|
+
return answer.trim();
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
prompt.close();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
export async function runSelftestOperatorCommand(input, options, io, handler) {
|
|
700
|
+
if (!options.interactive)
|
|
701
|
+
return handler(input);
|
|
702
|
+
if (options.json || options.ci && options.ci.length > 0) {
|
|
703
|
+
return selftestOperatorRefusal('interactive selftest requires a human terminal outside CI and JSON modes');
|
|
704
|
+
}
|
|
705
|
+
if ((!io.confirmSelftestExecutable || !io.confirmSelftestProbeSpecifications) && io.stdin?.isTTY !== true) {
|
|
706
|
+
return selftestOperatorRefusal('interactive selftest requires terminal input');
|
|
707
|
+
}
|
|
708
|
+
return handler({
|
|
709
|
+
...input,
|
|
710
|
+
requestExecutableApproval: (candidate) => confirmSelftestExecutable(io, candidate),
|
|
711
|
+
requestProbeSpecificationsApproval: (candidate) => confirmSelftestProbeSpecifications(io, candidate),
|
|
712
|
+
});
|
|
713
|
+
}
|