@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/src/protocol.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TaskSecretApplied, TaskSecretStagingPut, TaskSecretStagingAck } from "./task-secrets";
1
2
  import {
2
3
  parseCaptureFlowWire,
3
4
  parseCaptureResultWire,
@@ -48,6 +49,7 @@ export enum HostErrorCode {
48
49
  NoSuchPermission = "no_such_permission",
49
50
  HostUnavailable = "host_unavailable",
50
51
  Internal = "internal",
52
+ PayloadTooLarge = "payload_too_large",
51
53
  }
52
54
 
53
55
  export type HostCommandResult<T> =
@@ -62,8 +64,10 @@ export type HostCommandResult<T> =
62
64
  * ignore this additive field; new readers use it only to bridge the
63
65
  * short supervisor restart after a managed-runtime activation. Generic
64
66
  * `retryable` failures (for example, no Docker runtime) remain fail-fast.
67
+ * `connection_changed` is cloud-only: the runner's socket generation
68
+ * moved after this launch was built, so the command was not sent.
65
69
  */
66
- retryReason?: "managed_restart" | "runtime_setup";
70
+ retryReason?: "managed_restart" | "runtime_setup" | "connection_changed";
67
71
  /**
68
72
  * Who decided this failed.
69
73
  *
@@ -93,6 +97,8 @@ export type HostCommandResult<T> =
93
97
  };
94
98
 
95
99
  /** Capability ids shared by host advertisement and fail-closed cloud gates. */
100
+ export const ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE = "assignment-evidence-v1";
101
+ export const LEAD_EVIDENCE_PROTOCOL_FEATURE = "lead-evidence-v1";
96
102
  export const TRANSCRIPT_TARGETS_PROTOCOL_FEATURE = "transcript-targets-v1";
97
103
  export const SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE =
98
104
  "secretary-typed-dispatch-v1";
@@ -232,6 +238,14 @@ export const LIVE_ROSTER_EDIT_PROTOCOL_FEATURE = "live-roster-edit-v1";
232
238
  * generation is not its roster entry's, so a token minted for an earlier agent
233
239
  * under the same handle stays dead. */
234
240
  export const AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE = "agent-token-generation-v1";
241
+ /** ADR-161 §2: the runner can add a repository to a RUNNING task through the
242
+ * fenced `taskProjectPrepare` → `taskProjectActivate` operation (with
243
+ * `taskProjectStatus` and `taskProjectCompensate` for recovery). It clones a
244
+ * self-contained repository inside the task's own runtime, as the task user,
245
+ * journals an activation intent before the rename into `/workspace/<slug>`,
246
+ * and answers published / not_published / uncertain from the disk. A cloud
247
+ * refuses the add with HOST_UPDATE_REQUIRED until a runner advertises it. */
248
+ export const LIVE_TASK_PROJECTS_PROTOCOL_FEATURE = "live-task-projects-v1";
235
249
  /** ADR-153 declarations + owner bindings. Advertising this means the host
236
250
  * understands every additive env frame field, persists bindings separately
237
251
  * from values, and reports immutable repository schema observations. Keep it
@@ -3097,7 +3111,7 @@ export interface CommandContext {
3097
3111
  */
3098
3112
  timeoutMs?: number;
3099
3113
  /**
3100
- * Cloud-internal routing override for a durable taskDown. Lifecycle cleanup
3114
+ * Cloud-internal routing override for durable taskDown or archival attachmentRead. Lifecycle cleanup
3101
3115
  * may outlive the task row whose host assignment normally routes commands.
3102
3116
  * This field is context metadata: it is never serialized onto the host
3103
3117
  * command frame, and no host/request payload may populate it.
@@ -3110,6 +3124,15 @@ export interface CommandContext {
3110
3124
  * Like targetHostId, this is relay metadata and never reaches the host wire.
3111
3125
  */
3112
3126
  expectedHostTokenHash?: string;
3127
+ /**
3128
+ * Cloud-internal generation of the host connection whose advertisement
3129
+ * built this taskUp. The bridge does not send the launch when the socket
3130
+ * that would receive it is a different generation. Never serialized onto
3131
+ * the host frame. Absent skips the fence. Null means the features were
3132
+ * read with no live connection, so a socket that appears before the send
3133
+ * is a change and the launch must be built again.
3134
+ */
3135
+ capabilityGeneration?: string | null;
3113
3136
  }
