@runuai/host 0.9.1 → 0.9.3
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/images/standard/container/uai-init +38 -19
- package/lib/agent.ts +42 -16
- package/lib/git-identity.ts +349 -70
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-tokens.ts +756 -110
- package/lib/orchestrator.ts +70 -28
- package/lib/repo-clone.ts +12 -101
- package/lib/ssh.ts +11 -8
- package/package.json +1 -1
- package/scripts/agent/_common.sh +214 -0
- package/scripts/agent/task-down.sh +35 -59
- package/scripts/agent/task-up.sh +914 -72
- package/src/index.ts +78 -5
- package/src/main.ts +100 -31
- package/src/protocol.ts +4 -0
package/src/index.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { cloneRepo } from "../lib/repo-clone";
|
|
|
4
4
|
import { handleFilesOp } from "../lib/shared-files";
|
|
5
5
|
import { getOrchestrator } from "../lib/orchestrator";
|
|
6
6
|
import { storeTaskCliSecret } from "../lib/agent-cli";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearRefresh,
|
|
9
|
+
reconcileTaskGitAuth,
|
|
10
|
+
} from "../lib/github-tokens";
|
|
11
|
+
import {
|
|
12
|
+
prepareTaskGithubGitCredential,
|
|
13
|
+
type TaskGithubGitCredential,
|
|
14
|
+
} from "../lib/github-git-auth";
|
|
8
15
|
import { readAttachment, writeAttachment } from "../lib/attachments";
|
|
9
16
|
import { appendTranscript as writeTranscript } from "../lib/transcript";
|
|
10
17
|
import { buildTaskDiff } from "../lib/task-diff";
|
|
@@ -219,7 +226,37 @@ export const hostCommands: HostCommands = {
|
|
|
219
226
|
{},
|
|
220
227
|
);
|
|
221
228
|
}
|
|
222
|
-
|
|
229
|
+
let gitCredential: TaskGithubGitCredential | null;
|
|
230
|
+
try {
|
|
231
|
+
gitCredential = input.projects.some((project) =>
|
|
232
|
+
isGithubRepo(project.repoUrl),
|
|
233
|
+
)
|
|
234
|
+
? await prepareTaskGithubGitCredential(input.task.ownerUserId)
|
|
235
|
+
: null;
|
|
236
|
+
} catch {
|
|
237
|
+
throw new AgentError(
|
|
238
|
+
"GITHUB_AUTH_UNAVAILABLE",
|
|
239
|
+
"GitHub is connected on this host, but Uai could not prepare its credential for Git. Retry the task; if it persists, reconnect GitHub on this host.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return gitCredential
|
|
244
|
+
? await gitCredential.run(() =>
|
|
245
|
+
agent.taskUp(input, {
|
|
246
|
+
githubCredentialSocket: gitCredential.socketPath,
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
: await agent.taskUp(input);
|
|
250
|
+
} finally {
|
|
251
|
+
await gitCredential?.close().catch((error: unknown) => {
|
|
252
|
+
// Cleanup must not replace the actual task-up result/error. The
|
|
253
|
+
// credential is short-lived and close() also removes its private
|
|
254
|
+
// directory in a finally block; retain diagnostics for operators.
|
|
255
|
+
console.warn(
|
|
256
|
+
`[github] task ${input.task.id}: credential cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
223
260
|
});
|
|
224
261
|
if (result.ok) {
|
|
225
262
|
orchestrator.allowChannel(input.task.id);
|
|
@@ -237,10 +274,19 @@ export const hostCommands: HostCommands = {
|
|
|
237
274
|
// background — it must never block or fail task-up. Awaiting it here would
|
|
238
275
|
// couple the command result to a network token-exchange: a slow/hung
|
|
239
276
|
// exchange (e.g. cloud mid-deploy) would trip the cloud's command timeout
|
|
240
|
-
// and mark a running task as errored.
|
|
277
|
+
// and mark a running task as errored. The reconciler injects + schedules
|
|
241
278
|
// (or emits a system note on failure) on its own; the agents come up
|
|
242
279
|
// regardless and the token lands well before the first `gh` call.
|
|
243
|
-
void
|
|
280
|
+
void reconcileTaskGitAuth(
|
|
281
|
+
input.task.id,
|
|
282
|
+
input.task.ownerUserId,
|
|
283
|
+
).catch((err) =>
|
|
284
|
+
console.warn(
|
|
285
|
+
`[github] task ${input.task.id}: post-start reconciliation failed: ${
|
|
286
|
+
err instanceof Error ? err.message : String(err)
|
|
287
|
+
}`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
244
290
|
} else {
|
|
245
291
|
recordTaskError(input.task.id);
|
|
246
292
|
}
|
|
@@ -554,6 +600,14 @@ function mapAgentError(code: string): HostErrorCode {
|
|
|
554
600
|
return HostErrorCode.WorktreeFailed;
|
|
555
601
|
case "CLONE_FAILED":
|
|
556
602
|
return HostErrorCode.CloneFailed;
|
|
603
|
+
case "GITHUB_SSH_ACCESS_DENIED":
|
|
604
|
+
return HostErrorCode.GitHubSshAccessDenied;
|
|
605
|
+
case "GITHUB_TOKEN_ACCESS_DENIED":
|
|
606
|
+
return HostErrorCode.GitHubTokenAccessDenied;
|
|
607
|
+
case "GITHUB_AUTH_UNAVAILABLE":
|
|
608
|
+
return HostErrorCode.GitHubAuthUnavailable;
|
|
609
|
+
case "GITHUB_CONNECTION_REQUIRED":
|
|
610
|
+
return HostErrorCode.GitHubConnectionRequired;
|
|
557
611
|
case "FETCH_FAILED":
|
|
558
612
|
return HostErrorCode.FetchFailed;
|
|
559
613
|
case "RENDER_FAILED":
|
|
@@ -567,6 +621,21 @@ function mapAgentError(code: string): HostErrorCode {
|
|
|
567
621
|
}
|
|
568
622
|
}
|
|
569
623
|
|
|
624
|
+
/** Host-side predicate only; API-boundary normalization already canonicalizes
|
|
625
|
+
* normal GitHub projects. Keep scratchpads/GitLab/local remotes independent of
|
|
626
|
+
* an unrelated broken GitHub connection on the same host. */
|
|
627
|
+
function isGithubRepo(input: string): boolean {
|
|
628
|
+
const value = input.trim();
|
|
629
|
+
if (/^(?:[^@/]+@)?(?:www\.)?github\.com:/i.test(value)) return true;
|
|
630
|
+
if (/^[\w.-]+\/[\w.-]+(?:\.git)?\/?$/i.test(value)) return true;
|
|
631
|
+
try {
|
|
632
|
+
const url = new URL(value);
|
|
633
|
+
return /^(?:www\.)?github\.com$/i.test(url.hostname);
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
570
639
|
function mapDeliverError(message: string): HostErrorCode {
|
|
571
640
|
if (message.includes("not found")) return HostErrorCode.TaskNotFound;
|
|
572
641
|
if (message.includes("not running")) return HostErrorCode.TaskNotRunning;
|
|
@@ -575,7 +644,11 @@ function mapDeliverError(message: string): HostErrorCode {
|
|
|
575
644
|
}
|
|
576
645
|
|
|
577
646
|
function isRetryableAgentError(code: string): boolean {
|
|
578
|
-
return
|
|
647
|
+
return (
|
|
648
|
+
code === "FETCH_FAILED" ||
|
|
649
|
+
code === "CONTAINER_INIT_FAILED" ||
|
|
650
|
+
code === "GITHUB_AUTH_UNAVAILABLE"
|
|
651
|
+
);
|
|
579
652
|
}
|
|
580
653
|
|
|
581
654
|
function toLegacyDecision(decision: PermissionDecision): "accept" | "decline" {
|
package/src/main.ts
CHANGED
|
@@ -30,11 +30,17 @@ import {
|
|
|
30
30
|
onConnectClear,
|
|
31
31
|
onConnectSet,
|
|
32
32
|
onGithubChange,
|
|
33
|
+
reconcileTaskGitAuth,
|
|
34
|
+
runGithubConnectionTransition,
|
|
33
35
|
setAuthExpiredHandler,
|
|
34
36
|
} from "../lib/github-tokens";
|
|
37
|
+
import { invalidateTaskGithubGitCredentials } from "../lib/github-git-auth";
|
|
38
|
+
import {
|
|
39
|
+
deleteUserSshIdentity,
|
|
40
|
+
removeTaskSshIdentityFromContainer,
|
|
41
|
+
} from "../lib/git-identity";
|
|
35
42
|
import { reinjectCodexRunningTasks, watchCodexAuth } from "../lib/codex-auth";
|
|
36
43
|
import {
|
|
37
|
-
deleteKey as deleteSshKey,
|
|
38
44
|
ensureKeyForUser as ensureSshKeyForUser,
|
|
39
45
|
getPublicKey as getSshPublicKey,
|
|
40
46
|
} from "../lib/ssh";
|
|
@@ -335,45 +341,108 @@ function connect(): void {
|
|
|
335
341
|
closeTunnel(socket, frame.tunnelId, frame.reason);
|
|
336
342
|
break;
|
|
337
343
|
case "gh.connect.set": {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
344
|
+
// Account switches fence every host-side Git operation using the old
|
|
345
|
+
// credential before the replacement grant is stored or reinjected.
|
|
346
|
+
void runGithubConnectionTransition(frame.userId, () =>
|
|
347
|
+
onConnectSet(frame, {
|
|
348
|
+
invalidateCredentials: invalidateTaskGithubGitCredentials,
|
|
349
|
+
reconcile: (taskId, userId) =>
|
|
350
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
351
|
+
reconcileTaskGitAuth(taskId, userId),
|
|
352
|
+
),
|
|
353
|
+
}),
|
|
354
|
+
).then(
|
|
355
|
+
(result) =>
|
|
356
|
+
send(
|
|
357
|
+
socket,
|
|
358
|
+
result.ok
|
|
359
|
+
? { kind: "gh.connect.ack", userId: frame.userId, ok: true }
|
|
360
|
+
: {
|
|
361
|
+
kind: "gh.connect.ack",
|
|
362
|
+
userId: frame.userId,
|
|
363
|
+
ok: false,
|
|
364
|
+
error: result.error ?? "store failed",
|
|
365
|
+
},
|
|
366
|
+
),
|
|
367
|
+
(err) => {
|
|
368
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
369
|
+
console.warn(`[github] connect.set failed: ${error}`);
|
|
370
|
+
send(socket, {
|
|
371
|
+
kind: "gh.connect.ack",
|
|
372
|
+
userId: frame.userId,
|
|
373
|
+
ok: false,
|
|
374
|
+
error,
|
|
375
|
+
});
|
|
376
|
+
},
|
|
349
377
|
);
|
|
350
378
|
break;
|
|
351
379
|
}
|
|
352
|
-
case "gh.connect.clear":
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
void
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
380
|
+
case "gh.connect.clear": {
|
|
381
|
+
// Local deletion happens synchronously inside onConnectClear and the
|
|
382
|
+
// remote revoke starts immediately in the background. Delay the ack
|
|
383
|
+
// only for live-container logout + transport transition, so the UI
|
|
384
|
+
// cannot report Disconnected while a task can still use the old token.
|
|
385
|
+
void runGithubConnectionTransition(frame.userId, () =>
|
|
386
|
+
onConnectClear(frame.userId, {
|
|
387
|
+
invalidateCredentials: invalidateTaskGithubGitCredentials,
|
|
388
|
+
reconcile: (taskId, userId) =>
|
|
389
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
390
|
+
reconcileTaskGitAuth(taskId, userId),
|
|
391
|
+
),
|
|
392
|
+
}),
|
|
393
|
+
).then(
|
|
394
|
+
() =>
|
|
395
|
+
send(socket, {
|
|
396
|
+
kind: "gh.connect.ack",
|
|
397
|
+
userId: frame.userId,
|
|
398
|
+
ok: true,
|
|
399
|
+
}),
|
|
400
|
+
(err) => {
|
|
401
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.warn(`[github] connect.clear failed: ${error}`);
|
|
403
|
+
send(socket, {
|
|
404
|
+
kind: "gh.connect.ack",
|
|
405
|
+
userId: frame.userId,
|
|
406
|
+
ok: false,
|
|
407
|
+
error,
|
|
408
|
+
});
|
|
409
|
+
},
|
|
361
410
|
);
|
|
362
|
-
send(socket, { kind: "gh.connect.ack", userId: frame.userId, ok: true });
|
|
363
411
|
break;
|
|
412
|
+
}
|
|
364
413
|
case "ssh.key.get":
|
|
365
414
|
case "ssh.key.ensure":
|
|
366
415
|
case "ssh.key.delete": {
|
|
416
|
+
if (frame.kind === "ssh.key.delete") {
|
|
417
|
+
void deleteUserSshIdentity(frame.userId, {
|
|
418
|
+
removeFromContainer: (taskId) =>
|
|
419
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
420
|
+
removeTaskSshIdentityFromContainer(taskId),
|
|
421
|
+
),
|
|
422
|
+
}).then(
|
|
423
|
+
() =>
|
|
424
|
+
send(socket, {
|
|
425
|
+
kind: "ssh.key.ack",
|
|
426
|
+
userId: frame.userId,
|
|
427
|
+
ok: true,
|
|
428
|
+
publicKey: null,
|
|
429
|
+
}),
|
|
430
|
+
(err) =>
|
|
431
|
+
send(socket, {
|
|
432
|
+
kind: "ssh.key.ack",
|
|
433
|
+
userId: frame.userId,
|
|
434
|
+
ok: false,
|
|
435
|
+
error:
|
|
436
|
+
err instanceof Error ? err.message : "ssh key delete failed",
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
367
441
|
try {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
} else if (frame.kind === "ssh.key.ensure") {
|
|
373
|
-
publicKey = ensureSshKeyForUser(frame.userId);
|
|
374
|
-
} else {
|
|
375
|
-
publicKey = getSshPublicKey(frame.userId);
|
|
376
|
-
}
|
|
442
|
+
const publicKey =
|
|
443
|
+
frame.kind === "ssh.key.ensure"
|
|
444
|
+
? ensureSshKeyForUser(frame.userId)
|
|
445
|
+
: getSshPublicKey(frame.userId);
|
|
377
446
|
send(socket, { kind: "ssh.key.ack", userId: frame.userId, ok: true, publicKey });
|
|
378
447
|
} catch (err) {
|
|
379
448
|
send(socket, {
|
package/src/protocol.ts
CHANGED
|
@@ -9,6 +9,10 @@ export enum HostErrorCode {
|
|
|
9
9
|
ComposeDownFailed = "compose_down_failed",
|
|
10
10
|
WorktreeFailed = "worktree_failed",
|
|
11
11
|
CloneFailed = "clone_failed",
|
|
12
|
+
GitHubSshAccessDenied = "github_ssh_access_denied",
|
|
13
|
+
GitHubTokenAccessDenied = "github_token_access_denied",
|
|
14
|
+
GitHubAuthUnavailable = "github_auth_unavailable",
|
|
15
|
+
GitHubConnectionRequired = "github_connection_required",
|
|
12
16
|
FetchFailed = "fetch_failed",
|
|
13
17
|
RenderFailed = "render_failed",
|
|
14
18
|
DbFailed = "db_failed",
|