@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +375 -0
- package/LICENSE +21 -0
- package/README.md +21 -1
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2574 -0
- package/dist/autonomy.d.ts +44 -0
- package/dist/autonomy.js +156 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +62 -0
- package/dist/browser/commands.js +349 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +117 -0
- package/dist/browser/runtime.js +183 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +556 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +139 -0
- package/dist/cli-main.js +1405 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +113 -0
- package/dist/command-table.js +512 -0
- package/dist/config/load.d.ts +171 -0
- package/dist/config/load.js +345 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +335 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +425 -0
- package/dist/environment/application/doctor.d.ts +89 -0
- package/dist/environment/application/doctor.js +235 -0
- package/dist/environment/application/guard.d.ts +43 -0
- package/dist/environment/application/guard.js +622 -0
- package/dist/environment/application/health.d.ts +134 -0
- package/dist/environment/application/health.js +379 -0
- package/dist/environment/application/lifecycle-context.d.ts +228 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +201 -0
- package/dist/environment/application/lifecycle.d.ts +17 -0
- package/dist/environment/application/lifecycle.js +729 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1709 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +117 -0
- package/dist/environment/compose-topology.js +413 -0
- package/dist/environment/contract.d.ts +489 -0
- package/dist/environment/contract.js +761 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +26 -0
- package/dist/environment/executable.js +167 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +189 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +276 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +484 -0
- package/dist/environment/generated-application/shared.js +349 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +389 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +699 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +109 -0
- package/dist/environment/git-observation.d.ts +183 -0
- package/dist/environment/git-observation.js +544 -0
- package/dist/environment/health-composition.d.ts +11 -0
- package/dist/environment/health-composition.js +628 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +308 -0
- package/dist/environment/init-application.js +697 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +29 -0
- package/dist/environment/lifecycle-composition.js +675 -0
- package/dist/environment/lifecycle-stage.d.ts +115 -0
- package/dist/environment/lifecycle-stage.js +398 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +344 -0
- package/dist/environment/operator-application.d.ts +50 -0
- package/dist/environment/operator-application.js +155 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/read-composition.d.ts +27 -0
- package/dist/environment/read-composition.js +352 -0
- package/dist/environment/readiness-composition.d.ts +38 -0
- package/dist/environment/readiness-composition.js +130 -0
- package/dist/environment/reclaim.d.ts +333 -0
- package/dist/environment/reclaim.js +1835 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +505 -0
- package/dist/environment/runtime.js +2335 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/state.d.ts +371 -0
- package/dist/environment/state.js +2013 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +96 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +102 -0
- package/dist/environment-exec/execute.js +1175 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +275 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/evaluator.d.ts +113 -0
- package/dist/guards/evaluator.js +1226 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +40 -0
- package/dist/guards/local-identity.js +123 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +175 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +40 -0
- package/dist/init/command.js +310 -0
- package/dist/init/derive.d.ts +51 -0
- package/dist/init/derive.js +715 -0
- package/dist/init/draft.d.ts +138 -0
- package/dist/init/draft.js +1205 -0
- package/dist/init/render.d.ts +35 -0
- package/dist/init/render.js +82 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1322 -0
- package/dist/schema.js +934 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +180 -0
- package/dist/surfaces/harness-registry.js +117 -0
- package/dist/surfaces/mcp.d.ts +95 -0
- package/dist/surfaces/mcp.js +186 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +681 -0
- package/dist/surfaces/render.d.ts +118 -0
- package/dist/surfaces/render.js +633 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +512 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/work-orders/application.d.ts +99 -0
- package/dist/work-orders/application.js +383 -0
- package/dist/work-orders/composition.d.ts +124 -0
- package/dist/work-orders/composition.js +1010 -0
- package/dist/work-orders/contract.d.ts +61 -0
- package/dist/work-orders/contract.js +122 -0
- package/dist/work-orders/store.d.ts +158 -0
- package/dist/work-orders/store.js +197 -0
- package/docs/README.md +65 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +50 -0
- package/docs/cli-reference.md +599 -0
- package/docs/command-list.md +51 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +171 -0
- package/docs/configuration-essentials.md +172 -0
- package/docs/configuration-reference.md +621 -0
- package/docs/environment.md +760 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +111 -0
- package/docs/getting-started.md +608 -0
- package/docs/harness-surfaces.md +433 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +419 -0
- package/docs/troubleshooting.md +361 -0
- package/docs/tutorial.md +170 -0
- package/docs/work-and-browser.md +254 -0
- package/libexec/browser-operation.cjs +186 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +76 -1
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +54 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +61 -0
- package/surfaces/skills/procedures/fix.md +81 -0
- package/surfaces/skills/procedures/setup.md +379 -0
- package/surfaces/skills/procedures/work.md +105 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Anchored filesystem operations for packages/cockpit/src/generated-environment/transaction.ts:
|
|
4
|
+
// authenticated reads, adoption relocation, and the staged publication transaction (prepare,
|
|
5
|
+
// validate, publish, verify, commit, rollback). Runs from a staged snapshot, so it must stay
|
|
6
|
+
// self-contained.
|
|
7
|
+
//
|
|
8
|
+
// It is a separate process because Node has no openat(): the only way to pin a directory while
|
|
9
|
+
// operating inside it is to make it the working directory. Every walk moves the cwd one segment at
|
|
10
|
+
// a time and re-checks the entered directory's dev/ino, so a directory swapped for a symlink or
|
|
11
|
+
// another inode mid-walk is refused instead of followed, and every later operation is relative to
|
|
12
|
+
// the pinned directory. Changing the cwd of the cockpit process itself would redirect every
|
|
13
|
+
// concurrent relative path in it, and worker threads cannot change it at all.
|
|
14
|
+
//
|
|
15
|
+
// argv: <request JSON> [staged input paths...]. The request carries `version`, `nonce`, `action`,
|
|
16
|
+
// the expected `identity` of the starting cwd, the bounds in `limits`, and the action's fields.
|
|
17
|
+
// stdout: one JSON line { version: 1, nonce, action, kind: 'complete' | 'missing' | ..., ... }
|
|
18
|
+
// or { ..., kind: 'refusal', reason }. Any operation that cannot be proved exact is a refusal.
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs')
|
|
21
|
+
const crypto = require('node:crypto')
|
|
22
|
+
const path = require('node:path')
|
|
23
|
+
|
|
24
|
+
const c = fs.constants
|
|
25
|
+
const request = JSON.parse(process.argv[2])
|
|
26
|
+
const INPUT_ARGUMENT_OFFSET = 3
|
|
27
|
+
let limits
|
|
28
|
+
|
|
29
|
+
const parseLimits = value => {
|
|
30
|
+
const keys = ['fileBytes', 'frameBytes', 'readBatchEntries', 'readBatchBytes', 'readBatchReceiptBytes', 'publicationGroups']
|
|
31
|
+
if (!value || typeof value !== 'object' || Object.keys(value).length !== keys.length
|
|
32
|
+
|| !keys.every(key => Number.isSafeInteger(value[key]) && value[key] > 0)) throw new Error('limits')
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const anchoredName = value => {
|
|
37
|
+
if (typeof value !== 'string' || value.length === 0 || value === '.' || value === '..' || value.includes('/')
|
|
38
|
+
|| value.includes('\\') || value.includes('\0')) throw new Error('name')
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
const name = value => {
|
|
42
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 255 || Buffer.byteLength(value, 'utf8') > 255
|
|
43
|
+
|| /[<>:"/\\|?*\x00-\x1f\x7f]/.test(value) || /[. ]$/.test(value)
|
|
44
|
+
|| /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value)) throw new Error('nonportable-name')
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
const safeSegments = (value, validate = name) => {
|
|
48
|
+
if (!Array.isArray(value)) throw new Error('segments')
|
|
49
|
+
return value.map(validate)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const same = (actual, expected) => actual.isDirectory() && !actual.isSymbolicLink() && actual.dev === expected.dev && actual.ino === expected.ino
|
|
53
|
+
const identity = () => {
|
|
54
|
+
const value = fs.lstatSync('.')
|
|
55
|
+
if (!same(value, request.identity)) throw new Error('identity')
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
const currentDirectory = () => {
|
|
59
|
+
const value = fs.lstatSync('.')
|
|
60
|
+
if (!value.isDirectory() || value.isSymbolicLink()) throw new Error('directory')
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
const syncDirectory = () => {
|
|
64
|
+
const fd = fs.openSync('.', c.O_RDONLY)
|
|
65
|
+
try { fs.fsyncSync(fd) } finally { fs.closeSync(fd) }
|
|
66
|
+
}
|
|
67
|
+
const digest = value => crypto.createHash('sha256').update(value).digest('hex')
|
|
68
|
+
const readLimit = value => {
|
|
69
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > limits.fileBytes) throw new Error('limit')
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
const writeExclusive = (target, bytes) => {
|
|
73
|
+
const fd = fs.openSync(target, c.O_WRONLY | c.O_CREAT | c.O_EXCL | c.O_NOFOLLOW, 0o600)
|
|
74
|
+
try { fs.writeFileSync(fd, bytes) } finally { fs.closeSync(fd) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const read = (target, maxBytes = limits.fileBytes, expected) => {
|
|
78
|
+
maxBytes = readLimit(maxBytes)
|
|
79
|
+
if (c.O_NOFOLLOW === undefined) throw new Error('no-follow')
|
|
80
|
+
const fd = fs.openSync(target, c.O_RDONLY | c.O_NOFOLLOW | c.O_NONBLOCK)
|
|
81
|
+
try {
|
|
82
|
+
const status = fs.fstatSync(fd)
|
|
83
|
+
if (!status.isFile() || status.size > maxBytes || expected && (status.dev !== expected.dev || status.ino !== expected.ino)) throw new Error('file')
|
|
84
|
+
const chunks = []
|
|
85
|
+
let total = 0
|
|
86
|
+
const chunk = Buffer.allocUnsafe(65536)
|
|
87
|
+
while (true) {
|
|
88
|
+
const length = fs.readSync(fd, chunk, 0, Math.min(chunk.length, maxBytes - total + 1), null)
|
|
89
|
+
if (length === 0) break
|
|
90
|
+
total += length
|
|
91
|
+
if (total > maxBytes) throw new Error('limit')
|
|
92
|
+
chunks.push(Buffer.from(chunk.subarray(0, length)))
|
|
93
|
+
}
|
|
94
|
+
const after = fs.fstatSync(fd)
|
|
95
|
+
if (total !== status.size || after.dev !== status.dev || after.ino !== status.ino || after.size !== status.size
|
|
96
|
+
|| after.mtimeMs !== status.mtimeMs || after.ctimeMs !== status.ctimeMs) throw new Error('changed')
|
|
97
|
+
return { bytes: Buffer.concat(chunks, total), status }
|
|
98
|
+
} finally { fs.closeSync(fd) }
|
|
99
|
+
}
|
|
100
|
+
// Published files are committed, so they get the ordinary mode (after umask) a new file gets; 0600
|
|
101
|
+
// made every generated file unreadable to anything but its owner, containers included.
|
|
102
|
+
const copy = (source, target, expected) => {
|
|
103
|
+
const value = read(source)
|
|
104
|
+
if (digest(value.bytes) !== expected) throw new Error('digest')
|
|
105
|
+
const fd = fs.openSync(name(target), c.O_WRONLY | c.O_CREAT | c.O_EXCL | (c.O_NOFOLLOW || 0), 0o644)
|
|
106
|
+
try {
|
|
107
|
+
fs.writeFileSync(fd, value.bytes)
|
|
108
|
+
fs.fsyncSync(fd)
|
|
109
|
+
} finally { fs.closeSync(fd) }
|
|
110
|
+
}
|
|
111
|
+
const hash = (target, validate = name) => digest(read(validate(target)).bytes)
|
|
112
|
+
|
|
113
|
+
// Walks that change the cwd. Each pushes { name, dev, ino, created } rows describing the chain it
|
|
114
|
+
// entered, so a later operation can walk the same chain and prove it reached the same inodes.
|
|
115
|
+
const walk = (segments, chain = [], validate = name) => {
|
|
116
|
+
for (const raw of segments) {
|
|
117
|
+
const segment = validate(raw)
|
|
118
|
+
let created = false
|
|
119
|
+
let before
|
|
120
|
+
try { before = fs.lstatSync(segment) } catch (error) {
|
|
121
|
+
if (error.code !== 'ENOENT') throw error
|
|
122
|
+
fs.mkdirSync(segment, { mode: 0o700 })
|
|
123
|
+
syncDirectory()
|
|
124
|
+
created = true
|
|
125
|
+
before = fs.lstatSync(segment)
|
|
126
|
+
}
|
|
127
|
+
if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('directory')
|
|
128
|
+
process.chdir(segment)
|
|
129
|
+
const after = fs.lstatSync('.')
|
|
130
|
+
if (after.dev !== before.dev || after.ino !== before.ino || !after.isDirectory() || after.isSymbolicLink()) throw new Error('walk')
|
|
131
|
+
chain.push({ name: segment, dev: after.dev, ino: after.ino, created })
|
|
132
|
+
}
|
|
133
|
+
return chain
|
|
134
|
+
}
|
|
135
|
+
// Creates each missing segment, or enters one this batch already created (keyed by its path from
|
|
136
|
+
// the batch root in `created`). Any other existing directory is refused: it appeared after the
|
|
137
|
+
// caller observed the path as missing.
|
|
138
|
+
const walkCreating = (segments, chain, created, prefix) => {
|
|
139
|
+
for (const raw of segments) {
|
|
140
|
+
const segment = name(raw)
|
|
141
|
+
const key = [...prefix, ...chain.map(entry => entry.name), segment].join('/')
|
|
142
|
+
let before
|
|
143
|
+
try { before = fs.lstatSync(segment) } catch (error) {
|
|
144
|
+
if (error.code !== 'ENOENT') throw error
|
|
145
|
+
}
|
|
146
|
+
if (before) {
|
|
147
|
+
const issued = created.get(key)
|
|
148
|
+
if (!issued || !same(before, issued)) throw new Error('unexpected-directory')
|
|
149
|
+
chain.push({ name: segment, dev: before.dev, ino: before.ino, created: false })
|
|
150
|
+
} else {
|
|
151
|
+
fs.mkdirSync(segment, { mode: 0o700 })
|
|
152
|
+
before = fs.lstatSync(segment)
|
|
153
|
+
if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('created-directory')
|
|
154
|
+
created.set(key, { dev: before.dev, ino: before.ino })
|
|
155
|
+
chain.push({ name: segment, dev: before.dev, ino: before.ino, created: true })
|
|
156
|
+
syncDirectory()
|
|
157
|
+
}
|
|
158
|
+
process.chdir(segment)
|
|
159
|
+
if (!same(fs.lstatSync('.'), before)) throw new Error('created-walk')
|
|
160
|
+
}
|
|
161
|
+
return chain
|
|
162
|
+
}
|
|
163
|
+
const walkSource = (segments, chain = []) => {
|
|
164
|
+
for (const raw of segments) {
|
|
165
|
+
const segment = anchoredName(raw)
|
|
166
|
+
const before = fs.lstatSync(segment)
|
|
167
|
+
if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('directory')
|
|
168
|
+
process.chdir(segment)
|
|
169
|
+
const after = fs.lstatSync('.')
|
|
170
|
+
if (!same(after, before)) throw new Error('walk')
|
|
171
|
+
chain.push({ name: segment, dev: after.dev, ino: after.ino })
|
|
172
|
+
}
|
|
173
|
+
return chain
|
|
174
|
+
}
|
|
175
|
+
const walkExpected = (chain, validate = name) => {
|
|
176
|
+
const walked = []
|
|
177
|
+
for (const expected of chain) {
|
|
178
|
+
const segment = validate(expected.name)
|
|
179
|
+
const before = fs.lstatSync(segment)
|
|
180
|
+
if (!same(before, expected)) throw new Error('directory-identity')
|
|
181
|
+
process.chdir(segment)
|
|
182
|
+
const after = fs.lstatSync('.')
|
|
183
|
+
if (!same(after, expected)) throw new Error('walk-identity')
|
|
184
|
+
walked.push({ name: segment, dev: after.dev, ino: after.ino, created: false })
|
|
185
|
+
}
|
|
186
|
+
return walked
|
|
187
|
+
}
|
|
188
|
+
// Walks back up a chain to `start`, proving each parent and child are still the inodes walked down.
|
|
189
|
+
const verifySourceChain = (chain, start = request.identity) => {
|
|
190
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
191
|
+
process.chdir('..')
|
|
192
|
+
const parent = fs.lstatSync('.')
|
|
193
|
+
const expected = index === 0 ? start : chain[index - 1]
|
|
194
|
+
if (!same(parent, expected)) throw new Error('parent-identity')
|
|
195
|
+
const child = fs.lstatSync(anchoredName(chain[index].name))
|
|
196
|
+
if (!same(child, chain[index])) throw new Error('child-identity')
|
|
197
|
+
}
|
|
198
|
+
if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
|
|
199
|
+
}
|
|
200
|
+
const removeCreatedChain = (chain, start, validate = name) => {
|
|
201
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
202
|
+
process.chdir('..')
|
|
203
|
+
const parent = fs.lstatSync('.')
|
|
204
|
+
const expected = index === 0 ? start : chain[index - 1]
|
|
205
|
+
if (!same(parent, expected)) throw new Error('parent-identity')
|
|
206
|
+
const child = fs.lstatSync(validate(chain[index].name))
|
|
207
|
+
if (!same(child, chain[index])) throw new Error('child-identity')
|
|
208
|
+
if (chain[index].created) {
|
|
209
|
+
try {
|
|
210
|
+
fs.rmdirSync(validate(chain[index].name))
|
|
211
|
+
syncDirectory()
|
|
212
|
+
} catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
|
|
216
|
+
}
|
|
217
|
+
const removeCreatedDirectories = (entries, start) => {
|
|
218
|
+
const base = process.cwd()
|
|
219
|
+
const created = new Map()
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
const names = []
|
|
222
|
+
for (const [index, directory] of entry.chain.entries()) {
|
|
223
|
+
names.push(directory.name)
|
|
224
|
+
if (directory.created) created.set(names.join('/'), entry.chain.slice(0, index + 1))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const chains = [...created.values()].sort((left, right) => right.length - left.length)
|
|
228
|
+
for (const chain of chains) {
|
|
229
|
+
process.chdir(base)
|
|
230
|
+
const parents = walkExpected(chain.slice(0, -1))
|
|
231
|
+
const child = chain[chain.length - 1]
|
|
232
|
+
if (!same(fs.lstatSync(name(child.name)), child)) throw new Error('created-identity')
|
|
233
|
+
try {
|
|
234
|
+
fs.rmdirSync(name(child.name))
|
|
235
|
+
syncDirectory()
|
|
236
|
+
} catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
|
|
237
|
+
verifySourceChain(parents, start)
|
|
238
|
+
}
|
|
239
|
+
process.chdir(base)
|
|
240
|
+
if (!same(fs.lstatSync('.'), start)) throw new Error('start-identity')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const state = target => {
|
|
244
|
+
try {
|
|
245
|
+
const status = fs.lstatSync(target)
|
|
246
|
+
if (status.isSymbolicLink()) return { kind: 'symlink', target: fs.readlinkSync(target), identity: { dev: status.dev, ino: status.ino } }
|
|
247
|
+
if (status.isFile()) return { kind: 'file', sha256: hash(target), mode: status.mode & 511, identity: { dev: status.dev, ino: status.ino } }
|
|
248
|
+
return { kind: 'other' }
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error.code === 'ENOENT') return { kind: 'missing' }
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const matches = (actual, expected) => actual.kind === expected.kind
|
|
255
|
+
&& (actual.kind !== 'file' || actual.sha256 === expected.sha256)
|
|
256
|
+
&& (actual.kind !== 'file' || expected.mode === undefined || actual.mode === expected.mode)
|
|
257
|
+
&& (actual.kind !== 'symlink' || actual.target === expected.target)
|
|
258
|
+
&& (actual.kind === 'missing' || !expected.identity || actual.identity.dev === expected.identity.dev && actual.identity.ino === expected.identity.ino)
|
|
259
|
+
const priorState = entry => ({ kind: entry.priorKind, sha256: entry.priorSha256, target: entry.priorTarget, mode: entry.priorMode, identity: entry.priorIdentity })
|
|
260
|
+
const postState = entry => entry.operation === 'write-file' ? { kind: 'file', sha256: entry.sha256 }
|
|
261
|
+
: entry.operation === 'write-symlink' ? { kind: 'symlink', target: entry.target } : { kind: 'missing' }
|
|
262
|
+
|
|
263
|
+
const publishEntries = (entries, start = currentDirectory()) => {
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const chain = walkExpected(entry.chain)
|
|
266
|
+
name(entry.name)
|
|
267
|
+
if (!matches(state(entry.name), priorState(entry))) throw new Error('prior-state')
|
|
268
|
+
if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
|
|
269
|
+
if (entry.operation === 'write-file') {
|
|
270
|
+
name(entry.stage)
|
|
271
|
+
if (hash(entry.stage) !== entry.sha256) throw new Error('stage-digest')
|
|
272
|
+
fs.renameSync(entry.stage, entry.name)
|
|
273
|
+
} else if (entry.operation === 'write-symlink') {
|
|
274
|
+
name(entry.stage)
|
|
275
|
+
if (fs.readlinkSync(entry.stage) !== entry.target) throw new Error('stage-target')
|
|
276
|
+
fs.renameSync(entry.stage, entry.name)
|
|
277
|
+
} else fs.unlinkSync(entry.name)
|
|
278
|
+
syncDirectory()
|
|
279
|
+
verifySourceChain(chain, start)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const validateEntries = (entries, start = currentDirectory()) => {
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
const chain = walkExpected(entry.chain)
|
|
285
|
+
name(entry.name)
|
|
286
|
+
if (!matches(state(entry.name), priorState(entry))) throw new Error('prior-state')
|
|
287
|
+
if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
|
|
288
|
+
if (entry.operation === 'write-file' && hash(name(entry.stage)) !== entry.sha256) throw new Error('stage-digest')
|
|
289
|
+
if (entry.operation === 'write-symlink' && fs.readlinkSync(name(entry.stage)) !== entry.target) throw new Error('stage-target')
|
|
290
|
+
verifySourceChain(chain, start)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const verifyEntries = (entries, start = currentDirectory()) => {
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
const chain = walkExpected(entry.chain)
|
|
296
|
+
name(entry.name)
|
|
297
|
+
if (!matches(state(entry.name), postState(entry))) throw new Error('target-state')
|
|
298
|
+
if (entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('backup-digest')
|
|
299
|
+
verifySourceChain(chain, start)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const respond = value => process.stdout.write(JSON.stringify({ version: 1, nonce: request.nonce, action: request.action, ...value }) + '\n')
|
|
304
|
+
const refuse = value => {
|
|
305
|
+
respond({ kind: 'refusal', ...value })
|
|
306
|
+
process.exit(0)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const readSource = entry => {
|
|
310
|
+
const chain = []
|
|
311
|
+
let before
|
|
312
|
+
const filename = anchoredName(entry.name)
|
|
313
|
+
try {
|
|
314
|
+
walkSource(entry.segments, chain)
|
|
315
|
+
before = fs.lstatSync(filename)
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error.code === 'ENOENT') {
|
|
318
|
+
verifySourceChain(chain)
|
|
319
|
+
return { kind: 'missing' }
|
|
320
|
+
}
|
|
321
|
+
throw error
|
|
322
|
+
}
|
|
323
|
+
if (entry.action === 'read-entry' && before.isSymbolicLink()) {
|
|
324
|
+
const target = fs.readlinkSync(filename)
|
|
325
|
+
const current = fs.lstatSync(filename)
|
|
326
|
+
if (!current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) throw new Error('source-changed')
|
|
327
|
+
verifySourceChain(chain)
|
|
328
|
+
return { kind: 'symlink', target, identity: { dev: before.dev, ino: before.ino } }
|
|
329
|
+
}
|
|
330
|
+
if (entry.action === 'read-entry' && !before.isFile()) {
|
|
331
|
+
verifySourceChain(chain)
|
|
332
|
+
return { kind: 'other' }
|
|
333
|
+
}
|
|
334
|
+
if (!before.isFile() || before.isSymbolicLink()) throw new Error('source')
|
|
335
|
+
const value = read(filename, entry.maxBytes)
|
|
336
|
+
if (value.status.dev !== before.dev || value.status.ino !== before.ino) throw new Error('source-identity')
|
|
337
|
+
const current = fs.lstatSync(filename)
|
|
338
|
+
if (!current.isFile() || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) throw new Error('source-changed')
|
|
339
|
+
verifySourceChain(chain)
|
|
340
|
+
writeExclusive(entry.output, value.bytes)
|
|
341
|
+
return {
|
|
342
|
+
kind: entry.action === 'read-entry' ? 'file' : 'complete',
|
|
343
|
+
identity: { root: request.identity, chain: chain.map(({ dev, ino }) => ({ dev, ino })), file: { dev: before.dev, ino: before.ino } },
|
|
344
|
+
...(entry.action === 'read-entry' ? { mode: before.mode & 511 } : {}),
|
|
345
|
+
sha256: digest(value.bytes),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const readBatch = () => {
|
|
350
|
+
if (!Array.isArray(request.entries) || request.entries.length < 1 || request.entries.length > limits.readBatchEntries) throw new Error('batch')
|
|
351
|
+
const requestedBytes = request.entries.reduce((total, entry) => total + readLimit(entry.maxBytes), 0)
|
|
352
|
+
if (!Number.isSafeInteger(requestedBytes) || requestedBytes > limits.readBatchBytes) throw new Error('batch-limit')
|
|
353
|
+
const entries = []
|
|
354
|
+
for (const entry of request.entries) {
|
|
355
|
+
if (!entry || typeof entry !== 'object' || typeof entry.nonce !== 'string'
|
|
356
|
+
|| !['read-file', 'read-entry'].includes(entry.action)) throw new Error('batch-entry')
|
|
357
|
+
try {
|
|
358
|
+
const result = readSource(entry)
|
|
359
|
+
if (Buffer.byteLength(JSON.stringify({ version: 1, nonce: entry.nonce, action: entry.action, ...result }) + '\n') > limits.frameBytes) throw new Error('entry-frame')
|
|
360
|
+
entries.push({ nonce: entry.nonce, action: entry.action, segments: entry.segments, name: entry.name, maxBytes: entry.maxBytes, ...result })
|
|
361
|
+
} catch {
|
|
362
|
+
// The parent accepts only this exact refusal frame, so the cause has no channel; it names the failed entry instead.
|
|
363
|
+
refuse({ failedNonce: entry.nonce })
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const receipt = Buffer.from(JSON.stringify({ version: 1, nonce: request.nonce, action: request.action, kind: 'complete', entries }) + '\n')
|
|
367
|
+
if (receipt.length > limits.readBatchReceiptBytes) throw new Error('receipt-limit')
|
|
368
|
+
writeExclusive(request.receiptOutput, receipt)
|
|
369
|
+
respond({ kind: 'complete', receiptBytes: receipt.length, receiptSha256: digest(receipt) })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const relocate = () => {
|
|
373
|
+
const base = process.cwd()
|
|
374
|
+
const destinationChain = []
|
|
375
|
+
const sourceCapture = name('.cockpit-adopt-source-' + request.nonce)
|
|
376
|
+
const destinationCapture = name('.cockpit-adopt-destination-' + request.nonce)
|
|
377
|
+
const destinationPolicy = request.action === 'restore-file' ? anchoredName : name
|
|
378
|
+
let sourceCapturePath
|
|
379
|
+
let sourceMode
|
|
380
|
+
let sourceCaptured = false
|
|
381
|
+
let sourceRestored = false
|
|
382
|
+
let originalRetired = false
|
|
383
|
+
let destinationCreated = false
|
|
384
|
+
let destinationQuarantined = false
|
|
385
|
+
let destinationIdentity
|
|
386
|
+
const restoreSource = () => {
|
|
387
|
+
process.chdir(base)
|
|
388
|
+
const chain = walkExpected(request.source.chain, anchoredName)
|
|
389
|
+
const sourceName = anchoredName(request.source.name)
|
|
390
|
+
if (state(sourceName).kind !== 'missing') throw new Error('source-recovery-occupied')
|
|
391
|
+
fs.linkSync(sourceCapture, sourceName)
|
|
392
|
+
const restored = fs.lstatSync(sourceName)
|
|
393
|
+
const captured = fs.lstatSync(sourceCapture)
|
|
394
|
+
if (!restored.isFile() || restored.isSymbolicLink() || restored.dev !== captured.dev || restored.ino !== captured.ino) throw new Error('source-recovery-identity')
|
|
395
|
+
sourceRestored = true
|
|
396
|
+
syncDirectory()
|
|
397
|
+
verifySourceChain(chain)
|
|
398
|
+
}
|
|
399
|
+
const retireSourceCapture = (requireRestored, retired = () => {}) => {
|
|
400
|
+
process.chdir(base)
|
|
401
|
+
const chain = walkExpected(request.source.chain, anchoredName)
|
|
402
|
+
const sourceName = anchoredName(request.source.name)
|
|
403
|
+
const captured = fs.lstatSync(sourceCapture)
|
|
404
|
+
if (!captured.isFile() || captured.isSymbolicLink() || captured.dev !== request.source.file.dev || captured.ino !== request.source.file.ino) throw new Error('source-retirement-identity')
|
|
405
|
+
if (requireRestored) {
|
|
406
|
+
const restored = fs.lstatSync(sourceName)
|
|
407
|
+
if (!restored.isFile() || restored.isSymbolicLink() || restored.dev !== captured.dev || restored.ino !== captured.ino) throw new Error('source-retirement-public-identity')
|
|
408
|
+
}
|
|
409
|
+
fs.unlinkSync(sourceCapture)
|
|
410
|
+
sourceCaptured = false
|
|
411
|
+
retired()
|
|
412
|
+
syncDirectory()
|
|
413
|
+
verifySourceChain(chain)
|
|
414
|
+
}
|
|
415
|
+
const removeDestination = () => {
|
|
416
|
+
process.chdir(base)
|
|
417
|
+
const chain = walkExpected(destinationChain, destinationPolicy)
|
|
418
|
+
const destinationName = destinationPolicy(request.destination.name)
|
|
419
|
+
if (state(destinationCapture).kind !== 'missing') throw new Error('destination-recovery-capture')
|
|
420
|
+
fs.renameSync(destinationName, destinationCapture)
|
|
421
|
+
destinationQuarantined = true
|
|
422
|
+
const captured = fs.lstatSync(destinationCapture)
|
|
423
|
+
if (!captured.isFile() || captured.isSymbolicLink() || captured.dev !== destinationIdentity.dev || captured.ino !== destinationIdentity.ino) {
|
|
424
|
+
fs.linkSync(destinationCapture, destinationName)
|
|
425
|
+
fs.unlinkSync(destinationCapture)
|
|
426
|
+
destinationQuarantined = false
|
|
427
|
+
syncDirectory()
|
|
428
|
+
throw new Error('destination-recovery-identity')
|
|
429
|
+
}
|
|
430
|
+
fs.unlinkSync(destinationCapture)
|
|
431
|
+
destinationQuarantined = false
|
|
432
|
+
destinationCreated = false
|
|
433
|
+
syncDirectory()
|
|
434
|
+
verifySourceChain(chain)
|
|
435
|
+
}
|
|
436
|
+
const regularWithIdentity = (status, expected) => status.isFile() && !status.isSymbolicLink() && status.dev === expected.dev && status.ino === expected.ino
|
|
437
|
+
try {
|
|
438
|
+
const sourceChain = walkExpected(request.source.chain, anchoredName)
|
|
439
|
+
const sourceName = anchoredName(request.source.name)
|
|
440
|
+
if (!regularWithIdentity(fs.lstatSync(sourceName), request.source.file)) throw new Error('source-identity')
|
|
441
|
+
if (state(sourceCapture).kind !== 'missing') throw new Error('source-capture')
|
|
442
|
+
sourceCapturePath = path.join(process.cwd(), sourceCapture)
|
|
443
|
+
fs.renameSync(sourceName, sourceCapture)
|
|
444
|
+
sourceCaptured = true
|
|
445
|
+
syncDirectory()
|
|
446
|
+
const captured = fs.lstatSync(sourceCapture)
|
|
447
|
+
if (!regularWithIdentity(captured, request.source.file)) throw new Error('source-captured-identity')
|
|
448
|
+
const source = read(sourceCapture, request.source.maxBytes, request.source.file)
|
|
449
|
+
if (source.status.dev !== captured.dev || source.status.ino !== captured.ino || digest(source.bytes) !== request.source.sha256) throw new Error('source-digest')
|
|
450
|
+
sourceMode = source.status.mode & 511
|
|
451
|
+
verifySourceChain(sourceChain)
|
|
452
|
+
|
|
453
|
+
process.chdir(base)
|
|
454
|
+
walk(safeSegments(request.destination.segments, destinationPolicy), destinationChain, destinationPolicy)
|
|
455
|
+
const destinationName = destinationPolicy(request.destination.name)
|
|
456
|
+
if (state(destinationName).kind !== 'missing') throw new Error('destination-state')
|
|
457
|
+
process.chdir(base)
|
|
458
|
+
const linkSourceChain = walkExpected(request.source.chain, anchoredName)
|
|
459
|
+
const currentCapture = read(sourceCapturePath, request.source.maxBytes, request.source.file)
|
|
460
|
+
if (currentCapture.status.dev !== request.source.file.dev || currentCapture.status.ino !== request.source.file.ino
|
|
461
|
+
|| digest(currentCapture.bytes) !== request.source.sha256 || (currentCapture.status.mode & 511) !== sourceMode) throw new Error('source-capture-changed')
|
|
462
|
+
verifySourceChain(linkSourceChain)
|
|
463
|
+
process.chdir(base)
|
|
464
|
+
walkExpected(destinationChain, destinationPolicy)
|
|
465
|
+
fs.linkSync(sourceCapturePath, destinationName)
|
|
466
|
+
destinationCreated = true
|
|
467
|
+
destinationIdentity = { ...request.source.file }
|
|
468
|
+
const destination = fs.lstatSync(destinationName)
|
|
469
|
+
if (!regularWithIdentity(destination, destinationIdentity)) throw new Error('destination-linked-identity')
|
|
470
|
+
process.chdir(base)
|
|
471
|
+
const linkedSourceChain = walkExpected(request.source.chain, anchoredName)
|
|
472
|
+
if (!regularWithIdentity(fs.lstatSync(sourceCapture), destinationIdentity)) throw new Error('source-linked-identity')
|
|
473
|
+
verifySourceChain(linkedSourceChain)
|
|
474
|
+
process.chdir(base)
|
|
475
|
+
walkExpected(destinationChain, destinationPolicy)
|
|
476
|
+
if (hash(destinationName, destinationPolicy) !== request.source.sha256 || (destination.mode & 511) !== sourceMode) throw new Error('destination-postimage')
|
|
477
|
+
syncDirectory()
|
|
478
|
+
verifySourceChain(destinationChain)
|
|
479
|
+
|
|
480
|
+
process.chdir(base)
|
|
481
|
+
const currentSourceChain = walkExpected(request.source.chain, anchoredName)
|
|
482
|
+
if (state(sourceName).kind !== 'missing') throw new Error('source-postimage')
|
|
483
|
+
if (!regularWithIdentity(fs.lstatSync(sourceCapture), destinationIdentity)) throw new Error('source-capture-identity')
|
|
484
|
+
verifySourceChain(currentSourceChain)
|
|
485
|
+
|
|
486
|
+
process.chdir(base)
|
|
487
|
+
const currentDestinationChain = walkExpected(destinationChain, destinationPolicy)
|
|
488
|
+
const currentDestination = fs.lstatSync(destinationName)
|
|
489
|
+
if (!regularWithIdentity(currentDestination, destinationIdentity) || hash(destinationName, destinationPolicy) !== request.source.sha256
|
|
490
|
+
|| (currentDestination.mode & 511) !== sourceMode) throw new Error('destination-changed')
|
|
491
|
+
verifySourceChain(currentDestinationChain)
|
|
492
|
+
process.chdir(base)
|
|
493
|
+
identity()
|
|
494
|
+
retireSourceCapture(false, () => { originalRetired = true })
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const describe = value => value instanceof Error ? value.message : String(value)
|
|
497
|
+
const failure = describe(error)
|
|
498
|
+
const recoveryFailures = []
|
|
499
|
+
if (sourceCaptured) {
|
|
500
|
+
try { restoreSource() } catch (recoveryError) { recoveryFailures.push('source:' + describe(recoveryError)) }
|
|
501
|
+
}
|
|
502
|
+
if (destinationCreated && !originalRetired) {
|
|
503
|
+
try { removeDestination() } catch (recoveryError) { recoveryFailures.push('destination:' + describe(recoveryError)) }
|
|
504
|
+
}
|
|
505
|
+
if (sourceCaptured && sourceRestored && !destinationCreated) {
|
|
506
|
+
try { retireSourceCapture(true) } catch (recoveryError) { recoveryFailures.push('source-retirement:' + describe(recoveryError)) }
|
|
507
|
+
}
|
|
508
|
+
if (originalRetired) recoveryFailures.push('source:original-retired')
|
|
509
|
+
if (destinationChain.length > 0) {
|
|
510
|
+
try {
|
|
511
|
+
process.chdir(base)
|
|
512
|
+
walkExpected(destinationChain, destinationPolicy)
|
|
513
|
+
removeCreatedChain(destinationChain, request.identity, destinationPolicy)
|
|
514
|
+
} catch (recoveryError) { recoveryFailures.push('directories:' + describe(recoveryError)) }
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
process.chdir(base)
|
|
518
|
+
identity()
|
|
519
|
+
} catch (recoveryError) { recoveryFailures.push('root:' + describe(recoveryError)) }
|
|
520
|
+
if (recoveryFailures.length > 0) {
|
|
521
|
+
const retained = []
|
|
522
|
+
if (sourceCaptured) retained.push([...request.source.chain.map(entry => entry.name), sourceCapture].join('/'))
|
|
523
|
+
if (destinationQuarantined) retained.push([...request.destination.segments, destinationCapture].join('/'))
|
|
524
|
+
else if (destinationCreated) retained.push([...request.destination.segments, request.destination.name].join('/'))
|
|
525
|
+
refuse({ reason: 'relocate-recovery-failed', failure, recoveryFailures, retained })
|
|
526
|
+
}
|
|
527
|
+
refuse({ reason: 'relocate-failed', failure })
|
|
528
|
+
}
|
|
529
|
+
respond({ kind: 'complete', sha256: request.source.sha256 })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Stages one group: creates its missing parents, copies each staged input beside its target, and
|
|
533
|
+
// copies each prior file to a backup. Targets are never changed here, so an interrupted prepare
|
|
534
|
+
// leaves only stage and backup files. On failure it removes what this group created.
|
|
535
|
+
const prepareGroup = (group, base, created) => {
|
|
536
|
+
const chain = []
|
|
537
|
+
const prepared = []
|
|
538
|
+
const attempted = []
|
|
539
|
+
let start
|
|
540
|
+
let final
|
|
541
|
+
let groupPath
|
|
542
|
+
try {
|
|
543
|
+
process.chdir(base)
|
|
544
|
+
if (!group || typeof group !== 'object' || typeof group.nonce !== 'string' || !Array.isArray(group.anchor) || !Array.isArray(group.entries)) throw new Error('batch-group')
|
|
545
|
+
walkExpected(group.anchor)
|
|
546
|
+
start = fs.lstatSync('.')
|
|
547
|
+
if (!same(start, group.identity)) throw new Error('anchor-identity')
|
|
548
|
+
walkCreating(safeSegments(group.segments), chain, created, group.anchor.map(entry => entry.name))
|
|
549
|
+
final = fs.lstatSync('.')
|
|
550
|
+
groupPath = process.cwd()
|
|
551
|
+
const groupPrefix = [...group.anchor, ...chain].map(entry => entry.name)
|
|
552
|
+
for (const [index, entry] of group.entries.entries()) {
|
|
553
|
+
process.chdir(groupPath)
|
|
554
|
+
const nested = []
|
|
555
|
+
attempted.push({ index, chain: nested })
|
|
556
|
+
nested.push(...walkExpected(entry.chain))
|
|
557
|
+
walkCreating(safeSegments(entry.segments), nested, created, groupPrefix)
|
|
558
|
+
const target = name(entry.name)
|
|
559
|
+
const stage = name('.cockpit-stage-' + group.nonce + '-' + index)
|
|
560
|
+
const backup = name('.cockpit-backup-' + group.nonce + '-' + index)
|
|
561
|
+
const prior = state(target)
|
|
562
|
+
if (!matches(prior, entry.expected)) throw new Error('prestate')
|
|
563
|
+
let stageName = null
|
|
564
|
+
if (entry.operation === 'write-file') {
|
|
565
|
+
copy(process.argv[INPUT_ARGUMENT_OFFSET + entry.input], stage, entry.sha256)
|
|
566
|
+
stageName = stage
|
|
567
|
+
} else if (entry.operation === 'write-symlink') {
|
|
568
|
+
fs.symlinkSync(entry.target, stage)
|
|
569
|
+
stageName = stage
|
|
570
|
+
} else if (entry.operation !== 'remove-file' && entry.operation !== 'remove-symlink') throw new Error('operation')
|
|
571
|
+
let backupName = null
|
|
572
|
+
if (prior.kind === 'file') {
|
|
573
|
+
copy(target, backup, prior.sha256)
|
|
574
|
+
backupName = backup
|
|
575
|
+
}
|
|
576
|
+
prepared.push({
|
|
577
|
+
chain: nested, name: target, operation: entry.operation, sha256: entry.sha256 ?? null, target: entry.target ?? null,
|
|
578
|
+
stage: stageName, backup: backupName, priorKind: prior.kind, priorSha256: prior.sha256 ?? null,
|
|
579
|
+
priorTarget: prior.target ?? null, priorMode: prior.mode ?? null, priorIdentity: prior.identity ?? null,
|
|
580
|
+
})
|
|
581
|
+
verifySourceChain(nested, final)
|
|
582
|
+
}
|
|
583
|
+
process.chdir(groupPath)
|
|
584
|
+
syncDirectory()
|
|
585
|
+
verifySourceChain(chain, group.identity)
|
|
586
|
+
} catch {
|
|
587
|
+
if (!start) return { reason: 'prepare-failed' }
|
|
588
|
+
try {
|
|
589
|
+
if (groupPath && final) {
|
|
590
|
+
for (const item of [...attempted].reverse()) {
|
|
591
|
+
process.chdir(groupPath)
|
|
592
|
+
walkExpected(item.chain)
|
|
593
|
+
fs.rmSync(name('.cockpit-stage-' + group.nonce + '-' + item.index), { force: true })
|
|
594
|
+
fs.rmSync(name('.cockpit-backup-' + group.nonce + '-' + item.index), { force: true })
|
|
595
|
+
syncDirectory()
|
|
596
|
+
removeCreatedChain(item.chain, final)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
process.chdir(base)
|
|
600
|
+
walkExpected(group.anchor)
|
|
601
|
+
if (chain.length > 0) {
|
|
602
|
+
walkExpected(chain)
|
|
603
|
+
removeCreatedChain(chain, group.identity)
|
|
604
|
+
}
|
|
605
|
+
if (!same(fs.lstatSync('.'), group.identity)) throw new Error('anchor-identity')
|
|
606
|
+
} catch {
|
|
607
|
+
// The receipt carries a fixed reason code that the parent matches exactly; cleanup failing is that code.
|
|
608
|
+
return { reason: 'prepare-cleanup-failed' }
|
|
609
|
+
}
|
|
610
|
+
return { reason: 'prepare-failed' }
|
|
611
|
+
}
|
|
612
|
+
return { receipt: { nonce: group.nonce, identity: { dev: final.dev, ino: final.ino }, chain, entries: prepared } }
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Prepares every group in order from the transaction root. A later group may enter a directory an
|
|
616
|
+
// earlier group created. On a refusal the receipt still lists the groups already prepared, so the
|
|
617
|
+
// caller can roll them back.
|
|
618
|
+
const prepareBatch = () => {
|
|
619
|
+
if (!Array.isArray(request.groups) || request.groups.length < 1 || request.groups.length > limits.publicationGroups) throw new Error('batch')
|
|
620
|
+
const base = process.cwd()
|
|
621
|
+
const created = new Map()
|
|
622
|
+
const groups = []
|
|
623
|
+
for (const group of request.groups) {
|
|
624
|
+
const outcome = prepareGroup(group, base, created)
|
|
625
|
+
// No process.exit here: this receipt can exceed a pipe buffer, and exiting would truncate it.
|
|
626
|
+
if (!outcome.receipt) return respond({ kind: 'refusal', reason: outcome.reason, groups })
|
|
627
|
+
groups.push(outcome.receipt)
|
|
628
|
+
}
|
|
629
|
+
respond({ kind: 'complete', groups })
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// validate-batch and verify-batch only check. publish-batch validates every group before it
|
|
633
|
+
// renames any staged entry; commit-batch verifies every group before it retires any backup and
|
|
634
|
+
// reports a verification failure as `verify-failed`, so the caller knows nothing was retired.
|
|
635
|
+
const publicationBatch = () => {
|
|
636
|
+
if (!Array.isArray(request.groups) || request.groups.length < 1 || request.groups.length > limits.publicationGroups) throw new Error('batch')
|
|
637
|
+
const base = process.cwd()
|
|
638
|
+
const eachGroup = operation => {
|
|
639
|
+
for (const group of request.groups) {
|
|
640
|
+
process.chdir(base)
|
|
641
|
+
if (!group || typeof group !== 'object' || typeof group.nonce !== 'string' || typeof group.parent !== 'string'
|
|
642
|
+
|| !Array.isArray(group.chain) || !Array.isArray(group.entries)) throw new Error('batch-group')
|
|
643
|
+
const chain = walkExpected(group.chain)
|
|
644
|
+
if (!same(fs.lstatSync('.'), group.identity)) throw new Error('group-identity')
|
|
645
|
+
operation(group)
|
|
646
|
+
verifySourceChain(chain)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (request.action === 'validate-batch' || request.action === 'publish-batch') eachGroup(group => validateEntries(group.entries))
|
|
650
|
+
else {
|
|
651
|
+
try { eachGroup(group => verifyEntries(group.entries)) } catch (error) {
|
|
652
|
+
if (request.action === 'commit-batch') refuse({ reason: 'verify-failed' })
|
|
653
|
+
throw error
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (request.action === 'publish-batch') eachGroup(group => publishEntries(group.entries))
|
|
657
|
+
if (request.action === 'commit-batch') {
|
|
658
|
+
eachGroup(group => {
|
|
659
|
+
for (const entry of group.entries) {
|
|
660
|
+
verifyEntries([entry], group.identity)
|
|
661
|
+
const nested = walkExpected(entry.chain)
|
|
662
|
+
if (entry.backup !== null) fs.unlinkSync(name(entry.backup))
|
|
663
|
+
syncDirectory()
|
|
664
|
+
verifySourceChain(nested, group.identity)
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
respond({ kind: 'complete', groups: request.groups.map(group => ({ nonce: group.nonce, parent: group.parent, kind: 'complete' })) })
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Restores one prepared or published group from its backups, then removes the directories it created.
|
|
672
|
+
const rollback = () => {
|
|
673
|
+
const groupPath = process.cwd()
|
|
674
|
+
for (const entry of request.entries) {
|
|
675
|
+
process.chdir(groupPath)
|
|
676
|
+
const nested = walkExpected(entry.chain)
|
|
677
|
+
name(entry.name)
|
|
678
|
+
if (entry.stage !== null) name(entry.stage)
|
|
679
|
+
const current = state(entry.name)
|
|
680
|
+
if (!matches(current, postState(entry)) && !matches(current, priorState(entry))
|
|
681
|
+
|| entry.backup !== null && hash(entry.backup) !== entry.priorSha256) throw new Error('recovery-state')
|
|
682
|
+
verifySourceChain(nested, request.identity)
|
|
683
|
+
}
|
|
684
|
+
for (const entry of [...request.entries].reverse()) {
|
|
685
|
+
process.chdir(groupPath)
|
|
686
|
+
const nested = walkExpected(entry.chain)
|
|
687
|
+
const current = state(entry.name)
|
|
688
|
+
if (matches(current, postState(entry))) {
|
|
689
|
+
if (current.kind !== 'missing') fs.unlinkSync(entry.name)
|
|
690
|
+
if (entry.priorKind === 'file') {
|
|
691
|
+
fs.renameSync(entry.backup, entry.name)
|
|
692
|
+
fs.chmodSync(entry.name, entry.priorMode)
|
|
693
|
+
} else if (entry.priorKind === 'symlink') fs.symlinkSync(entry.priorTarget, entry.name)
|
|
694
|
+
} else if (entry.backup !== null) fs.unlinkSync(entry.backup)
|
|
695
|
+
if (entry.stage !== null) fs.rmSync(entry.stage, { force: true })
|
|
696
|
+
syncDirectory()
|
|
697
|
+
verifySourceChain(nested, request.identity)
|
|
698
|
+
}
|
|
699
|
+
process.chdir(groupPath)
|
|
700
|
+
removeCreatedDirectories(request.entries, request.identity)
|
|
701
|
+
for (let index = request.chain.length - 1; index >= 0; index--) {
|
|
702
|
+
const child = request.chain[index]
|
|
703
|
+
process.chdir('..')
|
|
704
|
+
const parent = fs.lstatSync('.')
|
|
705
|
+
const expected = index === 0 ? request.start : request.chain[index - 1]
|
|
706
|
+
if (!same(parent, expected)) throw new Error('parent-identity')
|
|
707
|
+
if (!same(fs.lstatSync(name(child.name)), child)) throw new Error('child-identity')
|
|
708
|
+
if (child.created) {
|
|
709
|
+
try {
|
|
710
|
+
fs.rmdirSync(name(child.name))
|
|
711
|
+
syncDirectory()
|
|
712
|
+
} catch (error) { if (error.code !== 'ENOTEMPTY') throw error }
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
respond({ kind: 'complete' })
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
limits = parseLimits(request.limits)
|
|
720
|
+
identity()
|
|
721
|
+
if (request.action === 'read-batch') readBatch()
|
|
722
|
+
else if (request.action === 'read-file' || request.action === 'read-entry') respond(readSource(request))
|
|
723
|
+
else if (request.action === 'relocate-file' || request.action === 'restore-file') relocate()
|
|
724
|
+
else if (request.action === 'prepare-batch') prepareBatch()
|
|
725
|
+
else if (['validate-batch', 'publish-batch', 'verify-batch', 'commit-batch'].includes(request.action)) publicationBatch()
|
|
726
|
+
else if (request.action === 'rollback') rollback()
|
|
727
|
+
else throw new Error('action')
|
|
728
|
+
} catch {
|
|
729
|
+
// The parent accepts only this fixed refusal frame, so the cause has no channel.
|
|
730
|
+
respond({ kind: 'refusal', reason: 'operation-refused' })
|
|
731
|
+
}
|