@b00dea/cockpit 0.0.0-stage → 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 +22 -2
- 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 +74 -4
- 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,26 @@
|
|
|
1
|
+
import type { DerivedRegistry } from '../../registry-derivation.js';
|
|
2
|
+
import type { SurfaceSourceFile } from '../../surfaces/contract.js';
|
|
3
|
+
import type { OwnershipManifest } from '../../surfaces/ownership.js';
|
|
4
|
+
import { type CustomSurfaceReceipt } from '../../surfaces/render.js';
|
|
5
|
+
import { type CustomSourceIdentity, type GeneratedEnvironmentCommandAdapters, type PreparedEnvironment } from './shared.js';
|
|
6
|
+
export declare function executeCustomCapability(prepared: PreparedEnvironment, adapters: GeneratedEnvironmentCommandAdapters, harness: string, capability: Extract<DerivedRegistry['config']['surfaces']['harnesses'][number]['capabilities'][number], {
|
|
7
|
+
state: 'enforced' | 'instructed';
|
|
8
|
+
}>, sources: readonly SurfaceSourceFile[]): Promise<{
|
|
9
|
+
receipt: CustomSurfaceReceipt;
|
|
10
|
+
sources: readonly CustomSourceIdentity[];
|
|
11
|
+
evidence?: Readonly<{
|
|
12
|
+
reference: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
}>;
|
|
15
|
+
} | undefined>;
|
|
16
|
+
export declare function projectExplicitNativeHooks(prepared: PreparedEnvironment, manifest: OwnershipManifest | undefined, adapters: GeneratedEnvironmentCommandAdapters, policyDeclaration: Readonly<{
|
|
17
|
+
path: string;
|
|
18
|
+
sha256: string;
|
|
19
|
+
}>, harnesses: readonly string[]): Promise<{
|
|
20
|
+
projection: import("../../testing/hooks.js").NativeHookProjection;
|
|
21
|
+
source: {
|
|
22
|
+
path: string;
|
|
23
|
+
sha256: string;
|
|
24
|
+
identity: import("../../generated-environment/transaction.js").FileIdentity;
|
|
25
|
+
};
|
|
26
|
+
} | undefined>;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { chmod, lstat, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { authenticatedExecutionOutcome } from '../../environment-exec/execute.js';
|
|
5
|
+
import { surfaceModelSha256 } from '../../surfaces/render.js';
|
|
6
|
+
import { readAnchoredFile, readAnchoredBatches } from '../../generated-environment/transaction.js';
|
|
7
|
+
import { snapshotExecutionStep, snapshotGeneratedJson, RefusalError, hasErrorCode } from '../contract.js';
|
|
8
|
+
import { resolveExecutablePath, resolveGitExecutable } from '../executable.js';
|
|
9
|
+
import { cockpitId } from '../identity.js';
|
|
10
|
+
import { NATIVE_HOOK_COST_SCRIPT, projectNativeHooks } from '../../testing/hooks.js';
|
|
11
|
+
import { exactKeys } from '../../exact-keys.js';
|
|
12
|
+
import { HELPER_TIMEOUT_MS, MAX_EXECUTABLE_BYTES, MAX_GENERATED_BYTES, MAX_HELPER_FRAME_BYTES, MAX_SURFACE_FILE_BYTES, NATIVE_HOOK_COST_TIMEOUT_MS, SHA256, confined, ownRecord, sha256, } from './shared.js';
|
|
13
|
+
// The worker runs in its own process and cannot import modules; `safe` mirrors safeSurfacePath in
|
|
14
|
+
// surfaces/contract.ts, including its case-folded reserved segments.
|
|
15
|
+
const CUSTOM_SURFACE_WORKER = String.raw `
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const crypto = require('node:crypto');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const request = JSON.parse(process.argv[1]);
|
|
20
|
+
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
21
|
+
const own = value => value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
22
|
+
const safe = value => typeof value === 'string' && value.length > 0 && !path.isAbsolute(value) && !value.includes('\\') && !value.includes('\0') && value.split('/').every(part => part && part !== '.' && part !== '..' && !['.GIT','NODE_MODULES'].includes(part.normalize('NFC').toUpperCase()));
|
|
23
|
+
(async () => {
|
|
24
|
+
const source = fs.readFileSync(process.argv[2]);
|
|
25
|
+
if (digest(source) !== request.sourceSha256) throw new RefusalError('module changed');
|
|
26
|
+
const imported = await import('data:text/javascript;base64,' + source.toString('base64'));
|
|
27
|
+
const fn = imported[request.export]; if (typeof fn !== 'function') throw new RefusalError('export');
|
|
28
|
+
const result = await fn(request.input);
|
|
29
|
+
if (!own(result) || Object.keys(result).sort().join(',') !== 'files,kind' || result.kind !== 'complete' || !Array.isArray(result.files) || result.files.length === 0 || result.files.length > 256) throw new RefusalError('result');
|
|
30
|
+
let total = 0; let spelling = false; const files = [];
|
|
31
|
+
for (const [index, file] of result.files.entries()) {
|
|
32
|
+
if (!own(file) || Object.keys(file).sort().join(',') !== 'bytes,path,scope,sha256' || !safe(file.path) || typeof file.bytes !== 'string' || Buffer.byteLength(file.bytes) > 65536 || digest(file.bytes) !== file.sha256 || !['always','scoped'].includes(file.scope)) throw new RefusalError('file');
|
|
33
|
+
spelling = spelling || file.path === request.spelling || file.bytes.includes(request.spelling);
|
|
34
|
+
total += Buffer.byteLength(file.bytes); if (total > 4194304) throw new RefusalError('limit');
|
|
35
|
+
const output = path.join(request.output, String(index)); fs.writeFileSync(output, file.bytes, { flag: 'wx', mode: 0o600 });
|
|
36
|
+
files.push({ path: file.path, sha256: file.sha256, scope: file.scope, output: String(index) });
|
|
37
|
+
}
|
|
38
|
+
if (!spelling) throw new RefusalError('spelling');
|
|
39
|
+
process.stdout.write(JSON.stringify({ version: 1, files }) + '\n');
|
|
40
|
+
})().catch(() => { process.stdout.write(JSON.stringify({ version: 1, refusal: true }) + '\n'); });
|
|
41
|
+
`;
|
|
42
|
+
export async function executeCustomCapability(prepared, adapters, harness, capability, sources) {
|
|
43
|
+
let evidenceSource;
|
|
44
|
+
if (capability.state === 'enforced') {
|
|
45
|
+
const evidenceRead = await readAnchoredFile(prepared.root, capability.evidence.reference, MAX_SURFACE_FILE_BYTES, prepared.node, adapters);
|
|
46
|
+
if (evidenceRead.kind !== 'complete' || evidenceRead.sha256 !== capability.evidence.sha256)
|
|
47
|
+
return undefined;
|
|
48
|
+
evidenceSource = { harness, feature: capability.feature, path: capability.evidence.reference,
|
|
49
|
+
sha256: evidenceRead.sha256, identity: evidenceRead.identity, maxBytes: MAX_SURFACE_FILE_BYTES };
|
|
50
|
+
}
|
|
51
|
+
const moduleRead = await readAnchoredFile(prepared.root, capability.mechanism.module, MAX_GENERATED_BYTES, prepared.node, adapters);
|
|
52
|
+
if (moduleRead.kind !== 'complete')
|
|
53
|
+
return undefined;
|
|
54
|
+
const permissionFlag = process.allowedNodeEnvironmentFlags.has('--permission') ? '--permission'
|
|
55
|
+
: process.allowedNodeEnvironmentFlags.has('--experimental-permission') ? '--experimental-permission' : undefined;
|
|
56
|
+
if (!permissionFlag || !process.allowedNodeEnvironmentFlags.has('--allow-fs-read')
|
|
57
|
+
|| !process.allowedNodeEnvironmentFlags.has('--allow-fs-write'))
|
|
58
|
+
return undefined;
|
|
59
|
+
let scratch;
|
|
60
|
+
let scratchIdentity;
|
|
61
|
+
try {
|
|
62
|
+
scratch = await realpath(await mkdtemp(join(tmpdir(), 'cockpit-custom-surface-')));
|
|
63
|
+
const scratchStatus = await lstat(scratch);
|
|
64
|
+
if (!scratchStatus.isDirectory() || scratchStatus.isSymbolicLink())
|
|
65
|
+
return undefined;
|
|
66
|
+
scratchIdentity = { dev: scratchStatus.dev, ino: scratchStatus.ino };
|
|
67
|
+
const input = snapshotGeneratedJson({ harness, feature: capability.feature, sources });
|
|
68
|
+
if (!input)
|
|
69
|
+
return undefined;
|
|
70
|
+
const absoluteModule = confined(prepared.root, capability.mechanism.module);
|
|
71
|
+
if (!absoluteModule || [scratch, absoluteModule].some(path => path.includes('*')))
|
|
72
|
+
return undefined;
|
|
73
|
+
const request = { export: capability.mechanism.export, input, output: scratch,
|
|
74
|
+
sourceSha256: moduleRead.sha256, spelling: capability.spelling };
|
|
75
|
+
const raw = JSON.stringify(request);
|
|
76
|
+
if (Buffer.byteLength(raw) > MAX_HELPER_FRAME_BYTES)
|
|
77
|
+
return undefined;
|
|
78
|
+
const step = snapshotExecutionStep({
|
|
79
|
+
id: `custom-surface-${harness}-${capability.feature}`, argv: [prepared.node.canonicalPath, permissionFlag,
|
|
80
|
+
'--allow-fs-read', absoluteModule, `--allow-fs-read=${scratch}`, `--allow-fs-write=${scratch}`,
|
|
81
|
+
'-e', CUSTOM_SURFACE_WORKER, raw, absoluteModule],
|
|
82
|
+
argv0: prepared.node.invocationPath, cwd: scratch, env: {}, timeoutMs: HELPER_TIMEOUT_MS,
|
|
83
|
+
spawnNotAfter: new Date(Date.now() + HELPER_TIMEOUT_MS).toISOString(), stdoutLimitBytes: MAX_HELPER_FRAME_BYTES,
|
|
84
|
+
stderrLimitBytes: 16_384, paths: [prepared.root, scratch], ports: [], labels: { harness, feature: capability.feature }, expected: [],
|
|
85
|
+
consumedFiles: [
|
|
86
|
+
{ path: prepared.node.canonicalPath, sha256: prepared.node.sha256, maxBytes: MAX_EXECUTABLE_BYTES, argvIndexes: [0], executable: true, executionMode: prepared.node.executionMode },
|
|
87
|
+
{ path: absoluteModule, sha256: moduleRead.sha256, maxBytes: MAX_GENERATED_BYTES, argvIndexes: [3, 9], executable: false },
|
|
88
|
+
], filesystemChecks: [{ path: scratch, expected: 'directory' }], compensation: [],
|
|
89
|
+
});
|
|
90
|
+
if (!step)
|
|
91
|
+
return undefined;
|
|
92
|
+
const outcome = authenticatedExecutionOutcome(await adapters.execute(step), step);
|
|
93
|
+
if (!outcome || outcome.kind !== 'complete' || outcome.stderr.byteLength !== 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(outcome.stdout);
|
|
96
|
+
const record = ownRecord(JSON.parse(text));
|
|
97
|
+
if (!record || !exactKeys(record, ['version', 'files']) || record.version !== 1 || !Array.isArray(record.files))
|
|
98
|
+
return undefined;
|
|
99
|
+
const descriptors = [];
|
|
100
|
+
for (const [index, value] of record.files.entries()) {
|
|
101
|
+
const row = ownRecord(value);
|
|
102
|
+
if (!row || !exactKeys(row, ['path', 'sha256', 'scope', 'output']) || row.output !== String(index)
|
|
103
|
+
|| typeof row.path !== 'string' || typeof row.sha256 !== 'string' || !SHA256.test(row.sha256)
|
|
104
|
+
|| row.scope !== 'always' && row.scope !== 'scoped')
|
|
105
|
+
return undefined;
|
|
106
|
+
descriptors.push({ path: row.path, sha256: row.sha256, scope: row.scope, output: row.output });
|
|
107
|
+
}
|
|
108
|
+
const reads = await readAnchoredBatches(scratch, descriptors.map(row => ({
|
|
109
|
+
path: row.output, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file',
|
|
110
|
+
})), prepared.node, adapters);
|
|
111
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== descriptors.length)
|
|
112
|
+
return undefined;
|
|
113
|
+
const files = [];
|
|
114
|
+
for (const [index, row] of descriptors.entries()) {
|
|
115
|
+
const read = reads.results[index];
|
|
116
|
+
if (read.kind !== 'complete' || read.sha256 !== row.sha256
|
|
117
|
+
|| read.identity.root.dev !== scratchIdentity.dev || read.identity.root.ino !== scratchIdentity.ino)
|
|
118
|
+
return undefined;
|
|
119
|
+
const bytes = new TextDecoder('utf-8', { fatal: true }).decode(read.bytes);
|
|
120
|
+
files.push({ path: row.path, bytes, sha256: row.sha256, scope: row.scope });
|
|
121
|
+
}
|
|
122
|
+
if (files.length === 0 || !files.some(file => file.path === capability.spelling || file.bytes.includes(capability.spelling)))
|
|
123
|
+
return undefined;
|
|
124
|
+
const resultSha256 = surfaceModelSha256(files);
|
|
125
|
+
if (!resultSha256)
|
|
126
|
+
return undefined;
|
|
127
|
+
return {
|
|
128
|
+
receipt: { harness, feature: capability.feature, state: capability.state, measuredOn: capability.measured_on,
|
|
129
|
+
module: capability.mechanism.module, export: capability.mechanism.export, sourceSha256: moduleRead.sha256,
|
|
130
|
+
resultSha256, files },
|
|
131
|
+
sources: [
|
|
132
|
+
{ harness, feature: capability.feature, path: capability.mechanism.module,
|
|
133
|
+
sha256: moduleRead.sha256, identity: moduleRead.identity, maxBytes: MAX_GENERATED_BYTES },
|
|
134
|
+
...(evidenceSource ? [evidenceSource] : []),
|
|
135
|
+
],
|
|
136
|
+
...(capability.state === 'enforced' ? { evidence: { ...capability.evidence } } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch { /* an unavailable authority is refused by the caller. */
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (scratch && scratchIdentity) {
|
|
144
|
+
try {
|
|
145
|
+
const current = await lstat(scratch);
|
|
146
|
+
if (current.isDirectory() && !current.isSymbolicLink()
|
|
147
|
+
&& current.dev === scratchIdentity.dev && current.ino === scratchIdentity.ino) {
|
|
148
|
+
await rm(scratch, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!hasErrorCode(error, 'ENOENT'))
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function projectExplicitNativeHooks(prepared, manifest, adapters, policyDeclaration, harnesses) {
|
|
159
|
+
if (!manifest)
|
|
160
|
+
return undefined;
|
|
161
|
+
const policyRead = await readAnchoredFile(prepared.root, policyDeclaration.path, MAX_SURFACE_FILE_BYTES, prepared.node, adapters);
|
|
162
|
+
if (policyRead.kind !== 'complete' || policyRead.sha256 !== policyDeclaration.sha256)
|
|
163
|
+
return undefined;
|
|
164
|
+
let policyBytes;
|
|
165
|
+
try {
|
|
166
|
+
policyBytes = new TextDecoder('utf-8', { fatal: true }).decode(policyRead.bytes);
|
|
167
|
+
}
|
|
168
|
+
catch { /* undecodable bytes are malformed input the caller refuses. */
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const policy = {
|
|
172
|
+
manifest,
|
|
173
|
+
authority: { root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath },
|
|
174
|
+
declarations: [],
|
|
175
|
+
path: policyDeclaration.path,
|
|
176
|
+
exists: true,
|
|
177
|
+
bytes: policyBytes,
|
|
178
|
+
};
|
|
179
|
+
const source = { path: policyDeclaration.path, sha256: policyRead.sha256, identity: policyRead.identity };
|
|
180
|
+
const git = await resolveGitExecutable(process.env);
|
|
181
|
+
if (git.state !== 'available')
|
|
182
|
+
return {
|
|
183
|
+
projection: { kind: 'refusal', reason: git.reason },
|
|
184
|
+
source,
|
|
185
|
+
};
|
|
186
|
+
const revisionStep = snapshotExecutionStep({
|
|
187
|
+
id: 'native-hook-revisions',
|
|
188
|
+
argv: [git.value.invocationPath, 'rev-list', '--max-count=2', 'HEAD'],
|
|
189
|
+
argv0: git.value.invocationPath,
|
|
190
|
+
cwd: prepared.root,
|
|
191
|
+
env: {},
|
|
192
|
+
timeoutMs: HELPER_TIMEOUT_MS,
|
|
193
|
+
spawnNotAfter: new Date(Date.now() + HELPER_TIMEOUT_MS).toISOString(),
|
|
194
|
+
stdoutLimitBytes: 256,
|
|
195
|
+
stderrLimitBytes: 4_096,
|
|
196
|
+
paths: [prepared.root],
|
|
197
|
+
ports: [],
|
|
198
|
+
labels: { action: 'native-hook-revisions' },
|
|
199
|
+
expected: [],
|
|
200
|
+
consumedFiles: [{ path: git.value.canonicalPath, sha256: git.value.sha256, maxBytes: MAX_EXECUTABLE_BYTES,
|
|
201
|
+
argvIndexes: [0], executable: true, executionMode: git.value.executionMode }],
|
|
202
|
+
filesystemChecks: [{ path: prepared.root, expected: 'directory' }],
|
|
203
|
+
compensation: [],
|
|
204
|
+
});
|
|
205
|
+
if (!revisionStep)
|
|
206
|
+
return undefined;
|
|
207
|
+
const revisionOutcome = authenticatedExecutionOutcome(await adapters.execute(revisionStep), revisionStep);
|
|
208
|
+
if (!revisionOutcome || revisionOutcome.kind !== 'complete' || revisionOutcome.stderr.byteLength !== 0)
|
|
209
|
+
return undefined;
|
|
210
|
+
let revisions;
|
|
211
|
+
try {
|
|
212
|
+
revisions = new TextDecoder('utf-8', { fatal: true }).decode(revisionOutcome.stdout).trim().split('\n');
|
|
213
|
+
}
|
|
214
|
+
catch { /* undecodable bytes are malformed input the caller refuses. */
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
if (revisions.length !== 2 || revisions[0] === revisions[1]
|
|
218
|
+
|| revisions.some(value => !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value) || !/[1-9a-f]/.test(value)))
|
|
219
|
+
return undefined;
|
|
220
|
+
let scratch;
|
|
221
|
+
let scratchIdentity;
|
|
222
|
+
try {
|
|
223
|
+
scratch = await realpath(await mkdtemp(join(tmpdir(), 'cockpit-native-hook-cost-')));
|
|
224
|
+
const status = await lstat(scratch);
|
|
225
|
+
if (!status.isDirectory() || status.isSymbolicLink())
|
|
226
|
+
return undefined;
|
|
227
|
+
scratchIdentity = { dev: status.dev, ino: status.ino };
|
|
228
|
+
const shell = await resolveExecutablePath('/bin/sh');
|
|
229
|
+
if (shell.state !== 'available')
|
|
230
|
+
return undefined;
|
|
231
|
+
const projection = await projectNativeHooks({
|
|
232
|
+
cockpitId: prepared.cockpitId,
|
|
233
|
+
harnesses,
|
|
234
|
+
install: 'explicit-native',
|
|
235
|
+
budget: {
|
|
236
|
+
policy,
|
|
237
|
+
prepareMeasurement: async (entry) => {
|
|
238
|
+
const path = join(scratch, 'pre-push');
|
|
239
|
+
await writeFile(path, entry.bytes, { flag: 'wx', mode: 0o700 });
|
|
240
|
+
await chmod(path, 0o700);
|
|
241
|
+
const step = snapshotExecutionStep({
|
|
242
|
+
id: 'native-hook-cost',
|
|
243
|
+
argv: [shell.value.canonicalPath, '-c', NATIVE_HOOK_COST_SCRIPT, 'native-hook-cost', path, revisions[0], revisions[1]],
|
|
244
|
+
argv0: shell.value.canonicalPath,
|
|
245
|
+
cwd: prepared.root,
|
|
246
|
+
env: { PATH: dirname(git.value.canonicalPath) },
|
|
247
|
+
timeoutMs: NATIVE_HOOK_COST_TIMEOUT_MS,
|
|
248
|
+
spawnNotAfter: new Date(Date.now() + HELPER_TIMEOUT_MS).toISOString(),
|
|
249
|
+
stdoutLimitBytes: 4_096,
|
|
250
|
+
stderrLimitBytes: 4_096,
|
|
251
|
+
paths: [prepared.root, scratch],
|
|
252
|
+
ports: [],
|
|
253
|
+
labels: { action: 'native-hook-cost' },
|
|
254
|
+
expected: [],
|
|
255
|
+
consumedFiles: [
|
|
256
|
+
{ path: shell.value.canonicalPath, sha256: shell.value.sha256, maxBytes: MAX_EXECUTABLE_BYTES,
|
|
257
|
+
argvIndexes: [0], executable: true, executionMode: shell.value.executionMode },
|
|
258
|
+
{ path, sha256: entry.sha256, maxBytes: Buffer.byteLength(entry.bytes), argvIndexes: [4], executable: false },
|
|
259
|
+
],
|
|
260
|
+
filesystemChecks: [{ path: prepared.root, expected: 'directory' }, { path: scratch, expected: 'directory' }],
|
|
261
|
+
compensation: [],
|
|
262
|
+
});
|
|
263
|
+
if (!step)
|
|
264
|
+
throw new RefusalError('native hook cost measurement could not be admitted');
|
|
265
|
+
return step;
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
projection,
|
|
271
|
+
source,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
if (scratch && scratchIdentity) {
|
|
276
|
+
try {
|
|
277
|
+
const current = await lstat(scratch);
|
|
278
|
+
if (current.isDirectory() && !current.isSymbolicLink()
|
|
279
|
+
&& current.dev === scratchIdentity.dev && current.ino === scratchIdentity.ino) {
|
|
280
|
+
await rm(scratch, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (!hasErrorCode(error, 'ENOENT'))
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type OwnershipManifest } from '../../surfaces/ownership.js';
|
|
2
|
+
import { type FileIdentity } from '../../generated-environment/transaction.js';
|
|
3
|
+
import { type GeneratedEnvironmentCommandAdapters, type GeneratedEnvironmentCommandInput, type GeneratedEnvironmentSyncInput, type PreparedEnvironment, type RefusalOutcome } from './shared.js';
|
|
4
|
+
export declare function commandInput(input: unknown): GeneratedEnvironmentCommandInput | undefined;
|
|
5
|
+
export declare function syncCommandInput(input: unknown): GeneratedEnvironmentSyncInput | undefined;
|
|
6
|
+
export declare function safePath(root: string, path: string, allowMissing: boolean): Promise<string | undefined>;
|
|
7
|
+
export type OwnershipState = {
|
|
8
|
+
readonly kind: 'missing';
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'invalid';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'unsupported';
|
|
13
|
+
readonly format: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'complete';
|
|
16
|
+
readonly manifest: OwnershipManifest;
|
|
17
|
+
readonly bytes: string;
|
|
18
|
+
readonly sha256: string;
|
|
19
|
+
readonly identity: FileIdentity;
|
|
20
|
+
};
|
|
21
|
+
export declare function readOwnership(prepared: PreparedEnvironment, adapters: GeneratedEnvironmentCommandAdapters): Promise<OwnershipState>;
|
|
22
|
+
export declare function expectedOutputExists(prepared: PreparedEnvironment): Promise<boolean>;
|
|
23
|
+
export declare function renamedStackRefusal(prepared: PreparedEnvironment): Promise<RefusalOutcome | undefined>;
|
|
24
|
+
/** Why no single Compose service publishes the endpoint's port on loopback, or undefined when exactly one does. */
|
|
25
|
+
export declare function browserEndpointUnpublished(endpoint: string, sources: readonly Readonly<{
|
|
26
|
+
reference: string;
|
|
27
|
+
source: string;
|
|
28
|
+
}>[], bases: Readonly<Record<string, number>>): string | undefined;
|
|
29
|
+
export declare function prepare(input: unknown, adapters: GeneratedEnvironmentCommandAdapters): Promise<PreparedEnvironment | RefusalOutcome>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { isProxy } from 'node:util/types';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { consumeWorkEnvironmentConfigProjection, loadCockpitConfig, projectWorkEnvironmentConfig } from '../../config/load.js';
|
|
6
|
+
import { deriveRegistry } from '../../registry-derivation.js';
|
|
7
|
+
import { SURFACE_TARGETS } from '../../surfaces/contract.js';
|
|
8
|
+
import { parseOwnershipManifest } from '../../surfaces/ownership.js';
|
|
9
|
+
import { nodeAuthority, readAnchoredFile, readAnchoredConfigSource } from '../../generated-environment/transaction.js';
|
|
10
|
+
import { ENVIRONMENT_CODES, hasErrorCode } from '../contract.js';
|
|
11
|
+
import { checkoutId, cockpitId, publicationCockpitId, stackId } from '../identity.js';
|
|
12
|
+
import { composePublishedPorts, variableName } from '../compose-topology.js';
|
|
13
|
+
import { COMPOSE_OWNERSHIP_FILE, renderComposeOwnership } from '../compose-ownership.js';
|
|
14
|
+
import { earlierNameRefusalMessage, findStackUnderEarlierName } from '../lifecycle-stage.js';
|
|
15
|
+
import { selectStateRoot } from '../state.js';
|
|
16
|
+
import { observeGeneratedCheckoutIdentity } from '../setup-observation.js';
|
|
17
|
+
import { exactKeys } from '../../exact-keys.js';
|
|
18
|
+
import { refusalOutcome as refuse } from '../../refusal.js';
|
|
19
|
+
import { COMPOSE_SOURCE_BYTES, MAX_OWNERSHIP_BYTES, OWNERSHIP_PATH, authenticateSiblingRoots, compareCodeUnits, confined, ownRecord, sha256, } from './shared.js';
|
|
20
|
+
export function commandInput(input) {
|
|
21
|
+
try {
|
|
22
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input) || isProxy(input))
|
|
23
|
+
return undefined;
|
|
24
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
25
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
26
|
+
if (keys.some((key) => typeof key !== 'string' || key !== 'configPath')
|
|
27
|
+
|| !Object.hasOwn(descriptors, 'configPath') || !('value' in descriptors.configPath))
|
|
28
|
+
return undefined;
|
|
29
|
+
const configPath = descriptors.configPath.value;
|
|
30
|
+
if (typeof configPath !== 'string' || configPath.length === 0 || configPath.includes('\0'))
|
|
31
|
+
return undefined;
|
|
32
|
+
return { configPath };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// a guard over caller-supplied values; a throwing accessor or proxy trap is malformed input the caller refuses.
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function syncCommandInput(input) {
|
|
40
|
+
try {
|
|
41
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input) || isProxy(input))
|
|
42
|
+
return undefined;
|
|
43
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
44
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
45
|
+
if (keys.some((key) => typeof key !== 'string' || key !== 'configPath' && key !== 'dryRun' && key !== 'bootstrapState')
|
|
46
|
+
|| !Object.hasOwn(descriptors, 'configPath') || !('value' in descriptors.configPath))
|
|
47
|
+
return undefined;
|
|
48
|
+
const configPath = descriptors.configPath.value;
|
|
49
|
+
const dryRun = Object.hasOwn(descriptors, 'dryRun') && 'value' in descriptors.dryRun ? descriptors.dryRun.value : false;
|
|
50
|
+
if (typeof configPath !== 'string' || configPath.length === 0 || configPath.includes('\0') || typeof dryRun !== 'boolean')
|
|
51
|
+
return undefined;
|
|
52
|
+
const bootstrapState = Object.hasOwn(descriptors, 'bootstrapState') && 'value' in descriptors.bootstrapState
|
|
53
|
+
? descriptors.bootstrapState.value : undefined;
|
|
54
|
+
if (bootstrapState !== undefined) {
|
|
55
|
+
const state = ownRecord(bootstrapState);
|
|
56
|
+
if (!state || !exactKeys(state, ['publication', 'address']))
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return { configPath, dryRun, ...(bootstrapState === undefined ? {} : { bootstrapState }) };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// a guard over caller-supplied values; a throwing accessor or proxy trap is malformed input the caller refuses.
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function safePath(root, path, allowMissing) {
|
|
67
|
+
const target = confined(root, path);
|
|
68
|
+
if (!target)
|
|
69
|
+
return undefined;
|
|
70
|
+
const parts = relative(root, target).split(sep);
|
|
71
|
+
let current = root;
|
|
72
|
+
for (const [index, part] of parts.entries()) {
|
|
73
|
+
current = resolve(current, part);
|
|
74
|
+
try {
|
|
75
|
+
const entry = await lstat(current);
|
|
76
|
+
if (entry.isSymbolicLink() || index < parts.length - 1 && !entry.isDirectory()
|
|
77
|
+
|| index === parts.length - 1 && !entry.isFile())
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!hasErrorCode(error, 'ENOENT'))
|
|
82
|
+
return undefined;
|
|
83
|
+
if (!allowMissing && index === parts.length - 1)
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return target;
|
|
88
|
+
}
|
|
89
|
+
export async function readOwnership(prepared, adapters) {
|
|
90
|
+
const read = await readAnchoredFile(prepared.root, OWNERSHIP_PATH, MAX_OWNERSHIP_BYTES, prepared.node, adapters);
|
|
91
|
+
if (read.kind === 'missing')
|
|
92
|
+
return { kind: 'missing' };
|
|
93
|
+
if (read.kind !== 'complete')
|
|
94
|
+
return { kind: 'invalid' };
|
|
95
|
+
try {
|
|
96
|
+
const bytes = new TextDecoder('utf-8', { fatal: true }).decode(read.bytes);
|
|
97
|
+
const parsed = parseOwnershipManifest(bytes, { root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath });
|
|
98
|
+
return parsed.kind === 'complete' ? { kind: 'complete', manifest: parsed.manifest, bytes, sha256: read.sha256, identity: read.identity } : parsed;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// invalid ownership bytes are reported as invalid.
|
|
102
|
+
return { kind: 'invalid' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function expectedOutputExists(prepared) {
|
|
106
|
+
for (const file of prepared.owned) {
|
|
107
|
+
if (await safePath(prepared.root, file.path, false))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// A ledger recorded under another name means `cockpit.name` was renamed. Publishing under the new
|
|
113
|
+
// name is safe for the files, but a stack still recorded under the old one would be left where
|
|
114
|
+
// down cannot reach it, so the rename waits until that stack is down.
|
|
115
|
+
export async function renamedStackRefusal(prepared) {
|
|
116
|
+
const checkout = prepared.runtimeCheckout;
|
|
117
|
+
if (!checkout || process.platform !== 'darwin' && process.platform !== 'linux')
|
|
118
|
+
return undefined;
|
|
119
|
+
const namespace = await selectStateRoot({ platform: process.platform, home: process.env.HOME,
|
|
120
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
121
|
+
checkoutRoots: [checkout.currentCheckoutPath, checkout.mainCheckoutPath], cockpitId: checkout.cockpitId });
|
|
122
|
+
if (namespace.kind !== 'complete')
|
|
123
|
+
return refuse(ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, `cannot tell whether a stack started before cockpit.name was renamed is still recorded: ${'message' in namespace ? namespace.message : 'the state root is missing'}; nothing was written`, prepared.configPath);
|
|
124
|
+
const earlier = await findStackUnderEarlierName({ namespace: namespace.value, cockpitId: checkout.cockpitId, checkoutId: checkout.checkoutId });
|
|
125
|
+
if (earlier.kind === 'refusal')
|
|
126
|
+
return refuse(earlier.code, `${earlier.message}; nothing was written`, earlier.target);
|
|
127
|
+
return earlier.name === undefined ? undefined
|
|
128
|
+
: refuse(ENVIRONMENT_CODES.STACK_RECORDED_UNDER_EARLIER_NAME, earlierNameRefusalMessage(prepared.configPath, earlier.name), prepared.configPath);
|
|
129
|
+
}
|
|
130
|
+
const LOOPBACK_HOST_IPS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
131
|
+
/** Why no single Compose service publishes the endpoint's port on loopback, or undefined when exactly one does. */
|
|
132
|
+
export function browserEndpointUnpublished(endpoint, sources, bases) {
|
|
133
|
+
const port = Number(new URL(endpoint).port || 80);
|
|
134
|
+
const publishers = [];
|
|
135
|
+
for (const { reference, source } of sources) {
|
|
136
|
+
const document = parseDocument(source);
|
|
137
|
+
const services = document.errors.length === 0 ? document.toJS()?.services : undefined;
|
|
138
|
+
for (const [id, service] of Object.entries(services ?? {})) {
|
|
139
|
+
const ports = composePublishedPorts(service?.ports);
|
|
140
|
+
if (!ports)
|
|
141
|
+
return `${reference} service ${id} has a ports entry cockpit cannot read, so it cannot tell whether it publishes browser.endpoint`;
|
|
142
|
+
if (ports.some(published => {
|
|
143
|
+
if (published.hostIp === null || !LOOPBACK_HOST_IPS.has(published.hostIp) || published.published === null)
|
|
144
|
+
return false;
|
|
145
|
+
const name = variableName(published.published);
|
|
146
|
+
return (name === undefined ? Number(published.published) : bases[name]) === port;
|
|
147
|
+
}))
|
|
148
|
+
publishers.push(`${reference}:${id}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return publishers.length === 1 ? undefined
|
|
152
|
+
: publishers.length === 0 ? `browser.endpoint port ${port} is not published on loopback by any service in environment.compose_files`
|
|
153
|
+
: `browser.endpoint port ${port} is published on loopback by more than one service (${publishers.join(', ')})`;
|
|
154
|
+
}
|
|
155
|
+
export async function prepare(input, adapters) {
|
|
156
|
+
const request = commandInput(input);
|
|
157
|
+
if (!request)
|
|
158
|
+
return refuse('GENERATED_INPUT_INVALID', 'generated environment command input is invalid', 'generated-environment');
|
|
159
|
+
const loaded = await loadCockpitConfig(request.configPath);
|
|
160
|
+
if (loaded.kind !== 'complete')
|
|
161
|
+
return {
|
|
162
|
+
kind: 'refusal', artifact: null, destination: null, result: null, status: 2,
|
|
163
|
+
diagnostics: [...loaded.diagnostics],
|
|
164
|
+
};
|
|
165
|
+
const workProjection = projectWorkEnvironmentConfig(loaded.result);
|
|
166
|
+
const workEnvironment = workProjection && consumeWorkEnvironmentConfigProjection(workProjection);
|
|
167
|
+
if (!workEnvironment || workEnvironment.source.path !== loaded.result.source.path
|
|
168
|
+
|| workEnvironment.source.sha256 !== loaded.result.source.sha256) {
|
|
169
|
+
return refuse('GENERATED_RENDER_UNAUTHENTICATED', 'the work environment configuration could not be consumed', loaded.result.source.path);
|
|
170
|
+
}
|
|
171
|
+
const derived = deriveRegistry(loaded.result.config);
|
|
172
|
+
if (derived.kind !== 'complete')
|
|
173
|
+
return refuse(derived.code, derived.message, loaded.result.source.path);
|
|
174
|
+
const config = derived.result.config;
|
|
175
|
+
const root = dirname(loaded.result.source.path);
|
|
176
|
+
const authenticatedSiblings = await authenticateSiblingRoots(root, derived.result.config, loaded.result.source.path);
|
|
177
|
+
if (authenticatedSiblings.kind === 'refusal')
|
|
178
|
+
return refuse(authenticatedSiblings.code ?? 'GENERATED_SIBLING_ROOT_INVALID', authenticatedSiblings.reason, authenticatedSiblings.target ?? 'sibling', authenticatedSiblings.fix);
|
|
179
|
+
const siblingRoots = authenticatedSiblings.roots;
|
|
180
|
+
const runtimeCheckout = workEnvironment.browser || config.environment.compose_files.length > 0
|
|
181
|
+
? await observeGeneratedCheckoutIdentity({ source: loaded.result.source, cockpitName: config.cockpit.name,
|
|
182
|
+
env: process.env }) : undefined;
|
|
183
|
+
if (runtimeCheckout?.state === 'failed')
|
|
184
|
+
return refuse('GENERATED_INPUT_INVALID', `generated checkout identity is unavailable: ${runtimeCheckout.reason}`, loaded.result.source.path, 'restore the configuration checkout and Git installation, then run npx --no cockpit sync --config <your cockpit.toml>');
|
|
185
|
+
const currentCockpit = runtimeCheckout?.cockpitId ?? cockpitId({ name: config.cockpit.name, mainCheckoutPath: root });
|
|
186
|
+
const publishedCockpit = publicationCockpitId({ name: config.cockpit.name });
|
|
187
|
+
const currentCheckout = runtimeCheckout?.checkoutId ?? checkoutId({ checkoutPath: root });
|
|
188
|
+
const currentStack = stackId({ cockpitId: currentCockpit, checkoutId: currentCheckout, variant: { kind: 'shared' } });
|
|
189
|
+
// Derived from the runtime declarations, not the file bytes, so rewording a purpose or a guard
|
|
190
|
+
// keeps the running stack's identity.
|
|
191
|
+
const generation = sha256(`generated-environment\0${loaded.result.runtimeSha256}`);
|
|
192
|
+
const node = await nodeAuthority();
|
|
193
|
+
if (!node)
|
|
194
|
+
return refuse('GENERATED_EXECUTABLE_UNAVAILABLE', 'the current Node executable could not be authenticated', process.execPath);
|
|
195
|
+
let composeOwnership;
|
|
196
|
+
const composeSources = [];
|
|
197
|
+
const sources = [];
|
|
198
|
+
if (config.environment.compose_files.length > 0) {
|
|
199
|
+
for (const reference of config.environment.compose_files) {
|
|
200
|
+
const read = await readAnchoredFile(root, reference, COMPOSE_SOURCE_BYTES, node, adapters);
|
|
201
|
+
if (read.kind !== 'complete')
|
|
202
|
+
return refuse('GENERATED_PATH_UNSAFE', 'a declared Compose file is missing, unreadable, or crosses a symbolic-link boundary', reference);
|
|
203
|
+
sources.push({ reference, source: read.bytes.toString('utf8') });
|
|
204
|
+
composeSources.push({ path: reference, sha256: read.sha256, identity: read.identity });
|
|
205
|
+
}
|
|
206
|
+
const ownership = renderComposeOwnership({ sources, stackId: currentStack, cockpitId: currentCockpit,
|
|
207
|
+
configSha256: loaded.result.runtimeSha256, resettableVolumes: config.environment.resettable_volumes });
|
|
208
|
+
if (ownership.kind !== 'complete')
|
|
209
|
+
return refuse('GENERATED_DECLARATION_INVALID', `the ownership override cannot be rendered from environment.compose_files: ${ownership.reason}`, ownership.target);
|
|
210
|
+
composeOwnership = { path: COMPOSE_OWNERSHIP_FILE, bytes: ownership.bytes, sha256: sha256(ownership.bytes) };
|
|
211
|
+
}
|
|
212
|
+
// A browser endpoint is a server the project's own Compose files run, so it must be reachable
|
|
213
|
+
// only from this machine, by exactly the service that publishes its port.
|
|
214
|
+
const endpoint = workEnvironment.browser?.endpoint;
|
|
215
|
+
const unpublished = endpoint === undefined ? undefined : browserEndpointUnpublished(endpoint, sources, config.ports.bases);
|
|
216
|
+
if (unpublished)
|
|
217
|
+
return refuse('GENERATED_BROWSER_ENDPOINT_UNPUBLISHED', unpublished, 'browser', 'publish the endpoint port from exactly one service in environment.compose_files as "127.0.0.1:${VAR}:<container port>" with VAR under [ports.bases] set to the endpoint port, or remove browser.endpoint to drive a local browser, then run npx --no cockpit sync --config <your cockpit.toml>');
|
|
218
|
+
const generated = composeOwnership ? [composeOwnership] : [];
|
|
219
|
+
// The anchored read of the configuration file pins the publication root's identity for the whole sync.
|
|
220
|
+
const sourcePath = relative(root, loaded.result.source.path);
|
|
221
|
+
const sourceTarget = await safePath(root, sourcePath, false);
|
|
222
|
+
if (!sourceTarget)
|
|
223
|
+
return refuse('GENERATED_CONFIG_SOURCE_UNAVAILABLE', 'the cockpit configuration file is missing or crosses a symbolic-link boundary', sourcePath);
|
|
224
|
+
const source = await readAnchoredConfigSource(root, sourcePath, node, adapters);
|
|
225
|
+
if (!source) {
|
|
226
|
+
return refuse('GENERATED_CONFIG_SOURCE_UNAVAILABLE', 'the cockpit configuration file is unreadable', sourcePath);
|
|
227
|
+
}
|
|
228
|
+
const owned = [...generated].sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
229
|
+
if (new Set(owned.map(({ path }) => path)).size !== owned.length
|
|
230
|
+
|| owned.some(({ path }) => path === OWNERSHIP_PATH)
|
|
231
|
+
|| owned.some(({ path }) => [loaded.result.source.path, sourceTarget].includes(resolve(root, path)))) {
|
|
232
|
+
return refuse('GENERATED_PATH_DUPLICATE', 'generated ownership contains a duplicate or input path', 'generated-environment');
|
|
233
|
+
}
|
|
234
|
+
for (const file of owned) {
|
|
235
|
+
if (!await safePath(root, file.path, true)) {
|
|
236
|
+
return refuse('GENERATED_PATH_UNSAFE', 'generated destination crosses an existing symbolic-link or non-directory boundary', file.path);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const producedAt = (adapters.now ?? (() => new Date().toISOString()))();
|
|
240
|
+
const now = producedAt.slice(0, 10);
|
|
241
|
+
return {
|
|
242
|
+
root,
|
|
243
|
+
cockpitId: publishedCockpit,
|
|
244
|
+
stackId: currentStack,
|
|
245
|
+
...(runtimeCheckout ? { runtimeCheckout } : {}),
|
|
246
|
+
configPath: loaded.result.source.path,
|
|
247
|
+
sourceSha256: loaded.result.source.sha256,
|
|
248
|
+
runtimeSha256: loaded.result.runtimeSha256,
|
|
249
|
+
configSource: { path: sourcePath, sha256: source.sha256 },
|
|
250
|
+
configSourceIdentity: source.identity,
|
|
251
|
+
generation,
|
|
252
|
+
generated,
|
|
253
|
+
owned,
|
|
254
|
+
surfaces: [],
|
|
255
|
+
compactSurfacePaths: new Set(),
|
|
256
|
+
configuredHarnesses: [...SURFACE_TARGETS],
|
|
257
|
+
activeSurfaceOverrides: [],
|
|
258
|
+
changedOverrideOriginals: [],
|
|
259
|
+
composeSources,
|
|
260
|
+
guideSources: { requests: [], observations: [], missing: [] },
|
|
261
|
+
openCodeGrantRoots: [],
|
|
262
|
+
sourceEvidence: [],
|
|
263
|
+
sourceRoots: [],
|
|
264
|
+
customSources: [],
|
|
265
|
+
adoptedSources: [],
|
|
266
|
+
surfaceAssets: [],
|
|
267
|
+
warnings: [],
|
|
268
|
+
unapprovedMcpServers: [],
|
|
269
|
+
registry: derived.result,
|
|
270
|
+
siblingRoots,
|
|
271
|
+
workEnvironment,
|
|
272
|
+
node,
|
|
273
|
+
now,
|
|
274
|
+
producedAt,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type GeneratedPathClassificationResult, type OwnershipManifest, type SurfaceDiagnostic, type SurfacePriorOwnedEntry } from '../../surfaces/ownership.js';
|
|
2
|
+
import { type SurfaceObservedEntry, type SurfaceChangePlanResult } from '../../surfaces/sync.js';
|
|
3
|
+
import { type AnchoredEntry, type AnchoredBatchReadResult, type PublicationPlan } from '../../generated-environment/transaction.js';
|
|
4
|
+
import { type GeneratedEnvironmentCommandAdapters, type GeneratedEnvironmentSyncInput, type PreparedEnvironment, type RefusalOutcome } from './shared.js';
|
|
5
|
+
import { type OwnershipState } from './prepare.js';
|
|
6
|
+
import { type UnfinishedInit } from './bootstrap-skill.js';
|
|
7
|
+
export declare function inputSourcesCurrent(prepared: PreparedEnvironment, adapters: GeneratedEnvironmentCommandAdapters, deadline?: number): Promise<boolean>;
|
|
8
|
+
export declare function ownedInputOverlap(prepared: PreparedEnvironment, prior: readonly Readonly<{
|
|
9
|
+
path: string;
|
|
10
|
+
}>[]): Promise<Readonly<{
|
|
11
|
+
kind: 'complete';
|
|
12
|
+
path?: string;
|
|
13
|
+
}> | Readonly<{
|
|
14
|
+
kind: 'invalid';
|
|
15
|
+
}>>;
|
|
16
|
+
export declare function observeSurfaceState(prepared: PreparedEnvironment, prior: readonly SurfacePriorOwnedEntry[], adapters: GeneratedEnvironmentCommandAdapters): Promise<{
|
|
17
|
+
observed: readonly SurfaceObservedEntry[];
|
|
18
|
+
raw: ReadonlyMap<string, AnchoredEntry>;
|
|
19
|
+
opaque: ReadonlySet<string>;
|
|
20
|
+
} | undefined>;
|
|
21
|
+
export declare function classifyOwnedState(prepared: PreparedEnvironment, manifest: OwnershipManifest | undefined, raw: ReadonlyMap<string, AnchoredBatchReadResult>): Promise<GeneratedPathClassificationResult>;
|
|
22
|
+
export declare function planObservedSurfaces(prepared: PreparedEnvironment, priorOwned: readonly SurfacePriorOwnedEntry[], state: NonNullable<Awaited<ReturnType<typeof observeSurfaceState>>>): SurfaceChangePlanResult;
|
|
23
|
+
/** A hand edit to a merged adopted file belongs in its adopted copy; say so where the conflict is reported. */
|
|
24
|
+
export declare function surfacePlanRefusal(prepared: PreparedEnvironment, diagnostic: SurfaceDiagnostic, unfinishedInit?: UnfinishedInit): RefusalOutcome;
|
|
25
|
+
export declare function preparePublication(input: GeneratedEnvironmentSyncInput, adapterOverrides?: Partial<GeneratedEnvironmentCommandAdapters>, seed?: PreparedEnvironment, knownOwnership?: OwnershipState): Promise<RefusalOutcome | Readonly<{
|
|
26
|
+
prepared: PreparedEnvironment;
|
|
27
|
+
ownership: OwnershipState;
|
|
28
|
+
plan: PublicationPlan;
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
prepared: PreparedEnvironment;
|
|
31
|
+
ownership: OwnershipState;
|
|
32
|
+
refusal: RefusalOutcome;
|
|
33
|
+
}>>;
|