@runuai/host 0.4.3 → 0.6.0

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/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { agent, AgentError } from "../lib/agent";
2
2
  import { cloneRepo } from "../lib/repo-clone";
3
+ import { handleFilesOp } from "../lib/shared-files";
3
4
  import { getOrchestrator } from "../lib/orchestrator";
4
5
  import { storeTaskCliSecret } from "../lib/agent-cli";
5
6
  import { setupTaskGithub, clearRefresh } from "../lib/github-tokens";
@@ -308,6 +309,15 @@ export const hostCommands: HostCommands = {
308
309
  }
309
310
  },
310
311
 
312
+ async filesOp(ctx, input) {
313
+ logCommand(ctx, "filesOp", input.op, `${input.scope}:${input.path}`);
314
+ try {
315
+ return ok(await handleFilesOp(input));
316
+ } catch (err) {
317
+ return failFromUnknown(err);
318
+ }
319
+ },
320
+
311
321
  async channelInterrupt(ctx, taskId, agentId) {
312
322
  logCommand(ctx, "channelInterrupt", taskId, agentId);
313
323
  try {
package/src/main.ts CHANGED
@@ -84,6 +84,7 @@ import {
84
84
  type TaskDiffInput,
85
85
  type TaskDownInput,
86
86
  type TaskLaunchInput,
87
+ FilesOpInput,
87
88
  } from "./protocol";
88
89
 
89
90
  const PING_INTERVAL_MS = 15_000;
@@ -875,6 +876,8 @@ function dispatchCommand(
875
876
  ctx,
876
877
  expectAttachmentReadInput(args, 0),
877
878
  );
879
+ case "filesOp":
880
+ return hostCommands.filesOp(ctx, expectFilesOpInput(args, 0));
878
881
  case "channelInterrupt":
879
882
  return hostCommands.channelInterrupt(
880
883
  ctx,
@@ -1123,6 +1126,7 @@ function isHostCommand(command: string): command is keyof HostCommands {
1123
1126
  "taskDiff",
1124
1127
  "attachmentWrite",
1125
1128
  "attachmentRead",
1129
+ "filesOp",
1126
1130
  "channelInterrupt",
1127
1131
  "appendTranscript",
1128
1132
  "previewEnsure",
@@ -1277,6 +1281,37 @@ function expectAttachmentReadInput(
1277
1281
  };
1278
1282
  }
1279
1283
 
1284
+ /** ADR-062: the THIRD whitelist for filesOp — op/scope enums, relative path
1285
+ * only (containment is re-checked in lib/shared-files.ts, the final gate). */
1286
+ function expectFilesOpInput(args: unknown[], index: number): FilesOpInput {
1287
+ const input = expectRecord(args[index], "files op input");
1288
+ const op = expectStringValue(input.op, "op");
1289
+ if (!["list", "read", "write", "mkdir", "delete"].includes(op)) {
1290
+ throw new Error(`invalid files op: ${op}`);
1291
+ }
1292
+ const scope = expectStringValue(input.scope, "scope");
1293
+ if (scope !== "org" && scope !== "me") {
1294
+ throw new Error(`invalid files scope: ${scope}`);
1295
+ }
1296
+ const out: FilesOpInput = {
1297
+ op: op as FilesOpInput["op"],
1298
+ hostId: typeof input.hostId === "string" ? input.hostId : "",
1299
+ scope,
1300
+ orgId: typeof input.orgId === "string" ? input.orgId : "",
1301
+ userId: typeof input.userId === "string" ? input.userId : "",
1302
+ path: typeof input.path === "string" ? input.path : "",
1303
+ };
1304
+ if (typeof input.offset === "number" && Number.isFinite(input.offset)) {
1305
+ out.offset = input.offset;
1306
+ }
1307
+ if (typeof input.length === "number" && Number.isFinite(input.length)) {
1308
+ out.length = input.length;
1309
+ }
1310
+ if (typeof input.dataBase64 === "string") out.dataBase64 = input.dataBase64;
1311
+ if (input.append === true) out.append = true;
1312
+ return out;
1313
+ }
1314
+
1280
1315
  function expectChannelEnsureInput(
1281
1316
  args: unknown[],
1282
1317
  index: number,
@@ -1334,6 +1369,14 @@ function expectChannelEnsureInput(
1334
1369
  });
1335
1370
  if (connections.length > 0) out.mcpConnections = connections;
1336
1371
  }
1372
+ // ADR-062: shared-files mode for the preamble "## Files" section.
1373
+ if (
1374
+ input.sharedFiles === "off" ||
1375
+ input.sharedFiles === "ro" ||
1376
+ input.sharedFiles === "rw"
1377
+ ) {
1378
+ out.sharedFiles = input.sharedFiles;
1379
+ }
1337
1380
  return out;
1338
1381
  }
1339
1382
 
@@ -1373,6 +1416,14 @@ function expectTaskCommandTask(value: unknown): TaskCommandTask {
1373
1416
  }
1374
1417
  if (Object.keys(pe).length > 0) out.previewEnv = pe;
1375
1418
  }
