surf-cli 2.8.0 → 2.10.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 (47) hide show
  1. package/README.md +146 -8
  2. package/native/abort.cjs +65 -0
  3. package/native/activity-journal.cjs +55 -0
  4. package/native/ai-queue.cjs +64 -0
  5. package/native/aistudio-build.cjs +21 -13
  6. package/native/aistudio-client.cjs +40 -20
  7. package/native/browser-lock.cjs +2 -2
  8. package/native/chatgpt-client.cjs +49 -31
  9. package/native/cli.cjs +352 -482
  10. package/native/client-transport.cjs +168 -0
  11. package/native/do-executor.cjs +68 -510
  12. package/native/do-parser.cjs +8 -249
  13. package/native/doctor.cjs +55 -5
  14. package/native/endpoint.cjs +174 -0
  15. package/native/file-transfer.cjs +734 -0
  16. package/native/gemini-client.cjs +156 -71
  17. package/native/grok-client.cjs +98 -89
  18. package/native/host-helpers.cjs +43 -26
  19. package/native/host-sessions.cjs +287 -0
  20. package/native/host.cjs +998 -620
  21. package/native/listener.cjs +20 -0
  22. package/native/mcp-server.cjs +60 -65
  23. package/native/network-export.cjs +116 -0
  24. package/native/network-store.cjs +38 -58
  25. package/native/perplexity-client.cjs +46 -17
  26. package/native/playbook-authoring.cjs +44 -0
  27. package/native/playbook-cli.cjs +157 -0
  28. package/native/playbook-client.cjs +259 -0
  29. package/native/playbook-receipts.cjs +109 -0
  30. package/native/playbook-records.cjs +208 -0
  31. package/native/playbook-runtime.cjs +177 -0
  32. package/native/playbooks.cjs +235 -0
  33. package/native/private-state.cjs +156 -0
  34. package/native/redaction.cjs +104 -0
  35. package/native/remote-auth.cjs +279 -0
  36. package/native/remote-transport.cjs +337 -0
  37. package/native/request-pending.cjs +148 -0
  38. package/native/socket-path.cjs +1 -1
  39. package/native/workflow-definition.cjs +368 -0
  40. package/native/workflow-runtime.cjs +225 -0
  41. package/package.json +9 -6
  42. package/playbooks/page/ops/read.json +22 -0
  43. package/playbooks/page/playbook.json +7 -0
  44. package/scripts/install-native-host.cjs +36 -5
  45. package/skills/README.md +11 -5
  46. package/skills/deep-x-research/SKILL.md +106 -0
  47. package/skills/surf/SKILL.md +72 -5
