@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/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" };
@@ -4314,6 +4337,114 @@ export interface TaskDiffInput {
4314
4337
  projects: Array<{ id: string; slug: string }>;
4315
4338
  }
4316
4339
 
4340
+ /**
4341
+ * ADR-161 §2.3: every live-project command names the task's run, the
4342
+ * operation, and the project-set revision the operation commits. The runner
4343
+ * refuses a command for a run it has not admitted, and any Prepare or
4344
+ * Activate for a revision older than one it has already activated or
4345
+ * compensated.
4346
+ */
4347
+ export interface TaskProjectOpFence {
4348
+ taskId: string;
4349
+ runEpoch: number;
4350
+ operationId: string;
4351
+ projectSetRevision: number;
4352
+ }
4353
+
4354
+ export interface TaskProjectPrepareInput extends TaskProjectOpFence {
4355
+ /** The task branch the clone checks out (from origin when it exists). */
4356
+ branch: string;
4357
+ /** `toolVersions` is the project's fallback `.tool-versions`, seeded into
4358
+ * a clone that ships none, exactly as task-up seeds a startup project. */
4359
+ project: { id: string; slug: string; repoUrl: string; toolVersions?: string };
4360
+ }
4361
+
4362
+ /** Why a live-project command did nothing. `grant_not_installed` and
4363
+ * `busy` are retryable; the rest are final for that operation. */
4364
+ export type TaskProjectRefusal =
4365
+ | "repo_access_denied"
4366
+ | "clone_failed"
4367
+ | "workspace_path_taken"
4368
+ | "stale"
4369
+ | "not_running"
4370
+ | "compensated"
4371
+ | "not_prepared"
4372
+ | "grant_not_installed"
4373
+ | "published"
4374
+ | "busy"
4375
+ /** The running environment cannot take a live project (no env spec or no
4376
+ * live env gateway): the cloud answers REQUIRES_TASK_RESTART. */
4377
+ | "unsupported_environment"
4378
+ /** Published, but a required step (Git root, env install) did not
4379
+ * complete; Activate repairs it when repeated. Never terminal. */
4380
+ | "incomplete";
4381
+
4382
+ export type TaskProjectPrepareResult =
4383
+ | {
4384
+ outcome: "prepared";
4385
+ head: string | null;
4386
+ /** The `.env.schema` observation read from the fresh clone, stored on
4387
+ * the runner before the project is exposed to anything (ADR-153 §4). */
4388
+ envSchema: ProjectEnvSchemaObservation | null;
4389
+ }
4390
+ | { outcome: "refused"; reason: TaskProjectRefusal; message: string };
4391
+
4392
+ export interface TaskProjectActivateInput extends TaskProjectOpFence {
4393
+ /** The full project, for the task's env spec and the agents' notice. */
4394
+ project: TaskCommandProject;
4395
+ /** The cloud-computed preview URL literals of this project's declared
4396
+ * previews (`urlEnv`), added to the task's env for names it lacks. */
4397
+ previewEnv: Record<string, string>;
4398
+ /** The exact grant the runner must hold before it publishes; null for a
4399
+ * task on the runner's own credentials, which has no per-task grant. */
4400
+ grant: { manifestRevision: number; manifestDigest: string; targetGeneration: number } | null;
4401
+ }
4402
+
4403
+ export type TaskProjectActivateResult =
4404
+ | { outcome: "ready" | "ready_with_warning"; warning: string | null }
4405
+ | { outcome: "refused"; reason: TaskProjectRefusal; message: string }
4406
+ /** The intent was recorded but the disk proves neither outcome. */
4407
+ | { outcome: "uncertain"; message: string };
4408
+
4409
+ export type TaskProjectCompensateInput = TaskProjectOpFence;
4410
+
4411
+ export type TaskProjectCompensateResult =
4412
+ | { outcome: "compensated" }
4413
+ | { outcome: "refused"; reason: TaskProjectRefusal | "uncertain"; message: string };
4414
+
4415
+ export interface TaskProjectStatusInput {
4416
+ taskId: string;
4417
+ runEpoch: number;
4418
+ operationId: string;
4419
+ projectId: string;
4420
+ }
4421
+
4422
+ /** The runner's journal for one operation, with any activation intent
4423
+ * resolved from the disk first. */
4424
+ export type TaskProjectMilestone =
4425
+ | "none"
4426
+ | "prepared"
4427
+ | "not_published"
4428
+ | "published"
4429
+ | "uncertain"
4430
+ | "ready"
4431
+ | "ready_with_warning"
4432
+ | "compensated";
4433
+
4434
+ export interface TaskProjectStatusResult {
4435
+ milestone: TaskProjectMilestone;
4436
+ warning: string | null;
4437
+ /** The grant this run holds right now (cloud credentials only). */
4438
+ installedGrant: { runEpoch: number; manifestRevision: number; manifestDigest: string } | null;
4439
+ /** Whether the task's env can serve `projectId` now: the open grant's
4440
+ * replica names it, or the runner serves its own store. */
4441
+ envServes: boolean;
4442
+ /** The projects the run's env serves right now (cloud credentials: the
4443
+ * open grant's, once its replica can serve them all); null otherwise. */
4444
+ servedProjectIds: string[] | null;
4445
+ credentials: "cloud" | "own";
4446
+ }
4447
+
4317
4448
  /** A human in the task chat (ADR-049) — owner or invited collaborator. */
