@runuai/host 0.9.148 → 0.9.150
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +408 -81
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +939 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2219 -15
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
export const TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION = 1 as const;
|
|
10
10
|
|
|
11
|
+
/** Durable rollout generation for project environment delivery (ADR-153). */
|
|
12
|
+
export type ProjectEnvMode = "baked" | "loader";
|
|
13
|
+
|
|
14
|
+
/** Every process launch is explicitly classified at the type boundary. */
|
|
15
|
+
export type ProjectEnvProfile = "maintenance" | "task";
|
|
16
|
+
|
|
11
17
|
export interface TaskEnvironmentLocator {
|
|
12
18
|
schemaVersion: typeof TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION;
|
|
13
19
|
/** Stable provider registry key, for example `docker-compose`. */
|
|
@@ -19,6 +25,8 @@ export interface TaskEnvironmentLocator {
|
|
|
19
25
|
export interface TaskEnvironmentDescriptor {
|
|
20
26
|
taskId: string;
|
|
21
27
|
locator: TaskEnvironmentLocator;
|
|
28
|
+
/** Persisted independently from the provider locator for rollout safety. */
|
|
29
|
+
envMode?: ProjectEnvMode;
|
|
22
30
|
/** Absolute path as observed inside the environment. */
|
|
23
31
|
workspacePath: string;
|
|
24
32
|
codeServerPort?: number;
|
|
@@ -34,9 +42,25 @@ export interface TaskEnvironmentExecRequest {
|
|
|
34
42
|
/** Host environment names inherited by name, without copying values into argv. */
|
|
35
43
|
inheritEnv?: readonly string[];
|
|
36
44
|
env?: Readonly<Record<string, string>>;
|
|
45
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
37
46
|
stdin?: Uint8Array;
|
|
38
47
|
timeoutMs: number;
|
|
39
48
|
maxOutputBytes: number;
|
|
49
|
+
/**
|
|
50
|
+
* Run with every capability, as root only: the secrets proxy's NAT step
|
|
51
|
+
* needs NET_ADMIN, which a task container does not have (ADR-153). Docker
|
|
52
|
+
* only (`docker exec --privileged`, one process, the container keeps its
|
|
53
|
+
* default capabilities); every other provider refuses it.
|
|
54
|
+
*/
|
|
55
|
+
privileged?: true;
|
|
56
|
+
/**
|
|
57
|
+
* Called by the provider at the last moment before the process starts,
|
|
58
|
+
* after anything it awaits first (a machine looks up its address);
|
|
59
|
+
* throwing refuses the exec unstarted. The secrets proxy's root-step
|
|
60
|
+
* fence rides on it, so a step's exec cannot start once its operation is
|
|
61
|
+
* no longer live, however long that lookup takes (ADR-153).
|
|
62
|
+
*/
|
|
63
|
+
admit?: () => void;
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
export interface TaskEnvironmentExecResult {
|
|
@@ -55,6 +79,7 @@ export interface TaskEnvironmentSpawnRequest {
|
|
|
55
79
|
/** Host environment names inherited by name, without copying values into argv. */
|
|
56
80
|
inheritEnv?: readonly string[];
|
|
57
81
|
env?: Readonly<Record<string, string>>;
|
|
82
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
58
83
|
timeoutMs: number;
|
|
59
84
|
maxOutputBytes: number;
|
|
60
85
|
}
|
|
@@ -70,6 +95,7 @@ export interface TaskEnvironmentSessionRequest {
|
|
|
70
95
|
user?: string;
|
|
71
96
|
inheritEnv?: readonly string[];
|
|
72
97
|
env?: Readonly<Record<string, string>>;
|
|
98
|
+
projectEnvProfile: ProjectEnvProfile;
|
|
73
99
|
maxOutputBytes: number;
|
|
74
100
|
}
|
|
75
101
|
|
|
@@ -161,6 +187,8 @@ export interface TaskEnvironmentRecoveryContext {
|
|
|
161
187
|
isCurrent(): boolean;
|
|
162
188
|
/** Drain/tombstone consumers before stopping or repairing a live writer. */
|
|
163
189
|
quarantineConsumers?: () => Promise<void>;
|
|
190
|
+
/** Reassert task-local security/runtime prerequisites before admission. */
|
|
191
|
+
prepareConsumers?: () => Promise<void>;
|
|
164
192
|
/** Re-open admission only after the environment is proven operational. */
|
|
165
193
|
allowConsumers?: () => void;
|
|
166
194
|
/** Recreate durable consumers after admission has reopened. */
|
|
@@ -232,6 +260,8 @@ export interface TaskEnvironmentHandle<TTeardownResult = void>
|
|
|
232
260
|
|
|
233
261
|
export interface TaskEnvironmentProvisionRequest<TInput, TCredentials> {
|
|
234
262
|
taskId: string;
|
|
263
|
+
/** Omitted only by direct legacy/provider callers; new orchestration passes loader. */
|
|
264
|
+
envMode?: ProjectEnvMode;
|
|
235
265
|
input: TInput;
|
|
236
266
|
credentials: TCredentials;
|
|
237
267
|
/**
|
|
@@ -260,6 +290,7 @@ export interface TaskEnvironmentProvider<
|
|
|
260
290
|
): Promise<TaskEnvironmentProvisioned<TResult, TTeardownResult>>;
|
|
261
291
|
reconstruct(
|
|
262
292
|
locator: TaskEnvironmentLocator,
|
|
293
|
+
envMode?: ProjectEnvMode,
|
|
263
294
|
): Promise<TaskEnvironmentHandle<TTeardownResult>>;
|
|
264
295
|
}
|
|
265
296
|
|
|
@@ -288,6 +319,17 @@ export function assertWorkspaceScopedPath(
|
|
|
288
319
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
289
320
|
|
|
290
321
|
/** Shared admission guard for provider implementations and fake providers. */
|
|
322
|
+
/** A one-shot exec: the only request that may be privileged, and then only
|
|
323
|
+
* as root. Providers pass the privilege on explicitly, never by reading the
|
|
324
|
+
* request, so no other surface can carry it. */
|
|
325
|
+
export function assertTaskEnvironmentExecRequest(request: TaskEnvironmentExecRequest): void {
|
|
326
|
+
const { privileged, ...unprivileged } = request;
|
|
327
|
+
if (privileged !== undefined && (privileged !== true || request.user !== "root")) {
|
|
328
|
+
throw new Error("a privileged task environment exec must run as root");
|
|
329
|
+
}
|
|
330
|
+
assertTaskEnvironmentProcessRequest(unprivileged);
|
|
331
|
+
}
|
|
332
|
+
|
|
291
333
|
export function assertTaskEnvironmentProcessRequest(
|
|
292
334
|
request: TaskEnvironmentExecRequest | TaskEnvironmentSpawnRequest,
|
|
293
335
|
): void {
|
|
@@ -297,6 +339,14 @@ export function assertTaskEnvironmentProcessRequest(
|
|
|
297
339
|
}
|
|
298
340
|
}
|
|
299
341
|
|
|
342
|
+
/** For providers with no privileged exec: refuse it rather than run the
|
|
343
|
+
* step without the capability it asked for. */
|
|
344
|
+
export function refusePrivilegedExec(request: TaskEnvironmentExecRequest): void {
|
|
345
|
+
if (request.privileged === true) {
|
|
346
|
+
throw new Error("this task environment provider has no privileged exec");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
300
350
|
export function assertTaskEnvironmentSessionRequest(
|
|
301
351
|
request:
|
|
302
352
|
| TaskEnvironmentSessionRequest
|
|
@@ -304,6 +354,19 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
304
354
|
| TaskEnvironmentExecRequest
|
|
305
355
|
| TaskEnvironmentSpawnRequest,
|
|
306
356
|
): void {
|
|
357
|
+
// Only a one-shot exec may be privileged (assertTaskEnvironmentExecRequest
|
|
358
|
+
// strips it after checking); anywhere else it is refused outright.
|
|
359
|
+
if ("privileged" in request) {
|
|
360
|
+
throw new Error("only a one-shot task environment exec may be privileged");
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
request.projectEnvProfile !== "maintenance" &&
|
|
364
|
+
request.projectEnvProfile !== "task"
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"task environment project env profile must be maintenance or task",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
307
370
|
if (
|
|
308
371
|
!Array.isArray(request.argv) ||
|
|
309
372
|
request.argv.length === 0 ||
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port the runner's task gateway listens on (`UAI_MCP_GATEWAY_PORT`,
|
|
3
|
+
* 5877 by default): where task forwarders relay, and where a machine's
|
|
4
|
+
* sidecar serves its own task directly on loopback. A module of its own so
|
|
5
|
+
* the secrets proxy can name it without loading the gateway.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MCP_GATEWAY_PORT = 5877;
|
|
9
|
+
|
|
10
|
+
function gatewayPort(value: string | undefined): number {
|
|
11
|
+
if (value === undefined || !/^\d+$/.test(value)) {
|
|
12
|
+
return DEFAULT_MCP_GATEWAY_PORT;
|
|
13
|
+
}
|
|
14
|
+
const port = Number(value);
|
|
15
|
+
return Number.isSafeInteger(port) && port >= 1 && port <= 65_535
|
|
16
|
+
? port
|
|
17
|
+
: DEFAULT_MCP_GATEWAY_PORT;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MCP_GATEWAY_PORT = gatewayPort(
|
|
21
|
+
process.env.UAI_MCP_GATEWAY_PORT,
|
|
22
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHmac,
|
|
3
|
+
timingSafeEqual,
|
|
4
|
+
type KeyObject,
|
|
5
|
+
} from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Binary, versioned task-forwarder handshake (ADR-153).
|
|
9
|
+
*
|
|
10
|
+
* The leading NUL cannot be the first byte of either HTTP/1.1 or TLS. That
|
|
11
|
+
* lets the shared gateway reject direct TLS while keeping the existing MCP
|
|
12
|
+
* HTTP listener on the same port. Length-prefixed fields keep unauthenticated
|
|
13
|
+
* input bounded before a task identity has been proved.
|
|
14
|
+
*/
|
|
15
|
+
export const TASK_FORWARDER_PREAMBLE_MAGIC = Buffer.from([
|
|
16
|
+
0x00, 0x55, 0x41, 0x49, 0x46, 0x57, 0x44, 0x01,
|
|
17
|
+
]);
|
|
18
|
+
export const TASK_FORWARDER_CHALLENGE_MAGIC = Buffer.from("UAICHL01", "ascii");
|
|
19
|
+
export const TASK_FORWARDER_RESPONSE_MAGIC = Buffer.from("UAIRES01", "ascii");
|
|
20
|
+
export const TASK_FORWARDER_ACCEPT_MAGIC = Buffer.from("UAIOK001", "ascii");
|
|
21
|
+
|
|
22
|
+
export const TASK_FORWARDER_SECRET_BYTES = 32;
|
|
23
|
+
/** Numerical container uid reserved for the forwarder (neither root nor node). */
|
|
24
|
+
export const TASK_FORWARDER_UID = 65_532;
|
|
25
|
+
export const TASK_FORWARDER_NONCE_BYTES = 32;
|
|
26
|
+
export const TASK_FORWARDER_MAC_BYTES = 32;
|
|
27
|
+
export const TASK_FORWARDER_MAX_TASK_ID_BYTES = 128;
|
|
28
|
+
export const TASK_FORWARDER_MAX_GENERATION_BYTES = 64;
|
|
29
|
+
export const TASK_FORWARDER_PREAMBLE_HEADER_BYTES =
|
|
30
|
+
TASK_FORWARDER_PREAMBLE_MAGIC.length + 2;
|
|
31
|
+
export const TASK_FORWARDER_MAX_PREAMBLE_BYTES =
|
|
32
|
+
TASK_FORWARDER_PREAMBLE_HEADER_BYTES +
|
|
33
|
+
TASK_FORWARDER_MAX_TASK_ID_BYTES +
|
|
34
|
+
TASK_FORWARDER_MAX_GENERATION_BYTES;
|
|
35
|
+
export const TASK_FORWARDER_CHALLENGE_BYTES =
|
|
36
|
+
TASK_FORWARDER_CHALLENGE_MAGIC.length + TASK_FORWARDER_NONCE_BYTES;
|
|
37
|
+
export const TASK_FORWARDER_RESPONSE_BYTES =
|
|
38
|
+
TASK_FORWARDER_RESPONSE_MAGIC.length + TASK_FORWARDER_MAC_BYTES;
|
|
39
|
+
|
|
40
|
+
const FORWARDER_HMAC_DOMAIN = Buffer.from(
|
|
41
|
+
"uai-task-forwarder-v1\0",
|
|
42
|
+
"ascii",
|
|
43
|
+
);
|
|
44
|
+
const FIELD_SEPARATOR = Buffer.from([0]);
|
|
45
|
+
const FORWARDER_GENERATION = /^[A-Za-z0-9_-]{16,64}$/;
|
|
46
|
+
|
|
47
|
+
export interface TaskForwarderIdentity {
|
|
48
|
+
taskId: string;
|
|
49
|
+
generation: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function encodeTaskForwarderPreamble(
|
|
53
|
+
identity: TaskForwarderIdentity,
|
|
54
|
+
): Buffer {
|
|
55
|
+
const taskId = Buffer.from(identity.taskId, "utf8");
|
|
56
|
+
const generation = Buffer.from(identity.generation, "ascii");
|
|
57
|
+
if (
|
|
58
|
+
taskId.length < 1 ||
|
|
59
|
+
taskId.length > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
|
|
60
|
+
generation.length < 1 ||
|
|
61
|
+
generation.length > TASK_FORWARDER_MAX_GENERATION_BYTES ||
|
|
62
|
+
!FORWARDER_GENERATION.test(identity.generation)
|
|
63
|
+
) {
|
|
64
|
+
throw new Error("invalid task forwarder identity");
|
|
65
|
+
}
|
|
66
|
+
return Buffer.concat([
|
|
67
|
+
TASK_FORWARDER_PREAMBLE_MAGIC,
|
|
68
|
+
Buffer.from([taskId.length, generation.length]),
|
|
69
|
+
taskId,
|
|
70
|
+
generation,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function taskForwarderPreambleLength(header: Buffer): number | null {
|
|
75
|
+
if (
|
|
76
|
+
header.length !== TASK_FORWARDER_PREAMBLE_HEADER_BYTES ||
|
|
77
|
+
!header.subarray(0, TASK_FORWARDER_PREAMBLE_MAGIC.length).equals(
|
|
78
|
+
TASK_FORWARDER_PREAMBLE_MAGIC,
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
|
|
84
|
+
const generationBytes =
|
|
85
|
+
header[TASK_FORWARDER_PREAMBLE_MAGIC.length + 1] ?? 0;
|
|
86
|
+
if (
|
|
87
|
+
taskIdBytes < 1 ||
|
|
88
|
+
taskIdBytes > TASK_FORWARDER_MAX_TASK_ID_BYTES ||
|
|
89
|
+
generationBytes < 1 ||
|
|
90
|
+
generationBytes > TASK_FORWARDER_MAX_GENERATION_BYTES
|
|
91
|
+
) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return TASK_FORWARDER_PREAMBLE_HEADER_BYTES + taskIdBytes + generationBytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function decodeTaskForwarderPreamble(
|
|
98
|
+
preamble: Buffer,
|
|
99
|
+
): TaskForwarderIdentity | null {
|
|
100
|
+
const header = preamble.subarray(0, TASK_FORWARDER_PREAMBLE_HEADER_BYTES);
|
|
101
|
+
const expectedLength = taskForwarderPreambleLength(header);
|
|
102
|
+
if (expectedLength === null || preamble.length !== expectedLength) return null;
|
|
103
|
+
|
|
104
|
+
const taskIdBytes = header[TASK_FORWARDER_PREAMBLE_MAGIC.length] ?? 0;
|
|
105
|
+
const taskStart = TASK_FORWARDER_PREAMBLE_HEADER_BYTES;
|
|
106
|
+
const generationStart = taskStart + taskIdBytes;
|
|
107
|
+
const rawTaskId = preamble.subarray(taskStart, generationStart);
|
|
108
|
+
const rawGeneration = preamble.subarray(generationStart);
|
|
109
|
+
const taskId = rawTaskId.toString("utf8");
|
|
110
|
+
const generation = rawGeneration.toString("ascii");
|
|
111
|
+
// Both Node decoders replace or mask malformed bytes. Require exact
|
|
112
|
+
// round-trips so two wire identities can never alias the same HMAC input.
|
|
113
|
+
if (
|
|
114
|
+
!Buffer.from(taskId, "utf8").equals(rawTaskId) ||
|
|
115
|
+
!Buffer.from(generation, "ascii").equals(rawGeneration) ||
|
|
116
|
+
!FORWARDER_GENERATION.test(generation)
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { taskId, generation };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function encodeTaskForwarderChallenge(nonce: Buffer): Buffer {
|
|
124
|
+
if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
|
|
125
|
+
throw new Error("invalid task forwarder nonce");
|
|
126
|
+
}
|
|
127
|
+
return Buffer.concat([TASK_FORWARDER_CHALLENGE_MAGIC, nonce]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function decodeTaskForwarderChallenge(
|
|
131
|
+
challenge: Buffer,
|
|
132
|
+
): Buffer | null {
|
|
133
|
+
if (
|
|
134
|
+
challenge.length !== TASK_FORWARDER_CHALLENGE_BYTES ||
|
|
135
|
+
!challenge.subarray(0, TASK_FORWARDER_CHALLENGE_MAGIC.length).equals(
|
|
136
|
+
TASK_FORWARDER_CHALLENGE_MAGIC,
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return Buffer.from(challenge.subarray(TASK_FORWARDER_CHALLENGE_MAGIC.length));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function taskForwarderMac(
|
|
145
|
+
secret: Buffer | KeyObject,
|
|
146
|
+
identity: TaskForwarderIdentity,
|
|
147
|
+
nonce: Buffer,
|
|
148
|
+
): Buffer {
|
|
149
|
+
if (nonce.length !== TASK_FORWARDER_NONCE_BYTES) {
|
|
150
|
+
throw new Error("invalid task forwarder nonce");
|
|
151
|
+
}
|
|
152
|
+
return createHmac("sha256", secret)
|
|
153
|
+
.update(FORWARDER_HMAC_DOMAIN)
|
|
154
|
+
.update(identity.taskId, "utf8")
|
|
155
|
+
.update(FIELD_SEPARATOR)
|
|
156
|
+
.update(identity.generation, "ascii")
|
|
157
|
+
.update(FIELD_SEPARATOR)
|
|
158
|
+
.update(nonce)
|
|
159
|
+
.digest();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function encodeTaskForwarderResponse(mac: Buffer): Buffer {
|
|
163
|
+
if (mac.length !== TASK_FORWARDER_MAC_BYTES) {
|
|
164
|
+
throw new Error("invalid task forwarder response");
|
|
165
|
+
}
|
|
166
|
+
return Buffer.concat([TASK_FORWARDER_RESPONSE_MAGIC, mac]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function decodeTaskForwarderResponse(response: Buffer): Buffer | null {
|
|
170
|
+
if (
|
|
171
|
+
response.length !== TASK_FORWARDER_RESPONSE_BYTES ||
|
|
172
|
+
!response.subarray(0, TASK_FORWARDER_RESPONSE_MAGIC.length).equals(
|
|
173
|
+
TASK_FORWARDER_RESPONSE_MAGIC,
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return Buffer.from(response.subarray(TASK_FORWARDER_RESPONSE_MAGIC.length));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function verifyTaskForwarderMac(
|
|
182
|
+
expected: Buffer,
|
|
183
|
+
provided: Buffer,
|
|
184
|
+
): boolean {
|
|
185
|
+
return (
|
|
186
|
+
expected.length === TASK_FORWARDER_MAC_BYTES &&
|
|
187
|
+
provided.length === expected.length &&
|
|
188
|
+
timingSafeEqual(expected, provided)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activeLoaderHostTasks,
|
|
3
|
+
hostTaskProjectEnvSpec,
|
|
4
|
+
} from "./runtime-state";
|
|
5
|
+
import { taskEnvResolver } from "./task-env-source";
|
|
6
|
+
import { recoverTaskGatewayTask } from "./task-gateway-state";
|
|
7
|
+
|
|
8
|
+
export interface TaskGatewayRecoverySummary {
|
|
9
|
+
readonly recovered: number;
|
|
10
|
+
readonly failed: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reattach value resolvers before the gateway starts listening. Persisted
|
|
15
|
+
* credentials load inactive, so a malformed row or missing identity stays
|
|
16
|
+
* fail-closed and its surviving forwarder cannot authenticate.
|
|
17
|
+
*/
|
|
18
|
+
export function recoverActiveTaskGateways(): TaskGatewayRecoverySummary {
|
|
19
|
+
let recovered = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
for (const task of activeLoaderHostTasks()) {
|
|
22
|
+
try {
|
|
23
|
+
const spec = hostTaskProjectEnvSpec(task);
|
|
24
|
+
if (spec === null) {
|
|
25
|
+
throw new Error("loader task has no persisted project env spec");
|
|
26
|
+
}
|
|
27
|
+
const identity = recoverTaskGatewayTask(task.taskId, {
|
|
28
|
+
resolveEnv: taskEnvResolver(task.taskId, spec),
|
|
29
|
+
});
|
|
30
|
+
if (identity === null) {
|
|
31
|
+
throw new Error("loader task has no valid persisted gateway identity");
|
|
32
|
+
}
|
|
33
|
+
recovered += 1;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
failed += 1;
|
|
36
|
+
console.warn(
|
|
37
|
+
`[task-gateway] task ${task.taskId}: boot recovery stayed closed: ${
|
|
38
|
+
error instanceof Error ? error.message : String(error)
|
|
39
|
+
}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { recovered, failed };
|
|
44
|
+
}
|