@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,685 @@
|
|
|
1
|
+
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, readSync, realpathSync, unlinkSync, writeSync, } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { EnvironmentFile, Sibling } from '../schema.js';
|
|
6
|
+
import { ENVIRONMENT_CODES, OBSERVATION_REASON_BYTE_LIMIT, isCanonicalTimestamp, snapshotEnvironmentObservation, snapshotJson, RefusalError, hasErrorCode } from './contract.js';
|
|
7
|
+
const OWNER = 'environment-template-seeding';
|
|
8
|
+
export const observationDeclarations = Object.freeze([
|
|
9
|
+
{ id: 'environment-key-freshness', owner: OWNER, postures: ['environment'], stages: [{ stage: 'bootstrap', disposition: 'required' }, { stage: 'steady', disposition: 'required' }], code: ENVIRONMENT_CODES.ENVIRONMENT_KEYS_STALE, isIssued: isTemplateObservation, issuedAt: observationIssuedAt },
|
|
10
|
+
{ id: 'secret-file-presence', owner: OWNER, postures: ['environment'], stages: [{ stage: 'bootstrap', disposition: 'required' }, { stage: 'steady', disposition: 'required' }], code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_NOT_FOUND, isIssued: isTemplateObservation, issuedAt: observationIssuedAt },
|
|
11
|
+
{ id: 'sibling-presence', owner: OWNER, postures: ['environment'], stages: [{ stage: 'bootstrap', disposition: 'required' }, { stage: 'steady', disposition: 'required' }], code: ENVIRONMENT_CODES.SIBLING_MISSING, isIssued: isTemplateObservation, issuedAt: observationIssuedAt },
|
|
12
|
+
]);
|
|
13
|
+
const issuedObservations = new WeakMap();
|
|
14
|
+
function issueObservation(value) {
|
|
15
|
+
const snapshot = snapshotEnvironmentObservation(value);
|
|
16
|
+
if (!snapshot || snapshot.owner !== OWNER)
|
|
17
|
+
throw new RefusalError('template observation is malformed');
|
|
18
|
+
issuedObservations.set(snapshot, Date.now());
|
|
19
|
+
return snapshot;
|
|
20
|
+
}
|
|
21
|
+
function isTemplateObservation(value) {
|
|
22
|
+
return value !== null && typeof value === 'object' && issuedObservations.has(value);
|
|
23
|
+
}
|
|
24
|
+
function observationIssuedAt(value) {
|
|
25
|
+
return value !== null && typeof value === 'object' ? issuedObservations.get(value) : undefined;
|
|
26
|
+
}
|
|
27
|
+
export async function observeSiblingPresence(input) {
|
|
28
|
+
const snapshot = snapshotJson(input);
|
|
29
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot))
|
|
30
|
+
throw new RefusalError('sibling presence input is malformed');
|
|
31
|
+
const owned = snapshot;
|
|
32
|
+
const sibling = Sibling.safeParse(owned.sibling);
|
|
33
|
+
if (typeof owned.stackId !== 'string' || !/^stack:[a-f0-9]{64}$/.test(owned.stackId)
|
|
34
|
+
|| typeof owned.observedAt !== 'string' || !isCanonicalTimestamp(owned.observedAt)
|
|
35
|
+
|| typeof owned.siblingRoot !== 'string' || !isAbsolute(owned.siblingRoot)
|
|
36
|
+
|| normalize(owned.siblingRoot) !== owned.siblingRoot || !sibling.success)
|
|
37
|
+
throw new RefusalError('sibling presence input is malformed');
|
|
38
|
+
const path = resolve(owned.siblingRoot, sibling.data.dir);
|
|
39
|
+
const confined = relative(owned.siblingRoot, path);
|
|
40
|
+
if (confined === '..' || confined.startsWith(`..${sep}`) || isAbsolute(confined))
|
|
41
|
+
throw new RefusalError('sibling presence input is malformed');
|
|
42
|
+
let present = false;
|
|
43
|
+
try {
|
|
44
|
+
const status = await lstat(path);
|
|
45
|
+
present = status.isDirectory() && !status.isSymbolicLink();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!hasErrorCode(error, 'ENOENT'))
|
|
49
|
+
throw new RefusalError('sibling presence could not be observed');
|
|
50
|
+
}
|
|
51
|
+
return issueObservation({ code: ENVIRONMENT_CODES.SIBLING_MISSING, owner: OWNER, stackId: owned.stackId, source: path,
|
|
52
|
+
observed: { state: 'available', value: present, observedAt: owned.observedAt }, expectation: { kind: 'equal', value: true },
|
|
53
|
+
applicability: { state: 'required', posture: 'environment' }, freshness: { state: 'current' }, remedy: 'restore the declared sibling checkout' });
|
|
54
|
+
}
|
|
55
|
+
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
56
|
+
const ENVIRONMENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
57
|
+
function projectAssignment(leftHandSide) {
|
|
58
|
+
let candidate = leftHandSide.trim();
|
|
59
|
+
if (candidate.startsWith('export '))
|
|
60
|
+
candidate = candidate.slice('export '.length).trim();
|
|
61
|
+
return ENVIRONMENT_KEY.test(candidate) ? candidate : null;
|
|
62
|
+
}
|
|
63
|
+
async function projectKeys(root, path, maxBytes) {
|
|
64
|
+
const keys = new Set();
|
|
65
|
+
let bytesRead = 0;
|
|
66
|
+
let line = 1;
|
|
67
|
+
let leftHandSide = '';
|
|
68
|
+
let assignment = false;
|
|
69
|
+
let ignoredLine = false;
|
|
70
|
+
function finishLine() {
|
|
71
|
+
const candidate = leftHandSide.endsWith('\r') ? leftHandSide.slice(0, -1) : leftHandSide;
|
|
72
|
+
const trimmed = candidate.trim();
|
|
73
|
+
if (!assignment && !ignoredLine && trimmed !== '' && !trimmed.startsWith('#')) {
|
|
74
|
+
return { kind: 'malformed', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_MALFORMED, line };
|
|
75
|
+
}
|
|
76
|
+
if (assignment) {
|
|
77
|
+
const key = projectAssignment(candidate);
|
|
78
|
+
if (key === null || keys.has(key)) {
|
|
79
|
+
return { kind: 'malformed', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_MALFORMED, line };
|
|
80
|
+
}
|
|
81
|
+
keys.add(key);
|
|
82
|
+
}
|
|
83
|
+
line += 1;
|
|
84
|
+
leftHandSide = '';
|
|
85
|
+
assignment = false;
|
|
86
|
+
ignoredLine = false;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
let handle;
|
|
90
|
+
try {
|
|
91
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const code = error.code;
|
|
95
|
+
return {
|
|
96
|
+
kind: 'unreadable',
|
|
97
|
+
code: code === 'ENOENT'
|
|
98
|
+
? ENVIRONMENT_CODES.ENVIRONMENT_FILE_NOT_FOUND
|
|
99
|
+
: ENVIRONMENT_CODES.ENVIRONMENT_FILE_UNREADABLE,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const opened = await handle.stat({ bigint: true });
|
|
104
|
+
if (!opened.isFile())
|
|
105
|
+
return { kind: 'unreadable', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_UNREADABLE };
|
|
106
|
+
if (opened.size > BigInt(maxBytes)) {
|
|
107
|
+
return { kind: 'unreadable', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_TOO_LARGE };
|
|
108
|
+
}
|
|
109
|
+
const canonicalPath = await realpath(path);
|
|
110
|
+
const named = await lstat(path, { bigint: true });
|
|
111
|
+
if (named.isSymbolicLink() ||
|
|
112
|
+
named.dev !== opened.dev ||
|
|
113
|
+
named.ino !== opened.ino ||
|
|
114
|
+
!isContained(root, canonicalPath)) {
|
|
115
|
+
return { kind: 'unreadable', code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT };
|
|
116
|
+
}
|
|
117
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
118
|
+
while (true) {
|
|
119
|
+
const { bytesRead: count } = await handle.read(buffer, 0, buffer.byteLength, null);
|
|
120
|
+
if (count === 0)
|
|
121
|
+
break;
|
|
122
|
+
bytesRead += count;
|
|
123
|
+
if (bytesRead > maxBytes) {
|
|
124
|
+
return { kind: 'unreadable', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_TOO_LARGE };
|
|
125
|
+
}
|
|
126
|
+
for (let index = 0; index < count; index += 1) {
|
|
127
|
+
const byte = buffer[index];
|
|
128
|
+
if (byte === 0x0a) {
|
|
129
|
+
const failure = finishLine();
|
|
130
|
+
if (failure)
|
|
131
|
+
return failure;
|
|
132
|
+
}
|
|
133
|
+
else if (!assignment && !ignoredLine && byte === 0x3d) {
|
|
134
|
+
if (leftHandSide.trim().startsWith('#'))
|
|
135
|
+
ignoredLine = true;
|
|
136
|
+
else
|
|
137
|
+
assignment = true;
|
|
138
|
+
}
|
|
139
|
+
else if (!assignment && !ignoredLine) {
|
|
140
|
+
leftHandSide += String.fromCharCode(byte);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// The unreadable result carries a code, not a cause, because read errors echo environment-file paths.
|
|
147
|
+
return { kind: 'unreadable', code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_UNREADABLE };
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await handle.close();
|
|
151
|
+
}
|
|
152
|
+
if (leftHandSide !== '' || assignment || ignoredLine) {
|
|
153
|
+
const failure = finishLine();
|
|
154
|
+
if (failure)
|
|
155
|
+
return failure;
|
|
156
|
+
}
|
|
157
|
+
return { kind: 'keys', keys: [...keys].sort() };
|
|
158
|
+
}
|
|
159
|
+
export async function inspectEnvironmentKeys(input) {
|
|
160
|
+
const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
161
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
162
|
+
return {
|
|
163
|
+
kind: 'unreadable',
|
|
164
|
+
file: 'template',
|
|
165
|
+
path: input.templatePath,
|
|
166
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_TOO_LARGE,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
let canonicalRoot;
|
|
170
|
+
try {
|
|
171
|
+
const root = await lstat(input.siblingRoot, { bigint: true });
|
|
172
|
+
if (root.isSymbolicLink() || !root.isDirectory())
|
|
173
|
+
throw new RefusalError('invalid root');
|
|
174
|
+
canonicalRoot = await realpath(input.siblingRoot);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// The unreadable result carries the template-path code, not a cause.
|
|
178
|
+
return {
|
|
179
|
+
kind: 'unreadable',
|
|
180
|
+
file: 'template',
|
|
181
|
+
path: input.templatePath,
|
|
182
|
+
code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const template = await projectKeys(canonicalRoot, input.templatePath, maxBytes);
|
|
186
|
+
if (template.kind !== 'keys') {
|
|
187
|
+
return template.kind === 'malformed'
|
|
188
|
+
? {
|
|
189
|
+
kind: 'malformed',
|
|
190
|
+
file: 'template',
|
|
191
|
+
path: input.templatePath,
|
|
192
|
+
line: template.line,
|
|
193
|
+
code: template.code,
|
|
194
|
+
}
|
|
195
|
+
: { kind: 'unreadable', file: 'template', path: input.templatePath, code: template.code };
|
|
196
|
+
}
|
|
197
|
+
const instance = await projectKeys(canonicalRoot, input.instancePath, maxBytes);
|
|
198
|
+
if (instance.kind === 'unreadable' && instance.code === ENVIRONMENT_CODES.ENVIRONMENT_FILE_NOT_FOUND) {
|
|
199
|
+
return {
|
|
200
|
+
kind: 'absent',
|
|
201
|
+
templatePath: input.templatePath,
|
|
202
|
+
instancePath: input.instancePath,
|
|
203
|
+
requiredKeys: template.keys,
|
|
204
|
+
presentKeys: [],
|
|
205
|
+
missingKeys: template.keys,
|
|
206
|
+
unexpectedKeys: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (instance.kind !== 'keys') {
|
|
210
|
+
return instance.kind === 'malformed'
|
|
211
|
+
? {
|
|
212
|
+
kind: 'malformed',
|
|
213
|
+
file: 'instance',
|
|
214
|
+
path: input.instancePath,
|
|
215
|
+
line: instance.line,
|
|
216
|
+
code: instance.code,
|
|
217
|
+
}
|
|
218
|
+
: { kind: 'unreadable', file: 'instance', path: input.instancePath, code: instance.code };
|
|
219
|
+
}
|
|
220
|
+
const present = new Set(instance.keys);
|
|
221
|
+
const required = new Set(template.keys);
|
|
222
|
+
const missingKeys = template.keys.filter((key) => !present.has(key));
|
|
223
|
+
const unexpectedKeys = instance.keys.filter((key) => !required.has(key));
|
|
224
|
+
return {
|
|
225
|
+
kind: missingKeys.length === 0 && unexpectedKeys.length === 0 ? 'fresh' : 'stale',
|
|
226
|
+
templatePath: input.templatePath,
|
|
227
|
+
instancePath: input.instancePath,
|
|
228
|
+
requiredKeys: template.keys,
|
|
229
|
+
presentKeys: instance.keys,
|
|
230
|
+
missingKeys,
|
|
231
|
+
unexpectedKeys,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export async function observeEnvironmentKeys(input) {
|
|
235
|
+
const snapshot = snapshotJson(input);
|
|
236
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot))
|
|
237
|
+
throw new RefusalError('environment-key observation input is malformed');
|
|
238
|
+
const owned = snapshot;
|
|
239
|
+
const declaration = EnvironmentFile.safeParse(owned.declaration);
|
|
240
|
+
if (!declaration.success || typeof owned.stackId !== 'string' || !/^stack:[a-f0-9]{64}$/.test(owned.stackId)
|
|
241
|
+
|| typeof owned.observedAt !== 'string' || !isCanonicalTimestamp(owned.observedAt) || typeof owned.siblingRoot !== 'string') {
|
|
242
|
+
throw new RefusalError('environment-key observation input is malformed');
|
|
243
|
+
}
|
|
244
|
+
const inspection = await inspectEnvironmentKeys({
|
|
245
|
+
siblingRoot: owned.siblingRoot,
|
|
246
|
+
templatePath: resolve(owned.siblingRoot, declaration.data.template),
|
|
247
|
+
instancePath: resolve(owned.siblingRoot, declaration.data.instance),
|
|
248
|
+
});
|
|
249
|
+
const source = inspection.kind === 'unreadable' || inspection.kind === 'malformed' ? inspection.path : inspection.instancePath;
|
|
250
|
+
const presence = inspection.kind !== 'absent' && !(inspection.kind === 'unreadable' && inspection.file === 'instance');
|
|
251
|
+
const fresh = inspection.kind === 'fresh';
|
|
252
|
+
return [
|
|
253
|
+
issueObservation({
|
|
254
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_KEYS_STALE, owner: OWNER, stackId: owned.stackId, source,
|
|
255
|
+
observed: inspection.kind === 'unreadable' || inspection.kind === 'malformed'
|
|
256
|
+
? { state: 'failed', reason: `${inspection.file} environment file could not be projected`, observedAt: owned.observedAt }
|
|
257
|
+
: { state: 'available', value: fresh, observedAt: owned.observedAt },
|
|
258
|
+
expectation: { kind: 'equal', value: true }, applicability: { state: 'required', posture: 'environment' }, freshness: { state: 'current' },
|
|
259
|
+
remedy: 'synchronize the instance key names with its tracked template',
|
|
260
|
+
}),
|
|
261
|
+
issueObservation({
|
|
262
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_NOT_FOUND, owner: OWNER, stackId: owned.stackId, source,
|
|
263
|
+
observed: { state: 'available', value: presence, observedAt: owned.observedAt }, expectation: { kind: 'equal', value: true },
|
|
264
|
+
applicability: { state: 'required', posture: 'environment' }, freshness: { state: 'current' }, remedy: 'seed the missing environment instance from its tracked template',
|
|
265
|
+
}),
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
/** Health admits one row per code; aggregate here so every declaration retains owner-issued evidence. */
|
|
269
|
+
export async function observeTemplateHealth(input) {
|
|
270
|
+
const groups = new Map();
|
|
271
|
+
let environmentFileCount = 0;
|
|
272
|
+
const add = (observation) => {
|
|
273
|
+
const group = groups.get(observation.code) ?? [];
|
|
274
|
+
group.push(observation);
|
|
275
|
+
groups.set(observation.code, group);
|
|
276
|
+
};
|
|
277
|
+
for (const value of input.siblings) {
|
|
278
|
+
const sibling = Sibling.parse(value);
|
|
279
|
+
add(await observeSiblingPresence({ stackId: input.stackId, observedAt: input.observedAt,
|
|
280
|
+
siblingRoot: input.siblingRoot, sibling }));
|
|
281
|
+
for (const declaration of sibling.environment_files ?? []) {
|
|
282
|
+
environmentFileCount += 1;
|
|
283
|
+
const observations = await observeEnvironmentKeys({ stackId: input.stackId, observedAt: input.observedAt,
|
|
284
|
+
siblingRoot: resolve(input.siblingRoot, sibling.dir), declaration });
|
|
285
|
+
observations.forEach(add);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (environmentFileCount === 0) {
|
|
289
|
+
add(issueObservation({
|
|
290
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_KEYS_STALE, owner: OWNER, stackId: input.stackId,
|
|
291
|
+
source: input.siblingRoot, observed: { state: 'available', value: true, observedAt: input.observedAt },
|
|
292
|
+
expectation: { kind: 'equal', value: true }, applicability: { state: 'required', posture: 'environment' },
|
|
293
|
+
freshness: { state: 'current' }, remedy: 'synchronize the instance key names with its tracked template',
|
|
294
|
+
}));
|
|
295
|
+
add(issueObservation({
|
|
296
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_FILE_NOT_FOUND, owner: OWNER, stackId: input.stackId,
|
|
297
|
+
source: input.siblingRoot, observed: { state: 'available', value: true, observedAt: input.observedAt },
|
|
298
|
+
expectation: { kind: 'equal', value: true }, applicability: { state: 'required', posture: 'environment' },
|
|
299
|
+
freshness: { state: 'current' }, remedy: 'seed the missing environment instance from its tracked template',
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
return [...groups.values()].map(group => {
|
|
303
|
+
if (group.length === 1)
|
|
304
|
+
return group[0];
|
|
305
|
+
const failures = group.filter(row => row.observed.state !== 'available' || row.observed.value !== true);
|
|
306
|
+
const unreadable = failures.filter(row => row.observed.state !== 'available');
|
|
307
|
+
const bounded = (value) => Buffer.byteLength(value) <= OBSERVATION_REASON_BYTE_LIMIT ? value
|
|
308
|
+
: `${Buffer.from(value).subarray(0, OBSERVATION_REASON_BYTE_LIMIT - 3).toString('utf8').replace(/�$/, '')}...`;
|
|
309
|
+
return issueObservation({
|
|
310
|
+
...group[0],
|
|
311
|
+
source: bounded((failures.length > 0 ? failures : group).map(row => row.source).join('; ')),
|
|
312
|
+
observed: unreadable.length > 0
|
|
313
|
+
? { state: 'failed', reason: bounded(unreadable.map(row => `${row.source}: ${row.observed.state === 'available' ? '' : row.observed.reason}`).join('; ')), observedAt: input.observedAt }
|
|
314
|
+
: { state: 'available', value: failures.length === 0, observedAt: input.observedAt },
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function identityOf(stat) {
|
|
319
|
+
return {
|
|
320
|
+
dev: stat.dev.toString(),
|
|
321
|
+
ino: stat.ino.toString(),
|
|
322
|
+
mode: stat.mode.toString(),
|
|
323
|
+
size: stat.size.toString(),
|
|
324
|
+
mtimeNs: stat.mtimeNs.toString(),
|
|
325
|
+
ctimeNs: stat.ctimeNs.toString(),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function identitiesMatch(left, right) {
|
|
329
|
+
return (left.dev === right.dev &&
|
|
330
|
+
left.ino === right.ino &&
|
|
331
|
+
left.mode === right.mode &&
|
|
332
|
+
left.size === right.size &&
|
|
333
|
+
left.mtimeNs === right.mtimeNs &&
|
|
334
|
+
left.ctimeNs === right.ctimeNs);
|
|
335
|
+
}
|
|
336
|
+
function sameNode(left, right) {
|
|
337
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;
|
|
338
|
+
}
|
|
339
|
+
function isContained(root, candidate) {
|
|
340
|
+
const path = relative(root, candidate);
|
|
341
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
|
|
342
|
+
}
|
|
343
|
+
async function secureIdentity(path) {
|
|
344
|
+
const stat = await lstat(path, { bigint: true });
|
|
345
|
+
return {
|
|
346
|
+
identity: identityOf(stat),
|
|
347
|
+
directory: stat.isDirectory(),
|
|
348
|
+
file: stat.isFile(),
|
|
349
|
+
symbolicLink: stat.isSymbolicLink(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async function hasSafeComponents(root, relativePath, includeLeaf) {
|
|
353
|
+
const parts = relativePath.split('/');
|
|
354
|
+
const limit = includeLeaf ? parts.length : parts.length - 1;
|
|
355
|
+
let current = root;
|
|
356
|
+
for (let index = 0; index < limit; index += 1) {
|
|
357
|
+
current = join(current, parts[index]);
|
|
358
|
+
try {
|
|
359
|
+
const component = await secureIdentity(current);
|
|
360
|
+
if (component.symbolicLink || (index < limit - 1 && !component.directory))
|
|
361
|
+
return false;
|
|
362
|
+
const canonical = await realpath(current);
|
|
363
|
+
if (!isContained(root, canonical))
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Any filesystem failure on a component means it is not safely contained; the caller refuses the path.
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
function refusal(code, path) {
|
|
374
|
+
return { kind: 'refusal', code, path };
|
|
375
|
+
}
|
|
376
|
+
export async function planEnvironmentSeed(input) {
|
|
377
|
+
if (input.environmentFile === undefined)
|
|
378
|
+
return { kind: 'none' };
|
|
379
|
+
const declaration = EnvironmentFile.safeParse(input.environmentFile);
|
|
380
|
+
if (!declaration.success)
|
|
381
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, input.siblingRoot);
|
|
382
|
+
let root;
|
|
383
|
+
let canonicalRoot;
|
|
384
|
+
try {
|
|
385
|
+
root = await secureIdentity(input.siblingRoot);
|
|
386
|
+
canonicalRoot = await realpath(input.siblingRoot);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// The refusal carries a code and path, not a cause.
|
|
390
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, input.siblingRoot);
|
|
391
|
+
}
|
|
392
|
+
if (root.symbolicLink || !root.directory) {
|
|
393
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, input.siblingRoot);
|
|
394
|
+
}
|
|
395
|
+
if (typeof process.getuid === 'function') {
|
|
396
|
+
const rootStat = await lstat(input.siblingRoot, { bigint: true });
|
|
397
|
+
if (rootStat.uid !== BigInt(process.getuid())) {
|
|
398
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, input.siblingRoot);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const source = resolve(canonicalRoot, declaration.data.template);
|
|
402
|
+
const destination = resolve(canonicalRoot, declaration.data.instance);
|
|
403
|
+
if (!isContained(canonicalRoot, source) || !isContained(canonicalRoot, destination) || source === destination) {
|
|
404
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
405
|
+
}
|
|
406
|
+
if (!(await hasSafeComponents(canonicalRoot, declaration.data.template, true))) {
|
|
407
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, source);
|
|
408
|
+
}
|
|
409
|
+
if (!(await hasSafeComponents(canonicalRoot, declaration.data.instance, false))) {
|
|
410
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
411
|
+
}
|
|
412
|
+
let sourceState;
|
|
413
|
+
let parentState;
|
|
414
|
+
try {
|
|
415
|
+
sourceState = await secureIdentity(source);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// The refusal carries a code and path, not a cause.
|
|
419
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_NOT_FOUND, source);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
parentState = await secureIdentity(dirname(destination));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// The refusal carries a code and path, not a cause.
|
|
426
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
427
|
+
}
|
|
428
|
+
if (sourceState.symbolicLink || !sourceState.file || parentState.symbolicLink || !parentState.directory) {
|
|
429
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, source);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
if (!isContained(canonicalRoot, await realpath(source)) ||
|
|
433
|
+
!isContained(canonicalRoot, await realpath(dirname(destination)))) {
|
|
434
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, source);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// The refusal carries a code and path, not a cause.
|
|
439
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, source);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const destinationState = await secureIdentity(destination);
|
|
443
|
+
if (destinationState.symbolicLink || !destinationState.file) {
|
|
444
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
445
|
+
}
|
|
446
|
+
if (destinationState.identity.dev === sourceState.identity.dev &&
|
|
447
|
+
destinationState.identity.ino === sourceState.identity.ino) {
|
|
448
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
449
|
+
}
|
|
450
|
+
return { kind: 'unchanged', siblingRoot: input.siblingRoot, destination };
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (!hasErrorCode(error, 'ENOENT')) {
|
|
454
|
+
return refusal(ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT, destination);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
kind: 'ready',
|
|
459
|
+
siblingRoot: input.siblingRoot,
|
|
460
|
+
canonicalRoot,
|
|
461
|
+
source,
|
|
462
|
+
destination,
|
|
463
|
+
rootIdentity: root.identity,
|
|
464
|
+
sourceIdentity: sourceState.identity,
|
|
465
|
+
destinationParentIdentity: parentState.identity,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function revalidatePlan(plan) {
|
|
469
|
+
try {
|
|
470
|
+
const rootStat = lstatSync(plan.canonicalRoot, { bigint: true });
|
|
471
|
+
const sourceStat = lstatSync(plan.source, { bigint: true });
|
|
472
|
+
const parentStat = lstatSync(dirname(plan.destination), { bigint: true });
|
|
473
|
+
const root = { identity: identityOf(rootStat), directory: rootStat.isDirectory(), symbolicLink: rootStat.isSymbolicLink() };
|
|
474
|
+
const source = { identity: identityOf(sourceStat), file: sourceStat.isFile(), symbolicLink: sourceStat.isSymbolicLink() };
|
|
475
|
+
const parent = { identity: identityOf(parentStat), directory: parentStat.isDirectory(), symbolicLink: parentStat.isSymbolicLink() };
|
|
476
|
+
const structurallySafe = !root.symbolicLink &&
|
|
477
|
+
root.directory &&
|
|
478
|
+
!source.symbolicLink &&
|
|
479
|
+
source.file &&
|
|
480
|
+
!parent.symbolicLink &&
|
|
481
|
+
parent.directory &&
|
|
482
|
+
sameNode(root.identity, plan.rootIdentity) &&
|
|
483
|
+
sameNode(parent.identity, plan.destinationParentIdentity) &&
|
|
484
|
+
isContained(plan.canonicalRoot, realpathSync(plan.source)) &&
|
|
485
|
+
isContained(plan.canonicalRoot, realpathSync(dirname(plan.destination)));
|
|
486
|
+
if (!structurallySafe)
|
|
487
|
+
return ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT;
|
|
488
|
+
// A regular template replaced inside the root is a changed precondition like an in-place
|
|
489
|
+
// rewrite, whatever inode it lands on: ext4 often reuses the one just freed, APFS does not.
|
|
490
|
+
return identitiesMatch(source.identity, plan.sourceIdentity)
|
|
491
|
+
? undefined
|
|
492
|
+
: ENVIRONMENT_CODES.ENVIRONMENT_SEED_PRECONDITION_CHANGED;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Any filesystem failure while rechecking means the paths are no longer provably inside the root.
|
|
496
|
+
return ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function seedFromPinnedDirectory(plan) {
|
|
500
|
+
const destinationDirectory = dirname(plan.destination);
|
|
501
|
+
const destinationName = basename(plan.destination);
|
|
502
|
+
const temporaryName = `.${destinationName}.cockpit-seed-${randomUUID()}`;
|
|
503
|
+
const originalDirectory = process.cwd();
|
|
504
|
+
let sourceDescriptor;
|
|
505
|
+
let temporaryDescriptor;
|
|
506
|
+
let temporaryCreated = false;
|
|
507
|
+
let result = {
|
|
508
|
+
kind: 'refusal',
|
|
509
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_FAILED,
|
|
510
|
+
path: plan.destination,
|
|
511
|
+
};
|
|
512
|
+
try {
|
|
513
|
+
process.chdir(destinationDirectory);
|
|
514
|
+
const pinnedDirectory = realpathSync('.');
|
|
515
|
+
const declaredDirectory = realpathSync(destinationDirectory);
|
|
516
|
+
const pinnedState = lstatSync('.', { bigint: true });
|
|
517
|
+
if (pinnedDirectory !== declaredDirectory ||
|
|
518
|
+
!isContained(plan.canonicalRoot, pinnedDirectory) ||
|
|
519
|
+
!pinnedState.isDirectory() ||
|
|
520
|
+
!sameNode(identityOf(pinnedState), plan.destinationParentIdentity)) {
|
|
521
|
+
result = {
|
|
522
|
+
kind: 'refusal',
|
|
523
|
+
code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT,
|
|
524
|
+
path: plan.destination,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
sourceDescriptor = openSync(plan.source, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
529
|
+
const sourceBefore = identityOf(fstatSync(sourceDescriptor, { bigint: true }));
|
|
530
|
+
const namedSource = lstatSync(plan.source, { bigint: true });
|
|
531
|
+
if (namedSource.isSymbolicLink() ||
|
|
532
|
+
!namedSource.isFile() ||
|
|
533
|
+
!identitiesMatch(sourceBefore, plan.sourceIdentity) ||
|
|
534
|
+
namedSource.dev !== BigInt(sourceBefore.dev) ||
|
|
535
|
+
namedSource.ino !== BigInt(sourceBefore.ino) ||
|
|
536
|
+
!isContained(plan.canonicalRoot, realpathSync(plan.source))) {
|
|
537
|
+
result = {
|
|
538
|
+
kind: 'refusal',
|
|
539
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_PRECONDITION_CHANGED,
|
|
540
|
+
path: plan.source,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
temporaryDescriptor = openSync(temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
545
|
+
temporaryCreated = true;
|
|
546
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
547
|
+
while (true) {
|
|
548
|
+
const count = readSync(sourceDescriptor, buffer, 0, buffer.byteLength, null);
|
|
549
|
+
if (count === 0)
|
|
550
|
+
break;
|
|
551
|
+
let offset = 0;
|
|
552
|
+
while (offset < count)
|
|
553
|
+
offset += writeSync(temporaryDescriptor, buffer, offset, count - offset);
|
|
554
|
+
}
|
|
555
|
+
fchmodSync(temporaryDescriptor, 0o600);
|
|
556
|
+
fsyncSync(temporaryDescriptor);
|
|
557
|
+
if (!identitiesMatch(identityOf(fstatSync(sourceDescriptor, { bigint: true })), plan.sourceIdentity)) {
|
|
558
|
+
result = {
|
|
559
|
+
kind: 'refusal',
|
|
560
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_PRECONDITION_CHANGED,
|
|
561
|
+
path: plan.source,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
else if (realpathSync(destinationDirectory) !== pinnedDirectory ||
|
|
565
|
+
!sameNode(identityOf(lstatSync('.', { bigint: true })), plan.destinationParentIdentity)) {
|
|
566
|
+
result = {
|
|
567
|
+
kind: 'refusal',
|
|
568
|
+
code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT,
|
|
569
|
+
path: plan.destination,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
try {
|
|
574
|
+
linkSync(temporaryName, destinationName);
|
|
575
|
+
const temporaryState = lstatSync(temporaryName, { bigint: true });
|
|
576
|
+
const destinationState = lstatSync(destinationName, { bigint: true });
|
|
577
|
+
result =
|
|
578
|
+
destinationState.isFile() &&
|
|
579
|
+
destinationState.dev === temporaryState.dev &&
|
|
580
|
+
destinationState.ino === temporaryState.ino
|
|
581
|
+
? { kind: 'created', siblingRoot: plan.siblingRoot, destination: plan.destination }
|
|
582
|
+
: {
|
|
583
|
+
kind: 'refusal',
|
|
584
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_FAILED,
|
|
585
|
+
path: plan.destination,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
if (!hasErrorCode(error, 'EEXIST')) {
|
|
590
|
+
result = {
|
|
591
|
+
kind: 'refusal',
|
|
592
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_FAILED,
|
|
593
|
+
path: plan.destination,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
let destinationDescriptor;
|
|
598
|
+
try {
|
|
599
|
+
const namedDestination = lstatSync(destinationName, { bigint: true });
|
|
600
|
+
destinationDescriptor = openSync(destinationName, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
601
|
+
const openedDestination = fstatSync(destinationDescriptor, { bigint: true });
|
|
602
|
+
const sameDestination = namedDestination.dev === openedDestination.dev &&
|
|
603
|
+
namedDestination.ino === openedDestination.ino;
|
|
604
|
+
const aliasesSource = openedDestination.dev === BigInt(sourceBefore.dev) &&
|
|
605
|
+
openedDestination.ino === BigInt(sourceBefore.ino);
|
|
606
|
+
result =
|
|
607
|
+
namedDestination.isFile() &&
|
|
608
|
+
openedDestination.isFile() &&
|
|
609
|
+
sameDestination &&
|
|
610
|
+
!aliasesSource
|
|
611
|
+
? {
|
|
612
|
+
kind: 'unchanged',
|
|
613
|
+
siblingRoot: plan.siblingRoot,
|
|
614
|
+
destination: plan.destination,
|
|
615
|
+
}
|
|
616
|
+
: {
|
|
617
|
+
kind: 'refusal',
|
|
618
|
+
code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT,
|
|
619
|
+
path: plan.destination,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
// The refusal carries a code and path, not a cause.
|
|
624
|
+
result = {
|
|
625
|
+
kind: 'refusal',
|
|
626
|
+
code: ENVIRONMENT_CODES.TEMPLATE_PATH_OUTSIDE_ROOT,
|
|
627
|
+
path: plan.destination,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
if (destinationDescriptor !== undefined)
|
|
632
|
+
closeSync(destinationDescriptor);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// The refusal carries a code and path, not a cause.
|
|
642
|
+
result = {
|
|
643
|
+
kind: 'refusal',
|
|
644
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_FAILED,
|
|
645
|
+
path: plan.destination,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
if (temporaryDescriptor !== undefined)
|
|
650
|
+
closeSync(temporaryDescriptor);
|
|
651
|
+
if (sourceDescriptor !== undefined)
|
|
652
|
+
closeSync(sourceDescriptor);
|
|
653
|
+
if (temporaryCreated) {
|
|
654
|
+
try {
|
|
655
|
+
unlinkSync(temporaryName);
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
if (!hasErrorCode(error, 'ENOENT')) {
|
|
659
|
+
result = {
|
|
660
|
+
kind: 'refusal',
|
|
661
|
+
code: ENVIRONMENT_CODES.ENVIRONMENT_SEED_FAILED,
|
|
662
|
+
path: plan.destination,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
process.chdir(originalDirectory);
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
export function seedEnvironmentInstanceSync(plan) {
|
|
672
|
+
if (plan.kind !== 'ready')
|
|
673
|
+
return plan;
|
|
674
|
+
const invalidPlan = revalidatePlan(plan);
|
|
675
|
+
if (invalidPlan !== undefined) {
|
|
676
|
+
return {
|
|
677
|
+
kind: 'refusal',
|
|
678
|
+
code: invalidPlan,
|
|
679
|
+
path: invalidPlan === ENVIRONMENT_CODES.ENVIRONMENT_SEED_PRECONDITION_CHANGED
|
|
680
|
+
? plan.source
|
|
681
|
+
: plan.destination,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return seedFromPinnedDirectory(plan);
|
|
685
|
+
}
|