@skrr-ai/cli 0.1.94 → 0.1.95

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.
Files changed (120) hide show
  1. package/README.md +153 -165
  2. package/bin/run.js +32 -0
  3. package/bin/update-npm-runner.cjs +33 -0
  4. package/bin/update-session.cjs +194 -0
  5. package/bin/update-worker.cjs +208 -0
  6. package/dist/base-command.js +0 -15
  7. package/dist/commands/code/doctor.d.ts +1 -0
  8. package/dist/commands/code/doctor.js +6 -0
  9. package/dist/commands/code/index.js +4 -0
  10. package/dist/commands/code/install.d.ts +5 -62
  11. package/dist/commands/code/install.js +86 -279
  12. package/dist/commands/code/rollback.d.ts +8 -0
  13. package/dist/commands/code/rollback.js +20 -0
  14. package/dist/commands/code/update.d.ts +8 -37
  15. package/dist/commands/code/update.js +134 -213
  16. package/dist/commands/daemon/install.js +4 -0
  17. package/dist/commands/generate-recovery-code.d.ts +13 -1
  18. package/dist/commands/generate-recovery-code.js +146 -13
  19. package/dist/commands/instructions/guide.d.ts +20 -0
  20. package/dist/commands/instructions/guide.js +84 -0
  21. package/dist/commands/instructions/install.js +57 -33
  22. package/dist/commands/instructions/setup.d.ts +14 -0
  23. package/dist/commands/instructions/setup.js +72 -0
  24. package/dist/commands/integrations/execute.js +1 -1
  25. package/dist/commands/login.d.ts +5 -6
  26. package/dist/commands/login.js +27 -11
  27. package/dist/commands/logout.js +7 -7
  28. package/dist/commands/security/overview.d.ts +11 -0
  29. package/dist/commands/security/overview.js +158 -0
  30. package/dist/commands/spaces/claim.d.ts +12 -0
  31. package/dist/commands/spaces/claim.js +42 -0
  32. package/dist/commands/spaces/ownership.d.ts +12 -0
  33. package/dist/commands/spaces/ownership.js +46 -0
  34. package/dist/commands/tasks/cancel-cascade.js +1 -1
  35. package/dist/commands/tasks/create-tree.d.ts +5 -1
  36. package/dist/commands/tasks/create-tree.js +35 -7
  37. package/dist/commands/tasks/interrupt.js +2 -2
  38. package/dist/commands/tasks/pause-schedule.js +1 -1
  39. package/dist/commands/token/list.d.ts +3 -5
  40. package/dist/commands/token/list.js +99 -57
  41. package/dist/commands/update.d.ts +14 -0
  42. package/dist/commands/update.js +61 -0
  43. package/dist/generated/managed-instruction-guides.json +9 -0
  44. package/dist/hooks/init.d.ts +5 -0
  45. package/dist/hooks/init.js +27 -0
  46. package/dist/lib/commitments.d.ts +0 -8
  47. package/dist/lib/commitments.js +0 -52
  48. package/dist/lib/daemon-installer.d.ts +2 -0
  49. package/dist/lib/daemon-installer.js +100 -24
  50. package/dist/lib/first-party-harness-doctor.js +31 -1
  51. package/dist/lib/first-party-harness-remote-install.d.ts +71 -0
  52. package/dist/lib/first-party-harness-remote-install.js +310 -0
  53. package/dist/lib/first-party-harness-remote-update.d.ts +46 -0
  54. package/dist/lib/first-party-harness-remote-update.js +242 -0
  55. package/dist/lib/first-party-harness-startup-update.d.ts +26 -0
  56. package/dist/lib/first-party-harness-startup-update.js +166 -0
  57. package/dist/lib/first-party-harness-update.d.ts +46 -0
  58. package/dist/lib/first-party-harness-update.js +289 -0
  59. package/dist/lib/first-party-harness.js +7 -4
  60. package/dist/lib/managed-instruction-local.d.ts +12 -0
  61. package/dist/lib/managed-instruction-local.js +58 -0
  62. package/dist/lib/managed-instruction-setup.d.ts +35 -0
  63. package/dist/lib/managed-instruction-setup.js +306 -0
  64. package/dist/lib/memory-sync.d.ts +1 -0
  65. package/dist/lib/memory-sync.js +279 -68
  66. package/dist/lib/platform-update-local.d.ts +2 -0
  67. package/dist/lib/platform-update-local.js +113 -0
  68. package/dist/lib/platform-update.d.ts +39 -0
  69. package/dist/lib/platform-update.js +84 -0
  70. package/dist/lib/profile-machine-identity-guard.d.ts +2 -0
  71. package/dist/lib/profile-machine-identity-guard.js +13 -0
  72. package/dist/lib/prompt.d.ts +1 -0
  73. package/dist/lib/prompt.js +4 -3
  74. package/dist/lib/task-batch.d.ts +9 -0
  75. package/dist/lib/task-batch.js +94 -17
  76. package/dist/lib/task-instruction-offer.d.ts +1 -4
  77. package/dist/lib/task-instruction-offer.js +3 -129
  78. package/dist/lib/update-check.d.ts +61 -53
  79. package/dist/lib/update-check.js +453 -314
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +3 -4
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +3 -4
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.d.ts +82 -0
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.js +105 -0
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.d.ts +110 -0
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.js +945 -0
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.d.ts +32 -0
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.js +62 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -0
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -1
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +35 -5
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.d.ts +63 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.js +169 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.d.ts +2 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.js +44 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +3 -4
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +3 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.d.ts +82 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.js +102 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.d.ts +110 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.js +923 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.js +59 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +35 -5
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.d.ts +63 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.js +163 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.d.ts +2 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.js +41 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/package.json +31 -1
  111. package/dist/node_modules/@skrr-ai/data-provider/index.js +6584 -6567
  112. package/docs/v1/README.md +28 -0
  113. package/docs/v1/api.md +55 -0
  114. package/docs/v1/changelog.md +16 -0
  115. package/docs/v1/check-examples.mjs +178 -0
  116. package/docs/v1/examples/api-readonly.mjs +144 -0
  117. package/docs/v1/operations.md +87 -0
  118. package/docs/v1/quickstart.md +71 -0
  119. package/oclif.manifest.json +46007 -45462
  120. package/package.json +13 -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
+ }