mecord-connect 1.0.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.
Files changed (82) hide show
  1. package/DISCLOSURE +47 -0
  2. package/LICENSE +64 -0
  3. package/README.md +44 -0
  4. package/bin/operator.mjs +10 -0
  5. package/package.json +58 -0
  6. package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
  7. package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
  8. package/runtime/app/apps/local-agent/src/cli.js +237 -0
  9. package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
  10. package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
  11. package/runtime/app/apps/local-agent/src/main.js +311 -0
  12. package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
  13. package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
  14. package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
  15. package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
  16. package/runtime/app/apps/local-agent/src/remote.js +130 -0
  17. package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
  18. package/runtime/app/apps/local-agent/src/server.js +578 -0
  19. package/runtime/app/package.json +4 -0
  20. package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
  21. package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
  22. package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
  23. package/runtime/app/src/capabilities/browser-cdp.js +384 -0
  24. package/runtime/app/src/capabilities/browser-managed.js +345 -0
  25. package/runtime/app/src/capabilities/docker.js +448 -0
  26. package/runtime/app/src/capabilities/filesystem.js +264 -0
  27. package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
  28. package/runtime/app/src/capabilities/git-write.js +321 -0
  29. package/runtime/app/src/capabilities/git.js +252 -0
  30. package/runtime/app/src/capabilities/index.js +16 -0
  31. package/runtime/app/src/capabilities/path-scope.js +86 -0
  32. package/runtime/app/src/capabilities/postgres.js +566 -0
  33. package/runtime/app/src/capabilities/process.js +258 -0
  34. package/runtime/app/src/capabilities/project-command.js +599 -0
  35. package/runtime/app/src/capabilities/project-transaction.js +215 -0
  36. package/runtime/app/src/capabilities/project.js +67 -0
  37. package/runtime/app/src/capabilities/system.js +40 -0
  38. package/runtime/app/src/capabilities/vscode.js +312 -0
  39. package/runtime/app/src/capabilities/windows-uia.js +331 -0
  40. package/runtime/app/src/core/account-device-registry.js +604 -0
  41. package/runtime/app/src/core/action-identity.js +44 -0
  42. package/runtime/app/src/core/audit.js +295 -0
  43. package/runtime/app/src/core/capability-policy.js +58 -0
  44. package/runtime/app/src/core/child-environment.js +29 -0
  45. package/runtime/app/src/core/device-enrollment.js +417 -0
  46. package/runtime/app/src/core/device-identity.js +436 -0
  47. package/runtime/app/src/core/device-registry.js +477 -0
  48. package/runtime/app/src/core/device-reset.js +178 -0
  49. package/runtime/app/src/core/device-routing.js +305 -0
  50. package/runtime/app/src/core/durable-state.js +306 -0
  51. package/runtime/app/src/core/errors.js +20 -0
  52. package/runtime/app/src/core/evidence.js +9 -0
  53. package/runtime/app/src/core/index.js +17 -0
  54. package/runtime/app/src/core/machine-state.js +160 -0
  55. package/runtime/app/src/core/network-authority.js +20 -0
  56. package/runtime/app/src/core/policy.js +67 -0
  57. package/runtime/app/src/core/product-identity.js +7 -0
  58. package/runtime/app/src/core/provenance.js +18 -0
  59. package/runtime/app/src/core/provider-learning.js +274 -0
  60. package/runtime/app/src/core/public-plugin-surface.js +25 -0
  61. package/runtime/app/src/core/public-restricted-data.js +105 -0
  62. package/runtime/app/src/core/rate-limit.js +118 -0
  63. package/runtime/app/src/core/relay-client.js +568 -0
  64. package/runtime/app/src/core/relay-delivery-store.js +512 -0
  65. package/runtime/app/src/core/relay-result-store.js +321 -0
  66. package/runtime/app/src/core/release-update.js +253 -0
  67. package/runtime/app/src/core/router.js +96 -0
  68. package/runtime/app/src/core/runtime.js +136 -0
  69. package/runtime/app/src/core/scoped-path-syntax.js +16 -0
  70. package/runtime/app/src/core/session-token.js +498 -0
  71. package/runtime/app/src/core/task-orchestrator.js +1148 -0
  72. package/runtime/app/src/core/task-store.js +517 -0
  73. package/runtime/app/src/core/task.js +142 -0
  74. package/runtime/app/src/core/trusted-executable.js +124 -0
  75. package/runtime/app/src/core/types.js +83 -0
  76. package/runtime/app/src/core/windows-path-lease.js +138 -0
  77. package/runtime/native/operator-windows-dpapi.exe +0 -0
  78. package/runtime/native/operator-windows-path-lease.exe +0 -0
  79. package/runtime/native/operator-windows-uia.exe +0 -0
  80. package/runtime/runtime-manifest.json +380 -0
  81. package/src/cli.mjs +345 -0
  82. package/src/publish-check.mjs +22 -0
