@awak-app/simy-cli 0.2.3 → 0.4.0

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.
@@ -0,0 +1,2607 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import { isIP } from "node:net";
5
+ import {
6
+ access,
7
+ appendFile,
8
+ chmod,
9
+ lstat,
10
+ mkdir,
11
+ readFile,
12
+ realpath,
13
+ rename,
14
+ rm,
15
+ writeFile,
16
+ } from "node:fs/promises";
17
+ import path from "node:path";
18
+ import { promisify } from "node:util";
19
+
20
+ import {
21
+ createLocalTask,
22
+ localTaskArtifact,
23
+ localTaskSnapshot,
24
+ persistLocalTask,
25
+ prepareLocalTaskWorkspace,
26
+ startLocalTask,
27
+ stopLocalTask,
28
+ } from "./local-task.js";
29
+ import {
30
+ buildLocalTaskArtifactRefs,
31
+ LocalTaskArtifactContractError,
32
+ } from "./local-task-artifact-contract.js";
33
+ import {
34
+ LOCAL_TASK_FILE_CAPABILITY_VERSION,
35
+ validateLocalTaskFile,
36
+ } from "./local-task-file-capabilities.js";
37
+ import {
38
+ normalizeLocalTaskScenarioPack,
39
+ validateLocalTaskScenarioInputs,
40
+ } from "./local-task-scenario-packs.js";
41
+ import {
42
+ buildControlledLocalTaskPlan,
43
+ durableLocalTaskStepApiContract,
44
+ durableLocalTaskStepExecutionId,
45
+ durableLocalTaskStepInstruction,
46
+ durableLocalTaskStepWorkspaceId,
47
+ isDurableLocalTaskStepClaim,
48
+ isLocalTaskPlanOrchestration,
49
+ normalizeDurableLocalTaskStepClaim,
50
+ normalizeDurableLocalTaskStepTransition,
51
+ } from "./durable-local-task-steps-contract.js";
52
+ import {
53
+ normalizeDurableLocalTaskExecutionClaimV2,
54
+ } from "./bounded-local-task-subtasks-contract.js";
55
+ import {
56
+ createBoundedLocalTaskSubtaskPool,
57
+ } from "./bounded-local-task-subtask-pool.js";
58
+ import {
59
+ webApiErrorMessage,
60
+ webApiHeaders,
61
+ webApiUrl,
62
+ } from "./web-api.js";
63
+ import { normalizeGitHubRemote } from "./workspace-context.js";
64
+
65
+ const DEFAULT_LEASE_SECONDS = 120;
66
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
67
+ const REQUEST_TIMEOUT_MS = 10_000;
68
+ const ARTIFACT_UPLOAD_TIMEOUT_MS = 300_000;
69
+ const CLAIM_LEASE_EXPIRY_TOLERANCE_MS = 30_000;
70
+ const SUPPORTED_SCHEMA_VERSION = "local_task_execution.v1";
71
+ const SUPPORTED_BACKENDS = new Set(["codex", "claude"]);
72
+ const SUPPORTED_OPERATIONS = new Set(["observe", "read", "execute"]);
73
+ const SUPPORTED_PERMISSION_MODES = new Set(["read_only", "workspace_write"]);
74
+ const SUPPORTED_WORKSPACE_MODES = new Set(["task_sandbox", "repository"]);
75
+ const MACHINE_CODE_PATTERN = /^[a-z][a-z0-9_]{0,127}$/;
76
+ const REPOSITORY_OUTPUT_EXCLUDE = "/.simy/local-tasks/";
77
+ const DURABLE_STEP_ATTACHMENT_MANIFEST_SCHEMA =
78
+ "durable_local_task_attachments.v1";
79
+ const DURABLE_STEP_ATTACHMENT_MANIFEST_NAME =
80
+ "durable-step-attachments.v1.json";
81
+ const execFileAsync = promisify(execFile);
82
+
83
+ export class DurableLocalTaskApiError extends Error {
84
+ constructor(message, { status = null, code = null, ambiguous = false } = {}) {
85
+ super(message);
86
+ this.name = "DurableLocalTaskApiError";
87
+ this.status = status;
88
+ this.code = safeMachineCode(code, "durable_local_task_api_error");
89
+ this.ambiguous = ambiguous;
90
+ }
91
+ }
92
+
93
+ export function createDurableLocalTaskApi({
94
+ getConnection,
95
+ fetchImpl = globalThis.fetch,
96
+ requestTimeoutMs = REQUEST_TIMEOUT_MS,
97
+ artifactUploadTimeoutMs = ARTIFACT_UPLOAD_TIMEOUT_MS,
98
+ } = {}) {
99
+ if (typeof getConnection !== "function") {
100
+ throw new TypeError("getConnection is required");
101
+ }
102
+ if (typeof fetchImpl !== "function") {
103
+ throw new TypeError("fetchImpl is required");
104
+ }
105
+
106
+ const request = async (path, body, { ambiguousOnTransport = false } = {}) => {
107
+ const connection = getConnection();
108
+ if (
109
+ !connection?.apiOrigin ||
110
+ !connection?.token
111
+ ) {
112
+ throw new DurableLocalTaskApiError(
113
+ "The SIMY CLI session is not connected.",
114
+ { code: "local_cli_session_unavailable" },
115
+ );
116
+ }
117
+ let response;
118
+ try {
119
+ response = await fetchImpl(
120
+ webApiUrl(path, {
121
+ webOrigin: connection.apiOrigin,
122
+ apiBaseUrl: connection.apiBaseUrl,
123
+ }),
124
+ {
125
+ method: "POST",
126
+ headers: webApiHeaders(
127
+ { token: connection.token },
128
+ { "Content-Type": "application/json" },
129
+ ),
130
+ body: JSON.stringify(body),
131
+ signal: AbortSignal.timeout(requestTimeoutMs),
132
+ },
133
+ );
134
+ } catch (error) {
135
+ throw new DurableLocalTaskApiError(
136
+ `SIMY Web could not be reached: ${errorMessage(error)}`,
137
+ {
138
+ code: "local_task_gateway_unavailable",
139
+ ambiguous: ambiguousOnTransport,
140
+ },
141
+ );
142
+ }
143
+ const payload = await response.json().catch(() => null);
144
+ if (!response.ok) {
145
+ throw new DurableLocalTaskApiError(
146
+ webApiErrorMessage(payload, response.status),
147
+ {
148
+ status: response.status,
149
+ code: safeMachineCode(
150
+ payload?.code,
151
+ "durable_local_task_api_error",
152
+ ),
153
+ },
154
+ );
155
+ }
156
+ return payload;
157
+ };
158
+ const stepContract = durableLocalTaskStepApiContract();
159
+
160
+ return {
161
+ async claimSubtaskParent({
162
+ workerId,
163
+ leaseSeconds = DEFAULT_LEASE_SECONDS,
164
+ turnId = null,
165
+ }) {
166
+ const body = {
167
+ worker_id: workerId,
168
+ lease_seconds: leaseSeconds,
169
+ };
170
+ if (turnId !== null) body.turn_id = turnId;
171
+ return request("local-tasks/subtasks/parent-claim", body);
172
+ },
173
+ async claim({
174
+ workerId,
175
+ leaseSeconds = DEFAULT_LEASE_SECONDS,
176
+ turnId = null,
177
+ controlledPlan = null,
178
+ }) {
179
+ const body = {
180
+ worker_id: workerId,
181
+ lease_seconds: leaseSeconds,
182
+ turn_id: turnId,
183
+ };
184
+ if (controlledPlan !== null) body.controlled_plan = controlledPlan;
185
+ const payload = await request(
186
+ "local-tasks/claim",
187
+ body,
188
+ { ambiguousOnTransport: controlledPlan !== null },
189
+ );
190
+ if (isLocalTaskPlanOrchestration(payload)) return payload;
191
+ return payload?.claim ?? null;
192
+ },
193
+ async heartbeat({
194
+ turnId,
195
+ workerId,
196
+ leaseEpoch,
197
+ leaseSeconds = DEFAULT_LEASE_SECONDS,
198
+ }) {
199
+ const payload = await request(
200
+ `local-tasks/${encodeURIComponent(turnId)}/heartbeat`,
201
+ {
202
+ worker_id: workerId,
203
+ lease_epoch: leaseEpoch,
204
+ lease_seconds: leaseSeconds,
205
+ },
206
+ );
207
+ return {
208
+ ok: payload?.ok === true,
209
+ cancelRequested: payload?.cancel_requested === true,
210
+ };
211
+ },
212
+ async markInvocationStarted({ turnId, workerId, leaseEpoch }) {
213
+ const payload = await request(
214
+ `local-tasks/${encodeURIComponent(turnId)}/invocation-start`,
215
+ {
216
+ worker_id: workerId,
217
+ lease_epoch: leaseEpoch,
218
+ },
219
+ { ambiguousOnTransport: true },
220
+ );
221
+ return payload?.ok === true;
222
+ },
223
+ async updateStatus({
224
+ turnId,
225
+ workerId,
226
+ leaseEpoch,
227
+ status,
228
+ resultSummary = null,
229
+ artifactRefs = [],
230
+ failureCode = null,
231
+ }) {
232
+ const payload = await request(
233
+ `local-tasks/${encodeURIComponent(turnId)}/status`,
234
+ {
235
+ worker_id: workerId,
236
+ lease_epoch: leaseEpoch,
237
+ status,
238
+ result_summary: resultSummary,
239
+ artifact_refs: artifactRefs,
240
+ failure_code: failureCode,
241
+ },
242
+ { ambiguousOnTransport: true },
243
+ );
244
+ return payload?.ok === true;
245
+ },
246
+ async prepareArtifact({
247
+ turnId,
248
+ localArtifactId,
249
+ name,
250
+ mimeType,
251
+ sizeBytes,
252
+ sha256,
253
+ }) {
254
+ return request(
255
+ "local-tasks/artifacts/prepare",
256
+ {
257
+ turn_id: turnId,
258
+ local_artifact_id: localArtifactId,
259
+ name,
260
+ mime_type: mimeType,
261
+ size_bytes: sizeBytes,
262
+ sha256,
263
+ },
264
+ { ambiguousOnTransport: true },
265
+ );
266
+ },
267
+ async uploadArtifact({ uploadUrl, method = "PUT", headers = {}, bytes }) {
268
+ const target = trustedArtifactUploadUrl(uploadUrl);
269
+ const uploadHeaders = trustedArtifactUploadHeaders(headers);
270
+ if (method !== "PUT" || !Buffer.isBuffer(bytes)) {
271
+ throw new DurableLocalTaskApiError(
272
+ "SIMY Web returned an invalid Pipeline artifact upload.",
273
+ { code: "artifact_materialization_invalid_response" },
274
+ );
275
+ }
276
+ let response;
277
+ try {
278
+ response = await fetchImpl(target, {
279
+ method: "PUT",
280
+ headers: uploadHeaders,
281
+ body: bytes,
282
+ signal: AbortSignal.timeout(artifactUploadTimeoutMs),
283
+ });
284
+ } catch (error) {
285
+ throw new DurableLocalTaskApiError(
286
+ `SIMY could not upload the Local Task output: ${errorMessage(error)}`,
287
+ {
288
+ code: "local_task_artifact_upload_unavailable",
289
+ ambiguous: true,
290
+ },
291
+ );
292
+ }
293
+ if (!response.ok) {
294
+ throw new DurableLocalTaskApiError(
295
+ `SIMY artifact storage returned HTTP ${response.status}.`,
296
+ {
297
+ status: response.status,
298
+ code: "local_task_artifact_upload_failed",
299
+ },
300
+ );
301
+ }
302
+ return true;
303
+ },
304
+ async commitArtifact({
305
+ uploadId,
306
+ turnId,
307
+ localArtifactId,
308
+ sha256,
309
+ sizeBytes,
310
+ }) {
311
+ return request(
312
+ "local-tasks/artifacts/commit",
313
+ {
314
+ upload_id: uploadId,
315
+ turn_id: turnId,
316
+ local_artifact_id: localArtifactId,
317
+ sha256,
318
+ size_bytes: sizeBytes,
319
+ },
320
+ { ambiguousOnTransport: true },
321
+ );
322
+ },
323
+ async heartbeatStep(input) {
324
+ const contract = stepContract.heartbeat(input);
325
+ const payload = await request(
326
+ contract.path,
327
+ contract.body,
328
+ { ambiguousOnTransport: contract.ambiguousOnTransport },
329
+ );
330
+ return {
331
+ ok: payload?.ok === true,
332
+ cancelRequested: payload?.cancel_requested === true,
333
+ };
334
+ },
335
+ async markStepInvocationStarted(input) {
336
+ const contract = stepContract.invocationStart(input);
337
+ const payload = await request(
338
+ contract.path,
339
+ contract.body,
340
+ { ambiguousOnTransport: contract.ambiguousOnTransport },
341
+ );
342
+ return payload?.ok === true;
343
+ },
344
+ async updateStepStatus(input) {
345
+ const contract = stepContract.status(input);
346
+ const payload = await request(
347
+ contract.path,
348
+ contract.body,
349
+ { ambiguousOnTransport: contract.ambiguousOnTransport },
350
+ );
351
+ if (payload?.ok !== true) return false;
352
+ const {
353
+ ok: _ok,
354
+ ...transitionPayload
355
+ } = payload;
356
+ return {
357
+ ok: true,
358
+ transition: normalizeDurableLocalTaskStepTransition(
359
+ transitionPayload,
360
+ { completedStepId: input.stepId },
361
+ ),
362
+ };
363
+ },
364
+ async initialize_local_task_subtask_batch_v2(input) {
365
+ return request(
366
+ boundedSubtaskPath(input, "initialize"),
367
+ {
368
+ worker_id: input.p_worker_id,
369
+ parent_lease_epoch: input.p_parent_lease_epoch,
370
+ plan: input.p_plan,
371
+ },
372
+ { ambiguousOnTransport: true },
373
+ );
374
+ },
375
+ async claim_local_task_subtask_attempt_v2(input) {
376
+ return request(
377
+ boundedSubtaskPath(input, "claim"),
378
+ {
379
+ worker_id: input.p_worker_id,
380
+ parent_lease_epoch: input.p_parent_lease_epoch,
381
+ cancel_generation: input.p_cancel_generation,
382
+ lease_seconds: input.p_lease_seconds,
383
+ step_id: input.p_step_id,
384
+ },
385
+ { ambiguousOnTransport: true },
386
+ );
387
+ },
388
+ async heartbeat_local_task_subtask_attempt_v2(input) {
389
+ const payload = await request(
390
+ boundedSubtaskStepPath(input, "heartbeat"),
391
+ {
392
+ ...boundedSubtaskFenceBody(input),
393
+ lease_seconds: input.p_lease_seconds,
394
+ },
395
+ );
396
+ const cancelGeneration = payload?.cancel_generation;
397
+ if (
398
+ cancelGeneration !== undefined &&
399
+ (typeof cancelGeneration !== "string" ||
400
+ !/^(?:0|[1-9][0-9]*)$/.test(cancelGeneration))
401
+ ) {
402
+ throw new DurableLocalTaskApiError(
403
+ "SIMY Web returned an invalid bounded subtask heartbeat.",
404
+ { code: "local_task_subtask_invalid_response" },
405
+ );
406
+ }
407
+ return {
408
+ schema_version: payload?.schema_version,
409
+ ok: payload?.ok === true,
410
+ terminal: payload?.terminal === true,
411
+ attempt_state: payload?.attempt_state,
412
+ batch_state: payload?.batch_state,
413
+ cancel_requested: payload?.cancel_requested === true,
414
+ cancel_generation: cancelGeneration,
415
+ attempt_lease_epoch: payload?.attempt_lease_epoch,
416
+ };
417
+ },
418
+ async mark_local_task_subtask_invocation_started_v2(input) {
419
+ const payload = await request(
420
+ boundedSubtaskStepPath(input, "invocation-start"),
421
+ boundedSubtaskFenceBody(input),
422
+ { ambiguousOnTransport: true },
423
+ );
424
+ return (
425
+ payload?.schema_version === "local_task_subtask_transition.v2" &&
426
+ payload?.state === "started"
427
+ );
428
+ },
429
+ async complete_local_task_subtask_attempt_v2(input) {
430
+ const payload = await request(
431
+ boundedSubtaskStepPath(input, "status"),
432
+ {
433
+ ...boundedSubtaskFenceBody(input),
434
+ status: input.p_status,
435
+ result_summary: input.p_result_summary,
436
+ artifact_refs: input.p_artifact_refs,
437
+ provider_tokens_used: input.p_provider_tokens_used,
438
+ elapsed_ms: input.p_elapsed_ms,
439
+ failure_code: input.p_failure_code,
440
+ },
441
+ { ambiguousOnTransport: true },
442
+ );
443
+ return payload;
444
+ },
445
+ async abandon_local_task_subtask_attempt_v2(input) {
446
+ const payload = await request(
447
+ boundedSubtaskStepPath(input, "abandon"),
448
+ {
449
+ ...boundedSubtaskFenceBody(input),
450
+ reason: input.p_reason_code,
451
+ },
452
+ { ambiguousOnTransport: true },
453
+ );
454
+ return payload;
455
+ },
456
+ async download_local_task_subtask_source_v2({
457
+ turnId,
458
+ batchId,
459
+ stepId,
460
+ attemptId,
461
+ source,
462
+ signal = null,
463
+ }) {
464
+ const input = {
465
+ p_turn_id: turnId,
466
+ p_step_id: stepId,
467
+ };
468
+ const body = {
469
+ batch_id: batchId,
470
+ attempt_id: attemptId,
471
+ source_id: source?.source_id,
472
+ upload_id: source?.upload_id,
473
+ sha256: source?.sha256,
474
+ };
475
+ const payload = await request(
476
+ boundedSubtaskStepPath(input, "sources/download"),
477
+ body,
478
+ );
479
+ const prepared = validateSubtaskSourceDownload(payload, source);
480
+ let response;
481
+ try {
482
+ response = await fetchImpl(prepared.url, {
483
+ method: "GET",
484
+ headers: prepared.headers,
485
+ signal: signal
486
+ ? AbortSignal.any([
487
+ signal,
488
+ AbortSignal.timeout(artifactUploadTimeoutMs),
489
+ ])
490
+ : AbortSignal.timeout(artifactUploadTimeoutMs),
491
+ });
492
+ } catch (error) {
493
+ throw new DurableLocalTaskApiError(
494
+ `SIMY could not download the verified subtask source: ${errorMessage(error)}`,
495
+ {
496
+ code: "local_task_subtask_source_download_unavailable",
497
+ },
498
+ );
499
+ }
500
+ if (!response.ok) {
501
+ throw new DurableLocalTaskApiError(
502
+ `SIMY source storage returned HTTP ${response.status}.`,
503
+ {
504
+ status: response.status,
505
+ code: "local_task_subtask_source_download_failed",
506
+ },
507
+ );
508
+ }
509
+ return Buffer.from(await response.arrayBuffer());
510
+ },
511
+ };
512
+ }
513
+
514
+ function boundedSubtaskPath(input, operation) {
515
+ const turnId = String(input?.p_turn_id || "");
516
+ if (!isUuid(turnId)) {
517
+ throw new DurableLocalTaskApiError(
518
+ "The bounded Local Task turn ID is invalid.",
519
+ { code: "invalid_local_task_subtask_request" },
520
+ );
521
+ }
522
+ return (
523
+ `local-tasks/${encodeURIComponent(turnId)}` +
524
+ `/subtasks/${operation}`
525
+ );
526
+ }
527
+
528
+ function boundedSubtaskStepPath(input, operation) {
529
+ const stepId = String(input?.p_step_id || "");
530
+ if (!isUuid(stepId)) {
531
+ throw new DurableLocalTaskApiError(
532
+ "The bounded Local Task step ID is invalid.",
533
+ { code: "invalid_local_task_subtask_request" },
534
+ );
535
+ }
536
+ return (
537
+ boundedSubtaskPath(input, encodeURIComponent(stepId)) +
538
+ `/${operation}`
539
+ );
540
+ }
541
+
542
+ function boundedSubtaskFenceBody(input) {
543
+ return {
544
+ worker_id: input.p_worker_id,
545
+ parent_lease_epoch: input.p_parent_lease_epoch,
546
+ batch_id: input.p_batch_id,
547
+ attempt_id: input.p_attempt_id,
548
+ attempt_lease_epoch: input.p_attempt_lease_epoch,
549
+ cancel_generation: input.p_cancel_generation,
550
+ };
551
+ }
552
+
553
+ function validateSubtaskSourceDownload(value, expected) {
554
+ const topKeys = new Set(["schema_version", "source", "download"]);
555
+ const sourceKeys = new Set([
556
+ "source_id",
557
+ "name",
558
+ "mime_type",
559
+ "size_bytes",
560
+ "sha256",
561
+ ]);
562
+ const downloadKeys = new Set([
563
+ "url",
564
+ "method",
565
+ "headers",
566
+ "expires_at",
567
+ ]);
568
+ if (
569
+ !value ||
570
+ typeof value !== "object" ||
571
+ Array.isArray(value) ||
572
+ Object.keys(value).some((key) => !topKeys.has(key)) ||
573
+ Object.keys(value).length !== topKeys.size ||
574
+ value.schema_version !== "local_task_subtask_source_download.v2" ||
575
+ !value.source ||
576
+ typeof value.source !== "object" ||
577
+ Array.isArray(value.source) ||
578
+ Object.keys(value.source).some((key) => !sourceKeys.has(key)) ||
579
+ Object.keys(value.source).length !== sourceKeys.size ||
580
+ value.source.source_id !== expected?.source_id ||
581
+ value.source.name !== expected?.name ||
582
+ value.source.mime_type !== expected?.mime_type ||
583
+ value.source.size_bytes !== expected?.size_bytes ||
584
+ String(value.source.sha256 || "").toLowerCase() !== expected?.sha256 ||
585
+ !value.download ||
586
+ typeof value.download !== "object" ||
587
+ Array.isArray(value.download) ||
588
+ Object.keys(value.download).some((key) => !downloadKeys.has(key)) ||
589
+ Object.keys(value.download).length !== downloadKeys.size ||
590
+ value.download.method !== "GET" ||
591
+ !Number.isFinite(Date.parse(value.download.expires_at)) ||
592
+ Date.parse(value.download.expires_at) <= Date.now()
593
+ ) {
594
+ throw new DurableLocalTaskApiError(
595
+ "SIMY Web returned an invalid bounded subtask source download.",
596
+ { code: "local_task_subtask_source_invalid_response" },
597
+ );
598
+ }
599
+ return {
600
+ url: trustedSubtaskSourceDownloadUrl(value.download.url),
601
+ headers: trustedArtifactUploadHeaders(value.download.headers),
602
+ };
603
+ }
604
+
605
+ export function createDurableLocalTaskWorker({
606
+ api,
607
+ registry,
608
+ storageRoot,
609
+ workspaceRoot,
610
+ resolveRepository = async () => null,
611
+ runLocalTask = startLocalTask,
612
+ prepareWorkspace = prepareLocalTaskWorkspace,
613
+ persist = persistLocalTask,
614
+ stopTask = stopLocalTask,
615
+ materializeArtifacts = materializePipelineArtifacts,
616
+ attachmentStore = null,
617
+ getDeviceId = () => null,
618
+ beginWork = () => () => {},
619
+ workerId = `simy-cli-${randomUUID()}`,
620
+ leaseSeconds = DEFAULT_LEASE_SECONDS,
621
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
622
+ setIntervalImpl = setInterval,
623
+ clearIntervalImpl = clearInterval,
624
+ onError = () => {},
625
+ createSubtaskPool = createBoundedLocalTaskSubtaskPool,
626
+ runBoundedSubtask = null,
627
+ } = {}) {
628
+ if (!api || typeof api.claim !== "function") {
629
+ throw new TypeError("api is required");
630
+ }
631
+ if (!registry || typeof registry.create !== "function") {
632
+ throw new TypeError("registry is required");
633
+ }
634
+
635
+ let closed = false;
636
+ let pollPromise = null;
637
+ let active = null;
638
+ let activeExecutionPromise = null;
639
+ let lastStepDisposition = null;
640
+ let pendingStepTurnId = null;
641
+ const boundedTasks = new Map();
642
+
643
+ const executeBoundedRuntime = runBoundedSubtask ?? (async (runtime, { signal }) => {
644
+ const startedAt = Date.now();
645
+ const task = createLocalTask({
646
+ id: runtime.workspace_id,
647
+ instruction: runtime.instruction,
648
+ backend: runtime.backend,
649
+ model: runtime.model,
650
+ operation: runtime.operation,
651
+ permissionMode: "read_only",
652
+ workspaceMode: "task_sandbox",
653
+ repository: null,
654
+ workspacePath: runtime.workspace_path,
655
+ taskRoot: runtime.task_root,
656
+ inputsRoot: runtime.inputs_root,
657
+ outputsRoot: runtime.outputs_root,
658
+ outputsRootExpected: runtime.outputs_root_expected,
659
+ attachments: runtime.verified_uploaded_inputs,
660
+ expectedOutputs: [],
661
+ providerTokenBudget: Number(runtime.provider_token_budget),
662
+ timeoutMs: Number(runtime.timeout_ms),
663
+ });
664
+ task.durable_turn_id = runtime.parent_turn_id;
665
+ task.durable_step_id = runtime.step_id;
666
+ await persist(task);
667
+ registry.create(task);
668
+ boundedTasks.set(runtime.attempt_id, task);
669
+ if (active) active.task = task;
670
+ const stopOnAbort = () => {
671
+ void stopTask(task).catch(() => undefined);
672
+ };
673
+ signal.addEventListener("abort", stopOnAbort, { once: true });
674
+ try {
675
+ const completed = await runLocalTask(task);
676
+ const status = completed.status === "succeeded"
677
+ ? "succeeded"
678
+ : completed.status === "stopped"
679
+ ? "cancelled"
680
+ : "failed";
681
+ return {
682
+ status,
683
+ result_summary:
684
+ completed.result?.summary ||
685
+ completed.error ||
686
+ `Bounded Local Task subtask ${status}.`,
687
+ provider_tokens_used: String(completed.provider_tokens_used || 0),
688
+ elapsed_ms: String(Math.max(0, Date.now() - startedAt)),
689
+ failure_code:
690
+ status === "failed"
691
+ ? safeMachineCode(
692
+ completed.reason_code,
693
+ "local_task_subtask_failed",
694
+ )
695
+ : null,
696
+ };
697
+ } finally {
698
+ signal.removeEventListener("abort", stopOnAbort);
699
+ boundedTasks.delete(runtime.attempt_id);
700
+ }
701
+ });
702
+
703
+ const executeStepClaim = async (claim, releaseWork) => {
704
+ let task = null;
705
+ let heartbeatTimer = null;
706
+ let heartbeatInFlight = null;
707
+ let knownLeaseExpiry = Number.NaN;
708
+ let boundary = "not_started";
709
+ let leaseLost = false;
710
+ let cancelRequested = false;
711
+ let normalized = null;
712
+ const executionStartedAt = Date.now();
713
+
714
+ const stopForLeaseLoss = async (error) => {
715
+ if (leaseLost) return;
716
+ leaseLost = true;
717
+ onError(
718
+ recoveryError(
719
+ "SIMY lost the durable step lease and stopped the local Provider to prevent duplicate work.",
720
+ error,
721
+ "local_task_step_lease_lost",
722
+ ),
723
+ );
724
+ if (task) await stopTask(task).catch(() => undefined);
725
+ };
726
+
727
+ try {
728
+ normalized = normalizeDurableLocalTaskStepClaim(claim, {
729
+ leaseSeconds,
730
+ });
731
+ if (normalized.disposition !== "execute") {
732
+ throw workerError(
733
+ normalized.disposition === "approval_required"
734
+ ? "This durable Local Task step is waiting for human approval."
735
+ : "The previous Provider result is unknown and cannot be replayed.",
736
+ normalized.disposition === "approval_required"
737
+ ? "local_task_step_approval_required"
738
+ : "local_task_step_provider_result_unknown",
739
+ );
740
+ }
741
+ knownLeaseExpiry = normalized.leaseExpiresAtMs;
742
+ const workspace = await resolveStepWorkspace(normalized, {
743
+ storageRoot,
744
+ workspaceRoot,
745
+ prepareWorkspace,
746
+ resolveRepository,
747
+ });
748
+ const attachments = await loadDurableStepAttachments({
749
+ turnId: normalized.turnId,
750
+ taskRoot: workspace.turn_task_root,
751
+ inputsRoot: workspace.inputs_root,
752
+ });
753
+ task = createLocalTask({
754
+ id: workspace.localTaskId,
755
+ instruction: durableLocalTaskStepInstruction(normalized),
756
+ backend: normalized.step.backend,
757
+ operation: normalized.step.operation,
758
+ permissionMode: normalized.step.permission_mode,
759
+ workspaceMode: normalized.step.workspace_mode,
760
+ repository: normalized.step.repository,
761
+ workspacePath: workspace.workspace_path,
762
+ taskRoot: workspace.task_root,
763
+ inputsRoot: workspace.inputs_root,
764
+ outputsRoot: workspace.outputs_root,
765
+ outputsRootExpected: workspace.outputs_root_expected,
766
+ attachments,
767
+ expectedOutputs: normalized.step.expected_outputs,
768
+ providerTokenBudget: Math.min(
769
+ normalized.step.provider_token_budget,
770
+ normalized.remainingProviderTokens,
771
+ ),
772
+ timeoutMs: Math.min(
773
+ normalized.step.timeout_ms,
774
+ normalized.remainingTimeoutMs,
775
+ ),
776
+ });
777
+ task.durable_turn_id = normalized.turnId;
778
+ task.durable_plan_id = normalized.plan.plan_id;
779
+ task.durable_plan_hash = normalized.plan.plan_hash;
780
+ task.durable_step_id = normalized.step.step_id;
781
+ task.durable_step_index = normalized.step.index;
782
+ task.durable_lease_epoch = normalized.leaseEpoch;
783
+ await persist(task);
784
+ registry.create(task);
785
+ if (active) active.task = task;
786
+
787
+ const heartbeat = async () => {
788
+ if (
789
+ !active ||
790
+ active.claim.turn_id !== normalized.turnId ||
791
+ leaseLost
792
+ ) {
793
+ return;
794
+ }
795
+ try {
796
+ const response = await api.heartbeatStep({
797
+ turnId: normalized.turnId,
798
+ stepId: normalized.step.step_id,
799
+ workerId,
800
+ leaseEpoch: normalized.leaseEpoch,
801
+ leaseSeconds,
802
+ });
803
+ if (response.ok !== true) {
804
+ await stopForLeaseLoss(
805
+ workerError(
806
+ "SIMY Web did not renew the durable step lease.",
807
+ "local_task_step_lease_not_renewed",
808
+ ),
809
+ );
810
+ return;
811
+ }
812
+ knownLeaseExpiry = Date.now() + leaseSeconds * 1_000;
813
+ if (response.cancelRequested) {
814
+ cancelRequested = true;
815
+ await stopTask(task);
816
+ }
817
+ } catch (error) {
818
+ if (
819
+ error instanceof DurableLocalTaskApiError &&
820
+ error.status === 409
821
+ ) {
822
+ await stopForLeaseLoss(error);
823
+ return;
824
+ }
825
+ onError(
826
+ recoveryError(
827
+ "SIMY could not renew this durable step yet; it will keep trying while the lease is safe.",
828
+ error,
829
+ "local_task_step_heartbeat_failed",
830
+ ),
831
+ );
832
+ if (
833
+ Number.isFinite(knownLeaseExpiry) &&
834
+ Date.now() + heartbeatIntervalMs >= knownLeaseExpiry
835
+ ) {
836
+ await stopForLeaseLoss(error);
837
+ }
838
+ }
839
+ };
840
+ const runHeartbeat = () => {
841
+ if (!heartbeatInFlight) {
842
+ heartbeatInFlight = heartbeat().finally(() => {
843
+ heartbeatInFlight = null;
844
+ });
845
+ }
846
+ return heartbeatInFlight;
847
+ };
848
+
849
+ heartbeatTimer = setIntervalImpl(() => {
850
+ void runHeartbeat();
851
+ }, heartbeatIntervalMs);
852
+ heartbeatTimer?.unref?.();
853
+
854
+ const completed = await runLocalTask(task, {
855
+ beforeProviderInvocation: async () => {
856
+ if (leaseLost || cancelRequested || closed) {
857
+ throw workerError(
858
+ "The durable step was stopped before the Provider started.",
859
+ "local_task_step_cancelled_before_invocation",
860
+ );
861
+ }
862
+ try {
863
+ await markInvocationStartedWithReplay(
864
+ {
865
+ markInvocationStarted: (input) =>
866
+ api.markStepInvocationStarted({
867
+ ...input,
868
+ stepId: normalized.step.step_id,
869
+ }),
870
+ },
871
+ {
872
+ turnId: normalized.turnId,
873
+ workerId,
874
+ leaseEpoch: normalized.leaseEpoch,
875
+ },
876
+ );
877
+ boundary = "started";
878
+ } catch (error) {
879
+ boundary = error?.ambiguous ? "unknown" : "not_started";
880
+ throw workerError(
881
+ error?.ambiguous
882
+ ? "SIMY could not confirm the durable step Provider boundary. This step will not be started again."
883
+ : `SIMY could not authorize the durable step Provider invocation: ${errorMessage(error)}`,
884
+ error?.ambiguous
885
+ ? "local_task_step_provider_result_unknown"
886
+ : "local_task_step_invocation_not_authorized",
887
+ error,
888
+ );
889
+ }
890
+ },
891
+ });
892
+
893
+ if (heartbeatTimer) {
894
+ clearIntervalImpl(heartbeatTimer);
895
+ heartbeatTimer = null;
896
+ }
897
+ await runHeartbeat();
898
+ if (leaseLost) return;
899
+ const status = completed.status === "stopped"
900
+ ? "cancelled"
901
+ : boundary === "unknown"
902
+ ? "unknown"
903
+ : completed.status === "succeeded"
904
+ ? "succeeded"
905
+ : "failed";
906
+ let reportStatus = status;
907
+ let reportFailureCode =
908
+ status === "failed" || status === "unknown"
909
+ ? safeMachineCode(completed.reason_code, "local_task_step_failed")
910
+ : null;
911
+ let reportSummary = durableResultSummary(
912
+ completed,
913
+ status,
914
+ reportFailureCode,
915
+ );
916
+ let artifactRefs = [];
917
+ try {
918
+ artifactRefs = buildLocalTaskArtifactRefs(
919
+ completed.artifacts,
920
+ completed.id,
921
+ );
922
+ } catch (error) {
923
+ artifactRefs = [];
924
+ if (status !== "cancelled") {
925
+ reportStatus = status === "unknown" ? "unknown" : "failed";
926
+ reportFailureCode = "artifact_verification_failed";
927
+ reportSummary =
928
+ "SIMY could not safely verify this durable step's output files.";
929
+ completed.status =
930
+ reportStatus === "failed" ? "failed" : completed.status;
931
+ completed.reason_code = reportFailureCode;
932
+ completed.error = error.message;
933
+ completed.artifacts = [];
934
+ if (completed.result) completed.result.artifacts = [];
935
+ await persist(completed);
936
+ }
937
+ onError(
938
+ recoveryError(
939
+ "SIMY rejected unsafe durable step output metadata before sending it to SIMY Web.",
940
+ error,
941
+ reportFailureCode,
942
+ ),
943
+ );
944
+ }
945
+ const statusInput = {
946
+ turnId: normalized.turnId,
947
+ stepId: normalized.step.step_id,
948
+ workerId,
949
+ leaseEpoch: normalized.leaseEpoch,
950
+ status: reportStatus,
951
+ resultSummary: reportSummary,
952
+ artifactRefs,
953
+ providerTokensUsed: completed.provider_tokens_used,
954
+ elapsedMs: Math.max(0, Date.now() - executionStartedAt),
955
+ failureCode: reportFailureCode,
956
+ };
957
+ let statusResolution;
958
+ try {
959
+ statusResolution = await updateStatusWithResolution(
960
+ {
961
+ updateStatus: (input) => api.updateStepStatus({
962
+ ...input,
963
+ stepId: normalized.step.step_id,
964
+ providerTokensUsed: statusInput.providerTokensUsed,
965
+ elapsedMs: statusInput.elapsedMs,
966
+ }),
967
+ },
968
+ statusInput,
969
+ );
970
+ } catch (error) {
971
+ onError(
972
+ recoveryError(
973
+ "SIMY completed the durable Provider step but could not save its final server state. It will not rerun the Provider.",
974
+ error,
975
+ "local_task_step_status_unavailable",
976
+ ),
977
+ );
978
+ return;
979
+ }
980
+ const statusConfirmed =
981
+ statusResolution === true || statusResolution?.ok === true;
982
+ if (!statusConfirmed) {
983
+ onError(
984
+ recoveryError(
985
+ "SIMY could not confirm the durable step's final server state. It will not rerun the Provider.",
986
+ new Error("The durable step lease was already fenced."),
987
+ "local_task_step_status_unconfirmed",
988
+ ),
989
+ );
990
+ return;
991
+ }
992
+ if (statusResolution?.transition?.status === "advanced") {
993
+ const transition = statusResolution.transition;
994
+ const nextStep = normalized.plan.steps[normalized.step.index + 1];
995
+ if (
996
+ reportStatus !== "succeeded" ||
997
+ transition.plan_id !== normalized.plan.plan_id ||
998
+ transition.current_step_id !== nextStep?.step_id ||
999
+ transition.current_step_index !== nextStep?.index + 1
1000
+ ) {
1001
+ onError(
1002
+ recoveryError(
1003
+ "SIMY Web returned an inconsistent durable plan handoff. The CLI stopped before starting another Provider.",
1004
+ new Error("The authoritative step transition did not match the claimed plan."),
1005
+ "local_task_plan_handoff_invalid",
1006
+ ),
1007
+ );
1008
+ return;
1009
+ }
1010
+ pendingStepTurnId = normalized.turnId;
1011
+ }
1012
+ } catch (error) {
1013
+ if (!leaseLost) {
1014
+ onError(
1015
+ recoveryError(
1016
+ "SIMY could not execute this durable Local Task step safely.",
1017
+ error,
1018
+ safeMachineCode(error?.code, "durable_local_task_step_failed"),
1019
+ ),
1020
+ );
1021
+ if (normalized?.disposition === "execute") {
1022
+ const claimStatus = boundary === "not_started" ? "failed" : "unknown";
1023
+ await api.updateStepStatus({
1024
+ turnId: normalized.turnId,
1025
+ stepId: normalized.step.step_id,
1026
+ workerId,
1027
+ leaseEpoch: normalized.leaseEpoch,
1028
+ status: claimStatus,
1029
+ resultSummary:
1030
+ claimStatus === "unknown"
1031
+ ? "SIMY could not confirm the Provider result and will not replay this durable step."
1032
+ : safeResultSummary(
1033
+ `SIMY could not start this durable step. ${plainRecovery(error)}`,
1034
+ ),
1035
+ artifactRefs: [],
1036
+ providerTokensUsed: task?.provider_tokens_used || 0,
1037
+ elapsedMs: Math.max(0, Date.now() - executionStartedAt),
1038
+ failureCode: safeMachineCode(
1039
+ error?.code,
1040
+ "durable_local_task_step_failed",
1041
+ ),
1042
+ }).catch((statusError) => onError(statusError));
1043
+ }
1044
+ }
1045
+ } finally {
1046
+ if (heartbeatTimer) clearIntervalImpl(heartbeatTimer);
1047
+ active = null;
1048
+ releaseWork?.();
1049
+ }
1050
+ };
1051
+
1052
+ const executeClaim = async (claim, releaseWork) => {
1053
+ if (isDurableLocalTaskStepClaim(claim)) {
1054
+ return executeStepClaim(claim, releaseWork);
1055
+ }
1056
+ let task = null;
1057
+ let heartbeatTimer = null;
1058
+ let heartbeatInFlight = null;
1059
+ let knownLeaseExpiry = Number.NaN;
1060
+ let boundary = "not_started";
1061
+ let leaseLost = false;
1062
+ let cancelRequested = false;
1063
+ let materializedAttachmentRefs = null;
1064
+
1065
+ const stopForLeaseLoss = async (error) => {
1066
+ if (leaseLost) return;
1067
+ leaseLost = true;
1068
+ onError(
1069
+ recoveryError(
1070
+ "SIMY lost the execution lease and stopped the local Provider to prevent duplicate work.",
1071
+ error,
1072
+ "local_task_lease_lost",
1073
+ ),
1074
+ );
1075
+ if (task) await stopTask(task).catch(() => undefined);
1076
+ };
1077
+
1078
+ try {
1079
+ const normalized = normalizeClaim(claim, { leaseSeconds });
1080
+ knownLeaseExpiry = normalized.leaseExpiresAtMs;
1081
+ const workspace = await resolveWorkspace(normalized, {
1082
+ storageRoot,
1083
+ workspaceRoot,
1084
+ prepareWorkspace,
1085
+ resolveRepository,
1086
+ });
1087
+ let attachments = [];
1088
+ if (normalized.attachmentRefs.length > 0) {
1089
+ if (!attachmentStore || typeof attachmentStore.materialize !== "function") {
1090
+ throw workerError(
1091
+ "Update SIMY CLI before running Local Tasks with attached files.",
1092
+ "durable_local_task_attachments_unavailable",
1093
+ );
1094
+ }
1095
+ const deviceId = getDeviceId();
1096
+ attachments = await attachmentStore.materialize({
1097
+ refs: normalized.attachmentRefs,
1098
+ deviceId,
1099
+ inputsRoot: workspace.inputs_root,
1100
+ });
1101
+ materializedAttachmentRefs = normalized.attachmentRefs;
1102
+ }
1103
+ validateLocalTaskScenarioInputs(normalized.scenarioPack, {
1104
+ instruction: normalized.instruction,
1105
+ attachments,
1106
+ });
1107
+ task = createLocalTask({
1108
+ id: workspace.localTaskId,
1109
+ instruction: normalized.instruction,
1110
+ backend: normalized.spec.backend,
1111
+ operation: normalized.spec.operation,
1112
+ permissionMode: normalized.spec.permission_mode,
1113
+ workspaceMode: normalized.spec.workspace_mode,
1114
+ repository: normalized.spec.repository,
1115
+ workspacePath: workspace.workspace_path,
1116
+ taskRoot: workspace.task_root,
1117
+ inputsRoot: workspace.inputs_root,
1118
+ outputsRoot: workspace.outputs_root,
1119
+ outputsRootExpected: workspace.outputs_root_expected,
1120
+ attachments,
1121
+ expectedOutputs: normalized.spec.expected_outputs,
1122
+ scenarioPack: normalized.scenarioPack,
1123
+ providerTokenBudget: normalized.spec.provider_token_budget,
1124
+ timeoutMs: normalized.spec.timeout_ms,
1125
+ });
1126
+ task.durable_turn_id = normalized.turnId;
1127
+ task.durable_lease_epoch = normalized.leaseEpoch;
1128
+ await persist(task);
1129
+ registry.create(task);
1130
+ if (active) active.task = task;
1131
+
1132
+ const heartbeat = async () => {
1133
+ if (!active || active.claim.turn_id !== normalized.turnId) return;
1134
+ try {
1135
+ const response = await api.heartbeat({
1136
+ turnId: normalized.turnId,
1137
+ workerId,
1138
+ leaseEpoch: normalized.leaseEpoch,
1139
+ leaseSeconds,
1140
+ });
1141
+ if (response.ok !== true) {
1142
+ await stopForLeaseLoss(
1143
+ workerError(
1144
+ "SIMY Web did not renew the execution lease.",
1145
+ "local_task_lease_not_renewed",
1146
+ ),
1147
+ );
1148
+ return;
1149
+ }
1150
+ knownLeaseExpiry = Date.now() + leaseSeconds * 1_000;
1151
+ if (response.cancelRequested) {
1152
+ cancelRequested = true;
1153
+ await stopTask(task);
1154
+ }
1155
+ } catch (error) {
1156
+ if (
1157
+ error instanceof DurableLocalTaskApiError &&
1158
+ error.status === 409
1159
+ ) {
1160
+ await stopForLeaseLoss(error);
1161
+ return;
1162
+ }
1163
+ onError(
1164
+ recoveryError(
1165
+ "SIMY could not renew this Local Task yet; it will keep trying while the lease is safe.",
1166
+ error,
1167
+ "local_task_heartbeat_failed",
1168
+ ),
1169
+ );
1170
+ if (
1171
+ Number.isFinite(knownLeaseExpiry) &&
1172
+ Date.now() + heartbeatIntervalMs >= knownLeaseExpiry
1173
+ ) {
1174
+ await stopForLeaseLoss(error);
1175
+ }
1176
+ }
1177
+ };
1178
+ const runHeartbeat = () => {
1179
+ if (!heartbeatInFlight) {
1180
+ heartbeatInFlight = heartbeat().finally(() => {
1181
+ heartbeatInFlight = null;
1182
+ });
1183
+ }
1184
+ return heartbeatInFlight;
1185
+ };
1186
+
1187
+ heartbeatTimer = setIntervalImpl(() => {
1188
+ void runHeartbeat();
1189
+ }, heartbeatIntervalMs);
1190
+ heartbeatTimer?.unref?.();
1191
+
1192
+ const completed = await runLocalTask(task, {
1193
+ beforeProviderInvocation: async () => {
1194
+ if (leaseLost || cancelRequested || closed) {
1195
+ throw workerError(
1196
+ "The Local Task was stopped before the Provider started.",
1197
+ "local_task_cancelled_before_invocation",
1198
+ );
1199
+ }
1200
+ try {
1201
+ await markInvocationStartedWithReplay(api, {
1202
+ turnId: normalized.turnId,
1203
+ workerId,
1204
+ leaseEpoch: normalized.leaseEpoch,
1205
+ });
1206
+ boundary = "started";
1207
+ } catch (error) {
1208
+ boundary = error?.ambiguous ? "unknown" : "not_started";
1209
+ throw workerError(
1210
+ error?.ambiguous
1211
+ ? "SIMY could not confirm whether the Provider boundary was recorded. The task was not started again."
1212
+ : `SIMY could not authorize the Provider invocation: ${errorMessage(error)}`,
1213
+ error?.ambiguous
1214
+ ? "local_provider_boundary_unknown"
1215
+ : "local_provider_invocation_not_authorized",
1216
+ error,
1217
+ );
1218
+ }
1219
+ },
1220
+ });
1221
+
1222
+ if (heartbeatTimer) {
1223
+ clearIntervalImpl(heartbeatTimer);
1224
+ heartbeatTimer = null;
1225
+ }
1226
+ await runHeartbeat();
1227
+ if (leaseLost) return;
1228
+ // Report the Provider operation's observed outcome. A cancellation that
1229
+ // arrives after Provider completion still cancels the parent Turn on the
1230
+ // server, but must not rewrite a known successful operation as cancelled.
1231
+ const status = completed.status === "stopped"
1232
+ ? "cancelled"
1233
+ : boundary === "unknown"
1234
+ ? "unknown"
1235
+ : completed.status === "succeeded"
1236
+ ? "succeeded"
1237
+ : "failed";
1238
+ const failureCode =
1239
+ status === "failed" || status === "unknown"
1240
+ ? safeMachineCode(completed.reason_code, "local_task_failed")
1241
+ : null;
1242
+ let reportStatus = status;
1243
+ let reportFailureCode = failureCode;
1244
+ let reportSummary = durableResultSummary(completed, status, failureCode);
1245
+ let artifactRefs = [];
1246
+ try {
1247
+ artifactRefs = buildLocalTaskArtifactRefs(completed.artifacts, completed.id);
1248
+ if (
1249
+ reportStatus === "succeeded" &&
1250
+ normalized.pipelineContext &&
1251
+ artifactRefs.length > 0
1252
+ ) {
1253
+ artifactRefs = await materializeArtifacts({
1254
+ api,
1255
+ turnId: normalized.turnId,
1256
+ task: completed,
1257
+ artifactRefs,
1258
+ });
1259
+ }
1260
+ } catch (error) {
1261
+ const contractFailure = error instanceof LocalTaskArtifactContractError;
1262
+ // Pipeline status accepts only refs that already have a durable
1263
+ // crew_artifact link. Keep locally verified files on disk for recovery,
1264
+ // but never report an unmaterialized manifest to Backend.
1265
+ artifactRefs = [];
1266
+ if (status !== "cancelled") {
1267
+ reportStatus = status === "unknown" ? "unknown" : "failed";
1268
+ reportFailureCode = contractFailure
1269
+ ? "artifact_verification_failed"
1270
+ : "artifact_materialization_failed";
1271
+ reportSummary =
1272
+ contractFailure
1273
+ ? "SIMY could not safely verify the Local Task output files."
1274
+ : "SIMY created the output files locally but could not safely attach them to this Pipeline run. Keep SIMY CLI running, check the connection, and start a new Pipeline run.";
1275
+ completed.status = reportStatus === "failed" ? "failed" : completed.status;
1276
+ completed.reason_code = reportFailureCode;
1277
+ completed.error = error.message;
1278
+ if (contractFailure) {
1279
+ completed.artifacts = [];
1280
+ if (completed.result) completed.result.artifacts = [];
1281
+ }
1282
+ await persist(completed);
1283
+ }
1284
+ onError(contractFailure
1285
+ ? recoveryError(
1286
+ "SIMY rejected unsafe Local Task output metadata before sending it to SIMY Web.",
1287
+ error,
1288
+ reportFailureCode,
1289
+ )
1290
+ : workerError(
1291
+ `SIMY could not persist the Local Task output files for this Pipeline run. ${errorMessage(error)} Keep SIMY CLI running, check the connection, and start a new Pipeline run.`,
1292
+ reportFailureCode,
1293
+ error,
1294
+ ));
1295
+ }
1296
+ let statusConfirmed;
1297
+ try {
1298
+ statusConfirmed = await updateStatusWithResolution(api, {
1299
+ turnId: normalized.turnId,
1300
+ workerId,
1301
+ leaseEpoch: normalized.leaseEpoch,
1302
+ status: reportStatus,
1303
+ resultSummary: reportSummary,
1304
+ artifactRefs,
1305
+ failureCode: reportFailureCode,
1306
+ });
1307
+ } catch (error) {
1308
+ onError(
1309
+ recoveryError(
1310
+ "SIMY completed the local Provider call but could not save its final server state. It will not rerun the Provider.",
1311
+ error,
1312
+ "local_task_status_unavailable",
1313
+ ),
1314
+ );
1315
+ return;
1316
+ }
1317
+ if (!statusConfirmed) {
1318
+ onError(
1319
+ recoveryError(
1320
+ "SIMY stopped this Local Task but could not confirm its final server state. It will not rerun the Provider.",
1321
+ new Error("The execution lease was already fenced."),
1322
+ "local_task_status_unconfirmed",
1323
+ ),
1324
+ );
1325
+ }
1326
+ } catch (error) {
1327
+ if (!leaseLost) {
1328
+ onError(
1329
+ recoveryError(
1330
+ "SIMY could not execute this Local Task safely.",
1331
+ error,
1332
+ safeMachineCode(error?.code, "durable_local_task_failed"),
1333
+ ),
1334
+ );
1335
+ const claimStatus =
1336
+ boundary === "not_started" ? "failed" : "unknown";
1337
+ const statusConfirmed = await updateStatusWithResolution(api, {
1338
+ turnId: claim.turn_id,
1339
+ workerId,
1340
+ leaseEpoch: claim.lease_epoch,
1341
+ status: claimStatus,
1342
+ resultSummary:
1343
+ claimStatus === "unknown"
1344
+ ? "SIMY did not start this task again because the Provider boundary could not be confirmed. Check the task before retrying."
1345
+ : safeResultSummary(
1346
+ `SIMY could not start this Local Task. ${plainRecovery(error)}`,
1347
+ ),
1348
+ artifactRefs: [],
1349
+ failureCode: safeMachineCode(error?.code, "durable_local_task_failed"),
1350
+ }).catch((statusError) => {
1351
+ onError(statusError);
1352
+ return false;
1353
+ });
1354
+ if (!statusConfirmed) {
1355
+ onError(
1356
+ recoveryError(
1357
+ "SIMY will not rerun this Provider because the final task state could not be confirmed.",
1358
+ new Error("The execution lease was already fenced."),
1359
+ "local_task_status_unconfirmed",
1360
+ ),
1361
+ );
1362
+ }
1363
+ }
1364
+ } finally {
1365
+ if (heartbeatTimer) clearIntervalImpl(heartbeatTimer);
1366
+ if (
1367
+ materializedAttachmentRefs &&
1368
+ boundary === "started" &&
1369
+ attachmentStore &&
1370
+ typeof attachmentStore.cleanup === "function"
1371
+ ) {
1372
+ await attachmentStore.cleanup({
1373
+ refs: materializedAttachmentRefs,
1374
+ deviceId: getDeviceId(),
1375
+ }).catch((error) => onError(recoveryError(
1376
+ "SIMY could not remove the staged attachment copy.",
1377
+ error,
1378
+ "local_task_attachment_cleanup_failed",
1379
+ )));
1380
+ }
1381
+ active = null;
1382
+ releaseWork?.();
1383
+ }
1384
+ };
1385
+
1386
+ return {
1387
+ get workerId() {
1388
+ return workerId;
1389
+ },
1390
+ snapshot() {
1391
+ return {
1392
+ closed,
1393
+ polling: Boolean(pollPromise),
1394
+ active_turn_id: active?.claim?.turn_id || null,
1395
+ durable_step_disposition: lastStepDisposition,
1396
+ pending_step_turn_id: pendingStepTurnId,
1397
+ local_task: active?.task ? localTaskSnapshot(active.task) : null,
1398
+ };
1399
+ },
1400
+ async poll() {
1401
+ if (closed || active || pollPromise) return false;
1402
+ const releaseWork = beginWork?.();
1403
+ if (!releaseWork) return false;
1404
+ pollPromise = (async () => {
1405
+ try {
1406
+ const requestedTurnId = pendingStepTurnId;
1407
+ if (
1408
+ !requestedTurnId &&
1409
+ typeof api.claimSubtaskParent === "function"
1410
+ ) {
1411
+ const parentEnvelope = await api.claimSubtaskParent({
1412
+ workerId,
1413
+ leaseSeconds,
1414
+ });
1415
+ const parentClaim = normalizeDurableLocalTaskExecutionClaimV2(
1416
+ parentEnvelope,
1417
+ { workerId, leaseSeconds },
1418
+ );
1419
+ if (parentClaim.disposition === "claimed") {
1420
+ const pool = createSubtaskPool({
1421
+ api,
1422
+ parentAuthority: parentClaim.parentAuthority,
1423
+ batchPlan: parentClaim.batchPlan,
1424
+ storageRoot,
1425
+ workspaceRoot,
1426
+ runSubtask: executeBoundedRuntime,
1427
+ stopSubtask: async ({ attemptId }) => {
1428
+ const task = boundedTasks.get(attemptId);
1429
+ if (task) await stopTask(task);
1430
+ },
1431
+ prepareWorkspace,
1432
+ leaseSeconds,
1433
+ heartbeatIntervalMs,
1434
+ setIntervalImpl,
1435
+ clearIntervalImpl,
1436
+ onError,
1437
+ });
1438
+ active = {
1439
+ claim: { turn_id: parentClaim.turnId },
1440
+ task: null,
1441
+ subtaskPool: pool,
1442
+ };
1443
+ const executionPromise = pool.run()
1444
+ .catch((error) => {
1445
+ onError(
1446
+ recoveryError(
1447
+ "SIMY could not finish the bounded Local Task pool safely.",
1448
+ error,
1449
+ "bounded_local_task_subtask_pool_failed",
1450
+ ),
1451
+ );
1452
+ })
1453
+ .finally(() => {
1454
+ active = null;
1455
+ releaseWork();
1456
+ if (activeExecutionPromise === executionPromise) {
1457
+ activeExecutionPromise = null;
1458
+ }
1459
+ });
1460
+ activeExecutionPromise = executionPromise;
1461
+ void executionPromise;
1462
+ return true;
1463
+ }
1464
+ }
1465
+ let claim = await api.claim({
1466
+ workerId,
1467
+ leaseSeconds,
1468
+ turnId: requestedTurnId,
1469
+ });
1470
+ if (!claim) {
1471
+ if (requestedTurnId) {
1472
+ throw workerError(
1473
+ "SIMY Web advanced the durable plan but did not return its next step.",
1474
+ "local_task_plan_handoff_unavailable",
1475
+ );
1476
+ }
1477
+ releaseWork();
1478
+ return false;
1479
+ }
1480
+ if (isLocalTaskPlanOrchestration(claim)) {
1481
+ const orchestration = claim;
1482
+ const controlledPlan = buildControlledLocalTaskPlan(orchestration);
1483
+ await prepareDurableStepAttachments({
1484
+ orchestration,
1485
+ storageRoot,
1486
+ workspaceRoot,
1487
+ prepareWorkspace,
1488
+ attachmentStore,
1489
+ deviceId: getDeviceId(),
1490
+ onError,
1491
+ });
1492
+ claim = await claimControlledPlanWithReplay(api, {
1493
+ workerId,
1494
+ leaseSeconds,
1495
+ turnId: orchestration.turn_id,
1496
+ controlledPlan,
1497
+ });
1498
+ if (!claim || isLocalTaskPlanOrchestration(claim)) {
1499
+ throw workerError(
1500
+ "SIMY Web did not initialize the controlled Local Task plan.",
1501
+ "local_task_plan_not_initialized",
1502
+ );
1503
+ }
1504
+ if (claim.turn_id !== orchestration.turn_id) {
1505
+ throw workerError(
1506
+ "SIMY Web returned a different Local Task after plan initialization.",
1507
+ "local_task_plan_claim_mismatch",
1508
+ );
1509
+ }
1510
+ }
1511
+ if (requestedTurnId && claim.turn_id !== requestedTurnId) {
1512
+ throw workerError(
1513
+ "SIMY Web returned a different Local Task during the durable plan handoff.",
1514
+ "local_task_plan_claim_mismatch",
1515
+ );
1516
+ }
1517
+ if (isDurableLocalTaskStepClaim(claim)) {
1518
+ const normalized = normalizeDurableLocalTaskStepClaim(claim, {
1519
+ leaseSeconds,
1520
+ });
1521
+ lastStepDisposition = normalized.disposition;
1522
+ if (requestedTurnId) pendingStepTurnId = null;
1523
+ if (normalized.disposition !== "execute") {
1524
+ // Approval checkpoints and unknown Provider results are durable
1525
+ // Backend states, not executable leases. Keep no process,
1526
+ // heartbeat, or update-manager work reservation while waiting.
1527
+ releaseWork();
1528
+ return false;
1529
+ }
1530
+ } else {
1531
+ if (requestedTurnId) {
1532
+ throw workerError(
1533
+ "SIMY Web did not return a durable step for the advanced plan.",
1534
+ "local_task_plan_handoff_invalid",
1535
+ );
1536
+ }
1537
+ lastStepDisposition = null;
1538
+ }
1539
+ active = { claim, task: null };
1540
+ const executionPromise = executeClaim(claim, releaseWork)
1541
+ .catch((error) => {
1542
+ onError(
1543
+ recoveryError(
1544
+ "SIMY could not finish the active Local Task worker safely.",
1545
+ error,
1546
+ "durable_local_task_worker_failed",
1547
+ ),
1548
+ );
1549
+ })
1550
+ .finally(() => {
1551
+ if (activeExecutionPromise === executionPromise) {
1552
+ activeExecutionPromise = null;
1553
+ }
1554
+ });
1555
+ activeExecutionPromise = executionPromise;
1556
+ void executionPromise;
1557
+ return true;
1558
+ } catch (error) {
1559
+ releaseWork();
1560
+ onError(
1561
+ recoveryError(
1562
+ "SIMY could not check for My AI Local Tasks. Keep SIMY running; it will try again automatically.",
1563
+ error,
1564
+ "local_task_claim_failed",
1565
+ ),
1566
+ );
1567
+ return false;
1568
+ } finally {
1569
+ pollPromise = null;
1570
+ }
1571
+ })();
1572
+ return pollPromise;
1573
+ },
1574
+ async waitForIdle() {
1575
+ await pollPromise;
1576
+ await activeExecutionPromise;
1577
+ },
1578
+ async close() {
1579
+ closed = true;
1580
+ await pollPromise;
1581
+ await active?.subtaskPool?.close?.().catch(() => undefined);
1582
+ if (active?.task) await stopTask(active.task).catch(() => undefined);
1583
+ await activeExecutionPromise;
1584
+ },
1585
+ };
1586
+ }
1587
+
1588
+ async function claimControlledPlanWithReplay(api, input) {
1589
+ let lastError = null;
1590
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1591
+ try {
1592
+ return await api.claim(input);
1593
+ } catch (error) {
1594
+ lastError = error;
1595
+ const retryable =
1596
+ error?.ambiguous === true ||
1597
+ (Number.isInteger(error?.status) && error.status >= 500);
1598
+ if (!retryable || attempt > 0) throw error;
1599
+ }
1600
+ }
1601
+ throw lastError;
1602
+ }
1603
+
1604
+ async function markInvocationStartedWithReplay(api, input) {
1605
+ let lastError = null;
1606
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1607
+ try {
1608
+ if (await api.markInvocationStarted(input)) return true;
1609
+ lastError = new DurableLocalTaskApiError(
1610
+ "SIMY Web did not confirm the Provider boundary.",
1611
+ { code: "local_provider_boundary_not_confirmed" },
1612
+ );
1613
+ } catch (error) {
1614
+ lastError = error;
1615
+ if (!error?.ambiguous || attempt > 0) throw error;
1616
+ }
1617
+ }
1618
+ throw lastError;
1619
+ }
1620
+
1621
+ async function updateStatusWithResolution(api, input) {
1622
+ let lastError = null;
1623
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1624
+ try {
1625
+ return await api.updateStatus(input);
1626
+ } catch (error) {
1627
+ lastError = error;
1628
+ // A response can be lost after the durable transaction commits. A 409
1629
+ // is not proof of commit: report it as unconfirmed, never rerun Provider.
1630
+ if (error instanceof DurableLocalTaskApiError && error.status === 409) {
1631
+ return false;
1632
+ }
1633
+ const retryable =
1634
+ error?.ambiguous === true ||
1635
+ (Number.isInteger(error?.status) && error.status >= 500);
1636
+ if (!retryable || attempt > 0) throw error;
1637
+ }
1638
+ }
1639
+ throw lastError;
1640
+ }
1641
+
1642
+ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
1643
+ const turnId = String(claim?.turn_id || "");
1644
+ const leaseEpoch = Number(claim?.lease_epoch);
1645
+ if (!/^[a-f0-9-]{36}$/i.test(turnId) || !Number.isInteger(leaseEpoch) || leaseEpoch < 1) {
1646
+ throw workerError("SIMY Web returned an invalid Local Task lease.", "invalid_local_task_claim");
1647
+ }
1648
+ const leaseExpiresAtMs = Date.parse(claim?.lease_expires_at);
1649
+ const maximumLeaseExpiry =
1650
+ nowMs + Number(leaseSeconds) * 1_000 + CLAIM_LEASE_EXPIRY_TOLERANCE_MS;
1651
+ if (
1652
+ !Number.isFinite(leaseExpiresAtMs) ||
1653
+ leaseExpiresAtMs <= nowMs ||
1654
+ !Number.isFinite(maximumLeaseExpiry) ||
1655
+ leaseExpiresAtMs > maximumLeaseExpiry
1656
+ ) {
1657
+ throw workerError(
1658
+ "SIMY Web returned an invalid Local Task lease expiry.",
1659
+ "invalid_local_task_claim",
1660
+ );
1661
+ }
1662
+ const payload = claim.request_payload;
1663
+ const spec = claim.execution_spec;
1664
+ if (!payload || typeof payload !== "object" || payload.mode !== "local_task") {
1665
+ throw workerError("SIMY Web returned an invalid Local Task request.", "invalid_local_task_request");
1666
+ }
1667
+ const instruction = String(payload.input || "").trim();
1668
+ if (!instruction) {
1669
+ throw workerError("The Local Task has no instruction.", "local_task_instruction_required");
1670
+ }
1671
+ const attachmentRefs = payload.attachment_refs ?? [];
1672
+ if (!Array.isArray(attachmentRefs)) {
1673
+ throw workerError(
1674
+ "SIMY Web returned invalid attached-file references.",
1675
+ "invalid_local_task_claim",
1676
+ );
1677
+ }
1678
+ validateExecutionSpec(spec);
1679
+ if (
1680
+ attachmentRefs.length > 0 &&
1681
+ spec.attachment_capability_version !== LOCAL_TASK_FILE_CAPABILITY_VERSION
1682
+ ) {
1683
+ throw workerError(
1684
+ "Update SIMY CLI before running this Local Task with attached files.",
1685
+ "unsupported_local_task_attachment_capability",
1686
+ );
1687
+ }
1688
+ const pipelineContext = normalizePipelineContext(spec);
1689
+ const scenarioPack = normalizeLocalTaskScenarioPack(spec.scenario_pack);
1690
+ return {
1691
+ turnId,
1692
+ leaseEpoch,
1693
+ leaseExpiresAtMs,
1694
+ instruction,
1695
+ attachmentRefs,
1696
+ spec,
1697
+ pipelineContext,
1698
+ scenarioPack,
1699
+ };
1700
+ }
1701
+
1702
+ function normalizePipelineContext(spec) {
1703
+ const value = spec?.origin;
1704
+ if (value === null || value === undefined) return null;
1705
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1706
+ throw workerError(
1707
+ "SIMY Web returned an invalid Pipeline Local Task binding.",
1708
+ "invalid_local_task_claim",
1709
+ );
1710
+ }
1711
+ const allowed = new Set([
1712
+ "schema_version",
1713
+ "kind",
1714
+ "pipeline_run_id",
1715
+ "block_run_id",
1716
+ "node_key",
1717
+ "artifact_delivery",
1718
+ ]);
1719
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
1720
+ throw workerError(
1721
+ "SIMY Web returned an invalid Pipeline Local Task binding.",
1722
+ "invalid_local_task_claim",
1723
+ );
1724
+ }
1725
+ const pipelineRunId = String(value.pipeline_run_id || "");
1726
+ const blockRunId = String(value.block_run_id || "");
1727
+ const nodeKey = String(value.node_key || "");
1728
+ if (
1729
+ value.schema_version !== "pipeline_local_task_origin.v1" ||
1730
+ value.kind !== "pipeline_block" ||
1731
+ value.artifact_delivery !== "crew_artifact_link_required" ||
1732
+ !isUuid(pipelineRunId) ||
1733
+ !isUuid(blockRunId) ||
1734
+ !/^[A-Za-z0-9._:-]{1,128}$/.test(nodeKey)
1735
+ ) {
1736
+ throw workerError(
1737
+ "SIMY Web returned an invalid Pipeline Local Task binding.",
1738
+ "invalid_local_task_claim",
1739
+ );
1740
+ }
1741
+ return {
1742
+ pipeline_run_id: pipelineRunId,
1743
+ block_run_id: blockRunId,
1744
+ node_key: nodeKey,
1745
+ };
1746
+ }
1747
+
1748
+ function validateExecutionSpec(spec) {
1749
+ if (!spec || typeof spec !== "object" || spec.schema_version !== SUPPORTED_SCHEMA_VERSION) {
1750
+ throw workerError(
1751
+ "Update SIMY CLI before running this Local Task.",
1752
+ "unsupported_local_task_execution_spec",
1753
+ );
1754
+ }
1755
+ if (!SUPPORTED_BACKENDS.has(spec.backend)) {
1756
+ throw workerError("Unsupported Local Task Provider.", "unsupported_local_task_backend");
1757
+ }
1758
+ if (!SUPPORTED_OPERATIONS.has(spec.operation)) {
1759
+ throw workerError("Unsupported Local Task operation.", "unsupported_local_task_operation");
1760
+ }
1761
+ if (!SUPPORTED_PERMISSION_MODES.has(spec.permission_mode)) {
1762
+ throw workerError("Unsupported Local Task permission.", "unsupported_local_task_permission");
1763
+ }
1764
+ if (!SUPPORTED_WORKSPACE_MODES.has(spec.workspace_mode)) {
1765
+ throw workerError("Unsupported Local Task workspace.", "unsupported_local_task_workspace");
1766
+ }
1767
+ if (Number(spec.max_provider_invocations) !== 1) {
1768
+ throw workerError(
1769
+ "Local Tasks must allow exactly one Provider invocation.",
1770
+ "invalid_local_task_invocation_limit",
1771
+ );
1772
+ }
1773
+ if (spec.operation !== "execute" && spec.permission_mode !== "read_only") {
1774
+ throw workerError(
1775
+ "Read-only Local Tasks cannot request workspace write access.",
1776
+ "invalid_local_task_permission",
1777
+ );
1778
+ }
1779
+ if (spec.workspace_mode === "repository" && !String(spec.repository || "").trim()) {
1780
+ throw workerError(
1781
+ "This Local Task requires a repository.",
1782
+ "local_task_repository_required",
1783
+ );
1784
+ }
1785
+ }
1786
+
1787
+ async function resolveWorkspace(
1788
+ normalized,
1789
+ { storageRoot, workspaceRoot, prepareWorkspace, resolveRepository },
1790
+ ) {
1791
+ const localTaskId =
1792
+ `local_task_${normalized.turnId.replaceAll("-", "")}_${normalized.leaseEpoch}`;
1793
+ const prepared = await prepareWorkspace({
1794
+ taskId: localTaskId,
1795
+ root: storageRoot,
1796
+ workspaceRoot,
1797
+ });
1798
+ if (normalized.spec.workspace_mode !== "repository") {
1799
+ return { ...prepared, localTaskId };
1800
+ }
1801
+ const repository = await resolveRepository(normalized.spec.repository);
1802
+ if (!repository?.local_path) {
1803
+ throw workerError(
1804
+ `${normalized.spec.repository} is not available on this desktop. Open SIMY CLI and allow its repository folder, then try a new task.`,
1805
+ "repository_not_authorized",
1806
+ );
1807
+ }
1808
+ const repositoryWorkspace = await prepareRepositoryWorkspace({
1809
+ repositoryPath: repository.local_path,
1810
+ expectedRepository: normalized.spec.repository,
1811
+ localTaskId,
1812
+ });
1813
+ return {
1814
+ ...prepared,
1815
+ localTaskId,
1816
+ ...repositoryWorkspace,
1817
+ };
1818
+ }
1819
+
1820
+ async function resolveStepWorkspace(
1821
+ normalized,
1822
+ { storageRoot, workspaceRoot, prepareWorkspace, resolveRepository },
1823
+ ) {
1824
+ const workspaceId = durableLocalTaskStepWorkspaceId(normalized.turnId);
1825
+ const localTaskId = durableLocalTaskStepExecutionId(
1826
+ normalized.turnId,
1827
+ normalized.step.step_id,
1828
+ normalized.leaseEpoch,
1829
+ );
1830
+ const prepared = await prepareWorkspace({
1831
+ taskId: workspaceId,
1832
+ root: storageRoot,
1833
+ workspaceRoot,
1834
+ });
1835
+ let resolved = prepared;
1836
+ if (normalized.step.workspace_mode === "repository") {
1837
+ const repository = await resolveRepository(normalized.step.repository);
1838
+ if (!repository?.local_path) {
1839
+ throw workerError(
1840
+ `${normalized.step.repository} is not available on this desktop. Open SIMY CLI and allow its repository folder, then try again.`,
1841
+ "repository_not_authorized",
1842
+ );
1843
+ }
1844
+ resolved = {
1845
+ ...prepared,
1846
+ ...(await prepareRepositoryWorkspace({
1847
+ repositoryPath: repository.local_path,
1848
+ expectedRepository: normalized.step.repository,
1849
+ localTaskId: workspaceId,
1850
+ })),
1851
+ };
1852
+ }
1853
+
1854
+ const outputsRoot = path.join(
1855
+ resolved.outputs_root,
1856
+ normalized.step.step_id,
1857
+ );
1858
+ await mkdir(outputsRoot, { recursive: true, mode: 0o700 });
1859
+ const outputDetails = await lstat(outputsRoot);
1860
+ if (!outputDetails.isDirectory() || outputDetails.isSymbolicLink()) {
1861
+ throw workerError(
1862
+ "The durable step output directory is not safe to use.",
1863
+ "local_task_output_path_unsafe",
1864
+ );
1865
+ }
1866
+ const canonicalOutputsRoot = await realpath(outputsRoot);
1867
+ const canonicalParent = await realpath(resolved.outputs_root);
1868
+ if (
1869
+ path.dirname(canonicalOutputsRoot) !== canonicalParent ||
1870
+ path.basename(canonicalOutputsRoot) !== normalized.step.step_id
1871
+ ) {
1872
+ throw workerError(
1873
+ "The durable step output directory escaped its turn workspace.",
1874
+ "local_task_output_path_unsafe",
1875
+ );
1876
+ }
1877
+
1878
+ return {
1879
+ ...resolved,
1880
+ localTaskId,
1881
+ turn_task_root: prepared.task_root,
1882
+ // Persist each lease attempt independently while keeping inputs and the
1883
+ // Provider workspace stable for the whole turn.
1884
+ task_root: path.join(path.dirname(prepared.task_root), localTaskId),
1885
+ outputs_root: canonicalOutputsRoot,
1886
+ outputs_root_expected: canonicalOutputsRoot,
1887
+ };
1888
+ }
1889
+
1890
+ async function prepareDurableStepAttachments({
1891
+ orchestration,
1892
+ storageRoot,
1893
+ workspaceRoot,
1894
+ prepareWorkspace,
1895
+ attachmentStore,
1896
+ deviceId,
1897
+ onError,
1898
+ }) {
1899
+ const refs = orchestration?.request_payload?.attachment_refs ?? [];
1900
+ if (!Array.isArray(refs)) {
1901
+ throw workerError(
1902
+ "SIMY Web returned invalid attached-file references.",
1903
+ "invalid_local_task_claim",
1904
+ );
1905
+ }
1906
+ if (refs.length === 0) return [];
1907
+ if (
1908
+ orchestration?.execution_spec?.attachment_capability_version !==
1909
+ LOCAL_TASK_FILE_CAPABILITY_VERSION
1910
+ ) {
1911
+ throw workerError(
1912
+ "Update SIMY CLI before running this Local Task with attached files.",
1913
+ "unsupported_local_task_attachment_capability",
1914
+ );
1915
+ }
1916
+ if (!attachmentStore || typeof attachmentStore.materialize !== "function") {
1917
+ throw workerError(
1918
+ "This SIMY CLI cannot safely receive attached files.",
1919
+ "unsupported_local_task_attachment_capability",
1920
+ );
1921
+ }
1922
+
1923
+ const turnId = String(orchestration.turn_id || "");
1924
+ const workspaceId = durableLocalTaskStepWorkspaceId(turnId);
1925
+ const prepared = await prepareWorkspace({
1926
+ taskId: workspaceId,
1927
+ root: storageRoot,
1928
+ workspaceRoot,
1929
+ });
1930
+ const sourceRefsSha256 = createHash("sha256")
1931
+ .update(JSON.stringify(refs))
1932
+ .digest("hex");
1933
+ const existing = await loadDurableStepAttachments({
1934
+ turnId,
1935
+ taskRoot: prepared.task_root,
1936
+ inputsRoot: prepared.inputs_root,
1937
+ expectedSourceRefsSha256: sourceRefsSha256,
1938
+ allowMissing: true,
1939
+ });
1940
+ if (existing) return existing;
1941
+
1942
+ const attachments = await attachmentStore.materialize({
1943
+ refs,
1944
+ deviceId,
1945
+ inputsRoot: prepared.inputs_root,
1946
+ });
1947
+ await persistDurableStepAttachmentManifest({
1948
+ turnId,
1949
+ taskRoot: prepared.task_root,
1950
+ sourceRefsSha256,
1951
+ attachments,
1952
+ });
1953
+ if (typeof attachmentStore.cleanup === "function") {
1954
+ await attachmentStore.cleanup({ refs, deviceId }).catch((error) =>
1955
+ onError(
1956
+ recoveryError(
1957
+ "SIMY cached the attached files for this plan but could not remove their staging copy.",
1958
+ error,
1959
+ "local_task_attachment_cleanup_failed",
1960
+ ),
1961
+ ));
1962
+ }
1963
+ return attachments;
1964
+ }
1965
+
1966
+ async function loadDurableStepAttachments({
1967
+ turnId,
1968
+ taskRoot,
1969
+ inputsRoot,
1970
+ expectedSourceRefsSha256 = null,
1971
+ allowMissing = false,
1972
+ }) {
1973
+ const manifestPath = path.join(
1974
+ taskRoot,
1975
+ DURABLE_STEP_ATTACHMENT_MANIFEST_NAME,
1976
+ );
1977
+ let manifest;
1978
+ try {
1979
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1980
+ } catch (error) {
1981
+ if (allowMissing && error?.code === "ENOENT") return null;
1982
+ if (!allowMissing && error?.code === "ENOENT") return [];
1983
+ throw workerError(
1984
+ "SIMY could not verify the durable task attachment manifest.",
1985
+ "local_task_attachment_manifest_invalid",
1986
+ error,
1987
+ );
1988
+ }
1989
+ if (
1990
+ !manifest ||
1991
+ typeof manifest !== "object" ||
1992
+ Array.isArray(manifest) ||
1993
+ manifest.schema_version !== DURABLE_STEP_ATTACHMENT_MANIFEST_SCHEMA ||
1994
+ manifest.turn_id !== turnId ||
1995
+ !/^[a-f0-9]{64}$/.test(manifest.source_refs_sha256) ||
1996
+ (expectedSourceRefsSha256 !== null &&
1997
+ manifest.source_refs_sha256 !== expectedSourceRefsSha256) ||
1998
+ !Array.isArray(manifest.attachments)
1999
+ ) {
2000
+ throw workerError(
2001
+ "SIMY rejected a mismatched durable task attachment manifest.",
2002
+ "local_task_attachment_manifest_invalid",
2003
+ );
2004
+ }
2005
+
2006
+ const requestedInputsRoot = path.resolve(inputsRoot);
2007
+ const canonicalInputsRoot = await realpath(requestedInputsRoot);
2008
+ const verified = [];
2009
+ for (const attachment of manifest.attachments) {
2010
+ if (
2011
+ !attachment ||
2012
+ typeof attachment !== "object" ||
2013
+ Array.isArray(attachment) ||
2014
+ attachment.integrity_status !== "verified" ||
2015
+ !Number.isInteger(attachment.size_bytes) ||
2016
+ attachment.size_bytes < 0 ||
2017
+ !/^[a-f0-9]{64}$/.test(attachment.sha256) ||
2018
+ typeof attachment.local_path !== "string"
2019
+ ) {
2020
+ throw workerError(
2021
+ "SIMY rejected invalid durable task attachment metadata.",
2022
+ "local_task_attachment_manifest_invalid",
2023
+ );
2024
+ }
2025
+ const candidate = path.resolve(attachment.local_path);
2026
+ const relative = path.relative(requestedInputsRoot, candidate);
2027
+ if (
2028
+ relative === "" ||
2029
+ relative.startsWith(`..${path.sep}`) ||
2030
+ relative === ".." ||
2031
+ path.isAbsolute(relative)
2032
+ ) {
2033
+ throw workerError(
2034
+ "SIMY rejected a durable task attachment outside its input workspace.",
2035
+ "local_task_attachment_path_unsafe",
2036
+ );
2037
+ }
2038
+ const details = await lstat(candidate).catch(() => null);
2039
+ if (!details?.isFile() || details.isSymbolicLink()) {
2040
+ throw workerError(
2041
+ "A durable task attachment is no longer a safe regular file.",
2042
+ "local_task_attachment_path_unsafe",
2043
+ );
2044
+ }
2045
+ const canonicalPath = await realpath(candidate);
2046
+ if (path.dirname(canonicalPath) !== canonicalInputsRoot) {
2047
+ throw workerError(
2048
+ "SIMY rejected an unsafe durable task attachment path.",
2049
+ "local_task_attachment_path_unsafe",
2050
+ );
2051
+ }
2052
+ const bytes = await readFile(canonicalPath);
2053
+ const validated = validateLocalTaskFile(
2054
+ { ...attachment, bytes },
2055
+ { direction: "input", requireBytes: true },
2056
+ );
2057
+ const attachmentId = String(attachment.id || "");
2058
+ if (!/^attachment_[A-Za-z0-9-]{36}$/.test(attachmentId)) {
2059
+ throw workerError(
2060
+ "SIMY rejected an invalid durable task attachment identifier.",
2061
+ "local_task_attachment_manifest_invalid",
2062
+ );
2063
+ }
2064
+ verified.push({
2065
+ id: attachmentId,
2066
+ name: validated.name,
2067
+ mime_type: validated.mime_type,
2068
+ size_bytes: validated.size_bytes,
2069
+ sha256: validated.sha256,
2070
+ local_path: canonicalPath,
2071
+ integrity_status: "verified",
2072
+ executor_handoff_status: "pending",
2073
+ storage: "managed_copy",
2074
+ capability_version: validated.capability_version,
2075
+ });
2076
+ }
2077
+ return verified;
2078
+ }
2079
+
2080
+ async function persistDurableStepAttachmentManifest({
2081
+ turnId,
2082
+ taskRoot,
2083
+ sourceRefsSha256,
2084
+ attachments,
2085
+ }) {
2086
+ await mkdir(taskRoot, { recursive: true, mode: 0o700 });
2087
+ const manifestPath = path.join(
2088
+ taskRoot,
2089
+ DURABLE_STEP_ATTACHMENT_MANIFEST_NAME,
2090
+ );
2091
+ const temporaryPath = `${manifestPath}.${randomUUID()}.tmp`;
2092
+ const payload = JSON.stringify({
2093
+ schema_version: DURABLE_STEP_ATTACHMENT_MANIFEST_SCHEMA,
2094
+ turn_id: turnId,
2095
+ source_refs_sha256: sourceRefsSha256,
2096
+ attachments,
2097
+ });
2098
+ try {
2099
+ await writeFile(temporaryPath, payload, { flag: "wx", mode: 0o600 });
2100
+ await rename(temporaryPath, manifestPath);
2101
+ } catch (error) {
2102
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
2103
+ throw error;
2104
+ }
2105
+ }
2106
+
2107
+ async function prepareRepositoryWorkspace({
2108
+ repositoryPath,
2109
+ expectedRepository,
2110
+ localTaskId,
2111
+ }) {
2112
+ const candidate = path.resolve(String(repositoryPath || ""));
2113
+ const candidateDetails = await lstat(candidate).catch(() => null);
2114
+ if (!candidateDetails?.isDirectory() || candidateDetails.isSymbolicLink()) {
2115
+ throw workerError(
2116
+ "The selected repository folder is not safe to use.",
2117
+ "repository_not_authorized",
2118
+ );
2119
+ }
2120
+ const repositoryRoot = await realpath(candidate);
2121
+ const gitRoot = await gitOutput(repositoryRoot, [
2122
+ "rev-parse",
2123
+ "--show-toplevel",
2124
+ ]).catch(() => "");
2125
+ const canonicalGitRoot = gitRoot
2126
+ ? await realpath(path.resolve(gitRoot)).catch(() => "")
2127
+ : "";
2128
+ if (!canonicalGitRoot || canonicalGitRoot !== repositoryRoot) {
2129
+ throw workerError(
2130
+ "The selected folder is not the root of an available Git repository.",
2131
+ "repository_not_authorized",
2132
+ );
2133
+ }
2134
+ const currentRemote = await gitOutput(repositoryRoot, [
2135
+ "remote",
2136
+ "get-url",
2137
+ "origin",
2138
+ ]).catch(() => "");
2139
+ if (
2140
+ normalizeGitHubRemote(currentRemote)?.toLowerCase() !==
2141
+ normalizeGitHubRemote(expectedRepository)?.toLowerCase()
2142
+ ) {
2143
+ throw workerError(
2144
+ "The selected repository no longer matches the repository authorized in SIMY CLI. Scan repositories again, then start a new task.",
2145
+ "repository_not_authorized",
2146
+ );
2147
+ }
2148
+
2149
+ const trackedOutputs = await gitOutput(repositoryRoot, [
2150
+ "ls-files",
2151
+ "--",
2152
+ ".simy/local-tasks",
2153
+ ]);
2154
+ if (trackedOutputs.trim()) {
2155
+ throw workerError(
2156
+ "This repository already tracks .simy/local-tasks. Remove it from version control before running a Local Task.",
2157
+ "repository_output_path_tracked",
2158
+ );
2159
+ }
2160
+
2161
+ await assertExistingRepositoryPathSafe(
2162
+ repositoryRoot,
2163
+ [".simy", "local-tasks", localTaskId, "outputs"],
2164
+ );
2165
+ await ensureRepositoryOutputExcluded(repositoryRoot);
2166
+ const taskRoot = await ensureSafeRepositoryDirectory(
2167
+ repositoryRoot,
2168
+ [".simy", "local-tasks", localTaskId],
2169
+ );
2170
+ const outputsRoot = await ensureSafeRepositoryDirectory(
2171
+ repositoryRoot,
2172
+ [".simy", "local-tasks", localTaskId, "outputs"],
2173
+ );
2174
+ await chmod(taskRoot, 0o700);
2175
+ await chmod(outputsRoot, 0o700);
2176
+ await access(outputsRoot, fsConstants.W_OK);
2177
+ return {
2178
+ workspace_path: repositoryRoot,
2179
+ outputs_root: outputsRoot,
2180
+ outputs_root_expected: outputsRoot,
2181
+ };
2182
+ }
2183
+
2184
+ async function assertExistingRepositoryPathSafe(repositoryRoot, segments) {
2185
+ let current = repositoryRoot;
2186
+ for (const segment of segments) {
2187
+ current = path.join(current, segment);
2188
+ const details = await lstat(current).catch((error) => {
2189
+ if (error?.code === "ENOENT") return null;
2190
+ throw error;
2191
+ });
2192
+ if (!details) return;
2193
+ if (!details.isDirectory() || details.isSymbolicLink()) {
2194
+ throw workerError(
2195
+ "The repository Local Task output path is not a safe directory.",
2196
+ "repository_output_path_unsafe",
2197
+ );
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ async function ensureSafeRepositoryDirectory(repositoryRoot, segments) {
2203
+ let current = repositoryRoot;
2204
+ for (const segment of segments) {
2205
+ current = path.join(current, segment);
2206
+ let details = await lstat(current).catch((error) => {
2207
+ if (error?.code === "ENOENT") return null;
2208
+ throw error;
2209
+ });
2210
+ if (!details) {
2211
+ await mkdir(current, { mode: 0o700 });
2212
+ details = await lstat(current);
2213
+ }
2214
+ if (!details.isDirectory() || details.isSymbolicLink()) {
2215
+ throw workerError(
2216
+ "The repository Local Task output path is not a safe directory.",
2217
+ "repository_output_path_unsafe",
2218
+ );
2219
+ }
2220
+ }
2221
+ const canonical = await realpath(current);
2222
+ if (!isWithin(repositoryRoot, canonical)) {
2223
+ throw workerError(
2224
+ "The repository Local Task output path escapes the repository.",
2225
+ "repository_output_path_unsafe",
2226
+ );
2227
+ }
2228
+ return canonical;
2229
+ }
2230
+
2231
+ async function ensureRepositoryOutputExcluded(repositoryRoot) {
2232
+ const commonDirectoryValue = await gitOutput(repositoryRoot, [
2233
+ "rev-parse",
2234
+ "--git-common-dir",
2235
+ ]);
2236
+ const commonDirectory = await realpath(
2237
+ path.resolve(repositoryRoot, commonDirectoryValue),
2238
+ );
2239
+ const excludePath = path.join(commonDirectory, "info", "exclude");
2240
+ const excludeParent = path.dirname(excludePath);
2241
+ await mkdir(excludeParent, { recursive: true, mode: 0o700 });
2242
+ const canonicalParent = await realpath(excludeParent);
2243
+ if (!isWithin(commonDirectory, canonicalParent)) {
2244
+ throw workerError(
2245
+ "The repository Git exclude path is not safe.",
2246
+ "repository_output_path_unsafe",
2247
+ );
2248
+ }
2249
+ const existingDetails = await lstat(excludePath).catch((error) => {
2250
+ if (error?.code === "ENOENT") return null;
2251
+ throw error;
2252
+ });
2253
+ if (existingDetails?.isSymbolicLink() || existingDetails?.isDirectory()) {
2254
+ throw workerError(
2255
+ "The repository Git exclude file is not safe.",
2256
+ "repository_output_path_unsafe",
2257
+ );
2258
+ }
2259
+ const existing = await readFile(excludePath, "utf8").catch((error) => {
2260
+ if (error?.code === "ENOENT") return "";
2261
+ throw error;
2262
+ });
2263
+ if (
2264
+ existing
2265
+ .split(/\r?\n/)
2266
+ .some((line) => line.trim() === REPOSITORY_OUTPUT_EXCLUDE)
2267
+ ) {
2268
+ return;
2269
+ }
2270
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2271
+ await appendFile(
2272
+ excludePath,
2273
+ `${prefix}${REPOSITORY_OUTPUT_EXCLUDE}\n`,
2274
+ { encoding: "utf8", mode: 0o600 },
2275
+ );
2276
+ }
2277
+
2278
+ async function gitOutput(repositoryRoot, args) {
2279
+ const { stdout } = await execFileAsync("git", ["-C", repositoryRoot, ...args], {
2280
+ encoding: "utf8",
2281
+ maxBuffer: 1024 * 1024,
2282
+ });
2283
+ return stdout.trim();
2284
+ }
2285
+
2286
+ function isWithin(root, candidate) {
2287
+ const relative = path.relative(root, candidate);
2288
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
2289
+ }
2290
+
2291
+ export async function materializePipelineArtifacts({
2292
+ api,
2293
+ turnId,
2294
+ task,
2295
+ artifactRefs,
2296
+ }) {
2297
+ if (
2298
+ !api ||
2299
+ typeof api.prepareArtifact !== "function" ||
2300
+ typeof api.uploadArtifact !== "function" ||
2301
+ typeof api.commitArtifact !== "function"
2302
+ ) {
2303
+ throw workerError(
2304
+ "This SIMY CLI cannot persist Pipeline output files. Update SIMY CLI and retry the Pipeline run.",
2305
+ "artifact_materialization_unsupported",
2306
+ );
2307
+ }
2308
+
2309
+ const materialized = [];
2310
+ for (const ref of artifactRefs) {
2311
+ const artifact = await localTaskArtifact(task, ref.id);
2312
+ if (!artifact) {
2313
+ throw workerError(
2314
+ `The Local Task output ${ref.name} changed before it could be attached to the Pipeline run.`,
2315
+ "artifact_materialization_source_changed",
2316
+ );
2317
+ }
2318
+ const bytes = await readFile(artifact.local_path);
2319
+ const digest = createHash("sha256").update(bytes).digest("hex");
2320
+ if (bytes.byteLength !== ref.size_bytes || digest !== ref.sha256.toLowerCase()) {
2321
+ throw workerError(
2322
+ `The Local Task output ${ref.name} changed before it could be attached to the Pipeline run.`,
2323
+ "artifact_materialization_source_changed",
2324
+ );
2325
+ }
2326
+
2327
+ const prepared = await retryAmbiguousArtifactStep(() =>
2328
+ api.prepareArtifact({
2329
+ turnId,
2330
+ localArtifactId: ref.id,
2331
+ name: ref.name,
2332
+ mimeType: ref.mime_type,
2333
+ sizeBytes: ref.size_bytes,
2334
+ sha256: ref.sha256.toLowerCase(),
2335
+ }));
2336
+ validatePreparedArtifact(prepared);
2337
+ await retryAmbiguousArtifactStep(() =>
2338
+ api.uploadArtifact({
2339
+ uploadUrl: prepared.upload_url,
2340
+ method: prepared.method,
2341
+ headers: prepared.headers,
2342
+ bytes,
2343
+ }));
2344
+ const committed = await retryAmbiguousArtifactStep(() =>
2345
+ api.commitArtifact({
2346
+ uploadId: prepared.upload_id,
2347
+ turnId,
2348
+ localArtifactId: ref.id,
2349
+ sha256: ref.sha256.toLowerCase(),
2350
+ sizeBytes: ref.size_bytes,
2351
+ }));
2352
+ materialized.push(validateCommittedArtifact(committed, ref));
2353
+ }
2354
+ return materialized;
2355
+ }
2356
+
2357
+ async function retryAmbiguousArtifactStep(operation) {
2358
+ let lastError = null;
2359
+ for (let attempt = 0; attempt < 2; attempt += 1) {
2360
+ try {
2361
+ return await operation();
2362
+ } catch (error) {
2363
+ lastError = error;
2364
+ const retryable =
2365
+ error?.ambiguous === true ||
2366
+ (Number.isInteger(error?.status) && error.status >= 500);
2367
+ if (!retryable || attempt > 0) throw error;
2368
+ }
2369
+ }
2370
+ throw lastError;
2371
+ }
2372
+
2373
+ function validatePreparedArtifact(value) {
2374
+ if (
2375
+ !value ||
2376
+ typeof value !== "object" ||
2377
+ !/^[A-Za-z0-9._:-]{1,256}$/.test(String(value.upload_id || "")) ||
2378
+ value.method !== "PUT" ||
2379
+ !value.headers ||
2380
+ typeof value.headers !== "object" ||
2381
+ Array.isArray(value.headers)
2382
+ ) {
2383
+ throw workerError(
2384
+ "SIMY Web returned an invalid Pipeline artifact upload.",
2385
+ "artifact_materialization_invalid_response",
2386
+ );
2387
+ }
2388
+ trustedArtifactUploadUrl(value.upload_url);
2389
+ trustedArtifactUploadHeaders(value.headers);
2390
+ }
2391
+
2392
+ function validateCommittedArtifact(value, expected) {
2393
+ const artifact = value?.crew_artifact;
2394
+ if (
2395
+ !artifact ||
2396
+ typeof artifact !== "object" ||
2397
+ !isUuid(String(artifact.artifact_id || "")) ||
2398
+ !/^s3:\/\/[^/]+\/.+/.test(String(artifact.s3_uri || "")) ||
2399
+ artifact.name !== expected.name ||
2400
+ artifact.mime_type !== expected.mime_type ||
2401
+ artifact.size_bytes !== expected.size_bytes ||
2402
+ String(artifact.sha256 || "").toLowerCase() !== expected.sha256.toLowerCase()
2403
+ ) {
2404
+ throw workerError(
2405
+ "SIMY could not confirm the persisted Pipeline output file.",
2406
+ "artifact_materialization_invalid_response",
2407
+ );
2408
+ }
2409
+ return {
2410
+ artifact_id: artifact.artifact_id,
2411
+ s3_uri: artifact.s3_uri,
2412
+ name: artifact.name,
2413
+ mime_type: artifact.mime_type,
2414
+ size_bytes: artifact.size_bytes,
2415
+ sha256: artifact.sha256.toLowerCase(),
2416
+ verified: true,
2417
+ };
2418
+ }
2419
+
2420
+ function trustedArtifactUploadUrl(value) {
2421
+ let target;
2422
+ try {
2423
+ target = new URL(String(value || ""));
2424
+ } catch {
2425
+ throw workerError(
2426
+ "SIMY Web returned an invalid Pipeline artifact upload address.",
2427
+ "artifact_materialization_invalid_response",
2428
+ );
2429
+ }
2430
+ const localHttp =
2431
+ target.protocol === "http:" &&
2432
+ ["127.0.0.1", "localhost", "[::1]"].includes(target.hostname);
2433
+ const awsS3 =
2434
+ target.protocol === "https:" &&
2435
+ /^(?:[a-z0-9][a-z0-9.-]*\.)?s3(?:[.-][a-z0-9-]+)*\.amazonaws\.com$/i.test(
2436
+ target.hostname,
2437
+ );
2438
+ if (target.username || target.password || (!awsS3 && !localHttp)) {
2439
+ throw workerError(
2440
+ "SIMY Web returned an unsafe Pipeline artifact upload address.",
2441
+ "artifact_materialization_invalid_response",
2442
+ );
2443
+ }
2444
+ return target;
2445
+ }
2446
+
2447
+ function trustedSubtaskSourceDownloadUrl(value) {
2448
+ let target;
2449
+ try {
2450
+ target = new URL(String(value || ""));
2451
+ } catch {
2452
+ throw workerError(
2453
+ "SIMY Web returned an invalid bounded subtask download address.",
2454
+ "local_task_subtask_source_invalid_response",
2455
+ );
2456
+ }
2457
+ const hostname = target.hostname.toLowerCase();
2458
+ const localHttp =
2459
+ target.protocol === "http:" &&
2460
+ ["127.0.0.1", "localhost", "[::1]"].includes(hostname);
2461
+ const publicHttps =
2462
+ target.protocol === "https:" &&
2463
+ !isLocalOrPrivateHostname(hostname);
2464
+ if (
2465
+ target.username ||
2466
+ target.password ||
2467
+ target.port === "0" ||
2468
+ (!publicHttps && !localHttp)
2469
+ ) {
2470
+ throw workerError(
2471
+ "SIMY Web returned an unsafe bounded subtask download address.",
2472
+ "local_task_subtask_source_invalid_response",
2473
+ );
2474
+ }
2475
+ return target;
2476
+ }
2477
+
2478
+ function isLocalOrPrivateHostname(hostname) {
2479
+ if (
2480
+ hostname === "localhost" ||
2481
+ hostname.endsWith(".localhost")
2482
+ ) {
2483
+ return true;
2484
+ }
2485
+ const literal = hostname.startsWith("[") && hostname.endsWith("]")
2486
+ ? hostname.slice(1, -1)
2487
+ : hostname;
2488
+ const ipVersion = isIP(literal);
2489
+ if (ipVersion === 4) return isNonPublicIpv4(literal);
2490
+ if (ipVersion !== 6) return false;
2491
+ const normalized = literal.toLowerCase();
2492
+ if (
2493
+ normalized === "::" ||
2494
+ normalized === "::1" ||
2495
+ normalized.startsWith("fc") ||
2496
+ normalized.startsWith("fd") ||
2497
+ /^fe[89ab]/.test(normalized)
2498
+ ) {
2499
+ return true;
2500
+ }
2501
+ const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
2502
+ return mapped ? isNonPublicIpv4(mapped[1]) : false;
2503
+ }
2504
+
2505
+ function isNonPublicIpv4(value) {
2506
+ const octets = value.split(".").map(Number);
2507
+ if (
2508
+ octets.length !== 4 ||
2509
+ octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)
2510
+ ) {
2511
+ return true;
2512
+ }
2513
+ const [first, second] = octets;
2514
+ return (
2515
+ first === 0 ||
2516
+ first === 10 ||
2517
+ first === 127 ||
2518
+ (first === 100 && second >= 64 && second <= 127) ||
2519
+ (first === 169 && second === 254) ||
2520
+ (first === 172 && second >= 16 && second <= 31) ||
2521
+ (first === 192 && second === 168) ||
2522
+ (first === 198 && (second === 18 || second === 19)) ||
2523
+ first >= 224
2524
+ );
2525
+ }
2526
+
2527
+ function trustedArtifactUploadHeaders(value) {
2528
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2529
+ throw workerError(
2530
+ "SIMY Web returned invalid Pipeline artifact upload headers.",
2531
+ "artifact_materialization_invalid_response",
2532
+ );
2533
+ }
2534
+ const headers = {};
2535
+ for (const [key, rawValue] of Object.entries(value)) {
2536
+ const normalizedKey = String(key).trim().toLowerCase();
2537
+ if (
2538
+ !normalizedKey ||
2539
+ ["authorization", "cookie", "proxy-authorization", "host"].includes(normalizedKey) ||
2540
+ typeof rawValue !== "string" ||
2541
+ /[\r\n]/.test(rawValue)
2542
+ ) {
2543
+ throw workerError(
2544
+ "SIMY Web returned unsafe Pipeline artifact upload headers.",
2545
+ "artifact_materialization_invalid_response",
2546
+ );
2547
+ }
2548
+ headers[normalizedKey] = rawValue;
2549
+ }
2550
+ return headers;
2551
+ }
2552
+
2553
+ function isUuid(value) {
2554
+ return /^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(
2555
+ value,
2556
+ );
2557
+ }
2558
+
2559
+ function recoverySummary(status, failureCode) {
2560
+ if (status === "cancelled") return "The Local Task was stopped.";
2561
+ if (status === "unknown") {
2562
+ return "SIMY could not confirm the Provider result. Review the task before retrying.";
2563
+ }
2564
+ return `The Local Task failed (${failureCode || "unknown error"}).`;
2565
+ }
2566
+
2567
+ function durableResultSummary(completed, status, failureCode) {
2568
+ // Process exit details such as signal-derived code 130 are useful only in
2569
+ // the local technical record. A user-requested stop has one stable public
2570
+ // outcome regardless of how the Provider process terminated.
2571
+ if (status === "cancelled") return recoverySummary(status, failureCode);
2572
+ return safeResultSummary(
2573
+ completed.result?.summary ||
2574
+ completed.error ||
2575
+ recoverySummary(status, failureCode),
2576
+ );
2577
+ }
2578
+
2579
+ function safeResultSummary(value) {
2580
+ return String(value || "").slice(0, 20_000);
2581
+ }
2582
+
2583
+ function workerError(message, code, cause = null) {
2584
+ const error = new Error(message, cause ? { cause } : undefined);
2585
+ error.code = safeMachineCode(code, "durable_local_task_failed");
2586
+ return error;
2587
+ }
2588
+
2589
+ function safeMachineCode(value, fallback) {
2590
+ const candidate = typeof value === "string" ? value.trim() : "";
2591
+ return MACHINE_CODE_PATTERN.test(candidate) ? candidate : fallback;
2592
+ }
2593
+
2594
+ function recoveryError(message, error, code) {
2595
+ return workerError(`${message} ${plainRecovery(error)}`, code, error);
2596
+ }
2597
+
2598
+ function plainRecovery(error) {
2599
+ const detail = errorMessage(error);
2600
+ return detail
2601
+ ? `${detail} Keep SIMY CLI running and use “Try again” in My AI after checking the connection.`
2602
+ : "Keep SIMY CLI running and use “Try again” in My AI after checking the connection.";
2603
+ }
2604
+
2605
+ function errorMessage(error) {
2606
+ return error instanceof Error ? error.message : String(error || "");
2607
+ }