@skrr-ai/cli 0.1.94 → 0.1.96
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/README.md +175 -165
- package/bin/run.js +32 -0
- package/bin/update-npm-runner.cjs +33 -0
- package/bin/update-session.cjs +194 -0
- package/bin/update-worker.cjs +208 -0
- package/dist/base-command.js +0 -15
- package/dist/commands/agent/message/send.d.ts +2 -0
- package/dist/commands/agent/message/send.js +75 -5
- package/dist/commands/agents/chat.js +1 -1
- package/dist/commands/agents/create.d.ts +0 -3
- package/dist/commands/agents/create.js +0 -26
- package/dist/commands/agents/egress.d.ts +17 -0
- package/dist/commands/agents/egress.js +84 -0
- package/dist/commands/agents/mailbox/messages.d.ts +14 -0
- package/dist/commands/agents/mailbox/messages.js +52 -0
- package/dist/commands/agents/mailbox/show.d.ts +13 -0
- package/dist/commands/agents/mailbox/show.js +52 -0
- package/dist/commands/agents/operating-plan.js +8 -0
- package/dist/commands/agents/roles/capture.d.ts +16 -0
- package/dist/commands/agents/roles/capture.js +53 -0
- package/dist/commands/agents/roles/install.d.ts +1 -0
- package/dist/commands/agents/roles/install.js +8 -1
- package/dist/commands/agents/roles/preview.d.ts +2 -0
- package/dist/commands/agents/roles/preview.js +27 -3
- package/dist/commands/agents/update.d.ts +0 -3
- package/dist/commands/agents/update.js +1 -31
- package/dist/commands/agents/usage-cap.d.ts +4 -1
- package/dist/commands/agents/usage-cap.js +49 -10
- package/dist/commands/code/doctor.d.ts +1 -0
- package/dist/commands/code/doctor.js +6 -0
- package/dist/commands/code/index.js +4 -0
- package/dist/commands/code/install.d.ts +5 -62
- package/dist/commands/code/install.js +86 -279
- package/dist/commands/code/jobs/show.js +12 -0
- package/dist/commands/code/rollback.d.ts +8 -0
- package/dist/commands/code/rollback.js +20 -0
- package/dist/commands/code/update.d.ts +8 -37
- package/dist/commands/code/update.js +134 -213
- package/dist/commands/commitments/action-proposals/decide.d.ts +2 -0
- package/dist/commands/commitments/action-proposals/decide.js +12 -0
- package/dist/commands/commitments/action-proposals.js +7 -0
- package/dist/commands/commitments/create.d.ts +1 -0
- package/dist/commands/commitments/create.js +17 -1
- package/dist/commands/commitments/delivery.d.ts +1 -0
- package/dist/commands/commitments/delivery.js +11 -1
- package/dist/commands/commitments/doctor.js +4 -0
- package/dist/commands/commitments/effective-policy.js +26 -0
- package/dist/commands/commitments/endpoints/create.js +5 -2
- package/dist/commands/commitments/observations.d.ts +10 -0
- package/dist/commands/commitments/observations.js +88 -0
- package/dist/commands/commitments/pause.d.ts +1 -1
- package/dist/commands/commitments/pause.js +4 -3
- package/dist/commands/commitments/preflight.js +2 -1
- package/dist/commands/commitments/reports.d.ts +2 -0
- package/dist/commands/commitments/reports.js +22 -4
- package/dist/commands/commitments/update.d.ts +1 -0
- package/dist/commands/commitments/update.js +35 -5
- package/dist/commands/daemon/install.js +4 -0
- package/dist/commands/generate-recovery-code.d.ts +13 -1
- package/dist/commands/generate-recovery-code.js +146 -13
- package/dist/commands/grants/create.d.ts +1 -0
- package/dist/commands/grants/create.js +8 -0
- package/dist/commands/grants/rotate.d.ts +5 -0
- package/dist/commands/grants/rotate.js +13 -1
- package/dist/commands/grants/update.d.ts +12 -0
- package/dist/commands/grants/update.js +27 -0
- package/dist/commands/instructions/guide.d.ts +20 -0
- package/dist/commands/instructions/guide.js +84 -0
- package/dist/commands/instructions/install.js +57 -33
- package/dist/commands/instructions/setup.d.ts +14 -0
- package/dist/commands/instructions/setup.js +72 -0
- package/dist/commands/integrations/execute.js +1 -1
- package/dist/commands/login.d.ts +5 -6
- package/dist/commands/login.js +27 -11
- package/dist/commands/logout.js +7 -7
- package/dist/commands/machines/hosted/start.js +7 -1
- package/dist/commands/machines/list.js +4 -4
- package/dist/commands/measures/confirm.d.ts +1 -7
- package/dist/commands/measures/confirm.js +15 -14
- package/dist/commands/measures/list.d.ts +1 -7
- package/dist/commands/measures/list.js +1 -7
- package/dist/commands/measures/record.js +4 -4
- package/dist/commands/measures/show.d.ts +11 -0
- package/dist/commands/measures/show.js +26 -0
- package/dist/commands/northstars/governance/configure.d.ts +1 -0
- package/dist/commands/northstars/governance/configure.js +8 -1
- package/dist/commands/proposals/discard.d.ts +5 -0
- package/dist/commands/proposals/discard.js +8 -0
- package/dist/commands/proposals/keep.d.ts +5 -0
- package/dist/commands/proposals/keep.js +8 -0
- package/dist/commands/proposals/list.d.ts +15 -0
- package/dist/commands/proposals/list.js +74 -0
- package/dist/commands/proposals/show.d.ts +12 -0
- package/dist/commands/proposals/show.js +27 -0
- package/dist/commands/proposals/snooze.d.ts +5 -0
- package/dist/commands/proposals/snooze.js +8 -0
- package/dist/commands/security/overview.d.ts +11 -0
- package/dist/commands/security/overview.js +158 -0
- package/dist/commands/signals/monitors/create.d.ts +9 -0
- package/dist/commands/signals/monitors/create.js +29 -0
- package/dist/commands/signals/monitors/list.d.ts +8 -0
- package/dist/commands/signals/monitors/list.js +16 -0
- package/dist/commands/signals/monitors/pause.d.ts +11 -0
- package/dist/commands/signals/monitors/pause.js +17 -0
- package/dist/commands/signals/monitors/resume.d.ts +11 -0
- package/dist/commands/signals/monitors/resume.js +17 -0
- package/dist/commands/signals/monitors/revise.d.ts +12 -0
- package/dist/commands/signals/monitors/revise.js +25 -0
- package/dist/commands/signals/monitors/show.d.ts +11 -0
- package/dist/commands/signals/monitors/show.js +17 -0
- package/dist/commands/signals/monitors/test.d.ts +12 -0
- package/dist/commands/signals/monitors/test.js +25 -0
- package/dist/commands/signals/monitors/transitions.d.ts +11 -0
- package/dist/commands/signals/monitors/transitions.js +17 -0
- package/dist/commands/signals/monitors/validate.d.ts +9 -0
- package/dist/commands/signals/monitors/validate.js +24 -0
- package/dist/commands/signals/schemas/list.d.ts +8 -0
- package/dist/commands/signals/schemas/list.js +16 -0
- package/dist/commands/signals/schemas/show.d.ts +11 -0
- package/dist/commands/signals/schemas/show.js +17 -0
- package/dist/commands/signals/sources/list.d.ts +8 -0
- package/dist/commands/signals/sources/list.js +16 -0
- package/dist/commands/signals/sources/receipts.d.ts +11 -0
- package/dist/commands/signals/sources/receipts.js +17 -0
- package/dist/commands/signals/sources/show.d.ts +11 -0
- package/dist/commands/signals/sources/show.js +17 -0
- package/dist/commands/spaces/claim.d.ts +12 -0
- package/dist/commands/spaces/claim.js +42 -0
- package/dist/commands/spaces/ownership.d.ts +12 -0
- package/dist/commands/spaces/ownership.js +46 -0
- package/dist/commands/spaces/usage.d.ts +12 -0
- package/dist/commands/spaces/usage.js +28 -0
- package/dist/commands/spaces/workflows/run.js +3 -0
- package/dist/commands/spaces/workflows/runs.js +2 -1
- package/dist/commands/spaces/workflows/update.d.ts +6 -1
- package/dist/commands/spaces/workflows/update.js +34 -2
- package/dist/commands/tasks/cancel-cascade.js +1 -1
- package/dist/commands/tasks/cascade-status.d.ts +2 -0
- package/dist/commands/tasks/cascade-status.js +27 -1
- package/dist/commands/tasks/create-tree.d.ts +5 -1
- package/dist/commands/tasks/create-tree.js +35 -7
- package/dist/commands/tasks/create.d.ts +4 -0
- package/dist/commands/tasks/create.js +16 -0
- package/dist/commands/tasks/deliverable/add.js +12 -3
- package/dist/commands/tasks/economics-compare.d.ts +15 -0
- package/dist/commands/tasks/economics-compare.js +60 -0
- package/dist/commands/tasks/expectations/evaluate.d.ts +13 -0
- package/dist/commands/tasks/expectations/evaluate.js +43 -0
- package/dist/commands/tasks/interrupt.js +2 -2
- package/dist/commands/tasks/list.d.ts +1 -0
- package/dist/commands/tasks/list.js +44 -1
- package/dist/commands/tasks/pause-schedule.js +1 -1
- package/dist/commands/tasks/show.d.ts +5 -0
- package/dist/commands/tasks/show.js +17 -0
- package/dist/commands/tasks/update.d.ts +4 -0
- package/dist/commands/tasks/update.js +9 -0
- package/dist/commands/tasks/workflow-findings.js +2 -2
- package/dist/commands/token/list.d.ts +3 -5
- package/dist/commands/token/list.js +99 -57
- package/dist/commands/triggers/create.d.ts +2 -0
- package/dist/commands/triggers/create.js +15 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +61 -0
- package/dist/commands/workspaces/grants.d.ts +12 -0
- package/dist/commands/workspaces/grants.js +31 -0
- package/dist/commands/workspaces/stewardship.d.ts +13 -0
- package/dist/commands/workspaces/stewardship.js +34 -0
- package/dist/generated/managed-instruction-guides.json +9 -0
- package/dist/hooks/init.d.ts +5 -0
- package/dist/hooks/init.js +27 -0
- package/dist/lib/agent-config.d.ts +0 -12
- package/dist/lib/agent-config.js +1 -27
- package/dist/lib/agent-mailbox.d.ts +35 -0
- package/dist/lib/agent-mailbox.js +55 -0
- package/dist/lib/commitment-endpoints.d.ts +2 -1
- package/dist/lib/commitment-endpoints.js +2 -5
- package/dist/lib/commitment-product.d.ts +3 -1
- package/dist/lib/commitment-product.js +10 -0
- package/dist/lib/commitment-terms.js +1 -1
- package/dist/lib/commitments.d.ts +17 -15
- package/dist/lib/commitments.js +173 -137
- package/dist/lib/compasses.d.ts +14 -0
- package/dist/lib/compasses.js +25 -11
- package/dist/lib/daemon-installer.d.ts +2 -0
- package/dist/lib/daemon-installer.js +100 -24
- package/dist/lib/first-party-harness-doctor.js +31 -1
- package/dist/lib/first-party-harness-remote-install.d.ts +71 -0
- package/dist/lib/first-party-harness-remote-install.js +310 -0
- package/dist/lib/first-party-harness-remote-update.d.ts +46 -0
- package/dist/lib/first-party-harness-remote-update.js +242 -0
- package/dist/lib/first-party-harness-startup-update.d.ts +26 -0
- package/dist/lib/first-party-harness-startup-update.js +166 -0
- package/dist/lib/first-party-harness-update.d.ts +46 -0
- package/dist/lib/first-party-harness-update.js +289 -0
- package/dist/lib/first-party-harness.js +7 -4
- package/dist/lib/grants.d.ts +5 -1
- package/dist/lib/grants.js +9 -1
- package/dist/lib/hosted-machines.d.ts +7 -0
- package/dist/lib/mailbox-human-command.d.ts +11 -0
- package/dist/lib/mailbox-human-command.js +41 -0
- package/dist/lib/managed-instruction-local.d.ts +12 -0
- package/dist/lib/managed-instruction-local.js +58 -0
- package/dist/lib/managed-instruction-setup.d.ts +35 -0
- package/dist/lib/managed-instruction-setup.js +306 -0
- package/dist/lib/measures.d.ts +28 -2
- package/dist/lib/measures.js +38 -14
- package/dist/lib/memory-sync.d.ts +1 -0
- package/dist/lib/memory-sync.js +279 -68
- package/dist/lib/message-intent.d.ts +6 -2
- package/dist/lib/message-intent.js +1 -0
- package/dist/lib/northstars.js +2 -1
- package/dist/lib/platform-update-local.d.ts +2 -0
- package/dist/lib/platform-update-local.js +113 -0
- package/dist/lib/platform-update.d.ts +39 -0
- package/dist/lib/platform-update.js +84 -0
- package/dist/lib/profile-machine-identity-guard.d.ts +2 -0
- package/dist/lib/profile-machine-identity-guard.js +13 -0
- package/dist/lib/prompt.d.ts +1 -0
- package/dist/lib/prompt.js +4 -3
- package/dist/lib/proposal-decision-command.d.ts +19 -0
- package/dist/lib/proposal-decision-command.js +64 -0
- package/dist/lib/proposals.d.ts +8 -0
- package/dist/lib/proposals.js +22 -0
- package/dist/lib/roles.d.ts +141 -6
- package/dist/lib/roles.js +8 -2
- package/dist/lib/signals.d.ts +20 -0
- package/dist/lib/signals.js +24 -0
- package/dist/lib/space-usage.d.ts +3 -0
- package/dist/lib/space-usage.js +17 -0
- package/dist/lib/stewardship-output.d.ts +4 -0
- package/dist/lib/stewardship-output.js +82 -0
- package/dist/lib/task-asset-upload.d.ts +3 -1
- package/dist/lib/task-asset-upload.js +1 -0
- package/dist/lib/task-batch.d.ts +9 -0
- package/dist/lib/task-batch.js +94 -17
- package/dist/lib/task-economics.d.ts +16 -0
- package/dist/lib/task-economics.js +22 -0
- package/dist/lib/task-instruction-offer.d.ts +1 -4
- package/dist/lib/task-instruction-offer.js +3 -129
- package/dist/lib/triggers.d.ts +11 -3
- package/dist/lib/triggers.js +58 -1
- package/dist/lib/update-check.d.ts +61 -53
- package/dist/lib/update-check.js +453 -314
- package/dist/lib/usage-discovery.d.ts +10 -0
- package/dist/lib/usage-discovery.js +24 -3
- package/dist/lib/workflow-run-budget.d.ts +12 -0
- package/dist/lib/workflow-run-budget.js +37 -0
- package/dist/lib/workspaces.d.ts +48 -0
- package/dist/lib/workspaces.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +14 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.d.ts +82 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.d.ts +110 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.js +945 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/githubPushRefusal.d.ts +9 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/githubPushRefusal.js +49 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.js +62 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +35 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.d.ts +63 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.js +169 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.js +44 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.d.ts +82 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.js +102 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.d.ts +110 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.js +923 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/githubPushRefusal.d.ts +9 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/githubPushRefusal.js +44 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +35 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.d.ts +63 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.js +163 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.js +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +8069 -7121
- package/docs/v1/README.md +28 -0
- package/docs/v1/api.md +55 -0
- package/docs/v1/changelog.md +16 -0
- package/docs/v1/check-examples.mjs +178 -0
- package/docs/v1/examples/api-readonly.mjs +144 -0
- package/docs/v1/operations.md +87 -0
- package/docs/v1/quickstart.md +71 -0
- package/oclif.manifest.json +49424 -46174
- package/package.json +34 -11
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One signed engine installation transaction for CLI, Daemon and Desktop.
|
|
3
|
+
* Releases contain both executable and provenance; replacing `current` is the
|
|
4
|
+
* only activation operation. Existing sessions keep their immutable executable.
|
|
5
|
+
* The signing identity is independent of the daemon release identity.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import lockfile from 'proper-lockfile';
|
|
11
|
+
import { FIRST_PARTY_HARNESS, FIRST_PARTY_HARNESS_ENV, firstPartyHarnessArtifactSpellings, firstPartyHarnessBinaryName, readFirstPartyHarnessEnv, } from './firstPartyHarness.js';
|
|
12
|
+
import { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, migrateFirstPartyHarnessHome, } from './firstPartyHarnessHome.js';
|
|
13
|
+
import { FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessFeedCandidates, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel, } from './firstPartyHarnessChannels.js';
|
|
14
|
+
import { verifyArtifact, verifyManifest, } from './releaseManifest.js';
|
|
15
|
+
import { FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS } from './firstPartyHarnessReleaseKeys.js';
|
|
16
|
+
import { compareReleaseVersions } from './releaseVersions.js';
|
|
17
|
+
import { assessReleasePolicy, verifyReleasePolicy, } from './releasePolicy.js';
|
|
18
|
+
export { FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS } from './firstPartyHarnessReleaseKeys.js';
|
|
19
|
+
export { firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, } from './firstPartyHarnessChannels.js';
|
|
20
|
+
export { firstPartyHarnessManifestFile } from './firstPartyHarness.js';
|
|
21
|
+
export const PREVIOUS_SUFFIX = FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX;
|
|
22
|
+
export const PROVENANCE_SUFFIX = FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX;
|
|
23
|
+
export const MAX_ARTIFACT_BYTES = 400 * 1024 * 1024;
|
|
24
|
+
const MANIFEST_FETCH_TIMEOUT_MS = 30_000;
|
|
25
|
+
const ARTIFACT_FETCH_TIMEOUT_MS = 10 * 60_000;
|
|
26
|
+
const LOCK_STALE_MS = 120_000;
|
|
27
|
+
function fail(code, error) {
|
|
28
|
+
return { ok: false, code, error };
|
|
29
|
+
}
|
|
30
|
+
function fetchSignal(options, timeout) {
|
|
31
|
+
return options.signal
|
|
32
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(timeout)])
|
|
33
|
+
: AbortSignal.timeout(timeout);
|
|
34
|
+
}
|
|
35
|
+
export function firstPartyHarnessInstallDir(env = process.env) {
|
|
36
|
+
return firstPartyHarnessBinDir(env);
|
|
37
|
+
}
|
|
38
|
+
export function firstPartyHarnessBinaryPath(env = process.env) {
|
|
39
|
+
return firstPartyHarnessInstalledBinaryPath(env);
|
|
40
|
+
}
|
|
41
|
+
export function firstPartyHarnessPlatformKey(platform = process.platform, arch = process.arch) {
|
|
42
|
+
const cpu = arch === 'arm64' || arch === 'aarch64'
|
|
43
|
+
? 'arm64'
|
|
44
|
+
: arch === 'x64' || arch === 'x86_64'
|
|
45
|
+
? 'x64'
|
|
46
|
+
: null;
|
|
47
|
+
if (!cpu ||
|
|
48
|
+
!['darwin', 'linux', 'win32'].includes(platform) ||
|
|
49
|
+
(platform === 'win32' && cpu !== 'x64'))
|
|
50
|
+
return null;
|
|
51
|
+
return `${platform}-${cpu}`;
|
|
52
|
+
}
|
|
53
|
+
export async function fetchVerifiedManifest(options = {}) {
|
|
54
|
+
const env = options.env ?? process.env;
|
|
55
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
56
|
+
const rawFetch = options.fetchImpl ?? globalThis.fetch;
|
|
57
|
+
if (options.version && compareReleaseVersions(options.version, options.version) === null)
|
|
58
|
+
return fail('version-invalid', 'requested version is not a semantic release version');
|
|
59
|
+
if (!keys || keys.length === 0) {
|
|
60
|
+
// See the header: unlike the daemon's self-update, an unverifiable harness
|
|
61
|
+
// is refused rather than installed. Provisioning the keypair must never be
|
|
62
|
+
// the thing that REDUCES what the daemon accepts.
|
|
63
|
+
return fail('no-pinned-keys', `no pinned ${FIRST_PARTY_HARNESS.displayName} release keys — refusing to install`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof rawFetch !== 'function') {
|
|
66
|
+
return fail('feed-unreachable', 'no fetch implementation available');
|
|
67
|
+
}
|
|
68
|
+
// A host the egress policy blocks must say so, not read as a certificate
|
|
69
|
+
// error (OSK-11947; see fetch-failure.ts).
|
|
70
|
+
const doFetch = rawFetch;
|
|
71
|
+
// WL-6.6 — the channel decides WHICH signed manifest, and nothing else.
|
|
72
|
+
//
|
|
73
|
+
// `latest/` was the only path this installer could read, which left the
|
|
74
|
+
// channel prefixes provisioned in terraform unreachable by any client. The
|
|
75
|
+
// channel is resolved here and nowhere else: artifact urls come from INSIDE
|
|
76
|
+
// the manifest and are never rewritten, because a channel in an artifact url
|
|
77
|
+
// would enter the signed message and turn every promotion into a re-signing
|
|
78
|
+
// ceremony.
|
|
79
|
+
//
|
|
80
|
+
// An unrecognised channel resolves to `stable` and reports rather than
|
|
81
|
+
// throwing. A typo must not leave a machine unable to install anything —
|
|
82
|
+
// that turns a harmless mistake into the outage this installer exists to
|
|
83
|
+
// avoid.
|
|
84
|
+
const { channel, invalid, source } = resolveFirstPartyHarnessChannel(env);
|
|
85
|
+
if (invalid) {
|
|
86
|
+
options.onProgress?.(`unknown update channel ${JSON.stringify(invalid)} in ${source ?? FIRST_PARTY_HARNESS_ENV.channel.name}; ` +
|
|
87
|
+
`using ${channel}. Valid: ${FIRST_PARTY_HARNESS_CHANNELS.join(', ')}.`);
|
|
88
|
+
}
|
|
89
|
+
// Every place the manifest may be published, canonical spelling first.
|
|
90
|
+
//
|
|
91
|
+
// Which spellings depends on what is read. A CHANNEL pointer is current state,
|
|
92
|
+
// so only the accepted spellings are tried: a former spelling's pointer stopped
|
|
93
|
+
// moving when publishing stopped writing it, and falling back to one would
|
|
94
|
+
// install whatever it named on the day of contract. A PINNED release is an
|
|
95
|
+
// immutable artifact, so former spellings are tried too: a release published
|
|
96
|
+
// before the canonical prefix existed lives only under the old one, with the
|
|
97
|
+
// same signed bytes it would have anywhere.
|
|
98
|
+
//
|
|
99
|
+
// Move to the next candidate ONLY when this one is not there. A candidate that
|
|
100
|
+
// answers with a manifest ends the search, whatever that manifest turns out to
|
|
101
|
+
// be: falling through on a malformed or badly signed canonical manifest would
|
|
102
|
+
// let anyone who can make the canonical object fail verification steer the
|
|
103
|
+
// install onto an older spelling's feed — a downgrade by denial.
|
|
104
|
+
//
|
|
105
|
+
// "Not there" is 403 as well as 404. A prefix with no manifest answers 403 (the
|
|
106
|
+
// bucket denies ListBucket, so a missing key cannot be reported as 404).
|
|
107
|
+
const notFound = [];
|
|
108
|
+
let body;
|
|
109
|
+
for (const candidate of firstPartyHarnessFeedCandidates(env, {
|
|
110
|
+
pinnedRelease: Boolean(options.version),
|
|
111
|
+
})) {
|
|
112
|
+
const url = resolveChannelManifestUrl({
|
|
113
|
+
base: candidate.base,
|
|
114
|
+
manifestFilename: candidate.manifestFilename,
|
|
115
|
+
channel,
|
|
116
|
+
pinnedVersion: options.version ?? null,
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const res = await doFetch(url, {
|
|
120
|
+
signal: fetchSignal(options, options.manifestTimeoutMs ?? MANIFEST_FETCH_TIMEOUT_MS),
|
|
121
|
+
redirect: 'follow',
|
|
122
|
+
});
|
|
123
|
+
if (res.status === 403 || res.status === 404) {
|
|
124
|
+
notFound.push(`HTTP ${res.status} for ${url}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
return fail('feed-unreachable', `manifest fetch failed: HTTP ${res.status} for ${url}`);
|
|
129
|
+
}
|
|
130
|
+
body = await res.text();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return fail('feed-unreachable', `manifest fetch failed: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (body === undefined) {
|
|
138
|
+
// Name the CHANNEL when the object simply is not there anywhere.
|
|
139
|
+
//
|
|
140
|
+
// A 403 reads as "you are not allowed" — the one thing it does not mean,
|
|
141
|
+
// since this feed is public. A user on the default channel with nothing
|
|
142
|
+
// published yet would otherwise see a permissions error for a
|
|
143
|
+
// release-management state, and go looking at their credentials. A PINNED
|
|
144
|
+
// version keeps the raw failure: `releases/<v>/` missing means that exact
|
|
145
|
+
// release does not exist, and a channel setting has nothing to do with it.
|
|
146
|
+
const tried = notFound.join('; ');
|
|
147
|
+
if (!options.version) {
|
|
148
|
+
return fail('feed-unreachable', `no ${FIRST_PARTY_HARNESS.displayName} release published on the '${channel}' channel (${tried}). ` +
|
|
149
|
+
`Set ${FIRST_PARTY_HARNESS_ENV.channel.name} to a channel that has one, or pin a version.`);
|
|
150
|
+
}
|
|
151
|
+
return fail('feed-unreachable', `manifest fetch failed: ${tried}`);
|
|
152
|
+
}
|
|
153
|
+
let manifest;
|
|
154
|
+
try {
|
|
155
|
+
manifest = JSON.parse(body);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return fail('manifest-malformed', `manifest is not JSON: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
if (!manifest ||
|
|
161
|
+
typeof manifest.version !== 'string' ||
|
|
162
|
+
!manifest.platforms ||
|
|
163
|
+
typeof manifest.platforms !== 'object' ||
|
|
164
|
+
Array.isArray(manifest.platforms) ||
|
|
165
|
+
compareReleaseVersions(manifest.version, manifest.version) === null) {
|
|
166
|
+
return fail('manifest-malformed', 'manifest is missing version or platforms');
|
|
167
|
+
}
|
|
168
|
+
const verdict = verifyManifest(manifest, keys);
|
|
169
|
+
if (!('ok' in verdict) || verdict.ok !== true) {
|
|
170
|
+
const reason = 'reason' in verdict ? verdict.reason : 'verification disabled';
|
|
171
|
+
return fail('manifest-unverified', `manifest signature rejected: ${reason}`);
|
|
172
|
+
}
|
|
173
|
+
if (options.version && manifest.version !== options.version) {
|
|
174
|
+
return fail('manifest-malformed', 'pinned manifest version does not match requested version');
|
|
175
|
+
}
|
|
176
|
+
if (compareReleaseVersions(manifest.minimum, manifest.version) === null ||
|
|
177
|
+
compareReleaseVersions(manifest.minimum, manifest.version) > 0) {
|
|
178
|
+
return fail('manifest-malformed', 'manifest minimum is invalid or newer than its release');
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const policy = validateEnginePolicy(manifest, options);
|
|
182
|
+
if (policy)
|
|
183
|
+
return policy;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return fail('policy-rejected', `engine release policy is malformed: ${describe(error)}`);
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, manifest };
|
|
189
|
+
}
|
|
190
|
+
/** A present policy must explicitly describe this component, never the daemon. */
|
|
191
|
+
function validateEnginePolicy(manifest, options) {
|
|
192
|
+
if (!manifest.policy)
|
|
193
|
+
return null;
|
|
194
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
195
|
+
const policy = manifest.policy;
|
|
196
|
+
const verdict = verifyReleasePolicy(policy, keys);
|
|
197
|
+
if (!('ok' in verdict) || !verdict.ok)
|
|
198
|
+
return fail('policy-rejected', 'engine release policy signature/shape rejected');
|
|
199
|
+
const component = [FIRST_PARTY_HARNESS.provider, 'firstPartyHarness'].find((name) => policy.components[name]);
|
|
200
|
+
if (!component)
|
|
201
|
+
return fail('policy-rejected', 'release policy has no first-party engine component');
|
|
202
|
+
const assessment = assessReleasePolicy({
|
|
203
|
+
policy,
|
|
204
|
+
targetDaemonVersion: manifest.version,
|
|
205
|
+
targetComponent: component,
|
|
206
|
+
channel: resolveFirstPartyHarnessChannel(options.env).channel,
|
|
207
|
+
contracts: options.contracts ?? {},
|
|
208
|
+
cohortSubject: options.cohortSubject ?? '',
|
|
209
|
+
autonomous: options.autonomous === true,
|
|
210
|
+
now: options.now,
|
|
211
|
+
});
|
|
212
|
+
if (!assessment.valid || !assessment.eligible)
|
|
213
|
+
return fail('policy-rejected', assessment.reason ?? 'engine release is not eligible for this policy');
|
|
214
|
+
const minimum = policy.components[component].minimumVersion;
|
|
215
|
+
if (minimum &&
|
|
216
|
+
(compareReleaseVersions(manifest.version, minimum) === null ||
|
|
217
|
+
compareReleaseVersions(manifest.version, minimum) < 0))
|
|
218
|
+
return fail('minimum-version', 'engine release is below its signed policy minimum');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function describe(error) {
|
|
222
|
+
return error instanceof Error ? error.message : String(error);
|
|
223
|
+
}
|
|
224
|
+
function paths(env) {
|
|
225
|
+
const home = firstPartyHarnessHomeDir(env);
|
|
226
|
+
return {
|
|
227
|
+
home,
|
|
228
|
+
releases: path.join(home, 'releases'),
|
|
229
|
+
current: path.join(home, 'current'),
|
|
230
|
+
previous: path.join(home, 'previous'),
|
|
231
|
+
state: path.join(home, 'update-state.json'),
|
|
232
|
+
floor: path.join(home, 'minimum-manifest.json'),
|
|
233
|
+
lock: path.join(path.dirname(home), `.${FIRST_PARTY_HARNESS.provider}-update`),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function prepareLock(env) {
|
|
237
|
+
const target = paths(env).lock;
|
|
238
|
+
mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
239
|
+
writeFileSync(target, '', { flag: 'a', mode: 0o600 });
|
|
240
|
+
return target;
|
|
241
|
+
}
|
|
242
|
+
function atomicJson(file, data) {
|
|
243
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
244
|
+
const staging = `${file}.tmp-${randomUUID()}`;
|
|
245
|
+
try {
|
|
246
|
+
writeFileSync(staging, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
247
|
+
renameSync(staging, file);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
rmSync(staging, { force: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function atomicLink(target, link) {
|
|
254
|
+
const staging = `${link}.tmp-${randomUUID()}`;
|
|
255
|
+
try {
|
|
256
|
+
symlinkSync(target, staging);
|
|
257
|
+
renameSync(staging, link);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
rmSync(staging, { force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function readLink(file) {
|
|
264
|
+
try {
|
|
265
|
+
return readlinkSync(file);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function hasEntry(file) {
|
|
272
|
+
try {
|
|
273
|
+
lstatSync(file);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function isManagedFirstPartyHarnessReleasePath(binaryPath, env = process.env) {
|
|
281
|
+
const canonical = firstPartyHarnessBinaryPath(env);
|
|
282
|
+
return (path.resolve(binaryPath) === path.resolve(canonical) &&
|
|
283
|
+
readLink(canonical) === `../current/${path.basename(canonical)}`);
|
|
284
|
+
}
|
|
285
|
+
/** Read a single immutable binary/sidecar pair, never two different `current` targets. */
|
|
286
|
+
export function getVerifiedInstalledFirstPartyHarness(options = {}) {
|
|
287
|
+
const canonical = firstPartyHarnessBinaryPath(options.env);
|
|
288
|
+
return inspectBinary(canonical, options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS, firstPartyHarnessPlatformKey(options.platform, options.arch));
|
|
289
|
+
}
|
|
290
|
+
function inspectBinary(binaryPath, keys, expectedPlatform = firstPartyHarnessPlatformKey()) {
|
|
291
|
+
const result = {
|
|
292
|
+
binaryPath,
|
|
293
|
+
version: null,
|
|
294
|
+
verified: false,
|
|
295
|
+
exists: existsSync(binaryPath),
|
|
296
|
+
provenance: null,
|
|
297
|
+
};
|
|
298
|
+
if (!result.exists || !keys.length)
|
|
299
|
+
return result;
|
|
300
|
+
try {
|
|
301
|
+
const resolved = realpathSync(binaryPath);
|
|
302
|
+
const size = statSync(resolved).size;
|
|
303
|
+
if (size > MAX_ARTIFACT_BYTES)
|
|
304
|
+
return result;
|
|
305
|
+
const record = JSON.parse(readFileSync(resolved + PROVENANCE_SUFFIX, 'utf8'));
|
|
306
|
+
if (!record ||
|
|
307
|
+
compareReleaseVersions(record.version, record.version) === null ||
|
|
308
|
+
typeof record.platform !== 'string' ||
|
|
309
|
+
typeof record.sha256 !== 'string' ||
|
|
310
|
+
typeof record.url !== 'string' ||
|
|
311
|
+
typeof record.sig !== 'string')
|
|
312
|
+
return result;
|
|
313
|
+
record.sha256 = record.sha256.toLowerCase();
|
|
314
|
+
result.provenance = record;
|
|
315
|
+
if (record.platform !== expectedPlatform)
|
|
316
|
+
return result;
|
|
317
|
+
const verdict = verifyArtifact(record.platform, record.version, { ...record, file: path.basename(binaryPath), size }, keys);
|
|
318
|
+
if (!('ok' in verdict) ||
|
|
319
|
+
!verdict.ok ||
|
|
320
|
+
createHash('sha256').update(readFileSync(resolved)).digest('hex') !==
|
|
321
|
+
record.sha256.toLowerCase())
|
|
322
|
+
return result;
|
|
323
|
+
result.version = record.version;
|
|
324
|
+
result.verified = true;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
/* absent/malformed/tampered provenance is not a version authority */
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
export function readFirstPartyHarnessUpdateState(env = process.env) {
|
|
332
|
+
try {
|
|
333
|
+
const value = JSON.parse(readFileSync(paths(env).state, 'utf8'));
|
|
334
|
+
if (!value ||
|
|
335
|
+
!['install', 'rollback'].includes(value.operation) ||
|
|
336
|
+
!['running', 'succeeded', 'failed'].includes(value.status) ||
|
|
337
|
+
!Number.isFinite(value.attemptedAt))
|
|
338
|
+
return null;
|
|
339
|
+
// A killed updater cannot finalize its durable running record. Diagnose it
|
|
340
|
+
// without writing during a status/check command; live lock heartbeats keep
|
|
341
|
+
// long downloads distinguishable from interrupted transactions.
|
|
342
|
+
if (value.status === 'running' &&
|
|
343
|
+
!lockfile.checkSync(paths(env).lock, { realpath: false, stale: LOCK_STALE_MS })) {
|
|
344
|
+
return {
|
|
345
|
+
...value,
|
|
346
|
+
status: 'failed',
|
|
347
|
+
code: 'install-failed',
|
|
348
|
+
error: 'The previous update was interrupted. Run `skrr code update` to retry.',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function finishState(env, operation, attemptedAt, result, targetVersion, keys = FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS) {
|
|
358
|
+
atomicJson(paths(env).state, {
|
|
359
|
+
operation,
|
|
360
|
+
status: result.ok ? 'succeeded' : 'failed',
|
|
361
|
+
attemptedAt,
|
|
362
|
+
completedAt: Date.now(),
|
|
363
|
+
targetVersion,
|
|
364
|
+
installedVersion: result.version ?? getVerifiedInstalledFirstPartyHarness({ env, keys }).version ?? undefined,
|
|
365
|
+
code: result.code,
|
|
366
|
+
error: result.error,
|
|
367
|
+
changed: result.changed,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function retainedManifest(env, keys) {
|
|
371
|
+
try {
|
|
372
|
+
const manifest = JSON.parse(readFileSync(paths(env).floor, 'utf8'));
|
|
373
|
+
const verdict = verifyManifest(manifest, keys);
|
|
374
|
+
if (!('ok' in verdict) ||
|
|
375
|
+
!verdict.ok ||
|
|
376
|
+
compareReleaseVersions(manifest.minimum, manifest.version) === null ||
|
|
377
|
+
compareReleaseVersions(manifest.minimum, manifest.version) > 0)
|
|
378
|
+
throw new Error('retained minimum manifest is invalid; refusing to discard the signed floor');
|
|
379
|
+
return manifest;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (hasEntry(paths(env).floor))
|
|
383
|
+
throw error;
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function effectiveMinimum(manifest, keys) {
|
|
388
|
+
let minimum = manifest.minimum;
|
|
389
|
+
if (manifest.policy) {
|
|
390
|
+
const policy = manifest.policy;
|
|
391
|
+
const verdict = verifyReleasePolicy(policy, keys);
|
|
392
|
+
if (!('ok' in verdict) || !verdict.ok)
|
|
393
|
+
throw new Error('retained signed policy is invalid');
|
|
394
|
+
const component = [FIRST_PARTY_HARNESS.provider, 'firstPartyHarness'].find((name) => policy.components[name]);
|
|
395
|
+
const floor = component ? policy.components[component].minimumVersion : undefined;
|
|
396
|
+
if (floor && compareReleaseVersions(floor, minimum) === null)
|
|
397
|
+
throw new Error('signed policy minimum is invalid');
|
|
398
|
+
if (floor && compareReleaseVersions(floor, minimum) === 1)
|
|
399
|
+
minimum = floor;
|
|
400
|
+
}
|
|
401
|
+
return minimum;
|
|
402
|
+
}
|
|
403
|
+
function retainedMinimum(env, keys) {
|
|
404
|
+
const manifest = retainedManifest(env, keys);
|
|
405
|
+
return manifest ? effectiveMinimum(manifest, keys) : null;
|
|
406
|
+
}
|
|
407
|
+
function preserveMinimum(env, keys, manifest) {
|
|
408
|
+
if (!manifest)
|
|
409
|
+
return;
|
|
410
|
+
const previous = retainedManifest(env, keys);
|
|
411
|
+
if (!previous ||
|
|
412
|
+
compareReleaseVersions(effectiveMinimum(manifest, keys), effectiveMinimum(previous, keys)) === 1)
|
|
413
|
+
atomicJson(paths(env).floor, manifest);
|
|
414
|
+
}
|
|
415
|
+
function createAliases(env) {
|
|
416
|
+
const canonical = firstPartyHarnessBinaryPath(env);
|
|
417
|
+
for (const spelling of firstPartyHarnessArtifactSpellings()) {
|
|
418
|
+
const alias = path.join(path.dirname(canonical), firstPartyHarnessBinaryName(spelling));
|
|
419
|
+
if (alias === canonical)
|
|
420
|
+
continue;
|
|
421
|
+
for (const suffix of [PROVENANCE_SUFFIX, '']) {
|
|
422
|
+
if (!hasEntry(alias + suffix)) {
|
|
423
|
+
try {
|
|
424
|
+
symlinkSync(path.basename(canonical) + suffix, alias + suffix);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
/* compatibility aliases must not invalidate an activated release */
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function stableLinks(env) {
|
|
434
|
+
const binary = firstPartyHarnessBinaryPath(env);
|
|
435
|
+
mkdirSync(path.dirname(binary), { recursive: true, mode: 0o700 });
|
|
436
|
+
for (const suffix of [PROVENANCE_SUFFIX, '']) {
|
|
437
|
+
const target = `../current/${path.basename(binary)}${suffix}`;
|
|
438
|
+
if (readLink(binary + suffix) !== target)
|
|
439
|
+
atomicLink(target, binary + suffix);
|
|
440
|
+
}
|
|
441
|
+
if (hasEntry(paths(env).previous)) {
|
|
442
|
+
for (const suffix of [PROVENANCE_SUFFIX, '']) {
|
|
443
|
+
const target = `../previous/${path.basename(binary)}${suffix}`;
|
|
444
|
+
if (readLink(binary + PREVIOUS_SUFFIX + suffix) !== target)
|
|
445
|
+
atomicLink(target, binary + PREVIOUS_SUFFIX + suffix);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
createAliases(env);
|
|
449
|
+
}
|
|
450
|
+
function migrateFlatLayout(env, _progress) {
|
|
451
|
+
const p = paths(env);
|
|
452
|
+
const binary = firstPartyHarnessBinaryPath(env);
|
|
453
|
+
mkdirSync(p.releases, { recursive: true, mode: 0o700 });
|
|
454
|
+
if (isManagedFirstPartyHarnessReleasePath(binary, env) || !existsSync(binary))
|
|
455
|
+
return;
|
|
456
|
+
// Adopt flat bytes without pretending an unsigned legacy binary is trusted.
|
|
457
|
+
// Copies leave the original runnable throughout the one-time layout change.
|
|
458
|
+
const dir = path.join(p.releases, `legacy-${randomUUID()}`);
|
|
459
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
460
|
+
const adopted = path.join(dir, path.basename(binary));
|
|
461
|
+
copyFileSync(binary, adopted);
|
|
462
|
+
chmodSync(adopted, 0o555);
|
|
463
|
+
if (existsSync(binary + PROVENANCE_SUFFIX)) {
|
|
464
|
+
copyFileSync(binary + PROVENANCE_SUFFIX, adopted + PROVENANCE_SUFFIX);
|
|
465
|
+
chmodSync(adopted + PROVENANCE_SUFFIX, 0o444);
|
|
466
|
+
}
|
|
467
|
+
chmodSync(dir, 0o700);
|
|
468
|
+
const outgoing = readLink(p.current);
|
|
469
|
+
if (outgoing)
|
|
470
|
+
atomicLink(outgoing, p.previous);
|
|
471
|
+
atomicLink(path.relative(p.home, dir), p.current);
|
|
472
|
+
stableLinks(env);
|
|
473
|
+
}
|
|
474
|
+
function validateArtifact(options) {
|
|
475
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
476
|
+
const record = options.provenance;
|
|
477
|
+
if (!record ||
|
|
478
|
+
typeof record !== 'object' ||
|
|
479
|
+
typeof record.version !== 'string' ||
|
|
480
|
+
typeof record.platform !== 'string' ||
|
|
481
|
+
typeof record.sha256 !== 'string' ||
|
|
482
|
+
!/^[a-fA-F0-9]{64}$/.test(record.sha256) ||
|
|
483
|
+
typeof record.url !== 'string' ||
|
|
484
|
+
typeof record.sig !== 'string' ||
|
|
485
|
+
!Buffer.isBuffer(options.binary))
|
|
486
|
+
return fail('artifact-unverified', 'artifact provenance is malformed');
|
|
487
|
+
if (!keys.length)
|
|
488
|
+
return fail('no-pinned-keys', 'no pinned engine release keys; refusing activation');
|
|
489
|
+
if (compareReleaseVersions(record.version, record.version) === null)
|
|
490
|
+
return fail('version-invalid', 'artifact version is not a semantic release version');
|
|
491
|
+
if (record.platform !== firstPartyHarnessPlatformKey(options.platform, options.arch))
|
|
492
|
+
return fail('unsupported-platform', `artifact ${record.platform} does not match this host`);
|
|
493
|
+
if ((options.platform ?? process.platform) === 'win32')
|
|
494
|
+
return fail('unsupported-platform', 'managed engine release activation currently requires macOS or Linux');
|
|
495
|
+
const verdict = verifyArtifact(record.platform, record.version, {
|
|
496
|
+
...record,
|
|
497
|
+
file: firstPartyHarnessBinaryName(),
|
|
498
|
+
size: options.binary.length,
|
|
499
|
+
}, keys);
|
|
500
|
+
if (!('ok' in verdict) || !verdict.ok)
|
|
501
|
+
return fail('artifact-unverified', 'artifact provenance signature rejected');
|
|
502
|
+
if (options.binary.length > MAX_ARTIFACT_BYTES)
|
|
503
|
+
return fail('too-large', 'artifact exceeds the download ceiling');
|
|
504
|
+
if (createHash('sha256').update(options.binary).digest('hex') !== record.sha256.toLowerCase())
|
|
505
|
+
return fail('hash-mismatch', 'artifact bytes do not match signed provenance');
|
|
506
|
+
if (options.manifest) {
|
|
507
|
+
const policyFailure = validateEnginePolicy(options.manifest, options);
|
|
508
|
+
if (policyFailure)
|
|
509
|
+
return policyFailure;
|
|
510
|
+
const manifestVerdict = verifyManifest(options.manifest, keys);
|
|
511
|
+
const artifact = options.manifest.platforms?.[record.platform];
|
|
512
|
+
if (!('ok' in manifestVerdict) ||
|
|
513
|
+
!manifestVerdict.ok ||
|
|
514
|
+
options.manifest.version !== record.version ||
|
|
515
|
+
artifact?.sha256?.toLowerCase() !== record.sha256.toLowerCase() ||
|
|
516
|
+
compareReleaseVersions(options.manifest.minimum, record.version) === null)
|
|
517
|
+
return fail('manifest-unverified', 'activation manifest does not authenticate this artifact');
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
function activateLocked(options) {
|
|
522
|
+
const env = options.env ?? process.env;
|
|
523
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
524
|
+
const progress = options.onProgress ?? (() => { });
|
|
525
|
+
const p = paths(env);
|
|
526
|
+
const binary = firstPartyHarnessBinaryPath(env);
|
|
527
|
+
const canonicalLink = readLink(binary);
|
|
528
|
+
// Older spelling links are migration inputs; arbitrary symlinks belong to
|
|
529
|
+
// an external install and are never followed for writes.
|
|
530
|
+
const legacyLinks = firstPartyHarnessArtifactSpellings().map((spelling) => firstPartyHarnessBinaryName(spelling));
|
|
531
|
+
if (canonicalLink &&
|
|
532
|
+
!isManagedFirstPartyHarnessReleasePath(binary, env) &&
|
|
533
|
+
!legacyLinks.includes(canonicalLink)) {
|
|
534
|
+
return options.preserveExternalLink
|
|
535
|
+
? {
|
|
536
|
+
ok: true,
|
|
537
|
+
changed: false,
|
|
538
|
+
binaryPath: binary,
|
|
539
|
+
reason: 'canonical engine is an externally owned symlink',
|
|
540
|
+
}
|
|
541
|
+
: fail('install-failed', 'managed engine path is an external symlink; update its owning installation');
|
|
542
|
+
}
|
|
543
|
+
if (options.preserveExternalLink && !hasEntry(binary)) {
|
|
544
|
+
for (const spelling of firstPartyHarnessArtifactSpellings()) {
|
|
545
|
+
const alias = path.join(path.dirname(binary), firstPartyHarnessBinaryName(spelling));
|
|
546
|
+
const aliasLink = readLink(alias);
|
|
547
|
+
if (aliasLink && aliasLink !== path.basename(binary))
|
|
548
|
+
return {
|
|
549
|
+
ok: true,
|
|
550
|
+
changed: false,
|
|
551
|
+
binaryPath: binary,
|
|
552
|
+
reason: `legacy engine at ${alias} is an externally owned symlink`,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const migration = migrateFirstPartyHarnessHome({ env });
|
|
557
|
+
for (const change of migration.moved)
|
|
558
|
+
progress(`engine home migration: ${change}`);
|
|
559
|
+
for (const error of migration.errors)
|
|
560
|
+
progress(`engine home migration: ${error}`);
|
|
561
|
+
const installed = getVerifiedInstalledFirstPartyHarness({
|
|
562
|
+
env,
|
|
563
|
+
keys,
|
|
564
|
+
platform: options.platform,
|
|
565
|
+
arch: options.arch,
|
|
566
|
+
});
|
|
567
|
+
const record = { ...options.provenance, sha256: options.provenance.sha256.toLowerCase() };
|
|
568
|
+
const floor = retainedMinimum(env, keys);
|
|
569
|
+
if (floor && compareReleaseVersions(record.version, floor) < 0)
|
|
570
|
+
return fail('minimum-version', `release ${record.version} is below the retained signed minimum ${floor}`);
|
|
571
|
+
if (options.manifest && compareReleaseVersions(record.version, options.manifest.minimum) < 0)
|
|
572
|
+
return fail('minimum-version', 'release is below its signed minimum');
|
|
573
|
+
if (installed.exists && !installed.verified && options.preserveUnknownInstalled)
|
|
574
|
+
return {
|
|
575
|
+
ok: true,
|
|
576
|
+
changed: false,
|
|
577
|
+
binaryPath: binary,
|
|
578
|
+
reason: 'installed engine has no verified provenance',
|
|
579
|
+
};
|
|
580
|
+
if (installed.version) {
|
|
581
|
+
const comparison = compareReleaseVersions(record.version, installed.version);
|
|
582
|
+
if (comparison === 0 && installed.provenance?.sha256 !== record.sha256)
|
|
583
|
+
return fail('manifest-malformed', 'release version is immutable but signed artifact digest changed');
|
|
584
|
+
if (comparison === 0 || (comparison < 0 && !options.allowDowngrade)) {
|
|
585
|
+
preserveMinimum(env, keys, options.manifest);
|
|
586
|
+
return {
|
|
587
|
+
ok: true,
|
|
588
|
+
changed: false,
|
|
589
|
+
replaced: true,
|
|
590
|
+
version: installed.version,
|
|
591
|
+
platform: installed.provenance?.platform,
|
|
592
|
+
binaryPath: binary,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
migrateFlatLayout(env, progress);
|
|
597
|
+
const slot = path.join(p.releases, `${record.version}-${record.sha256.toLowerCase()}`);
|
|
598
|
+
const slotBinary = path.join(slot, path.basename(binary));
|
|
599
|
+
if (hasEntry(slot)) {
|
|
600
|
+
if (lstatSync(slot).isSymbolicLink())
|
|
601
|
+
return fail('install-failed', 'release directory must be locally owned, not a symlink');
|
|
602
|
+
const existing = inspectBinary(slotBinary, keys, firstPartyHarnessPlatformKey(options.platform, options.arch));
|
|
603
|
+
if (!existing.verified || existing.provenance?.sha256 !== record.sha256)
|
|
604
|
+
return fail('install-failed', 'immutable release directory already exists with invalid bytes');
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
const staging = path.join(p.releases, `.staging-${randomUUID()}`);
|
|
608
|
+
mkdirSync(staging, { mode: 0o700 });
|
|
609
|
+
try {
|
|
610
|
+
writeFileSync(path.join(staging, path.basename(binary)), options.binary, { mode: 0o755 });
|
|
611
|
+
writeFileSync(path.join(staging, path.basename(binary) + PROVENANCE_SUFFIX), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
612
|
+
chmodSync(path.join(staging, path.basename(binary)), 0o555);
|
|
613
|
+
chmodSync(path.join(staging, path.basename(binary) + PROVENANCE_SUFFIX), 0o444);
|
|
614
|
+
chmodSync(staging, 0o700);
|
|
615
|
+
renameSync(staging, slot);
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
if (hasEntry(staging)) {
|
|
619
|
+
chmodSync(staging, 0o700);
|
|
620
|
+
rmSync(staging, { recursive: true, force: true });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const previous = readLink(p.current);
|
|
625
|
+
const target = path.relative(p.home, slot);
|
|
626
|
+
if (previous && previous !== target)
|
|
627
|
+
atomicLink(previous, p.previous);
|
|
628
|
+
// Prepare the stable entry points while the original release still runs.
|
|
629
|
+
// Subsequent installations only change this ONE pointer.
|
|
630
|
+
stableLinks(env);
|
|
631
|
+
preserveMinimum(env, keys, options.manifest);
|
|
632
|
+
atomicLink(target, p.current);
|
|
633
|
+
progress(`${FIRST_PARTY_HARNESS.displayName} ${record.version} installed; existing sessions keep their current release.`);
|
|
634
|
+
return {
|
|
635
|
+
ok: true,
|
|
636
|
+
changed: true,
|
|
637
|
+
replaced: installed.exists,
|
|
638
|
+
version: record.version,
|
|
639
|
+
platform: record.platform,
|
|
640
|
+
binaryPath: binary,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/** Desktop's already-downloaded signed artifact uses the exact same transaction. */
|
|
644
|
+
export function activateVerifiedFirstPartyHarnessArtifact(options) {
|
|
645
|
+
const invalid = validateArtifact(options);
|
|
646
|
+
const env = options.env ?? process.env;
|
|
647
|
+
const attemptedAt = Date.now();
|
|
648
|
+
let release;
|
|
649
|
+
try {
|
|
650
|
+
release = lockfile.lockSync(prepareLock(env), { realpath: false, stale: LOCK_STALE_MS });
|
|
651
|
+
const result = invalid ?? activateLocked(options);
|
|
652
|
+
finishState(env, 'install', attemptedAt, result, options.provenance?.version, options.keys);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
const result = fail(error.code === 'ELOCKED' ? 'lock-busy' : 'install-failed', describe(error));
|
|
657
|
+
if (release) {
|
|
658
|
+
try {
|
|
659
|
+
finishState(env, 'install', attemptedAt, result, options.provenance?.version, options.keys);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// Preserve the activation failure if its diagnostic journal is also unwritable.
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
release?.();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** Signed-manifest check reads only; no lock, migration, state writes or login. */
|
|
672
|
+
export async function checkFirstPartyHarnessUpdate(options = {}) {
|
|
673
|
+
const fetched = await fetchVerifiedManifest(options);
|
|
674
|
+
if (!fetched.ok)
|
|
675
|
+
return fetched;
|
|
676
|
+
const env = options.env ?? process.env;
|
|
677
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
678
|
+
const platform = firstPartyHarnessPlatformKey(options.platform, options.arch);
|
|
679
|
+
if (!platform || (options.platform ?? process.platform) === 'win32')
|
|
680
|
+
return fail('unsupported-platform', 'this host has no managed engine artifact');
|
|
681
|
+
const artifact = fetched.manifest.platforms[platform];
|
|
682
|
+
if (!artifact)
|
|
683
|
+
return fail('artifact-missing', `release has no ${platform} artifact`);
|
|
684
|
+
const verdict = verifyArtifact(platform, fetched.manifest.version, artifact, keys);
|
|
685
|
+
if (!('ok' in verdict) || !verdict.ok)
|
|
686
|
+
return fail('artifact-unverified', 'offered artifact signature rejected');
|
|
687
|
+
let minimum = effectiveMinimum(fetched.manifest, keys);
|
|
688
|
+
try {
|
|
689
|
+
const floor = retainedMinimum(env, keys);
|
|
690
|
+
if (floor && compareReleaseVersions(fetched.manifest.version, floor) < 0)
|
|
691
|
+
return fail('minimum-version', 'offered release is below the retained signed minimum');
|
|
692
|
+
if (floor && compareReleaseVersions(floor, minimum) === 1)
|
|
693
|
+
minimum = floor;
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
return fail('minimum-version', describe(error));
|
|
697
|
+
}
|
|
698
|
+
const installed = getVerifiedInstalledFirstPartyHarness(options);
|
|
699
|
+
if (installed.version === fetched.manifest.version &&
|
|
700
|
+
installed.provenance?.sha256 !== artifact.sha256.toLowerCase())
|
|
701
|
+
return fail('manifest-malformed', 'same release version offers a different signed artifact digest');
|
|
702
|
+
const override = readFirstPartyHarnessEnv(env, 'path').value;
|
|
703
|
+
return {
|
|
704
|
+
ok: true,
|
|
705
|
+
installedVersion: installed.version,
|
|
706
|
+
latestVersion: fetched.manifest.version,
|
|
707
|
+
channel: resolveFirstPartyHarnessChannel(env).channel,
|
|
708
|
+
updateAvailable: !installed.version ||
|
|
709
|
+
compareReleaseVersions(installed.version, fetched.manifest.version) === -1,
|
|
710
|
+
binaryPath: installed.binaryPath,
|
|
711
|
+
owner: override && path.resolve(override) !== path.resolve(installed.binaryPath)
|
|
712
|
+
? 'external'
|
|
713
|
+
: 'managed',
|
|
714
|
+
installedVerified: installed.verified,
|
|
715
|
+
minimumVersion: minimum,
|
|
716
|
+
lastUpdate: readFirstPartyHarnessUpdateState(env),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
export async function installFirstPartyHarness(options = {}) {
|
|
720
|
+
const env = options.env ?? process.env;
|
|
721
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
722
|
+
const progress = options.onProgress ?? (() => { });
|
|
723
|
+
const platform = firstPartyHarnessPlatformKey(options.platform, options.arch);
|
|
724
|
+
if (!platform || (options.platform ?? process.platform) === 'win32')
|
|
725
|
+
return fail('unsupported-platform', 'no managed engine activation for this platform');
|
|
726
|
+
if (options.version && compareReleaseVersions(options.version, options.version) === null)
|
|
727
|
+
return fail('version-invalid', 'requested version is not a semantic release version');
|
|
728
|
+
if (options.allowDowngrade && !options.version)
|
|
729
|
+
return fail('downgrade-refused', 'a downgrade requires an explicit pinned version');
|
|
730
|
+
const attemptedAt = Date.now();
|
|
731
|
+
let release;
|
|
732
|
+
let result;
|
|
733
|
+
try {
|
|
734
|
+
release = await lockfile.lock(prepareLock(env), {
|
|
735
|
+
realpath: false,
|
|
736
|
+
stale: LOCK_STALE_MS,
|
|
737
|
+
update: 10_000,
|
|
738
|
+
retries: { retries: 20, minTimeout: 100, maxTimeout: 500, factor: 1.2 },
|
|
739
|
+
});
|
|
740
|
+
atomicJson(paths(env).state, {
|
|
741
|
+
operation: 'install',
|
|
742
|
+
status: 'running',
|
|
743
|
+
attemptedAt,
|
|
744
|
+
targetVersion: options.version,
|
|
745
|
+
});
|
|
746
|
+
result = await downloadAndActivate();
|
|
747
|
+
finishState(env, 'install', attemptedAt, result, result.version ?? options.version, keys);
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
result = fail(error.code === 'ELOCKED' ? 'lock-busy' : 'install-failed', describe(error));
|
|
752
|
+
if (release) {
|
|
753
|
+
try {
|
|
754
|
+
finishState(env, 'install', attemptedAt, result, options.version, keys);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
/* preserve original error */
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
await release?.();
|
|
764
|
+
}
|
|
765
|
+
async function downloadAndActivate() {
|
|
766
|
+
progress('Fetching signed release manifest…');
|
|
767
|
+
const fetched = await fetchVerifiedManifest(options);
|
|
768
|
+
if (!fetched.ok)
|
|
769
|
+
return fetched;
|
|
770
|
+
const { manifest } = fetched;
|
|
771
|
+
const artifact = manifest.platforms[platform];
|
|
772
|
+
if (!artifact)
|
|
773
|
+
return fail('artifact-missing', `release ${manifest.version} has no ${platform} artifact`);
|
|
774
|
+
const verdict = verifyArtifact(platform, manifest.version, artifact, keys);
|
|
775
|
+
if (!('ok' in verdict) || !verdict.ok)
|
|
776
|
+
return fail('artifact-unverified', 'artifact signature rejected');
|
|
777
|
+
if (typeof artifact.size === 'number' && artifact.size > MAX_ARTIFACT_BYTES)
|
|
778
|
+
return fail('too-large', 'artifact declares bytes above the ceiling');
|
|
779
|
+
const installed = getVerifiedInstalledFirstPartyHarness({
|
|
780
|
+
env,
|
|
781
|
+
keys,
|
|
782
|
+
platform: options.platform,
|
|
783
|
+
arch: options.arch,
|
|
784
|
+
});
|
|
785
|
+
const floor = retainedMinimum(env, keys);
|
|
786
|
+
if (floor && compareReleaseVersions(manifest.version, floor) < 0)
|
|
787
|
+
return fail('minimum-version', `offered release is below the retained signed minimum ${floor}`);
|
|
788
|
+
if (installed.version &&
|
|
789
|
+
compareReleaseVersions(manifest.version, installed.version) < 0 &&
|
|
790
|
+
!options.allowDowngrade) {
|
|
791
|
+
return fail('downgrade-refused', `offered ${manifest.version} is older than installed ${installed.version}; pin a version and use --allow-downgrade for recovery`);
|
|
792
|
+
}
|
|
793
|
+
if (installed.version === manifest.version) {
|
|
794
|
+
if (installed.provenance?.sha256 !== artifact.sha256.toLowerCase())
|
|
795
|
+
return fail('manifest-malformed', 'same release version offers a different signed artifact digest');
|
|
796
|
+
preserveMinimum(env, keys, manifest);
|
|
797
|
+
return {
|
|
798
|
+
ok: true,
|
|
799
|
+
changed: false,
|
|
800
|
+
version: installed.version,
|
|
801
|
+
binaryPath: installed.binaryPath,
|
|
802
|
+
platform: installed.provenance?.platform,
|
|
803
|
+
replaced: true,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
progress(`Downloading ${FIRST_PARTY_HARNESS.displayName} ${manifest.version} (${platform})…`);
|
|
807
|
+
let binary;
|
|
808
|
+
try {
|
|
809
|
+
const response = await (options.fetchImpl ?? globalThis.fetch)(artifact.url, {
|
|
810
|
+
signal: fetchSignal(options, ARTIFACT_FETCH_TIMEOUT_MS),
|
|
811
|
+
redirect: 'follow',
|
|
812
|
+
});
|
|
813
|
+
if (!response.ok)
|
|
814
|
+
return fail('download-failed', `HTTP ${response.status} for ${artifact.url}`);
|
|
815
|
+
const contentLength = Number(response.headers.get('content-length'));
|
|
816
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_ARTIFACT_BYTES)
|
|
817
|
+
return fail('too-large', 'response declares bytes above the ceiling');
|
|
818
|
+
const chunks = [];
|
|
819
|
+
let size = 0;
|
|
820
|
+
if (!response.body)
|
|
821
|
+
return fail('download-failed', 'artifact response has no body');
|
|
822
|
+
const reader = response.body.getReader();
|
|
823
|
+
try {
|
|
824
|
+
while (true) {
|
|
825
|
+
const part = await reader.read();
|
|
826
|
+
if (part.done)
|
|
827
|
+
break;
|
|
828
|
+
size += part.value.byteLength;
|
|
829
|
+
if (size > MAX_ARTIFACT_BYTES) {
|
|
830
|
+
await reader.cancel();
|
|
831
|
+
return fail('too-large', 'download exceeds the ceiling');
|
|
832
|
+
}
|
|
833
|
+
chunks.push(part.value);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
reader.releaseLock();
|
|
838
|
+
}
|
|
839
|
+
binary = Buffer.concat(chunks, size);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
return fail('download-failed', `download failed: ${describe(error)}`);
|
|
843
|
+
}
|
|
844
|
+
const provenance = {
|
|
845
|
+
version: manifest.version,
|
|
846
|
+
platform: platform,
|
|
847
|
+
sha256: artifact.sha256.toLowerCase(),
|
|
848
|
+
url: artifact.url,
|
|
849
|
+
sig: artifact.sig,
|
|
850
|
+
...(artifact.sigs ? { sigs: artifact.sigs } : {}),
|
|
851
|
+
...(manifest.keyId ? { keyId: manifest.keyId } : {}),
|
|
852
|
+
};
|
|
853
|
+
const activation = { ...options, binary, provenance, manifest };
|
|
854
|
+
const invalid = validateArtifact(activation);
|
|
855
|
+
return invalid ?? activateLocked(activation);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/** Rollback validates the retained immutable release and preserves its signature. */
|
|
859
|
+
export function rollbackFirstPartyHarness(env = process.env, options = {}) {
|
|
860
|
+
const keys = options.keys ?? FIRST_PARTY_HARNESS_RELEASE_PUBLIC_KEYS;
|
|
861
|
+
const p = paths(env);
|
|
862
|
+
const attemptedAt = Date.now();
|
|
863
|
+
let release;
|
|
864
|
+
try {
|
|
865
|
+
release = lockfile.lockSync(prepareLock(env), { realpath: false, stale: LOCK_STALE_MS });
|
|
866
|
+
const result = performRollback();
|
|
867
|
+
finishState(env, 'rollback', attemptedAt, result, result.version, keys);
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
const result = fail(error.code === 'ELOCKED' ? 'lock-busy' : 'install-failed', `rollback failed: ${describe(error)}`);
|
|
872
|
+
if (release) {
|
|
873
|
+
try {
|
|
874
|
+
finishState(env, 'rollback', attemptedAt, result, undefined, keys);
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
// Preserve the rollback failure if its diagnostic journal is also unwritable.
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
finally {
|
|
883
|
+
release?.();
|
|
884
|
+
}
|
|
885
|
+
function performRollback() {
|
|
886
|
+
if (!isManagedFirstPartyHarnessReleasePath(firstPartyHarnessBinaryPath(env), env))
|
|
887
|
+
return fail('install-failed', 'active engine is outside the managed release store; install an explicitly pinned signed release before rolling back');
|
|
888
|
+
const previous = readLink(p.previous);
|
|
889
|
+
const current = readLink(p.current);
|
|
890
|
+
if (!previous ||
|
|
891
|
+
!current ||
|
|
892
|
+
!previous.startsWith('releases/') ||
|
|
893
|
+
path.resolve(p.home, previous).startsWith(p.releases + path.sep) === false)
|
|
894
|
+
return fail('install-failed', 'no managed previous release to roll back to');
|
|
895
|
+
const restored = inspectBinary(path.join(p.home, previous, path.basename(firstPartyHarnessBinaryPath(env))), keys);
|
|
896
|
+
if (!restored.verified || !restored.version)
|
|
897
|
+
return fail('artifact-unverified', 'previous release provenance is missing or invalid; install an explicitly pinned signed release for recovery');
|
|
898
|
+
const floor = retainedMinimum(env, keys);
|
|
899
|
+
if (floor && compareReleaseVersions(restored.version, floor) < 0)
|
|
900
|
+
return fail('minimum-version', `previous release is below the retained signed minimum ${floor}`);
|
|
901
|
+
// Preserve the outgoing target only after activation. A crash between these
|
|
902
|
+
// operations still leaves `current` on a complete verified release.
|
|
903
|
+
atomicLink(previous, p.current);
|
|
904
|
+
atomicLink(current, p.previous);
|
|
905
|
+
stableLinks(env);
|
|
906
|
+
return {
|
|
907
|
+
ok: true,
|
|
908
|
+
changed: true,
|
|
909
|
+
replaced: true,
|
|
910
|
+
binaryPath: firstPartyHarnessBinaryPath(env),
|
|
911
|
+
version: restored.version,
|
|
912
|
+
platform: restored.provenance?.platform,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
export function installedFirstPartyHarnessSize(env = process.env) {
|
|
917
|
+
try {
|
|
918
|
+
return statSync(firstPartyHarnessBinaryPath(env)).size;
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
}
|