@ashlr/hub 3.3.0 → 3.3.2
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 +46 -2
- package/README.md +51 -38
- package/dist/build-identity.json +2 -2
- package/dist/cli/completions.js +1 -2
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/dashboard.js +5 -4
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/help.js +7 -10
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/serve.js +7 -3
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -3
- package/dist/cli/setup.js +14 -7
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/update.js +3 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/core/daemon/activation-permit.d.ts +33 -268
- package/dist/core/daemon/activation-permit.js +106 -1111
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/loop.d.ts +1 -13
- package/dist/core/daemon/loop.js +10 -113
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +5 -1
- package/dist/core/daemon/service-install-authority.js +7 -20
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/fleet/automerge-pass.js +28 -77
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +1 -1
- package/dist/core/fleet/decisions-ledger.js.map +1 -1
- package/dist/core/fleet/judge-trace.js +2 -1
- package/dist/core/fleet/judge-trace.js.map +1 -1
- package/dist/core/fleet/manager.d.ts +6 -0
- package/dist/core/fleet/manager.js +35 -17
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit-label.d.ts +10 -0
- package/dist/core/fleet/post-merge-credit-label.js +13 -0
- package/dist/core/fleet/post-merge-credit-label.js.map +1 -0
- package/dist/core/fleet/post-merge-credit.d.ts +20 -38
- package/dist/core/fleet/post-merge-credit.js +38 -39
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +7 -7
- package/dist/core/fleet/self-improve.js +104 -20
- package/dist/core/fleet/self-improve.js.map +1 -1
- package/dist/core/fleet/status.js +0 -1
- package/dist/core/fleet/status.js.map +1 -1
- package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
- package/dist/core/foundry/automerge-scope-policy.js +2 -9
- package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
- package/dist/core/goals/store.js +60 -20
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/inbox/apply.js +3 -5
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +0 -84
- package/dist/core/inbox/merge.js +23 -277
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/learn/reflect.js +10 -0
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/onboard.d.ts +2 -0
- package/dist/core/onboard.js +9 -3
- package/dist/core/onboard.js.map +1 -1
- package/dist/core/run/orchestrator.js +2 -0
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/types.d.ts +5 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.d.ts +6 -0
- package/dist/core/web/api.js +69 -11
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/public/app.js +5 -1
- package/dist/core/web/public/next/assets/index-CDHQraxS.css +1 -0
- package/dist/core/web/public/next/assets/index-zVKD9krw.js +69 -0
- package/dist/core/web/public/next/assets/{index-Ct0mf8n4.js.map → index-zVKD9krw.js.map} +1 -1
- package/dist/core/web/public/next/index.html +2 -2
- package/dist/core/web/read-session-revocations.d.ts +26 -0
- package/dist/core/web/read-session-revocations.js +48 -0
- package/dist/core/web/read-session-revocations.js.map +1 -0
- package/dist/core/web/server.js +73 -27
- package/dist/core/web/server.js.map +1 -1
- package/dist/core/web/static.d.ts +1 -1
- package/dist/core/web/static.js +54 -20
- package/dist/core/web/static.js.map +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +0 -11
- package/node_modules/fast-uri/index.js +201 -48
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +388 -91
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
- package/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +7 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security.test.js +75 -3
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
- package/node_modules/qs/CHANGELOG.md +26 -0
- package/node_modules/qs/README.md +19 -1
- package/node_modules/qs/dist/qs.js +25 -25
- package/node_modules/qs/eslint.config.mjs +1 -1
- package/node_modules/qs/lib/parse.js +16 -6
- package/node_modules/qs/lib/stringify.js +23 -8
- package/node_modules/qs/lib/utils.js +57 -11
- package/node_modules/qs/package.json +8 -7
- package/node_modules/qs/test/parse.js +419 -0
- package/node_modules/qs/test/stringify.js +317 -0
- package/node_modules/qs/test/utils.js +206 -0
- package/package.json +5 -4
- package/schema/config.schema.json +7 -7
- package/dist/cli/activation.d.ts +0 -41
- package/dist/cli/activation.js +0 -485
- package/dist/cli/activation.js.map +0 -1
- package/dist/core/web/public/next/assets/index-CY0U241s.css +0 -1
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +0 -69
|
@@ -1,69 +1,15 @@
|
|
|
1
|
-
import { createHash, createPrivateKey, createPublicKey,
|
|
2
|
-
import { chmodSync, closeSync, constants as fsConstants, fchmodSync, fstatSync, fsyncSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync,
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify, } from 'node:crypto';
|
|
2
|
+
import { chmodSync, closeSync, constants as fsConstants, fchmodSync, fstatSync, fsyncSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, unlinkSync, writeSync, } from 'node:fs';
|
|
3
3
|
import { homedir, hostname } from 'node:os';
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { readBuildIdentity } from '../build-identity.js';
|
|
6
6
|
import { acquireLocalStoreLock, releaseLocalStoreLock, } from '../fleet/local-store-lock.js';
|
|
7
|
-
import { audit } from '../sandbox/audit.js';
|
|
8
7
|
import { killSwitchOn, readEnrollmentRegistry } from '../sandbox/policy.js';
|
|
9
8
|
import { fsyncDirectory } from '../util/durability.js';
|
|
10
9
|
import { assurePrivateStoragePath } from '../util/private-storage.js';
|
|
11
10
|
import { diagnoseGuardHealth } from './guard-health.js';
|
|
12
|
-
/**
|
|
13
|
-
* M461 → M470 daemon activation authority.
|
|
14
|
-
*
|
|
15
|
-
* The default for anyone who has not explicitly run `ashlr activation init`
|
|
16
|
-
* is EXACTLY today's behavior: every guard denies, with the same reasons as
|
|
17
|
-
* before. Authority only exists once an operator:
|
|
18
|
-
*
|
|
19
|
-
* 1. Runs `ashlr activation init` — generates an Ed25519 keypair on this
|
|
20
|
-
* machine (private key never leaves it) and registers the public half
|
|
21
|
-
* as a trust root in ~/.ashlr/activation/trust-roots.json (0600, dir
|
|
22
|
-
* 0700, owned by the operator's uid — checked on every read).
|
|
23
|
-
* 2. Runs `ashlr activation grant <scope...>` — signs and mints either a
|
|
24
|
-
* single-use activation PERMIT (scopes: once, resident) consumed by
|
|
25
|
-
* `daemon start`, or a standing, expiring GRANT (scopes: residentStanding,
|
|
26
|
-
* conductor, install, automerge, repair, deploy) checked on every call to
|
|
27
|
-
* the corresponding guard.
|
|
28
|
-
*
|
|
29
|
-
* `resident` is grantable BOTH ways, and both forms remain live:
|
|
30
|
-
* - one-shot `resident` permit: fixed 2-minute window, requires a human to
|
|
31
|
-
* run `grant` immediately before `daemon start`. Unusable after a reboot
|
|
32
|
-
* with nobody present.
|
|
33
|
-
* - standing `residentStanding` grant: up to the normal 30-day cap, checked
|
|
34
|
-
* at resident-daemon startup instead of requiring a fresh permit — this is
|
|
35
|
-
* what lets a launchd/systemd/Task-Scheduler-triggered restart authorize
|
|
36
|
-
* itself with no human in the loop. `daemon start` (resident mode) tries
|
|
37
|
-
* the standing grant first and falls back to the one-shot permit path
|
|
38
|
-
* unchanged, so an operator who never grants `residentStanding` sees
|
|
39
|
-
* EXACTLY today's one-shot-only behavior.
|
|
40
|
-
*
|
|
41
|
-
* Both permits and grants are Ed25519-signed, scoped, expiring, and
|
|
42
|
-
* independently revocable (`ashlr activation revoke`). Every init / grant /
|
|
43
|
-
* revoke / consume / denial writes an audit row via ../sandbox/audit.js so
|
|
44
|
-
* an operator can answer "who activated this fleet, when, with what scope."
|
|
45
|
-
*
|
|
46
|
-
* Revocation of a standing grant is file-based: it stops the NEXT
|
|
47
|
-
* authorization check, immediately. For `residentStanding` specifically it is
|
|
48
|
-
* stronger than that — because standing grants are not single-use (unlike the
|
|
49
|
-
* one-shot capability, which a resident loop must consume exactly once, see
|
|
50
|
-
* `tick()`'s `activationScope` handling in ../loop.ts), a resident daemon that
|
|
51
|
-
* started off a standing grant re-checks it on every tick and stops at the
|
|
52
|
-
* next one if the grant is revoked, expired, or its trust root is revoked —
|
|
53
|
-
* not just at the next `daemon start`. It cannot interrupt a tick already in
|
|
54
|
-
* flight.
|
|
55
|
-
*
|
|
56
|
-
* File loading fails CLOSED on every ambiguity: missing, unreadable, wrong
|
|
57
|
-
* permissions, wrong owner, symlinked, malformed, or non-canonically
|
|
58
|
-
* encoded trust-root/grant/permit files are all treated as "no authority" —
|
|
59
|
-
* never as "trust everything." Windows is denied outright: node:fs chmod()
|
|
60
|
-
* on Windows does not restrict per-user read access on NTFS (it only
|
|
61
|
-
* toggles the read-only attribute), so the "reject world/group-readable
|
|
62
|
-
* private files" invariant this module depends on cannot be enforced there.
|
|
63
|
-
*/
|
|
64
11
|
const POLICY_VERSION = 'm461-proposal-once-v1';
|
|
65
12
|
const SIGNING_DOMAIN = 'ashlr:daemon-activation-permit:m461:v1\0';
|
|
66
|
-
const GRANT_SIGNING_DOMAIN = 'ashlr:daemon-activation-grant:v1\0';
|
|
67
13
|
const MAX_PERMIT_BYTES = 32 * 1024;
|
|
68
14
|
const MAX_VALIDITY_MS = 120_000;
|
|
69
15
|
const MAX_FUTURE_SKEW_MS = 30_000;
|
|
@@ -73,51 +19,18 @@ const PRIVATE_FILE_MODE = 0o600;
|
|
|
73
19
|
const DIGEST_RE = /^[a-f0-9]{64}$/u;
|
|
74
20
|
const ID_RE = /^[a-f0-9]{32}$/u;
|
|
75
21
|
const KEY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
76
|
-
const MAX_TRUST_STORE_BYTES = 256 * 1024;
|
|
77
|
-
const MAX_GRANTS_BYTES = 512 * 1024;
|
|
78
|
-
const MAX_LABEL_LENGTH = 200;
|
|
79
|
-
/** Hard cap on any standing grant's validity window (30 days). */
|
|
80
|
-
export const MAX_STANDING_GRANT_VALIDITY_MS = 30 * 24 * 60 * 60 * 1000;
|
|
81
|
-
/** Default TTL `ashlr activation grant` uses when --ttl is not given (24h). */
|
|
82
|
-
export const DEFAULT_STANDING_GRANT_VALIDITY_MS = 24 * 60 * 60 * 1000;
|
|
83
22
|
const capabilityBrand = Symbol('ashlr.daemon-activation-capability');
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'automerge',
|
|
91
|
-
'repair',
|
|
92
|
-
'deploy',
|
|
93
|
-
'install',
|
|
94
|
-
'proposalOnly',
|
|
95
|
-
];
|
|
96
|
-
const SCOPE_KEYS = DAEMON_ACTIVATION_SCOPE_KEYS;
|
|
97
|
-
/** All-false scope — the base every CLI `grant` invocation flips bits on top of. */
|
|
98
|
-
export const EMPTY_DAEMON_ACTIVATION_SCOPE = Object.freeze({
|
|
99
|
-
once: false,
|
|
100
|
-
resident: false,
|
|
101
|
-
residentStanding: false,
|
|
102
|
-
conductor: false,
|
|
103
|
-
automerge: false,
|
|
104
|
-
repair: false,
|
|
105
|
-
deploy: false,
|
|
106
|
-
install: false,
|
|
107
|
-
proposalOnly: false,
|
|
108
|
-
});
|
|
109
|
-
/** Preserves pre-M470 test/caller behavior when `buildDaemonActivationPermitPayload` omits `scope`. */
|
|
110
|
-
const LEGACY_ONCE_PROPOSAL_SCOPE = Object.freeze({
|
|
111
|
-
...EMPTY_DAEMON_ACTIVATION_SCOPE,
|
|
112
|
-
once: true,
|
|
113
|
-
proposalOnly: true,
|
|
114
|
-
});
|
|
23
|
+
/**
|
|
24
|
+
* Provisioning a production trust root requires a reviewed source change.
|
|
25
|
+
* No environment variable, config field, CLI argument, or writable trust file
|
|
26
|
+
* can add authority at runtime.
|
|
27
|
+
*/
|
|
28
|
+
export const DAEMON_ACTIVATION_TRUST_ROOTS = Object.freeze([]);
|
|
115
29
|
const validCapabilities = new WeakMap();
|
|
116
|
-
function mintCapability(permitId,
|
|
30
|
+
function mintCapability(permitId, validateAtClaim) {
|
|
117
31
|
const capability = Object.freeze({
|
|
118
32
|
kind: 'proposal-once',
|
|
119
33
|
permitId,
|
|
120
|
-
scope: Object.freeze({ ...scope }),
|
|
121
34
|
[capabilityBrand]: true,
|
|
122
35
|
});
|
|
123
36
|
validCapabilities.set(capability, validateAtClaim);
|
|
@@ -139,6 +52,14 @@ export function isDaemonActivationCapability(value) {
|
|
|
139
52
|
return false;
|
|
140
53
|
}
|
|
141
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Goal and simple conductors have broader authority than M461's one-item
|
|
57
|
+
* proposal profile. They remain dormant until a separately reviewed permit
|
|
58
|
+
* contract exists for those entry points.
|
|
59
|
+
*/
|
|
60
|
+
export function liveConductorActivationAuthorized() {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
142
63
|
function isRecord(value) {
|
|
143
64
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
144
65
|
}
|
|
@@ -245,43 +166,33 @@ function validFileBinding(value) {
|
|
|
245
166
|
&& DIGEST_RE.test(String(value['sha256']));
|
|
246
167
|
}
|
|
247
168
|
function validScope(value) {
|
|
248
|
-
if (!isRecord(value) || !hasExactKeys(value,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*
|
|
263
|
-
* This accepts any subset of the current scope keys (no unrecognized keys,
|
|
264
|
-
* every present key boolean) and leaves absent keys simply absent. Every
|
|
265
|
-
* read site treats `record.scope[key] !== true` / falsy the same way
|
|
266
|
-
* `undefined` would, so an old record's missing new-generation keys read as
|
|
267
|
-
* "not granted" — never widened, never silently trusted. Signature
|
|
268
|
-
* verification is unaffected: `grantSigningBytes` canonicalizes whatever
|
|
269
|
-
* `scope` object the record actually contains, i.e. exactly what was
|
|
270
|
-
* originally signed, not a backfilled/padded version — so an old record's
|
|
271
|
-
* signature still verifies against its original (smaller) scope shape.
|
|
272
|
-
*/
|
|
273
|
-
function validStoredScope(value) {
|
|
274
|
-
if (!isRecord(value))
|
|
169
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
170
|
+
'action',
|
|
171
|
+
'once',
|
|
172
|
+
'dryRun',
|
|
173
|
+
'resident',
|
|
174
|
+
'drain',
|
|
175
|
+
'drainLimit',
|
|
176
|
+
'proposalOnly',
|
|
177
|
+
'automerge',
|
|
178
|
+
'repair',
|
|
179
|
+
'deploy',
|
|
180
|
+
'install',
|
|
181
|
+
'selfTarget',
|
|
182
|
+
]))
|
|
275
183
|
return false;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
184
|
+
return value['action'] === 'daemon-proposal-once'
|
|
185
|
+
&& value['once'] === true
|
|
186
|
+
&& value['dryRun'] === false
|
|
187
|
+
&& value['resident'] === false
|
|
188
|
+
&& value['drain'] === false
|
|
189
|
+
&& value['drainLimit'] === null
|
|
190
|
+
&& value['proposalOnly'] === true
|
|
191
|
+
&& value['automerge'] === false
|
|
192
|
+
&& value['repair'] === false
|
|
193
|
+
&& value['deploy'] === false
|
|
194
|
+
&& value['install'] === false
|
|
195
|
+
&& value['selfTarget'] === false;
|
|
285
196
|
}
|
|
286
197
|
function validBindings(value) {
|
|
287
198
|
if (!isRecord(value) || !hasExactKeys(value, [
|
|
@@ -374,11 +285,6 @@ function trustRootMap(roots) {
|
|
|
374
285
|
function equalCanonical(left, right) {
|
|
375
286
|
return canonicalizeDaemonActivationValue(left) === canonicalizeDaemonActivationValue(right);
|
|
376
287
|
}
|
|
377
|
-
/** Which top-level binding fields differ, field by field — never a single opaque yes/no. */
|
|
378
|
-
function bindingFieldMismatches(actual, expected) {
|
|
379
|
-
const keys = Object.keys(expected);
|
|
380
|
-
return keys.filter((key) => !equalCanonical(actual[key], expected[key]));
|
|
381
|
-
}
|
|
382
288
|
export function verifyDaemonActivationPermit(value, context, trustRoots) {
|
|
383
289
|
if (trustRoots.length === 0)
|
|
384
290
|
return { ok: false, reason: 'no-trusted-activation-roots' };
|
|
@@ -427,11 +333,7 @@ export function verifyDaemonActivationPermit(value, context, trustRoots) {
|
|
|
427
333
|
guardHealth: 'healthy',
|
|
428
334
|
};
|
|
429
335
|
if (!equalCanonical(envelope.payload.bindings, expectedBindings)) {
|
|
430
|
-
return {
|
|
431
|
-
ok: false,
|
|
432
|
-
reason: 'permit-runtime-binding-mismatch',
|
|
433
|
-
mismatchedBindings: bindingFieldMismatches(envelope.payload.bindings, expectedBindings),
|
|
434
|
-
};
|
|
336
|
+
return { ok: false, reason: 'permit-runtime-binding-mismatch' };
|
|
435
337
|
}
|
|
436
338
|
const publicKey = trusted.get(envelope.payload.keyId);
|
|
437
339
|
if (!publicKey)
|
|
@@ -455,7 +357,6 @@ export function verifyDaemonActivationPermit(value, context, trustRoots) {
|
|
|
455
357
|
};
|
|
456
358
|
}
|
|
457
359
|
export function buildDaemonActivationPermitPayload(input) {
|
|
458
|
-
const scope = input.scope ?? LEGACY_ONCE_PROPOSAL_SCOPE;
|
|
459
360
|
const payload = {
|
|
460
361
|
schemaVersion: 1,
|
|
461
362
|
policyVersion: POLICY_VERSION,
|
|
@@ -464,7 +365,20 @@ export function buildDaemonActivationPermitPayload(input) {
|
|
|
464
365
|
keyId: input.keyId,
|
|
465
366
|
issuedAt: input.issuedAt,
|
|
466
367
|
expiresAt: input.expiresAt,
|
|
467
|
-
scope: {
|
|
368
|
+
scope: {
|
|
369
|
+
action: 'daemon-proposal-once',
|
|
370
|
+
once: true,
|
|
371
|
+
dryRun: false,
|
|
372
|
+
resident: false,
|
|
373
|
+
drain: false,
|
|
374
|
+
drainLimit: null,
|
|
375
|
+
proposalOnly: true,
|
|
376
|
+
automerge: false,
|
|
377
|
+
repair: false,
|
|
378
|
+
deploy: false,
|
|
379
|
+
install: false,
|
|
380
|
+
selfTarget: false,
|
|
381
|
+
},
|
|
468
382
|
bindings: {
|
|
469
383
|
configDigest: input.context.configDigest,
|
|
470
384
|
buildIdentity: { ...input.context.buildIdentity },
|
|
@@ -496,12 +410,6 @@ function safeOwnedFile(stat) {
|
|
|
496
410
|
const privateMode = process.platform === 'win32' || (stat.mode & 63n) === 0n;
|
|
497
411
|
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n && owned && privateMode;
|
|
498
412
|
}
|
|
499
|
-
function safeOwnedDirectory(stat) {
|
|
500
|
-
const owned = process.platform === 'win32' || typeof process.getuid !== 'function'
|
|
501
|
-
|| stat.uid === BigInt(process.getuid());
|
|
502
|
-
const privateMode = process.platform === 'win32' || (stat.mode & 63n) === 0n;
|
|
503
|
-
return stat.isDirectory() && !stat.isSymbolicLink() && owned && privateMode;
|
|
504
|
-
}
|
|
505
413
|
function sameFileSnapshot(left, right) {
|
|
506
414
|
return left.dev === right.dev
|
|
507
415
|
&& left.ino === right.ino
|
|
@@ -712,26 +620,7 @@ function hashStableReleaseTree(entrypointPath) {
|
|
|
712
620
|
export function daemonActivationReleaseTreeBinding(entrypointPath) {
|
|
713
621
|
return hashStableReleaseTree(entrypointPath);
|
|
714
622
|
}
|
|
715
|
-
|
|
716
|
-
* Fingerprints "this authority store instance" — the ~/.ashlr directory a
|
|
717
|
-
* permit is bound to — so a permit can't be replayed against a different
|
|
718
|
-
* store (e.g. copied to another machine, or after a full delete+recreate).
|
|
719
|
-
*
|
|
720
|
-
* Deliberately uses dev+ino, NOT mtime. ~/.ashlr's own mtime changes every
|
|
721
|
-
* time a direct child is created or removed — and ordinary `daemon start`
|
|
722
|
-
* does exactly that before it ever reaches this check: acquireDaemonLock()
|
|
723
|
-
* (src/core/daemon/state.ts) creates ~/.ashlr/daemon.lock via
|
|
724
|
-
* `writeFileSync(..., {flag:'wx'})`, a brand-new direct child of ~/.ashlr.
|
|
725
|
-
* Binding to mtime meant every freshly minted permit was invalidated by the
|
|
726
|
-
* daemon's own startup lock-file creation, 100% reproducibly, before it
|
|
727
|
-
* could ever be consumed — a self-inflicted permit-runtime-binding-mismatch,
|
|
728
|
-
* not a real "different store" signal. dev+ino uniquely and STABLY identify
|
|
729
|
-
* the directory's inode: they survive ordinary child churn (locks, control
|
|
730
|
-
* files, receipts) and change only if ~/.ashlr is deleted and recreated (or
|
|
731
|
-
* moved to a different filesystem) — exactly the case this binding exists
|
|
732
|
-
* to catch.
|
|
733
|
-
*/
|
|
734
|
-
export function daemonActivationAuthorityStateDigest() {
|
|
623
|
+
function currentAuthorityStateDigest() {
|
|
735
624
|
const home = realpathSync(resolve(homedir()));
|
|
736
625
|
const ashlrHome = realpathSync(join(home, '.ashlr'));
|
|
737
626
|
const state = lstatSync(ashlrHome, { bigint: true });
|
|
@@ -750,6 +639,7 @@ export function daemonActivationAuthorityStateDigest() {
|
|
|
750
639
|
path: ashlrHome,
|
|
751
640
|
dev: state.dev.toString(),
|
|
752
641
|
ino: state.ino.toString(),
|
|
642
|
+
mtimeNs: state.mtimeNs.toString(),
|
|
753
643
|
},
|
|
754
644
|
enrollment: [...enrollment.repos].sort(),
|
|
755
645
|
}));
|
|
@@ -766,7 +656,7 @@ function collectRuntimeContext(cfg) {
|
|
|
766
656
|
executable: hashStableFile(process.execPath),
|
|
767
657
|
entrypoint: hashStableFile(entrypoint),
|
|
768
658
|
releaseTree: daemonActivationReleaseTreeBinding(entrypoint),
|
|
769
|
-
authorityStateDigest:
|
|
659
|
+
authorityStateDigest: currentAuthorityStateDigest(),
|
|
770
660
|
killSwitchOff: !killSwitchOn(),
|
|
771
661
|
guardHealthHealthy: !guard.blocked,
|
|
772
662
|
};
|
|
@@ -784,734 +674,7 @@ function daemonActivationNonceReceiptPath(nonceDigest) {
|
|
|
784
674
|
throw new Error('invalid daemon activation nonce digest');
|
|
785
675
|
return join(homedir(), '.ashlr', 'control', 'activation-receipts', 'by-nonce', `${nonceDigest}.json`);
|
|
786
676
|
}
|
|
787
|
-
|
|
788
|
-
// Operator authority store — trust roots + standing grants.
|
|
789
|
-
//
|
|
790
|
-
// Root of trust is OS-level file ownership/permissions on the operator's own
|
|
791
|
-
// machine (mirrors the one-shot permit's existing safety model), not a
|
|
792
|
-
// central registry. Every store read fails CLOSED to "no authority" on any
|
|
793
|
-
// ambiguity: missing file, unreadable, wrong owner, wrong permissions,
|
|
794
|
-
// symlinked path, malformed JSON, non-canonical encoding, or duplicate ids.
|
|
795
|
-
// ---------------------------------------------------------------------------
|
|
796
|
-
export function daemonActivationDirPath() {
|
|
797
|
-
return join(homedir(), '.ashlr', 'activation');
|
|
798
|
-
}
|
|
799
|
-
export function daemonActivationTrustRootsPath() {
|
|
800
|
-
return join(daemonActivationDirPath(), 'trust-roots.json');
|
|
801
|
-
}
|
|
802
|
-
export function daemonActivationGrantsPath() {
|
|
803
|
-
return join(daemonActivationDirPath(), 'grants.json');
|
|
804
|
-
}
|
|
805
|
-
export function daemonActivationOperatorKeyPath() {
|
|
806
|
-
return join(daemonActivationDirPath(), 'operator-private-key.pem');
|
|
807
|
-
}
|
|
808
|
-
function daemonActivationStoreLockPath() {
|
|
809
|
-
return join(daemonActivationDirPath(), 'activation-store.lock');
|
|
810
|
-
}
|
|
811
|
-
function pathEntryPresent(path) {
|
|
812
|
-
try {
|
|
813
|
-
lstatSync(path);
|
|
814
|
-
return true;
|
|
815
|
-
}
|
|
816
|
-
catch (error) {
|
|
817
|
-
if (error.code === 'ENOENT')
|
|
818
|
-
return false;
|
|
819
|
-
throw error;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
/** Safe, non-consuming read of a canonical-JSON-array operator store file. */
|
|
823
|
-
function safeReadOwnedJsonArray(path, anchorPath, maxBytes) {
|
|
824
|
-
if (process.platform === 'win32') {
|
|
825
|
-
return { ok: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
826
|
-
}
|
|
827
|
-
let dirStat;
|
|
828
|
-
try {
|
|
829
|
-
dirStat = lstatSync(dirname(path), { bigint: true });
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
return { ok: false, reason: error.code === 'ENOENT' ? 'missing' : 'stat-failed' };
|
|
833
|
-
}
|
|
834
|
-
if (!safeOwnedDirectory(dirStat))
|
|
835
|
-
return { ok: false, reason: 'unsafe-directory' };
|
|
836
|
-
const dirAssurance = assurePrivateStoragePath(dirname(path), 'directory', 'inspect-owned', { anchorPath });
|
|
837
|
-
if (!dirAssurance.ok)
|
|
838
|
-
return { ok: false, reason: `unsafe-directory:${dirAssurance.reason}` };
|
|
839
|
-
// Open first, then validate the fd — the check (owner/mode/regular-file/
|
|
840
|
-
// size) and the read below share one file description, so there is no
|
|
841
|
-
// window between "checked" and "used" for an attacker to swap the path.
|
|
842
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
843
|
-
let fd;
|
|
844
|
-
try {
|
|
845
|
-
fd = openSync(path, fsConstants.O_RDONLY | noFollow);
|
|
846
|
-
}
|
|
847
|
-
catch (error) {
|
|
848
|
-
return { ok: false, reason: error.code === 'ENOENT' ? 'missing' : 'open-failed' };
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
const opened = fstatSync(fd, { bigint: true });
|
|
852
|
-
if (!safeOwnedFile(opened) || opened.size <= 0n || opened.size > BigInt(maxBytes)) {
|
|
853
|
-
return { ok: false, reason: 'unsafe-file' };
|
|
854
|
-
}
|
|
855
|
-
const size = Number(opened.size);
|
|
856
|
-
const bytes = Buffer.alloc(size);
|
|
857
|
-
let offset = 0;
|
|
858
|
-
while (offset < size) {
|
|
859
|
-
const count = readSync(fd, bytes, offset, size - offset, offset);
|
|
860
|
-
if (count <= 0)
|
|
861
|
-
return { ok: false, reason: 'short-read' };
|
|
862
|
-
offset += count;
|
|
863
|
-
}
|
|
864
|
-
const after = fstatSync(fd, { bigint: true });
|
|
865
|
-
if (!sameFileSnapshot(opened, after))
|
|
866
|
-
return { ok: false, reason: 'changed-during-read' };
|
|
867
|
-
const text = bytes.toString('utf8');
|
|
868
|
-
let parsed;
|
|
869
|
-
try {
|
|
870
|
-
parsed = JSON.parse(text);
|
|
871
|
-
}
|
|
872
|
-
catch {
|
|
873
|
-
return { ok: false, reason: 'invalid-json' };
|
|
874
|
-
}
|
|
875
|
-
if (`${canonicalizeDaemonActivationValue(parsed)}\n` !== text) {
|
|
876
|
-
return { ok: false, reason: 'noncanonical-encoding' };
|
|
877
|
-
}
|
|
878
|
-
if (!Array.isArray(parsed))
|
|
879
|
-
return { ok: false, reason: 'not-an-array' };
|
|
880
|
-
const fileAssurance = assurePrivateStoragePath(path, 'file', 'inspect-owned', { anchorPath });
|
|
881
|
-
if (!fileAssurance.ok)
|
|
882
|
-
return { ok: false, reason: `unsafe-file:${fileAssurance.reason}` };
|
|
883
|
-
return { ok: true, value: parsed };
|
|
884
|
-
}
|
|
885
|
-
finally {
|
|
886
|
-
closeSync(fd);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
/** Atomically replace a canonical-JSON-array operator store file (temp write + rename). */
|
|
890
|
-
function writeCanonicalJsonArrayFileAtomic(path, value, anchorPath) {
|
|
891
|
-
assurePrivateDirectory(dirname(path), anchorPath);
|
|
892
|
-
const tmpPath = `${path}.tmp-${randomBytes(8).toString('hex')}`;
|
|
893
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
894
|
-
let fd;
|
|
895
|
-
try {
|
|
896
|
-
fd = openSync(tmpPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, PRIVATE_FILE_MODE);
|
|
897
|
-
const initial = fstatSync(fd, { bigint: true });
|
|
898
|
-
if (!safeOwnedFile(initial) || initial.size !== 0n)
|
|
899
|
-
throw new Error('unsafe-activation-store-temp-file');
|
|
900
|
-
fchmodSync(fd, PRIVATE_FILE_MODE);
|
|
901
|
-
const bytes = Buffer.from(`${canonicalizeDaemonActivationValue(value)}\n`, 'utf8');
|
|
902
|
-
writeAll(fd, bytes);
|
|
903
|
-
fsyncSync(fd);
|
|
904
|
-
const written = fstatSync(fd, { bigint: true });
|
|
905
|
-
if (!safeOwnedFile(written) || written.size !== BigInt(bytes.length)) {
|
|
906
|
-
throw new Error('activation-store-temp-file-changed-during-write');
|
|
907
|
-
}
|
|
908
|
-
closeSync(fd);
|
|
909
|
-
fd = undefined;
|
|
910
|
-
renameSync(tmpPath, path);
|
|
911
|
-
const assurance = assurePrivateStoragePath(path, 'file', 'secure-created', { anchorPath });
|
|
912
|
-
if (!assurance.ok)
|
|
913
|
-
throw new Error(`unsafe-activation-store-path:${assurance.reason}`);
|
|
914
|
-
fsyncDirectory(dirname(path));
|
|
915
|
-
}
|
|
916
|
-
finally {
|
|
917
|
-
if (fd !== undefined) {
|
|
918
|
-
try {
|
|
919
|
-
closeSync(fd);
|
|
920
|
-
}
|
|
921
|
-
catch { /* best-effort */ }
|
|
922
|
-
}
|
|
923
|
-
try {
|
|
924
|
-
if (existsSync(tmpPath))
|
|
925
|
-
unlinkSync(tmpPath);
|
|
926
|
-
}
|
|
927
|
-
catch { /* best-effort cleanup */ }
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
function withActivationStoreLock(fn) {
|
|
931
|
-
const home = resolve(homedir());
|
|
932
|
-
// Two single-level creates: ~/.ashlr may not exist yet on a machine where
|
|
933
|
-
// `activation init` is the very first ashlr command ever run.
|
|
934
|
-
assurePrivateDirectory(join(homedir(), '.ashlr'), home);
|
|
935
|
-
assurePrivateDirectory(daemonActivationDirPath(), home);
|
|
936
|
-
const lock = acquireLocalStoreLock(daemonActivationStoreLockPath(), LOCK_WAIT_MS, {
|
|
937
|
-
anchorPath: home,
|
|
938
|
-
exactPrivateStorage: true,
|
|
939
|
-
});
|
|
940
|
-
if (!lock)
|
|
941
|
-
throw new Error('activation-store-lock-unavailable');
|
|
942
|
-
let result;
|
|
943
|
-
try {
|
|
944
|
-
result = fn();
|
|
945
|
-
}
|
|
946
|
-
catch (error) {
|
|
947
|
-
releaseLocalStoreLock(lock);
|
|
948
|
-
throw error;
|
|
949
|
-
}
|
|
950
|
-
if (!releaseLocalStoreLock(lock))
|
|
951
|
-
throw new Error('activation-store-lock-release-failed');
|
|
952
|
-
return result;
|
|
953
|
-
}
|
|
954
|
-
function readOperatorPrivateKeyPem() {
|
|
955
|
-
const path = daemonActivationOperatorKeyPath();
|
|
956
|
-
const home = resolve(homedir());
|
|
957
|
-
// Open first, then validate the fd. The owner/mode/size check and the read
|
|
958
|
-
// below run against the same file description, so the path cannot be
|
|
959
|
-
// swapped between "checked" and "used".
|
|
960
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
961
|
-
const fd = openSync(path, fsConstants.O_RDONLY | noFollow);
|
|
962
|
-
try {
|
|
963
|
-
const opened = fstatSync(fd, { bigint: true });
|
|
964
|
-
if (!safeOwnedFile(opened) || opened.size <= 0n || opened.size > BigInt(MAX_PERMIT_BYTES)) {
|
|
965
|
-
throw new Error('unsafe-operator-key-file');
|
|
966
|
-
}
|
|
967
|
-
const size = Number(opened.size);
|
|
968
|
-
const bytes = Buffer.alloc(size);
|
|
969
|
-
let offset = 0;
|
|
970
|
-
while (offset < size) {
|
|
971
|
-
const count = readSync(fd, bytes, offset, size - offset, offset);
|
|
972
|
-
if (count <= 0)
|
|
973
|
-
throw new Error('short-operator-key-read');
|
|
974
|
-
offset += count;
|
|
975
|
-
}
|
|
976
|
-
const after = fstatSync(fd, { bigint: true });
|
|
977
|
-
if (!sameFileSnapshot(opened, after))
|
|
978
|
-
throw new Error('operator-key-changed-during-read');
|
|
979
|
-
const assurance = assurePrivateStoragePath(path, 'file', 'inspect-owned', { anchorPath: home });
|
|
980
|
-
if (!assurance.ok)
|
|
981
|
-
throw new Error(`unsafe-operator-key-path:${assurance.reason}`);
|
|
982
|
-
return bytes.toString('utf8');
|
|
983
|
-
}
|
|
984
|
-
finally {
|
|
985
|
-
closeSync(fd);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
function validTrustRootRecord(value) {
|
|
989
|
-
if (!isRecord(value) || !hasExactKeys(value, ['keyId', 'publicKeyPem', 'createdAt', 'revokedAt', 'label'])) {
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
if (typeof value['keyId'] !== 'string' || !KEY_ID_RE.test(value['keyId']))
|
|
993
|
-
return false;
|
|
994
|
-
if (typeof value['publicKeyPem'] !== 'string')
|
|
995
|
-
return false;
|
|
996
|
-
try {
|
|
997
|
-
const key = createPublicKey(value['publicKeyPem']);
|
|
998
|
-
if (key.asymmetricKeyType !== 'ed25519')
|
|
999
|
-
return false;
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
if (!validIsoTimestamp(value['createdAt']))
|
|
1005
|
-
return false;
|
|
1006
|
-
if (value['revokedAt'] !== null && !validIsoTimestamp(value['revokedAt']))
|
|
1007
|
-
return false;
|
|
1008
|
-
if (typeof value['label'] !== 'string' || value['label'].length > MAX_LABEL_LENGTH)
|
|
1009
|
-
return false;
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
function loadTrustRootStore() {
|
|
1013
|
-
const read = safeReadOwnedJsonArray(daemonActivationTrustRootsPath(), resolve(homedir()), MAX_TRUST_STORE_BYTES);
|
|
1014
|
-
if (!read.ok) {
|
|
1015
|
-
if (read.reason === 'missing')
|
|
1016
|
-
return { ok: true, records: [] };
|
|
1017
|
-
return { ok: false, reason: read.reason };
|
|
1018
|
-
}
|
|
1019
|
-
const records = [];
|
|
1020
|
-
const seen = new Set();
|
|
1021
|
-
for (const entry of read.value) {
|
|
1022
|
-
if (!validTrustRootRecord(entry) || seen.has(entry.keyId)) {
|
|
1023
|
-
return { ok: false, reason: 'invalid-trust-root-store' };
|
|
1024
|
-
}
|
|
1025
|
-
seen.add(entry.keyId);
|
|
1026
|
-
records.push(entry);
|
|
1027
|
-
}
|
|
1028
|
-
return { ok: true, records };
|
|
1029
|
-
}
|
|
1030
|
-
/**
|
|
1031
|
-
* The ONLY source of production trust roots: an operator-owned, protected
|
|
1032
|
-
* file the operator populates via `ashlr activation init`. Missing, unsafe,
|
|
1033
|
-
* or malformed → [] — identical denial to before this file loading existed.
|
|
1034
|
-
*/
|
|
1035
|
-
export function loadDaemonActivationTrustRoots() {
|
|
1036
|
-
const loaded = loadTrustRootStore();
|
|
1037
|
-
if (!loaded.ok)
|
|
1038
|
-
return [];
|
|
1039
|
-
return loaded.records
|
|
1040
|
-
.filter((record) => record.revokedAt === null)
|
|
1041
|
-
.map((record) => ({ keyId: record.keyId, publicKeyPem: record.publicKeyPem }));
|
|
1042
|
-
}
|
|
1043
|
-
/** Full trust-root status (incl. revoked history) for `ashlr activation status`. */
|
|
1044
|
-
export function daemonActivationTrustRootStatus() {
|
|
1045
|
-
return loadTrustRootStore();
|
|
1046
|
-
}
|
|
1047
|
-
function activeTrustRootKeyMap() {
|
|
1048
|
-
const loaded = loadTrustRootStore();
|
|
1049
|
-
if (!loaded.ok)
|
|
1050
|
-
return null;
|
|
1051
|
-
const active = loaded.records
|
|
1052
|
-
.filter((record) => record.revokedAt === null)
|
|
1053
|
-
.map((record) => ({ keyId: record.keyId, publicKeyPem: record.publicKeyPem }));
|
|
1054
|
-
return trustRootMap(active);
|
|
1055
|
-
}
|
|
1056
|
-
function validGrantRecord(value) {
|
|
1057
|
-
if (!isRecord(value) || !hasExactKeys(value, [
|
|
1058
|
-
'schemaVersion', 'grantId', 'keyId', 'issuedAt', 'expiresAt', 'scope', 'signature', 'revokedAt',
|
|
1059
|
-
]))
|
|
1060
|
-
return false;
|
|
1061
|
-
if (value['schemaVersion'] !== 1)
|
|
1062
|
-
return false;
|
|
1063
|
-
if (typeof value['grantId'] !== 'string' || !ID_RE.test(value['grantId']))
|
|
1064
|
-
return false;
|
|
1065
|
-
if (typeof value['keyId'] !== 'string' || !KEY_ID_RE.test(value['keyId']))
|
|
1066
|
-
return false;
|
|
1067
|
-
if (!validIsoTimestamp(value['issuedAt']) || !validIsoTimestamp(value['expiresAt']))
|
|
1068
|
-
return false;
|
|
1069
|
-
// validStoredScope (not validScope): a persisted STANDING grant may
|
|
1070
|
-
// predate a newly-added scope key by up to 30 days — see its doc comment.
|
|
1071
|
-
if (!validStoredScope(value['scope']))
|
|
1072
|
-
return false;
|
|
1073
|
-
if (typeof value['signature'] !== 'string')
|
|
1074
|
-
return false;
|
|
1075
|
-
try {
|
|
1076
|
-
const sig = Buffer.from(value['signature'], 'base64');
|
|
1077
|
-
if (sig.length !== 64 || sig.toString('base64') !== value['signature'])
|
|
1078
|
-
return false;
|
|
1079
|
-
}
|
|
1080
|
-
catch {
|
|
1081
|
-
return false;
|
|
1082
|
-
}
|
|
1083
|
-
if (value['revokedAt'] !== null && !validIsoTimestamp(value['revokedAt']))
|
|
1084
|
-
return false;
|
|
1085
|
-
return true;
|
|
1086
|
-
}
|
|
1087
|
-
function loadGrantStore() {
|
|
1088
|
-
const read = safeReadOwnedJsonArray(daemonActivationGrantsPath(), resolve(homedir()), MAX_GRANTS_BYTES);
|
|
1089
|
-
if (!read.ok) {
|
|
1090
|
-
if (read.reason === 'missing')
|
|
1091
|
-
return { ok: true, records: [] };
|
|
1092
|
-
return { ok: false, reason: read.reason };
|
|
1093
|
-
}
|
|
1094
|
-
const records = [];
|
|
1095
|
-
const seen = new Set();
|
|
1096
|
-
for (const entry of read.value) {
|
|
1097
|
-
if (!validGrantRecord(entry) || seen.has(entry.grantId)) {
|
|
1098
|
-
return { ok: false, reason: 'invalid-grant-store' };
|
|
1099
|
-
}
|
|
1100
|
-
seen.add(entry.grantId);
|
|
1101
|
-
records.push(entry);
|
|
1102
|
-
}
|
|
1103
|
-
return { ok: true, records };
|
|
1104
|
-
}
|
|
1105
|
-
/** Full standing-grant status (incl. revoked/expired history) for `ashlr activation status`. */
|
|
1106
|
-
export function daemonActivationGrantStatus() {
|
|
1107
|
-
return loadGrantStore();
|
|
1108
|
-
}
|
|
1109
|
-
function grantSigningCore(record) {
|
|
1110
|
-
return {
|
|
1111
|
-
schemaVersion: record.schemaVersion,
|
|
1112
|
-
grantId: record.grantId,
|
|
1113
|
-
keyId: record.keyId,
|
|
1114
|
-
issuedAt: record.issuedAt,
|
|
1115
|
-
expiresAt: record.expiresAt,
|
|
1116
|
-
scope: record.scope,
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
function grantSigningBytes(core) {
|
|
1120
|
-
return Buffer.from(`${GRANT_SIGNING_DOMAIN}${canonicalizeDaemonActivationValue(core)}`, 'utf8');
|
|
1121
|
-
}
|
|
1122
|
-
export function signDaemonActivationGrant(input) {
|
|
1123
|
-
const core = grantSigningCore({
|
|
1124
|
-
schemaVersion: 1,
|
|
1125
|
-
grantId: input.grantId,
|
|
1126
|
-
keyId: input.keyId,
|
|
1127
|
-
issuedAt: input.issuedAt,
|
|
1128
|
-
expiresAt: input.expiresAt,
|
|
1129
|
-
scope: { ...input.scope },
|
|
1130
|
-
});
|
|
1131
|
-
const key = typeof input.privateKey === 'string' ? createPrivateKey(input.privateKey) : input.privateKey;
|
|
1132
|
-
if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') {
|
|
1133
|
-
throw new Error('daemon activation grants require an Ed25519 private key');
|
|
1134
|
-
}
|
|
1135
|
-
const signature = sign(null, grantSigningBytes(core), key).toString('base64');
|
|
1136
|
-
const record = { ...core, signature, revokedAt: null };
|
|
1137
|
-
if (!validGrantRecord(record))
|
|
1138
|
-
throw new Error('invalid daemon activation grant record');
|
|
1139
|
-
return record;
|
|
1140
|
-
}
|
|
1141
|
-
function auditActivationEvent(action, summary, result) {
|
|
1142
|
-
audit({ action, repo: null, sandboxId: null, summary, result });
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Checks whether ANY non-revoked, non-expired, validly-signed standing grant
|
|
1146
|
-
* (from a non-revoked trust root) currently grants `scopeKey`. Unlike the
|
|
1147
|
-
* one-shot permit, standing grants are NOT bound to exact executable/
|
|
1148
|
-
* entrypoint/release-tree hashes — they must remain valid across routine
|
|
1149
|
-
* rebuilds within their expiry window. They ARE bound to kill-switch-off,
|
|
1150
|
-
* guard-health-healthy, and a non-revoked trust root, and always expire.
|
|
1151
|
-
*/
|
|
1152
|
-
/**
|
|
1153
|
-
* Shared core of `daemonActivationScopeGranted`. Returns the matched grant
|
|
1154
|
-
* RECORD (not just a boolean) so callers that need to act on it — currently
|
|
1155
|
-
* only the resident-standing startup path — can read its scope and grantId
|
|
1156
|
-
* without a second, redundant store read. Same checks, same order, as the
|
|
1157
|
-
* pre-refactor inline body of `daemonActivationScopeGranted`.
|
|
1158
|
-
*/
|
|
1159
|
-
function findGrantedRecord(scopeKey) {
|
|
1160
|
-
if (process.platform === 'win32') {
|
|
1161
|
-
return { granted: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
1162
|
-
}
|
|
1163
|
-
if (killSwitchOn())
|
|
1164
|
-
return { granted: false, reason: 'kill-switch-is-on' };
|
|
1165
|
-
let guardHealthy;
|
|
1166
|
-
try {
|
|
1167
|
-
guardHealthy = !diagnoseGuardHealth().blocked;
|
|
1168
|
-
}
|
|
1169
|
-
catch {
|
|
1170
|
-
return { granted: false, reason: 'guard-health-check-failed' };
|
|
1171
|
-
}
|
|
1172
|
-
if (!guardHealthy)
|
|
1173
|
-
return { granted: false, reason: 'guard-health-degraded' };
|
|
1174
|
-
const trusted = activeTrustRootKeyMap();
|
|
1175
|
-
if (!trusted || trusted.size === 0)
|
|
1176
|
-
return { granted: false, reason: 'no-trusted-activation-roots' };
|
|
1177
|
-
const grants = loadGrantStore();
|
|
1178
|
-
if (!grants.ok)
|
|
1179
|
-
return { granted: false, reason: `activation-grant-store-${grants.reason}` };
|
|
1180
|
-
const nowMs = Date.now();
|
|
1181
|
-
for (const record of grants.records) {
|
|
1182
|
-
if (record.revokedAt !== null)
|
|
1183
|
-
continue;
|
|
1184
|
-
if (record.scope[scopeKey] !== true)
|
|
1185
|
-
continue;
|
|
1186
|
-
const publicKey = trusted.get(record.keyId);
|
|
1187
|
-
if (!publicKey)
|
|
1188
|
-
continue;
|
|
1189
|
-
if (Date.parse(record.expiresAt) <= nowMs)
|
|
1190
|
-
continue;
|
|
1191
|
-
let signature;
|
|
1192
|
-
try {
|
|
1193
|
-
signature = Buffer.from(record.signature, 'base64');
|
|
1194
|
-
}
|
|
1195
|
-
catch {
|
|
1196
|
-
continue;
|
|
1197
|
-
}
|
|
1198
|
-
if (!verify(null, grantSigningBytes(grantSigningCore(record)), publicKey, signature))
|
|
1199
|
-
continue;
|
|
1200
|
-
return { granted: true, record };
|
|
1201
|
-
}
|
|
1202
|
-
return { granted: false, reason: 'no-matching-standing-grant' };
|
|
1203
|
-
}
|
|
1204
|
-
export function daemonActivationScopeGranted(scopeKey) {
|
|
1205
|
-
const found = findGrantedRecord(scopeKey);
|
|
1206
|
-
return found.granted
|
|
1207
|
-
? { granted: true, reason: `granted-by-${found.record.grantId}` }
|
|
1208
|
-
: { granted: false, reason: found.reason };
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* The goal and simple conductors consult this every cycle. Real, gated,
|
|
1212
|
-
* revocable authority: granted only after `ashlr activation init` +
|
|
1213
|
-
* `ashlr activation grant conductor`. Default (no grant) is denied, exactly
|
|
1214
|
-
* as before this mechanism existed. Every check is audited.
|
|
1215
|
-
*/
|
|
1216
|
-
export function liveConductorActivationAuthorized() {
|
|
1217
|
-
const check = daemonActivationScopeGranted('conductor');
|
|
1218
|
-
auditActivationEvent('daemon-activation:conductor-check', `conductor activation ${check.granted ? 'authorized' : 'denied'}: ${check.reason}`, check.granted ? 'ok' : 'refused');
|
|
1219
|
-
return check.granted;
|
|
1220
|
-
}
|
|
1221
|
-
// ---------------------------------------------------------------------------
|
|
1222
|
-
// Operator authority mutation — init / grant / revoke. Called from
|
|
1223
|
-
// `src/cli/activation.ts`; kept here so all signing/writing stays alongside
|
|
1224
|
-
// the verification it must satisfy.
|
|
1225
|
-
// ---------------------------------------------------------------------------
|
|
1226
|
-
export function generateDaemonActivationOperatorKeypair() {
|
|
1227
|
-
const pair = generateKeyPairSync('ed25519');
|
|
1228
|
-
return {
|
|
1229
|
-
privateKeyPem: pair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
1230
|
-
publicKeyPem: pair.publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
function sanitizeKeyId(candidate) {
|
|
1234
|
-
const cleaned = candidate.replace(/[^A-Za-z0-9._-]/gu, '-').slice(0, 55);
|
|
1235
|
-
const seeded = cleaned.length > 0 && /^[A-Za-z0-9]/u.test(cleaned) ? cleaned : `k${cleaned}`;
|
|
1236
|
-
return seeded.slice(0, 63) || 'operator';
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Generates an Ed25519 keypair on THIS machine (private key never leaves it,
|
|
1240
|
-
* written 0600 under an 0700 directory) and registers the public half as a
|
|
1241
|
-
* new active trust root. Refuses to silently clobber an existing active
|
|
1242
|
-
* root or key file unless `force` is set (key rotation: the new root is
|
|
1243
|
-
* ADDED, not swapped in place, so grants signed by an older still-active
|
|
1244
|
-
* root keep working until explicitly revoked).
|
|
1245
|
-
*/
|
|
1246
|
-
export function daemonActivationInit(input) {
|
|
1247
|
-
if (process.platform === 'win32') {
|
|
1248
|
-
return { ok: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
1249
|
-
}
|
|
1250
|
-
return withActivationStoreLock(() => {
|
|
1251
|
-
const existing = loadTrustRootStore();
|
|
1252
|
-
if (!existing.ok)
|
|
1253
|
-
return { ok: false, reason: existing.reason };
|
|
1254
|
-
const activeCount = existing.records.filter((record) => record.revokedAt === null).length;
|
|
1255
|
-
if (activeCount > 0 && input.force !== true) {
|
|
1256
|
-
return { ok: false, reason: 'trust-root-already-initialized' };
|
|
1257
|
-
}
|
|
1258
|
-
const keyPath = daemonActivationOperatorKeyPath();
|
|
1259
|
-
if (pathEntryPresent(keyPath) && input.force !== true) {
|
|
1260
|
-
return { ok: false, reason: 'operator-key-file-already-exists' };
|
|
1261
|
-
}
|
|
1262
|
-
const { privateKeyPem, publicKeyPem } = generateDaemonActivationOperatorKeypair();
|
|
1263
|
-
const keyId = sanitizeKeyId(`${hostname()}-${randomBytes(4).toString('hex')}`);
|
|
1264
|
-
const now = new Date().toISOString();
|
|
1265
|
-
const record = {
|
|
1266
|
-
keyId,
|
|
1267
|
-
publicKeyPem,
|
|
1268
|
-
createdAt: now,
|
|
1269
|
-
revokedAt: null,
|
|
1270
|
-
label: (input.label ?? '').slice(0, MAX_LABEL_LENGTH),
|
|
1271
|
-
};
|
|
1272
|
-
const home = resolve(homedir());
|
|
1273
|
-
assurePrivateDirectory(daemonActivationDirPath(), home);
|
|
1274
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
1275
|
-
let fd;
|
|
1276
|
-
try {
|
|
1277
|
-
if (pathEntryPresent(keyPath))
|
|
1278
|
-
unlinkSync(keyPath);
|
|
1279
|
-
fd = openSync(keyPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, PRIVATE_FILE_MODE);
|
|
1280
|
-
const initial = fstatSync(fd, { bigint: true });
|
|
1281
|
-
if (!safeOwnedFile(initial) || initial.size !== 0n)
|
|
1282
|
-
throw new Error('unsafe-operator-key-file');
|
|
1283
|
-
fchmodSync(fd, PRIVATE_FILE_MODE);
|
|
1284
|
-
writeAll(fd, Buffer.from(privateKeyPem, 'utf8'));
|
|
1285
|
-
fsyncSync(fd);
|
|
1286
|
-
closeSync(fd);
|
|
1287
|
-
fd = undefined;
|
|
1288
|
-
const keyAssurance = assurePrivateStoragePath(keyPath, 'file', 'secure-created', { anchorPath: home });
|
|
1289
|
-
if (!keyAssurance.ok)
|
|
1290
|
-
throw new Error(`unsafe-operator-key-path:${keyAssurance.reason}`);
|
|
1291
|
-
fsyncDirectory(dirname(keyPath));
|
|
1292
|
-
}
|
|
1293
|
-
catch (error) {
|
|
1294
|
-
return { ok: false, reason: error.message || 'operator-key-write-failed' };
|
|
1295
|
-
}
|
|
1296
|
-
finally {
|
|
1297
|
-
if (fd !== undefined) {
|
|
1298
|
-
try {
|
|
1299
|
-
closeSync(fd);
|
|
1300
|
-
}
|
|
1301
|
-
catch { /* best-effort */ }
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
writeCanonicalJsonArrayFileAtomic(daemonActivationTrustRootsPath(), [...existing.records, record], home);
|
|
1305
|
-
auditActivationEvent('daemon-activation:init', `trust root registered: keyId=${keyId} label=${JSON.stringify(record.label)} rotated=${activeCount > 0}`, 'ok');
|
|
1306
|
-
return { ok: true, keyId, publicKeyPem, rotated: activeCount > 0 };
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
export function daemonActivationRevokeTrustRoot(keyId) {
|
|
1310
|
-
if (!KEY_ID_RE.test(keyId))
|
|
1311
|
-
return { ok: false, reason: 'invalid-key-id' };
|
|
1312
|
-
return withActivationStoreLock(() => {
|
|
1313
|
-
const existing = loadTrustRootStore();
|
|
1314
|
-
if (!existing.ok)
|
|
1315
|
-
return { ok: false, reason: existing.reason };
|
|
1316
|
-
const index = existing.records.findIndex((record) => record.keyId === keyId);
|
|
1317
|
-
if (index === -1)
|
|
1318
|
-
return { ok: false, reason: 'trust-root-not-found' };
|
|
1319
|
-
if (existing.records[index].revokedAt !== null) {
|
|
1320
|
-
auditActivationEvent('daemon-activation:revoke-root', `trust root already revoked: keyId=${keyId}`, 'ok');
|
|
1321
|
-
return { ok: true };
|
|
1322
|
-
}
|
|
1323
|
-
const revokedAt = new Date().toISOString();
|
|
1324
|
-
const updated = existing.records.map((record, i) => (i === index ? { ...record, revokedAt } : record));
|
|
1325
|
-
writeCanonicalJsonArrayFileAtomic(daemonActivationTrustRootsPath(), updated, resolve(homedir()));
|
|
1326
|
-
auditActivationEvent('daemon-activation:revoke-root', `trust root revoked: keyId=${keyId}`, 'ok');
|
|
1327
|
-
return { ok: true };
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
export function daemonActivationRevokeGrant(grantId) {
|
|
1331
|
-
if (!ID_RE.test(grantId))
|
|
1332
|
-
return { ok: false, reason: 'invalid-grant-id' };
|
|
1333
|
-
return withActivationStoreLock(() => {
|
|
1334
|
-
const existing = loadGrantStore();
|
|
1335
|
-
if (!existing.ok)
|
|
1336
|
-
return { ok: false, reason: existing.reason };
|
|
1337
|
-
const index = existing.records.findIndex((record) => record.grantId === grantId);
|
|
1338
|
-
if (index === -1)
|
|
1339
|
-
return { ok: false, reason: 'grant-not-found' };
|
|
1340
|
-
if (existing.records[index].revokedAt !== null) {
|
|
1341
|
-
auditActivationEvent('daemon-activation:revoke-grant', `standing grant already revoked: grantId=${grantId}`, 'ok');
|
|
1342
|
-
return { ok: true };
|
|
1343
|
-
}
|
|
1344
|
-
const revokedAt = new Date().toISOString();
|
|
1345
|
-
const updated = existing.records.map((record, i) => (i === index ? { ...record, revokedAt } : record));
|
|
1346
|
-
writeCanonicalJsonArrayFileAtomic(daemonActivationGrantsPath(), updated, resolve(homedir()));
|
|
1347
|
-
auditActivationEvent('daemon-activation:revoke-grant', `standing grant revoked: grantId=${grantId}`, 'ok');
|
|
1348
|
-
return { ok: true };
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
/** Mints a signed, expiring standing grant (conductor/automerge/repair/deploy/install/proposalOnly). */
|
|
1352
|
-
export function daemonActivationMintStandingGrant(input) {
|
|
1353
|
-
if (process.platform === 'win32') {
|
|
1354
|
-
return { ok: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
1355
|
-
}
|
|
1356
|
-
if (!Number.isFinite(input.ttlMs) || input.ttlMs <= 0 || input.ttlMs > MAX_STANDING_GRANT_VALIDITY_MS) {
|
|
1357
|
-
return { ok: false, reason: 'invalid-grant-ttl' };
|
|
1358
|
-
}
|
|
1359
|
-
if (!validScope(input.scope))
|
|
1360
|
-
return { ok: false, reason: 'invalid-grant-scope' };
|
|
1361
|
-
return withActivationStoreLock(() => {
|
|
1362
|
-
const trustStore = loadTrustRootStore();
|
|
1363
|
-
if (!trustStore.ok)
|
|
1364
|
-
return { ok: false, reason: trustStore.reason };
|
|
1365
|
-
const active = trustStore.records.filter((record) => record.revokedAt === null);
|
|
1366
|
-
if (active.length === 0)
|
|
1367
|
-
return { ok: false, reason: 'no-trusted-activation-roots' };
|
|
1368
|
-
if (active.length > 1)
|
|
1369
|
-
return { ok: false, reason: 'ambiguous-signing-key-run-activation-status' };
|
|
1370
|
-
const root = active[0];
|
|
1371
|
-
let privateKeyPem;
|
|
1372
|
-
try {
|
|
1373
|
-
privateKeyPem = readOperatorPrivateKeyPem();
|
|
1374
|
-
}
|
|
1375
|
-
catch (error) {
|
|
1376
|
-
return { ok: false, reason: error.message || 'operator-key-unreadable' };
|
|
1377
|
-
}
|
|
1378
|
-
const existingGrants = loadGrantStore();
|
|
1379
|
-
if (!existingGrants.ok)
|
|
1380
|
-
return { ok: false, reason: existingGrants.reason };
|
|
1381
|
-
const now = Date.now();
|
|
1382
|
-
const record = signDaemonActivationGrant({
|
|
1383
|
-
grantId: randomBytes(16).toString('hex'),
|
|
1384
|
-
keyId: root.keyId,
|
|
1385
|
-
issuedAt: new Date(now).toISOString(),
|
|
1386
|
-
expiresAt: new Date(now + input.ttlMs).toISOString(),
|
|
1387
|
-
scope: input.scope,
|
|
1388
|
-
privateKey: privateKeyPem,
|
|
1389
|
-
});
|
|
1390
|
-
writeCanonicalJsonArrayFileAtomic(daemonActivationGrantsPath(), [...existingGrants.records, record], resolve(homedir()));
|
|
1391
|
-
auditActivationEvent('daemon-activation:grant', `standing grant minted: grantId=${record.grantId} keyId=${root.keyId} `
|
|
1392
|
-
+ `scope=${canonicalizeDaemonActivationValue(record.scope)} expiresAt=${record.expiresAt}`, 'ok');
|
|
1393
|
-
return { ok: true, record };
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
/**
|
|
1397
|
-
* Mints and pins a single-use activation permit (once/resident scopes) to
|
|
1398
|
-
* daemonActivationPermitPath(), ready for `ashlr daemon start` to consume.
|
|
1399
|
-
* Signs against THIS exact machine's current build (executable/entrypoint/
|
|
1400
|
-
* release-tree hashes) — an operator runs this locally, immediately before
|
|
1401
|
-
* starting the daemon it authorizes.
|
|
1402
|
-
*/
|
|
1403
|
-
export function daemonActivationMintOneShotPermit(input) {
|
|
1404
|
-
if (process.platform === 'win32') {
|
|
1405
|
-
return { ok: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
1406
|
-
}
|
|
1407
|
-
if (!validScope(input.scope))
|
|
1408
|
-
return { ok: false, reason: 'invalid-grant-scope' };
|
|
1409
|
-
return withActivationStoreLock(() => {
|
|
1410
|
-
const trustStore = loadTrustRootStore();
|
|
1411
|
-
if (!trustStore.ok)
|
|
1412
|
-
return { ok: false, reason: trustStore.reason };
|
|
1413
|
-
const active = trustStore.records.filter((record) => record.revokedAt === null);
|
|
1414
|
-
if (active.length === 0)
|
|
1415
|
-
return { ok: false, reason: 'no-trusted-activation-roots' };
|
|
1416
|
-
if (active.length > 1)
|
|
1417
|
-
return { ok: false, reason: 'ambiguous-signing-key-run-activation-status' };
|
|
1418
|
-
const root = active[0];
|
|
1419
|
-
let privateKeyPem;
|
|
1420
|
-
try {
|
|
1421
|
-
privateKeyPem = readOperatorPrivateKeyPem();
|
|
1422
|
-
}
|
|
1423
|
-
catch (error) {
|
|
1424
|
-
return { ok: false, reason: error.message || 'operator-key-unreadable' };
|
|
1425
|
-
}
|
|
1426
|
-
let configSnapshot;
|
|
1427
|
-
try {
|
|
1428
|
-
configSnapshot = strictConfigSnapshot(input.cfg);
|
|
1429
|
-
}
|
|
1430
|
-
catch {
|
|
1431
|
-
return { ok: false, reason: 'activation-config-not-strict-json' };
|
|
1432
|
-
}
|
|
1433
|
-
// Cheap, context-independent check first: no point collecting a full
|
|
1434
|
-
// runtime context (file hashing, release-tree walk) if there is already
|
|
1435
|
-
// a pending permit occupying the single fixed path.
|
|
1436
|
-
const permitPath = daemonActivationPermitPath();
|
|
1437
|
-
if (pathEntryPresent(permitPath))
|
|
1438
|
-
return { ok: false, reason: 'activation-permit-already-pending' };
|
|
1439
|
-
let context;
|
|
1440
|
-
try {
|
|
1441
|
-
context = collectRuntimeContext(configSnapshot);
|
|
1442
|
-
}
|
|
1443
|
-
catch (error) {
|
|
1444
|
-
return { ok: false, reason: `runtime-context-unavailable:${error.message}` };
|
|
1445
|
-
}
|
|
1446
|
-
if (!context.killSwitchOff)
|
|
1447
|
-
return { ok: false, reason: 'kill-switch-is-on' };
|
|
1448
|
-
if (!context.guardHealthHealthy)
|
|
1449
|
-
return { ok: false, reason: 'guard-health-degraded' };
|
|
1450
|
-
if (!validBuildIdentity(context.buildIdentity)) {
|
|
1451
|
-
// Most commonly: a `git` provenance build with a dirty working tree
|
|
1452
|
-
// (dist/ was built from uncommitted changes). Fail closed — a permit
|
|
1453
|
-
// bound to an untrusted build identity would never verify anyway.
|
|
1454
|
-
return { ok: false, reason: 'runtime-build-identity-untrusted' };
|
|
1455
|
-
}
|
|
1456
|
-
const now = Date.now();
|
|
1457
|
-
const permitId = randomBytes(16).toString('hex');
|
|
1458
|
-
let payload;
|
|
1459
|
-
let envelope;
|
|
1460
|
-
try {
|
|
1461
|
-
payload = buildDaemonActivationPermitPayload({
|
|
1462
|
-
permitId,
|
|
1463
|
-
nonce: randomBytes(32).toString('hex'),
|
|
1464
|
-
keyId: root.keyId,
|
|
1465
|
-
issuedAt: new Date(now).toISOString(),
|
|
1466
|
-
expiresAt: new Date(now + MAX_VALIDITY_MS).toISOString(),
|
|
1467
|
-
scope: input.scope,
|
|
1468
|
-
context,
|
|
1469
|
-
});
|
|
1470
|
-
envelope = signDaemonActivationPermit(payload, privateKeyPem);
|
|
1471
|
-
}
|
|
1472
|
-
catch (error) {
|
|
1473
|
-
return { ok: false, reason: error.message || 'activation-permit-build-failed' };
|
|
1474
|
-
}
|
|
1475
|
-
const home = resolve(homedir());
|
|
1476
|
-
assurePrivateDirectory(dirname(permitPath), home);
|
|
1477
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
1478
|
-
let fd;
|
|
1479
|
-
try {
|
|
1480
|
-
fd = openSync(permitPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, PRIVATE_FILE_MODE);
|
|
1481
|
-
const initial = fstatSync(fd, { bigint: true });
|
|
1482
|
-
if (!safeOwnedFile(initial) || initial.size !== 0n)
|
|
1483
|
-
throw new Error('unsafe-activation-permit-file');
|
|
1484
|
-
fchmodSync(fd, PRIVATE_FILE_MODE);
|
|
1485
|
-
const bytes = Buffer.from(`${canonicalizeDaemonActivationValue(envelope)}\n`, 'utf8');
|
|
1486
|
-
writeAll(fd, bytes);
|
|
1487
|
-
fsyncSync(fd);
|
|
1488
|
-
closeSync(fd);
|
|
1489
|
-
fd = undefined;
|
|
1490
|
-
const assurance = assurePrivateStoragePath(permitPath, 'file', 'secure-created', { anchorPath: home });
|
|
1491
|
-
if (!assurance.ok)
|
|
1492
|
-
throw new Error(`unsafe-activation-permit-path:${assurance.reason}`);
|
|
1493
|
-
fsyncDirectory(dirname(permitPath));
|
|
1494
|
-
}
|
|
1495
|
-
catch (error) {
|
|
1496
|
-
return { ok: false, reason: error.message || 'activation-permit-write-failed' };
|
|
1497
|
-
}
|
|
1498
|
-
finally {
|
|
1499
|
-
if (fd !== undefined) {
|
|
1500
|
-
try {
|
|
1501
|
-
closeSync(fd);
|
|
1502
|
-
}
|
|
1503
|
-
catch { /* best-effort */ }
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
auditActivationEvent('daemon-activation:grant', `one-shot permit minted: permitId=${permitId} keyId=${root.keyId} `
|
|
1507
|
-
+ `scope=${canonicalizeDaemonActivationValue(input.scope)} expiresAt=${payload.expiresAt}`, 'ok');
|
|
1508
|
-
return { ok: true, permitId, permitPath, expiresAt: payload.expiresAt };
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
// ---------------------------------------------------------------------------
|
|
1512
|
-
// One-shot permit inspection / consumption — `ashlr daemon start` [--once].
|
|
1513
|
-
// ---------------------------------------------------------------------------
|
|
1514
|
-
function activationReadiness(state, reason, trustRootCount, extra) {
|
|
677
|
+
function activationReadiness(state, reason, trustRootCount) {
|
|
1515
678
|
return {
|
|
1516
679
|
schemaVersion: 1,
|
|
1517
680
|
policyVersion: POLICY_VERSION,
|
|
@@ -1519,234 +682,112 @@ function activationReadiness(state, reason, trustRootCount, extra) {
|
|
|
1519
682
|
sourceState: state === 'degraded' ? 'degraded' : 'healthy',
|
|
1520
683
|
state,
|
|
1521
684
|
commandEligible: state === 'ready',
|
|
1522
|
-
requestedShape:
|
|
685
|
+
requestedShape: 'proposal-once',
|
|
1523
686
|
trustRootCount,
|
|
1524
|
-
residentAuthorized:
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
repairAuthorized: extra?.repairAuthorized ?? false,
|
|
687
|
+
residentAuthorized: false,
|
|
688
|
+
installAuthorized: false,
|
|
689
|
+
repairAuthorized: false,
|
|
1528
690
|
reason,
|
|
1529
691
|
};
|
|
1530
692
|
}
|
|
1531
|
-
|
|
1532
|
-
function requestedShapeLabel(shape) {
|
|
1533
|
-
return shape === 'once' ? 'proposal-once' : 'resident';
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* What activation shape (if any) `opts` is structurally eligible to have
|
|
1537
|
-
* authorized at all — independent of whether any permit/trust root exists.
|
|
1538
|
-
* `null` means "denied before ever reading a permit," matching the pre-M470
|
|
1539
|
-
* behavior for drain modes and any dry-run other than {once:true,dryRun:true}.
|
|
1540
|
-
*/
|
|
1541
|
-
function requestedActivationShape(opts) {
|
|
1542
|
-
if (opts.drain !== undefined || opts.drainLimit !== undefined)
|
|
1543
|
-
return null;
|
|
1544
|
-
if (opts.dryRun === true)
|
|
1545
|
-
return null;
|
|
1546
|
-
if (opts.once === true)
|
|
1547
|
-
return 'once';
|
|
1548
|
-
if (opts.once === false)
|
|
1549
|
-
return 'resident';
|
|
1550
|
-
return null;
|
|
1551
|
-
}
|
|
1552
|
-
function needsPermit(opts) {
|
|
1553
|
-
return !(opts.once === true && opts.dryRun === true);
|
|
1554
|
-
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Read-only diagnostic (never authorizes, never consumes) for `ashlr
|
|
1557
|
-
* activation status`: peeks the pending one-shot permit, recomputes the
|
|
1558
|
-
* current runtime context, and reports exactly which binding field(s)
|
|
1559
|
-
* differ and what the two values were. `permit-runtime-binding-mismatch`
|
|
1560
|
-
* alone can't tell an operator what to fix — this can.
|
|
1561
|
-
*/
|
|
1562
|
-
export function daemonActivationDiagnosePendingPermitBindings(cfg) {
|
|
1563
|
-
const permitPath = daemonActivationPermitPath();
|
|
1564
|
-
if (process.platform === 'win32') {
|
|
1565
|
-
return { ok: false, reason: 'activation-permit-v1-unsupported-on-windows' };
|
|
1566
|
-
}
|
|
1567
|
-
let pinned;
|
|
693
|
+
function pathEntryPresent(path) {
|
|
1568
694
|
try {
|
|
1569
|
-
|
|
695
|
+
lstatSync(path);
|
|
696
|
+
return true;
|
|
1570
697
|
}
|
|
1571
698
|
catch (error) {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
? 'activation-permit-missing'
|
|
1576
|
-
: 'activation-permit-inspection-failed',
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
|
-
try {
|
|
1580
|
-
let envelope;
|
|
1581
|
-
try {
|
|
1582
|
-
envelope = JSON.parse(pinned.text);
|
|
1583
|
-
}
|
|
1584
|
-
catch {
|
|
1585
|
-
return { ok: false, reason: 'invalid-permit-json' };
|
|
1586
|
-
}
|
|
1587
|
-
const parsed = parseEnvelope(envelope);
|
|
1588
|
-
if (!parsed)
|
|
1589
|
-
return { ok: false, reason: 'invalid-permit-schema' };
|
|
1590
|
-
let configSnapshot;
|
|
1591
|
-
try {
|
|
1592
|
-
configSnapshot = strictConfigSnapshot(cfg);
|
|
1593
|
-
}
|
|
1594
|
-
catch {
|
|
1595
|
-
return { ok: false, reason: 'activation-config-not-strict-json' };
|
|
1596
|
-
}
|
|
1597
|
-
let context;
|
|
1598
|
-
try {
|
|
1599
|
-
context = collectRuntimeContext(configSnapshot);
|
|
1600
|
-
}
|
|
1601
|
-
catch (error) {
|
|
1602
|
-
return { ok: false, reason: `runtime-context-unavailable:${error.message}` };
|
|
1603
|
-
}
|
|
1604
|
-
if (!context.killSwitchOff)
|
|
1605
|
-
return { ok: false, reason: 'kill-switch-is-on' };
|
|
1606
|
-
if (!context.guardHealthHealthy)
|
|
1607
|
-
return { ok: false, reason: 'guard-health-degraded' };
|
|
1608
|
-
const expectedBindings = {
|
|
1609
|
-
configDigest: context.configDigest,
|
|
1610
|
-
buildIdentity: context.buildIdentity,
|
|
1611
|
-
executable: context.executable,
|
|
1612
|
-
entrypoint: context.entrypoint,
|
|
1613
|
-
releaseTree: context.releaseTree,
|
|
1614
|
-
authorityStateDigest: context.authorityStateDigest,
|
|
1615
|
-
killSwitch: 'off',
|
|
1616
|
-
guardHealth: 'healthy',
|
|
1617
|
-
};
|
|
1618
|
-
const mismatchedKeys = bindingFieldMismatches(parsed.payload.bindings, expectedBindings);
|
|
1619
|
-
if (mismatchedKeys.length === 0)
|
|
1620
|
-
return { ok: true, reason: 'bindings-match' };
|
|
1621
|
-
const mismatches = {};
|
|
1622
|
-
for (const key of mismatchedKeys) {
|
|
1623
|
-
mismatches[key] = { signed: parsed.payload.bindings[key], current: expectedBindings[key] };
|
|
1624
|
-
}
|
|
1625
|
-
return { ok: false, reason: 'permit-runtime-binding-mismatch', mismatches };
|
|
1626
|
-
}
|
|
1627
|
-
finally {
|
|
1628
|
-
try {
|
|
1629
|
-
closeSync(pinned.fd);
|
|
1630
|
-
}
|
|
1631
|
-
catch { /* diagnostic-only; best-effort close */ }
|
|
699
|
+
if (error.code === 'ENOENT')
|
|
700
|
+
return false;
|
|
701
|
+
throw error;
|
|
1632
702
|
}
|
|
1633
703
|
}
|
|
1634
|
-
function inspectPinnedActivationPermit(pinned, permitPath, cfg, trustRoots, suppliedContext
|
|
1635
|
-
const shapeLabel = requestedShapeLabel(requestedShape);
|
|
1636
|
-
const extraBase = {
|
|
1637
|
-
requestedShape: shapeLabel,
|
|
1638
|
-
installAuthorized: standingAuth.install,
|
|
1639
|
-
repairAuthorized: standingAuth.repair,
|
|
1640
|
-
residentStandingAuthorized: standingAuth.residentStanding,
|
|
1641
|
-
};
|
|
704
|
+
function inspectPinnedActivationPermit(pinned, permitPath, cfg, trustRoots, suppliedContext) {
|
|
1642
705
|
let envelope;
|
|
1643
706
|
try {
|
|
1644
707
|
envelope = JSON.parse(pinned.text);
|
|
1645
708
|
}
|
|
1646
709
|
catch {
|
|
1647
|
-
return activationReadiness('degraded', 'invalid-permit-json', trustRoots.length
|
|
710
|
+
return activationReadiness('degraded', 'invalid-permit-json', trustRoots.length);
|
|
1648
711
|
}
|
|
1649
712
|
if (`${canonicalizeDaemonActivationValue(envelope)}\n` !== pinned.text) {
|
|
1650
|
-
return activationReadiness('degraded', 'noncanonical-permit-encoding', trustRoots.length
|
|
713
|
+
return activationReadiness('degraded', 'noncanonical-permit-encoding', trustRoots.length);
|
|
1651
714
|
}
|
|
1652
715
|
const parsed = parseEnvelope(envelope);
|
|
1653
716
|
if (!parsed) {
|
|
1654
|
-
return activationReadiness('degraded', 'invalid-permit-schema', trustRoots.length
|
|
717
|
+
return activationReadiness('degraded', 'invalid-permit-schema', trustRoots.length);
|
|
1655
718
|
}
|
|
1656
719
|
let configSnapshot;
|
|
1657
720
|
try {
|
|
1658
721
|
configSnapshot = strictConfigSnapshot(cfg);
|
|
1659
722
|
}
|
|
1660
723
|
catch {
|
|
1661
|
-
return activationReadiness('degraded', 'activation-config-not-strict-json', trustRoots.length
|
|
724
|
+
return activationReadiness('degraded', 'activation-config-not-strict-json', trustRoots.length);
|
|
1662
725
|
}
|
|
1663
726
|
const context = suppliedContext ?? collectRuntimeContext(configSnapshot);
|
|
1664
727
|
if (context.configDigest !== daemonActivationConfigDigest(configSnapshot)) {
|
|
1665
|
-
return activationReadiness('blocked', 'runtime-config-digest-mismatch', trustRoots.length
|
|
728
|
+
return activationReadiness('blocked', 'runtime-config-digest-mismatch', trustRoots.length);
|
|
1666
729
|
}
|
|
1667
730
|
const verification = verifyDaemonActivationPermit(parsed, context, trustRoots);
|
|
1668
731
|
if (!verification.ok || !verification.permitId) {
|
|
1669
|
-
return activationReadiness('blocked', verification.reason, trustRoots.length
|
|
1670
|
-
}
|
|
1671
|
-
const residentAuthorized = parsed.payload.scope.resident === true;
|
|
1672
|
-
if (parsed.payload.scope[requestedShape] !== true) {
|
|
1673
|
-
return activationReadiness('blocked', 'activation-permit-scope-does-not-grant-requested-shape', trustRoots.length, { ...extraBase, residentAuthorized });
|
|
732
|
+
return activationReadiness('blocked', verification.reason, trustRoots.length);
|
|
1674
733
|
}
|
|
1675
734
|
const nonceDigest = sha256(parsed.payload.nonce);
|
|
1676
735
|
if (pathEntryPresent(daemonActivationReceiptPath(verification.permitId))
|
|
1677
736
|
|| pathEntryPresent(daemonActivationNonceReceiptPath(nonceDigest))) {
|
|
1678
|
-
return activationReadiness('blocked', 'activation-permit-already-consumed', trustRoots.length
|
|
737
|
+
return activationReadiness('blocked', 'activation-permit-already-consumed', trustRoots.length);
|
|
1679
738
|
}
|
|
1680
739
|
const openedAfterInspection = fstatSync(pinned.fd, { bigint: true });
|
|
1681
740
|
const namedAfterInspection = lstatSync(permitPath, { bigint: true });
|
|
1682
741
|
if (!sameFileSnapshot(pinned.stat, openedAfterInspection)
|
|
1683
742
|
|| !sameFileSnapshot(openedAfterInspection, namedAfterInspection)) {
|
|
1684
|
-
return activationReadiness('degraded', 'activation-permit-changed-during-inspection', trustRoots.length
|
|
743
|
+
return activationReadiness('degraded', 'activation-permit-changed-during-inspection', trustRoots.length);
|
|
1685
744
|
}
|
|
1686
|
-
|
|
1687
|
-
return activationReadiness('ready', readyReason, trustRoots.length, { ...extraBase, residentAuthorized });
|
|
745
|
+
return activationReadiness('ready', 'valid-proposal-once-permit', trustRoots.length);
|
|
1688
746
|
}
|
|
1689
747
|
function inspectWithAuthority(cfg, opts, trustRoots, suppliedContext, platform = process.platform) {
|
|
1690
|
-
const standingAuth = {
|
|
1691
|
-
install: daemonActivationScopeGranted('install').granted,
|
|
1692
|
-
repair: daemonActivationScopeGranted('repair').granted,
|
|
1693
|
-
residentStanding: daemonActivationScopeGranted('residentStanding').granted,
|
|
1694
|
-
};
|
|
1695
|
-
const standingAuthorizedFields = {
|
|
1696
|
-
installAuthorized: standingAuth.install,
|
|
1697
|
-
repairAuthorized: standingAuth.repair,
|
|
1698
|
-
residentStandingAuthorized: standingAuth.residentStanding,
|
|
1699
|
-
};
|
|
1700
748
|
if (!needsPermit(opts)) {
|
|
1701
|
-
return activationReadiness('ready', 'dry-run-once-does-not-require-activation-permit', trustRoots.length
|
|
749
|
+
return activationReadiness('ready', 'dry-run-once-does-not-require-activation-permit', trustRoots.length);
|
|
1702
750
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
return activationReadiness('blocked', 'activation-permit-cannot-authorize-requested-start-shape', trustRoots.length, standingAuthorizedFields);
|
|
751
|
+
if (!supportedProposalOnceShape(opts)) {
|
|
752
|
+
return activationReadiness('blocked', 'activation-permit-cannot-authorize-requested-start-shape', trustRoots.length);
|
|
1706
753
|
}
|
|
1707
|
-
const shapeLabel = requestedShapeLabel(requestedShape);
|
|
1708
754
|
if (trustRoots.length === 0) {
|
|
1709
|
-
return activationReadiness('blocked', 'no-trusted-activation-roots', 0
|
|
1710
|
-
requestedShape: shapeLabel,
|
|
1711
|
-
...standingAuthorizedFields,
|
|
1712
|
-
});
|
|
755
|
+
return activationReadiness('blocked', 'no-trusted-activation-roots', 0);
|
|
1713
756
|
}
|
|
1714
757
|
if (platform === 'win32') {
|
|
1715
|
-
return activationReadiness('blocked', 'activation-permit-v1-unsupported-on-windows', trustRoots.length
|
|
1716
|
-
}
|
|
1717
|
-
// A valid standing `residentStanding` grant means `daemon start` (resident)
|
|
1718
|
-
// would succeed RIGHT NOW without ever touching the one-shot permit file —
|
|
1719
|
-
// report that directly instead of inspecting a possibly-stale/unreadable
|
|
1720
|
-
// one-shot permit that resident no longer depends on. This is the fix for
|
|
1721
|
-
// status previously showing `resident: degraded
|
|
1722
|
-
// (activation-permit-inspection-failed)` when a stale one-shot permit file
|
|
1723
|
-
// was lying around: that inspection is simply skipped once a standing grant
|
|
1724
|
-
// covers it.
|
|
1725
|
-
if (requestedShape === 'resident' && standingAuth.residentStanding) {
|
|
1726
|
-
return activationReadiness('ready', 'ready-via-standing-resident-grant', trustRoots.length, { requestedShape: shapeLabel, residentAuthorized: true, ...standingAuthorizedFields });
|
|
758
|
+
return activationReadiness('blocked', 'activation-permit-v1-unsupported-on-windows', trustRoots.length);
|
|
1727
759
|
}
|
|
1728
760
|
const permitPath = daemonActivationPermitPath();
|
|
1729
761
|
let pinned;
|
|
1730
762
|
let result;
|
|
1731
763
|
try {
|
|
1732
764
|
pinned = openPinnedPermit(permitPath, resolve(homedir()));
|
|
1733
|
-
result = inspectPinnedActivationPermit(pinned, permitPath, cfg, trustRoots, suppliedContext
|
|
765
|
+
result = inspectPinnedActivationPermit(pinned, permitPath, cfg, trustRoots, suppliedContext);
|
|
1734
766
|
}
|
|
1735
767
|
catch (error) {
|
|
1736
768
|
result = activationReadiness(error.code === 'ENOENT' ? 'blocked' : 'degraded', error.code === 'ENOENT'
|
|
1737
769
|
? 'activation-permit-missing'
|
|
1738
|
-
: 'activation-permit-inspection-failed', trustRoots.length
|
|
770
|
+
: 'activation-permit-inspection-failed', trustRoots.length);
|
|
1739
771
|
}
|
|
1740
772
|
if (pinned) {
|
|
1741
773
|
try {
|
|
1742
774
|
closeSync(pinned.fd);
|
|
1743
775
|
}
|
|
1744
776
|
catch {
|
|
1745
|
-
return activationReadiness('degraded', 'activation-permit-close-failed', trustRoots.length
|
|
777
|
+
return activationReadiness('degraded', 'activation-permit-close-failed', trustRoots.length);
|
|
1746
778
|
}
|
|
1747
779
|
}
|
|
1748
780
|
return result;
|
|
1749
781
|
}
|
|
782
|
+
function needsPermit(opts) {
|
|
783
|
+
return !(opts.once === true && opts.dryRun === true);
|
|
784
|
+
}
|
|
785
|
+
function supportedProposalOnceShape(opts) {
|
|
786
|
+
return opts.once === true
|
|
787
|
+
&& opts.dryRun !== true
|
|
788
|
+
&& opts.drain === undefined
|
|
789
|
+
&& opts.drainLimit === undefined;
|
|
790
|
+
}
|
|
1750
791
|
function consumeWithAuthority(cfg, opts, trustRoots, suppliedContext, afterReceiptPersisted, mayMintCapability) {
|
|
1751
792
|
if (!needsPermit(opts)) {
|
|
1752
793
|
return {
|
|
@@ -1755,8 +796,7 @@ function consumeWithAuthority(cfg, opts, trustRoots, suppliedContext, afterRecei
|
|
|
1755
796
|
reason: 'dry-run-once-does-not-require-activation-permit',
|
|
1756
797
|
};
|
|
1757
798
|
}
|
|
1758
|
-
|
|
1759
|
-
if (!requestedShape) {
|
|
799
|
+
if (!supportedProposalOnceShape(opts)) {
|
|
1760
800
|
return {
|
|
1761
801
|
authorized: false,
|
|
1762
802
|
required: true,
|
|
@@ -1828,15 +868,6 @@ function consumeWithAuthority(cfg, opts, trustRoots, suppliedContext, afterRecei
|
|
|
1828
868
|
result = { authorized: false, required: true, reason: 'invalid-permit-schema' };
|
|
1829
869
|
return result;
|
|
1830
870
|
}
|
|
1831
|
-
if (parsed.payload.scope[requestedShape] !== true) {
|
|
1832
|
-
result = {
|
|
1833
|
-
authorized: false,
|
|
1834
|
-
required: true,
|
|
1835
|
-
reason: 'activation-permit-scope-does-not-grant-requested-shape',
|
|
1836
|
-
permitId: verification.permitId,
|
|
1837
|
-
};
|
|
1838
|
-
return result;
|
|
1839
|
-
}
|
|
1840
871
|
const receiptsDir = dirname(daemonActivationReceiptPath(verification.permitId));
|
|
1841
872
|
assurePrivateDirectory(receiptsDir, home);
|
|
1842
873
|
const receiptPath = daemonActivationReceiptPath(verification.permitId);
|
|
@@ -1918,12 +949,12 @@ function consumeWithAuthority(cfg, opts, trustRoots, suppliedContext, afterRecei
|
|
|
1918
949
|
result = {
|
|
1919
950
|
authorized: true,
|
|
1920
951
|
required: true,
|
|
1921
|
-
reason:
|
|
952
|
+
reason: 'proposal-once-activation-authorized',
|
|
1922
953
|
permitId: verification.permitId,
|
|
1923
954
|
receiptPath,
|
|
1924
955
|
...(mayMintCapability
|
|
1925
956
|
? {
|
|
1926
|
-
capability: mintCapability(verification.permitId,
|
|
957
|
+
capability: mintCapability(verification.permitId, () => {
|
|
1927
958
|
const claimContext = collectRuntimeContext(configSnapshot);
|
|
1928
959
|
return verifyDaemonActivationPermit(parsed, claimContext, trustRoots).ok;
|
|
1929
960
|
}),
|
|
@@ -1961,54 +992,18 @@ function consumeWithAuthority(cfg, opts, trustRoots, suppliedContext, afterRecei
|
|
|
1961
992
|
}
|
|
1962
993
|
}
|
|
1963
994
|
/**
|
|
1964
|
-
* Runtime entrypoint.
|
|
1965
|
-
*
|
|
1966
|
-
* `ashlr activation init` sees the store as empty and gets today's exact
|
|
1967
|
-
* denial. Every consume attempt — authorized or refused — is audited.
|
|
995
|
+
* Runtime entrypoint. Its immutable repository-owned roots are the only path
|
|
996
|
+
* that can mint an in-process activation capability.
|
|
1968
997
|
*/
|
|
1969
998
|
export function consumeDaemonActivationPermit(cfg, opts) {
|
|
1970
|
-
|
|
1971
|
-
// is what makes an unattended, launchd/systemd/Task-Scheduler-triggered
|
|
1972
|
-
// restart after reboot possible: there is nobody present to mint a fresh
|
|
1973
|
-
// one-shot permit, but a still-valid, still-signed, non-revoked standing
|
|
1974
|
-
// grant needs no human in the loop. It goes through every check
|
|
1975
|
-
// `daemonActivationScopeGranted` does (platform, kill switch, guard health,
|
|
1976
|
-
// trust root, expiry, revocation, signature) — deliberately NOT the
|
|
1977
|
-
// one-shot permit's build-identity binding, matching every other standing
|
|
1978
|
-
// grant (conductor/install/automerge/repair/deploy), because a standing
|
|
1979
|
-
// grant must keep working across routine rebuilds within its expiry
|
|
1980
|
-
// window. If no standing grant matches, this falls through to the
|
|
1981
|
-
// one-shot permit path unchanged — an operator who has only ever granted
|
|
1982
|
-
// the one-shot form (or nothing) sees EXACTLY today's behavior.
|
|
1983
|
-
if (requestedActivationShape(opts) === 'resident') {
|
|
1984
|
-
const standing = findGrantedRecord('residentStanding');
|
|
1985
|
-
if (standing.granted) {
|
|
1986
|
-
const result = {
|
|
1987
|
-
authorized: true,
|
|
1988
|
-
required: true,
|
|
1989
|
-
reason: `resident-standing-activation-authorized:${standing.record.grantId}`,
|
|
1990
|
-
scope: standing.record.scope,
|
|
1991
|
-
grantId: standing.record.grantId,
|
|
1992
|
-
};
|
|
1993
|
-
auditActivationEvent('daemon-activation:consume', `daemon activation consume authorized: ${result.reason}`, 'ok');
|
|
1994
|
-
return result;
|
|
1995
|
-
}
|
|
1996
|
-
auditActivationEvent('daemon-activation:resident-standing-check', `resident standing grant unavailable (${standing.reason}); falling back to one-shot permit`, 'refused');
|
|
1997
|
-
}
|
|
1998
|
-
const result = consumeWithAuthority(cfg, opts, loadDaemonActivationTrustRoots(), undefined, undefined, true);
|
|
1999
|
-
// The dry-run-once bypass (`required: false`) never touches a permit or
|
|
2000
|
-
// any authority state — it is not a consume attempt, so nothing is audited.
|
|
2001
|
-
if (result.required) {
|
|
2002
|
-
auditActivationEvent('daemon-activation:consume', `daemon activation consume ${result.authorized ? 'authorized' : 'refused'}: ${result.reason}`, result.authorized ? 'ok' : 'refused');
|
|
2003
|
-
}
|
|
2004
|
-
return result;
|
|
999
|
+
return consumeWithAuthority(cfg, opts, DAEMON_ACTIVATION_TRUST_ROOTS, undefined, undefined, true);
|
|
2005
1000
|
}
|
|
2006
1001
|
/**
|
|
2007
1002
|
* Read-only advisory inspection. A ready result is never authority: daemon
|
|
2008
1003
|
* start must still reopen, revalidate, durably consume, and mint a capability.
|
|
2009
1004
|
*/
|
|
2010
1005
|
export function inspectDaemonActivationPermit(cfg, opts) {
|
|
2011
|
-
return inspectWithAuthority(cfg, opts,
|
|
1006
|
+
return inspectWithAuthority(cfg, opts, DAEMON_ACTIVATION_TRUST_ROOTS, undefined);
|
|
2012
1007
|
}
|
|
2013
1008
|
/** Test-only inspection with injected roots/context; it cannot mint authority. */
|
|
2014
1009
|
export function inspectDaemonActivationPermitForVerification(cfg, opts, options) {
|