3114
3137
 
3115
3138
  export type PermissionDecision = { kind: "accept" } | { kind: "decline" };
@@ -4045,6 +4068,15 @@ export function isHostConfigAckFrame(
4045
4068
  * changes. The cloud caches it per hostId to drive the task-creation pickers.
4046
4069
  */
4047
4070
  export interface HostCapabilities {
4071
+ /** Filesystem capacity, refreshed each minute. Null means unmeasurable
4072
+ * (for example Docker in a VM); available excludes root-reserved blocks. */
4073
+ disk?: {
4074
+ sampledAt: number;
4075
+ /** Cloud-only: overwritten on authenticated receipt, never trusted from the host. */
4076
+ receivedAt?: number;
4077
+ home: { totalBytes: number; availableBytes: number } | null;
4078
+ docker: { totalBytes: number; availableBytes: number } | null;
4079
+ };
4048
4080
  /** The host-agent package version — the cloud UI shows it on the host page
4049
4081
  * and flags when npm has a newer release. Optional (older hosts omit it). */
4050
4082
  version?: string;
@@ -4314,6 +4346,124 @@ export interface TaskDiffInput {
4314
4346
  projects: Array<{ id: string; slug: string }>;
4315
4347
  }
4316
4348
 
4349
+ /** Cloud Prepare retry timing. */
4350
+ export const TASK_PROJECT_PREPARE_MAX_ATTEMPTS = 12;
4351
+ export const TASK_PROJECT_PREPARE_TIMEOUT_MS = 11 * 60_000;
4352
+ export function taskProjectRetryDelayMs(attempts: number): number {
4353
+ return Math.min(5 * 60_000, 2_000 * 2 ** Math.min(attempts, 7));
4354
+ }
4355
+ /** Bound the CLI tool call; the durable cloud operation may outlast it. */
4356
+ export const TASK_PROJECT_ADD_WAIT_MS = 40 * 60_000;
4357
+
4358
+ /**
4359
+ * ADR-161 §2.3: every live-project command names the task's run, the
4360
+ * operation, and the project-set revision the operation commits. The runner
4361
+ * refuses a command for a run it has not admitted, and any Prepare or
4362
+ * Activate for a revision older than one it has already activated or
4363
+ * compensated.
4364
+ */
4365
+ export interface TaskProjectOpFence {
4366
+ taskId: string;
4367
+ runEpoch: number;
4368
+ operationId: string;
4369
+ projectSetRevision: number;
4370
+ }
4371
+
4372
+ export interface TaskProjectPrepareInput extends TaskProjectOpFence {
4373
+ /** The task branch the clone checks out (from origin when it exists). */
4374
+ branch: string;
4375
+ /** `toolVersions` is the project's fallback `.tool-versions`, seeded into
4376
+ * a clone that ships none, exactly as task-up seeds a startup project. */
4377
+ project: { id: string; slug: string; repoUrl: string; toolVersions?: string };
4378
+ }
4379
+
4380
+ /** Why a live-project command did nothing. `grant_not_installed` and
4381
+ * `busy` are retryable; the rest are final for that operation. */
4382
+ export type TaskProjectRefusal =
4383
+ | "invalid_request"
4384
+ | "repo_access_denied"
4385
+ | "clone_failed"
4386
+ | "workspace_path_taken"
4387
+ | "stale"
4388
+ | "not_running"
4389
+ | "compensated"
4390
+ | "not_prepared"
4391
+ | "grant_not_installed"
4392
+ | "published"
4393
+ | "busy"
4394
+ /** The running environment cannot take a live project (no env spec or no
4395
+ * live env gateway): the cloud answers REQUIRES_TASK_RESTART. */
4396
+ | "unsupported_environment"
4397
+ /** Published, but a required step (Git root, env install) did not
4398
+ * complete; Activate repairs it when repeated. Never terminal. */
4399
+ | "incomplete";
4400
+
4401
+ export type TaskProjectPrepareResult =
4402
+ | {
4403
+ outcome: "prepared";
4404
+ head: string | null;
4405
+ /** The `.env.schema` observation read from the fresh clone, stored on
4406
+ * the runner before the project is exposed to anything (ADR-153 §4). */
4407
+ envSchema: ProjectEnvSchemaObservation | null;
4408
+ }
4409
+ | { outcome: "refused"; reason: TaskProjectRefusal; message: string };
4410
+
4411
+ export interface TaskProjectActivateInput extends TaskProjectOpFence {
4412
+ /** The full project, for the task's env spec and the agents' notice. */
4413
+ project: TaskCommandProject;
4414
+ /** The cloud-computed preview URL literals of this project's declared
4415
+ * previews (`urlEnv`), added to the task's env for names it lacks. */
4416
+ previewEnv: Record<string, string>;
4417
+ /** The exact grant the runner must hold before it publishes; null for a
4418
+ * task on the runner's own credentials, which has no per-task grant. */
4419
+ grant: { manifestRevision: number; manifestDigest: string; targetGeneration: number } | null;
4420
+ }
4421
+
4422
+ export type TaskProjectActivateResult =
4423
+ | { outcome: "ready" | "ready_with_warning"; warning: string | null }
4424
+ | { outcome: "refused"; reason: TaskProjectRefusal; message: string }
4425
+ /** The intent was recorded but the disk proves neither outcome. */
4426
+ | { outcome: "uncertain"; message: string };
4427
+
4428
+ export type TaskProjectCompensateInput = TaskProjectOpFence;
4429
+
4430
+ export type TaskProjectCompensateResult =
4431
+ | { outcome: "compensated" }
4432
+ | { outcome: "refused"; reason: TaskProjectRefusal | "uncertain"; message: string };
4433
+
4434
+ export interface TaskProjectStatusInput {
4435
+ taskId: string;
4436
+ runEpoch: number;
4437
+ operationId: string;
4438
+ projectId: string;
4439
+ }
4440
+
4441
+ /** The runner's journal for one operation, with any activation intent
4442
+ * resolved from the disk first. */
4443
+ export type TaskProjectMilestone =
4444
+ | "none"
4445
+ | "prepared"
4446
+ | "not_published"
4447
+ | "published"
4448
+ | "uncertain"
4449
+ | "ready"
4450
+ | "ready_with_warning"
4451
+ | "compensated";
4452
+
4453
+ export interface TaskProjectStatusResult {
4454
+ milestone: TaskProjectMilestone;
4455
+ warning: string | null;
4456
+ /** The grant this run holds right now (cloud credentials only). */
4457
+ installedGrant: { runEpoch: number; manifestRevision: number; manifestDigest: string } | null;
4458
+ /** Whether the task's env can serve `projectId` now: the open grant's
4459
+ * replica names it, or the runner serves its own store. */
4460
+ envServes: boolean;
4461
+ /** The projects the run's env serves right now (cloud credentials: the
4462
+ * open grant's, once its replica can serve them all); null otherwise. */
4463
+ servedProjectIds: string[] | null;
4464
+ credentials: "cloud" | "own";
4465
+ }
4466
+
4317
4467
  /** A human in the task chat (ADR-049) — owner or invited collaborator. */
4318
4468
  export interface ChannelHuman {
4319
4469
  /** Per-task mention handle (`@diogo`), unique vs agent ids. */
@@ -4322,7 +4472,10 @@ export interface ChannelHuman {
4322
4472
  isOwner: boolean;
4323
4473
  }
4324
4474
 
4475
+
4325
4476
  export interface ChannelEnsureInput {
4477
+ /** Server-issued immutable lead context; old hosts leave measurements unknown. */
4478
+ leadRunId?: string;
4326
4479
  taskId: string;
4327
4480
  agents: TaskAgent[];
4328
4481
  /** ADR-083: channel projection/routing mode. Optional for older clouds. */
@@ -4496,6 +4649,7 @@ export interface HostCommands {
4496
4649
  * redeliveries are not messages a router would ever classify. Absent
4497
4650
  * from an older cloud, which the host reads as "not a human turn". */
4498
4651
  origin?: DeliveryOrigin,
4652
+ assignment?: AssignmentDelivery,
4499
4653
  ): Promise<HostCommandResult<void>>;
4500
4654
  channelResolvePermission(
4501
4655
  ctx: CommandContext,
@@ -4559,10 +4713,28 @@ export interface HostCommands {
4559
4713
  ): Promise<HostCommandResult<TaskDiffResult>>;
4560
4714
  // Chat attachments live in the task workspace on the host (ADR-015); the
4561
4715
  // cloud relays bytes (base64) in and out, never persisting them.
4716
+ /** ADR-161 §2.3 (`live-task-projects-v1`). */
4717
+ taskProjectPrepare(
4718
+ ctx: CommandContext,
4719
+ input: TaskProjectPrepareInput,
4720
+ ): Promise<HostCommandResult<TaskProjectPrepareResult>>;
4721
+ taskProjectActivate(
4722
+ ctx: CommandContext,
4723
+ input: TaskProjectActivateInput,
4724
+ ): Promise<HostCommandResult<TaskProjectActivateResult>>;
4725
+ taskProjectCompensate(
4726
+ ctx: CommandContext,
4727
+ input: TaskProjectCompensateInput,
4728
+ ): Promise<HostCommandResult<TaskProjectCompensateResult>>;
4729
+ taskProjectStatus(
4730
+ ctx: CommandContext,
4731
+ input: TaskProjectStatusInput,
4732
+ ): Promise<HostCommandResult<TaskProjectStatusResult>>;
4562
4733
  attachmentWrite(
4563
4734
  ctx: CommandContext,
4564
4735
  input: { taskId: string; filename: string; dataBase64: string },
4565
4736
  ): Promise<HostCommandResult<void>>;
4737
+ workspaceFileRead(ctx: CommandContext, input: { taskId: string; path: string; maxBytes: number }): Promise<HostCommandResult<{ dataBase64: string; size: number }>>;
4566
4738
  attachmentRead(
4567
4739
  ctx: CommandContext,
4568
4740
  input: { taskId: string; filename: string },
@@ -4745,6 +4917,7 @@ export type CloudToHost =
4745
4917
  | ProjectEnvReconcileBeginFrame
4746
4918
  | ProjectEnvReconciledFrame
4747
4919
  | ProjectEnvDeliverySkipFrame
4920
+ | TaskSecretStagingPut
4748
4921
  | ProjectEnvStagingPutFrame
4749
4922
  | ProjectEnvLegacyTagFrame
4750
4923
  | ProjectEnvReplicaBeginFrame
@@ -4809,6 +4982,30 @@ export type CloudToHost =
4809
4982
  | SshMintFrame
4810
4983
  | GithubRevokeFrame;
4811
4984
 
4985
+ /** ADR-162: cloud-bound delivery identity, preserved by the runner journal. */
4986
+ export interface AssignmentDelivery {
4987
+ assignmentId: string;
4988
+ deliveryId: string;
4989
+ runEpoch: number;
4990
+ recipientGeneration: string | null;
4991
+ }
4992
+ export interface AssignmentTurnEvidence {
4993
+ leadRunId?: string | null;
4994
+ turnId: string;
4995
+ assignmentId: string | null;
4996
+ deliveryId: string | null;
4997
+ sessionId: string;
4998
+ runEpoch: number;
4999
+ recipientGeneration: string | null;
5000
+ correlation: "explicit" | "followup" | "ambiguous" | "unknown";
5001
+ acceptedAt: number;
5002
+ endedAt?: number;
5003
+ engine: string;
5004
+ runtimeVersion: string | null;
5005
+ // A configured model/effort isn't an observation. Actual model comes from
5006
+ // provider usage; unreported effort, CLI version and duration remain null.
5007
+ }
5008
+
4812
5009
  /** ADR-141: provenance of a channelDeliver. */
4813
5010
  export type DeliveryOrigin = "human" | "agent" | "system";
4814
5011
 
@@ -4987,6 +5184,8 @@ export type HostToCloud =
4987
5184
  | ProjectEnvAckFrame
4988
5185
  | ProjectEnvReconcileAckFrame
4989
5186
  | ProjectEnvDeliveryAckFrame
5187
+ | TaskSecretApplied
5188
+ | TaskSecretStagingAck
4990
5189
  | ProjectEnvStagingAckFrame
4991
5190
  | ProjectEnvLegacyTagAckFrame
4992
5191
  | ProjectEnvReplicaAckFrame
@@ -5036,12 +5235,14 @@ export type HostEvent =
5036
5235
  }
5037
5236
  | {
5038
5237
  kind: "agent.message_delta";
5238
+ evidence?: AssignmentTurnEvidence;
5039
5239
  taskId: string;
5040
5240
  agentId: string;
5041
5241
  chunk: string;
5042
5242
  }
5043
5243
  | {
5044
5244
  kind: "agent.message_complete";
5245
+ evidence?: AssignmentTurnEvidence;
5045
5246
  taskId: string;
5046
5247
  agentId: string;
5047
5248
  fullText: string;
@@ -5055,6 +5256,7 @@ export type HostEvent =
5055
5256
  // of delivering it.
5056
5257
  | {
5057
5258
  kind: "agent.turn_complete";
5259
+ evidence?: AssignmentTurnEvidence;
5058
5260
  taskId: string;
5059
5261
  agentId: string;
5060
5262
  aborted?: boolean;
@@ -0,0 +1,28 @@
1
+ import { ENGINE_ENV_CREDENTIAL_KEYS } from "./cloud-credentials";
2
+
3
+ const RESERVED_TASK_ENV_EXACT = new Set([
4
+ "GH_TOKEN", "GITHUB_TOKEN", "GH_CONFIG_DIR", "GH_HOST",
5
+ "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY",
6
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_INDEX_FILE", "GIT_EXEC_PATH",
7
+ "GIT_ASKPASS", "SSH_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND",
8
+ "HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME",
9
+ "PATH", "LD_PRELOAD", "LD_AUDIT", "LD_LIBRARY_PATH", "NODE_OPTIONS",
10
+ "NODE_PATH", "SHELL", "SHELLOPTS", "BASHOPTS", "PS4", "BASH_ENV", "ENV",
11
+ "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE",
12
+ "__CURSOR_SANDBOX_ENV_RESTORE", "GROK_LOGIN_ENV",
13
+ // Every name an env-token engine's account may set (ADR-154's contract):
14
+ // a project value never stands in for an engine's credential.
15
+ ...Object.values(ENGINE_ENV_CREDENTIAL_KEYS).flat(),
16
+ "PLAYWRIGHT_BROWSERS_PATH",
17
+ ]);
18
+ const RESERVED_TASK_ENV_NPM = /^npm_config_(prefix|cache|logs[_-]dir|umask)$/i;
19
+ /** Toolchain prefixes, in any case, as task-up.sh matches them. */
20
+ const RESERVED_TASK_ENV_TOOLCHAIN = /^(asdf|corepack)_/i;
21
+
22
+ export function isReservedTaskEnvName(name: string): boolean {
23
+ if (RESERVED_TASK_ENV_EXACT.has(name)) return true;
24
+ if (name.startsWith("GIT_CONFIG") || RESERVED_TASK_ENV_TOOLCHAIN.test(name)) return true;
25
+ if (name.startsWith("UAI_")) return true;
26
+ if (name.startsWith("__UAI_ENV_")) return true;
27
+ return RESERVED_TASK_ENV_NPM.test(name);
28
+ }
@@ -0,0 +1,75 @@
1
+ /** ADR-161 §5: value-free task-secret wire contract. No I/O. */
2
+ export const TASK_SECRETS_FEATURE = "task-secrets-v1";
3
+ export const MAX_TASK_SECRETS = 20;
4
+ export const TASK_SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
5
+ export const TASK_SECRET_STAGING_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
6
+ export interface TaskSecretManifestEntry {
7
+ credentialId: string;
8
+ name: string;
9
+ stagingId: string;
10
+ generation: number;
11
+ parameter: string;
12
+ version: number;
13
+ mode: "proxied" | "delivered";
14
+ domains: string[];
15
+ placeholder: { prefix: string; totalLength: number } | null;
16
+ sessionId: string;
17
+ }
18
+ export interface TaskSecretStagingPut {
19
+ kind: "task.secret.staging.put";
20
+ opId: string;
21
+ orgId: string;
22
+ taskId: string;
23
+ name: string;
24
+ stagingId: string;
25
+ value: string;
26
+ }
27
+ export interface TaskSecretStagingAck {
28
+ kind: "task.secret.staging.ack";
29
+ opId: string;
30
+ stagingId: string;
31
+ ok: boolean;
32
+ }
33
+ const ID = /^[A-Za-z0-9_-]{1,128}$/;
34
+ export function isTaskSecretStagingPut(value: unknown): value is TaskSecretStagingPut {
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
36
+ const o = value as Record<string, unknown>;
37
+ return Object.keys(o).length === 7 && o.kind === "task.secret.staging.put" &&
38
+ [o.opId, o.orgId, o.taskId].every(v => typeof v === "string" && ID.test(v)) &&
39
+ typeof o.name === "string" && TASK_SECRET_NAME.test(o.name) &&
40
+ typeof o.stagingId === "string" && TASK_SECRET_STAGING_ID.test(o.stagingId) &&
41
+ typeof o.value === "string" && o.value.length > 0 && !o.value.includes("\0") &&
42
+ new TextEncoder().encode(JSON.stringify({ v: 1, kind: "task-secret", orgId: o.orgId, taskId: o.taskId, name: o.name, stagingId: o.stagingId, value: o.value })).byteLength <= 4096;
43
+ }
44
+ export function isTaskSecretStagingAck(value: unknown): value is TaskSecretStagingAck {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
46
+ const o = value as Record<string, unknown>;
47
+ return Object.keys(o).length === 4 && o.kind === "task.secret.staging.ack" && typeof o.opId === "string" && ID.test(o.opId) &&
48
+ typeof o.stagingId === "string" && TASK_SECRET_STAGING_ID.test(o.stagingId) && typeof o.ok === "boolean";
49
+ }
50
+ export interface TaskSecretMaterial {
51
+ class: "task-secret";
52
+ location: { orgId: string; taskId: string; name: string; stagingId: string };
53
+ value: string;
54
+ }
55
+ /** Compare every envelope identity; no provider text is included in errors. */
56
+ export function parseTaskSecretMaterial(raw: string, expected: TaskSecretMaterial["location"]): TaskSecretMaterial | null {
57
+ try {
58
+ if (new TextEncoder().encode(raw).byteLength > 4096) return null;
59
+ const o = JSON.parse(raw) as Record<string, unknown>;
60
+ if (!o || Object.keys(o).length !== 7 || o.v !== 1 || o.kind !== "task-secret" ||
61
+ o.orgId !== expected.orgId || o.taskId !== expected.taskId || o.name !== expected.name || o.stagingId !== expected.stagingId ||
62
+ typeof o.value !== "string" || !o.value.length || o.value.includes("\0")) return null;
63
+ return { class: "task-secret", location: { ...expected }, value: o.value };
64
+ } catch { return null; }
65
+ }
66
+
67
+ /** Sent only after the lease's consumers have opened on the exact new view. */
68
+ export interface TaskSecretApplied { kind: "task.secret.applied"; taskId: string; runEpoch: number; manifestRevision: number; manifestDigest: string }
69
+ export function isTaskSecretApplied(value: unknown): value is TaskSecretApplied {
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
71
+ const o = value as Record<string, unknown>;
72
+ return Object.keys(o).length === 5 && o.kind === "task.secret.applied" && typeof o.taskId === "string" && ID.test(o.taskId) &&
73
+ typeof o.runEpoch === "number" && Number.isSafeInteger(o.runEpoch) && o.runEpoch > 0 &&
74
+ typeof o.manifestRevision === "number" && Number.isSafeInteger(o.manifestRevision) && o.manifestRevision > 0 && typeof o.manifestDigest === "string" && /^[0-9a-f]{64}$/.test(o.manifestDigest);
75
+ }
@@ -0,0 +1,41 @@
1
+ import { ENGINE_ACCOUNT_PATH_RULES } from "./engine-account-paths";
2
+ export { ENGINE_ACCOUNT_PATH_RULES } from "./engine-account-paths";
3
+ /** Shared by cloud validation and runner reads. Check normalized and lexical
4
+ * components: normalization must never erase a traversal through credentials. */
5
+ export function normalizedWorkspacePath(input: string): string {
6
+ if (!input.startsWith("/workspace/") || /[\x00-\x1f\x7f\\]/.test(input)) throw new Error("Use an absolute path under /workspace.");
7
+ const parts: string[] = [];
8
+ for (const part of input.split("/").slice(2)) {
9
+ if (!part || part === ".") continue;
10
+ if (part === "..") { if (!parts.length) throw new Error("Path escapes /workspace."); parts.pop(); }
11
+ else parts.push(part);
12
+ }
13
+ if (!parts.length) throw new Error("Choose a file under /workspace.");
14
+ return `/workspace/${parts.join("/")}`;
15
+ }
16
+ export function assertShareablePath(input: string): string { return validateShareablePath(input, ENGINE_ACCOUNT_PATH_RULES); }
17
+ /** Kept self-contained for embedding together with its data in the agent CLI. */
18
+ export function validateShareablePath(input: string, engineRules: ReadonlyArray<{ components: readonly string[]; prefix: boolean }>): string {
19
+ if (/[\x00-\x1f\x7f\\]/.test(input)) throw new Error("This path cannot be shared.");
20
+ if (!input.startsWith("/")) throw new Error("Use an absolute file path.");
21
+ const lexical = input.split("/").filter(Boolean);
22
+ const parts: string[] = [];
23
+ for (const part of lexical) {
24
+ if (part === "..") parts.pop(); else if (part !== ".") parts.push(part);
25
+ // Refuse entering these roots even when later '..' components erase them.
26
+ if (engineRules.some((rule) => parts.some((_, i) => rule.components.every((component, offset) => offset === rule.components.length - 1 && rule.prefix ? (parts[i + offset] ?? "").startsWith(component) : parts[i + offset] === component)))) throw new Error("Credential and Uai internal paths cannot be shared.");
27
+ if (["proc", "sys"].includes(parts[0] ?? "") || (parts[0] === "run" && ((parts[1] ?? "").startsWith("uai") || parts[1] === "secrets"))) throw new Error("Credential and Uai internal paths cannot be shared.");
28
+ }
29
+ const normalized = `/${parts.join("/")}`;
30
+ // Keep this function self-contained: its source is embedded into the
31
+ // dependency-free CLI. Inline callbacks need no compiler name helpers.
32
+ for (const components of [lexical, parts]) {
33
+ if (components.some((part, i) =>
34
+ (part === ".env" || (part.startsWith(".env.") && ![".env.example", ".env.schema"].includes(part))) ||
35
+ part.startsWith(".codex") ||
36
+ [".git", ".claude", ".ssh", ".grok", ".cursor", ".opencode", ".claude.json", ".git-credentials", ".netrc", ".npmrc", ".pypirc", ".aws", ".docker", ".kube", ".gnupg"].includes(part) ||
37
+ (part === ".config" && components[i + 1] === "gh") ||
38
+ (part === ".uai" && components[i + 1] !== "attachments"))) throw new Error("Credential and Uai internal paths cannot be shared.");
39
+ }
40
+ return normalized;
41
+ }