@runuai/host 0.9.167 → 0.9.169
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/lib/agent-cli.ts +811 -21
- package/lib/agents/cursor.ts +16 -10
- package/lib/agents/durable-proc.ts +12 -5
- package/lib/agents/machine-durable.ts +12 -5
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/transport.ts +5 -0
- package/lib/assignment-evidence.ts +93 -0
- package/lib/cloud-credential-leases.ts +23 -6
- package/lib/cloud-credential-runtime.ts +10 -1
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/engine-accounts.ts +4 -6
- package/lib/host-env.ts +2 -27
- package/lib/orchestrator.ts +97 -16
- package/lib/project-env-loader.ts +5 -0
- package/lib/secrets-proxy/policy.ts +12 -3
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image.ts +31 -9
- package/lib/task-env-source.ts +2 -0
- package/lib/task-environment/types.ts +20 -15
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-token.ts +3 -0
- package/package.json +14 -2
- package/src/cloud-credentials.ts +41 -4
- package/src/engine-account-paths.ts +11 -0
- package/src/index.ts +66 -3
- package/src/main.ts +142 -0
- package/src/protocol.ts +204 -2
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/workspace-path-policy.ts +41 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Immutable write-only task secret writer. Never return a value or provider error. */
|
|
2
|
+
import { createHash, createHmac } from "node:crypto";
|
|
3
|
+
import { isTaskSecretStagingPut, type TaskSecretStagingPut } from "../src/task-secrets";
|
|
4
|
+
import { configuredCredentialStore, type CredentialStore } from "./credential-store";
|
|
5
|
+
import { masterKey } from "./secrets";
|
|
6
|
+
export function taskSecretParameter(taskId: string, name: string, stagingId: string): string {
|
|
7
|
+
return `/uai/lease/tasks/${taskId}/secrets/${createHash("sha256").update(name).digest("hex")}/${stagingId}`;
|
|
8
|
+
}
|
|
9
|
+
export async function writeTaskSecretStaging(frame: TaskSecretStagingPut, options: { store?: CredentialStore | null; hmacKey?: Buffer } = {}): Promise<void> {
|
|
10
|
+
if (!isTaskSecretStagingPut(frame)) throw new Error("invalid task secret staging request");
|
|
11
|
+
const store = Object.hasOwn(options, "store") ? options.store : configuredCredentialStore();
|
|
12
|
+
if (!store?.putImmutable) throw new Error("task secret writer unavailable");
|
|
13
|
+
const envelope = JSON.stringify({ v: 1, kind: "task-secret", orgId: frame.orgId, taskId: frame.taskId, name: frame.name, stagingId: frame.stagingId, value: frame.value });
|
|
14
|
+
const mac = createHmac("sha256", options.hmacKey ?? masterKey()).update("uai.task-secret.staging.v1\0").update(envelope).digest("hex");
|
|
15
|
+
await store.putImmutable(taskSecretParameter(frame.taskId, frame.name, frame.stagingId), envelope, { org: frame.orgId, task: frame.taskId, uai_staging_mac: mac });
|
|
16
|
+
}
|
package/lib/task-token.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface TaskTokenPayload {
|
|
|
13
13
|
agentId?: string;
|
|
14
14
|
/** ADR-157 §3: the agent's roster generation; the cloud requires it to match. */
|
|
15
15
|
generation?: string;
|
|
16
|
+
/** Server-issued lead context, frozen into a lead process token by the host. */
|
|
17
|
+
leadRunId?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const PREFIX = "uai_";
|
|
@@ -34,6 +36,7 @@ export function signTaskToken(payload: TaskTokenPayload, secret: string): string
|
|
|
34
36
|
p: payload.permissions,
|
|
35
37
|
...(payload.agentId ? { a: payload.agentId } : {}),
|
|
36
38
|
...(payload.generation ? { g: payload.generation } : {}),
|
|
39
|
+
...(payload.leadRunId ? { l: payload.leadRunId } : {}),
|
|
37
40
|
iat: Math.floor(Date.now() / 1000),
|
|
38
41
|
}),
|
|
39
42
|
),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runuai/host",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Uai host
|
|
3
|
+
"version": "0.9.169",
|
|
4
|
+
"description": "Uai host \u2014 runs ephemeral AI tasks in containers on a machine you control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uai Tech <team@runuai.com>",
|
|
7
7
|
"homepage": "https://github.com/runuai/uai#readme",
|
|
@@ -80,6 +80,18 @@
|
|
|
80
80
|
"./engine-usage": {
|
|
81
81
|
"types": "./src/engine-usage.ts",
|
|
82
82
|
"default": "./src/engine-usage.ts"
|
|
83
|
+
},
|
|
84
|
+
"./workspace-path-policy": {
|
|
85
|
+
"types": "./src/workspace-path-policy.ts",
|
|
86
|
+
"default": "./src/workspace-path-policy.ts"
|
|
87
|
+
},
|
|
88
|
+
"./task-secrets": {
|
|
89
|
+
"types": "./src/task-secrets.ts",
|
|
90
|
+
"default": "./src/task-secrets.ts"
|
|
91
|
+
},
|
|
92
|
+
"./task-env-names": {
|
|
93
|
+
"types": "./src/task-env-names.ts",
|
|
94
|
+
"default": "./src/task-env-names.ts"
|
|
83
95
|
}
|
|
84
96
|
},
|
|
85
97
|
"scripts": {
|
package/src/cloud-credentials.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MAX_TASK_SECRETS, TASK_SECRET_NAME, TASK_SECRET_STAGING_ID, type TaskSecretManifestEntry } from "./task-secrets";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* ADR-154 — the shared contract for cloud-only credentials.
|
|
3
5
|
*
|
|
@@ -31,6 +33,15 @@ export const HOST_CLOUD_CREDENTIALS_FEATURE = "host-cloud-credentials-v1";
|
|
|
31
33
|
*/
|
|
32
34
|
export const HOST_TASK_FILES_LEASE_FEATURE = "host-task-files-lease-v1";
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Advertised by a runner whose in-task `uai files` reads cloud links instead
|
|
38
|
+
* of a bind-mounted shared folder (ADR-161 §1.11). A task machine that still
|
|
39
|
+
* installs a Files session advertises the lease feature as well; the cloud
|
|
40
|
+
* treats "both" as a legacy session plus cloud links, and it does not mount
|
|
41
|
+
* the legacy folder into that container.
|
|
42
|
+
*/
|
|
43
|
+
export const HOST_AGENT_FILES_FEATURE = "host-agent-files-v1";
|
|
44
|
+
|
|
34
45
|
/**
|
|
35
46
|
* Set to `1` in a Uai Cloud task machine's root-only launch state
|
|
36
47
|
* (`/etc/uai/sidecar.env`) when the cloud launched it on cloud credentials
|
|
@@ -752,6 +763,8 @@ export interface LeaseManifestStatic {
|
|
|
752
763
|
|
|
753
764
|
/** What a task may read, by name and version — never a value. */
|
|
754
765
|
export interface CredentialManifest {
|
|
766
|
+
/** Omitted on older peers and when empty, preserving old digests. */
|
|
767
|
+
taskSecrets?: TaskSecretManifestEntry[];
|
|
755
768
|
/** The task's projects, lead first. */
|
|
756
769
|
projects: LeaseManifestProject[];
|
|
757
770
|
/** Per engine kind: the owner's personal accounts, then the workspace's. */
|
|
@@ -946,12 +959,13 @@ type WireObject = Record<string, unknown>;
|
|
|
946
959
|
function exactObject(
|
|
947
960
|
value: unknown,
|
|
948
961
|
required: readonly string[],
|
|
962
|
+
optional: readonly string[] = [],
|
|
949
963
|
): WireObject | null {
|
|
950
964
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
951
965
|
const object = value as WireObject;
|
|
952
966
|
const keys = Object.keys(object);
|
|
953
967
|
if (
|
|
954
|
-
keys.
|
|
968
|
+
keys.some(key => !required.includes(key) && !optional.includes(key)) ||
|
|
955
969
|
required.some((key) => !Object.hasOwn(object, key))
|
|
956
970
|
) {
|
|
957
971
|
return null;
|
|
@@ -1226,10 +1240,10 @@ function parseStatic(
|
|
|
1226
1240
|
|
|
1227
1241
|
function parseManifest(
|
|
1228
1242
|
value: unknown,
|
|
1229
|
-
grant: { orgId: string; userId: string },
|
|
1243
|
+
grant: { orgId: string; userId: string; taskId: string },
|
|
1230
1244
|
credentialSessions: ReadonlySet<string>,
|
|
1231
1245
|
): CredentialManifest | null {
|
|
1232
|
-
const o = exactObject(value, ["projects", "engines", "mcp", "github", "ssh"]);
|
|
1246
|
+
const o = exactObject(value, ["projects", "engines", "mcp", "github", "ssh"], ["taskSecrets"]);
|
|
1233
1247
|
if (
|
|
1234
1248
|
!o ||
|
|
1235
1249
|
!Array.isArray(o.projects) ||
|
|
@@ -1281,6 +1295,29 @@ function parseManifest(
|
|
|
1281
1295
|
) {
|
|
1282
1296
|
return null;
|
|
1283
1297
|
}
|
|
1298
|
+
if (o.taskSecrets !== undefined) {
|
|
1299
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(grant.taskId) || !Array.isArray(o.taskSecrets) || !o.taskSecrets.length || o.taskSecrets.length > MAX_TASK_SECRETS) return null;
|
|
1300
|
+
const entries: TaskSecretManifestEntry[] = [];
|
|
1301
|
+
for (const item of o.taskSecrets) {
|
|
1302
|
+
const e = exactObject(item, ["credentialId", "name", "stagingId", "generation", "parameter", "version", "mode", "domains", "placeholder", "sessionId"]);
|
|
1303
|
+
if (!e || typeof e.name !== "string" || !TASK_SECRET_NAME.test(e.name) ||
|
|
1304
|
+
typeof e.credentialId !== "string" || e.credentialId !== `task-secret:${e.name}` ||
|
|
1305
|
+
typeof e.stagingId !== "string" || !TASK_SECRET_STAGING_ID.test(e.stagingId) || !isPositiveInteger(e.generation) || e.version !== 1 ||
|
|
1306
|
+
typeof e.parameter !== "string" || (!e.parameter.startsWith(`/uai/lease/tasks/${grant.taskId}/secrets/`) || !/^[0-9a-f]{64}\/[0-9a-f-]{36}$/.test(e.parameter.slice(`/uai/lease/tasks/${grant.taskId}/secrets/`.length)) || !e.parameter.endsWith(`/${e.stagingId}`)) ||
|
|
1307
|
+
(e.mode !== "proxied" && e.mode !== "delivered") || !Array.isArray(e.domains) || e.domains.length > 16 ||
|
|
1308
|
+
!e.domains.every(d => typeof d === "string" && /^[a-z0-9.-]+\.[a-z]{2,}$/.test(d)) ||
|
|
1309
|
+
(e.mode === "proxied" && !e.domains.length) || (e.mode === "delivered" && e.domains.length) ||
|
|
1310
|
+
typeof e.sessionId !== "string" || !credentialSessions.has(e.sessionId)) return null;
|
|
1311
|
+
if (e.placeholder !== null) {
|
|
1312
|
+
const shape = exactObject(e.placeholder, ["prefix", "totalLength"]);
|
|
1313
|
+
if (!shape || typeof shape.prefix !== "string" || !/^(?:[A-Za-z0-9._~-]*[_-])?$/.test(shape.prefix) ||
|
|
1314
|
+
!isPositiveInteger(shape.totalLength) || shape.totalLength < shape.prefix.length + 32 || shape.totalLength > 256) return null;
|
|
1315
|
+
}
|
|
1316
|
+
entries.push(e as unknown as TaskSecretManifestEntry);
|
|
1317
|
+
}
|
|
1318
|
+
if (!uniqueBy(entries, e => e.name)) return null;
|
|
1319
|
+
return { projects, engines, mcp, github, ssh, taskSecrets: entries };
|
|
1320
|
+
}
|
|
1284
1321
|
return { projects, engines, mcp, github, ssh };
|
|
1285
1322
|
}
|
|
1286
1323
|
|
|
@@ -1359,7 +1396,7 @@ export function parseTaskLeaseV1(value: unknown): TaskLeaseV1 | null {
|
|
|
1359
1396
|
}
|
|
1360
1397
|
const files = o.files === null ? null : parseFiles(o.files);
|
|
1361
1398
|
if (o.files !== null && !files) return null;
|
|
1362
|
-
const grant = { orgId: o.orgId, userId: o.userId };
|
|
1399
|
+
const grant = { orgId: o.orgId, userId: o.userId, taskId: o.taskId };
|
|
1363
1400
|
const manifest = parseManifest(o.manifest, grant, credentialSessions);
|
|
1364
1401
|
if (!manifest) return null;
|
|
1365
1402
|
return {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Secret-free paths shared by account provisioning and credential safeguards. */
|
|
2
|
+
export const ENGINE_ACCOUNT_PATHS = {
|
|
3
|
+
grok: { defaultContainerDir: "/home/node/.grok", containerDirPrefix: "/home/node/.grok-acct-" },
|
|
4
|
+
codex: { defaultContainerDir: "/home/node/.codex", containerDirPrefix: "/home/node/.codex-acct-" },
|
|
5
|
+
opencode: { defaultContainerDir: "/home/node/.local/share/opencode", containerDirPrefix: "/home/node/.local/share/opencode-acct-" },
|
|
6
|
+
} as const;
|
|
7
|
+
/** Relative to the home, so safeguards cover relocated homes and symlinks too. */
|
|
8
|
+
export const ENGINE_ACCOUNT_PATH_RULES = Object.values(ENGINE_ACCOUNT_PATHS).flatMap((path) => [
|
|
9
|
+
{ components: path.defaultContainerDir.split("/").slice(3), prefix: false },
|
|
10
|
+
{ components: path.containerDirPrefix.split("/").slice(3), prefix: true },
|
|
11
|
+
]);
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertShareablePath, normalizedWorkspacePath } from "./workspace-path-policy";
|
|
2
|
+
import { readBoundedFile } from "../lib/task-environment/workspace-files";
|
|
1
3
|
import {
|
|
2
4
|
agent,
|
|
3
5
|
AgentError,
|
|
@@ -51,7 +53,7 @@ import {
|
|
|
51
53
|
import { readAttachment, writeAttachment } from "../lib/attachments";
|
|
52
54
|
import { readFileSync } from "node:fs";
|
|
53
55
|
import { resolve } from "node:path";
|
|
54
|
-
import { env, taskProjectWorktree } from "../lib/env";
|
|
56
|
+
import { env, taskProjectWorktree, taskWorkspaceDir } from "../lib/env";
|
|
55
57
|
import { getDecryptedForProject } from "../lib/host-env";
|
|
56
58
|
import {
|
|
57
59
|
envSchemaNote,
|
|
@@ -63,6 +65,7 @@ import {
|
|
|
63
65
|
appendTranscriptViaEnvironment,
|
|
64
66
|
} from "../lib/transcript";
|
|
65
67
|
import { buildTaskDiff } from "../lib/task-diff";
|
|
68
|
+
import { taskProjectOps } from "../lib/task-project-ops-host";
|
|
66
69
|
import { stoppedTaskDiff } from "../lib/task-diff-sandbox";
|
|
67
70
|
import {
|
|
68
71
|
admitTaskRunEpoch,
|
|
@@ -2036,7 +2039,7 @@ export const hostCommands: HostCommands = {
|
|
|
2036
2039
|
});
|
|
2037
2040
|
},
|
|
2038
2041
|
|
|
2039
|
-
async channelDeliver(ctx, taskId, agentId, text, origin) {
|
|
2042
|
+
async channelDeliver(ctx, taskId, agentId, text, origin, assignment) {
|
|
2040
2043
|
logCommand(ctx, "channelDeliver", taskId, agentId);
|
|
2041
2044
|
if (credentialsMode() === "pending") {
|
|
2042
2045
|
return switchingToCloudCredentials();
|
|
@@ -2047,7 +2050,7 @@ export const hostCommands: HostCommands = {
|
|
|
2047
2050
|
// An inbound turn is activity: the idle veto must cover the gap between
|
|
2048
2051
|
// this delivery landing and the agent's first output event.
|
|
2049
2052
|
getOrchestrator().markChannelActivity(taskId);
|
|
2050
|
-
const result = await getOrchestrator().deliver(taskId, agentId, text, origin);
|
|
2053
|
+
const result = await getOrchestrator().deliver(taskId, agentId, text, origin, assignment);
|
|
2051
2054
|
if (!result.ok) {
|
|
2052
2055
|
return {
|
|
2053
2056
|
ok: false,
|
|
@@ -2245,6 +2248,53 @@ export const hostCommands: HostCommands = {
|
|
|
2245
2248
|
}
|
|
2246
2249
|
},
|
|
2247
2250
|
|
|
2251
|
+
// ADR-161 §2.3: add a repository to a running task. Each command runs in
|
|
2252
|
+
// the task's live-project queue; the publication steps take its lifecycle
|
|
2253
|
+
// slot (lib/task-project-ops.ts).
|
|
2254
|
+
async taskProjectPrepare(ctx, input) {
|
|
2255
|
+
logCommand(ctx, "taskProjectPrepare", input.taskId, input.operationId);
|
|
2256
|
+
const runtimeFailure = runtimeUnavailable();
|
|
2257
|
+
if (runtimeFailure) return runtimeFailure;
|
|
2258
|
+
try {
|
|
2259
|
+
return ok(await taskProjectOps().prepare(input));
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
return failFromUnknown(err);
|
|
2262
|
+
}
|
|
2263
|
+
},
|
|
2264
|
+
|
|
2265
|
+
async taskProjectActivate(ctx, input) {
|
|
2266
|
+
logCommand(ctx, "taskProjectActivate", input.taskId, input.operationId);
|
|
2267
|
+
const runtimeFailure = runtimeUnavailable();
|
|
2268
|
+
if (runtimeFailure) return runtimeFailure;
|
|
2269
|
+
try {
|
|
2270
|
+
return ok(await taskProjectOps().activate(input));
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
return failFromUnknown(err);
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
|
|
2276
|
+
async taskProjectCompensate(ctx, input) {
|
|
2277
|
+
logCommand(ctx, "taskProjectCompensate", input.taskId, input.operationId);
|
|
2278
|
+
const runtimeFailure = runtimeUnavailable();
|
|
2279
|
+
if (runtimeFailure) return runtimeFailure;
|
|
2280
|
+
try {
|
|
2281
|
+
return ok(await taskProjectOps().compensate(input));
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
return failFromUnknown(err);
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
|
|
2287
|
+
async taskProjectStatus(ctx, input) {
|
|
2288
|
+
logCommand(ctx, "taskProjectStatus", input.taskId, input.operationId);
|
|
2289
|
+
const runtimeFailure = runtimeUnavailable();
|
|
2290
|
+
if (runtimeFailure) return runtimeFailure;
|
|
2291
|
+
try {
|
|
2292
|
+
return ok(await taskProjectOps().status(input));
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
return failFromUnknown(err);
|
|
2295
|
+
}
|
|
2296
|
+
},
|
|
2297
|
+
|
|
2248
2298
|
async attachmentWrite(ctx, input) {
|
|
2249
2299
|
logCommand(ctx, "attachmentWrite", input.taskId, input.filename);
|
|
2250
2300
|
try {
|
|
@@ -2273,6 +2323,19 @@ export const hostCommands: HostCommands = {
|
|
|
2273
2323
|
}
|
|
2274
2324
|
},
|
|
2275
2325
|
|
|
2326
|
+
async workspaceFileRead(ctx, input) {
|
|
2327
|
+
try {
|
|
2328
|
+
const path = normalizedWorkspacePath(assertShareablePath(input.path));
|
|
2329
|
+
const maxBytes = Math.min(8 * 1024 * 1024, Math.max(0, input.maxBytes));
|
|
2330
|
+
const environment = await reconstructHostTaskEnvironment(input.taskId);
|
|
2331
|
+
const bytes = environment ? await environment.readWorkspaceFile(`${environment.descriptor.workspacePath}/${path.slice("/workspace/".length)}`, { maxBytes: maxBytes + 1 }) :
|
|
2332
|
+
await readBoundedFile(taskWorkspaceDir(input.taskId), path.slice("/workspace/".length).split("/"), { maxBytes: maxBytes + 1 });
|
|
2333
|
+
if (!bytes) return { ok: false, code: HostErrorCode.TaskNotFound, message: "no such workspace file" };
|
|
2334
|
+
if (bytes.byteLength > maxBytes) return { ok: false, code: HostErrorCode.PayloadTooLarge, message: "Ask the agent to uai share this file (live preview is limited to 8 MiB)." };
|
|
2335
|
+
return ok({ dataBase64: Buffer.from(bytes).toString("base64"), size: bytes.byteLength });
|
|
2336
|
+
} catch { return { ok: false, code: HostErrorCode.TaskNotFound, message: "no such workspace file" }; }
|
|
2337
|
+
},
|
|
2338
|
+
|
|
2276
2339
|
async attachmentRead(ctx, input) {
|
|
2277
2340
|
logCommand(ctx, "attachmentRead", input.taskId, input.filename);
|
|
2278
2341
|
try {
|
package/src/main.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
+
import type { AssignmentDelivery, DeliveryOrigin } from "./protocol";
|
|
2
3
|
|
|
3
4
|
import "./load-env";
|
|
5
|
+
import { sampleDiskPressure } from "../lib/disk-pressure";
|
|
6
|
+
import { TASK_SECRETS_FEATURE, isTaskSecretStagingPut } from "./task-secrets";
|
|
7
|
+
import { writeTaskSecretStaging } from "../lib/task-secret-staging";
|
|
4
8
|
|
|
5
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
10
|
import {
|
|
@@ -113,6 +117,7 @@ import {
|
|
|
113
117
|
parseLeaseGrantV1Frame,
|
|
114
118
|
parseLeaseStaleFrame,
|
|
115
119
|
parseLeaseUnavailableFrame,
|
|
120
|
+
HOST_AGENT_FILES_FEATURE,
|
|
116
121
|
HOST_TASK_FILES_LEASE_FEATURE,
|
|
117
122
|
} from "./cloud-credentials";
|
|
118
123
|
import { captureEngineAccount } from "../lib/capture-flow";
|
|
@@ -301,6 +306,7 @@ import {
|
|
|
301
306
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
302
307
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
303
308
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
309
|
+
LIVE_TASK_PROJECTS_PROTOCOL_FEATURE,
|
|
304
310
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
305
311
|
isEngineAccountScope,
|
|
306
312
|
isTaskMachineRetireAckFrame,
|
|
@@ -318,6 +324,8 @@ import {
|
|
|
318
324
|
MAX_REMOTE_HOST_MESSAGE_BYTES,
|
|
319
325
|
MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE,
|
|
320
326
|
SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
|
|
327
|
+
ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE,
|
|
328
|
+
LEAD_EVIDENCE_PROTOCOL_FEATURE,
|
|
321
329
|
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
322
330
|
type CloudToHost,
|
|
323
331
|
type McpOp,
|
|
@@ -364,6 +372,10 @@ import {
|
|
|
364
372
|
type TaskCommandProject,
|
|
365
373
|
type TaskCommandTask,
|
|
366
374
|
type TaskDiffInput,
|
|
375
|
+
type TaskProjectActivateInput,
|
|
376
|
+
type TaskProjectOpFence,
|
|
377
|
+
type TaskProjectPrepareInput,
|
|
378
|
+
type TaskProjectStatusInput,
|
|
367
379
|
type TaskDownInput,
|
|
368
380
|
type TaskLaunchInput,
|
|
369
381
|
type TaskStopInput,
|
|
@@ -1462,13 +1474,32 @@ async function startLocalUi(): Promise<void> {
|
|
|
1462
1474
|
}
|
|
1463
1475
|
}
|
|
1464
1476
|
|
|
1477
|
+
// Capacity fits the existing capability/status snapshot; no new messages.
|
|
1478
|
+
let diskSnapshot: HostCapabilities["disk"];
|
|
1479
|
+
let diskSampling = false;
|
|
1480
|
+
async function refreshDiskSnapshot(): Promise<void> {
|
|
1481
|
+
if (diskSampling || sidecarMode()) return;
|
|
1482
|
+
diskSampling = true;
|
|
1483
|
+
try {
|
|
1484
|
+
diskSnapshot = await sampleDiskPressure();
|
|
1485
|
+
sendCapabilities();
|
|
1486
|
+
} finally { diskSampling = false; }
|
|
1487
|
+
}
|
|
1488
|
+
void refreshDiskSnapshot();
|
|
1489
|
+
const diskTimer = setInterval(() => void refreshDiskSnapshot(), 60_000);
|
|
1490
|
+
diskTimer.unref?.();
|
|
1491
|
+
|
|
1465
1492
|
/** Build the current host capability advertisement (ADR-021). */
|
|
1466
1493
|
function buildCapabilities(): HostCapabilities {
|
|
1467
1494
|
const runtime = containerRuntimeCapabilityState();
|
|
1468
1495
|
const projectEnvBindings = projectEnvBindingCapability();
|
|
1469
1496
|
return {
|
|
1470
1497
|
version: packageVersion(),
|
|
1498
|
+
...(diskSnapshot ? { disk: diskSnapshot } : {}),
|
|
1471
1499
|
protocolFeatures: [
|
|
1500
|
+
"host-workspace-file-read-v1",
|
|
1501
|
+
ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE,
|
|
1502
|
+
LEAD_EVIDENCE_PROTOCOL_FEATURE,
|
|
1472
1503
|
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
1473
1504
|
EVENT_REPLAY_PROTOCOL_FEATURE,
|
|
1474
1505
|
GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE,
|
|
@@ -1489,6 +1520,8 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1489
1520
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
1490
1521
|
ENGINE_USAGE_PROTOCOL_FEATURE,
|
|
1491
1522
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
1523
|
+
LIVE_TASK_PROJECTS_PROTOCOL_FEATURE,
|
|
1524
|
+
...(credentialsMode() === "active" || signInBoxMode() ? [TASK_SECRETS_FEATURE] : []),
|
|
1492
1525
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
1493
1526
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
1494
1527
|
// dispatch in mock mode would let the composer create a Secretary that
|
|
@@ -1504,6 +1537,9 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1504
1537
|
taskFilesMachineRetirementReady()
|
|
1505
1538
|
? [HOST_TASK_FILES_LEASE_FEATURE]
|
|
1506
1539
|
: []),
|
|
1540
|
+
// ADR-161 §1.11: this image's `uai files` reads cloud links. The cloud
|
|
1541
|
+
// captures the advertisement at container creation.
|
|
1542
|
+
HOST_AGENT_FILES_FEATURE,
|
|
1507
1543
|
// ADR-154: only the sign-in box writes cloud credentials.
|
|
1508
1544
|
...(signInBoxMode()
|
|
1509
1545
|
? [
|
|
@@ -3624,6 +3660,18 @@ async function connect(): Promise<void> {
|
|
|
3624
3660
|
);
|
|
3625
3661
|
break;
|
|
3626
3662
|
}
|
|
3663
|
+
case "task.secret.staging.put": {
|
|
3664
|
+
if (!signInBoxMode() || !isTaskSecretStagingPut(frame)) break;
|
|
3665
|
+
void managedMutationDrain.track(async () => {
|
|
3666
|
+
try {
|
|
3667
|
+
await writeTaskSecretStaging(frame);
|
|
3668
|
+
send(socket, { kind: "task.secret.staging.ack", opId: frame.opId, stagingId: frame.stagingId, ok: true });
|
|
3669
|
+
} catch {
|
|
3670
|
+
send(socket, { kind: "task.secret.staging.ack", opId: frame.opId, stagingId: frame.stagingId, ok: false });
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3673
|
+
break;
|
|
3674
|
+
}
|
|
3627
3675
|
case "env.staging.put": {
|
|
3628
3676
|
// Only the write-only sign-in box owns immutable staging paths. An
|
|
3629
3677
|
// owned runner or task sidecar must never accept plaintext here.
|
|
@@ -4502,6 +4550,8 @@ async function dispatchCommand(
|
|
|
4502
4550
|
expectString(args, 0),
|
|
4503
4551
|
expectString(args, 1),
|
|
4504
4552
|
expectString(args, 2),
|
|
4553
|
+
args[3] as DeliveryOrigin | undefined,
|
|
4554
|
+
args[4] as AssignmentDelivery | undefined,
|
|
4505
4555
|
);
|
|
4506
4556
|
case "channelResolvePermission":
|
|
4507
4557
|
return hostCommands.channelResolvePermission(
|
|
@@ -4515,11 +4565,23 @@ async function dispatchCommand(
|
|
|
4515
4565
|
return hostCommands.cloneRepo(ctx, expectCloneRepoInput(args, 0));
|
|
4516
4566
|
case "taskDiff":
|
|
4517
4567
|
return hostCommands.taskDiff(ctx, expectTaskDiffInput(args, 0));
|
|
4568
|
+
case "taskProjectPrepare":
|
|
4569
|
+
return hostCommands.taskProjectPrepare(ctx, expectTaskProjectPrepareInput(args, 0));
|
|
4570
|
+
case "taskProjectActivate":
|
|
4571
|
+
return hostCommands.taskProjectActivate(ctx, expectTaskProjectActivateInput(args, 0));
|
|
4572
|
+
case "taskProjectCompensate":
|
|
4573
|
+
return hostCommands.taskProjectCompensate(ctx, expectTaskProjectFence(args[0], "task project compensate input"));
|
|
4574
|
+
case "taskProjectStatus":
|
|
4575
|
+
return hostCommands.taskProjectStatus(ctx, expectTaskProjectStatusInput(args, 0));
|
|
4518
4576
|
case "attachmentWrite":
|
|
4519
4577
|
return hostCommands.attachmentWrite(
|
|
4520
4578
|
ctx,
|
|
4521
4579
|
expectAttachmentWriteInput(args, 0),
|
|
4522
4580
|
);
|
|
4581
|
+
case "workspaceFileRead": {
|
|
4582
|
+
const input = expectRecord(args[0], "workspace file read input");
|
|
4583
|
+
return hostCommands.workspaceFileRead(ctx, { taskId: expectStringValue(input.taskId, "taskId"), path: expectStringValue(input.path, "path"), maxBytes: expectNumberArg([input.maxBytes], 0) });
|
|
4584
|
+
}
|
|
4523
4585
|
case "attachmentRead":
|
|
4524
4586
|
return hostCommands.attachmentRead(
|
|
4525
4587
|
ctx,
|
|
@@ -4969,6 +5031,7 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
4969
5031
|
) {
|
|
4970
5032
|
return frame;
|
|
4971
5033
|
}
|
|
5034
|
+
if (isTaskSecretStagingPut(frame)) return frame;
|
|
4972
5035
|
if (isProjectEnvStagingPutFrame(frame)) return frame;
|
|
4973
5036
|
// Sidecar replica authority has a separate durable store and apply lane.
|
|
4974
5037
|
// Exact guards keep every attempt, cursor and immutable value reference;
|
|
@@ -5265,6 +5328,7 @@ function rejectFrameDuringShutdown(
|
|
|
5265
5328
|
return true;
|
|
5266
5329
|
case "env.var.remove":
|
|
5267
5330
|
case "env.binding.set":
|
|
5331
|
+
case "task.secret.staging.put":
|
|
5268
5332
|
case "env.staging.put":
|
|
5269
5333
|
case "env.reconcile.begin":
|
|
5270
5334
|
case "env.reconciled":
|
|
@@ -5431,6 +5495,7 @@ function rejectCredentialFrameOffOwnCredentials(socket: WebSocket, frame: CloudT
|
|
|
5431
5495
|
return true;
|
|
5432
5496
|
case "env.var.remove":
|
|
5433
5497
|
case "env.binding.set":
|
|
5498
|
+
case "task.secret.staging.put":
|
|
5434
5499
|
case "env.staging.put":
|
|
5435
5500
|
case "env.reconcile.begin":
|
|
5436
5501
|
case "env.reconciled":
|
|
@@ -5496,8 +5561,13 @@ const HOST_COMMAND_NAMES = {
|
|
|
5496
5561
|
channelResolvePermission: true,
|
|
5497
5562
|
cloneRepo: true,
|
|
5498
5563
|
taskDiff: true,
|
|
5564
|
+
taskProjectPrepare: true,
|
|
5565
|
+
taskProjectActivate: true,
|
|
5566
|
+
taskProjectCompensate: true,
|
|
5567
|
+
taskProjectStatus: true,
|
|
5499
5568
|
attachmentWrite: true,
|
|
5500
5569
|
attachmentRead: true,
|
|
5570
|
+
workspaceFileRead: true,
|
|
5501
5571
|
filesOp: true,
|
|
5502
5572
|
sidecarAssign: true,
|
|
5503
5573
|
channelInterrupt: true,
|
|
@@ -5683,6 +5753,77 @@ function expectTaskDiffInput(args: unknown[], index: number): TaskDiffInput {
|
|
|
5683
5753
|
};
|
|
5684
5754
|
}
|
|
5685
5755
|
|
|
5756
|
+
function expectPositiveSafeInteger(value: unknown, label: string): number {
|
|
5757
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
|
5758
|
+
throw new Error(`invalid command args: expected positive integer ${label}`);
|
|
5759
|
+
}
|
|
5760
|
+
return value as number;
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function expectTaskProjectFence(value: unknown, label: string): TaskProjectOpFence {
|
|
5764
|
+
const input = expectRecord(value, label);
|
|
5765
|
+
return {
|
|
5766
|
+
taskId: expectStringValue(input.taskId, "taskId"),
|
|
5767
|
+
runEpoch: expectPositiveSafeInteger(input.runEpoch, "runEpoch"),
|
|
5768
|
+
operationId: expectStringValue(input.operationId, "operationId"),
|
|
5769
|
+
projectSetRevision: expectPositiveSafeInteger(input.projectSetRevision, "projectSetRevision"),
|
|
5770
|
+
};
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
function expectTaskProjectPrepareInput(args: unknown[], index: number): TaskProjectPrepareInput {
|
|
5774
|
+
const input = expectRecord(args[index], "task project prepare input");
|
|
5775
|
+
const project = expectRecord(input.project, "task project");
|
|
5776
|
+
return {
|
|
5777
|
+
...expectTaskProjectFence(input, "task project prepare input"),
|
|
5778
|
+
branch: expectStringValue(input.branch, "branch"),
|
|
5779
|
+
project: {
|
|
5780
|
+
id: expectStringValue(project.id, "project.id"),
|
|
5781
|
+
slug: expectStringValue(project.slug, "project.slug"),
|
|
5782
|
+
repoUrl: expectStringValue(project.repoUrl, "project.repoUrl"),
|
|
5783
|
+
...(typeof project.toolVersions === "string" ? { toolVersions: project.toolVersions } : {}),
|
|
5784
|
+
},
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
|
|
5788
|
+
function expectTaskProjectActivateInput(args: unknown[], index: number): TaskProjectActivateInput {
|
|
5789
|
+
const input = expectRecord(args[index], "task project activate input");
|
|
5790
|
+
let grant: TaskProjectActivateInput["grant"] = null;
|
|
5791
|
+
if (input.grant !== null) {
|
|
5792
|
+
const row = expectRecord(input.grant, "task project grant");
|
|
5793
|
+
grant = {
|
|
5794
|
+
manifestRevision: expectPositiveSafeInteger(row.manifestRevision, "grant.manifestRevision"),
|
|
5795
|
+
manifestDigest: expectStringValue(row.manifestDigest, "grant.manifestDigest"),
|
|
5796
|
+
targetGeneration: expectPositiveSafeInteger(row.targetGeneration, "grant.targetGeneration"),
|
|
5797
|
+
};
|
|
5798
|
+
}
|
|
5799
|
+
return {
|
|
5800
|
+
...expectTaskProjectFence(input, "task project activate input"),
|
|
5801
|
+
project: expectTaskCommandProject(input.project),
|
|
5802
|
+
previewEnv: expectStringRecord(input.previewEnv ?? {}, "previewEnv"),
|
|
5803
|
+
grant,
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5807
|
+
function expectStringRecord(value: unknown, label: string): Record<string, string> {
|
|
5808
|
+
const record = expectRecord(value, label);
|
|
5809
|
+
const out: Record<string, string> = {};
|
|
5810
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
5811
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`invalid command args: ${label} name ${key}`);
|
|
5812
|
+
out[key] = expectStringValue(entry, `${label}.${key}`);
|
|
5813
|
+
}
|
|
5814
|
+
return out;
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
function expectTaskProjectStatusInput(args: unknown[], index: number): TaskProjectStatusInput {
|
|
5818
|
+
const input = expectRecord(args[index], "task project status input");
|
|
5819
|
+
return {
|
|
5820
|
+
taskId: expectStringValue(input.taskId, "taskId"),
|
|
5821
|
+
runEpoch: expectPositiveSafeInteger(input.runEpoch, "runEpoch"),
|
|
5822
|
+
operationId: expectStringValue(input.operationId, "operationId"),
|
|
5823
|
+
projectId: expectStringValue(input.projectId, "projectId"),
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5686
5827
|
function expectAttachmentWriteInput(
|
|
5687
5828
|
args: unknown[],
|
|
5688
5829
|
index: number,
|
|
@@ -5768,6 +5909,7 @@ function expectChannelEnsureInput(
|
|
|
5768
5909
|
// Both are optional for wire compatibility with older clouds.
|
|
5769
5910
|
const mode = parseChannelMode(input.mode);
|
|
5770
5911
|
if (mode !== undefined) out.mode = mode;
|
|
5912
|
+
if (typeof input.leadRunId === "string") out.leadRunId = input.leadRunId;
|
|
5771
5913
|
if (typeof input.secretaryAgentId === "string") {
|
|
5772
5914
|
out.secretaryAgentId = input.secretaryAgentId;
|
|
5773
5915
|
}
|