@runuai/host 0.9.150 → 0.9.152
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/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/meta/_journal.json +14 -0
- package/db/schema.ts +188 -0
- package/lib/agent-cli.ts +11 -13
- package/lib/agent.ts +177 -7
- package/lib/agents/durable-proc.ts +56 -44
- package/lib/agents/registry.ts +19 -3
- package/lib/agents/session-state.ts +23 -10
- package/lib/agents/transport.ts +101 -69
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +4 -3
- package/lib/attachments.ts +19 -17
- package/lib/capture-flow.ts +319 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2544 -0
- package/lib/cloud-credential-runtime.ts +1035 -0
- package/lib/codex-auth.ts +21 -1
- package/lib/container-runtime.ts +76 -0
- package/lib/credential-cleanup-inventory.ts +405 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +773 -0
- package/lib/credential-consumers/github.ts +594 -0
- package/lib/credential-consumers/mcp.ts +384 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-store-pickup.ts +15 -2
- package/lib/credential-store.ts +361 -33
- package/lib/credentials-activation-authority.ts +546 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/db.ts +11 -0
- package/lib/docker-exec.ts +3 -1
- package/lib/engine-accounts.ts +42 -32
- package/lib/engine-connect.ts +47 -2
- package/lib/engine-credential-env.ts +50 -0
- package/lib/engine-login.ts +83 -1
- package/lib/env-file.ts +62 -8
- package/lib/env.ts +5 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +219 -177
- package/lib/git-identity.ts +59 -14
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +56 -21
- package/lib/host-env.ts +17 -4
- package/lib/launch-mode.ts +25 -0
- package/lib/legacy-runtime-retirement.ts +388 -0
- package/lib/machine-exec.ts +14 -3
- package/lib/machine-provider-aws.ts +50 -8
- package/lib/machine-provider-local.ts +4 -0
- package/lib/machine-provider.ts +4 -0
- package/lib/managed-operation-drain.ts +8 -0
- package/lib/managed-runtime.ts +371 -7
- package/lib/mcp-capture.ts +860 -0
- package/lib/mcp-connections.ts +8 -1
- package/lib/mcp-gateway.ts +168 -10
- package/lib/obs.ts +2 -6
- package/lib/orchestrator.ts +1837 -224
- package/lib/person-capture.ts +327 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +50 -3
- package/lib/runtime-archive.ts +68 -1
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +429 -13
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/task-trust.ts +34 -3
- package/lib/sidecar-resume.ts +18 -5
- package/lib/skills.ts +47 -8
- package/lib/standard-image.ts +157 -103
- package/lib/stopped-task-status-queue.ts +68 -10
- package/lib/task-credential-cleanup.ts +525 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +66 -163
- package/lib/task-env-source.ts +68 -14
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +69 -28
- package/lib/task-environment/docker.ts +101 -20
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +459 -96
- package/lib/task-environment/machine.ts +101 -11
- package/lib/task-environment/self.ts +108 -50
- package/lib/task-environment/types.ts +89 -5
- package/lib/task-environment/workspace-files.ts +650 -25
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-gateway-recovery.ts +4 -1
- package/lib/task-gateway-state.ts +15 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +56 -2
- package/lib/token-broker-local.ts +28 -4
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +368 -28
- package/lib/token-lanes/openai-oauth.ts +7 -3
- package/lib/token-lanes/xai-oauth.ts +7 -3
- package/lib/transcript.ts +9 -5
- package/package.json +6 -2
- package/scripts/agent/task-up.sh +242 -100
- package/src/cloud-credentials.ts +2399 -0
- package/src/event-outbox.ts +8 -6
- package/src/index.ts +1386 -875
- package/src/main.ts +1061 -139
- package/src/outbound-frame.ts +31 -0
- package/src/project-env-policy.ts +125 -10
- package/src/protocol.ts +699 -23
- package/src/ui/server.ts +48 -10
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154 §6, §12: what task-up's host-side clone and fetches authenticate
|
|
3
|
+
* with, served by this process from memory for exactly as long as the
|
|
4
|
+
* operation holds the task owner's credentials (`holdTaskGitCredentials`).
|
|
5
|
+
*
|
|
6
|
+
* - GitHub over HTTPS: git's credential helper is a small script that asks
|
|
7
|
+
* this process for github.com's token over a private socket, each time git
|
|
8
|
+
* needs it. Nothing caches it: no `git credential-cache` daemon outlives
|
|
9
|
+
* the clone.
|
|
10
|
+
* - SSH: an ssh-agent this process speaks itself, holding only the task
|
|
11
|
+
* owner's ed25519 key (what the sign-in box mints), in memory. Only the
|
|
12
|
+
* public key is written, so ssh can name the identity it wants.
|
|
13
|
+
*
|
|
14
|
+
* Everything lives in a 0700 directory under /tmp, whose path has no
|
|
15
|
+
* whitespace (git runs the helper through a shell). The service closes when
|
|
16
|
+
* the hold's signal aborts or the caller closes it: the sockets stop
|
|
17
|
+
* answering and the directory is removed. A process that dies takes its
|
|
18
|
+
* sockets with it, and what it leaves on disk holds no secret.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createPrivateKey, createPublicKey, sign, type KeyObject } from "node:crypto";
|
|
22
|
+
import { chmodSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { createServer, type Server, type Socket } from "node:net";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import type { TaskGitCredentialHold } from "./credential-consumers/github";
|
|
27
|
+
|
|
28
|
+
/** What task-up is told, all non-secret paths. */
|
|
29
|
+
export interface CloneCredentialEnv {
|
|
30
|
+
/** The executable git runs as its credential helper (github.com only). */
|
|
31
|
+
UAI_GITHUB_CREDENTIAL_HELPER?: string;
|
|
32
|
+
/** The agent socket holding the owner's SSH key. */
|
|
33
|
+
UAI_TASK_SSH_AUTH_SOCK?: string;
|
|
34
|
+
/** The owner's public key: the identity ssh asks the agent for. */
|
|
35
|
+
UAI_TASK_SSH_PUBLIC_KEY?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CloneCredentialService {
|
|
39
|
+
env: CloneCredentialEnv;
|
|
40
|
+
/** Stop answering and remove the directory. Idempotent. */
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An SSH key the agent cannot hold: not an unencrypted ed25519 key, or one
|
|
45
|
+
* whose public half does not match. */
|
|
46
|
+
export class UnsupportedSshKey extends Error {
|
|
47
|
+
constructor(reason: string) {
|
|
48
|
+
super(`the SSH key cannot be used for the clone: ${reason}`);
|
|
49
|
+
this.name = "UnsupportedSshKey";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Serve `hold`'s credentials to task-up until the hold's signal aborts or
|
|
55
|
+
* `close()` is called. Rejects (serving nothing) when the hold is already
|
|
56
|
+
* aborted or its SSH key cannot be held.
|
|
57
|
+
*/
|
|
58
|
+
export async function serveCloneCredentials(
|
|
59
|
+
hold: Pick<TaskGitCredentialHold, "github" | "ssh" | "signal">,
|
|
60
|
+
): Promise<CloneCredentialService> {
|
|
61
|
+
if (hold.signal.aborted) throw new Error("the task's credentials were taken before the clone");
|
|
62
|
+
const key = hold.ssh ? ed25519Identity(hold.ssh.privateKey, hold.ssh.publicKey) : null;
|
|
63
|
+
const directory = mkdtempSync(join("/tmp", "uai-clone-credentials-"));
|
|
64
|
+
chmodSync(directory, 0o700);
|
|
65
|
+
const servers: Server[] = [];
|
|
66
|
+
const sockets = new Set<Socket>();
|
|
67
|
+
/** Listens not yet settled: closing fails them, so a start can never wait
|
|
68
|
+
* on a server closed under it. */
|
|
69
|
+
const starting = new Set<(error: Error) => void>();
|
|
70
|
+
let closing: Promise<void> | null = null;
|
|
71
|
+
const close = (): Promise<void> => {
|
|
72
|
+
closing ??= (async () => {
|
|
73
|
+
hold.signal.removeEventListener("abort", onAbort);
|
|
74
|
+
for (const fail of [...starting]) fail(new Error("the task's credentials were taken before the clone"));
|
|
75
|
+
for (const socket of sockets) socket.destroy();
|
|
76
|
+
await Promise.all(servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
|
|
77
|
+
rmSync(directory, { recursive: true, force: true });
|
|
78
|
+
})();
|
|
79
|
+
return closing;
|
|
80
|
+
};
|
|
81
|
+
const onAbort = (): void => void close();
|
|
82
|
+
hold.signal.addEventListener("abort", onAbort);
|
|
83
|
+
|
|
84
|
+
const listen = async (path: string, handle: (socket: Socket) => void): Promise<void> => {
|
|
85
|
+
const server = createServer((socket) => {
|
|
86
|
+
if (closing) {
|
|
87
|
+
socket.destroy();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
sockets.add(socket);
|
|
91
|
+
socket.on("close", () => sockets.delete(socket));
|
|
92
|
+
socket.on("error", () => socket.destroy());
|
|
93
|
+
handle(socket);
|
|
94
|
+
});
|
|
95
|
+
servers.push(server);
|
|
96
|
+
await new Promise<void>((resolve, reject) => {
|
|
97
|
+
const fail = (error: Error): void => {
|
|
98
|
+
starting.delete(fail);
|
|
99
|
+
reject(error);
|
|
100
|
+
};
|
|
101
|
+
starting.add(fail);
|
|
102
|
+
server.once("error", fail);
|
|
103
|
+
server.listen(path, () => {
|
|
104
|
+
starting.delete(fail);
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
if (closing) throw new Error("the task's credentials were taken before the clone");
|
|
109
|
+
chmodSync(path, 0o600);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const env: CloneCredentialEnv = {};
|
|
113
|
+
try {
|
|
114
|
+
if (hold.github) {
|
|
115
|
+
const token = hold.github.token;
|
|
116
|
+
const socketPath = join(directory, "github.sock");
|
|
117
|
+
await listen(socketPath, (socket) => answerGithub(socket, token));
|
|
118
|
+
const node = join(directory, "node");
|
|
119
|
+
symlinkSync(process.execPath, node);
|
|
120
|
+
const helper = join(directory, "git-credential-uai");
|
|
121
|
+
writeFileSync(helper, helperScript(node, socketPath), { mode: 0o700 });
|
|
122
|
+
env.UAI_GITHUB_CREDENTIAL_HELPER = helper;
|
|
123
|
+
}
|
|
124
|
+
if (key) {
|
|
125
|
+
const socketPath = join(directory, "ssh-agent.sock");
|
|
126
|
+
await listen(socketPath, (socket) => answerAgent(socket, key));
|
|
127
|
+
const publicKey = join(directory, "id_ed25519.pub");
|
|
128
|
+
writeFileSync(publicKey, `${key.publicLine}\n`, { mode: 0o600 });
|
|
129
|
+
env.UAI_TASK_SSH_AUTH_SOCK = socketPath;
|
|
130
|
+
env.UAI_TASK_SSH_PUBLIC_KEY = publicKey;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
await close();
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
if (hold.signal.aborted) {
|
|
137
|
+
await close();
|
|
138
|
+
throw new Error("the task's credentials were taken before the clone");
|
|
139
|
+
}
|
|
140
|
+
return { env, close };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// GitHub: the credential helper and what it asks
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** The helper git runs: only `get`, only https://github.com; it asks this
|
|
148
|
+
* process and prints what it answers (nothing once the service closed). */
|
|
149
|
+
function helperScript(node: string, socketPath: string): string {
|
|
150
|
+
return `#!${node}
|
|
151
|
+
"use strict";
|
|
152
|
+
if (process.argv[2] !== "get") process.exit(0);
|
|
153
|
+
let input = "";
|
|
154
|
+
process.stdin.setEncoding("utf8");
|
|
155
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
156
|
+
process.stdin.on("end", () => {
|
|
157
|
+
const fields = {};
|
|
158
|
+
for (const line of input.split("\\n")) {
|
|
159
|
+
const at = line.indexOf("=");
|
|
160
|
+
if (at > 0) fields[line.slice(0, at)] = line.slice(at + 1);
|
|
161
|
+
}
|
|
162
|
+
if (fields.protocol !== "https" || fields.host !== "github.com") process.exit(0);
|
|
163
|
+
const socket = require("node:net").connect(${JSON.stringify(socketPath)});
|
|
164
|
+
let answer = "";
|
|
165
|
+
socket.setEncoding("utf8");
|
|
166
|
+
socket.on("data", (chunk) => { answer += chunk; });
|
|
167
|
+
socket.on("end", () => { process.stdout.write(answer); });
|
|
168
|
+
socket.on("error", () => process.exit(0));
|
|
169
|
+
socket.end("protocol=https\\nhost=github.com\\n");
|
|
170
|
+
});
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function answerGithub(socket: Socket, token: string): void {
|
|
175
|
+
let request = "";
|
|
176
|
+
socket.setEncoding("utf8");
|
|
177
|
+
socket.on("data", (chunk: string) => {
|
|
178
|
+
request += chunk;
|
|
179
|
+
if (request.length > 1024) socket.destroy();
|
|
180
|
+
});
|
|
181
|
+
socket.on("end", () => {
|
|
182
|
+
const fields = new Set(request.split("\n"));
|
|
183
|
+
if (fields.has("protocol=https") && fields.has("host=github.com")) {
|
|
184
|
+
socket.end(`username=x-access-token\npassword=${token}\n`);
|
|
185
|
+
} else {
|
|
186
|
+
socket.end();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// SSH: an agent holding one ed25519 key (draft-miller-ssh-agent)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
const SSH_AGENT_FAILURE = 5;
|
|
196
|
+
const SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
197
|
+
const SSH_AGENT_IDENTITIES_ANSWER = 12;
|
|
198
|
+
const SSH_AGENTC_SIGN_REQUEST = 13;
|
|
199
|
+
const SSH_AGENT_SIGN_RESPONSE = 14;
|
|
200
|
+
/** A message larger than any request this agent answers. */
|
|
201
|
+
const MAX_AGENT_MESSAGE = 256 * 1024;
|
|
202
|
+
/** PKCS#8 for an ed25519 private key, up to its 32-byte seed (RFC 8410). */
|
|
203
|
+
const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
204
|
+
|
|
205
|
+
interface Ed25519Identity {
|
|
206
|
+
blob: Buffer;
|
|
207
|
+
comment: string;
|
|
208
|
+
publicLine: string;
|
|
209
|
+
privateKey: KeyObject;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sshString(bytes: Buffer): Buffer {
|
|
213
|
+
const length = Buffer.alloc(4);
|
|
214
|
+
length.writeUInt32BE(bytes.length);
|
|
215
|
+
return Buffer.concat([length, bytes]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Reads SSH wire fields in order; throws on anything short or malformed. */
|
|
219
|
+
class Reader {
|
|
220
|
+
#offset = 0;
|
|
221
|
+
constructor(private readonly bytes: Buffer) {}
|
|
222
|
+
uint32(): number {
|
|
223
|
+
if (this.#offset + 4 > this.bytes.length) throw new UnsupportedSshKey("a truncated field");
|
|
224
|
+
const value = this.bytes.readUInt32BE(this.#offset);
|
|
225
|
+
this.#offset += 4;
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
string(): Buffer {
|
|
229
|
+
const length = this.uint32();
|
|
230
|
+
if (this.#offset + length > this.bytes.length) throw new UnsupportedSshKey("a truncated field");
|
|
231
|
+
const value = this.bytes.subarray(this.#offset, this.#offset + length);
|
|
232
|
+
this.#offset += length;
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The one ed25519 identity in an unencrypted openssh-key-v1 private key,
|
|
238
|
+
* checked against its public line. */
|
|
239
|
+
export function ed25519Identity(privateKeyPem: string, publicLine: string): Ed25519Identity {
|
|
240
|
+
const pem = /-----BEGIN OPENSSH PRIVATE KEY-----([\s\S]+?)-----END OPENSSH PRIVATE KEY-----/.exec(privateKeyPem);
|
|
241
|
+
if (!pem) throw new UnsupportedSshKey("not an OpenSSH private key");
|
|
242
|
+
const body = Buffer.from(pem[1]!.replace(/\s+/g, ""), "base64");
|
|
243
|
+
const magic = Buffer.from("openssh-key-v1\0");
|
|
244
|
+
if (!body.subarray(0, magic.length).equals(magic)) throw new UnsupportedSshKey("not an OpenSSH private key");
|
|
245
|
+
const outer = new Reader(body.subarray(magic.length));
|
|
246
|
+
const cipher = outer.string().toString();
|
|
247
|
+
const kdf = outer.string().toString();
|
|
248
|
+
outer.string();
|
|
249
|
+
if (cipher !== "none" || kdf !== "none") throw new UnsupportedSshKey("an encrypted key");
|
|
250
|
+
if (outer.uint32() !== 1) throw new UnsupportedSshKey("more than one key");
|
|
251
|
+
const blob = Buffer.from(outer.string());
|
|
252
|
+
const secret = new Reader(outer.string());
|
|
253
|
+
if (secret.uint32() !== secret.uint32()) throw new UnsupportedSshKey("a corrupt key");
|
|
254
|
+
if (secret.string().toString() !== "ssh-ed25519") throw new UnsupportedSshKey("not an ed25519 key");
|
|
255
|
+
const pub = secret.string();
|
|
256
|
+
const pair = secret.string();
|
|
257
|
+
const comment = secret.string().toString();
|
|
258
|
+
if (pub.length !== 32 || pair.length !== 64 || !pair.subarray(32).equals(pub)) {
|
|
259
|
+
throw new UnsupportedSshKey("a corrupt ed25519 key");
|
|
260
|
+
}
|
|
261
|
+
if (!blob.equals(Buffer.concat([sshString(Buffer.from("ssh-ed25519")), sshString(pub)]))) {
|
|
262
|
+
throw new UnsupportedSshKey("a corrupt ed25519 key");
|
|
263
|
+
}
|
|
264
|
+
const [type, encoded] = publicLine.trim().split(/\s+/);
|
|
265
|
+
if (type !== "ssh-ed25519" || !encoded || !Buffer.from(encoded, "base64").equals(blob)) {
|
|
266
|
+
throw new UnsupportedSshKey("its public key does not match");
|
|
267
|
+
}
|
|
268
|
+
const privateKey = createPrivateKey({
|
|
269
|
+
key: Buffer.concat([PKCS8_ED25519_PREFIX, pair.subarray(0, 32)]),
|
|
270
|
+
format: "der",
|
|
271
|
+
type: "pkcs8",
|
|
272
|
+
});
|
|
273
|
+
// The seed's own public key: the one the agent advertises must be the one
|
|
274
|
+
// it signs with.
|
|
275
|
+
const derived = Buffer.from(createPublicKey(privateKey).export({ format: "jwk" }).x ?? "", "base64url");
|
|
276
|
+
if (!derived.equals(pub)) throw new UnsupportedSshKey("a corrupt ed25519 key");
|
|
277
|
+
return { blob, comment, publicLine: `ssh-ed25519 ${encoded}`, privateKey };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function agentMessage(type: number, payload: Buffer = Buffer.alloc(0)): Buffer {
|
|
281
|
+
const header = Buffer.alloc(5);
|
|
282
|
+
header.writeUInt32BE(payload.length + 1);
|
|
283
|
+
header.writeUInt8(type, 4);
|
|
284
|
+
return Buffer.concat([header, payload]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The agent's answer to one request: its identity, a signature by it, or
|
|
288
|
+
* failure for everything else (adding, removing, locking, extensions). */
|
|
289
|
+
export function agentAnswer(request: Buffer, key: Ed25519Identity): Buffer {
|
|
290
|
+
const type = request.readUInt8(0);
|
|
291
|
+
if (type === SSH_AGENTC_REQUEST_IDENTITIES) {
|
|
292
|
+
const count = Buffer.alloc(4);
|
|
293
|
+
count.writeUInt32BE(1);
|
|
294
|
+
return agentMessage(
|
|
295
|
+
SSH_AGENT_IDENTITIES_ANSWER,
|
|
296
|
+
Buffer.concat([count, sshString(key.blob), sshString(Buffer.from(key.comment))]),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (type === SSH_AGENTC_SIGN_REQUEST) {
|
|
300
|
+
try {
|
|
301
|
+
const reader = new Reader(request.subarray(1));
|
|
302
|
+
const blob = reader.string();
|
|
303
|
+
const data = reader.string();
|
|
304
|
+
reader.uint32();
|
|
305
|
+
if (!blob.equals(key.blob)) return agentMessage(SSH_AGENT_FAILURE);
|
|
306
|
+
const signature = sign(null, data, key.privateKey);
|
|
307
|
+
return agentMessage(
|
|
308
|
+
SSH_AGENT_SIGN_RESPONSE,
|
|
309
|
+
sshString(Buffer.concat([sshString(Buffer.from("ssh-ed25519")), sshString(signature)])),
|
|
310
|
+
);
|
|
311
|
+
} catch {
|
|
312
|
+
return agentMessage(SSH_AGENT_FAILURE);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return agentMessage(SSH_AGENT_FAILURE);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function answerAgent(socket: Socket, key: Ed25519Identity): void {
|
|
319
|
+
let pending = Buffer.alloc(0);
|
|
320
|
+
socket.on("data", (chunk: Buffer) => {
|
|
321
|
+
pending = Buffer.concat([pending, chunk]);
|
|
322
|
+
while (pending.length >= 4) {
|
|
323
|
+
const length = pending.readUInt32BE(0);
|
|
324
|
+
if (length === 0 || length > MAX_AGENT_MESSAGE) {
|
|
325
|
+
socket.destroy();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (pending.length < 4 + length) return;
|
|
329
|
+
const request = pending.subarray(4, 4 + length);
|
|
330
|
+
pending = pending.subarray(4 + length);
|
|
331
|
+
socket.write(agentAnswer(request, key));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|