@runuai/host 0.9.167 → 0.9.168
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 +789 -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/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/task-env-source.ts +2 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1052 -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 +125 -0
- package/src/protocol.ts +185 -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.168",
|
|
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,9 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
+
import type { AssignmentDelivery, DeliveryOrigin } from "./protocol";
|
|
2
3
|
|
|
3
4
|
import "./load-env";
|
|
5
|
+
import { TASK_SECRETS_FEATURE, isTaskSecretStagingPut } from "./task-secrets";
|
|
6
|
+
import { writeTaskSecretStaging } from "../lib/task-secret-staging";
|
|
4
7
|
|
|
5
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
9
|
import {
|
|
@@ -113,6 +116,7 @@ import {
|
|
|
113
116
|
parseLeaseGrantV1Frame,
|
|
114
117
|
parseLeaseStaleFrame,
|
|
115
118
|
parseLeaseUnavailableFrame,
|
|
119
|
+
HOST_AGENT_FILES_FEATURE,
|
|
116
120
|
HOST_TASK_FILES_LEASE_FEATURE,
|
|
117
121
|
} from "./cloud-credentials";
|
|
118
122
|
import { captureEngineAccount } from "../lib/capture-flow";
|
|
@@ -301,6 +305,7 @@ import {
|
|
|
301
305
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
302
306
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
303
307
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
308
|
+
LIVE_TASK_PROJECTS_PROTOCOL_FEATURE,
|
|
304
309
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
305
310
|
isEngineAccountScope,
|
|
306
311
|
isTaskMachineRetireAckFrame,
|
|
@@ -318,6 +323,8 @@ import {
|
|
|
318
323
|
MAX_REMOTE_HOST_MESSAGE_BYTES,
|
|
319
324
|
MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE,
|
|
320
325
|
SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
|
|
326
|
+
ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE,
|
|
327
|
+
LEAD_EVIDENCE_PROTOCOL_FEATURE,
|
|
321
328
|
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
322
329
|
type CloudToHost,
|
|
323
330
|
type McpOp,
|
|
@@ -364,6 +371,10 @@ import {
|
|
|
364
371
|
type TaskCommandProject,
|
|
365
372
|
type TaskCommandTask,
|
|
366
373
|
type TaskDiffInput,
|
|
374
|
+
type TaskProjectActivateInput,
|
|
375
|
+
type TaskProjectOpFence,
|
|
376
|
+
type TaskProjectPrepareInput,
|
|
377
|
+
type TaskProjectStatusInput,
|
|
367
378
|
type TaskDownInput,
|
|
368
379
|
type TaskLaunchInput,
|
|
369
380
|
type TaskStopInput,
|
|
@@ -1469,6 +1480,9 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1469
1480
|
return {
|
|
1470
1481
|
version: packageVersion(),
|
|
1471
1482
|
protocolFeatures: [
|
|
1483
|
+
"host-workspace-file-read-v1",
|
|
1484
|
+
ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE,
|
|
1485
|
+
LEAD_EVIDENCE_PROTOCOL_FEATURE,
|
|
1472
1486
|
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
1473
1487
|
EVENT_REPLAY_PROTOCOL_FEATURE,
|
|
1474
1488
|
GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE,
|
|
@@ -1489,6 +1503,8 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1489
1503
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
1490
1504
|
ENGINE_USAGE_PROTOCOL_FEATURE,
|
|
1491
1505
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
1506
|
+
LIVE_TASK_PROJECTS_PROTOCOL_FEATURE,
|
|
1507
|
+
...(credentialsMode() === "active" || signInBoxMode() ? [TASK_SECRETS_FEATURE] : []),
|
|
1492
1508
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
1493
1509
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
1494
1510
|
// dispatch in mock mode would let the composer create a Secretary that
|
|
@@ -1504,6 +1520,9 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1504
1520
|
taskFilesMachineRetirementReady()
|
|
1505
1521
|
? [HOST_TASK_FILES_LEASE_FEATURE]
|
|
1506
1522
|
: []),
|
|
1523
|
+
// ADR-161 §1.11: this image's `uai files` reads cloud links. The cloud
|
|
1524
|
+
// captures the advertisement at container creation.
|
|
1525
|
+
HOST_AGENT_FILES_FEATURE,
|
|
1507
1526
|
// ADR-154: only the sign-in box writes cloud credentials.
|
|
1508
1527
|
...(signInBoxMode()
|
|
1509
1528
|
? [
|
|
@@ -3624,6 +3643,18 @@ async function connect(): Promise<void> {
|
|
|
3624
3643
|
);
|
|
3625
3644
|
break;
|
|
3626
3645
|
}
|
|
3646
|
+
case "task.secret.staging.put": {
|
|
3647
|
+
if (!signInBoxMode() || !isTaskSecretStagingPut(frame)) break;
|
|
3648
|
+
void managedMutationDrain.track(async () => {
|
|
3649
|
+
try {
|
|
3650
|
+
await writeTaskSecretStaging(frame);
|
|
3651
|
+
send(socket, { kind: "task.secret.staging.ack", opId: frame.opId, stagingId: frame.stagingId, ok: true });
|
|
3652
|
+
} catch {
|
|
3653
|
+
send(socket, { kind: "task.secret.staging.ack", opId: frame.opId, stagingId: frame.stagingId, ok: false });
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3656
|
+
break;
|
|
3657
|
+
}
|
|
3627
3658
|
case "env.staging.put": {
|
|
3628
3659
|
// Only the write-only sign-in box owns immutable staging paths. An
|
|
3629
3660
|
// owned runner or task sidecar must never accept plaintext here.
|
|
@@ -4502,6 +4533,8 @@ async function dispatchCommand(
|
|
|
4502
4533
|
expectString(args, 0),
|
|
4503
4534
|
expectString(args, 1),
|
|
4504
4535
|
expectString(args, 2),
|
|
4536
|
+
args[3] as DeliveryOrigin | undefined,
|
|
4537
|
+
args[4] as AssignmentDelivery | undefined,
|
|
4505
4538
|
);
|
|
4506
4539
|
case "channelResolvePermission":
|
|
4507
4540
|
return hostCommands.channelResolvePermission(
|
|
@@ -4515,11 +4548,23 @@ async function dispatchCommand(
|
|
|
4515
4548
|
return hostCommands.cloneRepo(ctx, expectCloneRepoInput(args, 0));
|
|
4516
4549
|
case "taskDiff":
|
|
4517
4550
|
return hostCommands.taskDiff(ctx, expectTaskDiffInput(args, 0));
|
|
4551
|
+
case "taskProjectPrepare":
|
|
4552
|
+
return hostCommands.taskProjectPrepare(ctx, expectTaskProjectPrepareInput(args, 0));
|
|
4553
|
+
case "taskProjectActivate":
|
|
4554
|
+
return hostCommands.taskProjectActivate(ctx, expectTaskProjectActivateInput(args, 0));
|
|
4555
|
+
case "taskProjectCompensate":
|
|
4556
|
+
return hostCommands.taskProjectCompensate(ctx, expectTaskProjectFence(args[0], "task project compensate input"));
|
|
4557
|
+
case "taskProjectStatus":
|
|
4558
|
+
return hostCommands.taskProjectStatus(ctx, expectTaskProjectStatusInput(args, 0));
|
|
4518
4559
|
case "attachmentWrite":
|
|
4519
4560
|
return hostCommands.attachmentWrite(
|
|
4520
4561
|
ctx,
|
|
4521
4562
|
expectAttachmentWriteInput(args, 0),
|
|
4522
4563
|
);
|
|
4564
|
+
case "workspaceFileRead": {
|
|
4565
|
+
const input = expectRecord(args[0], "workspace file read input");
|
|
4566
|
+
return hostCommands.workspaceFileRead(ctx, { taskId: expectStringValue(input.taskId, "taskId"), path: expectStringValue(input.path, "path"), maxBytes: expectNumberArg([input.maxBytes], 0) });
|
|
4567
|
+
}
|
|
4523
4568
|
case "attachmentRead":
|
|
4524
4569
|
return hostCommands.attachmentRead(
|
|
4525
4570
|
ctx,
|
|
@@ -4969,6 +5014,7 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
4969
5014
|
) {
|
|
4970
5015
|
return frame;
|
|
4971
5016
|
}
|
|
5017
|
+
if (isTaskSecretStagingPut(frame)) return frame;
|
|
4972
5018
|
if (isProjectEnvStagingPutFrame(frame)) return frame;
|
|
4973
5019
|
// Sidecar replica authority has a separate durable store and apply lane.
|
|
4974
5020
|
// Exact guards keep every attempt, cursor and immutable value reference;
|
|
@@ -5265,6 +5311,7 @@ function rejectFrameDuringShutdown(
|
|
|
5265
5311
|
return true;
|
|
5266
5312
|
case "env.var.remove":
|
|
5267
5313
|
case "env.binding.set":
|
|
5314
|
+
case "task.secret.staging.put":
|
|
5268
5315
|
case "env.staging.put":
|
|
5269
5316
|
case "env.reconcile.begin":
|
|
5270
5317
|
case "env.reconciled":
|
|
@@ -5431,6 +5478,7 @@ function rejectCredentialFrameOffOwnCredentials(socket: WebSocket, frame: CloudT
|
|
|
5431
5478
|
return true;
|
|
5432
5479
|
case "env.var.remove":
|
|
5433
5480
|
case "env.binding.set":
|
|
5481
|
+
case "task.secret.staging.put":
|
|
5434
5482
|
case "env.staging.put":
|
|
5435
5483
|
case "env.reconcile.begin":
|
|
5436
5484
|
case "env.reconciled":
|
|
@@ -5496,8 +5544,13 @@ const HOST_COMMAND_NAMES = {
|
|
|
5496
5544
|
channelResolvePermission: true,
|
|
5497
5545
|
cloneRepo: true,
|
|
5498
5546
|
taskDiff: true,
|
|
5547
|
+
taskProjectPrepare: true,
|
|
5548
|
+
taskProjectActivate: true,
|
|
5549
|
+
taskProjectCompensate: true,
|
|
5550
|
+
taskProjectStatus: true,
|
|
5499
5551
|
attachmentWrite: true,
|
|
5500
5552
|
attachmentRead: true,
|
|
5553
|
+
workspaceFileRead: true,
|
|
5501
5554
|
filesOp: true,
|
|
5502
5555
|
sidecarAssign: true,
|
|
5503
5556
|
channelInterrupt: true,
|
|
@@ -5683,6 +5736,77 @@ function expectTaskDiffInput(args: unknown[], index: number): TaskDiffInput {
|
|
|
5683
5736
|
};
|
|
5684
5737
|
}
|
|
5685
5738
|
|
|
5739
|
+
function expectPositiveSafeInteger(value: unknown, label: string): number {
|
|
5740
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
|
5741
|
+
throw new Error(`invalid command args: expected positive integer ${label}`);
|
|
5742
|
+
}
|
|
5743
|
+
return value as number;
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
function expectTaskProjectFence(value: unknown, label: string): TaskProjectOpFence {
|
|
5747
|
+
const input = expectRecord(value, label);
|
|
5748
|
+
return {
|
|
5749
|
+
taskId: expectStringValue(input.taskId, "taskId"),
|
|
5750
|
+
runEpoch: expectPositiveSafeInteger(input.runEpoch, "runEpoch"),
|
|
5751
|
+
operationId: expectStringValue(input.operationId, "operationId"),
|
|
5752
|
+
projectSetRevision: expectPositiveSafeInteger(input.projectSetRevision, "projectSetRevision"),
|
|
5753
|
+
};
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
function expectTaskProjectPrepareInput(args: unknown[], index: number): TaskProjectPrepareInput {
|
|
5757
|
+
const input = expectRecord(args[index], "task project prepare input");
|
|
5758
|
+
const project = expectRecord(input.project, "task project");
|
|
5759
|
+
return {
|
|
5760
|
+
...expectTaskProjectFence(input, "task project prepare input"),
|
|
5761
|
+
branch: expectStringValue(input.branch, "branch"),
|
|
5762
|
+
project: {
|
|
5763
|
+
id: expectStringValue(project.id, "project.id"),
|
|
5764
|
+
slug: expectStringValue(project.slug, "project.slug"),
|
|
5765
|
+
repoUrl: expectStringValue(project.repoUrl, "project.repoUrl"),
|
|
5766
|
+
...(typeof project.toolVersions === "string" ? { toolVersions: project.toolVersions } : {}),
|
|
5767
|
+
},
|
|
5768
|
+
};
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
function expectTaskProjectActivateInput(args: unknown[], index: number): TaskProjectActivateInput {
|
|
5772
|
+
const input = expectRecord(args[index], "task project activate input");
|
|
5773
|
+
let grant: TaskProjectActivateInput["grant"] = null;
|
|
5774
|
+
if (input.grant !== null) {
|
|
5775
|
+
const row = expectRecord(input.grant, "task project grant");
|
|
5776
|
+
grant = {
|
|
5777
|
+
manifestRevision: expectPositiveSafeInteger(row.manifestRevision, "grant.manifestRevision"),
|
|
5778
|
+
manifestDigest: expectStringValue(row.manifestDigest, "grant.manifestDigest"),
|
|
5779
|
+
targetGeneration: expectPositiveSafeInteger(row.targetGeneration, "grant.targetGeneration"),
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
return {
|
|
5783
|
+
...expectTaskProjectFence(input, "task project activate input"),
|
|
5784
|
+
project: expectTaskCommandProject(input.project),
|
|
5785
|
+
previewEnv: expectStringRecord(input.previewEnv ?? {}, "previewEnv"),
|
|
5786
|
+
grant,
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
function expectStringRecord(value: unknown, label: string): Record<string, string> {
|
|
5791
|
+
const record = expectRecord(value, label);
|
|
5792
|
+
const out: Record<string, string> = {};
|
|
5793
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
5794
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`invalid command args: ${label} name ${key}`);
|
|
5795
|
+
out[key] = expectStringValue(entry, `${label}.${key}`);
|
|
5796
|
+
}
|
|
5797
|
+
return out;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
function expectTaskProjectStatusInput(args: unknown[], index: number): TaskProjectStatusInput {
|
|
5801
|
+
const input = expectRecord(args[index], "task project status input");
|
|
5802
|
+
return {
|
|
5803
|
+
taskId: expectStringValue(input.taskId, "taskId"),
|
|
5804
|
+
runEpoch: expectPositiveSafeInteger(input.runEpoch, "runEpoch"),
|
|
5805
|
+
operationId: expectStringValue(input.operationId, "operationId"),
|
|
5806
|
+
projectId: expectStringValue(input.projectId, "projectId"),
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5686
5810
|
function expectAttachmentWriteInput(
|
|
5687
5811
|
args: unknown[],
|
|
5688
5812
|
index: number,
|
|
@@ -5768,6 +5892,7 @@ function expectChannelEnsureInput(
|
|
|
5768
5892
|
// Both are optional for wire compatibility with older clouds.
|
|
5769
5893
|
const mode = parseChannelMode(input.mode);
|
|
5770
5894
|
if (mode !== undefined) out.mode = mode;
|
|
5895
|
+
if (typeof input.leadRunId === "string") out.leadRunId = input.leadRunId;
|
|
5771
5896
|
if (typeof input.secretaryAgentId === "string") {
|
|
5772
5897
|
out.secretaryAgentId = input.secretaryAgentId;
|
|
5773
5898
|
}
|