@runuai/host 0.9.13 → 0.9.42
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 +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +69 -4
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3070 -223
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1463 -109
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +871 -50
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
package/lib/git-identity.ts
CHANGED
|
@@ -17,6 +17,16 @@ import { eq } from "drizzle-orm";
|
|
|
17
17
|
|
|
18
18
|
import { getDb, schema } from "./db";
|
|
19
19
|
import { dockerCli } from "./docker-exec";
|
|
20
|
+
import {
|
|
21
|
+
removeTaskAppContainer,
|
|
22
|
+
taskAppContainerName,
|
|
23
|
+
taskAppContainerState,
|
|
24
|
+
taskContainerCli,
|
|
25
|
+
} from "./task-container-cli";
|
|
26
|
+
import { runtimeAuthorityDockerExecArgs } from "./runtime-authority";
|
|
27
|
+
import { RUNTIME_AUTHORITY_ENV } from "./runtime-authority";
|
|
28
|
+
import { assertSafeHostTaskId } from "./task-identity";
|
|
29
|
+
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
20
30
|
import {
|
|
21
31
|
deleteKey,
|
|
22
32
|
getPublicKey,
|
|
@@ -33,22 +43,38 @@ export type DockerExec = (
|
|
|
33
43
|
) => Promise<{ status: number | null; stdout?: string; stderr: string }>;
|
|
34
44
|
|
|
35
45
|
const defaultExec: DockerExec = async (args) => {
|
|
36
|
-
const res = await
|
|
46
|
+
const res = await taskContainerCli(args, { timeoutMs: EXEC_TIMEOUT_MS });
|
|
37
47
|
return { status: res.status, stdout: res.stdout, stderr: res.stderr };
|
|
38
48
|
};
|
|
39
49
|
|
|
50
|
+
function managedTaskExecArgs(
|
|
51
|
+
user: "node" | "root",
|
|
52
|
+
container: string,
|
|
53
|
+
command: string[],
|
|
54
|
+
): string[] {
|
|
55
|
+
return [
|
|
56
|
+
"exec",
|
|
57
|
+
"-u",
|
|
58
|
+
user,
|
|
59
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
60
|
+
container,
|
|
61
|
+
...command,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
export async function setupTaskGitIdentity(
|
|
41
66
|
taskId: string,
|
|
42
67
|
name: string | null,
|
|
43
68
|
email: string | null,
|
|
44
69
|
exec: DockerExec = defaultExec,
|
|
70
|
+
environment?: Pick<TaskEnvironmentHandle, "exec">,
|
|
45
71
|
): Promise<boolean> {
|
|
46
72
|
if (!email) {
|
|
47
73
|
console.log(`[git] task ${taskId}: no owner email — leaving git identity`);
|
|
48
74
|
return false;
|
|
49
75
|
}
|
|
50
76
|
const displayName = name && name.trim() ? name.trim() : email;
|
|
51
|
-
const container =
|
|
77
|
+
const container = taskAppContainerName(taskId);
|
|
52
78
|
// Values are passed as argv (never interpolated into a shell string), so a
|
|
53
79
|
// name/email can't shell-inject.
|
|
54
80
|
const entries: ReadonlyArray<readonly [string, string]> = [
|
|
@@ -56,17 +82,22 @@ export async function setupTaskGitIdentity(
|
|
|
56
82
|
["user.email", email],
|
|
57
83
|
];
|
|
58
84
|
for (const [key, value] of entries) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
const command = ["/usr/bin/git", "config", "--global", key, value] as [
|
|
86
|
+
string,
|
|
87
|
+
...string[],
|
|
88
|
+
];
|
|
89
|
+
const res = environment
|
|
90
|
+
? await environment.exec({
|
|
91
|
+
argv: command,
|
|
92
|
+
user: "node",
|
|
93
|
+
env: RUNTIME_AUTHORITY_ENV,
|
|
94
|
+
timeoutMs: EXEC_TIMEOUT_MS,
|
|
95
|
+
maxOutputBytes: 64 * 1024,
|
|
96
|
+
}).then((result) => ({
|
|
97
|
+
status: result.exitCode,
|
|
98
|
+
stderr: Buffer.from(result.stderr).toString("utf8"),
|
|
99
|
+
}))
|
|
100
|
+
: await exec(managedTaskExecArgs("node", container, command));
|
|
70
101
|
if (res.status !== 0) {
|
|
71
102
|
console.warn(
|
|
72
103
|
`[git] task ${taskId}: git config ${key} failed: ${res.stderr.trim()}`,
|
|
@@ -97,15 +128,11 @@ export async function configureTaskSshTransport(
|
|
|
97
128
|
taskId: string,
|
|
98
129
|
exec: DockerExec = defaultExec,
|
|
99
130
|
): Promise<boolean> {
|
|
100
|
-
const res = await exec([
|
|
101
|
-
"exec",
|
|
102
|
-
"-u",
|
|
103
|
-
"node",
|
|
104
|
-
`task-${taskId}-app-1`,
|
|
131
|
+
const res = await exec(managedTaskExecArgs("node", taskAppContainerName(taskId), [
|
|
105
132
|
"/bin/sh",
|
|
106
133
|
"-c",
|
|
107
134
|
SSH_TRANSPORT_CONFIG,
|
|
108
|
-
]);
|
|
135
|
+
]));
|
|
109
136
|
if (res.status !== 0) {
|
|
110
137
|
console.warn(
|
|
111
138
|
`[ssh] task ${taskId}: SSH transport fallback setup failed: ${res.stderr.trim()}`,
|
|
@@ -127,19 +154,33 @@ export async function injectSshIdentity(
|
|
|
127
154
|
keyPath: string,
|
|
128
155
|
exec: DockerExec = defaultExec,
|
|
129
156
|
): Promise<boolean> {
|
|
130
|
-
const container =
|
|
157
|
+
const container = taskAppContainerName(taskId);
|
|
131
158
|
const steps: ReadonlyArray<readonly [string, string[]]> = [
|
|
132
|
-
[
|
|
159
|
+
[
|
|
160
|
+
"mkdir ~/.ssh",
|
|
161
|
+
managedTaskExecArgs("root", container, [
|
|
162
|
+
"/bin/mkdir",
|
|
163
|
+
"-p",
|
|
164
|
+
"/home/node/.ssh",
|
|
165
|
+
]),
|
|
166
|
+
],
|
|
133
167
|
["copy key", ["cp", keyPath, `${container}:/home/node/.ssh/id_ed25519`]],
|
|
134
168
|
["copy pubkey", ["cp", `${keyPath}.pub`, `${container}:/home/node/.ssh/id_ed25519.pub`]],
|
|
135
169
|
[
|
|
136
170
|
"own + chmod",
|
|
137
|
-
[
|
|
138
|
-
"
|
|
171
|
+
managedTaskExecArgs("root", container, [
|
|
172
|
+
"/bin/sh", "-c",
|
|
139
173
|
"/bin/chown -R node:node /home/node/.ssh && /bin/chmod 700 /home/node/.ssh && /bin/chmod 600 /home/node/.ssh/id_ed25519",
|
|
140
|
-
],
|
|
174
|
+
]),
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"SSH signing config",
|
|
178
|
+
managedTaskExecArgs("node", container, [
|
|
179
|
+
"/bin/sh",
|
|
180
|
+
"-c",
|
|
181
|
+
SSH_SIGNING_CONFIG,
|
|
182
|
+
]),
|
|
141
183
|
],
|
|
142
|
-
["SSH signing config", ["exec", "-u", "node", container, "/bin/sh", "-c", SSH_SIGNING_CONFIG]],
|
|
143
184
|
];
|
|
144
185
|
for (const [label, args] of steps) {
|
|
145
186
|
const res = await exec(args);
|
|
@@ -167,6 +208,34 @@ interface SshEnsureInFlight {
|
|
|
167
208
|
}
|
|
168
209
|
|
|
169
210
|
const sshEnsureInFlight = new Map<string, SshEnsureInFlight>();
|
|
211
|
+
const sshDeleteInFlight = new Map<string, Promise<void>>();
|
|
212
|
+
/** Durable for this host process: an orphan id is immutable/non-reused, and
|
|
213
|
+
* no later channel/recovery continuation may materialize its private key. */
|
|
214
|
+
const retiredTaskSshEnsures = new Set<string>();
|
|
215
|
+
|
|
216
|
+
/** Fence future task SSH ensures and join the exact older materialization, if
|
|
217
|
+
* any. This is stronger than deleting the directory once: channel startup and
|
|
218
|
+
* runtime recovery intentionally launch SSH reconciliation in the background,
|
|
219
|
+
* and an older health probe can otherwise resume by writing the key after GC
|
|
220
|
+
* has already checked absence. Never rejects; strict removal is the final
|
|
221
|
+
* filesystem authority and will surface any cleanup failure to the cloud. */
|
|
222
|
+
export async function retireTaskSshIdentityEnsures(
|
|
223
|
+
taskId: string,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
assertSafeHostTaskId(taskId);
|
|
226
|
+
retiredTaskSshEnsures.add(taskId);
|
|
227
|
+
for (;;) {
|
|
228
|
+
const current = sshEnsureInFlight.get(taskId);
|
|
229
|
+
if (!current) return;
|
|
230
|
+
await current.promise.catch(() => false);
|
|
231
|
+
// The tracked promise includes write/copy/finally-remove. Its outer caller
|
|
232
|
+
// performs only authority bookkeeping after this point, so clearing the
|
|
233
|
+
// matching entry is safe and avoids depending on microtask ordering.
|
|
234
|
+
if (sshEnsureInFlight.get(taskId) === current) {
|
|
235
|
+
sshEnsureInFlight.delete(taskId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
170
239
|
|
|
171
240
|
function sshPublicKeyFingerprint(publicKey: string): string | null {
|
|
172
241
|
const [, encoded] = publicKey.trim().split(/\s+/, 3);
|
|
@@ -190,15 +259,11 @@ async function sshKeyHealthy(
|
|
|
190
259
|
expectedPublicKey: string,
|
|
191
260
|
exec: DockerExec,
|
|
192
261
|
): Promise<boolean> {
|
|
193
|
-
const res = await exec([
|
|
194
|
-
"exec",
|
|
195
|
-
"-u",
|
|
196
|
-
"node",
|
|
197
|
-
`task-${taskId}-app-1`,
|
|
262
|
+
const res = await exec(managedTaskExecArgs("node", taskAppContainerName(taskId), [
|
|
198
263
|
"/bin/sh",
|
|
199
264
|
"-c",
|
|
200
265
|
"test -r /home/node/.ssh/id_ed25519 && /bin/cat /home/node/.ssh/id_ed25519.pub",
|
|
201
|
-
]);
|
|
266
|
+
]));
|
|
202
267
|
const expectedFingerprint = sshPublicKeyFingerprint(expectedPublicKey);
|
|
203
268
|
const actualFingerprint = sshPublicKeyFingerprint(res.stdout ?? "");
|
|
204
269
|
return (
|
|
@@ -226,6 +291,14 @@ export async function ensureTaskSshIdentity(
|
|
|
226
291
|
exec: DockerExec = defaultExec,
|
|
227
292
|
deps: EnsureTaskSshIdentityDeps = {},
|
|
228
293
|
): Promise<boolean> {
|
|
294
|
+
if (retiredTaskSshEnsures.has(taskId)) return false;
|
|
295
|
+
if (ownerUserId) {
|
|
296
|
+
const deleting = sshDeleteInFlight.get(ownerUserId);
|
|
297
|
+
if (deleting) {
|
|
298
|
+
await deleting.catch(() => {});
|
|
299
|
+
return ensureTaskSshIdentity(taskId, ownerUserId, exec, deps);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
229
302
|
const publicKeyForUser = deps.publicKeyForUser ?? getPublicKey;
|
|
230
303
|
const expectedPublicKey = ownerUserId
|
|
231
304
|
? publicKeyForUser(ownerUserId)
|
|
@@ -285,15 +358,15 @@ async function ensureTaskSshIdentityOnce(
|
|
|
285
358
|
// Fast path: the DB-authoritative key is already good → re-assert only the
|
|
286
359
|
// cheap signing config and skip docker cp churn.
|
|
287
360
|
if (await sshKeyHealthy(taskId, expectedPublicKey, exec)) {
|
|
288
|
-
const configured = await exec(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`task-${taskId}-app-1`,
|
|
361
|
+
const configured = await exec(managedTaskExecArgs(
|
|
362
|
+
"node",
|
|
363
|
+
taskAppContainerName(taskId),
|
|
364
|
+
[
|
|
293
365
|
"/bin/sh",
|
|
294
366
|
"-c",
|
|
295
367
|
SSH_SIGNING_CONFIG,
|
|
296
|
-
]
|
|
368
|
+
],
|
|
369
|
+
));
|
|
297
370
|
return configured.status === 0;
|
|
298
371
|
}
|
|
299
372
|
|
|
@@ -378,43 +451,64 @@ export async function removeTaskSshIdentityFromContainer(
|
|
|
378
451
|
taskId: string,
|
|
379
452
|
exec: DockerExec = defaultExec,
|
|
380
453
|
): Promise<void> {
|
|
381
|
-
const container =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const remaining = await checkedSshDocker(
|
|
454
|
+
const container = taskAppContainerName(taskId);
|
|
455
|
+
if (exec === defaultExec) {
|
|
456
|
+
// Backend-aware state/removal: the Apple CLI has no `ps`/`rm` verbs
|
|
457
|
+
// (review 2026-08-18 round 3, verified live: "Plugin 'container-ps' not
|
|
458
|
+
// found"), so the shared helpers own the per-backend translation and
|
|
459
|
+
// the whole-line absence proofs. The injected-exec seam below keeps
|
|
460
|
+
// scripting docker-shaped answers for tests.
|
|
461
|
+
const state = await taskAppContainerState(container);
|
|
462
|
+
if (state.kind === "absent") return;
|
|
463
|
+
if (state.kind === "unknown") {
|
|
464
|
+
throw new Error(`inspect ${container} failed: ${state.detail}`);
|
|
465
|
+
}
|
|
466
|
+
if (state.kind === "stopped") {
|
|
467
|
+
// Non-running credential-bearing containers are removed without
|
|
468
|
+
// executing their filesystem; removal is absence-verified inside.
|
|
469
|
+
await removeTaskAppContainer(container);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
const inspectArgs = [
|
|
474
|
+
"ps",
|
|
475
|
+
"--all",
|
|
476
|
+
"--filter",
|
|
477
|
+
`name=^/${container}$`,
|
|
478
|
+
"--format",
|
|
479
|
+
"{{.State}}",
|
|
480
|
+
];
|
|
481
|
+
const listed = await checkedSshDocker(
|
|
410
482
|
exec,
|
|
411
483
|
inspectArgs,
|
|
412
|
-
`
|
|
484
|
+
`inspect ${container}`,
|
|
413
485
|
);
|
|
414
|
-
|
|
415
|
-
|
|
486
|
+
const states = (listed.stdout ?? "")
|
|
487
|
+
.split(/\r?\n/)
|
|
488
|
+
.map((state) => state.trim())
|
|
489
|
+
.filter(Boolean);
|
|
490
|
+
if (states.length === 0) return;
|
|
491
|
+
if (states.length !== 1) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`inspect ${container} returned ${states.length} containers`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (states[0] !== "running") {
|
|
497
|
+
await checkedSshDocker(
|
|
498
|
+
exec,
|
|
499
|
+
["rm", "--force", container],
|
|
500
|
+
`remove SSH credential-bearing ${container}`,
|
|
501
|
+
);
|
|
502
|
+
const remaining = await checkedSshDocker(
|
|
503
|
+
exec,
|
|
504
|
+
inspectArgs,
|
|
505
|
+
`verify removal of ${container}`,
|
|
506
|
+
);
|
|
507
|
+
if ((remaining.stdout ?? "").trim()) {
|
|
508
|
+
throw new Error(`remove ${container} was not verified`);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
416
511
|
}
|
|
417
|
-
return;
|
|
418
512
|
}
|
|
419
513
|
|
|
420
514
|
await checkedSshDocker(
|
|
@@ -424,6 +518,7 @@ export async function removeTaskSshIdentityFromContainer(
|
|
|
424
518
|
"-u",
|
|
425
519
|
"node",
|
|
426
520
|
...SANITIZED_SSH_EXEC_ENV,
|
|
521
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
427
522
|
container,
|
|
428
523
|
"/usr/bin/env",
|
|
429
524
|
"-i",
|
|
@@ -458,30 +553,47 @@ function sshCredentialTaskIdsForUser(userId: string): string[] {
|
|
|
458
553
|
}
|
|
459
554
|
|
|
460
555
|
export interface DeleteUserSshIdentityDeps {
|
|
556
|
+
/** Synchronous fail-closed check that must pass before authority changes. */
|
|
557
|
+
preflight?: () => void;
|
|
461
558
|
deleteStored?: (userId: string) => void;
|
|
462
559
|
taskIds?: (userId: string) => string[];
|
|
463
560
|
removeFromContainer?: (taskId: string) => Promise<void>;
|
|
464
561
|
}
|
|
465
562
|
|
|
466
|
-
/**
|
|
467
|
-
* material
|
|
563
|
+
/** Serialize deletion against new ensures, drain every older ensure, scrub all
|
|
564
|
+
* copied material, and only then delete authority. A failed scrub leaves the
|
|
565
|
+
* key authoritative and retryable instead of creating an unrecoverable split
|
|
566
|
+
* where host authority is gone but a valid private key remains in a container. */
|
|
468
567
|
export async function deleteUserSshIdentity(
|
|
469
568
|
userId: string,
|
|
470
569
|
deps: DeleteUserSshIdentityDeps = {},
|
|
471
570
|
): Promise<void> {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
deps.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
571
|
+
const existing = sshDeleteInFlight.get(userId);
|
|
572
|
+
if (existing) return existing;
|
|
573
|
+
const run = async (): Promise<void> => {
|
|
574
|
+
deps.preflight?.();
|
|
575
|
+
const taskIds = (deps.taskIds ?? sshCredentialTaskIdsForUser)(userId);
|
|
576
|
+
const removeFromContainer =
|
|
577
|
+
deps.removeFromContainer ?? removeTaskSshIdentityFromContainer;
|
|
578
|
+
const settled = await Promise.allSettled(
|
|
579
|
+
taskIds.map(async (taskId) => {
|
|
580
|
+
await sshEnsureInFlight.get(taskId)?.promise.catch(() => {});
|
|
581
|
+
removeTaskIdentity(taskId);
|
|
582
|
+
await removeFromContainer(taskId);
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
585
|
+
const failed = settled.find(
|
|
586
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
587
|
+
);
|
|
588
|
+
if (failed) throw failed.reason;
|
|
589
|
+
(deps.deleteStored ?? deleteKey)(userId);
|
|
590
|
+
};
|
|
591
|
+
let tracked!: Promise<void>;
|
|
592
|
+
tracked = run().finally(() => {
|
|
593
|
+
if (sshDeleteInFlight.get(userId) === tracked) {
|
|
594
|
+
sshDeleteInFlight.delete(userId);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
sshDeleteInFlight.set(userId, tracked);
|
|
598
|
+
return tracked;
|
|
487
599
|
}
|