@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.30
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/README.md +89 -14
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/agent-memory/policy.js +3 -1
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +16 -10
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +13 -11
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +88 -92
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +20 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +31 -19
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +84 -15
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +95 -184
- package/dist/server-url.js +25 -0
- package/dist/session.js +4 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +5 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { MachineTokenSchema } from "./computer-profile.js";
|
|
4
|
+
import { systemCommandRunner, } from "./computer-service.js";
|
|
5
|
+
const HttpUrlSchema = z.string().url().refine((value) => {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
return (url.protocol === "http:" || url.protocol === "https:")
|
|
8
|
+
&& url.username.length === 0
|
|
9
|
+
&& url.password.length === 0;
|
|
10
|
+
}, "expected an HTTP(S) URL without embedded credentials");
|
|
11
|
+
const ServerOriginSchema = HttpUrlSchema.refine((value) => {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
return (url.pathname === "" || url.pathname === "/")
|
|
14
|
+
&& url.search.length === 0
|
|
15
|
+
&& url.hash.length === 0;
|
|
16
|
+
}, "expected a server origin without a path, query, or hash");
|
|
17
|
+
const SetupStartSchema = z.object({
|
|
18
|
+
deviceCode: z.string().min(16).max(512),
|
|
19
|
+
userCode: z.string().min(4).max(128),
|
|
20
|
+
verificationUri: HttpUrlSchema,
|
|
21
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
22
|
+
intervalSeconds: z.number().int().positive().max(300),
|
|
23
|
+
}).strict();
|
|
24
|
+
const SetupBootstrapSchema = z.object({
|
|
25
|
+
setupId: z.string().uuid(),
|
|
26
|
+
workspaceId: z.string().uuid(),
|
|
27
|
+
workspaceSlug: z.string().min(1).max(80).optional(),
|
|
28
|
+
machineId: z.string().uuid(),
|
|
29
|
+
serverUrl: ServerOriginSchema,
|
|
30
|
+
environment: z.string().min(1).max(64),
|
|
31
|
+
environmentId: z.string().min(1).max(128).optional(),
|
|
32
|
+
profile: z.string().min(1).max(128),
|
|
33
|
+
minimumConnectionEpoch: z.number().int().nonnegative(),
|
|
34
|
+
daemonVersion: z.string().min(1).max(128).optional(),
|
|
35
|
+
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
36
|
+
registryUrl: HttpUrlSchema.optional(),
|
|
37
|
+
storageId: z.string().min(1).max(256).optional(),
|
|
38
|
+
expiresAt: z.string().datetime({ offset: true }).optional(),
|
|
39
|
+
machineToken: MachineTokenSchema,
|
|
40
|
+
}).strict();
|
|
41
|
+
const SetupPendingSchema = z.object({
|
|
42
|
+
status: z.enum(["authorization_pending", "pending"]),
|
|
43
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
44
|
+
}).strict();
|
|
45
|
+
const SetupSlowDownSchema = z.object({
|
|
46
|
+
status: z.literal("slow_down"),
|
|
47
|
+
intervalSeconds: z.number().int().positive().max(300),
|
|
48
|
+
}).strict();
|
|
49
|
+
const SetupDeniedSchema = z.object({
|
|
50
|
+
status: z.enum(["access_denied", "denied"]),
|
|
51
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
52
|
+
}).strict();
|
|
53
|
+
const SetupExpiredSchema = z.object({
|
|
54
|
+
status: z.enum(["expired_token", "expired"]),
|
|
55
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
56
|
+
}).strict();
|
|
57
|
+
const SetupReadySchema = z.object({
|
|
58
|
+
status: z.literal("ready"),
|
|
59
|
+
machineId: z.string().uuid(),
|
|
60
|
+
workspaceId: z.string().min(1).max(256),
|
|
61
|
+
environment: z.string().min(1).max(64),
|
|
62
|
+
profile: z.string().min(1).max(128),
|
|
63
|
+
storageId: z.string().min(1).max(256).optional(),
|
|
64
|
+
daemonHome: z.string().min(1).max(32768),
|
|
65
|
+
agentsRoot: z.string().min(1).max(32768),
|
|
66
|
+
}).strict();
|
|
67
|
+
export class ComputerSetupHttpError extends Error {
|
|
68
|
+
status;
|
|
69
|
+
code;
|
|
70
|
+
constructor(message, status, code) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "ComputerSetupHttpError";
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function redactSetupSecrets(value, secrets = []) {
|
|
78
|
+
let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>");
|
|
79
|
+
for (const secret of secrets) {
|
|
80
|
+
if (secret.length > 0)
|
|
81
|
+
redacted = redacted.replaceAll(secret, "<redacted>");
|
|
82
|
+
}
|
|
83
|
+
return redacted;
|
|
84
|
+
}
|
|
85
|
+
export function computerSetupRecoveryGuidance(target) {
|
|
86
|
+
const localTarget = [
|
|
87
|
+
...(target.profile === undefined ? [] : [`profile '${target.profile}'`]),
|
|
88
|
+
...(target.serviceId === undefined ? [] : [`service '${target.serviceId}'`]),
|
|
89
|
+
...(target.daemonHome === undefined ? [] : [`daemon home '${target.daemonHome}'`]),
|
|
90
|
+
];
|
|
91
|
+
return `Local profile/service state was preserved${localTarget.length === 0 ? "" : ` for ${localTarget.join(", ")}`}. `
|
|
92
|
+
+ `Do not restore old credentials. Check Computer '${target.machineId}' in Workspace '${target.workspaceId}' `
|
|
93
|
+
+ `on ${target.serverUrl}. Resolve the reported error and check connectivity, then rerun nowwork-computer setup to reconcile the installation.`;
|
|
94
|
+
}
|
|
95
|
+
function normalizeServerUrl(value) {
|
|
96
|
+
const parsed = new URL(ServerOriginSchema.parse(value));
|
|
97
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
98
|
+
}
|
|
99
|
+
function pathForMachine(machineId, suffix) {
|
|
100
|
+
return `/api/machines/${encodeURIComponent(machineId)}/setup/${suffix}`;
|
|
101
|
+
}
|
|
102
|
+
function parseBootstrap(raw) {
|
|
103
|
+
const value = (raw !== null && typeof raw === "object" && "bootstrap" in raw)
|
|
104
|
+
? raw.bootstrap
|
|
105
|
+
: raw;
|
|
106
|
+
const candidate = value !== null && typeof value === "object"
|
|
107
|
+
? (() => {
|
|
108
|
+
const record = value;
|
|
109
|
+
const { token: legacyToken, ...rest } = record;
|
|
110
|
+
return {
|
|
111
|
+
...rest,
|
|
112
|
+
...(rest.machineToken === undefined && typeof legacyToken === "string"
|
|
113
|
+
? { machineToken: legacyToken }
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
})()
|
|
117
|
+
: value;
|
|
118
|
+
const parsed = SetupBootstrapSchema.parse(candidate);
|
|
119
|
+
return {
|
|
120
|
+
setupId: parsed.setupId,
|
|
121
|
+
workspaceId: parsed.workspaceId,
|
|
122
|
+
...(parsed.workspaceSlug === undefined ? {} : { workspaceSlug: parsed.workspaceSlug }),
|
|
123
|
+
machineId: parsed.machineId,
|
|
124
|
+
serverUrl: parsed.serverUrl,
|
|
125
|
+
environment: parsed.environment,
|
|
126
|
+
profile: parsed.profile,
|
|
127
|
+
minimumConnectionEpoch: parsed.minimumConnectionEpoch,
|
|
128
|
+
machineToken: parsed.machineToken,
|
|
129
|
+
...(parsed.environmentId === undefined ? {} : { environmentId: parsed.environmentId }),
|
|
130
|
+
...(parsed.daemonVersion === undefined ? {} : { daemonVersion: parsed.daemonVersion }),
|
|
131
|
+
...(parsed.releaseTag === undefined ? {} : { releaseTag: parsed.releaseTag }),
|
|
132
|
+
...(parsed.registryUrl === undefined ? {} : { registryUrl: parsed.registryUrl }),
|
|
133
|
+
...(parsed.storageId === undefined ? {} : { storageId: parsed.storageId }),
|
|
134
|
+
...(parsed.expiresAt === undefined ? {} : { expiresAt: parsed.expiresAt }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function parseStart(raw) {
|
|
138
|
+
return SetupStartSchema.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
function parsePoll(raw) {
|
|
141
|
+
if (raw !== null && typeof raw === "object" && "status" in raw
|
|
142
|
+
&& raw.status === "authorized") {
|
|
143
|
+
const record = raw;
|
|
144
|
+
if ("bootstrap" in raw) {
|
|
145
|
+
return { status: "authorized", bootstrap: parseBootstrap(raw) };
|
|
146
|
+
}
|
|
147
|
+
if (record.target === undefined || typeof record.token !== "string") {
|
|
148
|
+
throw new Error("Invalid authorized computer setup response");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
status: "authorized",
|
|
152
|
+
bootstrap: parseBootstrap({
|
|
153
|
+
...record.target,
|
|
154
|
+
setupId: record.setupId,
|
|
155
|
+
machineToken: record.token,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const pending = SetupPendingSchema.safeParse(raw);
|
|
160
|
+
if (pending.success) {
|
|
161
|
+
return {
|
|
162
|
+
status: "authorization_pending",
|
|
163
|
+
intervalSeconds: pending.data.intervalSeconds ?? 5,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const slowDown = SetupSlowDownSchema.safeParse(raw);
|
|
167
|
+
if (slowDown.success)
|
|
168
|
+
return slowDown.data;
|
|
169
|
+
const denied = SetupDeniedSchema.safeParse(raw);
|
|
170
|
+
if (denied.success) {
|
|
171
|
+
return {
|
|
172
|
+
status: "access_denied",
|
|
173
|
+
intervalSeconds: denied.data.intervalSeconds ?? 5,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const expired = SetupExpiredSchema.safeParse(raw);
|
|
177
|
+
if (expired.success) {
|
|
178
|
+
return {
|
|
179
|
+
status: "expired_token",
|
|
180
|
+
intervalSeconds: expired.data.intervalSeconds ?? 5,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Invalid computer setup polling response");
|
|
184
|
+
}
|
|
185
|
+
function parseHello(raw) {
|
|
186
|
+
if (raw !== null && typeof raw === "object" && "helloConfirmed" in raw
|
|
187
|
+
&& raw.helloConfirmed === true) {
|
|
188
|
+
const value = raw;
|
|
189
|
+
const parsed = SetupReadySchema.parse({
|
|
190
|
+
status: "ready",
|
|
191
|
+
machineId: value.machineId,
|
|
192
|
+
workspaceId: value.workspaceId,
|
|
193
|
+
environment: value.environment,
|
|
194
|
+
profile: value.profile,
|
|
195
|
+
...(value.storageId === undefined ? {} : { storageId: value.storageId }),
|
|
196
|
+
...(value.daemonHome === undefined ? {} : { daemonHome: value.daemonHome }),
|
|
197
|
+
...(value.agentsRoot === undefined ? {} : { agentsRoot: value.agentsRoot }),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
status: "ready",
|
|
201
|
+
machineId: parsed.machineId,
|
|
202
|
+
workspaceId: parsed.workspaceId,
|
|
203
|
+
environment: parsed.environment,
|
|
204
|
+
profile: parsed.profile,
|
|
205
|
+
...(parsed.storageId === undefined ? {} : { storageId: parsed.storageId }),
|
|
206
|
+
daemonHome: parsed.daemonHome,
|
|
207
|
+
agentsRoot: parsed.agentsRoot,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const ready = SetupReadySchema.safeParse(raw);
|
|
211
|
+
if (ready.success) {
|
|
212
|
+
return {
|
|
213
|
+
status: "ready",
|
|
214
|
+
machineId: ready.data.machineId,
|
|
215
|
+
workspaceId: ready.data.workspaceId,
|
|
216
|
+
environment: ready.data.environment,
|
|
217
|
+
profile: ready.data.profile,
|
|
218
|
+
...(ready.data.storageId === undefined ? {} : { storageId: ready.data.storageId }),
|
|
219
|
+
daemonHome: ready.data.daemonHome,
|
|
220
|
+
agentsRoot: ready.data.agentsRoot,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const pending = z.object({
|
|
224
|
+
status: z.literal("pending"),
|
|
225
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
226
|
+
}).safeParse(raw);
|
|
227
|
+
if (pending.success)
|
|
228
|
+
return { status: "pending", intervalSeconds: pending.data.intervalSeconds ?? 2 };
|
|
229
|
+
throw new Error("Invalid computer setup hello response");
|
|
230
|
+
}
|
|
231
|
+
export function createComputerSetupClient(options) {
|
|
232
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
233
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
234
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
|
235
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => {
|
|
236
|
+
setTimeout(resolve, milliseconds);
|
|
237
|
+
}));
|
|
238
|
+
const now = options.now ?? Date.now;
|
|
239
|
+
let activeMachineId;
|
|
240
|
+
const request = async (method, path, body, bearer) => {
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetchImpl(`${serverUrl}${path}`, {
|
|
245
|
+
method,
|
|
246
|
+
headers: {
|
|
247
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
248
|
+
...(bearer === undefined ? {} : { authorization: `Bearer ${bearer}` }),
|
|
249
|
+
},
|
|
250
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
});
|
|
253
|
+
const payload = await response.json();
|
|
254
|
+
if (!response.ok || payload?.success === false) {
|
|
255
|
+
const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
|
|
256
|
+
throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
return (payload?.success === true ? payload.data : payload);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (controller.signal.aborted)
|
|
262
|
+
throw new Error("computer setup request timed out");
|
|
263
|
+
if (error instanceof ComputerSetupHttpError)
|
|
264
|
+
throw error;
|
|
265
|
+
throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
clearTimeout(timeoutId);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
start: async (input) => {
|
|
273
|
+
activeMachineId = input.machineId;
|
|
274
|
+
return parseStart(await request("POST", "/api/auth/computer-setup/start", {
|
|
275
|
+
...(input.workspace.kind === "id"
|
|
276
|
+
? { workspaceId: input.workspace.value }
|
|
277
|
+
: { workspaceSlug: input.workspace.value }),
|
|
278
|
+
machineId: input.machineId,
|
|
279
|
+
deviceName: (input.deviceName ?? hostname()).trim().slice(0, 120) || "computer",
|
|
280
|
+
serverUrl,
|
|
281
|
+
}));
|
|
282
|
+
},
|
|
283
|
+
poll: async (deviceCode) => {
|
|
284
|
+
if (activeMachineId === undefined)
|
|
285
|
+
throw new Error("computer setup polling has no active machine");
|
|
286
|
+
return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
|
|
287
|
+
},
|
|
288
|
+
waitForHello: async (input) => {
|
|
289
|
+
const timeoutMs = input.timeoutMs ?? 120_000;
|
|
290
|
+
const deadline = now() + timeoutMs;
|
|
291
|
+
for (;;) {
|
|
292
|
+
const status = parseHello(await request("GET", `/daemon/computer-setup/status?machine_id=${encodeURIComponent(input.machineId)}&workspace_id=${encodeURIComponent(input.workspaceId)}&profile=${encodeURIComponent(input.profile)}&setup_id=${encodeURIComponent(input.setupId)}&minimum_connection_epoch=${encodeURIComponent(String(input.minimumConnectionEpoch))}`, undefined, input.machineToken));
|
|
293
|
+
if (status.status === "ready")
|
|
294
|
+
return status;
|
|
295
|
+
if (now() >= deadline)
|
|
296
|
+
throw new Error("Timed out waiting for machine hello");
|
|
297
|
+
await sleep(Math.min(status.intervalSeconds * 1_000, Math.max(0, deadline - now())));
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
finalize: async (input) => {
|
|
301
|
+
const result = await request("POST", "/daemon/computer-setup/finalize", { setupId: input.setupId }, input.machineToken);
|
|
302
|
+
if (result?.finalized !== true)
|
|
303
|
+
throw new Error("computer setup credential finalization not confirmed");
|
|
304
|
+
},
|
|
305
|
+
rollback: async (input) => {
|
|
306
|
+
const result = await request("POST", "/daemon/computer-setup/rollback", { setupId: input.setupId }, input.machineToken);
|
|
307
|
+
return result.rolledBack === true;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export async function openSetupBrowser(url, platform = process.platform, runner = systemCommandRunner) {
|
|
312
|
+
const command = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
|
|
313
|
+
const args = [url];
|
|
314
|
+
const result = await runner(command, args);
|
|
315
|
+
if (result.exitCode !== 0) {
|
|
316
|
+
throw new Error(`Unable to open the computer setup browser: ${redactSetupSecrets(result.stderr.trim() || `exit ${result.exitCode}`)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { computerSetupRecoveryGuidance, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
2
|
+
import { isProductionServerHostname, serverUrlsMatch } from "./server-url.js";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { resolveAgentsRoot } from "./computer-profile.js";
|
|
5
|
+
function safeError(error, machineToken) {
|
|
6
|
+
const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
|
|
7
|
+
return new Error(message, error instanceof Error ? { cause: error } : undefined);
|
|
8
|
+
}
|
|
9
|
+
function assertBootstrapMatches(bootstrap, input) {
|
|
10
|
+
if (bootstrap.machineId !== input.args.machineId) {
|
|
11
|
+
throw new Error("setup bootstrap machine does not match --machine");
|
|
12
|
+
}
|
|
13
|
+
const workspaceMatches = input.args.workspace.kind === "id"
|
|
14
|
+
? input.args.workspace.value === bootstrap.workspaceId
|
|
15
|
+
: bootstrap.workspaceSlug !== undefined && input.args.workspace.value === bootstrap.workspaceSlug;
|
|
16
|
+
if (!workspaceMatches)
|
|
17
|
+
throw new Error("setup bootstrap Workspace does not match the requested Workspace");
|
|
18
|
+
const bootstrapUrl = new URL(bootstrap.serverUrl);
|
|
19
|
+
const requestedUrl = new URL(input.serverUrl);
|
|
20
|
+
if (!serverUrlsMatch(bootstrapUrl.origin, requestedUrl.origin)) {
|
|
21
|
+
throw new Error("setup bootstrap server does not match the requested server context");
|
|
22
|
+
}
|
|
23
|
+
const hostname = bootstrapUrl.hostname.toLowerCase();
|
|
24
|
+
const knownEnvironment = hostname === "nowwork-dev.nowcoder.com"
|
|
25
|
+
? "dev"
|
|
26
|
+
: isProductionServerHostname(hostname)
|
|
27
|
+
? "production"
|
|
28
|
+
: null;
|
|
29
|
+
if (knownEnvironment !== null) {
|
|
30
|
+
const expectedReleaseTag = knownEnvironment === "dev" ? "beta" : "latest";
|
|
31
|
+
if (bootstrap.environment !== knownEnvironment) {
|
|
32
|
+
throw new Error("setup bootstrap environment does not match the server hostname");
|
|
33
|
+
}
|
|
34
|
+
if (bootstrap.releaseTag !== undefined && bootstrap.releaseTag !== expectedReleaseTag) {
|
|
35
|
+
throw new Error("setup bootstrap release tag does not match the server hostname");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function secondsUntil(expiresAt, now) {
|
|
40
|
+
const remaining = Date.parse(expiresAt) - now();
|
|
41
|
+
return Math.max(0, Math.ceil(remaining / 1_000));
|
|
42
|
+
}
|
|
43
|
+
function localPathsMatch(left, right, platform) {
|
|
44
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
45
|
+
if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
|
|
46
|
+
return false;
|
|
47
|
+
const normalize = (value) => {
|
|
48
|
+
const resolved = resolveAgentsRoot(value, undefined, platform);
|
|
49
|
+
return platform === "win32" ? win32.normalize(resolved).toLocaleLowerCase("en-US") : resolved;
|
|
50
|
+
};
|
|
51
|
+
return normalize(left) === normalize(right);
|
|
52
|
+
}
|
|
53
|
+
async function obtainBootstrap(input, deps) {
|
|
54
|
+
const pairing = await deps.client.start({
|
|
55
|
+
workspace: input.args.workspace,
|
|
56
|
+
machineId: input.args.machineId,
|
|
57
|
+
});
|
|
58
|
+
deps.reportPairing(`Open ${pairing.verificationUri}`);
|
|
59
|
+
deps.reportPairing(`Code ${pairing.userCode}`);
|
|
60
|
+
try {
|
|
61
|
+
await deps.openBrowser(pairing.verificationUri);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
deps.reportPairing(`Browser open skipped: ${redactSetupSecrets(error.message)}`);
|
|
65
|
+
}
|
|
66
|
+
let intervalSeconds = pairing.intervalSeconds;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const remainingSeconds = secondsUntil(pairing.expiresAt, deps.now);
|
|
69
|
+
if (remainingSeconds <= 0)
|
|
70
|
+
throw new Error("computer setup pairing expired");
|
|
71
|
+
await deps.sleep(Math.min(intervalSeconds * 1_000, remainingSeconds * 1_000));
|
|
72
|
+
const result = await deps.client.poll(pairing.deviceCode);
|
|
73
|
+
if (result.status === "authorization_pending" || result.status === "slow_down") {
|
|
74
|
+
intervalSeconds = result.intervalSeconds;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (result.status === "access_denied")
|
|
78
|
+
throw new Error("computer setup pairing was denied");
|
|
79
|
+
if (result.status === "expired_token")
|
|
80
|
+
throw new Error("computer setup pairing expired");
|
|
81
|
+
return result.bootstrap;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function runComputerSetupOrchestration(input, deps) {
|
|
85
|
+
let machineToken;
|
|
86
|
+
let bootstrap;
|
|
87
|
+
let credentialFinalized = false;
|
|
88
|
+
let verifiedStorageId;
|
|
89
|
+
let rollbackAttempt;
|
|
90
|
+
const confirmRollback = () => {
|
|
91
|
+
// Reuse both success and failure so an outer catch cannot change the recovery decision.
|
|
92
|
+
rollbackAttempt ??= (async () => {
|
|
93
|
+
if (credentialFinalized)
|
|
94
|
+
throw new Error("computer setup credentials already finalized");
|
|
95
|
+
if (bootstrap === undefined || machineToken === undefined)
|
|
96
|
+
throw new Error("computer setup credentials unavailable");
|
|
97
|
+
for (let attempt = 0;; attempt += 1) {
|
|
98
|
+
try {
|
|
99
|
+
const confirmed = await deps.client.rollback({ setupId: bootstrap.setupId, machineToken });
|
|
100
|
+
if (confirmed !== true)
|
|
101
|
+
throw new Error("computer setup rollback not confirmed");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (attempt >= 2)
|
|
106
|
+
throw error;
|
|
107
|
+
await deps.sleep(1_000);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return rollbackAttempt;
|
|
112
|
+
};
|
|
113
|
+
try {
|
|
114
|
+
const authorized = await obtainBootstrap(input, deps);
|
|
115
|
+
const token = authorized.machineToken;
|
|
116
|
+
bootstrap = authorized;
|
|
117
|
+
machineToken = token;
|
|
118
|
+
assertBootstrapMatches(authorized, input);
|
|
119
|
+
const installation = await deps.installAutomatically({
|
|
120
|
+
serverUrl: authorized.serverUrl,
|
|
121
|
+
machineToken: token,
|
|
122
|
+
workspaceId: authorized.workspaceId,
|
|
123
|
+
machineId: authorized.machineId,
|
|
124
|
+
...(authorized.profile === undefined ? {} : { requestedProfile: authorized.profile }),
|
|
125
|
+
...(authorized.releaseTag === "latest" || authorized.releaseTag === "beta"
|
|
126
|
+
? { requestedReleaseTag: authorized.releaseTag }
|
|
127
|
+
: {}),
|
|
128
|
+
...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
|
|
129
|
+
beforeRollback: confirmRollback,
|
|
130
|
+
verifyAfterStart: async (candidate) => {
|
|
131
|
+
const hello = await deps.client.waitForHello({
|
|
132
|
+
machineToken: token,
|
|
133
|
+
machineId: authorized.machineId,
|
|
134
|
+
workspaceId: authorized.workspaceId,
|
|
135
|
+
profile: candidate.profile,
|
|
136
|
+
setupId: authorized.setupId,
|
|
137
|
+
minimumConnectionEpoch: authorized.minimumConnectionEpoch,
|
|
138
|
+
});
|
|
139
|
+
if (hello.machineId !== authorized.machineId)
|
|
140
|
+
throw new Error("machine hello identity mismatch");
|
|
141
|
+
if (hello.workspaceId !== authorized.workspaceId)
|
|
142
|
+
throw new Error("machine hello workspace identity mismatch");
|
|
143
|
+
if (hello.environment !== authorized.environment)
|
|
144
|
+
throw new Error("machine hello environment identity mismatch");
|
|
145
|
+
if (hello.profile !== candidate.profile)
|
|
146
|
+
throw new Error("machine hello profile identity mismatch");
|
|
147
|
+
const platform = deps.platform ?? process.platform;
|
|
148
|
+
if (!localPathsMatch(hello.daemonHome, candidate.daemonHome, platform))
|
|
149
|
+
throw new Error("machine hello daemon home mismatch");
|
|
150
|
+
if (!localPathsMatch(hello.agentsRoot, candidate.agentsRoot, platform))
|
|
151
|
+
throw new Error("machine hello agents root mismatch");
|
|
152
|
+
if (authorized.storageId !== undefined && hello.storageId !== authorized.storageId) {
|
|
153
|
+
throw new Error("machine hello storage identity mismatch");
|
|
154
|
+
}
|
|
155
|
+
verifiedStorageId = hello.storageId;
|
|
156
|
+
let finalizeError;
|
|
157
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
158
|
+
try {
|
|
159
|
+
await deps.client.finalize({ setupId: authorized.setupId, machineToken: token });
|
|
160
|
+
credentialFinalized = true;
|
|
161
|
+
finalizeError = undefined;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
finalizeError = error;
|
|
166
|
+
if (attempt < 2)
|
|
167
|
+
await deps.sleep(1_000);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!credentialFinalized) {
|
|
171
|
+
throw finalizeError ?? new Error("computer setup credential finalization failed");
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
workspaceId: authorized.workspaceId,
|
|
177
|
+
machineId: authorized.machineId,
|
|
178
|
+
environment: authorized.environment,
|
|
179
|
+
profile: installation.profile,
|
|
180
|
+
daemonVersion: installation.version,
|
|
181
|
+
serviceId: installation.serviceId,
|
|
182
|
+
serviceStatus: "running",
|
|
183
|
+
helloConfirmed: true,
|
|
184
|
+
...(authorized.environmentId === undefined ? {} : { environmentId: authorized.environmentId }),
|
|
185
|
+
...(verifiedStorageId === undefined ? {} : { storageId: verifiedStorageId }),
|
|
186
|
+
localNowworkHome: installation.localHome,
|
|
187
|
+
daemonHome: installation.daemonHome,
|
|
188
|
+
agentsRoot: installation.agentsRoot,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (bootstrap !== undefined && machineToken !== undefined && !credentialFinalized && rollbackAttempt === undefined) {
|
|
193
|
+
try {
|
|
194
|
+
await confirmRollback();
|
|
195
|
+
}
|
|
196
|
+
catch (rollbackError) {
|
|
197
|
+
error = new Error(`${error.message}; computer setup rollback failed: ${rollbackError.message}; `
|
|
198
|
+
+ `local recovery blocked. ${computerSetupRecoveryGuidance(bootstrap)}`, { cause: error });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw safeError(error, machineToken);
|
|
202
|
+
}
|
|
203
|
+
}
|