4318
4449
  export interface ChannelHuman {
4319
4450
  /** Per-task mention handle (`@diogo`), unique vs agent ids. */
@@ -4322,7 +4453,10 @@ export interface ChannelHuman {
4322
4453
  isOwner: boolean;
4323
4454
  }
4324
4455
 
4456
+
4325
4457
  export interface ChannelEnsureInput {
4458
+ /** Server-issued immutable lead context; old hosts leave measurements unknown. */
4459
+ leadRunId?: string;
4326
4460
  taskId: string;
4327
4461
  agents: TaskAgent[];
4328
4462
  /** ADR-083: channel projection/routing mode. Optional for older clouds. */
@@ -4496,6 +4630,7 @@ export interface HostCommands {
4496
4630
  * redeliveries are not messages a router would ever classify. Absent
4497
4631
  * from an older cloud, which the host reads as "not a human turn". */
4498
4632
  origin?: DeliveryOrigin,
4633
+ assignment?: AssignmentDelivery,
4499
4634
  ): Promise<HostCommandResult<void>>;
4500
4635
  channelResolvePermission(
4501
4636
  ctx: CommandContext,
@@ -4559,10 +4694,28 @@ export interface HostCommands {
4559
4694
  ): Promise<HostCommandResult<TaskDiffResult>>;
4560
4695
  // Chat attachments live in the task workspace on the host (ADR-015); the
4561
4696
  // cloud relays bytes (base64) in and out, never persisting them.
4697
+ /** ADR-161 §2.3 (`live-task-projects-v1`). */
4698
+ taskProjectPrepare(
4699
+ ctx: CommandContext,
4700
+ input: TaskProjectPrepareInput,
4701
+ ): Promise<HostCommandResult<TaskProjectPrepareResult>>;
4702
+ taskProjectActivate(
4703
+ ctx: CommandContext,
4704
+ input: TaskProjectActivateInput,
4705
+ ): Promise<HostCommandResult<TaskProjectActivateResult>>;
4706
+ taskProjectCompensate(
4707
+ ctx: CommandContext,
4708
+ input: TaskProjectCompensateInput,
4709
+ ): Promise<HostCommandResult<TaskProjectCompensateResult>>;
4710
+ taskProjectStatus(
4711
+ ctx: CommandContext,
4712
+ input: TaskProjectStatusInput,
4713
+ ): Promise<HostCommandResult<TaskProjectStatusResult>>;
4562
4714
  attachmentWrite(
4563
4715
  ctx: CommandContext,
4564
4716
  input: { taskId: string; filename: string; dataBase64: string },
4565
4717
  ): Promise<HostCommandResult<void>>;
4718
+ workspaceFileRead(ctx: CommandContext, input: { taskId: string; path: string; maxBytes: number }): Promise<HostCommandResult<{ dataBase64: string; size: number }>>;
4566
4719
  attachmentRead(
4567
4720
  ctx: CommandContext,
4568
4721
  input: { taskId: string; filename: string },
@@ -4745,6 +4898,7 @@ export type CloudToHost =
4745
4898
  | ProjectEnvReconcileBeginFrame
4746
4899
  | ProjectEnvReconciledFrame
4747
4900
  | ProjectEnvDeliverySkipFrame
4901
+ | TaskSecretStagingPut
4748
4902
  | ProjectEnvStagingPutFrame
4749
4903
  | ProjectEnvLegacyTagFrame
4750
4904
  | ProjectEnvReplicaBeginFrame
@@ -4809,6 +4963,30 @@ export type CloudToHost =
4809
4963
  | SshMintFrame
4810
4964
  | GithubRevokeFrame;
4811
4965
 
4966
+ /** ADR-162: cloud-bound delivery identity, preserved by the runner journal. */
4967
+ export interface AssignmentDelivery {
4968
+ assignmentId: string;
4969
+ deliveryId: string;
4970
+ runEpoch: number;
4971
+ recipientGeneration: string | null;
4972
+ }
4973
+ export interface AssignmentTurnEvidence {
4974
+ leadRunId?: string | null;
4975
+ turnId: string;
4976
+ assignmentId: string | null;
4977
+ deliveryId: string | null;
4978
+ sessionId: string;
4979
+ runEpoch: number;
4980
+ recipientGeneration: string | null;
4981
+ correlation: "explicit" | "followup" | "ambiguous" | "unknown";
4982
+ acceptedAt: number;
4983
+ endedAt?: number;
4984
+ engine: string;
4985
+ runtimeVersion: string | null;
4986
+ // A configured model/effort isn't an observation. Actual model comes from
4987
+ // provider usage; unreported effort, CLI version and duration remain null.
4988
+ }
4989
+
4812
4990
  /** ADR-141: provenance of a channelDeliver. */
4813
4991
  export type DeliveryOrigin = "human" | "agent" | "system";
4814
4992
 
@@ -4987,6 +5165,8 @@ export type HostToCloud =
4987
5165
  | ProjectEnvAckFrame
4988
5166
  | ProjectEnvReconcileAckFrame
4989
5167
  | ProjectEnvDeliveryAckFrame
5168
+ | TaskSecretApplied
5169
+ | TaskSecretStagingAck
4990
5170
  | ProjectEnvStagingAckFrame
4991
5171
  | ProjectEnvLegacyTagAckFrame
4992
5172
  | ProjectEnvReplicaAckFrame
@@ -5036,12 +5216,14 @@ export type HostEvent =
5036
5216
  }
5037
5217
  | {
5038
5218
  kind: "agent.message_delta";
5219
+ evidence?: AssignmentTurnEvidence;
5039
5220
  taskId: string;
5040
5221
  agentId: string;
5041
5222
  chunk: string;
5042
5223
  }
5043
5224
  | {
5044
5225
  kind: "agent.message_complete";
5226
+ evidence?: AssignmentTurnEvidence;
5045
5227
  taskId: string;
5046
5228
  agentId: string;
5047
5229
  fullText: string;
@@ -5055,6 +5237,7 @@ export type HostEvent =
5055
5237
  // of delivering it.
5056
5238
  | {
5057
5239
  kind: "agent.turn_complete";
5240
+ evidence?: AssignmentTurnEvidence;
5058
5241
  taskId: string;
5059
5242
  agentId: string;
5060
5243
  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
+ }