@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
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
|
|
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
|
+
}
|