@@ -0,0 +1,1148 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+
4
+
5
+
6
+ import { addTaskNode, createTask, finalizeTask, setNodeState } from './task.js';
7
+ import { TaskStore } from './task-store.js';
8
+ import { capabilityRiskRule } from './capability-policy.js';
9
+ import { evidence } from './evidence.js';
10
+ import { OperatorError } from './errors.js';
11
+ import { normalizeMachineObservation, observationDomain } from './machine-state.js';
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ /** Stable semantic observation boundary. A future visual provider can populate the
44
+ * same contract with channel="visual" without changing planner control flow. */
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+ export class TaskOrchestrator {
82
+ #runtime ;
83
+ #store ;
84
+ #permissions ;
85
+ #planners ;
86
+ #active = new Map ();
87
+ #controllers = new Map ();
88
+ #controlRequests = new Map ();
89
+ #stateWriteTails = new Map ();
90
+ #executeAction ;
91
+
92
+ constructor(options
93
+
94
+
95
+
96
+
97
+
98
+ ) {
99
+ this.#runtime = options.runtime;
100
+ this.#store = options.store;
101
+ const planners = options.planners ?? [new SemanticTaskPlanner(), new SemanticWorkflowPlanner()];
102
+ this.#planners = new Map(planners.map((planner) => [planner.id, planner]));
103
+ this.#permissions = structuredClone(options.permissions);
104
+ this.#executeAction = options.executeAction ?? ((action, permissions, context) => this.#runtime.execute(action, permissions, context));
105
+ }
106
+
107
+ async submit(input ) {
108
+ const goalKind = input.goal && typeof input.goal === 'object' ? String((input.goal ).kind ?? '') : '';
109
+ const goal = parseGoal(input.goal, goalKind);
110
+ const planner = [...this.#planners.values()].find((candidate) => candidate.supports(goal));
111
+ if (!planner) throw new OperatorError('TASK_PLANNER_UNAVAILABLE', `No task planner supports ${goal.kind}.`);
112
+
113
+ const normalized = {
114
+ userObjective: boundedText(input.objective, 16_384, 'objective'),
115
+ authorizedScope: boundedTextArray(input.authorizedScope, 1000, 4096, 'authorizedScope'),
116
+ prohibitedScope: boundedTextArray(input.prohibitedScope ?? [], 1000, 4096, 'prohibitedScope'),
117
+ successConditions: boundedTextArray(input.successConditions, 1000, 16_384, 'successConditions'),
118
+ maxSteps: boundedInteger(input.maxSteps, 1, 100, 20),
119
+ maxAttemptsPerStep: boundedInteger(input.maxAttemptsPerStep, 1, 5, 2),
120
+ timeoutMs: boundedInteger(input.timeoutMs, 100, 60 * 60_000, 10 * 60_000)
121
+ };
122
+ const requestId = input.requestId === undefined ? undefined : validTaskRequestId(input.requestId);
123
+ if (requestId) {
124
+ try {
125
+ const existing = await this.#store.get(requestId);
126
+ if (!sameTaskSubmission(existing, planner.id, goal, normalized)) {
127
+ throw new OperatorError('TASK_SUBMISSION_ID_CONFLICT', 'Task submission requestId is already bound to a different durable task request.');
128
+ }
129
+ return existing;
130
+ } catch (error) {
131
+ if (!(error instanceof OperatorError) || error.code !== 'TASK_NOT_FOUND') throw error;
132
+ }
133
+ }
134
+
135
+ const task = createTask({
136
+ userObjective: normalized.userObjective,
137
+ interpretedObjective: `${goal.kind}:${normalized.userObjective}`,
138
+ authorizedScope: normalized.authorizedScope,
139
+ prohibitedScope: normalized.prohibitedScope,
140
+ successConditions: normalized.successConditions
141
+ });
142
+ if (requestId) task.id = requestId;
143
+ task.execution = {
144
+ schemaVersion: 1,
145
+ plannerId: planner.id,
146
+ goalKind: goal.kind,
147
+ plannerState: { goal: structuredClone(goal), phase: 'start' },
148
+ maxSteps: normalized.maxSteps,
149
+ maxAttemptsPerStep: normalized.maxAttemptsPerStep,
150
+ timeoutMs: normalized.timeoutMs,
151
+ stepCount: 0,
152
+ records: []
153
+ };
154
+ if (!requestId) {
155
+ await this.#store.put(task);
156
+ return task;
157
+ }
158
+ try {
159
+ await this.#store.create(task);
160
+ return task;
161
+ } catch (error) {
162
+ if ((error ).code !== 'EEXIST') throw error;
163
+ const existing = await this.#store.get(requestId);
164
+ if (!sameTaskSubmission(existing, planner.id, goal, normalized)) {
165
+ throw new OperatorError('TASK_SUBMISSION_ID_CONFLICT', 'Task submission requestId is already bound to a different durable task request.');
166
+ }
167
+ return existing;
168
+ }
169
+ }
170
+
171
+ async run(taskId , approvedActionIds = []) {
172
+ const active = this.#active.get(taskId);
173
+ if (active) return await active;
174
+ const controller = new AbortController();
175
+ this.#controllers.set(taskId, controller);
176
+ const promise = this.#runWithLease(taskId, approvedActionIds, controller.signal).finally(() => {
177
+ this.#active.delete(taskId);
178
+ this.#controllers.delete(taskId);
179
+ this.#controlRequests.delete(taskId);
180
+ });
181
+ this.#active.set(taskId, promise);
182
+ return await promise;
183
+ }
184
+
185
+ async #runWithLease(taskId , approvedActionIds , signal ) {
186
+ const lease = await this.#store.acquireExecutionLease(taskId);
187
+ try { return await this.#run(taskId, approvedActionIds, lease.assertOwned, signal); }
188
+ finally { await lease.release(); }
189
+ }
190
+
191
+ async #run(taskId , approvedActionIds , assertLease , signal ) {
192
+ let task = await this.#store.get(taskId);
193
+ if (!task.execution) throw new OperatorError('TASK_EXECUTION_MISSING', 'Task has no execution metadata.');
194
+ if (['VERIFIED', 'CANCELLED', 'FAILED'].includes(task.state)) return task;
195
+ if (task.state === 'PAUSED') return task;
196
+ const execution = task.execution;
197
+ const planner = this.#planners.get(execution.plannerId);
198
+ if (!planner) throw new OperatorError('TASK_PLANNER_UNAVAILABLE', `Task planner ${execution.plannerId} is unavailable.`);
199
+ const goal = parseGoal(execution.plannerState.goal, execution.goalKind);
200
+ if (!execution.startedAt) {
201
+ execution.startedAt = new Date().toISOString();
202
+ execution.deadlineAt = new Date(Date.now() + execution.timeoutMs).toISOString();
203
+ }
204
+ task.state = 'RUNNING';
205
+ await this.#persistRunState(task, assertLease);
206
+
207
+ while (true) {
208
+ task = await this.#store.get(task.id);
209
+ await assertLease();
210
+ if (task.state === 'PAUSED' || task.state === 'CANCELLED') return task;
211
+ const current = task.execution ;
212
+ if (Date.now() >= Date.parse(current.deadlineAt )) return await this.#fail(task, 'TASK_TIMEOUT', 'Task execution exceeded its bounded deadline.', assertLease);
213
+ this.#markInterrupted(current);
214
+ const context = { task, goal };
215
+ let decision ;
216
+ try { decision = planner.next(context); }
217
+ catch (error) { return await this.#fail(task, 'TASK_PLANNER_FAILED', error instanceof Error ? error.message : String(error), assertLease); }
218
+ if (decision.type === 'complete') {
219
+ task.evidence.push(evidence('task_completion', 'pass', decision.message));
220
+ finalizeTask(task);
221
+ await this.#persistRunState(task, assertLease);
222
+ return task;
223
+ }
224
+ if (current.stepCount >= current.maxSteps) return await this.#fail(task, 'TASK_STEP_BUDGET_EXHAUSTED', 'Task execution exhausted its bounded step budget.', assertLease);
225
+
226
+ const inputHash = sha256(canonicalJson(decision.input));
227
+ if (detectPlannerLoop(current.records, decision.key, inputHash)) {
228
+ return await this.#fail(task, 'TASK_LOOP_DETECTED', `Planner repeated the ${decision.key} cycle without progress.`, assertLease);
229
+ }
230
+ const previous = [...current.records].reverse().find((record) => record.stepKey === decision.key && record.inputHash === inputHash);
231
+ const priorAttempts = current.records.filter((record) => record.stepKey === decision.key && record.inputHash === inputHash && record.state !== 'BLOCKED').length;
232
+ if (priorAttempts >= current.maxAttemptsPerStep) return await this.#fail(task, 'TASK_RETRY_BUDGET_EXHAUSTED', `Step ${decision.key} exhausted its retry budget.`, assertLease);
233
+ let risk ;
234
+ try { risk = await this.#canonicalRisk(decision.capability, decision.input); }
235
+ catch (error) { return await this.#fail(task, 'TASK_RISK_RESOLUTION_FAILED', error instanceof Error ? error.message : String(error), assertLease); }
236
+ const blockedReplay = previous?.state === 'BLOCKED' ? previous : undefined;
237
+ const attempt = blockedReplay?.attempt ?? priorAttempts + 1;
238
+ const actionId = blockedReplay?.actionId ?? deterministicActionId(task.id, decision.key, attempt, inputHash);
239
+ const node = task.nodes.find((candidate) => candidate.title === decision.title) ?? addTaskNode(task, decision.title);
240
+ setNodeState(task, node.id, 'RUNNING');
241
+ const record = blockedReplay ?? {
242
+ stepKey: decision.key, actionId, capability: decision.capability, risk, inputHash, attempt,
243
+ state: 'STARTED', startedAt: new Date().toISOString(), evidence: []
244
+ };
245
+ if (!blockedReplay) current.records.push(record);
246
+ else { record.state = 'STARTED'; record.startedAt = new Date().toISOString(); delete record.finishedAt; delete record.errorCode; record.evidence = []; }
247
+ current.stepCount += 1;
248
+ const preDispatchControl = await this.#persistRunState(task, assertLease);
249
+ if (preDispatchControl === 'PAUSED') {
250
+ current.records = current.records.filter((candidate) => candidate !== record);
251
+ current.stepCount = Math.max(0, current.stepCount - 1);
252
+ setNodeState(task, node.id, 'PENDING');
253
+ await this.#persistRunState(task, assertLease);
254
+ return task;
255
+ }
256
+ if (preDispatchControl === 'CANCELLED') {
257
+ record.state = 'INTERRUPTED';
258
+ record.finishedAt = new Date().toISOString();
259
+ record.errorCode = 'EXECUTION_ABORTED';
260
+ setNodeState(task, node.id, 'SKIPPED');
261
+ task.evidence.push(evidence('task_cancel', 'info', 'Task was cancelled before provider dispatch.'));
262
+ await this.#persistRunState(task, assertLease);
263
+ return task;
264
+ }
265
+
266
+ const action = {
267
+ id: actionId, taskId: task.id, capability: decision.capability, risk,
268
+ input: structuredClone(decision.input), provenance: { kind: 'trusted_policy', source: `task-planner:${planner.id}` },
269
+ ...(decision.target ? { target: decision.target } : {})
270
+ };
271
+ const permissions = approvedActionIds.length === 0 ? this.#permissions : {
272
+ ...this.#permissions,
273
+ approvedActionIds: [...new Set([...(this.#permissions.approvedActionIds ?? []), ...approvedActionIds])]
274
+ };
275
+ const learningContext = semanticLearningContext(goal, task);
276
+ let result ;
277
+ try { result = await this.#executeAction(action, permissions, { signal, learningContext }); }
278
+ catch (error) {
279
+ result = {
280
+ ok: false, capability: action.capability, provider: 'task-executor', evidence: [], durationMs: 0,
281
+ error: { code: 'TASK_EXECUTOR_EXCEPTION', message: error instanceof Error ? error.message : String(error), retryable: false }
282
+ };
283
+ }
284
+ const latest = await this.#store.get(task.id);
285
+ await assertLease();
286
+ const controlState = latest.state === 'PAUSED' || latest.state === 'CANCELLED' ? latest.state : undefined;
287
+ task = latest;
288
+ const latestExecution = task.execution ;
289
+ const latestRecord = latestExecution.records.find((candidate) => candidate.actionId === actionId);
290
+ if (!latestRecord) return await this.#fail(task, 'TASK_STATE_CONFLICT', 'Persisted action record disappeared during execution.', assertLease);
291
+ const latestNode = task.nodes.find((candidate) => candidate.title === decision.title);
292
+ if (!latestNode) return await this.#fail(task, 'TASK_STATE_CONFLICT', 'Persisted task node disappeared during execution.', assertLease);
293
+ const observation = observe(result);
294
+ const normalizedObservation = normalizeMachineObservation(action, result, observation.channel);
295
+ latestRecord.finishedAt = new Date().toISOString();
296
+ latestRecord.evidence = result.evidence;
297
+ latestRecord.observation = normalizedObservation;
298
+ latestNode.evidence.push(...result.evidence);
299
+ task.evidence.push(...result.evidence);
300
+ if (result.ok) {
301
+ try {
302
+ planner.accept({ task, goal }, decision, observation);
303
+ } catch (error) {
304
+ await this.#recordLearning(task, result, 'failed', learningContext);
305
+ latestRecord.state = 'FAILED';
306
+ latestRecord.errorCode = 'TASK_POSTCONDITION_FAILED';
307
+ setNodeState(task, latestNode.id, 'FAILED');
308
+ return await this.#fail(task, 'TASK_POSTCONDITION_FAILED', error instanceof Error ? error.message : String(error), assertLease);
309
+ }
310
+ await this.#recordLearning(task, result, 'verified', learningContext);
311
+ latestRecord.state = 'SUCCEEDED';
312
+ setNodeState(task, latestNode.id, 'VERIFIED');
313
+ if (controlState) task.state = controlState;
314
+ await this.#persistRunState(task, assertLease);
315
+ if (controlState) return task;
316
+ continue;
317
+ }
318
+
319
+ latestRecord.errorCode = result.error?.code ?? 'EXECUTION_FAILED';
320
+ if (controlState === 'CANCELLED' && latestRecord.errorCode === 'EXECUTION_ABORTED') {
321
+ latestRecord.state = 'INTERRUPTED';
322
+ setNodeState(task, latestNode.id, 'SKIPPED');
323
+ task.evidence.push(evidence('task_cancel', 'info', 'In-flight task execution was aborted after cancellation.'));
324
+ task.state = 'CANCELLED';
325
+ await this.#persistRunState(task, assertLease);
326
+ return task;
327
+ }
328
+ await this.#recordLearning(task, result, 'failed', learningContext);
329
+ if (latestRecord.errorCode === 'APPROVAL_REQUIRED') {
330
+ latestRecord.state = 'BLOCKED';
331
+ task.state = 'BLOCKED';
332
+ setNodeState(task, latestNode.id, 'BLOCKED');
333
+ await this.#persistRunState(task, assertLease);
334
+ return task;
335
+ }
336
+ if (planner.fallback?.({ task, goal }, decision, observation)) {
337
+ latestRecord.state = 'FAILED';
338
+ setNodeState(task, latestNode.id, 'SKIPPED');
339
+ if (controlState) task.state = controlState;
340
+ await this.#persistRunState(task, assertLease);
341
+ if (controlState) return task;
342
+ continue;
343
+ }
344
+ latestRecord.state = 'FAILED';
345
+ setNodeState(task, latestNode.id, 'FAILED');
346
+ if (controlState) {
347
+ task.state = controlState;
348
+ await this.#persistRunState(task, assertLease);
349
+ return task;
350
+ }
351
+ if (result.error?.retryable === true && risk === 'read') { await this.#persistRunState(task, assertLease); continue; }
352
+ return await this.#fail(task, latestRecord.errorCode, result.error?.message ?? 'Task action failed.', assertLease);
353
+ }
354
+ }
355
+
356
+ async pause(taskId ) { return await this.#setControlState(taskId, 'PAUSED'); }
357
+ async cancel(taskId ) {
358
+ const task = await this.#setControlState(taskId, 'CANCELLED');
359
+ this.#controllers.get(taskId)?.abort();
360
+ return task;
361
+ }
362
+ async resume(taskId , approvedActionIds = []) {
363
+ const active = this.#active.get(taskId);
364
+ if (active) await active;
365
+ await this.#withStateWriteLock(taskId, async () => {
366
+ const task = await this.#store.get(taskId);
367
+ if (this.#controlRequests.get(taskId) === 'CANCELLED') {
368
+ throw new OperatorError('TASK_TERMINAL', 'Task cancellation is already in progress.');
369
+ }
370
+ if (!['PAUSED', 'BLOCKED'].includes(task.state)) throw new OperatorError('TASK_NOT_RESUMABLE', 'Only paused or blocked tasks can resume.');
371
+ if (this.#controlRequests.get(taskId) === 'PAUSED') this.#controlRequests.delete(taskId);
372
+ task.state = 'PENDING';
373
+ for (const node of task.nodes) if (node.state === 'BLOCKED') node.state = 'PENDING';
374
+ await this.#store.put(task);
375
+ });
376
+ return await this.run(taskId, approvedActionIds);
377
+ }
378
+
379
+ async #setControlState(taskId , state ) {
380
+ const previous = this.#controlRequests.get(taskId);
381
+ this.#controlRequests.set(taskId, state);
382
+ try {
383
+ return await this.#withStateWriteLock(taskId, async () => {
384
+ const task = await this.#store.get(taskId);
385
+ if (task.state === state) {
386
+ if (!this.#active.has(taskId)) this.#controlRequests.delete(taskId);
387
+ return task;
388
+ }
389
+ if (['VERIFIED', 'FAILED', 'CANCELLED'].includes(task.state)) throw new OperatorError('TASK_TERMINAL', 'Terminal task state cannot change.');
390
+ task.state = state;
391
+ task.updatedAt = new Date().toISOString();
392
+ await this.#store.put(task);
393
+ if (!this.#active.has(taskId)) this.#controlRequests.delete(taskId);
394
+ return task;
395
+ });
396
+ } catch (error) {
397
+ if (previous) this.#controlRequests.set(taskId, previous);
398
+ else this.#controlRequests.delete(taskId);
399
+ throw error;
400
+ }
401
+ }
402
+
403
+ #applyControlRequest(task ) {
404
+ const requested = this.#controlRequests.get(task.id);
405
+ if (requested === 'CANCELLED') {
406
+ task.state = 'CANCELLED';
407
+ return 'CANCELLED';
408
+ }
409
+ if (requested === 'PAUSED' && !['VERIFIED', 'FAILED', 'CANCELLED'].includes(task.state)) {
410
+ task.state = 'PAUSED';
411
+ return 'PAUSED';
412
+ }
413
+ return undefined;
414
+ }
415
+
416
+ async #persistRunState(task , assertLease ) {
417
+ await this.#withStateWriteLock(task.id, async () => {
418
+ this.#applyControlRequest(task);
419
+ await assertLease();
420
+ await this.#store.put(task);
421
+ });
422
+ // Control intent is published before its serialized durable write. Re-apply
423
+ // synchronously so the runner cannot dispatch work while that write waits.
424
+ return this.#applyControlRequest(task);
425
+ }
426
+
427
+ async #withStateWriteLock (taskId , work ) {
428
+ const previous = this.#stateWriteTails.get(taskId) ?? Promise.resolve();
429
+ let release ;
430
+ const gate = new Promise ((resolve) => { release = resolve; });
431
+ const tail = previous.catch(() => undefined).then(() => gate);
432
+ this.#stateWriteTails.set(taskId, tail);
433
+ await previous.catch(() => undefined);
434
+ try {
435
+ return await work();
436
+ } finally {
437
+ release();
438
+ if (this.#stateWriteTails.get(taskId) === tail) this.#stateWriteTails.delete(taskId);
439
+ }
440
+ }
441
+
442
+ async #canonicalRisk(capability , input ) {
443
+ const rule = capabilityRiskRule(capability);
444
+ if (rule !== 'dynamic') return rule;
445
+ return await this.#runtime.router.resolveRisk({ id: 'task-risk-probe', capability, risk: 'read', input, provenance: { kind: 'trusted_policy' } });
446
+ }
447
+
448
+ async #recordLearning(task , result , outcome , learningContext ) {
449
+ try {
450
+ const recorded = await this.#runtime.router.recordOutcome(result.capability, result.provider, outcome, { context: learningContext, durationMs: result.durationMs });
451
+ if (recorded) {
452
+ task.evidence.push(evidence('adaptive_learning', 'info', 'Recorded a bounded provider outcome for future routing.', {
453
+ capability: result.capability,
454
+ provider: result.provider,
455
+ outcome
456
+ }));
457
+ }
458
+ } catch (error) {
459
+ const code = error instanceof OperatorError ? error.code : 'PROVIDER_LEARNING_UPDATE_FAILED';
460
+ task.evidence.push(evidence('adaptive_learning', 'fail', 'Provider learning update could not be persisted; the current action result remains authoritative.', { code }));
461
+ }
462
+ }
463
+
464
+ #markInterrupted(execution ) {
465
+ for (const record of execution.records) if (record.state === 'STARTED') {
466
+ record.state = 'INTERRUPTED';
467
+ record.finishedAt = new Date().toISOString();
468
+ record.errorCode = 'TASK_ACTION_INTERRUPTED';
469
+ }
470
+ }
471
+
472
+ async #fail(task , code , message , assertLease ) {
473
+ task.state = 'FAILED';
474
+ task.failures.push({ at: new Date().toISOString(), code, message });
475
+ task.evidence.push(evidence('task_failure', 'fail', message, { code }));
476
+ task.updatedAt = new Date().toISOString();
477
+ if (assertLease) await this.#persistRunState(task, assertLease);
478
+ else await this.#store.put(task);
479
+ return task;
480
+ }
481
+ }
482
+
483
+ export class SemanticTaskPlanner {
484
+ id = 'operator.semantic.v1';
485
+ supports(goal ) { return ['controlled-file-change', 'trusted-project-command', 'browser-navigation', 'docker-lifecycle', 'postgres-select', 'app-operation'].includes(goal.kind); }
486
+
487
+ next({ task, goal } ) {
488
+ if (goal.kind === 'semantic-workflow') throw new OperatorError('TASK_GOAL_INVALID', 'Atomic semantic planner cannot execute a workflow envelope.');
489
+ const state = task.execution .plannerState;
490
+ const phase = String(state.phase ?? 'start');
491
+ if (goal.kind === 'controlled-file-change') {
492
+ if (phase === 'start') return { type: 'step', key: 'list-parent', title: 'Observe target directory', capability: 'file.list', input: { path: path.dirname(goal.path) || '.' } };
493
+ if (phase === 'create') return { type: 'step', key: 'create-file', title: 'Create requested file', capability: 'file.create', input: { path: goal.path, content: goal.content } };
494
+ if (phase === 'read') return { type: 'step', key: 'verify-file', title: 'Verify exact file content', capability: 'file.read', input: { path: goal.path, encoding: 'utf8' } };
495
+ if (phase === 'git') return { type: 'step', key: 'inspect-git', title: 'Verify Git observes the file', capability: 'git.status', input: { cwd: goal.root } };
496
+ return { type: 'complete', message: 'Controlled file task satisfied exact-content and Git-state postconditions.' };
497
+ }
498
+ if (goal.kind === 'trusted-project-command') {
499
+ if (phase === 'start') return { type: 'step', key: 'inspect-project', title: 'Inspect project semantics', capability: 'project.inspect', input: { path: goal.root } };
500
+ if (phase === 'commands') return { type: 'step', key: 'inspect-commands', title: 'Discover trusted project commands', capability: 'project.command.inspect', input: { path: goal.root } };
501
+ if (phase === 'run') return { type: 'step', key: 'run-command', title: 'Execute trusted project command', capability: 'project.command.run', input: { path: goal.root, commandId: state.commandId, expectedRisk: state.commandRisk } };
502
+ return { type: 'complete', message: 'Trusted project command completed with its registered postcondition validation.' };
503
+ }
504
+ if (goal.kind === 'docker-lifecycle') {
505
+ if (phase === 'start') return { type: 'step', key: 'inspect-docker-project', title: 'Inspect Docker project state', capability: 'docker.inspect', input: { path: goal.root }, target: goal.root };
506
+ if (phase === 'manage') return {
507
+ type: 'step', key: 'manage-docker-services', title: 'Manage verified Docker services', capability: 'docker.manage',
508
+ input: {
509
+ path: goal.root, operation: goal.operation, services: goal.services,
510
+ expectedCurrentFingerprint: state.dockerFingerprint, timeoutMs: goal.timeoutMs ?? 60_000
511
+ },
512
+ target: goal.root
513
+ };
514
+ if (phase === 'verify') return { type: 'step', key: 'verify-docker-project', title: 'Re-inspect Docker lifecycle postcondition', capability: 'docker.inspect', input: { path: goal.root }, target: goal.root };
515
+ return { type: 'complete', message: 'Docker lifecycle operation satisfied fresh-state, approval, and semantic service-state postconditions.' };
516
+ }
517
+ if (goal.kind === 'postgres-select') {
518
+ if (phase === 'start') return {
519
+ type: 'step', key: 'inspect-postgres-profiles', title: 'Inspect trusted PostgreSQL profiles',
520
+ capability: 'postgres.inspect', input: { path: goal.root, operation: 'profiles' }, target: goal.root
521
+ };
522
+ if (phase === 'columns') return {
523
+ type: 'step', key: 'inspect-postgres-columns', title: 'Inspect PostgreSQL table columns',
524
+ capability: 'postgres.inspect', input: {
525
+ path: goal.root, operation: 'columns', profileId: goal.profileId,
526
+ schema: goal.schema ?? 'public', table: goal.table, timeoutMs: goal.timeoutMs ?? 5_000
527
+ },
528
+ target: goal.root
529
+ };
530
+ if (phase === 'select') return {
531
+ type: 'step', key: 'select-postgres-rows', title: 'Read bounded PostgreSQL rows',
532
+ capability: 'postgres.select', input: {
533
+ path: goal.root, profileId: goal.profileId, schema: goal.schema ?? 'public', table: goal.table,
534
+ columns: goal.columns ?? [], filters: goal.filters ?? [], orderBy: goal.orderBy ?? [],
535
+ limit: goal.limit ?? 100, offset: goal.offset ?? 0, timeoutMs: goal.timeoutMs ?? 5_000
536
+ },
537
+ target: goal.root
538
+ };
539
+ return { type: 'complete', message: 'PostgreSQL task satisfied trusted-profile, current-column, and bounded read-only SELECT postconditions.' };
540
+ }
541
+ if (goal.kind === 'app-operation') {
542
+ const selector = phase === 'verify' ? (goal.verifySelector ?? goal.selector) : goal.selector;
543
+ const inspectInput = { selector, maxNodes: 1, maxDepth: 0, waitMs: goal.waitMs ?? 0 };
544
+ if (phase === 'start') return { type: 'step', key: 'inspect-app-target', title: 'Inspect unique semantic app target', capability: 'app.inspect', input: inspectInput };
545
+ if (phase === 'operate') return {
546
+ type: 'step', key: 'operate-app-target', title: 'Operate verified semantic app target', capability: 'app.operate',
547
+ input: {
548
+ operation: goal.operation, selector: goal.selector, waitMs: goal.waitMs ?? 0,
549
+ ...(goal.value !== undefined ? { value: goal.value } : {}),
550
+ ...(goal.horizontalAmount !== undefined ? { horizontalAmount: goal.horizontalAmount } : {}),
551
+ ...(goal.verticalAmount !== undefined ? { verticalAmount: goal.verticalAmount } : {})
552
+ }
553
+ };
554
+ if (phase === 'verify') return { type: 'step', key: 'verify-app-target', title: 'Re-inspect app postcondition', capability: 'app.inspect', input: inspectInput };
555
+ return { type: 'complete', message: 'Application operation satisfied semantic targeting and deterministic postcondition verification.' };
556
+ }
557
+ if (phase === 'start') return { type: 'step', key: 'inspect-browser', title: 'Inspect semantic browser state', capability: 'browser.inspect', input: {} };
558
+ if (phase === 'navigate') return { type: 'step', key: 'navigate-browser', title: 'Navigate the selected browser target', capability: 'browser.navigate', input: { targetId: state.targetId, url: goal.url }, target: goal.url };
559
+ if (phase === 'verify') return { type: 'step', key: 'verify-browser', title: 'Verify semantic browser destination', capability: 'browser.inspect', input: { targetId: state.targetId }, target: goal.url };
560
+ return { type: 'complete', message: 'Browser navigation satisfied semantic destination postconditions.' };
561
+ }
562
+
563
+ accept({ task, goal } , step , result ) {
564
+ if (goal.kind === 'semantic-workflow') throw new OperatorError('TASK_GOAL_INVALID', 'Atomic semantic planner cannot accept a workflow envelope.');
565
+ const state = task.execution .plannerState;
566
+ if (goal.kind === 'controlled-file-change') {
567
+ if (step.key === 'list-parent') state.phase = 'create';
568
+ else if (step.key === 'create-file') state.phase = 'read';
569
+ else if (step.key === 'verify-file') {
570
+ const output = asRecord(result.output);
571
+ if (output.content !== goal.content) throw new OperatorError('TASK_CONTENT_MISMATCH', 'File content did not match the requested exact content.');
572
+ state.phase = 'git';
573
+ } else if (step.key === 'inspect-git') {
574
+ const entries = Array.isArray(asRecord(result.output).entries) ? asRecord(result.output).entries : [];
575
+ const wanted = path.relative(goal.root, goal.path).replace(/\\/g, '/').replace(/^\.\//, '');
576
+ if (!entries.some((entry) => String(entry.path ?? '').replace(/\\/g, '/') === wanted)) throw new OperatorError('TASK_GIT_POSTCONDITION_MISSING', 'Git status did not report the controlled file.');
577
+ state.phase = 'complete';
578
+ }
579
+ return;
580
+ }
581
+ if (goal.kind === 'trusted-project-command') {
582
+ if (step.key === 'inspect-project') state.phase = 'commands';
583
+ else if (step.key === 'inspect-commands') {
584
+ const commands = Array.isArray(asRecord(result.output).commands) ? asRecord(result.output).commands : [];
585
+ const selected = commands.find((command) => command.kind === goal.commandKind);
586
+ if (!selected || typeof selected.id !== 'string' || typeof selected.risk !== 'string') throw new OperatorError('TASK_TRUSTED_COMMAND_NOT_FOUND', `No trusted ${goal.commandKind} command is configured.`);
587
+ state.commandId = selected.id;
588
+ state.commandRisk = selected.risk;
589
+ state.phase = 'run';
590
+ } else if (step.key === 'run-command') state.phase = 'complete';
591
+ return;
592
+ }
593
+ if (goal.kind === 'docker-lifecycle') {
594
+ if (step.key === 'inspect-docker-project') {
595
+ const output = asRecord(result.output);
596
+ if (output.scope !== 'project') throw new OperatorError('TASK_DOCKER_PROJECT_REQUIRED', 'Docker lifecycle tasks require project-scoped inspection.');
597
+ const fingerprint = String(output.fingerprint ?? '');
598
+ if (!/^[0-9a-f]{64}$/i.test(fingerprint)) throw new OperatorError('TASK_DOCKER_FINGERPRINT_MISSING', 'Docker inspection did not return a valid project fingerprint.');
599
+ verifyDockerServicesPresent(goal.services, output.services);
600
+ state.dockerFingerprint = fingerprint.toLowerCase();
601
+ state.phase = 'manage';
602
+ } else if (step.key === 'manage-docker-services') {
603
+ const output = asRecord(result.output);
604
+ if (output.operation !== goal.operation) throw new OperatorError('TASK_DOCKER_POSTCONDITION_FAILED', 'Docker manage result did not match the requested operation.');
605
+ verifyDockerServicesState(goal.operation, goal.services, output.states);
606
+ state.phase = 'verify';
607
+ } else if (step.key === 'verify-docker-project') {
608
+ const output = asRecord(result.output);
609
+ if (output.scope !== 'project') throw new OperatorError('TASK_DOCKER_PROJECT_REQUIRED', 'Docker verification did not remain project-scoped.');
610
+ verifyDockerServicesState(goal.operation, goal.services, output.services);
611
+ state.phase = 'complete';
612
+ }
613
+ return;
614
+ }
615
+ if (goal.kind === 'postgres-select') {
616
+ if (step.key === 'inspect-postgres-profiles') {
617
+ const profiles = Array.isArray(asRecord(result.output).profiles) ? asRecord(result.output).profiles : [];
618
+ const profile = profiles.find((item) => item.id === goal.profileId);
619
+ if (!profile) throw new OperatorError('TASK_POSTGRES_PROFILE_NOT_FOUND', 'Requested PostgreSQL profile is not registered for the authorized root.');
620
+ if (profile.endpoint !== 'loopback' && profile.endpoint !== 'unix_socket') {
621
+ throw new OperatorError('TASK_POSTGRES_PROFILE_NOT_LOCAL', 'PostgreSQL task requires a local trusted profile.');
622
+ }
623
+ state.phase = 'columns';
624
+ } else if (step.key === 'inspect-postgres-columns') {
625
+ const output = asRecord(result.output);
626
+ if (output.profileId !== goal.profileId || output.schema !== (goal.schema ?? 'public') || output.table !== goal.table) {
627
+ throw new OperatorError('TASK_POSTGRES_METADATA_MISMATCH', 'PostgreSQL metadata inspection did not match the requested profile and table.');
628
+ }
629
+ verifyPostgresRequestedColumns(goal, output.rows);
630
+ state.phase = 'select';
631
+ } else if (step.key === 'select-postgres-rows') {
632
+ verifyPostgresSelectResult(goal, result.output);
633
+ state.phase = 'complete';
634
+ }
635
+ return;
636
+ }
637
+ if (goal.kind === 'app-operation') {
638
+ if (step.key === 'inspect-app-target') {
639
+ const element = uniqueInspectedUiaElement(result.output);
640
+ requireUiaOperationSupport(goal.operation, element);
641
+ state.targetIdentity = uiaElementIdentity(element);
642
+ state.phase = 'operate';
643
+ } else if (step.key === 'operate-app-target') {
644
+ verifyUiaOperationResult(goal, result.output);
645
+ state.phase = 'verify';
646
+ } else if (step.key === 'verify-app-target') {
647
+ const element = uniqueInspectedUiaElement(result.output);
648
+ verifyUiaReinspection(goal, element, state.targetIdentity);
649
+ state.phase = 'complete';
650
+ }
651
+ return;
652
+ }
653
+ if (step.key === 'inspect-browser') {
654
+ const tabs = Array.isArray(asRecord(result.output).tabs) ? asRecord(result.output).tabs : [];
655
+ const selected = goal.targetId
656
+ ? tabs.find((tab) => tab.id === goal.targetId)
657
+ : tabs.find((tab) => tab.type === 'page');
658
+ if (!selected || typeof selected.id !== 'string') throw new OperatorError('TASK_BROWSER_TARGET_NOT_FOUND', 'No matching semantic browser page target is available.');
659
+ state.targetId = selected.id;
660
+ state.phase = 'navigate';
661
+ } else if (step.key === 'navigate-browser') {
662
+ const output = asRecord(result.output);
663
+ if (typeof output.targetId !== 'string' || output.targetId !== state.targetId || !sameBrowserDestination(String(output.url ?? ''), goal.url)) {
664
+ throw new OperatorError('TASK_BROWSER_POSTCONDITION_FAILED', 'Browser navigation result did not match the selected target and destination.');
665
+ }
666
+ state.phase = 'verify';
667
+ } else if (step.key === 'verify-browser') {
668
+ const target = asRecord(asRecord(result.output).target);
669
+ if (target.id !== state.targetId || !sameBrowserDestination(String(target.url ?? ''), goal.url)) {
670
+ throw new OperatorError('TASK_BROWSER_POSTCONDITION_FAILED', 'Semantic browser re-observation did not confirm the requested destination.');
671
+ }
672
+ state.phase = 'complete';
673
+ }
674
+ }
675
+
676
+ fallback({ task } , step , result ) {
677
+ if (step.key === 'create-file' && result.error?.code === 'TARGET_EXISTS') {
678
+ task.execution .plannerState.phase = 'read';
679
+ task.evidence.push(evidence('strategy_fallback', 'info', 'Target already existed; switched to exact-content verification.'));
680
+ return true;
681
+ }
682
+ if (step.key === 'navigate-browser' && result.error?.code === 'BROWSER_TARGET_NOT_FOUND') {
683
+ task.execution .plannerState.phase = 'start';
684
+ delete task.execution .plannerState.targetId;
685
+ task.evidence.push(evidence('strategy_fallback', 'info', 'Browser target disappeared; switched to semantic target re-discovery.'));
686
+ return true;
687
+ }
688
+ if (step.key === 'manage-docker-services' && result.error?.code === 'DOCKER_STATE_CHANGED') {
689
+ task.execution .plannerState.phase = 'start';
690
+ delete task.execution .plannerState.dockerFingerprint;
691
+ task.evidence.push(evidence('strategy_fallback', 'info', 'Docker project state changed; switched to fresh inspection before requesting a new approval.'));
692
+ return true;
693
+ }
694
+ if (step.key === 'operate-app-target' && ['UIA_ELEMENT_NOT_FOUND', 'UIA_WAIT_TIMEOUT'].includes(result.error?.code ?? '')) {
695
+ task.execution .plannerState.phase = 'start';
696
+ delete task.execution .plannerState.targetIdentity;
697
+ task.evidence.push(evidence('strategy_fallback', 'info', 'UIA target disappeared or timed out; switched to bounded semantic target re-discovery.'));
698
+ return true;
699
+ }
700
+ return false;
701
+ }
702
+ }
703
+
704
+
705
+ export class SemanticWorkflowPlanner {
706
+ id = 'operator.semantic-workflow.v1';
707
+ #atomic = new SemanticTaskPlanner();
708
+
709
+ supports(goal ) { return goal.kind === 'semantic-workflow'; }
710
+
711
+ next({ task, goal } ) {
712
+ if (goal.kind !== 'semantic-workflow') throw new OperatorError('TASK_GOAL_INVALID', 'Workflow planner requires a semantic-workflow goal.');
713
+ const state = task.execution .plannerState;
714
+ let index = workflowIndex(state, goal.steps.length);
715
+ while (index < goal.steps.length) {
716
+ const child = goal.steps[index] ;
717
+ const childState = workflowChildState(state);
718
+ const proxy = taskWithPlannerState(task, childState);
719
+ const decision = this.#atomic.next({ task: proxy, goal: child });
720
+ state.workflowChildState = proxy.execution .plannerState;
721
+ if (decision.type === 'complete') {
722
+ task.evidence.push(evidence('workflow_step', 'pass', decision.message, { index, kind: child.kind }));
723
+ index += 1;
724
+ state.workflowIndex = index;
725
+ state.workflowChildState = { phase: 'start' };
726
+ continue;
727
+ }
728
+ return {
729
+ ...decision,
730
+ key: `workflow:${index}:${decision.key}`,
731
+ title: `[${index + 1}/${goal.steps.length}] ${decision.title}`
732
+ };
733
+ }
734
+ return { type: 'complete', message: `Semantic workflow completed ${goal.steps.length} verified goal(s).` };
735
+ }
736
+
737
+ accept({ task, goal } , step , observation ) {
738
+ const current = this.#current(task, goal, step);
739
+ this.#atomic.accept({ task: current.proxy, goal: current.child }, current.atomicStep, observation);
740
+ task.execution .plannerState.workflowChildState = current.proxy.execution .plannerState;
741
+ }
742
+
743
+ fallback({ task, goal } , step , observation ) {
744
+ const current = this.#current(task, goal, step);
745
+ const handled = this.#atomic.fallback?.({ task: current.proxy, goal: current.child }, current.atomicStep, observation) ?? false;
746
+ task.execution .plannerState.workflowChildState = current.proxy.execution .plannerState;
747
+ return handled;
748
+ }
749
+
750
+ #current(task , goal , step )
751
+
752
+
753
+
754
+ {
755
+ if (goal.kind !== 'semantic-workflow') throw new OperatorError('TASK_GOAL_INVALID', 'Workflow planner requires a semantic-workflow goal.');
756
+ const state = task.execution .plannerState;
757
+ const index = workflowIndex(state, goal.steps.length);
758
+ const child = goal.steps[index];
759
+ if (!child) throw new OperatorError('TASK_WORKFLOW_STATE_INVALID', 'Workflow action has no current semantic child goal.');
760
+ const proxy = taskWithPlannerState(task, workflowChildState(state));
761
+ const expected = this.#atomic.next({ task: proxy, goal: child });
762
+ if (expected.type !== 'step' || step.key !== `workflow:${index}:${expected.key}`) {
763
+ throw new OperatorError('TASK_WORKFLOW_STATE_INVALID', 'Workflow action does not match the current semantic child state.');
764
+ }
765
+ return { child, proxy, atomicStep: expected };
766
+ }
767
+ }
768
+
769
+ function semanticLearningContext(goal , task ) {
770
+ if (goal.kind !== 'semantic-workflow') return goal.kind;
771
+ const state = task.execution?.plannerState;
772
+ if (!state) return 'semantic-workflow';
773
+ const index = state.workflowIndex === undefined ? 0 : Number(state.workflowIndex);
774
+ if (!Number.isSafeInteger(index) || index < 0 || index >= goal.steps.length) return 'semantic-workflow';
775
+ return goal.steps[index]?.kind ?? 'semantic-workflow';
776
+ }
777
+
778
+ function workflowIndex(state , length ) {
779
+ const value = state.workflowIndex === undefined ? 0 : Number(state.workflowIndex);
780
+ if (!Number.isSafeInteger(value) || value < 0 || value > length) {
781
+ throw new OperatorError('TASK_WORKFLOW_STATE_INVALID', 'Workflow child index is invalid.');
782
+ }
783
+ return value;
784
+ }
785
+
786
+ function workflowChildState(state ) {
787
+ const value = state.workflowChildState;
788
+ if (value === undefined) return { phase: 'start' };
789
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
790
+ throw new OperatorError('TASK_WORKFLOW_STATE_INVALID', 'Workflow child planner state is invalid.');
791
+ }
792
+ return structuredClone(value );
793
+ }
794
+
795
+ function taskWithPlannerState(task , plannerState ) {
796
+ if (!task.execution) throw new OperatorError('TASK_EXECUTION_MISSING', 'Task has no execution metadata.');
797
+ return {
798
+ ...task,
799
+ execution: { ...task.execution, plannerState },
800
+ evidence: task.evidence,
801
+ nodes: task.nodes,
802
+ failures: task.failures
803
+ };
804
+ }
805
+
806
+ function parseGoal(input , expectedKind ) {
807
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new OperatorError('TASK_GOAL_INVALID', 'Stored task goal is invalid.');
808
+ const goal = structuredClone(input) ;
809
+ if (goal.kind !== expectedKind) throw new OperatorError('TASK_GOAL_INVALID', 'Stored task goal kind does not match execution metadata.');
810
+ if (goal.kind === 'semantic-workflow') {
811
+ if (!Array.isArray(goal.steps) || goal.steps.length < 1 || goal.steps.length > 20) {
812
+ throw new OperatorError('TASK_GOAL_INVALID', 'Semantic workflow requires 1-20 typed child goals.');
813
+ }
814
+ goal.steps = goal.steps.map((step, index) => {
815
+ if (!step || typeof step !== 'object' || Array.isArray(step)) throw new OperatorError('TASK_GOAL_INVALID', `Workflow step ${index} is invalid.`);
816
+ const kind = String((step ).kind ?? '');
817
+ if (kind === 'semantic-workflow') throw new OperatorError('TASK_GOAL_INVALID', 'Nested semantic workflows are not permitted.');
818
+ return parseGoal(step, kind) ;
819
+ });
820
+ } else if (goal.kind === 'controlled-file-change') {
821
+ boundedText(goal.root, 4096, 'goal root'); boundedText(goal.path, 4096, 'goal path'); boundedText(goal.content, 2 * 1024 * 1024, 'goal content');
822
+ const root = path.resolve(goal.root);
823
+ const target = path.resolve(root, goal.path);
824
+ const relative = path.relative(root, target);
825
+ if (relative.startsWith('..') || path.isAbsolute(relative)) throw new OperatorError('TASK_GOAL_INVALID', 'Controlled file path must remain inside its goal root.');
826
+ goal.root = root;
827
+ goal.path = target;
828
+ } else if (goal.kind === 'trusted-project-command') {
829
+ boundedText(goal.root, 4096, 'goal root');
830
+ if (!['build', 'test', 'lint'].includes(goal.commandKind)) throw new OperatorError('TASK_GOAL_INVALID', 'Trusted command kind is invalid.');
831
+ goal.root = path.resolve(goal.root);
832
+ } else if (goal.kind === 'browser-navigation') {
833
+ const rawUrl = boundedText(goal.url, 16_384, 'goal URL');
834
+ let url ;
835
+ try { url = new URL(rawUrl); } catch { throw new OperatorError('TASK_GOAL_INVALID', 'Browser goal requires a valid absolute URL.'); }
836
+ if (!['http:', 'https:'].includes(url.protocol)) throw new OperatorError('TASK_GOAL_INVALID', 'Browser goal permits only HTTP(S) destinations.');
837
+ if (url.username || url.password) throw new OperatorError('TASK_GOAL_INVALID', 'Browser goal URL must not contain credentials.');
838
+ goal.url = url.toString();
839
+ if (goal.targetId !== undefined) goal.targetId = boundedText(goal.targetId, 256, 'browser targetId');
840
+ } else if (goal.kind === 'docker-lifecycle') {
841
+ goal.root = path.resolve(boundedText(goal.root, 4096, 'docker root'));
842
+ if (!['start', 'stop', 'restart'].includes(goal.operation)) throw new OperatorError('TASK_GOAL_INVALID', 'Docker lifecycle operation must be start, stop, or restart.');
843
+ if (!Array.isArray(goal.services) || goal.services.length < 1 || goal.services.length > 50) throw new OperatorError('TASK_GOAL_INVALID', 'Docker lifecycle requires 1-50 service names.');
844
+ goal.services = [...new Set(goal.services.map((service, index) => {
845
+ const value = boundedText(service, 128, `docker services[${index}]`);
846
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value)) throw new OperatorError('TASK_GOAL_INVALID', 'Docker service names must use bounded alphanumeric/._- characters.');
847
+ return value;
848
+ }))].sort();
849
+ goal.timeoutMs = boundedInteger(goal.timeoutMs, 1_000, 300_000, 60_000);
850
+ } else if (goal.kind === 'postgres-select') {
851
+ goal.root = path.resolve(boundedText(goal.root, 4096, 'postgres root'));
852
+ goal.profileId = boundedText(goal.profileId, 64, 'postgres profileId');
853
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(goal.profileId)) throw new OperatorError('TASK_GOAL_INVALID', 'PostgreSQL profileId is invalid.');
854
+ goal.schema = postgresIdentifier(goal.schema ?? 'public', 'postgres schema');
855
+ goal.table = postgresIdentifier(goal.table, 'postgres table');
856
+ goal.columns = normalizePostgresColumns(goal.columns ?? []);
857
+ goal.filters = normalizePostgresFilters(goal.filters ?? []);
858
+ goal.orderBy = normalizePostgresOrder(goal.orderBy ?? []);
859
+ goal.limit = boundedInteger(goal.limit, 1, 500, 100);
860
+ goal.offset = boundedInteger(goal.offset, 0, 10_000, 0);
861
+ goal.timeoutMs = boundedInteger(goal.timeoutMs, 100, 30_000, 5_000);
862
+ } else if (goal.kind === 'app-operation') {
863
+ goal.selector = normalizeUiaTaskSelector(goal.selector, 'app selector');
864
+ if (goal.verifySelector !== undefined) goal.verifySelector = normalizeUiaTaskSelector(goal.verifySelector, 'app verifySelector');
865
+ if (!UIA_TASK_OPERATIONS.includes(goal.operation)) throw new OperatorError('TASK_GOAL_INVALID', 'App operation is not in the closed semantic operation set.');
866
+ if (goal.operation === 'invoke' && goal.verifySelector === undefined) throw new OperatorError('TASK_GOAL_INVALID', 'Invoke tasks require a semantic verifySelector so their external effect can be re-observed.');
867
+ if (goal.operation === 'set_value') goal.value = boundedText(goal.value, 64 * 1024, 'app value');
868
+ else if (goal.value !== undefined) throw new OperatorError('TASK_GOAL_INVALID', `${goal.operation} does not accept a value.`);
869
+ if (goal.operation === 'scroll') {
870
+ if (goal.horizontalAmount === undefined && goal.verticalAmount === undefined) throw new OperatorError('TASK_GOAL_INVALID', 'Scroll requires a bounded horizontalAmount or verticalAmount.');
871
+ if (goal.horizontalAmount !== undefined) goal.horizontalAmount = validUiaScrollAmount(goal.horizontalAmount, 'horizontalAmount');
872
+ if (goal.verticalAmount !== undefined) goal.verticalAmount = validUiaScrollAmount(goal.verticalAmount, 'verticalAmount');
873
+ } else if (goal.horizontalAmount !== undefined || goal.verticalAmount !== undefined) {
874
+ throw new OperatorError('TASK_GOAL_INVALID', `${goal.operation} does not accept scroll amounts.`);
875
+ }
876
+ goal.waitMs = boundedInteger(goal.waitMs, 0, 10_000, 0);
877
+ } else throw new OperatorError('TASK_GOAL_INVALID', 'Task goal kind is unsupported.');
878
+ return goal;
879
+ }
880
+
881
+ function postgresIdentifier(input , label ) {
882
+ const value = boundedText(input, 63, label);
883
+ if (!/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(value)) throw new OperatorError('TASK_GOAL_INVALID', `${label} is not a valid bounded PostgreSQL identifier.`);
884
+ return value;
885
+ }
886
+
887
+ function normalizePostgresColumns(input ) {
888
+ if (!Array.isArray(input) || input.length > 50) throw new OperatorError('TASK_GOAL_INVALID', 'PostgreSQL columns must contain at most 50 identifiers.');
889
+ const columns = input.map((value, index) => postgresIdentifier(value, `postgres columns[${index}]`));
890
+ if (new Set(columns).size !== columns.length) throw new OperatorError('TASK_GOAL_INVALID', 'PostgreSQL columns must not contain duplicates.');
891
+ return columns;
892
+ }
893
+
894
+ function normalizePostgresFilters(input ) {
895
+ if (!Array.isArray(input) || input.length > 20) throw new OperatorError('TASK_GOAL_INVALID', 'PostgreSQL filters must contain at most 20 entries.');
896
+ const ops = new Set (['eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'like', 'ilike', 'is_null', 'not_null']);
897
+ return input.map((entry, index) => {
898
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new OperatorError('TASK_GOAL_INVALID', `postgres filters[${index}] must be an object.`);
899
+ const raw = entry ;
900
+ const column = postgresIdentifier(raw.column, `postgres filters[${index}].column`);
901
+ const op = String(raw.op ?? '') ;
902
+ if (!ops.has(op)) throw new OperatorError('TASK_GOAL_INVALID', `postgres filters[${index}].op is invalid.`);
903
+ if (op === 'is_null' || op === 'not_null') return { column, op };
904
+ const value = raw.value === undefined ? '' : raw.value;
905
+ if (typeof value !== 'string' || value.length > 100_000 || value.includes('\0')) throw new OperatorError('TASK_GOAL_INVALID', `postgres filters[${index}].value is invalid.`);
906
+ return { column, op, value };
907
+ });
908
+ }
909
+
910
+ function normalizePostgresOrder(input ) {
911
+ if (!Array.isArray(input) || input.length > 5) throw new OperatorError('TASK_GOAL_INVALID', 'PostgreSQL orderBy must contain at most 5 entries.');
912
+ return input.map((entry, index) => {
913
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new OperatorError('TASK_GOAL_INVALID', `postgres orderBy[${index}] must be an object.`);
914
+ const raw = entry ;
915
+ const column = postgresIdentifier(raw.column, `postgres orderBy[${index}].column`);
916
+ const direction = String(raw.direction ?? '');
917
+ if (direction !== 'asc' && direction !== 'desc') throw new OperatorError('TASK_GOAL_INVALID', `postgres orderBy[${index}].direction is invalid.`);
918
+ return { column, direction };
919
+ });
920
+ }
921
+
922
+ function verifyPostgresRequestedColumns(goal , input ) {
923
+ const rows = Array.isArray(input) ? input.map(asRecord) : [];
924
+ const available = new Set(rows.map((row) => String(row.column_name ?? '')).filter(Boolean));
925
+ const required = new Set([
926
+ ...(goal.columns ?? []),
927
+ ...(goal.filters ?? []).map((filter) => filter.column),
928
+ ...(goal.orderBy ?? []).map((order) => order.column)
929
+ ]);
930
+ for (const column of required) {
931
+ if (!available.has(column)) throw new OperatorError('TASK_POSTGRES_COLUMN_MISSING', `PostgreSQL column ${column} is not present in the current table metadata.`);
932
+ }
933
+ }
934
+
935
+ function verifyPostgresSelectResult(goal , input ) {
936
+ const output = asRecord(input);
937
+ if (output.profileId !== goal.profileId || output.schema !== (goal.schema ?? 'public') || output.table !== goal.table) {
938
+ throw new OperatorError('TASK_POSTGRES_SELECT_MISMATCH', 'PostgreSQL SELECT result did not match the requested profile and table.');
939
+ }
940
+ const rowCount = Number(output.rowCount);
941
+ const limit = goal.limit ?? 100;
942
+ if (!Number.isSafeInteger(rowCount) || rowCount < 0 || rowCount > limit) throw new OperatorError('TASK_POSTGRES_SELECT_MISMATCH', 'PostgreSQL SELECT returned an invalid row count.');
943
+ const rows = Array.isArray(output.rows) ? output.rows : [];
944
+ if (rows.length !== rowCount) throw new OperatorError('TASK_POSTGRES_SELECT_MISMATCH', 'PostgreSQL SELECT row count did not match returned rows.');
945
+ if (Number(output.limit) !== limit || Number(output.offset) !== (goal.offset ?? 0)) throw new OperatorError('TASK_POSTGRES_SELECT_MISMATCH', 'PostgreSQL SELECT pagination did not match the requested bounds.');
946
+ const expectedColumns = (goal.columns ?? []).length === 0 ? ['*'] : goal.columns ;
947
+ const actualColumns = Array.isArray(output.columns) ? output.columns.map(String) : [];
948
+ if (canonicalJson(actualColumns) !== canonicalJson(expectedColumns)) throw new OperatorError('TASK_POSTGRES_SELECT_MISMATCH', 'PostgreSQL SELECT columns did not match the requested projection.');
949
+ }
950
+
951
+ function verifyDockerServicesPresent(expectedServices , input ) {
952
+ const services = Array.isArray(input) ? input.map(asRecord) : [];
953
+ for (const service of expectedServices) {
954
+ const found = services.find((item) => item.service === service);
955
+ if (!found) throw new OperatorError('TASK_DOCKER_SERVICE_MISSING', `Docker inspection did not report service ${service}.`);
956
+ const containers = Number(found.containers ?? 0);
957
+ if (!Number.isSafeInteger(containers) || containers < 1) throw new OperatorError('TASK_DOCKER_SERVICE_MISSING', `Docker service ${service} has no created containers.`);
958
+ }
959
+ }
960
+
961
+ function verifyDockerServicesState(operation , expectedServices , input ) {
962
+ const services = Array.isArray(input) ? input.map(asRecord) : [];
963
+ for (const service of expectedServices) {
964
+ const found = services.find((item) => item.service === service);
965
+ if (!found) throw new OperatorError('TASK_DOCKER_POSTCONDITION_FAILED', `Docker service ${service} disappeared during verification.`);
966
+ const states = Array.isArray(found.states) ? found.states.map(String) : [];
967
+ if (states.length === 0) throw new OperatorError('TASK_DOCKER_POSTCONDITION_FAILED', `Docker service ${service} returned no container states.`);
968
+ const expectedState = operation === 'stop' ? 'exited' : 'running';
969
+ if (!states.every((state) => state === expectedState)) {
970
+ throw new OperatorError('TASK_DOCKER_POSTCONDITION_FAILED', `Docker service ${service} did not reach ${expectedState}.`);
971
+ }
972
+ }
973
+ }
974
+
975
+ const UIA_TASK_OPERATIONS = ['invoke', 'set_value', 'focus', 'select', 'expand', 'collapse', 'scroll', 'activate_window'];
976
+ const UIA_SCROLL_AMOUNTS = ['large_decrement', 'small_decrement', 'none', 'large_increment', 'small_increment'] ;
977
+
978
+ function normalizeUiaTaskSelector(input , label ) {
979
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new OperatorError('TASK_GOAL_INVALID', `${label} must be an object.`);
980
+ const raw = input ;
981
+ const allowed = new Set(['name', 'automationId', 'className', 'controlType', 'processId']);
982
+ if (Object.keys(raw).some((key) => !allowed.has(key))) throw new OperatorError('TASK_GOAL_INVALID', `${label} contains an unsupported selector field.`);
983
+ const selector = {};
984
+ for (const key of ['name', 'automationId', 'className', 'controlType'] ) {
985
+ if (raw[key] !== undefined) selector[key] = boundedText(raw[key], 512, `${label}.${key}`);
986
+ }
987
+ if (raw.processId !== undefined) {
988
+ const pid = Number(raw.processId);
989
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) throw new OperatorError('TASK_GOAL_INVALID', `${label}.processId is invalid.`);
990
+ selector.processId = pid;
991
+ }
992
+ if (Object.keys(selector).length === 0) throw new OperatorError('TASK_GOAL_INVALID', `${label} must identify a semantic UIA target.`);
993
+ return selector;
994
+ }
995
+
996
+ function validUiaScrollAmount(input , label ) {
997
+ if (typeof input !== 'string' || !(UIA_SCROLL_AMOUNTS ).includes(input)) {
998
+ throw new OperatorError('TASK_GOAL_INVALID', `${label} must be one of ${UIA_SCROLL_AMOUNTS.join(', ')}.`);
999
+ }
1000
+ return input;
1001
+ }
1002
+
1003
+ function uniqueInspectedUiaElement(output ) {
1004
+ const elements = Array.isArray(asRecord(output).elements) ? asRecord(output).elements : [];
1005
+ if (elements.length !== 1 || !elements[0] || typeof elements[0] !== 'object' || Array.isArray(elements[0])) {
1006
+ throw new OperatorError('TASK_UIA_TARGET_NOT_UNIQUE', 'UIA inspection did not return exactly one semantic target.');
1007
+ }
1008
+ return elements[0] ;
1009
+ }
1010
+
1011
+ function requireUiaOperationSupport(operation , element ) {
1012
+ const patterns = asRecord(element.patterns);
1013
+ const supported = operation === 'invoke' ? patterns.invoke === true || patterns.legacy_iaccessible === true
1014
+ : operation === 'set_value' ? patterns.value === true || patterns.legacy_iaccessible === true
1015
+ : operation === 'select' ? patterns.selection_item === true
1016
+ : operation === 'expand' || operation === 'collapse' ? patterns.expand_collapse === true
1017
+ : operation === 'scroll' ? patterns.scroll === true
1018
+ : true;
1019
+ if (!supported) throw new OperatorError('TASK_UIA_PATTERN_UNAVAILABLE', `Semantic target does not support ${operation}.`);
1020
+ }
1021
+
1022
+ function uiaElementIdentity(element ) {
1023
+ return {
1024
+ automationId: String(element.automation_id ?? ''), className: String(element.class_name ?? ''),
1025
+ controlType: String(element.control_type ?? ''), processId: Number(element.process_id ?? 0)
1026
+ };
1027
+ }
1028
+
1029
+ function verifyUiaOperationResult(goal , outputValue ) {
1030
+ const output = asRecord(outputValue);
1031
+ if (output.operation !== goal.operation) throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA operation result did not identify the requested operation.');
1032
+ const postcondition = asRecord(output.postcondition);
1033
+ if (goal.operation === 'invoke') {
1034
+ if (postcondition.element_reachable !== true) throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA invoke did not prove the semantic target was reached.');
1035
+ return;
1036
+ }
1037
+ if (postcondition.verified !== true) throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA operation did not return a verified semantic postcondition.');
1038
+ if (goal.operation === 'set_value' && postcondition.actual_value !== goal.value) {
1039
+ throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA set_value did not verify the requested exact value.');
1040
+ }
1041
+ }
1042
+
1043
+ function verifyUiaReinspection(
1044
+ goal ,
1045
+ element ,
1046
+ priorIdentity
1047
+ ) {
1048
+ if (goal.verifySelector === undefined) {
1049
+ const before = asRecord(priorIdentity);
1050
+ const after = uiaElementIdentity(element);
1051
+ for (const key of ['automationId', 'className', 'controlType', 'processId']) {
1052
+ if (before[key] !== undefined && before[key] !== '' && before[key] !== 0 && before[key] !== after[key]) {
1053
+ throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA re-inspection resolved to a different semantic target.');
1054
+ }
1055
+ }
1056
+ }
1057
+ if (goal.operation === 'set_value' && element.value !== goal.value) throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA re-inspection did not confirm the requested value.');
1058
+ if (goal.operation === 'select' && element.selected !== true) throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA re-inspection did not confirm selection.');
1059
+ if (goal.operation === 'expand' && element.expand_collapse_state !== 'Expanded') throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA re-inspection did not confirm expansion.');
1060
+ if (goal.operation === 'collapse' && element.expand_collapse_state !== 'Collapsed') throw new OperatorError('TASK_UIA_POSTCONDITION_FAILED', 'UIA re-inspection did not confirm collapse.');
1061
+ }
1062
+
1063
+ function validTaskRequestId(input ) {
1064
+ const value = String(input ?? '').toLowerCase();
1065
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)) {
1066
+ throw new OperatorError('TASK_SUBMISSION_ID_INVALID', 'Task submission requestId must be a UUID.');
1067
+ }
1068
+ return value;
1069
+ }
1070
+
1071
+ function sameTaskSubmission(
1072
+ task ,
1073
+ plannerId ,
1074
+ goal ,
1075
+ normalized
1076
+
1077
+
1078
+
1079
+
1080
+
1081
+
1082
+
1083
+
1084
+ ) {
1085
+ const execution = task.execution;
1086
+ if (!execution) return false;
1087
+ return task.userObjective === normalized.userObjective
1088
+ && task.interpretedObjective === `${goal.kind}:${normalized.userObjective}`
1089
+ && canonicalJson(task.authorizedScope) === canonicalJson(normalized.authorizedScope)
1090
+ && canonicalJson(task.prohibitedScope) === canonicalJson(normalized.prohibitedScope)
1091
+ && canonicalJson(task.successConditions) === canonicalJson(normalized.successConditions)
1092
+ && execution.plannerId === plannerId
1093
+ && execution.goalKind === goal.kind
1094
+ && canonicalJson(execution.plannerState.goal) === canonicalJson(goal)
1095
+ && execution.maxSteps === normalized.maxSteps
1096
+ && execution.maxAttemptsPerStep === normalized.maxAttemptsPerStep
1097
+ && execution.timeoutMs === normalized.timeoutMs;
1098
+ }
1099
+
1100
+ function deterministicActionId(taskId , stepKey , attempt , inputHash ) {
1101
+ return `task-${sha256(`${taskId}\0${stepKey}\0${attempt}\0${inputHash}`)}`;
1102
+ }
1103
+ function canonicalJson(value ) {
1104
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
1105
+ if (!value || typeof value !== 'object') return JSON.stringify(value);
1106
+ const input = value ;
1107
+ return `{${Object.keys(input).sort().filter((key) => input[key] !== undefined).map((key) => `${JSON.stringify(key)}:${canonicalJson(input[key])}`).join(',')}}`;
1108
+ }
1109
+ function sha256(value ) { return crypto.createHash('sha256').update(value).digest('hex'); }
1110
+ function observe(result ) {
1111
+ return {
1112
+ channel: 'semantic', domain: observationDomain(result.capability, result.provider), observedAt: new Date().toISOString(),
1113
+ ok: result.ok, capability: result.capability, provider: result.provider,
1114
+ ...(result.output === undefined ? {} : { output: structuredClone(result.output) }),
1115
+ evidence: structuredClone(result.evidence),
1116
+ ...(result.error === undefined ? {} : { error: structuredClone(result.error) })
1117
+ };
1118
+ }
1119
+ function detectPlannerLoop(records , stepKey , inputHash ) {
1120
+ const signatures = records.filter((record) => record.state !== 'BLOCKED').map((record) => `${record.stepKey}:${record.inputHash}`);
1121
+ signatures.push(`${stepKey}:${inputHash}`);
1122
+ for (const width of [1, 2, 3]) {
1123
+ if (signatures.length < 3 * width) continue;
1124
+ const tail = signatures.slice(-width);
1125
+ const prior = signatures.slice(-2 * width, -width);
1126
+ const earlier = signatures.slice(-3 * width, -2 * width);
1127
+ if (tail.join('\0') === prior.join('\0') && tail.join('\0') === earlier.join('\0')) return true;
1128
+ }
1129
+ return false;
1130
+ }
1131
+ function sameBrowserDestination(actualRaw , expectedRaw ) {
1132
+ try {
1133
+ const actual = new URL(actualRaw);
1134
+ const expected = new URL(expectedRaw);
1135
+ actual.hash = '';
1136
+ expected.hash = '';
1137
+ return actual.origin === expected.origin
1138
+ && actual.search === expected.search
1139
+ && (actual.pathname === expected.pathname || actual.pathname.replace(/\/$/, '') === expected.pathname.replace(/\/$/, ''));
1140
+ } catch { return false; }
1141
+ }
1142
+ function asRecord(value ) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
1143
+ function boundedInteger(value , min , max , fallback ) { const parsed = value === undefined ? fallback : Number(value); if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new OperatorError('TASK_BUDGET_INVALID', 'Task budget is invalid.'); return parsed; }
1144
+ function boundedText(value , max , label ) { if (typeof value !== 'string' || !value || value.length > max || value.includes('\0')) throw new OperatorError('TASK_INPUT_INVALID', `${label} is invalid.`); return value; }
1145
+ function boundedTextArray(value , maxItems , maxText , label ) {
1146
+ if (!Array.isArray(value) || value.length > maxItems) throw new OperatorError('TASK_INPUT_INVALID', `${label} is invalid.`);
1147
+ return value.map((entry, index) => boundedText(entry, maxText, `${label}[${index}]`));
1148
+ }