@ashlr/hub 3.2.6 → 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 +290 -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 +37 -11
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/conductor-permit.d.ts +1 -0
- package/dist/cli/conductor-permit.js +122 -0
- package/dist/cli/conductor-permit.js.map +1 -0
- 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/goals.d.ts +2 -0
- package/dist/cli/goals.js +90 -5
- package/dist/cli/goals.js.map +1 -1
- package/dist/cli/help.js +21 -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 +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/loop.js +26 -5
- package/dist/cli/loop.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/comms/elon-dialogue.js +16 -7
- package/dist/core/comms/elon-dialogue.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 +357 -33
- package/dist/core/daemon/activation-permit.js +1585 -183
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/goal-conductor-permit-operator.d.ts +75 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js +842 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js.map +1 -0
- package/dist/core/daemon/loop.d.ts +13 -1
- package/dist/core/daemon/loop.js +294 -29
- 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/quota.d.ts +74 -7
- package/dist/core/fleet/quota.js +688 -34
- package/dist/core/fleet/quota.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 +7 -5
- 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/advance.js +34 -5
- package/dist/core/goals/advance.js.map +1 -1
- package/dist/core/goals/conductor-quota.d.ts +38 -0
- package/dist/core/goals/conductor-quota.js +110 -0
- package/dist/core/goals/conductor-quota.js.map +1 -0
- package/dist/core/goals/conductor-target.d.ts +24 -0
- package/dist/core/goals/conductor-target.js +35 -0
- package/dist/core/goals/conductor-target.js.map +1 -0
- package/dist/core/goals/conductor.d.ts +15 -0
- package/dist/core/goals/conductor.js +116 -9
- package/dist/core/goals/conductor.js.map +1 -1
- package/dist/core/goals/store.d.ts +29 -5
- package/dist/core/goals/store.js +197 -30
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/goals/timestamp-repair.d.ts +50 -0
- package/dist/core/goals/timestamp-repair.js +740 -0
- package/dist/core/goals/timestamp-repair.js.map +1 -0
- 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/integrations/pulse-sync.js +13 -4
- package/dist/core/integrations/pulse-sync.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/best-of-n.d.ts +18 -0
- package/dist/core/run/best-of-n.js +163 -18
- package/dist/core/run/best-of-n.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 +140 -60
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/strategy/goal-planner.js +7 -1
- package/dist/core/strategy/goal-planner.js.map +1 -1
- package/dist/core/swarm/planner.d.ts +2 -2
- package/dist/core/swarm/planner.js +52 -5
- package/dist/core/swarm/planner.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 +123 -11
- package/dist/core/swarm/runner.js.map +1 -1
- package/dist/core/types.d.ts +139 -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 +49 -34
- 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/dist/tui/app.js +2 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/render.d.ts +2 -0
- package/dist/tui/render.js +162 -4
- package/dist/tui/render.js.map +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,842 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes, } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants as fsConstants, fstatSync, existsSync, fchmodSync, lstatSync, openSync, opendirSync, readSync, realpathSync, fsyncSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fsyncDirectory } from '../util/durability.js';
|
|
6
|
+
import { assurePrivateStoragePath } from '../util/private-storage.js';
|
|
7
|
+
import { writeImmutablePrivateRecord, } from '../util/immutable-private-record-store.js';
|
|
8
|
+
import { resolveGoalConductorTarget, } from '../goals/conductor-target.js';
|
|
9
|
+
import { nextActionableMilestone } from '../goals/advance.js';
|
|
10
|
+
import { goalSnapshotDigest, listGoalsDetailed } from '../goals/store.js';
|
|
11
|
+
import { listEnrolled } from '../sandbox/policy.js';
|
|
12
|
+
import { GOAL_CONDUCTOR_ACTIVATION_TRUST_ROOTS, buildGoalConductorActivationPermitPayload, canonicalizeDaemonActivationValue, collectGoalConductorActivationContext, daemonActivationConfigDigest, goalConductorActivationNonceReceiptPath, goalConductorActivationReceiptPath, parseGoalConductorActivationPermitEnvelope, signGoalConductorActivationPermit, verifyGoalConductorActivationPermit, } from './activation-permit.js';
|
|
13
|
+
const REQUEST_POLICY = 'goal-conductor-permit-request-v1';
|
|
14
|
+
const SHA40_RE = /^[a-f0-9]{40}$/u;
|
|
15
|
+
const SHA64_RE = /^[a-f0-9]{64}$/u;
|
|
16
|
+
const MAX_OPERATOR_FILE_BYTES = 64 * 1024;
|
|
17
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
18
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
19
|
+
const PERMIT_LIFETIME_MS = 120_000;
|
|
20
|
+
const PERMIT_FILE_NAME = 'goal-conductor-activation-permit.json';
|
|
21
|
+
const PUBLICATION_ROOT_NAME = 'goal-conductor-permit-publication';
|
|
22
|
+
export const GOAL_CONDUCTOR_PERMIT_OPERATOR_TEST_CONTROL = Symbol.for('ashlr.goal-conductor-permit-operator.test-control.v1');
|
|
23
|
+
const TEST_CONTROL_STATE = Symbol.for('ashlr.goal-conductor-permit-operator.test-control.state.v1');
|
|
24
|
+
export function _setGoalConductorPermitOperatorTestControlForTest(sentinel, control) {
|
|
25
|
+
if (sentinel !== GOAL_CONDUCTOR_PERMIT_OPERATOR_TEST_CONTROL || process.env['VITEST'] !== 'true') {
|
|
26
|
+
throw new Error('goal conductor permit operator test control unavailable');
|
|
27
|
+
}
|
|
28
|
+
if (control === undefined)
|
|
29
|
+
Reflect.deleteProperty(globalThis, TEST_CONTROL_STATE);
|
|
30
|
+
else
|
|
31
|
+
Reflect.set(globalThis, TEST_CONTROL_STATE, Object.freeze({ ...control }));
|
|
32
|
+
}
|
|
33
|
+
function testControl() {
|
|
34
|
+
if (process.env['VITEST'] !== 'true')
|
|
35
|
+
return undefined;
|
|
36
|
+
return Reflect.get(globalThis, TEST_CONTROL_STATE);
|
|
37
|
+
}
|
|
38
|
+
function resolveLiveGoalConductorTarget(goalId) {
|
|
39
|
+
return resolveGoalConductorTarget(goalId, {
|
|
40
|
+
listGoalsDetailed,
|
|
41
|
+
listEnrolled,
|
|
42
|
+
nextActionableMilestone,
|
|
43
|
+
goalSnapshotDigest,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
function exactKeys(value, keys) {
|
|
50
|
+
const actual = Object.keys(value).sort();
|
|
51
|
+
const expected = [...keys].sort();
|
|
52
|
+
return actual.length === expected.length && actual.every((entry, index) => entry === expected[index]);
|
|
53
|
+
}
|
|
54
|
+
function iso(value) {
|
|
55
|
+
if (typeof value !== 'string')
|
|
56
|
+
return false;
|
|
57
|
+
const parsed = Date.parse(value);
|
|
58
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
59
|
+
}
|
|
60
|
+
function sha256(value) {
|
|
61
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
62
|
+
}
|
|
63
|
+
function sameSnapshot(left, right) {
|
|
64
|
+
return left.dev === right.dev
|
|
65
|
+
&& left.ino === right.ino
|
|
66
|
+
&& left.mode === right.mode
|
|
67
|
+
&& left.uid === right.uid
|
|
68
|
+
&& left.gid === right.gid
|
|
69
|
+
&& left.nlink === right.nlink
|
|
70
|
+
&& left.size === right.size
|
|
71
|
+
&& left.mtimeNs === right.mtimeNs
|
|
72
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
73
|
+
}
|
|
74
|
+
function ownedByCurrent(stat) {
|
|
75
|
+
return typeof process.getuid !== 'function' || stat.uid === BigInt(process.getuid());
|
|
76
|
+
}
|
|
77
|
+
function nestedWithin(anchor, candidate) {
|
|
78
|
+
const nested = relative(anchor, candidate);
|
|
79
|
+
return nested === '' || (nested !== '..' && !nested.startsWith(`..${sep}`) && !isAbsolute(nested));
|
|
80
|
+
}
|
|
81
|
+
function exactPrivateDirectory(path) {
|
|
82
|
+
try {
|
|
83
|
+
const stat = lstatSync(path, { bigint: true });
|
|
84
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && ownedByCurrent(stat)
|
|
85
|
+
&& (process.platform === 'win32' || (stat.mode & 511n) === BigInt(PRIVATE_DIRECTORY_MODE))
|
|
86
|
+
&& realpathSync(path) === path;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function canonicalHome() {
|
|
93
|
+
try {
|
|
94
|
+
const requested = resolve(homedir());
|
|
95
|
+
const canonical = realpathSync(requested);
|
|
96
|
+
return requested === canonical ? canonical : null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function inspectExistingPrivateControl() {
|
|
103
|
+
const home = canonicalHome();
|
|
104
|
+
if (!home)
|
|
105
|
+
return null;
|
|
106
|
+
const ashlrHome = join(home, '.ashlr');
|
|
107
|
+
const control = join(ashlrHome, 'control');
|
|
108
|
+
if (!exactPrivateDirectory(ashlrHome) || !exactPrivateDirectory(control))
|
|
109
|
+
return null;
|
|
110
|
+
const ashlrAssurance = assurePrivateStoragePath(ashlrHome, 'directory', 'inspect-existing', { anchorPath: home });
|
|
111
|
+
const controlAssurance = assurePrivateStoragePath(control, 'directory', 'inspect-existing', { anchorPath: home });
|
|
112
|
+
return ashlrAssurance.ok && controlAssurance.ok ? { home, ashlrHome, control } : null;
|
|
113
|
+
}
|
|
114
|
+
function nearestGitCheckout(path) {
|
|
115
|
+
let cursor = path;
|
|
116
|
+
for (;;) {
|
|
117
|
+
if (existsSync(join(cursor, '.git'))) {
|
|
118
|
+
try {
|
|
119
|
+
return realpathSync(cursor);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const parent = dirname(cursor);
|
|
126
|
+
if (parent === cursor)
|
|
127
|
+
return null;
|
|
128
|
+
cursor = parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function externalArtifactPathReason(path, context, peers = []) {
|
|
132
|
+
if (!isAbsolute(path) || resolve(path) !== path || basename(path) === '' || existsSync(path)) {
|
|
133
|
+
return 'goal-conductor-operator-output-must-be-new-absolute-canonical-path';
|
|
134
|
+
}
|
|
135
|
+
let parent;
|
|
136
|
+
try {
|
|
137
|
+
parent = realpathSync(dirname(path));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return 'goal-conductor-operator-output-parent-unavailable';
|
|
141
|
+
}
|
|
142
|
+
if (parent !== dirname(path) || !exactPrivateDirectory(parent)) {
|
|
143
|
+
return 'goal-conductor-operator-output-parent-not-exact-private';
|
|
144
|
+
}
|
|
145
|
+
const home = canonicalHome();
|
|
146
|
+
const gitRoot = nearestGitCheckout(parent);
|
|
147
|
+
const forbidden = [
|
|
148
|
+
...(home ? [join(home, '.ashlr')] : []),
|
|
149
|
+
...(gitRoot ? [gitRoot] : []),
|
|
150
|
+
...(context ? [context.releaseTree.path, context.target.projectPath] : []),
|
|
151
|
+
...peers.map((peer) => dirname(peer)),
|
|
152
|
+
].map((entry) => resolve(entry));
|
|
153
|
+
if (forbidden.some((entry) => nestedWithin(entry, path))) {
|
|
154
|
+
return 'goal-conductor-operator-output-inside-bound-or-custody-tree';
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function externalExistingArtifactReason(path, context) {
|
|
159
|
+
if (!isAbsolute(path) || resolve(path) !== path) {
|
|
160
|
+
return 'goal-conductor-operator-input-path-not-absolute-canonical';
|
|
161
|
+
}
|
|
162
|
+
let canonical;
|
|
163
|
+
try {
|
|
164
|
+
canonical = realpathSync(path);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return 'goal-conductor-operator-input-unavailable';
|
|
168
|
+
}
|
|
169
|
+
if (canonical !== path || !exactPrivateDirectory(dirname(path))) {
|
|
170
|
+
return 'goal-conductor-operator-input-parent-not-exact-private';
|
|
171
|
+
}
|
|
172
|
+
const home = canonicalHome();
|
|
173
|
+
const gitRoot = nearestGitCheckout(dirname(path));
|
|
174
|
+
const forbidden = [
|
|
175
|
+
...(home ? [join(home, '.ashlr')] : []),
|
|
176
|
+
...(gitRoot ? [gitRoot] : []),
|
|
177
|
+
context.releaseTree.path,
|
|
178
|
+
context.target.projectPath,
|
|
179
|
+
].map((entry) => resolve(entry));
|
|
180
|
+
return forbidden.some((entry) => nestedWithin(entry, path))
|
|
181
|
+
? 'goal-conductor-operator-input-inside-bound-tree'
|
|
182
|
+
: null;
|
|
183
|
+
}
|
|
184
|
+
function processGroupIds() {
|
|
185
|
+
const groups = new Set();
|
|
186
|
+
if (typeof process.getgid === 'function')
|
|
187
|
+
groups.add(BigInt(process.getgid()));
|
|
188
|
+
if (typeof process.getgroups === 'function') {
|
|
189
|
+
for (const gid of process.getgroups())
|
|
190
|
+
groups.add(BigInt(gid));
|
|
191
|
+
}
|
|
192
|
+
return groups;
|
|
193
|
+
}
|
|
194
|
+
function custodyAnchorForPath(path) {
|
|
195
|
+
const parent = dirname(path);
|
|
196
|
+
const home = canonicalHome();
|
|
197
|
+
return home && nestedWithin(home, parent) ? home : parent;
|
|
198
|
+
}
|
|
199
|
+
function inspectCustodyAncestors(path, expectedAnchor) {
|
|
200
|
+
if (process.platform === 'win32') {
|
|
201
|
+
throw new Error('goal-conductor-permit-operator-unsupported-on-windows');
|
|
202
|
+
}
|
|
203
|
+
const immediateParent = dirname(path);
|
|
204
|
+
const anchor = expectedAnchor ?? custodyAnchorForPath(path);
|
|
205
|
+
if (!isAbsolute(anchor) || resolve(anchor) !== anchor || !nestedWithin(anchor, immediateParent)) {
|
|
206
|
+
throw new Error('unsafe-custody-anchor');
|
|
207
|
+
}
|
|
208
|
+
const currentUid = typeof process.getuid === 'function' ? BigInt(process.getuid()) : null;
|
|
209
|
+
const allowedGroups = processGroupIds();
|
|
210
|
+
let cursor = immediateParent;
|
|
211
|
+
for (;;) {
|
|
212
|
+
const stat = lstatSync(cursor, { bigint: true });
|
|
213
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.nlink < 1n) {
|
|
214
|
+
throw new Error('unsafe-custody-ancestor');
|
|
215
|
+
}
|
|
216
|
+
const ownerAllowed = currentUid === null || stat.uid === currentUid || stat.uid === 0n;
|
|
217
|
+
const worldWritable = (stat.mode & 2n) !== 0n;
|
|
218
|
+
const groupWritable = (stat.mode & 16n) !== 0n;
|
|
219
|
+
const trustedGroupWrite = groupWritable && currentUid !== null
|
|
220
|
+
&& stat.uid === currentUid && allowedGroups.has(stat.gid);
|
|
221
|
+
if (!ownerAllowed || worldWritable || (groupWritable && !trustedGroupWrite)
|
|
222
|
+
|| realpathSync(cursor) !== cursor) {
|
|
223
|
+
throw new Error('unsafe-custody-ancestor');
|
|
224
|
+
}
|
|
225
|
+
if (cursor === immediateParent) {
|
|
226
|
+
if (!ownedByCurrent(stat) || (stat.mode & 511n) !== BigInt(PRIVATE_DIRECTORY_MODE)) {
|
|
227
|
+
throw new Error('unsafe-custody-parent');
|
|
228
|
+
}
|
|
229
|
+
const assurance = assurePrivateStoragePath(immediateParent, 'directory', 'inspect-owned', { anchorPath: anchor });
|
|
230
|
+
if (!assurance.ok)
|
|
231
|
+
throw new Error('unsafe-custody-parent-acl');
|
|
232
|
+
}
|
|
233
|
+
if (cursor === anchor)
|
|
234
|
+
return anchor;
|
|
235
|
+
const parent = dirname(cursor);
|
|
236
|
+
if (parent === cursor)
|
|
237
|
+
throw new Error('unsafe-custody-anchor');
|
|
238
|
+
cursor = parent;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function safePinnedPrivateFile(stat, maxBytes) {
|
|
242
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n
|
|
243
|
+
&& ownedByCurrent(stat) && (stat.mode & 511n) === 384n
|
|
244
|
+
&& stat.size > 0n && stat.size <= BigInt(maxBytes);
|
|
245
|
+
}
|
|
246
|
+
function readPinnedPrivateFile(path, maxBytes = MAX_OPERATOR_FILE_BYTES) {
|
|
247
|
+
if (!isAbsolute(path) || resolve(path) !== path) {
|
|
248
|
+
throw new Error('private-file-path-not-canonical');
|
|
249
|
+
}
|
|
250
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
251
|
+
const fd = openSync(path, fsConstants.O_RDONLY | noFollow);
|
|
252
|
+
let bytes;
|
|
253
|
+
let transferred = false;
|
|
254
|
+
try {
|
|
255
|
+
try {
|
|
256
|
+
const openedBefore = fstatSync(fd, { bigint: true });
|
|
257
|
+
if (!safePinnedPrivateFile(openedBefore, maxBytes)) {
|
|
258
|
+
throw new Error('private-file-custody-unsafe');
|
|
259
|
+
}
|
|
260
|
+
const control = testControl();
|
|
261
|
+
control?.pinnedFileAfterOpen?.({ path });
|
|
262
|
+
const custodyAnchor = inspectCustodyAncestors(path);
|
|
263
|
+
const assuranceBefore = assurePrivateStoragePath(path, 'file', 'inspect-owned', {
|
|
264
|
+
anchorPath: custodyAnchor,
|
|
265
|
+
});
|
|
266
|
+
if (!assuranceBefore.ok)
|
|
267
|
+
throw new Error('private-file-acl-unsafe');
|
|
268
|
+
if (realpathSync(path) !== path)
|
|
269
|
+
throw new Error('private-file-path-not-canonical');
|
|
270
|
+
const namedBefore = lstatSync(path, { bigint: true });
|
|
271
|
+
if (!safePinnedPrivateFile(namedBefore, maxBytes)
|
|
272
|
+
|| !sameSnapshot(openedBefore, namedBefore)) {
|
|
273
|
+
throw new Error('private-file-changed-during-open');
|
|
274
|
+
}
|
|
275
|
+
bytes = Buffer.alloc(Number(openedBefore.size));
|
|
276
|
+
control?.observePinnedFileBuffer?.(path, bytes);
|
|
277
|
+
let offset = 0;
|
|
278
|
+
while (offset < bytes.length) {
|
|
279
|
+
const length = bytes.length - offset;
|
|
280
|
+
const read = (requestedLength = length) => {
|
|
281
|
+
if (!Number.isSafeInteger(requestedLength) || requestedLength <= 0 || requestedLength > length) {
|
|
282
|
+
throw new Error('invalid-test-private-file-read-length');
|
|
283
|
+
}
|
|
284
|
+
return readSync(fd, bytes, offset, requestedLength, offset);
|
|
285
|
+
};
|
|
286
|
+
const count = control?.pinnedFileRead
|
|
287
|
+
? control.pinnedFileRead({ path, offset, length, read })
|
|
288
|
+
: read();
|
|
289
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > length) {
|
|
290
|
+
throw new Error('private-file-short-read');
|
|
291
|
+
}
|
|
292
|
+
offset += count;
|
|
293
|
+
}
|
|
294
|
+
const openedAfter = control?.pinnedFilePostFstat
|
|
295
|
+
? control.pinnedFilePostFstat({ path, stat: () => fstatSync(fd, { bigint: true }) })
|
|
296
|
+
: fstatSync(fd, { bigint: true });
|
|
297
|
+
const namedAfter = control?.pinnedFilePostLstat
|
|
298
|
+
? control.pinnedFilePostLstat({ path, stat: () => lstatSync(path, { bigint: true }) })
|
|
299
|
+
: lstatSync(path, { bigint: true });
|
|
300
|
+
if (!sameSnapshot(openedBefore, openedAfter) || !sameSnapshot(openedAfter, namedAfter)) {
|
|
301
|
+
throw new Error('private-file-changed-during-read');
|
|
302
|
+
}
|
|
303
|
+
inspectCustodyAncestors(path, custodyAnchor);
|
|
304
|
+
const assuranceAfter = assurePrivateStoragePath(path, 'file', 'inspect-owned', {
|
|
305
|
+
anchorPath: custodyAnchor,
|
|
306
|
+
});
|
|
307
|
+
if (!assuranceAfter.ok)
|
|
308
|
+
throw new Error('private-file-acl-unsafe');
|
|
309
|
+
if (realpathSync(path) !== path)
|
|
310
|
+
throw new Error('private-file-path-not-canonical');
|
|
311
|
+
const openedFinal = fstatSync(fd, { bigint: true });
|
|
312
|
+
const namedFinal = lstatSync(path, { bigint: true });
|
|
313
|
+
if (!safePinnedPrivateFile(openedFinal, maxBytes)
|
|
314
|
+
|| !safePinnedPrivateFile(namedFinal, maxBytes)
|
|
315
|
+
|| !sameSnapshot(openedBefore, openedFinal)
|
|
316
|
+
|| !sameSnapshot(openedFinal, namedFinal)) {
|
|
317
|
+
throw new Error('private-file-changed-during-read');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
const control = testControl();
|
|
322
|
+
const close = () => closeSync(fd);
|
|
323
|
+
if (control?.pinnedFileClose)
|
|
324
|
+
control.pinnedFileClose({ path, close });
|
|
325
|
+
else
|
|
326
|
+
close();
|
|
327
|
+
}
|
|
328
|
+
if (!bytes)
|
|
329
|
+
throw new Error('private-file-read-buffer-unavailable');
|
|
330
|
+
transferred = true;
|
|
331
|
+
return bytes;
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
if (!transferred)
|
|
335
|
+
bytes?.fill(0);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function privateKeyLocationReason(input, context) {
|
|
339
|
+
const path = input.privateKeyPath;
|
|
340
|
+
if (!isAbsolute(path) || resolve(path) !== path) {
|
|
341
|
+
return 'goal-conductor-private-key-path-not-absolute-canonical';
|
|
342
|
+
}
|
|
343
|
+
let canonical;
|
|
344
|
+
try {
|
|
345
|
+
canonical = realpathSync(path);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return 'goal-conductor-private-key-unavailable';
|
|
349
|
+
}
|
|
350
|
+
if (canonical !== path || !exactPrivateDirectory(dirname(path))) {
|
|
351
|
+
return 'goal-conductor-private-key-custody-directory-not-exact-private';
|
|
352
|
+
}
|
|
353
|
+
const home = canonicalHome();
|
|
354
|
+
const gitRoot = nearestGitCheckout(dirname(path));
|
|
355
|
+
const forbidden = [
|
|
356
|
+
...(home ? [join(home, '.ashlr')] : []),
|
|
357
|
+
...(gitRoot ? [gitRoot] : []),
|
|
358
|
+
context.releaseTree.path,
|
|
359
|
+
context.target.projectPath,
|
|
360
|
+
dirname(input.requestPath),
|
|
361
|
+
dirname(input.outputPath),
|
|
362
|
+
].map((entry) => resolve(entry));
|
|
363
|
+
if (forbidden.some((entry) => nestedWithin(entry, path))) {
|
|
364
|
+
return 'goal-conductor-private-key-inside-forbidden-tree';
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function validTrustRoot(value) {
|
|
369
|
+
if (!isRecord(value) || !exactKeys(value, ['keyId', 'publicKeyPem']))
|
|
370
|
+
return false;
|
|
371
|
+
if (typeof value['keyId'] !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(value['keyId'])) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (typeof value['publicKeyPem'] !== 'string')
|
|
375
|
+
return false;
|
|
376
|
+
try {
|
|
377
|
+
return createPublicKey(value['publicKeyPem']).asymmetricKeyType === 'ed25519';
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function parseRequest(value) {
|
|
384
|
+
if (!isRecord(value) || !exactKeys(value, [
|
|
385
|
+
'schemaVersion', 'policyVersion', 'requestId', 'requestedAt',
|
|
386
|
+
'expectedReleaseRevision', 'trustRoots', 'context',
|
|
387
|
+
]))
|
|
388
|
+
return null;
|
|
389
|
+
if (value['schemaVersion'] !== 1 || value['policyVersion'] !== REQUEST_POLICY
|
|
390
|
+
|| typeof value['requestId'] !== 'string' || !SHA64_RE.test(value['requestId'])
|
|
391
|
+
|| !iso(value['requestedAt'])
|
|
392
|
+
|| typeof value['expectedReleaseRevision'] !== 'string'
|
|
393
|
+
|| !SHA40_RE.test(value['expectedReleaseRevision'])
|
|
394
|
+
|| !Array.isArray(value['trustRoots']) || value['trustRoots'].length < 1
|
|
395
|
+
|| value['trustRoots'].length > 16 || !value['trustRoots'].every(validTrustRoot)
|
|
396
|
+
|| !isRecord(value['context']))
|
|
397
|
+
return null;
|
|
398
|
+
if (!exactKeys(value['context'], [
|
|
399
|
+
'nowMs', 'configDigest', 'buildIdentity', 'executable', 'entrypoint',
|
|
400
|
+
'releaseTree', 'authorityStateDigest', 'killSwitchOff',
|
|
401
|
+
'guardHealthHealthy', 'target',
|
|
402
|
+
]))
|
|
403
|
+
return null;
|
|
404
|
+
const context = value['context'];
|
|
405
|
+
try {
|
|
406
|
+
const probe = buildGoalConductorActivationPermitPayload({
|
|
407
|
+
permitId: '0'.repeat(32), nonce: '0'.repeat(64), keyId: value['trustRoots'][0].keyId,
|
|
408
|
+
issuedAt: value['requestedAt'], expiresAt: new Date(Date.parse(value['requestedAt']) + PERMIT_LIFETIME_MS).toISOString(),
|
|
409
|
+
context,
|
|
410
|
+
});
|
|
411
|
+
void probe;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const roots = value['trustRoots'];
|
|
417
|
+
if (new Set(roots.map((root) => root.keyId)).size !== roots.length)
|
|
418
|
+
return null;
|
|
419
|
+
const spkiIdentities = roots.map((root) => Buffer.from(createPublicKey(root.publicKeyPem).export({ format: 'der', type: 'spki' })).toString('base64'));
|
|
420
|
+
if (new Set(spkiIdentities).size !== spkiIdentities.length)
|
|
421
|
+
return null;
|
|
422
|
+
if (context.buildIdentity.provenance !== 'github-actions'
|
|
423
|
+
|| context.buildIdentity.dirty !== null
|
|
424
|
+
|| context.buildIdentity.revision !== value['expectedReleaseRevision']
|
|
425
|
+
|| !context.killSwitchOff
|
|
426
|
+
|| !context.guardHealthHealthy
|
|
427
|
+
|| context.releaseTree.path !== resolve(context.releaseTree.path)
|
|
428
|
+
|| basename(context.releaseTree.path) !== value['expectedReleaseRevision']
|
|
429
|
+
|| !context.entrypoint.path.startsWith(`${context.releaseTree.path}${sep}`))
|
|
430
|
+
return null;
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
export function canonicalGoalConductorPermitRequest(request) {
|
|
434
|
+
if (!parseRequest(request))
|
|
435
|
+
throw new Error('invalid-goal-conductor-permit-request');
|
|
436
|
+
return `${canonicalizeDaemonActivationValue(request)}\n`;
|
|
437
|
+
}
|
|
438
|
+
function immutableReleaseReason(context, revision) {
|
|
439
|
+
if (context.buildIdentity.provenance !== 'github-actions'
|
|
440
|
+
|| context.buildIdentity.dirty !== null
|
|
441
|
+
|| context.buildIdentity.revision !== revision) {
|
|
442
|
+
return 'goal-conductor-runtime-build-identity-not-exact-github-actions-release';
|
|
443
|
+
}
|
|
444
|
+
const canonicalHome = realpathSync(resolve(homedir()));
|
|
445
|
+
const expectedRoot = join(canonicalHome, '.local', 'share', 'ashlr', 'releases', revision);
|
|
446
|
+
if (context.releaseTree.path !== expectedRoot || basename(context.releaseTree.path) !== revision) {
|
|
447
|
+
return 'goal-conductor-runtime-is-not-exact-installed-release';
|
|
448
|
+
}
|
|
449
|
+
if (!context.entrypoint.path.startsWith(`${expectedRoot}${sep}`)) {
|
|
450
|
+
return 'goal-conductor-entrypoint-outside-installed-release';
|
|
451
|
+
}
|
|
452
|
+
const pending = [expectedRoot];
|
|
453
|
+
let entries = 0;
|
|
454
|
+
while (pending.length > 0) {
|
|
455
|
+
const path = pending.pop();
|
|
456
|
+
const stat = lstatSync(path, { bigint: true });
|
|
457
|
+
if (stat.isSymbolicLink() || !ownedByCurrent(stat) || (stat.mode & 146n) !== 0n) {
|
|
458
|
+
return 'goal-conductor-installed-release-not-immutable';
|
|
459
|
+
}
|
|
460
|
+
if (stat.isDirectory()) {
|
|
461
|
+
const directory = opendirSync(path);
|
|
462
|
+
try {
|
|
463
|
+
for (;;) {
|
|
464
|
+
const entry = directory.readSync();
|
|
465
|
+
if (!entry)
|
|
466
|
+
break;
|
|
467
|
+
entries += 1;
|
|
468
|
+
if (entries > 20_000)
|
|
469
|
+
return 'goal-conductor-installed-release-entry-bound-exceeded';
|
|
470
|
+
pending.push(join(path, entry.name));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
directory.closeSync();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (!stat.isFile()) {
|
|
478
|
+
return 'goal-conductor-installed-release-contains-unsupported-entry';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
export function buildGoalConductorPermitRequest(cfg, input, dependencies = {}) {
|
|
484
|
+
const roots = dependencies.trustRoots ?? GOAL_CONDUCTOR_ACTIVATION_TRUST_ROOTS;
|
|
485
|
+
if (roots.length === 0) {
|
|
486
|
+
return { ok: false, state: 'blocked', reason: 'no-trusted-goal-conductor-activation-roots' };
|
|
487
|
+
}
|
|
488
|
+
if (!SHA40_RE.test(input.expectedReleaseRevision)) {
|
|
489
|
+
return { ok: false, state: 'blocked', reason: 'expected-release-revision-invalid' };
|
|
490
|
+
}
|
|
491
|
+
if (!inspectExistingPrivateControl()) {
|
|
492
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-preexisting-private-control-required' };
|
|
493
|
+
}
|
|
494
|
+
const resolution = (dependencies.resolveTarget ?? resolveLiveGoalConductorTarget)(input.goalId);
|
|
495
|
+
if (!resolution.ok || !resolution.target) {
|
|
496
|
+
return { ok: false, state: 'blocked', reason: resolution.reason };
|
|
497
|
+
}
|
|
498
|
+
let context;
|
|
499
|
+
try {
|
|
500
|
+
context = (dependencies.collectContext ?? collectGoalConductorActivationContext)(cfg, resolution.target);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return { ok: false, state: 'degraded', reason: 'goal-conductor-runtime-context-unavailable' };
|
|
504
|
+
}
|
|
505
|
+
if (context.configDigest !== daemonActivationConfigDigest(cfg)) {
|
|
506
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-strict-config-digest-mismatch' };
|
|
507
|
+
}
|
|
508
|
+
if (!context.killSwitchOff) {
|
|
509
|
+
return { ok: false, state: 'blocked', reason: 'kill-switch-is-on' };
|
|
510
|
+
}
|
|
511
|
+
if (!context.guardHealthHealthy) {
|
|
512
|
+
return { ok: false, state: 'blocked', reason: 'guard-health-degraded' };
|
|
513
|
+
}
|
|
514
|
+
let releaseReason;
|
|
515
|
+
try {
|
|
516
|
+
releaseReason = (dependencies.verifyInstalledRelease ?? immutableReleaseReason)(context, input.expectedReleaseRevision);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
releaseReason = 'goal-conductor-installed-release-inspection-failed';
|
|
520
|
+
}
|
|
521
|
+
if (releaseReason)
|
|
522
|
+
return { ok: false, state: 'blocked', reason: releaseReason };
|
|
523
|
+
const outputReason = externalArtifactPathReason(input.outputPath, context);
|
|
524
|
+
if (outputReason)
|
|
525
|
+
return { ok: false, state: 'blocked', reason: outputReason };
|
|
526
|
+
const requestedAt = new Date((dependencies.nowMs ?? Date.now)()).toISOString();
|
|
527
|
+
const requestWithoutId = {
|
|
528
|
+
schemaVersion: 1,
|
|
529
|
+
policyVersion: REQUEST_POLICY,
|
|
530
|
+
requestedAt,
|
|
531
|
+
expectedReleaseRevision: input.expectedReleaseRevision,
|
|
532
|
+
trustRoots: roots.map((root) => ({ keyId: root.keyId, publicKeyPem: root.publicKeyPem })),
|
|
533
|
+
context,
|
|
534
|
+
};
|
|
535
|
+
const request = {
|
|
536
|
+
...requestWithoutId,
|
|
537
|
+
requestId: sha256(canonicalizeDaemonActivationValue(requestWithoutId)),
|
|
538
|
+
};
|
|
539
|
+
if (!parseRequest(request)) {
|
|
540
|
+
return { ok: false, state: 'degraded', reason: 'goal-conductor-permit-request-invalid' };
|
|
541
|
+
}
|
|
542
|
+
return { ok: true, state: 'ready', reason: 'goal-conductor-permit-request-ready', value: request };
|
|
543
|
+
}
|
|
544
|
+
function keySpki(key) {
|
|
545
|
+
return Buffer.from(createPublicKey(key).export({ format: 'der', type: 'spki' }));
|
|
546
|
+
}
|
|
547
|
+
export function mintGoalConductorPermitOffline(input) {
|
|
548
|
+
let requestBytes;
|
|
549
|
+
let privateBytes;
|
|
550
|
+
try {
|
|
551
|
+
requestBytes = readPinnedPrivateFile(input.requestPath);
|
|
552
|
+
const requestText = requestBytes.toString('utf8');
|
|
553
|
+
const raw = JSON.parse(requestText);
|
|
554
|
+
const request = parseRequest(raw);
|
|
555
|
+
if (!request || canonicalGoalConductorPermitRequest(request) !== requestText) {
|
|
556
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-permit-request-not-canonical' };
|
|
557
|
+
}
|
|
558
|
+
const expectedRequestId = sha256(canonicalizeDaemonActivationValue({
|
|
559
|
+
schemaVersion: request.schemaVersion,
|
|
560
|
+
policyVersion: request.policyVersion,
|
|
561
|
+
requestedAt: request.requestedAt,
|
|
562
|
+
expectedReleaseRevision: request.expectedReleaseRevision,
|
|
563
|
+
trustRoots: request.trustRoots,
|
|
564
|
+
context: request.context,
|
|
565
|
+
}));
|
|
566
|
+
if (request.requestId !== expectedRequestId) {
|
|
567
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-permit-request-id-mismatch' };
|
|
568
|
+
}
|
|
569
|
+
const requestPathReason = externalExistingArtifactReason(input.requestPath, request.context);
|
|
570
|
+
if (requestPathReason)
|
|
571
|
+
return { ok: false, state: 'blocked', reason: requestPathReason };
|
|
572
|
+
const outputReason = externalArtifactPathReason(input.outputPath, request.context, [input.requestPath]);
|
|
573
|
+
if (outputReason)
|
|
574
|
+
return { ok: false, state: 'blocked', reason: outputReason };
|
|
575
|
+
const keyLocationReason = privateKeyLocationReason(input, request.context);
|
|
576
|
+
if (keyLocationReason)
|
|
577
|
+
return { ok: false, state: 'blocked', reason: keyLocationReason };
|
|
578
|
+
privateBytes = readPinnedPrivateFile(input.privateKeyPath);
|
|
579
|
+
testControl()?.observePrivateKeyBuffer?.(privateBytes);
|
|
580
|
+
let privateKey;
|
|
581
|
+
try {
|
|
582
|
+
privateKey = createPrivateKey({ key: privateBytes, format: 'der', type: 'pkcs8' });
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-private-key-invalid' };
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
privateBytes.fill(0);
|
|
589
|
+
}
|
|
590
|
+
if (privateKey.asymmetricKeyType !== 'ed25519') {
|
|
591
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-private-key-not-ed25519' };
|
|
592
|
+
}
|
|
593
|
+
const publicDer = keySpki(privateKey);
|
|
594
|
+
let root;
|
|
595
|
+
for (const candidate of request.trustRoots) {
|
|
596
|
+
let trusted;
|
|
597
|
+
try {
|
|
598
|
+
trusted = Buffer.from(createPublicKey(candidate.publicKeyPem).export({ format: 'der', type: 'spki' }));
|
|
599
|
+
if (trusted.equals(publicDer)) {
|
|
600
|
+
root = candidate;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// The canonical request parser already rejects invalid roots.
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
trusted?.fill(0);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
publicDer.fill(0);
|
|
612
|
+
if (!root) {
|
|
613
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-private-key-not-request-trusted' };
|
|
614
|
+
}
|
|
615
|
+
const issuedAtMs = input.nowMs ?? Date.now();
|
|
616
|
+
const payload = buildGoalConductorActivationPermitPayload({
|
|
617
|
+
permitId: randomBytes(16).toString('hex'),
|
|
618
|
+
nonce: randomBytes(32).toString('hex'),
|
|
619
|
+
keyId: root.keyId,
|
|
620
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
621
|
+
expiresAt: new Date(issuedAtMs + PERMIT_LIFETIME_MS).toISOString(),
|
|
622
|
+
context: request.context,
|
|
623
|
+
});
|
|
624
|
+
const envelope = signGoalConductorActivationPermit(payload, privateKey);
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
state: 'ready',
|
|
628
|
+
reason: 'goal-conductor-permit-minted-offline',
|
|
629
|
+
permitId: payload.permitId,
|
|
630
|
+
value: envelope,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return { ok: false, state: 'degraded', reason: 'goal-conductor-offline-mint-failed' };
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
requestBytes?.fill(0);
|
|
638
|
+
privateBytes?.fill(0);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function receiptPresent(path) {
|
|
642
|
+
try {
|
|
643
|
+
lstatSync(path);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
if (error.code === 'ENOENT')
|
|
648
|
+
return false;
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
export function inspectGoalConductorPermit(cfg, envelope, dependencies = {}) {
|
|
653
|
+
const roots = dependencies.trustRoots ?? GOAL_CONDUCTOR_ACTIVATION_TRUST_ROOTS;
|
|
654
|
+
if (roots.length === 0) {
|
|
655
|
+
return { ok: false, state: 'blocked', reason: 'no-trusted-goal-conductor-activation-roots' };
|
|
656
|
+
}
|
|
657
|
+
const goalId = envelope?.payload?.bindings?.target?.goalId;
|
|
658
|
+
const resolution = (dependencies.resolveTarget ?? resolveLiveGoalConductorTarget)(goalId ?? '');
|
|
659
|
+
if (!resolution.ok || !resolution.target) {
|
|
660
|
+
return { ok: false, state: 'blocked', reason: resolution.reason };
|
|
661
|
+
}
|
|
662
|
+
let context;
|
|
663
|
+
try {
|
|
664
|
+
context = (dependencies.collectContext ?? collectGoalConductorActivationContext)(cfg, resolution.target);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
return { ok: false, state: 'degraded', reason: 'goal-conductor-runtime-context-unavailable' };
|
|
668
|
+
}
|
|
669
|
+
const checked = verifyGoalConductorActivationPermit(envelope, context, roots);
|
|
670
|
+
if (!checked.ok || !checked.permitId) {
|
|
671
|
+
return { ok: false, state: 'blocked', reason: checked.reason };
|
|
672
|
+
}
|
|
673
|
+
const revision = envelope.payload.bindings.buildIdentity.revision;
|
|
674
|
+
if (typeof revision !== 'string' || !SHA40_RE.test(revision)) {
|
|
675
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-release-revision-invalid' };
|
|
676
|
+
}
|
|
677
|
+
let releaseReason;
|
|
678
|
+
try {
|
|
679
|
+
releaseReason = (dependencies.verifyInstalledRelease ?? immutableReleaseReason)(context, revision);
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
releaseReason = 'goal-conductor-installed-release-inspection-failed';
|
|
683
|
+
}
|
|
684
|
+
if (releaseReason)
|
|
685
|
+
return { ok: false, state: 'blocked', reason: releaseReason };
|
|
686
|
+
const nonceDigest = sha256(envelope.payload.nonce);
|
|
687
|
+
try {
|
|
688
|
+
if (receiptPresent(goalConductorActivationReceiptPath(checked.permitId))
|
|
689
|
+
|| receiptPresent(goalConductorActivationNonceReceiptPath(nonceDigest))) {
|
|
690
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-permit-already-consumed' };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return { ok: false, state: 'degraded', reason: 'goal-conductor-replay-state-unavailable' };
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
state: 'ready',
|
|
699
|
+
reason: 'goal-conductor-permit-ready-to-stage',
|
|
700
|
+
permitId: checked.permitId,
|
|
701
|
+
value: envelope,
|
|
702
|
+
command: Object.freeze(['ashlr', 'loop', '--goal', resolution.target.goalId]),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function writeExclusivePrivate(path, text, anchorPath) {
|
|
706
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
707
|
+
const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, PRIVATE_FILE_MODE);
|
|
708
|
+
try {
|
|
709
|
+
fchmodSync(fd, PRIVATE_FILE_MODE);
|
|
710
|
+
const initial = fstatSync(fd, { bigint: true });
|
|
711
|
+
if (!initial.isFile() || initial.nlink !== 1n || !ownedByCurrent(initial)
|
|
712
|
+
|| (initial.mode & 511n) !== 384n || initial.size !== 0n) {
|
|
713
|
+
throw new Error('unsafe-created-private-file');
|
|
714
|
+
}
|
|
715
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
716
|
+
let offset = 0;
|
|
717
|
+
while (offset < bytes.length) {
|
|
718
|
+
const count = writeSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
719
|
+
if (count <= 0)
|
|
720
|
+
throw new Error('private-file-write-made-no-progress');
|
|
721
|
+
offset += count;
|
|
722
|
+
}
|
|
723
|
+
fsyncSync(fd);
|
|
724
|
+
const after = fstatSync(fd, { bigint: true });
|
|
725
|
+
if (after.dev !== initial.dev || after.ino !== initial.ino
|
|
726
|
+
|| after.size !== BigInt(bytes.length) || after.nlink !== 1n) {
|
|
727
|
+
throw new Error('private-file-changed-during-write');
|
|
728
|
+
}
|
|
729
|
+
const assurance = assurePrivateStoragePath(path, 'file', 'secure-created', { anchorPath });
|
|
730
|
+
if (!assurance.ok)
|
|
731
|
+
throw new Error('unsafe-created-private-file-acl');
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
closeSync(fd);
|
|
735
|
+
}
|
|
736
|
+
fsyncDirectory(dirname(path));
|
|
737
|
+
}
|
|
738
|
+
export function writeGoalConductorOperatorArtifact(path, value) {
|
|
739
|
+
if (!isAbsolute(path) || resolve(path) !== path)
|
|
740
|
+
throw new Error('operator-output-path-not-canonical');
|
|
741
|
+
const parent = dirname(path);
|
|
742
|
+
const canonicalParent = realpathSync(parent);
|
|
743
|
+
if (canonicalParent !== parent || !exactPrivateDirectory(parent)) {
|
|
744
|
+
throw new Error('operator-output-parent-not-exact-private');
|
|
745
|
+
}
|
|
746
|
+
const request = parseRequest(value);
|
|
747
|
+
const envelope = parseGoalConductorActivationPermitEnvelope(value);
|
|
748
|
+
const artifactContext = request?.context ?? (envelope ? contextFromEnvelope(envelope) : null);
|
|
749
|
+
if (!artifactContext)
|
|
750
|
+
throw new Error('operator-output-artifact-invalid');
|
|
751
|
+
const pathReason = externalArtifactPathReason(path, artifactContext);
|
|
752
|
+
if (pathReason)
|
|
753
|
+
throw new Error(pathReason);
|
|
754
|
+
const custodyAnchor = inspectCustodyAncestors(path);
|
|
755
|
+
writeExclusivePrivate(path, `${canonicalizeDaemonActivationValue(value)}\n`, custodyAnchor);
|
|
756
|
+
}
|
|
757
|
+
function contextFromEnvelope(value) {
|
|
758
|
+
const bindings = value.payload.bindings;
|
|
759
|
+
return {
|
|
760
|
+
nowMs: 0,
|
|
761
|
+
configDigest: bindings.configDigest,
|
|
762
|
+
buildIdentity: bindings.buildIdentity,
|
|
763
|
+
executable: bindings.executable,
|
|
764
|
+
entrypoint: bindings.entrypoint,
|
|
765
|
+
releaseTree: bindings.releaseTree,
|
|
766
|
+
authorityStateDigest: bindings.authorityStateDigest,
|
|
767
|
+
killSwitchOff: true,
|
|
768
|
+
guardHealthHealthy: true,
|
|
769
|
+
target: bindings.target,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const permitPublicationCodec = {
|
|
773
|
+
parse: parseGoalConductorActivationPermitEnvelope,
|
|
774
|
+
serialize: (envelope) => `${canonicalizeDaemonActivationValue(envelope)}\n`,
|
|
775
|
+
recordId: (envelope) => envelope.payload.permitId,
|
|
776
|
+
recordFileName: () => PERMIT_FILE_NAME,
|
|
777
|
+
isRecordFileName: (fileName) => fileName === PERMIT_FILE_NAME,
|
|
778
|
+
stageToken: (envelope) => sha256(canonicalizeDaemonActivationValue(envelope)).slice(0, 32),
|
|
779
|
+
equivalent: (left, right) => (canonicalizeDaemonActivationValue(left) === canonicalizeDaemonActivationValue(right)),
|
|
780
|
+
};
|
|
781
|
+
export function goalConductorPermitPublicationRootPath() {
|
|
782
|
+
return join(homedir(), '.ashlr', 'control', PUBLICATION_ROOT_NAME);
|
|
783
|
+
}
|
|
784
|
+
function permitPublicationConfig(control) {
|
|
785
|
+
return {
|
|
786
|
+
label: 'goal conductor activation permit',
|
|
787
|
+
anchorPath: control,
|
|
788
|
+
rootPath: join(control, PUBLICATION_ROOT_NAME),
|
|
789
|
+
lockFileName: '.goal-conductor-permit-publication.lock',
|
|
790
|
+
maxRecordBytes: MAX_OPERATOR_FILE_BYTES,
|
|
791
|
+
defaultMaxFiles: 1,
|
|
792
|
+
hardMaxFiles: 1,
|
|
793
|
+
defaultMaxBytes: MAX_OPERATOR_FILE_BYTES,
|
|
794
|
+
hardMaxBytes: MAX_OPERATOR_FILE_BYTES,
|
|
795
|
+
codecForWrite: () => permitPublicationCodec,
|
|
796
|
+
codecForRead: () => permitPublicationCodec,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
export function stageGoalConductorPermit(cfg, envelope, dependencies = {}) {
|
|
800
|
+
const inspection = inspectGoalConductorPermit(cfg, envelope, dependencies);
|
|
801
|
+
if (!inspection.ok || !inspection.command)
|
|
802
|
+
return inspection;
|
|
803
|
+
const privateControl = inspectExistingPrivateControl();
|
|
804
|
+
if (!privateControl) {
|
|
805
|
+
return { ok: false, state: 'blocked', reason: 'goal-conductor-preexisting-private-control-required' };
|
|
806
|
+
}
|
|
807
|
+
const disposition = writeImmutablePrivateRecord(permitPublicationConfig(privateControl.control), envelope);
|
|
808
|
+
if (disposition !== 'recorded') {
|
|
809
|
+
return {
|
|
810
|
+
ok: false,
|
|
811
|
+
state: 'blocked',
|
|
812
|
+
reason: disposition === 'replayed' || disposition === 'conflicted'
|
|
813
|
+
? 'goal-conductor-staged-permit-already-exists'
|
|
814
|
+
: 'goal-conductor-permit-stage-failed',
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
ok: true,
|
|
819
|
+
state: 'staged',
|
|
820
|
+
reason: 'goal-conductor-permit-staged',
|
|
821
|
+
permitId: inspection.permitId,
|
|
822
|
+
command: inspection.command,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
export function readGoalConductorOperatorEnvelope(path) {
|
|
826
|
+
const bytes = readPinnedPrivateFile(path);
|
|
827
|
+
try {
|
|
828
|
+
const text = bytes.toString('utf8');
|
|
829
|
+
const value = JSON.parse(text);
|
|
830
|
+
if (`${canonicalizeDaemonActivationValue(value)}\n` !== text) {
|
|
831
|
+
throw new Error('goal-conductor-permit-not-canonical');
|
|
832
|
+
}
|
|
833
|
+
const pathReason = externalExistingArtifactReason(path, contextFromEnvelope(value));
|
|
834
|
+
if (pathReason)
|
|
835
|
+
throw new Error(pathReason);
|
|
836
|
+
return value;
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
bytes.fill(0);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
//# sourceMappingURL=goal-conductor-permit-operator.js.map
|