@@ -0,0 +1,109 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const {
5
+ atomicWriteJson,
6
+ ensurePrivateDir,
7
+ getPrivateStateRoot,
8
+ readPrivateFile,
9
+ readPrivateJson,
10
+ writePrivateFileExclusive,
11
+ } = require("./private-state.cjs");
12
+
13
+ function receiptRoot(root = getPrivateStateRoot()) {
14
+ return path.join(root, "playbook-receipts");
15
+ }
16
+
17
+ function loadSalt(root) {
18
+ const directory = receiptRoot(root);
19
+ ensurePrivateDir(directory, root);
20
+ const saltPath = path.join(directory, ".salt");
21
+ const existing = readPrivateFile(saltPath, { root, allowMissing: true, fallback: null, encoding: "utf8" });
22
+ if (existing) return Buffer.from(existing.trim(), "hex");
23
+ const salt = crypto.randomBytes(32);
24
+ try {
25
+ writePrivateFileExclusive(saltPath, `${salt.toString("hex")}\n`, { root, encoding: "utf8" });
26
+ } catch (error) {
27
+ if (error?.code !== "EEXIST") throw error;
28
+ }
29
+ return Buffer.from(readPrivateFile(saltPath, { root, encoding: "utf8" }).trim(), "hex");
30
+ }
31
+
32
+ function semanticClaimKey({ playbookId, op, args, root = getPrivateStateRoot() }) {
33
+ const semantic = {};
34
+ for (const name of op.safety.key) {
35
+ if (args[name] === undefined) throw new Error(`semantic safety argument is missing: ${name}`);
36
+ semantic[name] = args[name];
37
+ }
38
+ const message = JSON.stringify({ playbook: playbookId, op: op.id, semantic });
39
+ return crypto.createHmac("sha256", loadSalt(root)).update(message).digest("hex");
40
+ }
41
+
42
+ function attemptFiles(claimDir) {
43
+ if (!fs.existsSync(claimDir)) return [];
44
+ return fs.readdirSync(claimDir)
45
+ .filter((name) => name.endsWith(".json"))
46
+ .map((name) => readPrivateJson(path.join(claimDir, name), null, { root: path.dirname(claimDir) }))
47
+ .filter(Boolean)
48
+ .sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
49
+ }
50
+
51
+ function reserveReceipt({ playbookId, op, args, repeat = false, retryAttempt, overrideInDoubt = false, attemptId = crypto.randomUUID(), now = Date.now(), root = getPrivateStateRoot() }) {
52
+ if (op.effect !== "write") return null;
53
+ const base = receiptRoot(root);
54
+ ensurePrivateDir(base, root);
55
+ const claimKey = semanticClaimKey({ playbookId, op, args, root });
56
+ const claimDir = path.join(base, claimKey);
57
+ let created = false;
58
+ try {
59
+ fs.mkdirSync(claimDir, { mode: 0o700 });
60
+ created = true;
61
+ } catch (error) {
62
+ if (error?.code !== "EEXIST") throw error;
63
+ }
64
+ if (!created) {
65
+ const existing = attemptFiles(claimDir);
66
+ const latest = existing.at(-1);
67
+ if (!latest) {
68
+ throw new Error("write blocked by an unresolved semantic claim created before an attempt receipt was durable");
69
+ }
70
+ if (retryAttempt) {
71
+ const retry = existing.find((attempt) => attempt.attemptId === retryAttempt);
72
+ if (!retry) throw new Error(`receipt attempt not found: ${retryAttempt}`);
73
+ if (retry.status === "verified") throw new Error(`receipt attempt is already verified: ${retryAttempt}`);
74
+ if (retry.status !== "reserved" && retry.status !== "not_dispatched" && !op.safety.serverIdempotency) {
75
+ throw new Error("--retry-attempt after dispatch requires declared server idempotency");
76
+ }
77
+ return { claimKey, claimDir, attemptId: retryAttempt, path: path.join(claimDir, `${retryAttempt}.json`), receipt: retry };
78
+ }
79
+ if (latest) {
80
+ const ageMs = Math.max(0, now - Date.parse(latest.updatedAt || latest.createdAt));
81
+ const windowMs = op.safety.windowMs ?? 30000;
82
+ const windowElapsed = op.safety.duplicate === "repeatable-window" && latest.status === "verified" && ageMs >= windowMs;
83
+ const deliberateOverride = overrideInDoubt && latest.status === "indeterminate";
84
+ const deliberateRepeat = repeat && latest.status === "verified";
85
+ if (!windowElapsed && !deliberateOverride && !deliberateRepeat) {
86
+ throw new Error(`write blocked by ${latest.status} receipt ${latest.attemptId}; use the policy-specific retry or override flag`);
87
+ }
88
+ }
89
+ }
90
+ const createdAt = new Date(now).toISOString();
91
+ const receipt = { version: 1, playbook: playbookId, op: op.id, claimKey, attemptId, status: "reserved", duplicate: op.safety.duplicate, createdAt, updatedAt: createdAt };
92
+ const receiptPath = path.join(claimDir, `${attemptId}.json`);
93
+ atomicWriteJson(path.join(claimDir, "claim"), { version: 1, playbook: playbookId, op: op.id, claimKey }, { root });
94
+ atomicWriteJson(receiptPath, receipt, { root });
95
+ return { claimKey, claimDir, attemptId, path: receiptPath, receipt };
96
+ }
97
+
98
+ function updateReceipt(handle, status, details = {}, root = getPrivateStateRoot()) {
99
+ if (!handle) return null;
100
+ if (!["reserved", "not_dispatched", "dispatched", "indeterminate", "verified"].includes(status)) throw new Error(`invalid receipt status: ${status}`);
101
+ const current = readPrivateJson(handle.path, null, { root });
102
+ if (!current) throw new Error(`receipt is missing: ${handle.attemptId}`);
103
+ const receipt = { ...current, status, updatedAt: new Date().toISOString(), ...(details.error ? { errorCode: crypto.createHash("sha256").update(String(details.error)).digest("hex").slice(0, 16) } : {}) };
104
+ atomicWriteJson(handle.path, receipt, { root });
105
+ handle.receipt = receipt;
106
+ return receipt;
107
+ }
108
+
109
+ module.exports = { receiptRoot, reserveReceipt, semanticClaimKey, updateReceipt };
@@ -0,0 +1,208 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const {
5
+ appendPrivateJsonLine,
6
+ assertWithin,
7
+ atomicWriteJson,
8
+ ensurePrivateDir,
9
+ getPrivateStateRoot,
10
+ readPrivateFile,
11
+ readPrivateJson,
12
+ } = require("./private-state.cjs");
13
+ const { redactSensitiveFields, redactUrlSecrets, safeHeaders } = require("./redaction.cjs");
14
+ const { commandMetadata, promoteRedactedStepArgs } = require("./workflow-definition.cjs");
15
+ const { version: PACKAGE_VERSION } = require("../package.json");
16
+
17
+ function recordsRoot(root = getPrivateStateRoot()) {
18
+ return path.join(root, "records");
19
+ }
20
+
21
+ function activePath(root = getPrivateStateRoot()) {
22
+ return path.join(recordsRoot(root), "active.json");
23
+ }
24
+
25
+ function recordDirectory(recordId, root = getPrivateStateRoot()) {
26
+ if (typeof recordId !== "string" || !/^rec-[a-zA-Z0-9-]+$/.test(recordId)) throw new Error("record ID is invalid");
27
+ return path.join(recordsRoot(root), recordId);
28
+ }
29
+
30
+ function readRecord(recordId, root = getPrivateStateRoot()) {
31
+ return readPrivateJson(path.join(recordDirectory(recordId, root), "record.json"), null, { root });
32
+ }
33
+
34
+ function activeRecord(root = getPrivateStateRoot()) {
35
+ const active = readPrivateJson(activePath(root), null, { root });
36
+ return active ? readRecord(active.recordId, root) : null;
37
+ }
38
+
39
+ function writeRecord(record, root) {
40
+ atomicWriteJson(path.join(recordDirectory(record.id, root), "record.json"), record, { root });
41
+ }
42
+
43
+ function startRecord({ site, op, watch = false, network = false, includeInputValues = false, tabId, origin, root = getPrivateStateRoot() }) {
44
+ if (!site || !op) throw new Error("record start requires site and op");
45
+ if (activeRecord(root)) throw new Error("a playbook record is already active");
46
+ const id = `rec-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
47
+ const directory = recordDirectory(id, root);
48
+ ensurePrivateDir(directory, root);
49
+ const record = {
50
+ version: 1,
51
+ id,
52
+ site,
53
+ op,
54
+ status: "recording",
55
+ capture: { watch, network },
56
+ redaction: { includeInputValues },
57
+ provenance: { generator: "surf-cli", version: PACKAGE_VERSION },
58
+ ...(tabId ? { tabId } : {}),
59
+ ...(origin ? { origin } : {}),
60
+ startedAt: new Date().toISOString(),
61
+ eventCount: 0,
62
+ };
63
+ writeRecord(record, root);
64
+ atomicWriteJson(activePath(root), { recordId: id }, { root });
65
+ return record;
66
+ }
67
+
68
+ function appendRecordEvent(event, { root = getPrivateStateRoot(), allowPaused = false } = {}) {
69
+ const record = activeRecord(root);
70
+ if (!record || (record.status !== "recording" && !allowPaused)) return false;
71
+ appendPrivateJsonLine(path.join(recordDirectory(record.id, root), "events.jsonl"), { version: 1, recordId: record.id, ...event }, { root });
72
+ record.eventCount++;
73
+ writeRecord(record, root);
74
+ return true;
75
+ }
76
+
77
+ function updateActiveStatus(status, root = getPrivateStateRoot()) {
78
+ const record = activeRecord(root);
79
+ if (!record) throw new Error("no active playbook record");
80
+ const allowed = { recording: ["paused", "stopping"], paused: ["recording", "stopping"] };
81
+ if (!allowed[record.status]?.includes(status)) throw new Error(`cannot change record from ${record.status} to ${status}`);
82
+ record.status = status;
83
+ writeRecord(record, root);
84
+ return record;
85
+ }
86
+
87
+ function updateRecordContext({ tabId, origin }, root = getPrivateStateRoot()) {
88
+ const record = activeRecord(root);
89
+ if (!record) throw new Error("no active playbook record");
90
+ if (tabId) record.tabId = tabId;
91
+ if (origin) record.origin = origin;
92
+ writeRecord(record, root);
93
+ return record;
94
+ }
95
+
96
+ function markRecord(label, root = getPrivateStateRoot()) {
97
+ if (typeof label !== "string" || !label.trim()) throw new Error("record mark requires text");
98
+ const record = activeRecord(root);
99
+ if (!record) throw new Error("no active playbook record");
100
+ appendRecordEvent({ type: "mark", label: label.trim(), timestamp: new Date().toISOString() }, { root, allowPaused: true });
101
+ return activeRecord(root);
102
+ }
103
+
104
+ function readEvents(recordId, root = getPrivateStateRoot()) {
105
+ const filePath = path.join(recordDirectory(recordId, root), "events.jsonl");
106
+ const content = readPrivateFile(filePath, { root, allowMissing: true, fallback: "", encoding: "utf8" });
107
+ return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
108
+ }
109
+
110
+ function sanitizeTraceEntry(entry, includeInputValues = false) {
111
+ return {
112
+ ...redactSensitiveFields(entry),
113
+ url: redactUrlSecrets(entry.url),
114
+ requestHeaders: safeHeaders(entry.requestHeaders),
115
+ responseHeaders: safeHeaders(entry.responseHeaders),
116
+ responseBody: entry.responseBody === undefined ? undefined : "<response-body>",
117
+ ...(includeInputValues ? {} : { requestBody: entry.requestBody === undefined ? undefined : "<request-body>" }),
118
+ };
119
+ }
120
+
121
+ function attachNetworkTrace(recordId, entries, root = getPrivateStateRoot()) {
122
+ const record = readRecord(recordId, root);
123
+ if (!record) throw new Error(`record not found: ${recordId}`);
124
+ const sanitized = entries.map((entry) => sanitizeTraceEntry(entry, record.redaction.includeInputValues));
125
+ const directory = path.join(recordDirectory(recordId, root), "network");
126
+ ensurePrivateDir(directory, root);
127
+ const tracePath = path.join(directory, "trace.json");
128
+ atomicWriteJson(tracePath, { version: 1, recordId, capturedAt: new Date().toISOString(), entries: sanitized }, { root });
129
+ record.trace = { path: "network/trace.json", count: sanitized.length };
130
+ writeRecord(record, root);
131
+ return { path: tracePath, count: sanitized.length };
132
+ }
133
+
134
+ function draftFromRecord(recordId, root = getPrivateStateRoot()) {
135
+ const record = readRecord(recordId, root);
136
+ if (!record) throw new Error(`record not found: ${recordId}`);
137
+ const events = readEvents(recordId, root).filter((event) => event.type === "tool.completed");
138
+ let effect = "read";
139
+ const steps = [];
140
+ for (const event of events) {
141
+ const metadata = commandMetadata(event.command);
142
+ if (!metadata.recordable) continue;
143
+ if (["page-write", "unknown"].includes(metadata.effect)) effect = "write";
144
+ steps.push({ tool: event.command, args: event.argsRedacted || {} });
145
+ }
146
+ const tracePath = path.join(recordDirectory(recordId, root), "network", "trace.json");
147
+ const trace = readPrivateJson(tracePath, null, { root });
148
+ const strategies = [];
149
+ const observed = trace?.entries?.findLast((entry) => ["GET", "HEAD", "OPTIONS", "POST"].includes(entry.method) && entry.status >= 200 && entry.status < 400);
150
+ if (observed) {
151
+ const url = new URL(observed.url);
152
+ const query = Object.fromEntries(url.searchParams.entries());
153
+ url.search = "";
154
+ strategies.push({ using: "network", request: { method: observed.method, url: url.toString(), query, headers: safeHeaders(observed.requestHeaders), ...(observed.requestBody !== undefined ? { body: observed.requestBody } : {}) } });
155
+ }
156
+ let args = {};
157
+ if (steps.length > 0) {
158
+ const promoted = promoteRedactedStepArgs(steps);
159
+ args = promoted.args;
160
+ strategies.push({ using: "workflow", steps: promoted.steps });
161
+ }
162
+ if (strategies.length === 0) throw new Error("record has no executable evidence");
163
+ const safety = effect === "write" ? { authorization: "explicit", duplicate: "transactional", key: Object.keys(args).length ? Object.keys(args) : ["review_key"] } : undefined;
164
+ if (effect === "write" && !args.review_key && safety.key.includes("review_key")) args.review_key = { required: true, desc: "Semantic key for reviewed write replay" };
165
+ const op = { id: record.op, description: `Drafted from ${record.id}`, effect, args, ...(safety ? { safety } : {}), run: strategies, provenance: { recordId: record.id } };
166
+ const draftDir = path.join(recordDirectory(recordId, root), "draft");
167
+ ensurePrivateDir(draftDir, root);
168
+ atomicWriteJson(path.join(draftDir, "op.json"), op, { root });
169
+ return op;
170
+ }
171
+
172
+ function stopRecord({ draft = false, root = getPrivateStateRoot() } = {}) {
173
+ let record = updateActiveStatus("stopping", root);
174
+ let op;
175
+ if (draft) op = draftFromRecord(record.id, root);
176
+ record = readRecord(record.id, root);
177
+ record.status = draft ? "draft_created" : "stopped";
178
+ record.stoppedAt = new Date().toISOString();
179
+ writeRecord(record, root);
180
+ fs.unlinkSync(activePath(root));
181
+ return { record, ...(op ? { draft: op } : {}) };
182
+ }
183
+
184
+ function discardRecord(root = getPrivateStateRoot()) {
185
+ const record = activeRecord(root);
186
+ if (!record) throw new Error("no active playbook record");
187
+ const directory = assertWithin(recordsRoot(root), recordDirectory(record.id, root));
188
+ fs.rmSync(directory, { recursive: true, force: true });
189
+ try { fs.unlinkSync(activePath(root)); } catch {}
190
+ return { discarded: record.id };
191
+ }
192
+
193
+ module.exports = {
194
+ activeRecord,
195
+ appendRecordEvent,
196
+ attachNetworkTrace,
197
+ discardRecord,
198
+ draftFromRecord,
199
+ markRecord,
200
+ pauseRecord: (root) => updateActiveStatus("paused", root),
201
+ readEvents,
202
+ readRecord,
203
+ recordsRoot,
204
+ resumeRecord: (root) => updateActiveStatus("recording", root),
205
+ startRecord,
206
+ stopRecord,
207
+ updateRecordContext,
208
+ };
@@ -0,0 +1,177 @@
1
+ const { executeWorkflow } = require("./workflow-runtime.cjs");
2
+
3
+ function applyTemplate(value, args) {
4
+ if (typeof value === "string") {
5
+ const exact = value.match(/^\{\{([a-zA-Z0-9._-]+)\}\}$/);
6
+ if (exact) return args[exact[1]];
7
+ return value.replace(/\{\{([a-zA-Z0-9._-]+)\}\}/g, (_, name) => {
8
+ if (args[name] === undefined) throw new Error(`missing template argument: ${name}`);
9
+ return String(args[name]);
10
+ });
11
+ }
12
+ if (Array.isArray(value)) return value.map((item) => applyTemplate(item, args));
13
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, applyTemplate(item, args)]));
14
+ return value;
15
+ }
16
+
17
+ function resolveArgs(op, provided) {
18
+ const args = { ...provided };
19
+ for (const [name, spec] of Object.entries(op.args || {})) {
20
+ if (args[name] === undefined && spec.default !== undefined) args[name] = spec.default;
21
+ if (spec.required && args[name] === undefined) throw new Error(`Missing required argument: --${name}`);
22
+ }
23
+ return args;
24
+ }
25
+
26
+ function getJsonPath(value, jsonPath) {
27
+ if (!jsonPath || jsonPath === "$") return value;
28
+ if (typeof jsonPath !== "string" || !jsonPath.startsWith("$.")) throw new Error(`unsupported jsonPath: ${jsonPath}`);
29
+ let current = value;
30
+ for (const part of jsonPath.slice(2).split(".")) {
31
+ if (current === null || current === undefined || !Object.hasOwn(Object(current), part)) throw new Error(`extract path not found: ${jsonPath}`);
32
+ current = current[part];
33
+ }
34
+ return current;
35
+ }
36
+
37
+ function extractResult(result, extract) {
38
+ if (!extract) return result;
39
+ if (extract.jsonPath) {
40
+ const source = result.bodyJson !== undefined ? result.bodyJson : result;
41
+ return getJsonPath(source, extract.jsonPath);
42
+ }
43
+ if (extract.field) return result[extract.field];
44
+ return result;
45
+ }
46
+
47
+ function verifyResult(value, expect = {}, raw = value) {
48
+ if (expect.status !== undefined && raw?.status !== expect.status) throw new Error(`expected status ${expect.status}`);
49
+ if (expect.minItems !== undefined && (!Array.isArray(value) || value.length < expect.minItems)) throw new Error(`expected at least ${expect.minItems} items`);
50
+ if (expect.truthy && !value) throw new Error("expected a truthy result");
51
+ return true;
52
+ }
53
+
54
+ function withServerIdempotency(request, idempotency, attemptId) {
55
+ if (!idempotency || !attemptId) return request;
56
+ const next = { ...request };
57
+ if (idempotency.header) next.headers = { ...(request.headers || {}), [idempotency.header]: attemptId };
58
+ if (idempotency.query) next.query = { ...(request.query || {}), [idempotency.query]: attemptId };
59
+ if (idempotency.body) {
60
+ if (next.body === undefined) next.body = {};
61
+ if (!next.body || typeof next.body !== "object" || Array.isArray(next.body)) {
62
+ throw new Error("server idempotency body field requires an object request body");
63
+ }
64
+ next.body = { ...next.body, [idempotency.body]: attemptId };
65
+ }
66
+ return next;
67
+ }
68
+
69
+ function networkScript(request, allowedOrigins = []) {
70
+ const headers = request.headers || {};
71
+ const query = request.query || {};
72
+ const body = request.body;
73
+ return `(async () => {
74
+ const url = new URL(${JSON.stringify(request.url)}, location.href);
75
+ const allowedOrigins = ${JSON.stringify(allowedOrigins)};
76
+ if (allowedOrigins.length > 0 && !allowedOrigins.includes(url.origin)) {
77
+ throw new Error(\`playbook network origin is not allowed: \${url.origin}\`);
78
+ }
79
+ for (const [key, value] of Object.entries(${JSON.stringify(query)})) {
80
+ if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
81
+ }
82
+ const response = await fetch(url.toString(), {
83
+ method: ${JSON.stringify(request.method || "GET")},
84
+ credentials: "include",
85
+ headers: ${JSON.stringify(headers)},
86
+ ${body === undefined ? "" : `body: ${JSON.stringify(typeof body === "string" ? body : JSON.stringify(body))},`}
87
+ });
88
+ const text = await response.text();
89
+ let json;
90
+ try { json = JSON.parse(text); } catch {}
91
+ return { status: response.status, ok: response.ok, url: response.url, headers: Object.fromEntries(response.headers.entries()), body: text, bodyJson: json };
92
+ })()`;
93
+ }
94
+
95
+ async function runStrategy(strategy, context) {
96
+ if (strategy.using === "workflow") {
97
+ const result = await executeWorkflow(applyTemplate(strategy.steps, context.args), {
98
+ autoWait: strategy.autoWait !== false,
99
+ executeTool: async (tool, args, options) => {
100
+ await context.markDispatched?.();
101
+ return context.executeTool(tool, args, options);
102
+ },
103
+ onError: strategy.onError || "stop",
104
+ onEvent: context.onEvent,
105
+ signal: context.signal,
106
+ sleep: context.sleep,
107
+ stepDelay: strategy.stepDelay ?? 100,
108
+ vars: context.args,
109
+ });
110
+ if (result.status !== "completed") throw new Error(result.error || "workflow strategy failed");
111
+ return result;
112
+ }
113
+ if (strategy.using === "network") {
114
+ const request = withServerIdempotency(
115
+ applyTemplate(strategy.request, context.args),
116
+ context.serverIdempotency,
117
+ context.attemptId,
118
+ );
119
+ await context.markDispatched?.();
120
+ const response = await context.executeTool("javascript_tool", { code: networkScript(request, context.allowedOrigins) }, { signal: context.signal });
121
+ if (response?.error) throw new Error(typeof response.error === "string" ? response.error : JSON.stringify(response.error));
122
+ const result = response?.output !== undefined ? JSON.parse(response.output) : response?.value ?? response;
123
+ if (!result?.ok && (strategy.acceptStatus || []).includes(result?.status) === false) throw new Error(`network strategy returned ${result?.status || "an unknown status"}`);
124
+ return result;
125
+ }
126
+ if (strategy.using === "native") {
127
+ if (typeof context.executeNative !== "function") throw new Error("native strategy is not available");
128
+ return context.executeNative(strategy.handler, context.args, { signal: context.signal, markDispatched: context.markDispatched });
129
+ }
130
+ throw new Error(`unsupported strategy: ${strategy.using}`);
131
+ }
132
+
133
+ async function runPlaybookOp({ playbook, op, args: providedArgs = {}, attemptId, executeTool, executeNative, signal, sleep, onEvent = () => {}, beforeDispatch = async () => {}, afterDispatch = async () => {} }) {
134
+ const args = resolveArgs(op, providedArgs);
135
+ const attempts = [];
136
+ for (let index = 0; index < op.run.length; index++) {
137
+ const strategy = op.run[index];
138
+ let dispatched = false;
139
+ const markDispatched = async () => {
140
+ if (dispatched) return;
141
+ dispatched = true;
142
+ await beforeDispatch({ strategy, index, args });
143
+ };
144
+ onEvent({ type: "strategy.started", playbook: playbook.id, op: op.id, using: strategy.using, index, startedAt: new Date().toISOString() });
145
+ try {
146
+ const raw = await runStrategy(strategy, {
147
+ args,
148
+ attemptId,
149
+ executeTool,
150
+ executeNative,
151
+ signal,
152
+ sleep,
153
+ onEvent,
154
+ allowedOrigins: op.origins || playbook.origins || [],
155
+ markDispatched: op.effect === "write" ? markDispatched : undefined,
156
+ serverIdempotency: op.effect === "write" ? op.safety?.serverIdempotency : undefined,
157
+ });
158
+ const value = extractResult(raw, strategy.extract);
159
+ verifyResult(value, strategy.verify || strategy.expect || op.on?.success?.expect, raw);
160
+ if (op.effect === "write") await afterDispatch({ status: "verified", strategy, index });
161
+ onEvent({ type: "strategy.completed", playbook: playbook.id, op: op.id, using: strategy.using, index, endedAt: new Date().toISOString() });
162
+ return { status: "completed", playbook: playbook.id, op: op.id, strategy: strategy.using, value, provenance: playbook.provenance, attempts };
163
+ } catch (error) {
164
+ const message = error?.message || String(error);
165
+ attempts.push({ using: strategy.using, error: message });
166
+ onEvent({ type: "strategy.failed", playbook: playbook.id, op: op.id, using: strategy.using, index, error: message, endedAt: new Date().toISOString() });
167
+ if (op.effect === "write") {
168
+ await afterDispatch({ status: dispatched ? "indeterminate" : "not_dispatched", strategy, index, error: message });
169
+ throw error;
170
+ }
171
+ }
172
+ }
173
+ const detail = attempts.map((attempt) => `${attempt.using}: ${attempt.error}`).join("; ");
174
+ throw new Error(`All strategies failed${detail ? ` (${detail})` : ""}`);
175
+ }
176
+
177
+ module.exports = { resolveArgs, runPlaybookOp };