@runuai/host 0.9.14 → 0.9.42
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/README.md +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +33 -2
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3060 -218
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1405 -107
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +766 -42
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-level task boundary (ADR-101 Phase C).
|
|
3
|
+
*
|
|
4
|
+
* Providers own machine/container details. Generic orchestration receives an
|
|
5
|
+
* opaque, durable locator and a handle with bounded argv-based operations; it
|
|
6
|
+
* never receives a Docker socket, Compose project, or generic remote shell.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION = 1 as const;
|
|
10
|
+
|
|
11
|
+
export interface TaskEnvironmentLocator {
|
|
12
|
+
schemaVersion: typeof TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION;
|
|
13
|
+
/** Stable provider registry key, for example `docker-compose`. */
|
|
14
|
+
provider: string;
|
|
15
|
+
/** Provider-owned, JSON-serializable reconstruction payload. */
|
|
16
|
+
value: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TaskEnvironmentDescriptor {
|
|
20
|
+
taskId: string;
|
|
21
|
+
locator: TaskEnvironmentLocator;
|
|
22
|
+
/** Absolute path as observed inside the environment. */
|
|
23
|
+
workspacePath: string;
|
|
24
|
+
codeServerPort?: number;
|
|
25
|
+
previewPorts?: Array<{ name: string; hostPort: number }>;
|
|
26
|
+
initWarning?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TaskEnvironmentExecRequest {
|
|
30
|
+
/** Executable followed by literal argv. Shell command strings are forbidden. */
|
|
31
|
+
argv: readonly [string, ...string[]];
|
|
32
|
+
cwd?: string;
|
|
33
|
+
user?: string;
|
|
34
|
+
/** Host environment names inherited by name, without copying values into argv. */
|
|
35
|
+
inheritEnv?: readonly string[];
|
|
36
|
+
env?: Readonly<Record<string, string>>;
|
|
37
|
+
stdin?: Uint8Array;
|
|
38
|
+
timeoutMs: number;
|
|
39
|
+
maxOutputBytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TaskEnvironmentExecResult {
|
|
43
|
+
exitCode: number | null;
|
|
44
|
+
signal: string | null;
|
|
45
|
+
stdout: Uint8Array;
|
|
46
|
+
stderr: Uint8Array;
|
|
47
|
+
stdoutTruncated: boolean;
|
|
48
|
+
stderrTruncated: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TaskEnvironmentSpawnRequest {
|
|
52
|
+
argv: readonly [string, ...string[]];
|
|
53
|
+
cwd?: string;
|
|
54
|
+
user?: string;
|
|
55
|
+
/** Host environment names inherited by name, without copying values into argv. */
|
|
56
|
+
inheritEnv?: readonly string[];
|
|
57
|
+
env?: Readonly<Record<string, string>>;
|
|
58
|
+
timeoutMs: number;
|
|
59
|
+
maxOutputBytes: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Long-lived process request used only by agent-session adapters. Its
|
|
64
|
+
* lifetime is owned by the session and therefore has no wall-clock timeout;
|
|
65
|
+
* unread stdout/stderr remains bounded by `maxOutputBytes`.
|
|
66
|
+
*/
|
|
67
|
+
export interface TaskEnvironmentSessionRequest {
|
|
68
|
+
argv: readonly [string, ...string[]];
|
|
69
|
+
cwd?: string;
|
|
70
|
+
user?: string;
|
|
71
|
+
inheritEnv?: readonly string[];
|
|
72
|
+
env?: Readonly<Record<string, string>>;
|
|
73
|
+
maxOutputBytes: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A bounded control-plane launch of a detached durable-session runner. */
|
|
77
|
+
export interface TaskEnvironmentDetachedSessionRequest
|
|
78
|
+
extends TaskEnvironmentSessionRequest {
|
|
79
|
+
launchTimeoutMs: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TaskEnvironmentProcessExit {
|
|
83
|
+
exitCode: number | null;
|
|
84
|
+
signal: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Streaming process primitive used by durable agent sessions. Providers must
|
|
89
|
+
* keep output buffering within `maxOutputBytes`; AsyncIterable does not imply
|
|
90
|
+
* permission to retain an unbounded unread stream.
|
|
91
|
+
*/
|
|
92
|
+
export interface TaskEnvironmentProcess {
|
|
93
|
+
readonly stdout: AsyncIterable<Uint8Array>;
|
|
94
|
+
readonly stderr: AsyncIterable<Uint8Array>;
|
|
95
|
+
readonly completion: Promise<TaskEnvironmentProcessExit>;
|
|
96
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
97
|
+
closeInput(): Promise<void>;
|
|
98
|
+
terminate(signal?: "SIGTERM" | "SIGKILL"): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Provider-neutral surface handed to agent adapters. `durableIdentity` is an
|
|
103
|
+
* opaque diagnostic/persistence value; callers must never parse it as a
|
|
104
|
+
* container or machine identifier.
|
|
105
|
+
*/
|
|
106
|
+
export interface TaskEnvironmentAgentSessionSurface {
|
|
107
|
+
readonly descriptor: TaskEnvironmentDescriptor;
|
|
108
|
+
readonly durableIdentity: string;
|
|
109
|
+
spawnSession(
|
|
110
|
+
request: TaskEnvironmentSessionRequest,
|
|
111
|
+
): Promise<TaskEnvironmentProcess>;
|
|
112
|
+
launchDetachedSession(
|
|
113
|
+
request: TaskEnvironmentDetachedSessionRequest,
|
|
114
|
+
): Promise<TaskEnvironmentExecResult>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type TaskEnvironmentCopyRequest =
|
|
118
|
+
| {
|
|
119
|
+
direction: "into";
|
|
120
|
+
/** Absolute host path. */
|
|
121
|
+
source: string;
|
|
122
|
+
/** Absolute environment path. */
|
|
123
|
+
destination: string;
|
|
124
|
+
}
|
|
125
|
+
| {
|
|
126
|
+
direction: "out";
|
|
127
|
+
/** Absolute environment path. */
|
|
128
|
+
source: string;
|
|
129
|
+
/** Absolute host path. */
|
|
130
|
+
destination: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export interface TaskEnvironmentPort {
|
|
134
|
+
name?: string;
|
|
135
|
+
containerPort: number;
|
|
136
|
+
host: "127.0.0.1";
|
|
137
|
+
hostPort: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type TaskEnvironmentStatus =
|
|
141
|
+
| {
|
|
142
|
+
state: "running" | "stopped" | "absent";
|
|
143
|
+
/** Provider-level runtime identities, mapped to the legacy wire's
|
|
144
|
+
* `containers` field while that protocol remains in service. */
|
|
145
|
+
instances: string[];
|
|
146
|
+
workspacePresent: boolean;
|
|
147
|
+
}
|
|
148
|
+
| { state: "unknown"; detail: string };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Recovery is allowed to perform provider-private repair, but its caller owns
|
|
152
|
+
* the surrounding task/channel lifecycle. These callbacks keep that ordering
|
|
153
|
+
* explicit without exposing a Docker container, Compose project, or VM
|
|
154
|
+
* identity to generic orchestration.
|
|
155
|
+
*/
|
|
156
|
+
export interface TaskEnvironmentRecoveryContext {
|
|
157
|
+
/** One-way host maintenance latch (standard image, shared CLI state, etc.). */
|
|
158
|
+
maintenanceReady: Promise<void>;
|
|
159
|
+
/** Provider work must stop before every externally-visible side effect when
|
|
160
|
+
* this generation is no longer current. */
|
|
161
|
+
isCurrent(): boolean;
|
|
162
|
+
/** Drain/tombstone consumers before stopping or repairing a live writer. */
|
|
163
|
+
quarantineConsumers?: () => Promise<void>;
|
|
164
|
+
/** Re-open admission only after the environment is proven operational. */
|
|
165
|
+
allowConsumers?: () => void;
|
|
166
|
+
/** Recreate durable consumers after admission has reopened. */
|
|
167
|
+
ensureConsumersStarted?: () => Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type TaskEnvironmentRecoveryResult =
|
|
171
|
+
| {
|
|
172
|
+
outcome: "recovered";
|
|
173
|
+
/** Optional fresh provider status. Recovery itself is the authoritative
|
|
174
|
+
* proof; callers must not issue a second inventory probe merely to fill
|
|
175
|
+
* this field. */
|
|
176
|
+
status?: TaskEnvironmentStatus;
|
|
177
|
+
descriptor?: TaskEnvironmentDescriptor;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
outcome: "deferred";
|
|
181
|
+
/** Bounded, operator-safe reason. Deferred recovery is retryable and
|
|
182
|
+
* must never be interpreted as absence. */
|
|
183
|
+
detail: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export interface TaskEnvironmentHandle<TTeardownResult = void>
|
|
187
|
+
extends TaskEnvironmentAgentSessionSurface {
|
|
188
|
+
readonly descriptor: TaskEnvironmentDescriptor;
|
|
189
|
+
exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult>;
|
|
190
|
+
spawn(request: TaskEnvironmentSpawnRequest): Promise<TaskEnvironmentProcess>;
|
|
191
|
+
copy(request: TaskEnvironmentCopyRequest): Promise<void>;
|
|
192
|
+
ports(): Promise<TaskEnvironmentPort[]>;
|
|
193
|
+
status(): Promise<TaskEnvironmentStatus>;
|
|
194
|
+
recover(
|
|
195
|
+
context: TaskEnvironmentRecoveryContext,
|
|
196
|
+
): Promise<TaskEnvironmentRecoveryResult>;
|
|
197
|
+
/** Resumable pause: halt the environment's processes while keeping every
|
|
198
|
+
* durable resource (worktree, volumes, container state) so a later
|
|
199
|
+
* provision/recovery can resume it. Idempotent for an already-stopped
|
|
200
|
+
* environment. */
|
|
201
|
+
stop(): Promise<void>;
|
|
202
|
+
/** Idempotent: success means provider-owned runtime resources are absent. */
|
|
203
|
+
teardown(): Promise<TTeardownResult>;
|
|
204
|
+
/** Direct-route inspection for backends whose container IPs are
|
|
205
|
+
* host-routable (ADR-106 Apple vmnet). Providers that publish host ports
|
|
206
|
+
* instead simply omit it. The proof must bind the answer to this exact
|
|
207
|
+
* task's container (label check), not merely a name lookup. */
|
|
208
|
+
inspectRoute?(): Promise<
|
|
209
|
+
| { kind: "running"; ipv4Address: string }
|
|
210
|
+
| { kind: "unavailable" }
|
|
211
|
+
| { kind: "unknown"; detail: string }
|
|
212
|
+
>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TaskEnvironmentProvisionRequest<TInput, TCredentials> {
|
|
216
|
+
taskId: string;
|
|
217
|
+
input: TInput;
|
|
218
|
+
credentials: TCredentials;
|
|
219
|
+
/**
|
|
220
|
+
* Awaited after the provider has derived and validated its durable identity,
|
|
221
|
+
* but before it creates any external resources. A crash after this callback
|
|
222
|
+
* can therefore be recovered from the opaque locator alone.
|
|
223
|
+
*/
|
|
224
|
+
onPrepared?: (locator: TaskEnvironmentLocator) => Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface TaskEnvironmentProvisioned<TResult, TTeardownResult = void> {
|
|
228
|
+
handle: TaskEnvironmentHandle<TTeardownResult>;
|
|
229
|
+
/** Provider launch result retained for rolling wire compatibility. */
|
|
230
|
+
result: TResult;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface TaskEnvironmentProvider<
|
|
234
|
+
TInput,
|
|
235
|
+
TCredentials,
|
|
236
|
+
TResult,
|
|
237
|
+
TTeardownResult = void,
|
|
238
|
+
> {
|
|
239
|
+
readonly kind: string;
|
|
240
|
+
provision(
|
|
241
|
+
request: TaskEnvironmentProvisionRequest<TInput, TCredentials>,
|
|
242
|
+
): Promise<TaskEnvironmentProvisioned<TResult, TTeardownResult>>;
|
|
243
|
+
reconstruct(
|
|
244
|
+
locator: TaskEnvironmentLocator,
|
|
245
|
+
): Promise<TaskEnvironmentHandle<TTeardownResult>>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
249
|
+
|
|
250
|
+
/** Shared admission guard for provider implementations and fake providers. */
|
|
251
|
+
export function assertTaskEnvironmentProcessRequest(
|
|
252
|
+
request: TaskEnvironmentExecRequest | TaskEnvironmentSpawnRequest,
|
|
253
|
+
): void {
|
|
254
|
+
assertTaskEnvironmentSessionRequest(request);
|
|
255
|
+
if (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs <= 0) {
|
|
256
|
+
throw new Error("task environment timeout must be a positive integer");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function assertTaskEnvironmentSessionRequest(
|
|
261
|
+
request:
|
|
262
|
+
| TaskEnvironmentSessionRequest
|
|
263
|
+
| TaskEnvironmentDetachedSessionRequest
|
|
264
|
+
| TaskEnvironmentExecRequest
|
|
265
|
+
| TaskEnvironmentSpawnRequest,
|
|
266
|
+
): void {
|
|
267
|
+
if (
|
|
268
|
+
!Array.isArray(request.argv) ||
|
|
269
|
+
request.argv.length === 0 ||
|
|
270
|
+
request.argv.some(
|
|
271
|
+
(part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
throw new Error("task environment argv must contain nonempty NUL-free strings");
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
request.cwd !== undefined &&
|
|
278
|
+
(!request.cwd.startsWith("/") || request.cwd.includes("\0"))
|
|
279
|
+
) {
|
|
280
|
+
throw new Error("task environment cwd must be an absolute NUL-free path");
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
request.user !== undefined &&
|
|
284
|
+
(request.user.length === 0 || request.user.includes("\0"))
|
|
285
|
+
) {
|
|
286
|
+
throw new Error("task environment user must be a nonempty NUL-free value");
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
request.inheritEnv !== undefined &&
|
|
290
|
+
(!Array.isArray(request.inheritEnv) ||
|
|
291
|
+
request.inheritEnv.length > 128 ||
|
|
292
|
+
new Set(request.inheritEnv).size !== request.inheritEnv.length ||
|
|
293
|
+
request.inheritEnv.some(
|
|
294
|
+
(name) => typeof name !== "string" || !ENV_NAME.test(name),
|
|
295
|
+
))
|
|
296
|
+
) {
|
|
297
|
+
throw new Error("task environment inherited env names are invalid");
|
|
298
|
+
}
|
|
299
|
+
for (const [name, value] of Object.entries(request.env ?? {})) {
|
|
300
|
+
if (
|
|
301
|
+
!ENV_NAME.test(name) ||
|
|
302
|
+
typeof value !== "string" ||
|
|
303
|
+
value.includes("\0")
|
|
304
|
+
) {
|
|
305
|
+
throw new Error("task environment env must contain valid NUL-free entries");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
!Number.isSafeInteger(request.maxOutputBytes) ||
|
|
310
|
+
request.maxOutputBytes <= 0
|
|
311
|
+
) {
|
|
312
|
+
throw new Error("task environment output limit must be a positive integer");
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
"launchTimeoutMs" in request &&
|
|
316
|
+
(!Number.isSafeInteger(request.launchTimeoutMs) ||
|
|
317
|
+
request.launchTimeoutMs <= 0)
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"task environment detached launch timeout must be a positive integer",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function serializeTaskEnvironmentLocator(
|
|
326
|
+
locator: TaskEnvironmentLocator,
|
|
327
|
+
): string {
|
|
328
|
+
validateTaskEnvironmentLocator(locator);
|
|
329
|
+
return JSON.stringify(locator);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function parseTaskEnvironmentLocator(raw: string): TaskEnvironmentLocator {
|
|
333
|
+
let parsed: unknown;
|
|
334
|
+
try {
|
|
335
|
+
parsed = JSON.parse(raw);
|
|
336
|
+
} catch {
|
|
337
|
+
throw new Error("task environment locator is not valid JSON");
|
|
338
|
+
}
|
|
339
|
+
return validateTaskEnvironmentLocator(parsed);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function validateTaskEnvironmentLocator(
|
|
343
|
+
value: unknown,
|
|
344
|
+
): TaskEnvironmentLocator {
|
|
345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
346
|
+
throw new Error("task environment locator must be an object");
|
|
347
|
+
}
|
|
348
|
+
const record = value as Record<string, unknown>;
|
|
349
|
+
if (
|
|
350
|
+
Object.keys(record).sort().join("\0") !==
|
|
351
|
+
["provider", "schemaVersion", "value"].sort().join("\0")
|
|
352
|
+
) {
|
|
353
|
+
throw new Error("task environment locator has unexpected fields");
|
|
354
|
+
}
|
|
355
|
+
if (record.schemaVersion !== TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION) {
|
|
356
|
+
throw new Error("unsupported task environment locator schema");
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
typeof record.provider !== "string" ||
|
|
360
|
+
!/^[a-z][a-z0-9-]{0,63}$/.test(record.provider)
|
|
361
|
+
) {
|
|
362
|
+
throw new Error("task environment locator provider is invalid");
|
|
363
|
+
}
|
|
364
|
+
assertJsonValue(record.value, 0);
|
|
365
|
+
return {
|
|
366
|
+
schemaVersion: TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
|
|
367
|
+
provider: record.provider,
|
|
368
|
+
value: record.value,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function assertJsonValue(value: unknown, depth: number): void {
|
|
373
|
+
if (depth > 16) throw new Error("task environment locator is too deeply nested");
|
|
374
|
+
if (
|
|
375
|
+
value === null ||
|
|
376
|
+
typeof value === "string" ||
|
|
377
|
+
typeof value === "boolean"
|
|
378
|
+
) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (typeof value === "number") {
|
|
382
|
+
if (!Number.isFinite(value)) {
|
|
383
|
+
throw new Error("task environment locator contains a non-finite number");
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
if (value.length > 256) {
|
|
389
|
+
throw new Error("task environment locator array is too large");
|
|
390
|
+
}
|
|
391
|
+
for (const item of value) assertJsonValue(item, depth + 1);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (value && typeof value === "object") {
|
|
395
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
396
|
+
if (entries.length > 256) {
|
|
397
|
+
throw new Error("task environment locator object is too large");
|
|
398
|
+
}
|
|
399
|
+
for (const [key, item] of entries) {
|
|
400
|
+
if (key.includes("\0")) {
|
|
401
|
+
throw new Error("task environment locator key contains NUL");
|
|
402
|
+
}
|
|
403
|
+
assertJsonValue(item, depth + 1);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
throw new Error("task environment locator is not JSON-serializable");
|
|
408
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS } from "../src/protocol";
|
|
2
|
+
|
|
3
|
+
/** One identity grammar for inventory cursors and every orphan-GC path/label. */
|
|
4
|
+
export const SAFE_HOST_TASK_ID = /^[a-z0-9][a-z0-9_-]*$/;
|
|
5
|
+
|
|
6
|
+
export function isSafeHostTaskId(value: unknown): value is string {
|
|
7
|
+
return (
|
|
8
|
+
typeof value === "string" &&
|
|
9
|
+
value.length > 0 &&
|
|
10
|
+
value.length <= MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS &&
|
|
11
|
+
SAFE_HOST_TASK_ID.test(value)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertSafeHostTaskId(value: unknown): asserts value is string {
|
|
16
|
+
if (!isSafeHostTaskId(value)) {
|
|
17
|
+
throw new Error("invalid host task identity");
|
|
18
|
+
}
|
|
19
|
+
}
|