@runuai/host 0.9.1 → 0.9.2
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 +746 -72
- package/src/index.ts +78 -5
- package/src/main.ts +100 -31
- package/src/protocol.ts +4 -0
|
@@ -37,33 +37,52 @@ else
|
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
39
|
# ---------------------------------------------------------------------------
|
|
40
|
-
# 0.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# on a prompt. `gh` is separate: it authenticates from its own config file,
|
|
46
|
-
# which the host writes via `gh auth login --with-token` with the user's
|
|
47
|
-
# short-lived token. We must NOT set GH_TOKEN in the env — `gh` would prefer
|
|
48
|
-
# it over that stored credential (and re-attribute every PR to it).
|
|
40
|
+
# 0. Git transport selection (ADR-027). A connected user's App token is the
|
|
41
|
+
# primary clone/fetch/push credential over HTTPS; the host writes it into
|
|
42
|
+
# gh's private config and runs `gh auth setup-git` before agents start. SSH
|
|
43
|
+
# transport is only the no-GitHub-connection fallback. The SSH key remains
|
|
44
|
+
# useful in either mode for commit/tag signing.
|
|
49
45
|
# ---------------------------------------------------------------------------
|
|
50
46
|
|
|
51
|
-
if [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
if [ "${UAI_SKIP_GIT_TRANSPORT:-0}" = "1" ]; then
|
|
48
|
+
# Recovery/connect/disconnect reconciles the current credential immediately
|
|
49
|
+
# before invoking us. Do not overwrite that live decision with the static
|
|
50
|
+
# transport marker captured when Compose was first created.
|
|
51
|
+
log "keeping the reconciled GitHub transport"
|
|
52
|
+
elif [ "${UAI_GIT_TRANSPORT:-anonymous}" = "ssh" ]; then
|
|
53
|
+
# Remove a prior token-mode reverse rewrite if this container is being
|
|
54
|
+
# repaired after disconnect, then route canonical HTTPS origins over SSH.
|
|
55
|
+
/usr/bin/git config --global --unset-all url."https://github.com/".insteadOf \
|
|
56
|
+
>/dev/null 2>&1 || true
|
|
57
|
+
/usr/bin/git config --global url."git@github.com:".insteadOf "https://github.com/"
|
|
58
|
+
/usr/bin/git config --global core.sshCommand "/usr/bin/ssh -o StrictHostKeyChecking=accept-new"
|
|
59
|
+
log "using SSH as the GitHub transport fallback"
|
|
60
|
+
else
|
|
61
|
+
# Heal containers created by the old SSH-only policy. Canonical origins are
|
|
62
|
+
# HTTPS; reverse legacy SSH URLs (including submodules) through gh as well.
|
|
63
|
+
/usr/bin/git config --global --unset-all url."git@github.com:".insteadOf \
|
|
64
|
+
>/dev/null 2>&1 || true
|
|
65
|
+
/usr/bin/git config --global --unset-all core.sshCommand >/dev/null 2>&1 || true
|
|
66
|
+
/usr/bin/git config --global --replace-all url."https://github.com/".insteadOf \
|
|
67
|
+
"git@github.com:"
|
|
68
|
+
/usr/bin/git config --global --add url."https://github.com/".insteadOf \
|
|
69
|
+
"ssh://git@github.com/"
|
|
70
|
+
if [ "${UAI_GIT_TRANSPORT:-}" = "https" ]; then
|
|
71
|
+
log "using the connected GitHub credential for HTTPS Git"
|
|
72
|
+
else
|
|
73
|
+
log "using anonymous HTTPS Git for public repositories"
|
|
74
|
+
fi
|
|
55
75
|
fi
|
|
56
76
|
|
|
57
77
|
# Signed commits (policy): when the uai SSH identity is present, configure git
|
|
58
78
|
# to SSH-sign every commit + tag with it. The pubkey is registered on GitHub
|
|
59
|
-
#
|
|
60
|
-
# git push over SSH (configured above).
|
|
79
|
+
# so commits show as Verified. Signing is independent of Git transport.
|
|
61
80
|
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
|
|
62
81
|
log "enabling SSH commit signing with the uai identity"
|
|
63
|
-
git config --global gpg.format ssh
|
|
64
|
-
git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub"
|
|
65
|
-
git config --global commit.gpgsign true
|
|
66
|
-
git config --global tag.gpgsign true
|
|
82
|
+
/usr/bin/git config --global gpg.format ssh
|
|
83
|
+
/usr/bin/git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub"
|
|
84
|
+
/usr/bin/git config --global commit.gpgsign true
|
|
85
|
+
/usr/bin/git config --global tag.gpgsign true
|
|
67
86
|
fi
|
|
68
87
|
|
|
69
88
|
# Attribution policy: never co-author with the agents. Claude Code honors
|
package/lib/agent.ts
CHANGED
|
@@ -33,6 +33,11 @@ import { getHostTask } from "./runtime-state";
|
|
|
33
33
|
import { removeTaskIdentity, writeTaskIdentity } from "./ssh";
|
|
34
34
|
import type { TaskDownInput, TaskLaunchInput } from "../src/protocol";
|
|
35
35
|
|
|
36
|
+
interface TaskUpCredentials {
|
|
37
|
+
/** Path to the task owner's private, ephemeral Git credential cache. */
|
|
38
|
+
githubCredentialSocket?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
// ---------------------------------------------------------------------------
|
|
37
42
|
// Locate the agent scripts. They ship inside the package, so by default we
|
|
38
43
|
// resolve them relative to this module — which works identically in a repo
|
|
@@ -136,11 +141,12 @@ async function runAgent<T extends z.ZodTypeAny>(
|
|
|
136
141
|
dataSchema: T,
|
|
137
142
|
commandDbPath: string,
|
|
138
143
|
extraEnv: Record<string, string> = {},
|
|
144
|
+
stdinPayload?: string,
|
|
139
145
|
): Promise<z.infer<T>> {
|
|
140
146
|
const scriptPath = resolve(agentDir(), scriptName);
|
|
141
147
|
|
|
142
148
|
const child = spawn(scriptPath, args, {
|
|
143
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
+
stdio: [stdinPayload === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
144
150
|
env: {
|
|
145
151
|
...process.env,
|
|
146
152
|
UAI_DB_PATH: commandDbPath,
|
|
@@ -151,12 +157,19 @@ async function runAgent<T extends z.ZodTypeAny>(
|
|
|
151
157
|
|
|
152
158
|
let stdoutBuf = "";
|
|
153
159
|
let stderrBuf = "";
|
|
154
|
-
child.stdout
|
|
160
|
+
child.stdout?.on("data", (chunk) => {
|
|
155
161
|
stdoutBuf += chunk.toString("utf8");
|
|
156
162
|
});
|
|
157
|
-
child.stderr
|
|
163
|
+
child.stderr?.on("data", (chunk) => {
|
|
158
164
|
stderrBuf += chunk.toString("utf8");
|
|
159
165
|
});
|
|
166
|
+
if (stdinPayload !== undefined && child.stdin) {
|
|
167
|
+
// A script that exits before consuming stdin may close the pipe first.
|
|
168
|
+
// The process exit/error below is authoritative; do not turn EPIPE into an
|
|
169
|
+
// unhandled stream error in the host agent.
|
|
170
|
+
child.stdin.on("error", () => {});
|
|
171
|
+
child.stdin.end(stdinPayload);
|
|
172
|
+
}
|
|
160
173
|
|
|
161
174
|
const exitCode: number = await new Promise((res, rej) => {
|
|
162
175
|
child.on("error", rej);
|
|
@@ -267,27 +280,39 @@ export function toolStderrDetail(stderr: string): string {
|
|
|
267
280
|
// ---------------------------------------------------------------------------
|
|
268
281
|
|
|
269
282
|
export const agent = {
|
|
270
|
-
async taskUp(
|
|
283
|
+
async taskUp(
|
|
284
|
+
input: TaskLaunchInput,
|
|
285
|
+
credentials: TaskUpCredentials = {},
|
|
286
|
+
): Promise<TaskUpResult> {
|
|
271
287
|
const commandDbPath = createTaskUpCommandDb(input);
|
|
272
|
-
// Materialize the creator's per-user SSH key
|
|
273
|
-
//
|
|
288
|
+
// Materialize the creator's per-user SSH key for commit signing and the
|
|
289
|
+
// explicit no-GitHub-connection transport fallback (ADR-029).
|
|
274
290
|
const identityDir = writeTaskIdentity(input.task.id, input.task.ownerUserId);
|
|
275
|
-
// Per-(project, key) env
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
291
|
+
// Per-(project, key) env values must NEVER enter task-up's host process
|
|
292
|
+
// environment. Besides ordinary app names, projects can declare names such
|
|
293
|
+
// as BASH_ENV, PATH, GIT_CONFIG_*, or UAI_*; exposing those before the host
|
|
294
|
+
// clone would let container-owned configuration influence host commands or
|
|
295
|
+
// read another credential boundary. Send one JSON object over stdin
|
|
296
|
+
// instead. task-up feeds it directly to Compose as an in-memory override,
|
|
297
|
+
// so values reach only the task container and are never written to disk.
|
|
298
|
+
// On a key collision the LEAD project (position 0) wins: iterate in
|
|
299
|
+
// descending position order so position-0 assignments apply last.
|
|
300
|
+
const projectEnv: Record<string, string> = {};
|
|
284
301
|
const orderedProjects = [...input.projects].sort(
|
|
285
302
|
(a, b) => b.position - a.position,
|
|
286
303
|
);
|
|
287
304
|
for (const project of orderedProjects) {
|
|
288
|
-
Object.assign(
|
|
305
|
+
Object.assign(projectEnv, getDecryptedForProject(project.id));
|
|
289
306
|
}
|
|
307
|
+
const extraEnv: Record<string, string> = {};
|
|
290
308
|
if (identityDir) extraEnv.UAI_TASK_IDENTITY_DIR = identityDir;
|
|
309
|
+
// Only a non-secret socket path crosses into Bash. The token itself was
|
|
310
|
+
// seeded over stdin and never enters argv, env, project data, Compose, or
|
|
311
|
+
// the task container.
|
|
312
|
+
if (credentials.githubCredentialSocket) {
|
|
313
|
+
extraEnv.UAI_GITHUB_CREDENTIAL_SOCKET =
|
|
314
|
+
credentials.githubCredentialSocket;
|
|
315
|
+
}
|
|
291
316
|
try {
|
|
292
317
|
return await runAgent(
|
|
293
318
|
"task-up.sh",
|
|
@@ -295,6 +320,7 @@ export const agent = {
|
|
|
295
320
|
TaskUpData,
|
|
296
321
|
commandDbPath,
|
|
297
322
|
extraEnv,
|
|
323
|
+
JSON.stringify(projectEnv),
|
|
298
324
|
);
|
|
299
325
|
} finally {
|
|
300
326
|
removeCommandDb(commandDbPath);
|