@botbuddy/cli 1.59.6 → 1.59.8
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +7 -0
- package/daemon-release/linux-x64.json +7 -0
- package/daemon-release/manifest.json +22 -0
- package/package.json +3 -1
- package/src/api.mjs +62 -0
- package/src/commands.mjs +6 -0
- package/src/daemon/approval-wait.mjs +126 -0
- package/src/daemon/bootstrap-resume.mjs +151 -0
- package/src/daemon/bootstrap.mjs +386 -0
- package/src/daemon/commands.mjs +478 -0
- package/src/daemon/enrollment.mjs +483 -0
- package/src/daemon/entrypoint.mjs +14 -0
- package/src/daemon/installer.mjs +438 -0
- package/src/daemon/profile-delivery.mjs +332 -0
- package/src/daemon/release.mjs +81 -0
- package/src/daemon/runtime-config.mjs +85 -0
- package/src/daemon/service.mjs +38 -0
- package/src/oauth-device.mjs +11 -0
- package/src/publish-native-guards.mjs +44 -0
- package/src/usage/codex.mjs +55 -12
- package/src/usage/sync.mjs +26 -2
- package/src/wait-core.mjs +22 -5
- package/src/wait.mjs +4 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// BOT-2057: public requester orchestration for a genuinely fresh daemon host.
|
|
2
|
+
// The target creates or reads its own P-256 scalar; this module handles only
|
|
3
|
+
// the public SPKI DTO and never receives a private key. There is intentionally
|
|
4
|
+
// no local approval polling loop: host policy wakes only through the pushed
|
|
5
|
+
// BOT-2324 raw wait condition, then this flow re-registers server-side.
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
import { DAEMON_PRODUCTION_MCP_SERVER, enrollDaemonDeviceProfileFromBootstrapIdentity } from "./enrollment.mjs";
|
|
9
|
+
import { installRemoteService, installedBinaryPath, stageRemoteDaemonRelease } from "./installer.mjs";
|
|
10
|
+
import {
|
|
11
|
+
createRemoteBootstrapIdentity,
|
|
12
|
+
deliverDaemonProfileFromBootstrapIdentityOverSSH,
|
|
13
|
+
inspectRemoteBootstrapIdentity,
|
|
14
|
+
verifyRemoteServiceIdentity,
|
|
15
|
+
} from "./profile-delivery.mjs";
|
|
16
|
+
import { validateDaemonRelease } from "./release.mjs";
|
|
17
|
+
import { renderLinuxUnit, renderMacDaemon } from "./service.mjs";
|
|
18
|
+
|
|
19
|
+
const HOST_KEY_RE = /^bbh-[0-9a-f]{24}$/;
|
|
20
|
+
const ABSOLUTE_PATH_RE = /^\/(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
|
|
21
|
+
const SERVICE_USER_RE = /^[a-z_][a-z0-9_-]*$/i;
|
|
22
|
+
const ENGINE_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
23
|
+
const P256_SPKI_PREFIX = Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex");
|
|
24
|
+
const DOCKER_APPROVAL_ORIGIN = "https://app.bot-buddy.ai";
|
|
25
|
+
|
|
26
|
+
export class DaemonBootstrapError extends Error {
|
|
27
|
+
constructor(code) {
|
|
28
|
+
super(code);
|
|
29
|
+
this.name = "DaemonBootstrapError";
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fail = (code) => { throw new DaemonBootstrapError(code); };
|
|
35
|
+
|
|
36
|
+
function validPath(value) {
|
|
37
|
+
return typeof value === "string" && ABSOLUTE_PATH_RE.test(value) && !value.split("/").includes("..");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function exactObject(value, keys) {
|
|
41
|
+
return value && typeof value === "object" && !Array.isArray(value) &&
|
|
42
|
+
Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function checkedSpec(spec) {
|
|
46
|
+
const keys = [
|
|
47
|
+
"mode", "serverUrl", "sshHost", "serviceUser", "daemonBinary", "profilePath", "requestingHost",
|
|
48
|
+
"runtimeEngineId", "platform", "arch", "releaseRoot", "home", "config", "approvalTimeoutSeconds",
|
|
49
|
+
];
|
|
50
|
+
const optional = ["caller", "callerHarness", "callerTicket"];
|
|
51
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec) ||
|
|
52
|
+
Object.keys(spec).some((key) => !keys.includes(key) && !optional.includes(key)) ||
|
|
53
|
+
keys.some((key) => !Object.hasOwn(spec, key))) fail("daemon_bootstrap_spec_invalid");
|
|
54
|
+
if (
|
|
55
|
+
!["create", "resume"].includes(spec.mode) || spec.serverUrl !== DAEMON_PRODUCTION_MCP_SERVER ||
|
|
56
|
+
typeof spec.sshHost !== "string" || !/^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9._-]+$/.test(spec.sshHost) ||
|
|
57
|
+
typeof spec.serviceUser !== "string" || spec.serviceUser === "root" || !SERVICE_USER_RE.test(spec.serviceUser) ||
|
|
58
|
+
!validPath(spec.daemonBinary) || !validPath(spec.profilePath) || !spec.profilePath.endsWith("/profile.json") ||
|
|
59
|
+
typeof spec.requestingHost !== "string" || !spec.requestingHost || Buffer.byteLength(spec.requestingHost, "utf8") > 80 ||
|
|
60
|
+
typeof spec.runtimeEngineId !== "string" || !ENGINE_ID_RE.test(spec.runtimeEngineId) ||
|
|
61
|
+
!["darwin", "linux"].includes(spec.platform) || typeof spec.arch !== "string" || !spec.arch ||
|
|
62
|
+
!validPath(spec.releaseRoot) || !validPath(spec.home) || !validPath(spec.config) ||
|
|
63
|
+
!Number.isSafeInteger(spec.approvalTimeoutSeconds) || spec.approvalTimeoutSeconds < 1 || spec.approvalTimeoutSeconds > 3_600 ||
|
|
64
|
+
optional.some((key) => spec[key] !== undefined && (typeof spec[key] !== "string" || !spec[key]))
|
|
65
|
+
) fail("daemon_bootstrap_spec_invalid");
|
|
66
|
+
return spec;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkedIdentity(value) {
|
|
70
|
+
if (!exactObject(value, ["version", "host_key", "public_key_spki_base64"]) ||
|
|
71
|
+
value.version !== 1 || typeof value.host_key !== "string" || !HOST_KEY_RE.test(value.host_key) ||
|
|
72
|
+
typeof value.public_key_spki_base64 !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(value.public_key_spki_base64) ||
|
|
73
|
+
value.public_key_spki_base64.length % 4 !== 0) fail("daemon_bootstrap_identity_invalid");
|
|
74
|
+
let spki;
|
|
75
|
+
try { spki = Buffer.from(value.public_key_spki_base64, "base64"); } catch { fail("daemon_bootstrap_identity_invalid"); }
|
|
76
|
+
if (
|
|
77
|
+
spki.toString("base64") !== value.public_key_spki_base64 ||
|
|
78
|
+
spki.length !== P256_SPKI_PREFIX.length + 65 ||
|
|
79
|
+
!spki.subarray(0, P256_SPKI_PREFIX.length).equals(P256_SPKI_PREFIX) ||
|
|
80
|
+
spki[P256_SPKI_PREFIX.length] !== 0x04 ||
|
|
81
|
+
`bbh-${createHash("sha256").update(spki).digest("hex").slice(0, 24)}` !== value.host_key
|
|
82
|
+
) fail("daemon_bootstrap_identity_invalid");
|
|
83
|
+
return { hostKey: value.host_key, spki };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkedRelease(value, spec) {
|
|
87
|
+
const target = `${spec.platform}-${spec.arch}`;
|
|
88
|
+
if (!value || typeof value !== "object" || value.target !== target ||
|
|
89
|
+
typeof value.releaseVersion !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value.releaseVersion) ||
|
|
90
|
+
typeof value.sourceCommit !== "string" || !/^[0-9a-f]{40}$/.test(value.sourceCommit) ||
|
|
91
|
+
typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(value.sha256) ||
|
|
92
|
+
!Buffer.isBuffer(value.payload)) fail("daemon_bootstrap_release_invalid");
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pemForSpki(spki) {
|
|
97
|
+
return `-----BEGIN PUBLIC KEY-----\n${spki.toString("base64")}\n-----END PUBLIC KEY-----\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function bootstrapResumeRecord(spec, identity, profilePhase = "pending") {
|
|
101
|
+
const publicIdentity = checkedIdentity(identity);
|
|
102
|
+
if (!["pending", "profile_delivered"].includes(profilePhase)) fail("daemon_bootstrap_resume_invalid");
|
|
103
|
+
return {
|
|
104
|
+
schema_version: 2,
|
|
105
|
+
host_key: publicIdentity.hostKey,
|
|
106
|
+
public_key_spki_sha256: `sha256:${createHash("sha256").update(publicIdentity.spki).digest("hex")}`,
|
|
107
|
+
server_url: spec.serverUrl,
|
|
108
|
+
ssh_host: spec.sshHost,
|
|
109
|
+
service_user: spec.serviceUser,
|
|
110
|
+
daemon_binary: spec.daemonBinary,
|
|
111
|
+
profile_path: spec.profilePath,
|
|
112
|
+
requesting_host: spec.requestingHost,
|
|
113
|
+
runtime_engine_id: spec.runtimeEngineId,
|
|
114
|
+
platform: spec.platform,
|
|
115
|
+
arch: spec.arch,
|
|
116
|
+
release_root: spec.releaseRoot,
|
|
117
|
+
home: spec.home,
|
|
118
|
+
config: spec.config,
|
|
119
|
+
approval_timeout_seconds: spec.approvalTimeoutSeconds,
|
|
120
|
+
profile_phase: profilePhase,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameResumeRecord(left, right) {
|
|
125
|
+
return left && right && typeof left === "object" && typeof right === "object" &&
|
|
126
|
+
Object.keys(left).length === Object.keys(right).length && Object.keys(left).every((key) => left[key] === right[key]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function completedProfilePhase(stored, pending, delivered) {
|
|
130
|
+
if (sameResumeRecord(stored, pending)) return false;
|
|
131
|
+
if (sameResumeRecord(stored, delivered)) return true;
|
|
132
|
+
fail("daemon_bootstrap_resume_mismatch");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function profileDeliveryCompleted(enrollment, hostKey) {
|
|
136
|
+
return enrollment && typeof enrollment === "object" && !Array.isArray(enrollment) &&
|
|
137
|
+
enrollment.outcome === "profile_delivered" && enrollment.expected_host_key === hostKey &&
|
|
138
|
+
enrollment.delivery && typeof enrollment.delivery === "object" && enrollment.delivery.installed === true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function dockerApprovalUrl(hostKey) {
|
|
142
|
+
if (typeof hostKey !== "string" || !HOST_KEY_RE.test(hostKey)) fail("daemon_bootstrap_identity_invalid");
|
|
143
|
+
// Docker.tsx treats this query shape as a Helper approval return and opens
|
|
144
|
+
// PairHostWizard for the exact candidate. This is not an invented route.
|
|
145
|
+
return `${DOCKER_APPROVAL_ORIGIN}/docker?host_key=${hostKey}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Build the complete server-validated registration body from target public data. */
|
|
149
|
+
export function bootstrapRegistrationPayload({ identity, release, runtimeEngineId } = {}) {
|
|
150
|
+
const target = { platform: release?.target?.split("-")[0], arch: release?.target?.split("-").slice(1).join("-") };
|
|
151
|
+
// checkedRelease needs only the target tuple; its caller has already selected
|
|
152
|
+
// platform/arch and the remaining shape is intentionally validated below.
|
|
153
|
+
if (!target.platform || !target.arch) fail("daemon_bootstrap_release_invalid");
|
|
154
|
+
const checked = checkedRelease(release, target);
|
|
155
|
+
const publicIdentity = checkedIdentity(identity);
|
|
156
|
+
if (typeof runtimeEngineId !== "string" || !ENGINE_ID_RE.test(runtimeEngineId)) fail("daemon_bootstrap_runtime_identity_invalid");
|
|
157
|
+
return {
|
|
158
|
+
host_key: publicIdentity.hostKey,
|
|
159
|
+
p256_public_key_pem: pemForSpki(publicIdentity.spki),
|
|
160
|
+
device_consent: true,
|
|
161
|
+
helper_version: `go-daemon/${checked.releaseVersion}`,
|
|
162
|
+
helper_git_sha: checked.sourceCommit,
|
|
163
|
+
helper_protocol_version: 2,
|
|
164
|
+
runtime_kind: "go_daemon",
|
|
165
|
+
runtime_artifact_sha256: `sha256:${checked.sha256}`,
|
|
166
|
+
runtime_engine_id: runtimeEngineId,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function registrationState(value, hostKey) {
|
|
171
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.host_key !== hostKey) {
|
|
172
|
+
fail("daemon_bootstrap_registration_invalid");
|
|
173
|
+
}
|
|
174
|
+
if (value.registered === false && value.state === "unregistered") return { registered: false, state: "unregistered" };
|
|
175
|
+
if (value.registered === true && typeof value.state === "string" && value.state && value.device_consent === true) {
|
|
176
|
+
return { registered: true, state: value.state, deviceConsent: true };
|
|
177
|
+
}
|
|
178
|
+
fail("daemon_bootstrap_registration_invalid");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function pendingReceipt(hostKey) {
|
|
182
|
+
return {
|
|
183
|
+
outcome: "approval_required",
|
|
184
|
+
state: "unregistered",
|
|
185
|
+
host_key: hostKey,
|
|
186
|
+
approval_url: dockerApprovalUrl(hostKey),
|
|
187
|
+
resume_mode: "resume",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function approvalTerminal(state, hostKey) {
|
|
192
|
+
if (!["denied", "expired", "cancelled"].includes(state)) fail("daemon_bootstrap_approval_wait_invalid");
|
|
193
|
+
return { outcome: `approval_${state}`, state, host_key: hostKey };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Drive the explicit requester-owned lifecycle. The public CLI uses one bounded
|
|
198
|
+
* pushed approval waiter; it gets exactly one wake and re-registers the same
|
|
199
|
+
* public body before it may create a daemon device family.
|
|
200
|
+
*/
|
|
201
|
+
export async function bootstrapFreshDaemon(input, {
|
|
202
|
+
verifyRemoteIdentity = verifyRemoteServiceIdentity,
|
|
203
|
+
createIdentity = createRemoteBootstrapIdentity,
|
|
204
|
+
inspectIdentity = inspectRemoteBootstrapIdentity,
|
|
205
|
+
validateRelease = validateDaemonRelease,
|
|
206
|
+
stageRemote = stageRemoteDaemonRelease,
|
|
207
|
+
registerHost,
|
|
208
|
+
awaitApproval = null,
|
|
209
|
+
onApprovalRequired = null,
|
|
210
|
+
resumeStore = null,
|
|
211
|
+
enroll = enrollDaemonDeviceProfileFromBootstrapIdentity,
|
|
212
|
+
deliver = deliverDaemonProfileFromBootstrapIdentityOverSSH,
|
|
213
|
+
installRemote = installRemoteService,
|
|
214
|
+
} = {}) {
|
|
215
|
+
const spec = checkedSpec(input);
|
|
216
|
+
if (typeof registerHost !== "function" || typeof verifyRemoteIdentity !== "function" ||
|
|
217
|
+
typeof createIdentity !== "function" || typeof inspectIdentity !== "function" ||
|
|
218
|
+
typeof validateRelease !== "function" || typeof stageRemote !== "function" || typeof enroll !== "function" ||
|
|
219
|
+
typeof deliver !== "function" || typeof installRemote !== "function" ||
|
|
220
|
+
(awaitApproval !== null && typeof awaitApproval !== "function") ||
|
|
221
|
+
(onApprovalRequired !== null && typeof onApprovalRequired !== "function") ||
|
|
222
|
+
(resumeStore !== null && (typeof resumeStore !== "object" || typeof resumeStore.save !== "function" ||
|
|
223
|
+
typeof resumeStore.load !== "function" || typeof resumeStore.remove !== "function"))) fail("daemon_bootstrap_dependencies_invalid");
|
|
224
|
+
|
|
225
|
+
let release;
|
|
226
|
+
try { release = checkedRelease(await validateRelease({ releaseRoot: spec.releaseRoot }, { platform: spec.platform, arch: spec.arch }), spec); }
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
229
|
+
fail("daemon_bootstrap_release_invalid");
|
|
230
|
+
}
|
|
231
|
+
const trustedDaemonBinary = installedBinaryPath(release);
|
|
232
|
+
try {
|
|
233
|
+
const staged = await stageRemote({
|
|
234
|
+
platform: spec.platform,
|
|
235
|
+
host: spec.sshHost,
|
|
236
|
+
user: spec.serviceUser,
|
|
237
|
+
release,
|
|
238
|
+
});
|
|
239
|
+
if (!staged || staged.outcome !== "staged" || staged.binary !== trustedDaemonBinary ||
|
|
240
|
+
staged.service_user !== spec.serviceUser || staged.host !== spec.sshHost) fail("daemon_bootstrap_release_stage_failed");
|
|
241
|
+
} catch { fail("daemon_bootstrap_release_stage_failed"); }
|
|
242
|
+
|
|
243
|
+
let remote;
|
|
244
|
+
try { remote = await verifyRemoteIdentity({ sshHost: spec.sshHost, serviceUser: spec.serviceUser }); }
|
|
245
|
+
catch { fail("daemon_bootstrap_remote_identity_invalid"); }
|
|
246
|
+
if (!remote || typeof remote.directInstall !== "boolean") fail("daemon_bootstrap_remote_identity_invalid");
|
|
247
|
+
|
|
248
|
+
const identityInput = {
|
|
249
|
+
sshHost: spec.sshHost,
|
|
250
|
+
serviceUser: spec.serviceUser,
|
|
251
|
+
daemonBinary: trustedDaemonBinary,
|
|
252
|
+
directInstall: remote.directInstall,
|
|
253
|
+
};
|
|
254
|
+
let identity;
|
|
255
|
+
try { identity = await (spec.mode === "create" ? createIdentity(identityInput) : inspectIdentity(identityInput)); }
|
|
256
|
+
catch { fail("daemon_bootstrap_identity_unavailable"); }
|
|
257
|
+
const publicIdentity = checkedIdentity(identity);
|
|
258
|
+
const storedResume = bootstrapResumeRecord(spec, identity);
|
|
259
|
+
const deliveredResume = bootstrapResumeRecord(spec, identity, "profile_delivered");
|
|
260
|
+
let reuseCompletedProfile = false;
|
|
261
|
+
if (resumeStore) {
|
|
262
|
+
try {
|
|
263
|
+
if (spec.mode === "create") await resumeStore.save(storedResume);
|
|
264
|
+
else reuseCompletedProfile = completedProfilePhase(
|
|
265
|
+
await resumeStore.load(publicIdentity.hostKey),
|
|
266
|
+
storedResume,
|
|
267
|
+
deliveredResume,
|
|
268
|
+
);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
271
|
+
fail(error?.message === "daemon_bootstrap_resume_conflict" ? "daemon_bootstrap_resume_mismatch" : "daemon_bootstrap_resume_unavailable");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const registration = bootstrapRegistrationPayload({ identity, release, runtimeEngineId: spec.runtimeEngineId });
|
|
276
|
+
let state;
|
|
277
|
+
try { state = registrationState(await registerHost(registration), publicIdentity.hostKey); }
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
280
|
+
fail("daemon_bootstrap_registration_failed");
|
|
281
|
+
}
|
|
282
|
+
if (!state.registered) {
|
|
283
|
+
const pending = pendingReceipt(publicIdentity.hostKey);
|
|
284
|
+
if (!awaitApproval) return pending;
|
|
285
|
+
// This is a display-only receipt. It makes the established owner action
|
|
286
|
+
// visible before the process parks, without changing authorization flow.
|
|
287
|
+
if (onApprovalRequired) await onApprovalRequired(pending);
|
|
288
|
+
let approval;
|
|
289
|
+
try {
|
|
290
|
+
approval = await awaitApproval({
|
|
291
|
+
hostKey: publicIdentity.hostKey,
|
|
292
|
+
approvalUrl: dockerApprovalUrl(publicIdentity.hostKey),
|
|
293
|
+
timeoutSeconds: spec.approvalTimeoutSeconds,
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
// A failed wait transport/receipt is not a human cancellation. Fail
|
|
297
|
+
// closed; only the semantic waiter may return an explicit cancellation.
|
|
298
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
299
|
+
fail("daemon_bootstrap_approval_wait_failed");
|
|
300
|
+
}
|
|
301
|
+
if (!approval || typeof approval !== "object" || typeof approval.state !== "string") fail("daemon_bootstrap_approval_wait_invalid");
|
|
302
|
+
if (approval.state !== "approved") return approvalTerminal(approval.state, publicIdentity.hostKey);
|
|
303
|
+
try { state = registrationState(await registerHost(registration), publicIdentity.hostKey); }
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
306
|
+
fail("daemon_bootstrap_registration_failed");
|
|
307
|
+
}
|
|
308
|
+
if (!state.registered) return pendingReceipt(publicIdentity.hostKey);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let enrollment;
|
|
312
|
+
if (reuseCompletedProfile) {
|
|
313
|
+
// The persisted phase is accepted only after inspectIdentity established the
|
|
314
|
+
// same target-owned P-256 identity and exact custody/spec record above.
|
|
315
|
+
enrollment = {
|
|
316
|
+
outcome: "profile_delivered",
|
|
317
|
+
expected_host_key: publicIdentity.hostKey,
|
|
318
|
+
delivery: { installed: true },
|
|
319
|
+
reused: true,
|
|
320
|
+
};
|
|
321
|
+
} else {
|
|
322
|
+
try {
|
|
323
|
+
enrollment = await enroll({
|
|
324
|
+
serverUrl: spec.serverUrl,
|
|
325
|
+
expectedHostKey: publicIdentity.hostKey,
|
|
326
|
+
requestingHost: spec.requestingHost,
|
|
327
|
+
...(spec.caller ? { caller: spec.caller } : {}),
|
|
328
|
+
...(spec.callerHarness ? { callerHarness: spec.callerHarness } : {}),
|
|
329
|
+
...(spec.callerTicket ? { callerTicket: spec.callerTicket } : {}),
|
|
330
|
+
deliver: (artifact) => deliver({
|
|
331
|
+
artifact,
|
|
332
|
+
sshHost: spec.sshHost,
|
|
333
|
+
serviceUser: spec.serviceUser,
|
|
334
|
+
daemonBinary: trustedDaemonBinary,
|
|
335
|
+
profilePath: spec.profilePath,
|
|
336
|
+
expectedHostKey: publicIdentity.hostKey,
|
|
337
|
+
directInstall: remote.directInstall,
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
342
|
+
fail("daemon_bootstrap_enrollment_failed");
|
|
343
|
+
}
|
|
344
|
+
if (!profileDeliveryCompleted(enrollment, publicIdentity.hostKey)) fail("daemon_bootstrap_enrollment_failed");
|
|
345
|
+
if (resumeStore) {
|
|
346
|
+
try { await resumeStore.save(deliveredResume); }
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (error instanceof DaemonBootstrapError) throw error;
|
|
349
|
+
fail(error?.message === "daemon_bootstrap_resume_conflict" ? "daemon_bootstrap_resume_mismatch" : "daemon_bootstrap_resume_unavailable");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const serviceSpec = {
|
|
355
|
+
user: spec.serviceUser,
|
|
356
|
+
home: spec.home,
|
|
357
|
+
config: spec.config,
|
|
358
|
+
binary: installedBinaryPath(release),
|
|
359
|
+
};
|
|
360
|
+
const definition = spec.platform === "darwin" ? renderMacDaemon(serviceSpec) : renderLinuxUnit(serviceSpec);
|
|
361
|
+
let installation;
|
|
362
|
+
try {
|
|
363
|
+
installation = await installRemote({
|
|
364
|
+
platform: spec.platform,
|
|
365
|
+
host: spec.sshHost,
|
|
366
|
+
user: spec.serviceUser,
|
|
367
|
+
definition,
|
|
368
|
+
release,
|
|
369
|
+
config: spec.config,
|
|
370
|
+
});
|
|
371
|
+
} catch { fail("daemon_bootstrap_installation_failed"); }
|
|
372
|
+
if (!installation || typeof installation !== "object" || installation.outcome !== "installed") {
|
|
373
|
+
fail("daemon_bootstrap_installation_failed");
|
|
374
|
+
}
|
|
375
|
+
if (resumeStore) {
|
|
376
|
+
try { await resumeStore.remove(publicIdentity.hostKey); }
|
|
377
|
+
catch { fail("daemon_bootstrap_resume_remove_failed"); }
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
outcome: "installed",
|
|
381
|
+
host_key: publicIdentity.hostKey,
|
|
382
|
+
registration: { state: state.state, device_consent: true },
|
|
383
|
+
enrollment,
|
|
384
|
+
installation,
|
|
385
|
+
};
|
|
386
|
+
}
|