1419
+ if (typeof row.ownerOrgId === "string") out.ownerOrgId = row.ownerOrgId;
1420
+ if (
1421
+ row.sharedFiles === "off" ||
1422
+ row.sharedFiles === "ro" ||
1423
+ row.sharedFiles === "rw"
1424
+ ) {
1425
+ out.sharedFiles = row.sharedFiles;
1426
+ }
1376
1427
  return out;
1377
1428
  }
1378
1429
 
package/src/protocol.ts CHANGED
@@ -144,6 +144,10 @@ export interface TaskCommandTask {
144
144
  * `urlEnv` and the cloud has a preview base domain configured.
145
145
  */
146
146
  previewEnv?: Record<string, string>;
147
+ /** ADR-062: owning org — locates the org shared-files root on the host. */
148
+ ownerOrgId?: string;
149
+ /** ADR-062: shared-files mount mode ("off" | "ro" | "rw"; default "ro"). */
150
+ sharedFiles?: string;
147
151
  }
148
152
 
149
153
  /**
@@ -203,6 +207,9 @@ export interface ChannelEnsureInput {
203
207
  /** ADR-057: the owner's usable MCP connections (policy "on", connected,
204
208
  * remote). The host writes gateway-URL MCP configs at session start. */
205
209
  mcpConnections?: Array<{ id: string; slug: string }>;
210
+ /** ADR-062: shared-files mount mode — drives the preamble "## Files"
211
+ * section ("off" | "ro" | "rw"). */
212
+ sharedFiles?: string;
206
213
  globalContext?: string;
207
214
  projects: Array<{ slug: string; defaultPrompt: string }>;
208
215
  branch: string;
@@ -256,6 +263,39 @@ export interface TaskDiffResult {
256
263
  repos: TaskDiffRepo[];
257
264
  }
258
265
 
266
+ /** ADR-062 shared-files op (cloud → host). Scope picks the root:
267
+ * "org" → <workspaceRoot>/shared/orgs/<orgId>, "me" → shared/users/<userId>. */
268
+ export interface FilesOpInput {
269
+ op: "list" | "read" | "write" | "mkdir" | "delete";
270
+ /** Routing only (cloud→bridge): which host's roots to touch. */
271
+ hostId: string;
272
+ scope: "org" | "me";
273
+ orgId: string;
274
+ userId: string;
275
+ /** Relative path inside the scope root ("" = root). */
276
+ path: string;
277
+ /** read: byte offset; default 0. */
278
+ offset?: number;
279
+ /** read: max bytes per chunk (host caps it regardless). */
280
+ length?: number;
281
+ /** write: base64 chunk. */
282
+ dataBase64?: string;
283
+ /** write: append to existing (true) or truncate/create (false/absent). */
284
+ append?: boolean;
285
+ }
286
+
287
+ export interface FilesEntry {
288
+ name: string;
289
+ dir: boolean;
290
+ size: number;
291
+ mtimeMs: number;
292
+ }
293
+
294
+ export type FilesOpValue =
295
+ | { entries: FilesEntry[] } // list
296
+ | { dataBase64: string; size: number; eof: boolean } // read
297
+ | Record<string, never>; // write / mkdir / delete
298
+
259
299
  export interface HostCommands {
260
300
  taskUp(
261
301
  ctx: CommandContext,
@@ -321,6 +361,13 @@ export interface HostCommands {
321
361
  ctx: CommandContext,
322
362
  input: { taskId: string; filename: string },
323
363
  ): Promise<HostCommandResult<{ dataBase64: string }>>;
364
+ /** ADR-062 shared files — list/read/write/mkdir/delete inside the host's
365
+ * org or personal shared root. Paths are RELATIVE; the host normalizes and
366
+ * rejects escapes. Reads/writes are chunked base64 (offset/append). */
367
+ filesOp(
368
+ ctx: CommandContext,
369
+ input: FilesOpInput,
370
+ ): Promise<HostCommandResult<FilesOpValue>>;
324
371
  channelInterrupt(
325
372
  ctx: CommandContext,
326
373
  taskId: string,