@ashlr/hub 3.2.7 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +268 -0
- package/README.md +4 -1
- package/dist/build-identity.json +2 -2
- package/dist/cli/activation.d.ts +41 -0
- package/dist/cli/activation.js +485 -0
- package/dist/cli/activation.js.map +1 -0
- package/dist/cli/audit.js +18 -0
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/backlog.d.ts +0 -5
- package/dist/cli/backlog.js +18 -0
- package/dist/cli/backlog.js.map +1 -1
- package/dist/cli/comms.js +20 -0
- package/dist/cli/comms.js.map +1 -1
- package/dist/cli/completions.d.ts +6 -1
- package/dist/cli/completions.js +35 -11
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/daemon.js +10 -3
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/demo.d.ts +0 -27
- package/dist/cli/demo.js +17 -0
- package/dist/cli/demo.js.map +1 -1
- package/dist/cli/help.js +19 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inbox.d.ts +14 -2
- package/dist/cli/inbox.js +137 -23
- package/dist/cli/inbox.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/onboard.d.ts +0 -13
- package/dist/cli/onboard.js +20 -0
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/resources.js +15 -0
- package/dist/cli/resources.js.map +1 -1
- package/dist/cli/run.js +1 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/sandbox.js +43 -0
- package/dist/cli/sandbox.js.map +1 -1
- package/dist/cli/tui.d.ts +0 -6
- package/dist/cli/tui.js +14 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/config.js +70 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/daemon/activation-permit.d.ts +268 -33
- package/dist/core/daemon/activation-permit.js +1111 -106
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/loop.d.ts +13 -1
- package/dist/core/daemon/loop.js +192 -9
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/runtime-activation-resident-start-protocol.d.ts +141 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js +462 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js.map +1 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.d.ts +27 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js +28 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.d.ts +139 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js +995 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.d.ts +36 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js +134 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-transaction.d.ts +41 -17
- package/dist/core/daemon/runtime-activation-transaction.js +80 -11
- package/dist/core/daemon/runtime-activation-transaction.js.map +1 -1
- package/dist/core/daemon/runtime-release-dependency-inventory.js +1 -0
- package/dist/core/daemon/runtime-release-dependency-inventory.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +0 -4
- package/dist/core/daemon/service-install-authority.js +18 -3
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/dashboard.d.ts +10 -0
- package/dist/core/dashboard.js +103 -28
- package/dist/core/dashboard.js.map +1 -1
- package/dist/core/effective-config.js +7 -0
- package/dist/core/effective-config.js.map +1 -1
- package/dist/core/fleet/automerge-pass.js +33 -0
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +35 -12
- package/dist/core/fleet/decisions-ledger.js.map +1 -1
- package/dist/core/fleet/external-skill-git-capture.js +9 -1
- package/dist/core/fleet/external-skill-git-capture.js.map +1 -1
- package/dist/core/fleet/judge-decision-metadata.d.ts +8 -1
- package/dist/core/fleet/judge-decision-metadata.js +14 -1
- package/dist/core/fleet/judge-decision-metadata.js.map +1 -1
- package/dist/core/fleet/manager.d.ts +15 -1
- package/dist/core/fleet/manager.js +108 -30
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit.d.ts +134 -9
- package/dist/core/fleet/post-merge-credit.js +350 -10
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +34 -1
- package/dist/core/fleet/self-improve.js +106 -1
- package/dist/core/fleet/self-improve.js.map +1 -1
- package/dist/core/fleet/skill-library.d.ts +21 -2
- package/dist/core/fleet/skill-library.js +54 -12
- package/dist/core/fleet/skill-library.js.map +1 -1
- package/dist/core/fleet/status.js +1 -0
- package/dist/core/fleet/status.js.map +1 -1
- package/dist/core/fleet/worked-ledger.d.ts +22 -0
- package/dist/core/fleet/worked-ledger.js +32 -0
- package/dist/core/fleet/worked-ledger.js.map +1 -1
- package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
- package/dist/core/foundry/automerge-scope-policy.js +9 -2
- package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
- package/dist/core/goals/store.js +18 -4
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/inbox/apply.js +5 -2
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +94 -1
- package/dist/core/inbox/merge.js +349 -16
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/integrations/github.d.ts +2 -0
- package/dist/core/integrations/github.js +2 -1
- package/dist/core/integrations/github.js.map +1 -1
- package/dist/core/learn/reflect.d.ts +42 -4
- package/dist/core/learn/reflect.js +56 -5
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/run/engines.d.ts +8 -0
- package/dist/core/run/engines.js +9 -1
- package/dist/core/run/engines.js.map +1 -1
- package/dist/core/run/learned-router.js +28 -3
- package/dist/core/run/learned-router.js.map +1 -1
- package/dist/core/run/orchestrator.d.ts +7 -2
- package/dist/core/run/orchestrator.js +86 -60
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/swarm/rollback.d.ts +11 -0
- package/dist/core/swarm/rollback.js +54 -0
- package/dist/core/swarm/rollback.js.map +1 -1
- package/dist/core/swarm/runner.js +94 -8
- package/dist/core/swarm/runner.js.map +1 -1
- package/dist/core/types.d.ts +100 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.js +105 -7
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/control.d.ts +20 -0
- package/dist/core/web/control.js +44 -32
- package/dist/core/web/control.js.map +1 -1
- package/dist/core/web/fleet-status-cache.d.ts +68 -0
- package/dist/core/web/fleet-status-cache.js +120 -0
- package/dist/core/web/fleet-status-cache.js.map +1 -0
- package/dist/core/web/public/next/assets/index-CY0U241s.css +1 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +69 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js.map +1 -0
- package/dist/core/web/public/next/index.html +15 -0
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +126 -21
- package/docs/contracts/CONTRACT-M521.md +102 -0
- package/package.json +20 -3
- package/schema/config.schema.json +800 -5
- package/node_modules/which/CHANGELOG.md +0 -166
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dormant, permit-gated stopped-release activation transaction.
|
|
3
|
+
*
|
|
4
|
+
* This module never starts, bootstraps, kickstarts, enables, or acknowledges a
|
|
5
|
+
* resident service. It may only replace the stopped launchd plist and select an
|
|
6
|
+
* already-admitted immutable release while preserving loaded=false and the
|
|
7
|
+
* exact observed disabled bit. Production trust roots are intentionally empty.
|
|
8
|
+
*
|
|
9
|
+
* The pointer operation is a host-local cooperative CAS: under Ashlr's outward
|
|
10
|
+
* and service lifecycle fences it compares the exact old symlink inode and raw
|
|
11
|
+
* target immediately before an atomic rename, then verifies the new target.
|
|
12
|
+
* It is not a kernel compare-exchange and is not a boundary against a hostile
|
|
13
|
+
* process running under the same UID.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash, createHmac, createPublicKey, sign, timingSafeEqual, verify, } from 'node:crypto';
|
|
16
|
+
import { existsSync, lstatSync, readlinkSync, realpathSync, renameSync, symlinkSync, unlinkSync, } from 'node:fs';
|
|
17
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
18
|
+
import { canonicalizeDaemonActivationValue } from './activation-permit.js';
|
|
19
|
+
import { installLaunchdPlistTransaction } from './launchd-plist-transaction.js';
|
|
20
|
+
import { activityAllowsStoppedRecovery, observeMaintenanceDefault, runtimeActivationStoppedRuntime, } from './runtime-activation-stopped-runtime.js';
|
|
21
|
+
import { readImmutablePrivateRecordPoint, writeImmutablePrivateRecord, } from '../util/immutable-private-record-store.js';
|
|
22
|
+
import { fsyncDirectory } from '../util/durability.js';
|
|
23
|
+
import { assurePrivateStoragePath } from '../util/private-storage.js';
|
|
24
|
+
import { writePrivateFileAtomically } from '../util/private-file-write.js';
|
|
25
|
+
import { readStableRegularFile } from '../util/stable-file-read.js';
|
|
26
|
+
const PROTOCOL = 'runtime-activation-stopped-consumer-v1';
|
|
27
|
+
const PERMIT_DOMAIN = 'ashlr:runtime-activation-stopped-consumer:permit:v1';
|
|
28
|
+
const PERMIT_SIGNATURE_DOMAIN = `${PERMIT_DOMAIN}\0`;
|
|
29
|
+
const JOURNAL_DOMAIN = 'ashlr:runtime-activation-stopped-consumer:journal:v1';
|
|
30
|
+
const CLAIM_DOMAIN = 'ashlr:runtime-activation-stopped-consumer:claim:v1';
|
|
31
|
+
const RECEIPT_DOMAIN = 'ashlr:runtime-activation-stopped-consumer:receipt:v1';
|
|
32
|
+
const MAX_PERMIT_BYTES = 64 * 1024;
|
|
33
|
+
const MAX_JOURNAL_BYTES = 512 * 1024;
|
|
34
|
+
const MAX_VALIDITY_MS = 120_000;
|
|
35
|
+
const MAX_FUTURE_SKEW_MS = 30_000;
|
|
36
|
+
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
37
|
+
const SHA1_RE = /^[a-f0-9]{40}$/u;
|
|
38
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
39
|
+
const KEY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
40
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
41
|
+
const MAX_RECORDS = 25_000;
|
|
42
|
+
const MAX_RECORD_BYTES = 32 * 1024;
|
|
43
|
+
export { RUNTIME_ACTIVATION_STOPPED_CONSUMER_TRUST_ROOTS, } from './runtime-activation-stopped-runtime.js';
|
|
44
|
+
function plainObject(value) {
|
|
45
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
46
|
+
return false;
|
|
47
|
+
const prototype = Object.getPrototypeOf(value);
|
|
48
|
+
return prototype === Object.prototype || prototype === null;
|
|
49
|
+
}
|
|
50
|
+
function exactKeys(value, keys) {
|
|
51
|
+
const actual = Object.keys(value).sort();
|
|
52
|
+
const expected = [...keys].sort();
|
|
53
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
54
|
+
}
|
|
55
|
+
function sha256(value) {
|
|
56
|
+
return createHash('sha256').update(value).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
function domainDigest(domain, value) {
|
|
59
|
+
return sha256(`${domain}\n${canonicalizeDaemonActivationValue(value)}`);
|
|
60
|
+
}
|
|
61
|
+
function sameDigest(left, right) {
|
|
62
|
+
return SHA256_RE.test(left) && SHA256_RE.test(right) &&
|
|
63
|
+
timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
|
|
64
|
+
}
|
|
65
|
+
function canonicalTimestamp(value) {
|
|
66
|
+
if (typeof value !== 'string')
|
|
67
|
+
return false;
|
|
68
|
+
const parsed = Date.parse(value);
|
|
69
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
70
|
+
}
|
|
71
|
+
function base64url(value, bytes) {
|
|
72
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/u.test(value))
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const decoded = Buffer.from(value, 'base64url');
|
|
76
|
+
return (bytes === undefined || decoded.length === bytes) && decoded.toString('base64url') === value
|
|
77
|
+
? decoded
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function permitSigningBytes(payload) {
|
|
85
|
+
return Buffer.from(`${PERMIT_SIGNATURE_DOMAIN}${canonicalizeDaemonActivationValue(payload)}`, 'utf8');
|
|
86
|
+
}
|
|
87
|
+
export function runtimeActivationStoppedConsumerKeyId(publicKey) {
|
|
88
|
+
const spki = publicKey.export({ format: 'der', type: 'spki' });
|
|
89
|
+
return sha256(Buffer.concat([Buffer.from(`${PERMIT_DOMAIN}:key\0`, 'utf8'), spki]));
|
|
90
|
+
}
|
|
91
|
+
export function signRuntimeActivationStoppedPermit(payload, privateKey) {
|
|
92
|
+
return {
|
|
93
|
+
payload: structuredClone(payload),
|
|
94
|
+
signature: sign(null, permitSigningBytes(payload), privateKey).toString('base64url'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parsePermit(value) {
|
|
98
|
+
if (!plainObject(value) || !exactKeys(value, ['payload', 'signature']) || !plainObject(value['payload']) ||
|
|
99
|
+
base64url(value['signature'], 64) === null)
|
|
100
|
+
return null;
|
|
101
|
+
const payload = value['payload'];
|
|
102
|
+
if (!exactKeys(payload, [
|
|
103
|
+
'schemaVersion', 'protocol', 'permitId', 'keyId', 'issuedAt', 'expiresAt', 'scope', 'bindings',
|
|
104
|
+
]) || payload['schemaVersion'] !== 1 || payload['protocol'] !== PROTOCOL ||
|
|
105
|
+
typeof payload['permitId'] !== 'string' || !UUID_RE.test(payload['permitId']) ||
|
|
106
|
+
typeof payload['keyId'] !== 'string' || !KEY_ID_RE.test(payload['keyId']) ||
|
|
107
|
+
!canonicalTimestamp(payload['issuedAt']) || !canonicalTimestamp(payload['expiresAt']) ||
|
|
108
|
+
!plainObject(payload['scope']) || !plainObject(payload['bindings']))
|
|
109
|
+
return null;
|
|
110
|
+
const scope = payload['scope'];
|
|
111
|
+
if (!exactKeys(scope, [
|
|
112
|
+
'action', 'maintenance', 'killSwitch', 'providerEffectsBlocked', 'serviceLoaded',
|
|
113
|
+
'serviceStart', 'serviceEnable', 'residentAcknowledgement',
|
|
114
|
+
]) || scope['action'] !== 'select-verified-3.2.7-stopped-release' || scope['maintenance'] !== true ||
|
|
115
|
+
scope['killSwitch'] !== 'healthy-engaged' || scope['providerEffectsBlocked'] !== true ||
|
|
116
|
+
scope['serviceLoaded'] !== false || scope['serviceStart'] !== false ||
|
|
117
|
+
scope['serviceEnable'] !== false || scope['residentAcknowledgement'] !== false)
|
|
118
|
+
return null;
|
|
119
|
+
const bindings = payload['bindings'];
|
|
120
|
+
if (!exactKeys(bindings, [
|
|
121
|
+
'activationId', 'admissionDigest', 'planDigest', 'canonicalRequestSha256',
|
|
122
|
+
'trustRootCanonicalSha256', 'candidateRevision', 'candidateVersion', 'rollbackRevision',
|
|
123
|
+
'priorCurrentTarget', 'priorPlistSha256', 'priorServiceDisabled', 'priorServiceLoaded',
|
|
124
|
+
]) || typeof bindings['activationId'] !== 'string' || !UUID_RE.test(bindings['activationId']) ||
|
|
125
|
+
![bindings['admissionDigest'], bindings['planDigest'], bindings['canonicalRequestSha256'],
|
|
126
|
+
bindings['trustRootCanonicalSha256'], bindings['priorPlistSha256']]
|
|
127
|
+
.every((entry) => typeof entry === 'string' && SHA256_RE.test(entry)) ||
|
|
128
|
+
typeof bindings['candidateRevision'] !== 'string' || !SHA1_RE.test(bindings['candidateRevision']) ||
|
|
129
|
+
bindings['candidateVersion'] !== '3.2.7' ||
|
|
130
|
+
typeof bindings['rollbackRevision'] !== 'string' || !SHA1_RE.test(bindings['rollbackRevision']) ||
|
|
131
|
+
typeof bindings['priorCurrentTarget'] !== 'string' || bindings['priorCurrentTarget'].length > 512 ||
|
|
132
|
+
typeof bindings['priorServiceDisabled'] !== 'boolean' || bindings['priorServiceLoaded'] !== false)
|
|
133
|
+
return null;
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function verifyPermit(envelope, roots, nowMs) {
|
|
137
|
+
const issued = Date.parse(envelope.payload.issuedAt);
|
|
138
|
+
const expires = Date.parse(envelope.payload.expiresAt);
|
|
139
|
+
if (!Number.isFinite(nowMs) || expires <= issued || expires - issued > MAX_VALIDITY_MS ||
|
|
140
|
+
nowMs < issued - MAX_FUTURE_SKEW_MS || nowMs >= expires)
|
|
141
|
+
return 'runtime activation stopped permit is outside its validity window';
|
|
142
|
+
const root = roots.find((entry) => entry.keyId === envelope.payload.keyId);
|
|
143
|
+
if (!root || root.algorithm !== 'ed25519' || !canonicalTimestamp(root.validFrom) ||
|
|
144
|
+
!canonicalTimestamp(root.validUntil) || nowMs < Date.parse(root.validFrom) || nowMs >= Date.parse(root.validUntil)) {
|
|
145
|
+
return 'runtime activation stopped permit signing root is unavailable';
|
|
146
|
+
}
|
|
147
|
+
const spki = base64url(root.publicKeySpki);
|
|
148
|
+
const signature = base64url(envelope.signature, 64);
|
|
149
|
+
if (!spki || !signature)
|
|
150
|
+
return 'runtime activation stopped permit signature is invalid';
|
|
151
|
+
try {
|
|
152
|
+
const key = createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
153
|
+
if (key.asymmetricKeyType !== 'ed25519' || runtimeActivationStoppedConsumerKeyId(key) !== root.keyId ||
|
|
154
|
+
!verify(null, permitSigningBytes(envelope.payload), key, signature)) {
|
|
155
|
+
return 'runtime activation stopped permit signature is invalid';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return 'runtime activation stopped permit signature is invalid';
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function permitPath(homePath, activationId) {
|
|
164
|
+
return join(homePath, '.ashlr', 'control', 'activation', 'consumer-permits', `${activationId}.json`);
|
|
165
|
+
}
|
|
166
|
+
function readPermit(homePath, activationId) {
|
|
167
|
+
const path = permitPath(homePath, activationId);
|
|
168
|
+
const read = readStableRegularFile(path, {
|
|
169
|
+
anchorPath: homePath,
|
|
170
|
+
maxFileBytes: MAX_PERMIT_BYTES,
|
|
171
|
+
remainingBytes: MAX_PERMIT_BYTES,
|
|
172
|
+
});
|
|
173
|
+
if (!read.ok)
|
|
174
|
+
throw new Error('runtime activation stopped permit is unavailable');
|
|
175
|
+
const stat = lstatSync(path, { bigint: true });
|
|
176
|
+
const assurance = assurePrivateStoragePath(path, 'file', 'inspect-owned', { anchorPath: homePath });
|
|
177
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !owned(stat) ||
|
|
178
|
+
(process.platform !== 'win32' && (stat.mode & 511n) !== 384n) || !assurance.ok) {
|
|
179
|
+
throw new Error('runtime activation stopped permit custody is unsafe');
|
|
180
|
+
}
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(read.text);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw new Error('runtime activation stopped permit is invalid');
|
|
187
|
+
}
|
|
188
|
+
const envelope = parsePermit(parsed);
|
|
189
|
+
if (!envelope || `${canonicalizeDaemonActivationValue(envelope)}\n` !== read.text) {
|
|
190
|
+
throw new Error('runtime activation stopped permit is not canonical');
|
|
191
|
+
}
|
|
192
|
+
return envelope;
|
|
193
|
+
}
|
|
194
|
+
function plistPath(homePath) {
|
|
195
|
+
return join(homePath, 'Library', 'LaunchAgents', 'ai.ashlr.daemon.plist');
|
|
196
|
+
}
|
|
197
|
+
function rawReleaseTarget(revision) {
|
|
198
|
+
return join('releases', revision);
|
|
199
|
+
}
|
|
200
|
+
function sameIdentity(left, right) {
|
|
201
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
202
|
+
}
|
|
203
|
+
function owned(stat) {
|
|
204
|
+
return typeof process.getuid !== 'function' || stat.uid === BigInt(process.getuid());
|
|
205
|
+
}
|
|
206
|
+
function safeSymlink(stat) {
|
|
207
|
+
return stat.isSymbolicLink() && stat.nlink === 1n && owned(stat);
|
|
208
|
+
}
|
|
209
|
+
function inspectCurrentPointer(path, expectedTarget) {
|
|
210
|
+
const parent = dirname(path);
|
|
211
|
+
if (!isAbsolute(path) || resolve(path) !== path || basename(path) !== 'current') {
|
|
212
|
+
throw new Error('runtime activation current pointer path is invalid');
|
|
213
|
+
}
|
|
214
|
+
const parentStat = lstatSync(parent, { bigint: true });
|
|
215
|
+
if (!parentStat.isDirectory() || parentStat.isSymbolicLink() || !owned(parentStat) ||
|
|
216
|
+
(process.platform !== 'win32' && (parentStat.mode & 18n) !== 0n) || realpathSync(parent) !== parent) {
|
|
217
|
+
throw new Error('runtime activation current pointer parent is unsafe');
|
|
218
|
+
}
|
|
219
|
+
const stat = lstatSync(path, { bigint: true });
|
|
220
|
+
if (!safeSymlink(stat) || readlinkSync(path, 'utf8') !== expectedTarget ||
|
|
221
|
+
resolve(parent, expectedTarget) !== join(parent, expectedTarget)) {
|
|
222
|
+
throw new Error('runtime activation current pointer does not match the signed prior release');
|
|
223
|
+
}
|
|
224
|
+
return stat;
|
|
225
|
+
}
|
|
226
|
+
function cooperativePointerCas(path, expectedTarget, desiredTarget) {
|
|
227
|
+
const expected = inspectCurrentPointer(path, expectedTarget);
|
|
228
|
+
const parent = dirname(path);
|
|
229
|
+
const temporary = join(parent, `.current.${process.pid}.${sha256(`${desiredTarget}:${Date.now()}`).slice(0, 24)}.tmp`);
|
|
230
|
+
let temporaryIdentity;
|
|
231
|
+
try {
|
|
232
|
+
symlinkSync(desiredTarget, temporary);
|
|
233
|
+
temporaryIdentity = lstatSync(temporary, { bigint: true });
|
|
234
|
+
if (!safeSymlink(temporaryIdentity) || readlinkSync(temporary, 'utf8') !== desiredTarget) {
|
|
235
|
+
throw new Error('runtime activation candidate pointer is unsafe');
|
|
236
|
+
}
|
|
237
|
+
fsyncDirectory(parent);
|
|
238
|
+
const current = lstatSync(path, { bigint: true });
|
|
239
|
+
if (!safeSymlink(current) || !sameIdentity(expected, current) || readlinkSync(path, 'utf8') !== expectedTarget) {
|
|
240
|
+
throw new Error('runtime activation current pointer changed before cooperative CAS');
|
|
241
|
+
}
|
|
242
|
+
renameSync(temporary, path);
|
|
243
|
+
temporaryIdentity = undefined;
|
|
244
|
+
fsyncDirectory(parent);
|
|
245
|
+
inspectCurrentPointer(path, desiredTarget);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (temporaryIdentity) {
|
|
249
|
+
try {
|
|
250
|
+
const current = lstatSync(temporary, { bigint: true });
|
|
251
|
+
if (sameIdentity(current, temporaryIdentity))
|
|
252
|
+
unlinkSync(temporary);
|
|
253
|
+
}
|
|
254
|
+
catch { /* exact temporary is absent or replaced */ }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function readExactPrivateFile(path, expectedSha256, homePath) {
|
|
259
|
+
const read = readStableRegularFile(path, {
|
|
260
|
+
anchorPath: homePath,
|
|
261
|
+
maxFileBytes: MAX_JOURNAL_BYTES,
|
|
262
|
+
remainingBytes: MAX_JOURNAL_BYTES,
|
|
263
|
+
});
|
|
264
|
+
if (!read.ok)
|
|
265
|
+
throw new Error('runtime activation prior plist is unavailable');
|
|
266
|
+
const bytes = Buffer.from(read.text, 'utf8');
|
|
267
|
+
if (!sameDigest(sha256(bytes), expectedSha256))
|
|
268
|
+
throw new Error('runtime activation prior plist digest mismatch');
|
|
269
|
+
return bytes;
|
|
270
|
+
}
|
|
271
|
+
function recordPayload(record) {
|
|
272
|
+
return { ...record, recordDigest: domainDigest(record.kind === 'claim' ? CLAIM_DOMAIN : RECEIPT_DOMAIN, record) };
|
|
273
|
+
}
|
|
274
|
+
function parseRecord(value) {
|
|
275
|
+
if (!plainObject(value) || !exactKeys(value, [
|
|
276
|
+
'schemaVersion', 'protocol', 'kind', 'permitId', 'activationId', 'admissionDigest',
|
|
277
|
+
'planDigest', 'candidateRevision', 'recordDigest',
|
|
278
|
+
]) || value['schemaVersion'] !== 1 || value['protocol'] !== PROTOCOL ||
|
|
279
|
+
(value['kind'] !== 'claim' && value['kind'] !== 'receipt') ||
|
|
280
|
+
typeof value['permitId'] !== 'string' || !UUID_RE.test(value['permitId']) ||
|
|
281
|
+
typeof value['activationId'] !== 'string' || !UUID_RE.test(value['activationId']) ||
|
|
282
|
+
typeof value['candidateRevision'] !== 'string' || !SHA1_RE.test(value['candidateRevision']) ||
|
|
283
|
+
![value['admissionDigest'], value['planDigest'], value['recordDigest']]
|
|
284
|
+
.every((entry) => typeof entry === 'string' && SHA256_RE.test(entry)))
|
|
285
|
+
return null;
|
|
286
|
+
const { recordDigest, ...unsigned } = value;
|
|
287
|
+
return sameDigest(recordDigest, domainDigest(value['kind'] === 'claim' ? CLAIM_DOMAIN : RECEIPT_DOMAIN, unsigned))
|
|
288
|
+
? value
|
|
289
|
+
: null;
|
|
290
|
+
}
|
|
291
|
+
function recordCodec() {
|
|
292
|
+
return {
|
|
293
|
+
parse: parseRecord,
|
|
294
|
+
serialize: (record) => `${canonicalizeDaemonActivationValue(record)}\n`,
|
|
295
|
+
recordId: (record) => `${record.kind}-${record.permitId}`,
|
|
296
|
+
recordFileName: (record) => `${record.kind}-${record.permitId}.json`,
|
|
297
|
+
isRecordFileName: (name) => /^(?:claim|receipt)-[0-9a-f-]{36}\.json$/iu.test(name),
|
|
298
|
+
stageToken: (record) => record.recordDigest,
|
|
299
|
+
equivalent: (left, right) => sameDigest(left.recordDigest, right.recordDigest),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function recordStore(homePath) {
|
|
303
|
+
const anchorPath = join(homePath, '.ashlr', 'control', 'activation');
|
|
304
|
+
const rootPath = join(anchorPath, 'consumer-records');
|
|
305
|
+
return {
|
|
306
|
+
label: 'runtime activation stopped consumer record',
|
|
307
|
+
anchorPath,
|
|
308
|
+
rootPath,
|
|
309
|
+
lockFileName: '.runtime-activation-stopped-consumer.lock',
|
|
310
|
+
maxRecordBytes: MAX_RECORD_BYTES,
|
|
311
|
+
defaultMaxFiles: MAX_RECORDS,
|
|
312
|
+
hardMaxFiles: MAX_RECORDS,
|
|
313
|
+
defaultMaxBytes: MAX_RECORDS * MAX_RECORD_BYTES,
|
|
314
|
+
hardMaxBytes: MAX_RECORDS * MAX_RECORD_BYTES,
|
|
315
|
+
codecForRead: recordCodec,
|
|
316
|
+
codecForWrite: recordCodec,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function journalUnsigned(value) {
|
|
320
|
+
const { recordDigest: _recordDigest, attestation: _attestation, ...unsigned } = value;
|
|
321
|
+
return unsigned;
|
|
322
|
+
}
|
|
323
|
+
function journalWithAttestation(value, key) {
|
|
324
|
+
const recordDigest = domainDigest(JOURNAL_DOMAIN, value);
|
|
325
|
+
const attestation = createHmac('sha256', key).update(`${JOURNAL_DOMAIN}\n${recordDigest}`).digest('hex');
|
|
326
|
+
return { ...value, recordDigest, attestation };
|
|
327
|
+
}
|
|
328
|
+
function verifyJournal(value, key) {
|
|
329
|
+
if (!plainObject(value))
|
|
330
|
+
return null;
|
|
331
|
+
const journal = value;
|
|
332
|
+
if (!exactKeys(value, [
|
|
333
|
+
'schemaVersion', 'protocol', 'phase', 'sequence', 'predecessorDigest', 'activationId', 'permitId',
|
|
334
|
+
'admissionDigest', 'planDigest', 'candidateRevision', 'currentPointerPath', 'priorCurrentTarget', 'candidateCurrentTarget',
|
|
335
|
+
'plistPath', 'priorPlistBase64', 'priorPlistSha256', 'candidatePlistSha256',
|
|
336
|
+
'priorServiceDisabled', 'priorServiceLoaded', 'recordDigest', 'attestation',
|
|
337
|
+
]) || journal.schemaVersion !== 1 || journal.protocol !== PROTOCOL ||
|
|
338
|
+
!['prepared', 'plist-replaced', 'pointer-switched'].includes(journal.phase) ||
|
|
339
|
+
!Number.isSafeInteger(journal.sequence) || journal.sequence < 1 ||
|
|
340
|
+
!UUID_RE.test(journal.activationId) || !UUID_RE.test(journal.permitId) || !SHA1_RE.test(journal.candidateRevision) ||
|
|
341
|
+
![journal.predecessorDigest, journal.admissionDigest, journal.planDigest, journal.priorPlistSha256,
|
|
342
|
+
journal.candidatePlistSha256, journal.recordDigest, journal.attestation]
|
|
343
|
+
.every((entry) => typeof entry === 'string' && SHA256_RE.test(entry)) ||
|
|
344
|
+
!isAbsolute(journal.currentPointerPath) || resolve(journal.currentPointerPath) !== journal.currentPointerPath ||
|
|
345
|
+
!isAbsolute(journal.plistPath) || resolve(journal.plistPath) !== journal.plistPath ||
|
|
346
|
+
typeof journal.priorCurrentTarget !== 'string' || typeof journal.candidateCurrentTarget !== 'string' ||
|
|
347
|
+
typeof journal.priorPlistBase64 !== 'string' || base64url(journal.priorPlistBase64) === null ||
|
|
348
|
+
typeof journal.priorServiceDisabled !== 'boolean' || journal.priorServiceLoaded !== false ||
|
|
349
|
+
!((journal.phase === 'prepared' && journal.sequence === 1 && /^0{64}$/u.test(journal.predecessorDigest)) ||
|
|
350
|
+
(journal.phase === 'plist-replaced' && journal.sequence === 2 && !/^0{64}$/u.test(journal.predecessorDigest)) ||
|
|
351
|
+
(journal.phase === 'pointer-switched' && journal.sequence === 3 && !/^0{64}$/u.test(journal.predecessorDigest))))
|
|
352
|
+
return null;
|
|
353
|
+
const recordDigest = domainDigest(JOURNAL_DOMAIN, journalUnsigned(journal));
|
|
354
|
+
const expected = createHmac('sha256', key).update(`${JOURNAL_DOMAIN}\n${recordDigest}`).digest('hex');
|
|
355
|
+
return sameDigest(journal.recordDigest, recordDigest) && sameDigest(journal.attestation, expected) ? journal : null;
|
|
356
|
+
}
|
|
357
|
+
function journalPath(homePath) {
|
|
358
|
+
return join(homePath, '.ashlr', 'locks', 'runtime-activation-stopped-consumer.journal.json');
|
|
359
|
+
}
|
|
360
|
+
function writeJournal(homePath, journal) {
|
|
361
|
+
const path = journalPath(homePath);
|
|
362
|
+
const temporary = `${path}.${process.pid}.${journal.sequence}.tmp`;
|
|
363
|
+
writePrivateFileAtomically(temporary, path, `${canonicalizeDaemonActivationValue(journal)}\n`, { anchorPath: homePath, label: 'runtime activation stopped consumer journal' });
|
|
364
|
+
}
|
|
365
|
+
function readJournal(homePath, key) {
|
|
366
|
+
const path = journalPath(homePath);
|
|
367
|
+
if (!existsSync(path))
|
|
368
|
+
return null;
|
|
369
|
+
const read = readStableRegularFile(path, {
|
|
370
|
+
anchorPath: homePath,
|
|
371
|
+
maxFileBytes: MAX_JOURNAL_BYTES,
|
|
372
|
+
remainingBytes: MAX_JOURNAL_BYTES,
|
|
373
|
+
});
|
|
374
|
+
if (!read.ok)
|
|
375
|
+
throw new Error('runtime activation stopped consumer journal is unsafe');
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(read.text);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
throw new Error('runtime activation stopped consumer journal is invalid');
|
|
382
|
+
}
|
|
383
|
+
const journal = verifyJournal(parsed, key);
|
|
384
|
+
if (!journal || `${canonicalizeDaemonActivationValue(journal)}\n` !== read.text) {
|
|
385
|
+
throw new Error('runtime activation stopped consumer journal authentication failed');
|
|
386
|
+
}
|
|
387
|
+
const priorBytes = base64url(journal.priorPlistBase64);
|
|
388
|
+
if (journal.currentPointerPath !== join(homePath, '.local', 'share', 'ashlr', 'current') ||
|
|
389
|
+
journal.plistPath !== plistPath(homePath) ||
|
|
390
|
+
dirname(journal.priorCurrentTarget) !== 'releases' || !SHA1_RE.test(basename(journal.priorCurrentTarget)) ||
|
|
391
|
+
dirname(journal.candidateCurrentTarget) !== 'releases' ||
|
|
392
|
+
basename(journal.candidateCurrentTarget) !== journal.candidateRevision ||
|
|
393
|
+
!priorBytes || !sameDigest(sha256(priorBytes), journal.priorPlistSha256)) {
|
|
394
|
+
throw new Error('runtime activation stopped consumer journal home binding is invalid');
|
|
395
|
+
}
|
|
396
|
+
return journal;
|
|
397
|
+
}
|
|
398
|
+
function removeJournal(homePath) {
|
|
399
|
+
const path = journalPath(homePath);
|
|
400
|
+
const stat = lstatSync(path, { bigint: true });
|
|
401
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !owned(stat) ||
|
|
402
|
+
(process.platform !== 'win32' && (stat.mode & 63n) !== 0n)) {
|
|
403
|
+
throw new Error('runtime activation stopped consumer journal changed before removal');
|
|
404
|
+
}
|
|
405
|
+
unlinkSync(path);
|
|
406
|
+
fsyncDirectory(dirname(path));
|
|
407
|
+
}
|
|
408
|
+
function replaceStoppedPlist(homePath, content, expectedDisabled, observe) {
|
|
409
|
+
installLaunchdPlistTransaction({
|
|
410
|
+
plistPath: plistPath(homePath),
|
|
411
|
+
trustedRoot: homePath,
|
|
412
|
+
content,
|
|
413
|
+
lockDir: join(homePath, '.ashlr', 'locks'),
|
|
414
|
+
operationLabel: 'runtime-activation-stopped',
|
|
415
|
+
preflight: ({ hasPrior }) => {
|
|
416
|
+
try {
|
|
417
|
+
const state = observe();
|
|
418
|
+
return hasPrior && state.loaded === false && state.disabled === expectedDisabled
|
|
419
|
+
? { ok: true, stdout: '', stderr: '', recoveryState: state }
|
|
420
|
+
: { ok: false, stdout: '', stderr: 'prior stopped launchd state or plist is not exact' };
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
unload: () => {
|
|
427
|
+
try {
|
|
428
|
+
const state = observe();
|
|
429
|
+
return state.loaded === false && state.disabled === expectedDisabled
|
|
430
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
431
|
+
: { ok: false, stdout: '', stderr: 'launchd state changed before plist replacement' };
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
load: () => {
|
|
438
|
+
try {
|
|
439
|
+
const state = observe();
|
|
440
|
+
return state.loaded === false && state.disabled === expectedDisabled
|
|
441
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
442
|
+
: { ok: false, stdout: '', stderr: 'plist replacement changed launchd stopped state' };
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
verify: () => {
|
|
449
|
+
try {
|
|
450
|
+
const state = observe();
|
|
451
|
+
return state.loaded === false && state.disabled === expectedDisabled
|
|
452
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
453
|
+
: { ok: false, stdout: '', stderr: 'launchd stopped state was not preserved' };
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
rollback: () => {
|
|
460
|
+
try {
|
|
461
|
+
const state = observe();
|
|
462
|
+
return state.loaded === false && state.disabled === expectedDisabled
|
|
463
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
464
|
+
: { ok: false, stdout: '', stderr: 'launchd stopped state cannot be restored without manager mutation' };
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
recover: () => {
|
|
471
|
+
try {
|
|
472
|
+
const state = observe();
|
|
473
|
+
return state.loaded === false && state.disabled === expectedDisabled
|
|
474
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
475
|
+
: { ok: false, stdout: '', stderr: 'launchd recovery state is no longer exact' };
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
return { ok: false, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
validateRecovery: (state) => plainObject(state) && exactKeys(state, ['loaded', 'disabled']) &&
|
|
482
|
+
state['loaded'] === false && state['disabled'] === expectedDisabled
|
|
483
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
484
|
+
: { ok: false, stdout: '', stderr: 'persisted stopped launchd state is invalid' },
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function inspectPrivateRoots(homePath, includePermitRoot = true) {
|
|
488
|
+
const paths = [
|
|
489
|
+
join(homePath, '.ashlr'),
|
|
490
|
+
join(homePath, '.ashlr', 'locks'),
|
|
491
|
+
join(homePath, '.ashlr', 'control'),
|
|
492
|
+
join(homePath, '.ashlr', 'control', 'activation'),
|
|
493
|
+
];
|
|
494
|
+
if (includePermitRoot)
|
|
495
|
+
paths.push(join(homePath, '.ashlr', 'control', 'activation', 'consumer-permits'));
|
|
496
|
+
for (const path of paths) {
|
|
497
|
+
const stat = lstatSync(path, { bigint: true });
|
|
498
|
+
const assurance = assurePrivateStoragePath(path, 'directory', 'inspect-owned', { anchorPath: homePath });
|
|
499
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !owned(stat) ||
|
|
500
|
+
(process.platform !== 'win32' && (stat.mode & 511n) !== BigInt(PRIVATE_DIRECTORY_MODE)) ||
|
|
501
|
+
realpathSync(path) !== path || !assurance.ok) {
|
|
502
|
+
throw new Error('runtime activation private control custody is unsafe');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function bindPermit(plan, envelope) {
|
|
507
|
+
const payload = plan.request.signedManifest.payload;
|
|
508
|
+
const binding = envelope.payload.bindings;
|
|
509
|
+
const admissionDigest = plan.preflight.plan.admissionDigest;
|
|
510
|
+
const planDigest = plan.preflight.plan.planDigest;
|
|
511
|
+
if (!admissionDigest || !planDigest ||
|
|
512
|
+
payload.candidate.packageVersion !== '3.2.7' || payload.candidate.releaseTag !== 'v3.2.7' ||
|
|
513
|
+
binding.activationId !== payload.planId || !sameDigest(binding.admissionDigest, admissionDigest) ||
|
|
514
|
+
!sameDigest(binding.planDigest, planDigest) ||
|
|
515
|
+
!sameDigest(binding.canonicalRequestSha256, plan.canonicalRequestSha256) ||
|
|
516
|
+
!sameDigest(binding.trustRootCanonicalSha256, plan.trustRootCanonicalSha256) ||
|
|
517
|
+
binding.candidateRevision !== payload.candidate.expectedRevision ||
|
|
518
|
+
binding.rollbackRevision !== payload.rollback.expectedRevision ||
|
|
519
|
+
binding.priorCurrentTarget !== rawReleaseTarget(payload.rollback.expectedRevision) ||
|
|
520
|
+
payload.execution.prior.currentRevision !== payload.rollback.expectedRevision ||
|
|
521
|
+
payload.execution.prior.plistSha256 === null ||
|
|
522
|
+
!sameDigest(binding.priorPlistSha256, payload.execution.prior.plistSha256)) {
|
|
523
|
+
return 'runtime activation stopped permit does not bind the exact admitted 3.2.7 plan';
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
function blocked(reason, rollbackRestored = false, permitId = null, recoveryJournalRetained = false, identity, durableOutcome = 'none', recoveryJournalObserved = false) {
|
|
528
|
+
return {
|
|
529
|
+
activationId: identity?.activationId ?? null,
|
|
530
|
+
candidateRevision: identity?.candidateRevision ?? null,
|
|
531
|
+
admissionDigest: identity?.admissionDigest ?? null,
|
|
532
|
+
planDigest: identity?.planDigest ?? null,
|
|
533
|
+
activated: false,
|
|
534
|
+
serviceStarted: false,
|
|
535
|
+
serviceEnabledChanged: false,
|
|
536
|
+
phase: 'blocked',
|
|
537
|
+
reason,
|
|
538
|
+
rollbackRestored,
|
|
539
|
+
recoveryJournalRetained,
|
|
540
|
+
recoveryJournalObserved,
|
|
541
|
+
durableOutcome,
|
|
542
|
+
permitId,
|
|
543
|
+
receiptPath: null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function settledCandidate(homePath, identity, permitId, reason, recoveryJournalObserved) {
|
|
547
|
+
return {
|
|
548
|
+
...identity,
|
|
549
|
+
activated: true,
|
|
550
|
+
serviceStarted: false,
|
|
551
|
+
serviceEnabledChanged: false,
|
|
552
|
+
phase: 'activated-stopped',
|
|
553
|
+
reason,
|
|
554
|
+
rollbackRestored: false,
|
|
555
|
+
recoveryJournalRetained: false,
|
|
556
|
+
recoveryJournalObserved,
|
|
557
|
+
durableOutcome: 'settled-candidate',
|
|
558
|
+
permitId,
|
|
559
|
+
receiptPath: join(recordStore(homePath).rootPath, 'records', `receipt-${permitId}.json`),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function recordFor(kind, plan, permitId) {
|
|
563
|
+
const payload = plan.request.signedManifest.payload;
|
|
564
|
+
return recordPayload({
|
|
565
|
+
schemaVersion: 1,
|
|
566
|
+
protocol: PROTOCOL,
|
|
567
|
+
kind,
|
|
568
|
+
permitId,
|
|
569
|
+
activationId: payload.planId,
|
|
570
|
+
admissionDigest: plan.preflight.plan.admissionDigest,
|
|
571
|
+
planDigest: plan.preflight.plan.planDigest,
|
|
572
|
+
candidateRevision: payload.candidate.expectedRevision,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function planIdentity(plan) {
|
|
576
|
+
return {
|
|
577
|
+
activationId: plan.request.signedManifest.payload.planId,
|
|
578
|
+
candidateRevision: plan.request.signedManifest.payload.candidate.expectedRevision,
|
|
579
|
+
admissionDigest: plan.preflight.plan.admissionDigest,
|
|
580
|
+
planDigest: plan.preflight.plan.planDigest,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function readExactRecord(homePath, record) {
|
|
584
|
+
const fileName = `${record.kind}-${record.permitId}.json`;
|
|
585
|
+
const point = readImmutablePrivateRecordPoint(recordStore(homePath), `${record.kind}-${record.permitId}`, fileName);
|
|
586
|
+
if (point.sourceState === 'missing')
|
|
587
|
+
return 'missing';
|
|
588
|
+
if (!point.exactReadComplete)
|
|
589
|
+
return 'degraded';
|
|
590
|
+
if (point.record === null)
|
|
591
|
+
return 'missing';
|
|
592
|
+
return point.record && recordCodec().equivalent(point.record, record) ? 'exact' : 'degraded';
|
|
593
|
+
}
|
|
594
|
+
function exactFileDigest(path, expectedSha256, homePath) {
|
|
595
|
+
const read = readStableRegularFile(path, {
|
|
596
|
+
anchorPath: homePath,
|
|
597
|
+
maxFileBytes: MAX_JOURNAL_BYTES,
|
|
598
|
+
remainingBytes: MAX_JOURNAL_BYTES,
|
|
599
|
+
});
|
|
600
|
+
return read.ok && sameDigest(sha256(Buffer.from(read.text, 'utf8')), expectedSha256);
|
|
601
|
+
}
|
|
602
|
+
function journalReceipt(journal) {
|
|
603
|
+
return recordPayload({
|
|
604
|
+
schemaVersion: 1,
|
|
605
|
+
protocol: PROTOCOL,
|
|
606
|
+
kind: 'receipt',
|
|
607
|
+
permitId: journal.permitId,
|
|
608
|
+
activationId: journal.activationId,
|
|
609
|
+
admissionDigest: journal.admissionDigest,
|
|
610
|
+
planDigest: journal.planDigest,
|
|
611
|
+
candidateRevision: journal.candidateRevision,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
function journalClaim(journal) {
|
|
615
|
+
return recordPayload({
|
|
616
|
+
schemaVersion: 1,
|
|
617
|
+
protocol: PROTOCOL,
|
|
618
|
+
kind: 'claim',
|
|
619
|
+
permitId: journal.permitId,
|
|
620
|
+
activationId: journal.activationId,
|
|
621
|
+
admissionDigest: journal.admissionDigest,
|
|
622
|
+
planDigest: journal.planDigest,
|
|
623
|
+
candidateRevision: journal.candidateRevision,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function journalIdentity(journal) {
|
|
627
|
+
return {
|
|
628
|
+
activationId: journal.activationId,
|
|
629
|
+
candidateRevision: journal.candidateRevision,
|
|
630
|
+
admissionDigest: journal.admissionDigest,
|
|
631
|
+
planDigest: journal.planDigest,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function restoreJournal(homePath, journal, observe, requireFences) {
|
|
635
|
+
requireFences();
|
|
636
|
+
if (readExactRecord(homePath, journalClaim(journal)) !== 'exact') {
|
|
637
|
+
throw new Error('runtime activation recovery claim is missing or degraded');
|
|
638
|
+
}
|
|
639
|
+
const receiptState = readExactRecord(homePath, journalReceipt(journal));
|
|
640
|
+
if (receiptState === 'degraded') {
|
|
641
|
+
throw new Error('runtime activation recovery receipt state is degraded');
|
|
642
|
+
}
|
|
643
|
+
if (receiptState === 'exact') {
|
|
644
|
+
requireFences();
|
|
645
|
+
const service = observe();
|
|
646
|
+
if (service.loaded !== false || service.disabled !== journal.priorServiceDisabled ||
|
|
647
|
+
readlinkSync(journal.currentPointerPath, 'utf8') !== journal.candidateCurrentTarget ||
|
|
648
|
+
!exactFileDigest(journal.plistPath, journal.candidatePlistSha256, homePath)) {
|
|
649
|
+
throw new Error('runtime activation settled state drifted and requires reconciliation');
|
|
650
|
+
}
|
|
651
|
+
requireFences();
|
|
652
|
+
removeJournal(homePath);
|
|
653
|
+
return 'settled';
|
|
654
|
+
}
|
|
655
|
+
const currentTarget = (() => {
|
|
656
|
+
try {
|
|
657
|
+
return readlinkSync(journal.currentPointerPath, 'utf8');
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
})();
|
|
663
|
+
if (currentTarget === journal.candidateCurrentTarget) {
|
|
664
|
+
requireFences();
|
|
665
|
+
cooperativePointerCas(journal.currentPointerPath, journal.candidateCurrentTarget, journal.priorCurrentTarget);
|
|
666
|
+
}
|
|
667
|
+
else if (currentTarget !== journal.priorCurrentTarget) {
|
|
668
|
+
throw new Error('runtime activation recovery found an interleaved current pointer');
|
|
669
|
+
}
|
|
670
|
+
const priorBytes = Buffer.from(journal.priorPlistBase64, 'base64url');
|
|
671
|
+
if (!exactFileDigest(journal.plistPath, journal.priorPlistSha256, homePath)) {
|
|
672
|
+
requireFences();
|
|
673
|
+
replaceStoppedPlist(homePath, priorBytes.toString('utf8'), journal.priorServiceDisabled, observe);
|
|
674
|
+
requireFences();
|
|
675
|
+
}
|
|
676
|
+
requireFences();
|
|
677
|
+
const restoredService = observe();
|
|
678
|
+
if (!exactFileDigest(journal.plistPath, journal.priorPlistSha256, homePath) ||
|
|
679
|
+
readlinkSync(journal.currentPointerPath, 'utf8') !== journal.priorCurrentTarget ||
|
|
680
|
+
restoredService.loaded !== false || restoredService.disabled !== journal.priorServiceDisabled) {
|
|
681
|
+
throw new Error('runtime activation recovery could not restore the exact prior stopped state');
|
|
682
|
+
}
|
|
683
|
+
requireFences();
|
|
684
|
+
removeJournal(homePath);
|
|
685
|
+
return 'restored';
|
|
686
|
+
}
|
|
687
|
+
function recoverExistingStoppedRuntimeRelease(homePath) {
|
|
688
|
+
if (!existsSync(journalPath(homePath)))
|
|
689
|
+
return null;
|
|
690
|
+
const observeLaunchd = runtimeActivationStoppedRuntime.observeLaunchd;
|
|
691
|
+
const observeMaintenance = () => runtimeActivationStoppedRuntime.observeMaintenance(homePath);
|
|
692
|
+
const acquireOutward = runtimeActivationStoppedRuntime.acquireOutward;
|
|
693
|
+
const ownsOutward = runtimeActivationStoppedRuntime.ownsOutward;
|
|
694
|
+
const releaseOutward = runtimeActivationStoppedRuntime.releaseOutward;
|
|
695
|
+
const acquireLifecycle = () => runtimeActivationStoppedRuntime.acquireLifecycle(homePath);
|
|
696
|
+
const ownsLifecycle = runtimeActivationStoppedRuntime.ownsLifecycle;
|
|
697
|
+
const releaseLifecycle = runtimeActivationStoppedRuntime.releaseLifecycle;
|
|
698
|
+
const journalKey = runtimeActivationStoppedRuntime.journalKey();
|
|
699
|
+
if (!journalKey) {
|
|
700
|
+
return blocked('runtime activation recovery journal key is unavailable', false, null, true, undefined, 'none', true);
|
|
701
|
+
}
|
|
702
|
+
let outward = null;
|
|
703
|
+
let lifecycle = null;
|
|
704
|
+
let permitId = null;
|
|
705
|
+
let journalBoundIdentity;
|
|
706
|
+
const requireFences = () => {
|
|
707
|
+
if (!outward || !lifecycle || !ownsOutward(outward) || !ownsLifecycle(lifecycle)) {
|
|
708
|
+
throw new Error('runtime activation recovery fence ownership was lost');
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
try {
|
|
712
|
+
if (journalKey.length !== 32) {
|
|
713
|
+
return blocked('runtime activation recovery journal key is invalid', false, null, true, undefined, 'none', true);
|
|
714
|
+
}
|
|
715
|
+
inspectPrivateRoots(homePath, false);
|
|
716
|
+
outward = acquireOutward();
|
|
717
|
+
if (!outward || !ownsOutward(outward))
|
|
718
|
+
throw new Error('runtime activation recovery outward fence is unavailable');
|
|
719
|
+
lifecycle = acquireLifecycle();
|
|
720
|
+
if (!lifecycle || !ownsLifecycle(lifecycle))
|
|
721
|
+
throw new Error('runtime activation recovery lifecycle fence is unavailable');
|
|
722
|
+
const maintenance = observeMaintenance();
|
|
723
|
+
if (!maintenance.ok || maintenance.daemonRoots !== 0 || maintenance.daemonDescendants !== 0) {
|
|
724
|
+
throw new Error(maintenance.reason || 'runtime activation recovery maintenance is not quiescent');
|
|
725
|
+
}
|
|
726
|
+
const journal = readJournal(homePath, journalKey);
|
|
727
|
+
if (!journal)
|
|
728
|
+
throw new Error('runtime activation recovery journal disappeared');
|
|
729
|
+
permitId = journal.permitId;
|
|
730
|
+
journalBoundIdentity = journalIdentity(journal);
|
|
731
|
+
if (!ownsOutward(outward) || !ownsLifecycle(lifecycle)) {
|
|
732
|
+
throw new Error('runtime activation recovery fence ownership was lost');
|
|
733
|
+
}
|
|
734
|
+
const service = observeLaunchd();
|
|
735
|
+
if (service.loaded !== false || service.disabled !== journal.priorServiceDisabled) {
|
|
736
|
+
throw new Error('runtime activation recovery launchd state is not exact');
|
|
737
|
+
}
|
|
738
|
+
if (!ownsOutward(outward) || !ownsLifecycle(lifecycle)) {
|
|
739
|
+
throw new Error('runtime activation recovery fence ownership was lost');
|
|
740
|
+
}
|
|
741
|
+
const recovered = restoreJournal(homePath, journal, observeLaunchd, requireFences);
|
|
742
|
+
if (recovered === 'settled') {
|
|
743
|
+
return settledCandidate(homePath, journalBoundIdentity, permitId, 'authenticated stopped-release receipt settled after restart without new mutation authority', true);
|
|
744
|
+
}
|
|
745
|
+
return blocked('authenticated stopped-release journal restored the exact prior stopped state after restart', true, permitId, false, journalBoundIdentity, 'restored-prior', true);
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
return blocked(`runtime activation recovery requires reconciliation: ${error instanceof Error ? error.message : String(error)}`, false, permitId, true, journalBoundIdentity, 'none', true);
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
if (lifecycle)
|
|
752
|
+
releaseLifecycle(lifecycle);
|
|
753
|
+
if (outward)
|
|
754
|
+
releaseOutward(outward);
|
|
755
|
+
journalKey.fill(0);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function activateVerifiedStoppedRuntimeRelease(plan, homePath, revalidateAdmission) {
|
|
759
|
+
const recovery = recoverExistingStoppedRuntimeRelease(homePath);
|
|
760
|
+
if (recovery)
|
|
761
|
+
return recovery;
|
|
762
|
+
const roots = runtimeActivationStoppedRuntime.roots;
|
|
763
|
+
if (roots.length === 0)
|
|
764
|
+
return blocked('runtime activation stopped consumer authority is unprovisioned');
|
|
765
|
+
const payload = plan.request.signedManifest.payload;
|
|
766
|
+
let envelope;
|
|
767
|
+
try {
|
|
768
|
+
envelope = readPermit(homePath, payload.planId);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
return blocked(error instanceof Error ? error.message : String(error));
|
|
772
|
+
}
|
|
773
|
+
const nowMs = runtimeActivationStoppedRuntime.nowMs();
|
|
774
|
+
const permitReason = verifyPermit(envelope, roots, nowMs) ?? bindPermit(plan, envelope);
|
|
775
|
+
if (permitReason)
|
|
776
|
+
return blocked(permitReason, false, envelope.payload.permitId);
|
|
777
|
+
const observeLaunchd = runtimeActivationStoppedRuntime.observeLaunchd;
|
|
778
|
+
const observeMaintenance = () => runtimeActivationStoppedRuntime.observeMaintenance(homePath);
|
|
779
|
+
const acquireOutward = runtimeActivationStoppedRuntime.acquireOutward;
|
|
780
|
+
const ownsOutward = runtimeActivationStoppedRuntime.ownsOutward;
|
|
781
|
+
const releaseOutward = runtimeActivationStoppedRuntime.releaseOutward;
|
|
782
|
+
const acquireLifecycle = () => runtimeActivationStoppedRuntime.acquireLifecycle(homePath);
|
|
783
|
+
const ownsLifecycle = runtimeActivationStoppedRuntime.ownsLifecycle;
|
|
784
|
+
const releaseLifecycle = runtimeActivationStoppedRuntime.releaseLifecycle;
|
|
785
|
+
let outward = null;
|
|
786
|
+
let lifecycle = null;
|
|
787
|
+
const journalKey = runtimeActivationStoppedRuntime.journalKey();
|
|
788
|
+
if (!journalKey)
|
|
789
|
+
return blocked('runtime activation stopped consumer journal key is unavailable', false, envelope.payload.permitId);
|
|
790
|
+
let mutationStarted = false;
|
|
791
|
+
let rollbackRestored = false;
|
|
792
|
+
let recoveryJournalObserved = false;
|
|
793
|
+
const fencesOwned = () => Boolean(outward && lifecycle && ownsOutward(outward) && ownsLifecycle(lifecycle));
|
|
794
|
+
const requireFences = () => {
|
|
795
|
+
if (!fencesOwned())
|
|
796
|
+
throw new Error('runtime activation fence ownership was lost');
|
|
797
|
+
};
|
|
798
|
+
try {
|
|
799
|
+
if (journalKey.length !== 32) {
|
|
800
|
+
return blocked('runtime activation stopped consumer journal key is invalid', false, envelope.payload.permitId);
|
|
801
|
+
}
|
|
802
|
+
inspectPrivateRoots(homePath);
|
|
803
|
+
outward = acquireOutward();
|
|
804
|
+
if (!outward || !ownsOutward(outward))
|
|
805
|
+
throw new Error('runtime activation outward maintenance fence is unavailable');
|
|
806
|
+
lifecycle = acquireLifecycle();
|
|
807
|
+
if (!lifecycle || !ownsLifecycle(lifecycle))
|
|
808
|
+
throw new Error('runtime activation daemon lifecycle fence is unavailable');
|
|
809
|
+
const maintenance = observeMaintenance();
|
|
810
|
+
if (!maintenance.ok || maintenance.daemonRoots !== 0 || maintenance.daemonDescendants !== 0) {
|
|
811
|
+
throw new Error(maintenance.reason || 'runtime activation maintenance is not quiescent');
|
|
812
|
+
}
|
|
813
|
+
const service = observeLaunchd();
|
|
814
|
+
if (service.loaded !== false || service.disabled !== envelope.payload.bindings.priorServiceDisabled) {
|
|
815
|
+
throw new Error('runtime activation launchd state does not match the explicit permit');
|
|
816
|
+
}
|
|
817
|
+
recoveryJournalObserved = existsSync(journalPath(homePath));
|
|
818
|
+
let existingJournal;
|
|
819
|
+
try {
|
|
820
|
+
existingJournal = readJournal(homePath, journalKey);
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
// readJournal only throws after it has observed a journal path. Preserve
|
|
824
|
+
// that fact even when the path arrived after the pre-read observation.
|
|
825
|
+
recoveryJournalObserved = true;
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
if (existingJournal)
|
|
829
|
+
recoveryJournalObserved = true;
|
|
830
|
+
if (recoveryJournalObserved && !existingJournal) {
|
|
831
|
+
throw new Error('runtime activation raced recovery journal disappeared');
|
|
832
|
+
}
|
|
833
|
+
if (existingJournal) {
|
|
834
|
+
requireFences();
|
|
835
|
+
const recovered = restoreJournal(homePath, existingJournal, observeLaunchd, requireFences);
|
|
836
|
+
if (recovered === 'settled') {
|
|
837
|
+
return settledCandidate(homePath, journalIdentity(existingJournal), existingJournal.permitId, 'authenticated raced stopped-release receipt settled without relabeling it as the supplied plan', true);
|
|
838
|
+
}
|
|
839
|
+
return blocked('authenticated raced stopped-release journal restored the exact prior stopped state', true, existingJournal.permitId, false, journalIdentity(existingJournal), 'restored-prior', true);
|
|
840
|
+
}
|
|
841
|
+
if (!revalidateAdmission())
|
|
842
|
+
throw new Error('runtime activation exact admission changed before stopped selection');
|
|
843
|
+
const pointerPath = payload.execution.currentPointerPath;
|
|
844
|
+
const priorTarget = envelope.payload.bindings.priorCurrentTarget;
|
|
845
|
+
const candidateTarget = rawReleaseTarget(payload.candidate.expectedRevision);
|
|
846
|
+
inspectCurrentPointer(pointerPath, priorTarget);
|
|
847
|
+
const priorPlist = readExactPrivateFile(plistPath(homePath), envelope.payload.bindings.priorPlistSha256, homePath);
|
|
848
|
+
const candidatePlistSha256 = sha256(plan.candidateServiceDescriptor);
|
|
849
|
+
requireFences();
|
|
850
|
+
const claim = recordFor('claim', plan, envelope.payload.permitId);
|
|
851
|
+
const claimDisposition = writeImmutablePrivateRecord(recordStore(homePath), claim);
|
|
852
|
+
if (claimDisposition !== 'recorded') {
|
|
853
|
+
throw new Error(`runtime activation stopped permit claim was ${claimDisposition}`);
|
|
854
|
+
}
|
|
855
|
+
let journal = journalWithAttestation({
|
|
856
|
+
schemaVersion: 1,
|
|
857
|
+
protocol: PROTOCOL,
|
|
858
|
+
phase: 'prepared',
|
|
859
|
+
sequence: 1,
|
|
860
|
+
predecessorDigest: '0'.repeat(64),
|
|
861
|
+
activationId: payload.planId,
|
|
862
|
+
permitId: envelope.payload.permitId,
|
|
863
|
+
admissionDigest: plan.preflight.plan.admissionDigest,
|
|
864
|
+
planDigest: plan.preflight.plan.planDigest,
|
|
865
|
+
candidateRevision: payload.candidate.expectedRevision,
|
|
866
|
+
currentPointerPath: pointerPath,
|
|
867
|
+
priorCurrentTarget: priorTarget,
|
|
868
|
+
candidateCurrentTarget: candidateTarget,
|
|
869
|
+
plistPath: plistPath(homePath),
|
|
870
|
+
priorPlistBase64: priorPlist.toString('base64url'),
|
|
871
|
+
priorPlistSha256: envelope.payload.bindings.priorPlistSha256,
|
|
872
|
+
candidatePlistSha256,
|
|
873
|
+
priorServiceDisabled: service.disabled,
|
|
874
|
+
priorServiceLoaded: false,
|
|
875
|
+
}, journalKey);
|
|
876
|
+
writeJournal(homePath, journal);
|
|
877
|
+
mutationStarted = true;
|
|
878
|
+
requireFences();
|
|
879
|
+
replaceStoppedPlist(homePath, plan.candidateServiceDescriptor, service.disabled, observeLaunchd);
|
|
880
|
+
if (!exactFileDigest(plistPath(homePath), candidatePlistSha256, homePath)) {
|
|
881
|
+
throw new Error('runtime activation candidate plist did not settle exactly');
|
|
882
|
+
}
|
|
883
|
+
journal = journalWithAttestation({
|
|
884
|
+
...journalUnsigned(journal),
|
|
885
|
+
phase: 'plist-replaced',
|
|
886
|
+
sequence: 2,
|
|
887
|
+
predecessorDigest: journal.recordDigest,
|
|
888
|
+
}, journalKey);
|
|
889
|
+
writeJournal(homePath, journal);
|
|
890
|
+
requireFences();
|
|
891
|
+
if (!revalidateAdmission())
|
|
892
|
+
throw new Error('runtime activation exact admission changed before pointer selection');
|
|
893
|
+
cooperativePointerCas(pointerPath, priorTarget, candidateTarget);
|
|
894
|
+
journal = journalWithAttestation({
|
|
895
|
+
...journalUnsigned(journal),
|
|
896
|
+
phase: 'pointer-switched',
|
|
897
|
+
sequence: 3,
|
|
898
|
+
predecessorDigest: journal.recordDigest,
|
|
899
|
+
}, journalKey);
|
|
900
|
+
writeJournal(homePath, journal);
|
|
901
|
+
requireFences();
|
|
902
|
+
if (!revalidateAdmission())
|
|
903
|
+
throw new Error('runtime activation exact admission changed before settlement');
|
|
904
|
+
const finalMaintenance = observeMaintenance();
|
|
905
|
+
const finalService = observeLaunchd();
|
|
906
|
+
if (!finalMaintenance.ok || finalMaintenance.daemonRoots !== 0 || finalMaintenance.daemonDescendants !== 0 ||
|
|
907
|
+
finalService.loaded !== false || finalService.disabled !== service.disabled ||
|
|
908
|
+
readlinkSync(pointerPath, 'utf8') !== candidateTarget ||
|
|
909
|
+
!exactFileDigest(plistPath(homePath), candidatePlistSha256, homePath)) {
|
|
910
|
+
throw new Error('runtime activation stopped settlement revalidation failed');
|
|
911
|
+
}
|
|
912
|
+
requireFences();
|
|
913
|
+
if (!revalidateAdmission())
|
|
914
|
+
throw new Error('runtime activation exact admission changed before receipt');
|
|
915
|
+
const receipt = recordFor('receipt', plan, envelope.payload.permitId);
|
|
916
|
+
const receiptDisposition = writeImmutablePrivateRecord(recordStore(homePath), receipt);
|
|
917
|
+
if (receiptDisposition !== 'recorded') {
|
|
918
|
+
throw new Error(`runtime activation stopped receipt was ${receiptDisposition}`);
|
|
919
|
+
}
|
|
920
|
+
requireFences();
|
|
921
|
+
if (!revalidateAdmission())
|
|
922
|
+
throw new Error('runtime activation exact admission changed after receipt');
|
|
923
|
+
const settledMaintenance = observeMaintenance();
|
|
924
|
+
const settledService = observeLaunchd();
|
|
925
|
+
if (!settledMaintenance.ok || settledMaintenance.daemonRoots !== 0 || settledMaintenance.daemonDescendants !== 0 ||
|
|
926
|
+
settledService.loaded !== false || settledService.disabled !== service.disabled ||
|
|
927
|
+
readlinkSync(pointerPath, 'utf8') !== candidateTarget ||
|
|
928
|
+
!exactFileDigest(plistPath(homePath), candidatePlistSha256, homePath)) {
|
|
929
|
+
throw new Error('runtime activation stopped post-receipt state requires reconciliation');
|
|
930
|
+
}
|
|
931
|
+
requireFences();
|
|
932
|
+
removeJournal(homePath);
|
|
933
|
+
mutationStarted = false;
|
|
934
|
+
return {
|
|
935
|
+
...planIdentity(plan),
|
|
936
|
+
activated: true,
|
|
937
|
+
serviceStarted: false,
|
|
938
|
+
serviceEnabledChanged: false,
|
|
939
|
+
phase: 'activated-stopped',
|
|
940
|
+
reason: 'verified 3.2.7 release selected while preserving the exact stopped service state',
|
|
941
|
+
rollbackRestored: false,
|
|
942
|
+
recoveryJournalRetained: false,
|
|
943
|
+
recoveryJournalObserved: false,
|
|
944
|
+
durableOutcome: 'settled-candidate',
|
|
945
|
+
permitId: envelope.payload.permitId,
|
|
946
|
+
receiptPath: join(recordStore(homePath).rootPath, 'records', `receipt-${envelope.payload.permitId}.json`),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
if (mutationStarted) {
|
|
951
|
+
if (!fencesOwned()) {
|
|
952
|
+
return blocked(`${error instanceof Error ? error.message : String(error)}; exact stopped rollback requires reconciliation: fence ownership lost`, false, envelope.payload.permitId, true);
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
const journal = readJournal(homePath, journalKey);
|
|
956
|
+
if (!journal)
|
|
957
|
+
throw new Error('runtime activation journal disappeared during rollback');
|
|
958
|
+
const recovery = restoreJournal(homePath, journal, observeLaunchd, requireFences);
|
|
959
|
+
if (recovery === 'settled') {
|
|
960
|
+
return settledCandidate(homePath, journalIdentity(journal), envelope.payload.permitId, `${error instanceof Error ? error.message : String(error)}; durable stopped settlement was retained`, false);
|
|
961
|
+
}
|
|
962
|
+
rollbackRestored = true;
|
|
963
|
+
}
|
|
964
|
+
catch (rollbackError) {
|
|
965
|
+
return blocked(`${error instanceof Error ? error.message : String(error)}; exact stopped rollback requires reconciliation: ` +
|
|
966
|
+
`${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, false, envelope.payload.permitId, true);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return blocked(error instanceof Error ? error.message : String(error), rollbackRestored, recoveryJournalObserved ? null : envelope.payload.permitId, recoveryJournalObserved, recoveryJournalObserved ? undefined : planIdentity(plan), rollbackRestored ? 'restored-prior' : 'none', recoveryJournalObserved);
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
if (lifecycle)
|
|
973
|
+
releaseLifecycle(lifecycle);
|
|
974
|
+
if (outward)
|
|
975
|
+
releaseOutward(outward);
|
|
976
|
+
journalKey.fill(0);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
export function recoverStoppedRuntimeReleaseForTransaction(homePath) {
|
|
980
|
+
return recoverExistingStoppedRuntimeRelease(homePath);
|
|
981
|
+
}
|
|
982
|
+
export function consumeStoppedRuntimeReleaseForTransaction(plan, homePath, revalidateAdmission) {
|
|
983
|
+
return activateVerifiedStoppedRuntimeRelease(plan, homePath, revalidateAdmission);
|
|
984
|
+
}
|
|
985
|
+
export const runtimeActivationStoppedConsumerInternals = {
|
|
986
|
+
PROTOCOL,
|
|
987
|
+
permitPath,
|
|
988
|
+
journalPath,
|
|
989
|
+
parsePermit,
|
|
990
|
+
verifyPermit,
|
|
991
|
+
activityAllowsStoppedRecovery,
|
|
992
|
+
observeMaintenanceDefault,
|
|
993
|
+
recordStore,
|
|
994
|
+
};
|
|
995
|
+
//# sourceMappingURL=runtime-activation-stopped-consumer.js.map
|