@nowcrew/daemon 0.6.89 → 0.6.91-beta.1
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 +64 -3
- package/dist/agent-ability/materializer.js +21 -10
- 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/automatic-daemon-installer.js +49 -1
- 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/daemon-updater.js +2 -12
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +13 -8
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +12 -12
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -84
- 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 +19 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +23 -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-command.js +21 -0
- package/dist/marketplace-controller.js +80 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +235 -0
- package/dist/marketplace-serve.js +41 -0
- package/dist/marketplace-source.js +103 -0
- package/dist/message-file-preview.js +246 -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/provider-env.js +26 -15
- package/dist/runner.js +17 -4
- 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/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 +78 -13
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +88 -178
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- 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 +13 -12
- 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 -241
- 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 -447
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { posix, win32 } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
4
5
|
import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
|
|
5
6
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
@@ -15,6 +16,7 @@ import { npmInvocation } from "./npm-command.js";
|
|
|
15
16
|
import { discoverExactLegacyService, } from "./automatic-service-migration.js";
|
|
16
17
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
17
18
|
import { serverHostIdentity } from "./server-url.js";
|
|
19
|
+
import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
|
|
18
20
|
function profileStorage(platform, userHome, runner) {
|
|
19
21
|
return {
|
|
20
22
|
platform,
|
|
@@ -247,8 +249,33 @@ function recoveryError(primary, failures) {
|
|
|
247
249
|
return original;
|
|
248
250
|
return new Error(`${original.message}; recovery failed: ${failures.map((error) => error.message).join("; ")}`, { cause: original });
|
|
249
251
|
}
|
|
252
|
+
function setupIdentity(input) {
|
|
253
|
+
if (input.workspaceId === undefined && input.machineId === undefined)
|
|
254
|
+
return null;
|
|
255
|
+
if (input.workspaceId === undefined || input.machineId === undefined) {
|
|
256
|
+
throw new Error("Computer setup requires both Workspace and Machine identity");
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
workspaceId: z.string().uuid().parse(input.workspaceId),
|
|
260
|
+
machineId: z.string().uuid().parse(input.machineId),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function assertExistingProfileIdentity(previous, identity) {
|
|
264
|
+
if (previous === null || identity === null)
|
|
265
|
+
return;
|
|
266
|
+
if (previous.workspaceId === undefined || previous.machineId === undefined) {
|
|
267
|
+
throw new Error(`Profile '${previous.name}' does not record its Workspace and Machine identity`);
|
|
268
|
+
}
|
|
269
|
+
if (previous.workspaceId !== identity.workspaceId) {
|
|
270
|
+
throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
|
|
271
|
+
}
|
|
272
|
+
if (previous.machineId !== identity.machineId) {
|
|
273
|
+
throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
250
276
|
export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
|
|
251
277
|
const machineToken = MachineTokenSchema.parse(input.machineToken);
|
|
278
|
+
const identity = setupIdentity(input);
|
|
252
279
|
const requestedLocalHome = input.requestedLocalHome ?? dependencies.environment.NOWWORK_LOCAL_HOME;
|
|
253
280
|
const requestedLocalHomeMode = input.requestedLocalHomeMode
|
|
254
281
|
?? dependencies.environment.NOWWORK_LOCAL_HOME_MODE
|
|
@@ -281,6 +308,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
281
308
|
});
|
|
282
309
|
const previousProfile = await dependencies.captureProfile(target);
|
|
283
310
|
await dependencies.assertProfileTarget(target, previousProfile);
|
|
311
|
+
assertExistingProfileIdentity(previousProfile, identity);
|
|
284
312
|
const previousService = await dependencies.captureService(spec, target);
|
|
285
313
|
if (previousService.installed && !previousService.exact) {
|
|
286
314
|
throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
|
|
@@ -331,6 +359,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
331
359
|
name: target.profile,
|
|
332
360
|
serverUrl: target.serverUrl,
|
|
333
361
|
machineToken,
|
|
362
|
+
...(identity === null ? {} : identity),
|
|
334
363
|
agentsRoot: target.agentsRoot,
|
|
335
364
|
releaseTag: target.releaseTag,
|
|
336
365
|
...(target.registryUrl === undefined ? {} : { registryUrl: target.registryUrl }),
|
|
@@ -342,14 +371,33 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
342
371
|
const status = await dependencies.verifyService(spec);
|
|
343
372
|
if (!status.running)
|
|
344
373
|
throw new Error(`Service '${spec.id}' did not reach running state`);
|
|
345
|
-
|
|
374
|
+
const result = {
|
|
346
375
|
profile: target.profile,
|
|
347
376
|
version: dependencies.version,
|
|
348
377
|
serviceId: spec.id,
|
|
349
378
|
running: true,
|
|
379
|
+
localHome: localHomeForTarget(target, dependencies.platform),
|
|
380
|
+
daemonHome: target.daemonHome,
|
|
381
|
+
agentsRoot: target.agentsRoot,
|
|
350
382
|
};
|
|
383
|
+
await input.verifyAfterStart?.(result);
|
|
384
|
+
return result;
|
|
351
385
|
}
|
|
352
386
|
catch (error) {
|
|
387
|
+
try {
|
|
388
|
+
await input.beforeRollback?.();
|
|
389
|
+
}
|
|
390
|
+
catch (rollbackError) {
|
|
391
|
+
throw new Error(`${error.message}; local recovery blocked: ${rollbackError.message}. `
|
|
392
|
+
+ computerSetupRecoveryGuidance({
|
|
393
|
+
serverUrl: target.serverUrl,
|
|
394
|
+
workspaceId: input.workspaceId ?? "unknown",
|
|
395
|
+
machineId: input.machineId ?? "unknown",
|
|
396
|
+
profile: target.profile,
|
|
397
|
+
serviceId: spec.id,
|
|
398
|
+
daemonHome: target.daemonHome,
|
|
399
|
+
}), { cause: error });
|
|
400
|
+
}
|
|
353
401
|
const failures = [];
|
|
354
402
|
if (profileSaved) {
|
|
355
403
|
try {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
3
|
+
import { runComputerSetupCommand, setupUsage } from "./computer-setup.js";
|
|
4
|
+
import { redactSetupSecrets } from "./computer-setup-client.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const result = await runComputerSetupCommand(process.argv.slice(2));
|
|
7
|
+
if (result !== null) {
|
|
8
|
+
process.exitCode = result;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const lang = detectDaemonLang();
|
|
12
|
+
process.stderr.write(`${formatDaemonText(lang, "Usage:")}\n${setupUsage((message, values) => formatDaemonText(lang, message, values))}`);
|
|
13
|
+
process.exitCode = 2;
|
|
14
|
+
}
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
process.stderr.write(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
package/dist/computer-profile.js
CHANGED
|
@@ -21,11 +21,12 @@ const ProfileFields = z.object({
|
|
|
21
21
|
name: z.string().regex(PROFILE_NAME),
|
|
22
22
|
serverUrl: ServerUrlSchema,
|
|
23
23
|
machineToken: MachineTokenSchema,
|
|
24
|
+
workspaceId: z.string().uuid().optional(),
|
|
25
|
+
machineId: z.string().uuid().optional(),
|
|
24
26
|
agentsRoot: z.string().min(1).optional(),
|
|
25
27
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
26
28
|
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
27
29
|
registryUrl: z.string().url().optional(),
|
|
28
|
-
codexHomeMode: z.enum(["shared", "private"]).optional(),
|
|
29
30
|
}).strict();
|
|
30
31
|
function validateProfileRegistry(value, context) {
|
|
31
32
|
const fields = value;
|
|
@@ -60,7 +61,13 @@ const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).
|
|
|
60
61
|
ciphertext: z.string().min(1).max(16384),
|
|
61
62
|
}).strict(),
|
|
62
63
|
}).strict().superRefine(validateProfileRegistry);
|
|
63
|
-
|
|
64
|
+
// 旧版本可能已保存 shared 字段;读取时丢弃退休设置,其余字段仍严格校验。
|
|
65
|
+
const StoredProfileSchema = z.preprocess((raw) => {
|
|
66
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
67
|
+
return raw;
|
|
68
|
+
const { codexHomeMode: _retiredMode, ...fields } = raw;
|
|
69
|
+
return fields;
|
|
70
|
+
}, z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]));
|
|
64
71
|
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
65
72
|
export class ProfileAgentsRootConflictError extends Error {
|
|
66
73
|
profile;
|
|
@@ -253,11 +260,12 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
253
260
|
version: 1,
|
|
254
261
|
name: profile.name,
|
|
255
262
|
serverUrl: profile.serverUrl,
|
|
263
|
+
...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
|
|
264
|
+
...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
|
|
256
265
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
257
266
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
258
267
|
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
259
268
|
...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
|
|
260
|
-
...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
|
|
261
269
|
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
262
270
|
});
|
|
263
271
|
await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
|
|
@@ -298,6 +306,8 @@ export async function loadProfile(name, home = daemonHome(), options = {}) {
|
|
|
298
306
|
version: stored.version,
|
|
299
307
|
name: stored.name,
|
|
300
308
|
serverUrl: stored.serverUrl,
|
|
309
|
+
...(stored.workspaceId === undefined ? {} : { workspaceId: stored.workspaceId }),
|
|
310
|
+
...(stored.machineId === undefined ? {} : { machineId: stored.machineId }),
|
|
301
311
|
...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
|
|
302
312
|
...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
|
|
303
313
|
...(stored.releaseTag === undefined ? {} : { releaseTag: stored.releaseTag }),
|
|
@@ -384,6 +394,8 @@ export function publicProfile(profile) {
|
|
|
384
394
|
name: profile.name,
|
|
385
395
|
serverUrl: profile.serverUrl,
|
|
386
396
|
machineTokenConfigured: true,
|
|
397
|
+
...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
|
|
398
|
+
...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
|
|
387
399
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
388
400
|
};
|
|
389
401
|
}
|
|
@@ -404,5 +416,6 @@ export function applyProfileToEnv(profile, env) {
|
|
|
404
416
|
env.CREW_NPM_REGISTRY = profile.registryUrl;
|
|
405
417
|
else
|
|
406
418
|
delete env.CREW_NPM_REGISTRY;
|
|
407
|
-
|
|
419
|
+
// 不再向运行时传播退休的模式开关;CODEX_HOME 沿用用户环境。
|
|
420
|
+
delete env.CREW_CODEX_HOME_MODE;
|
|
408
421
|
}
|
|
@@ -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
|
+
}
|