pi-background-tasks 0.9.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKGROUND-TASKS-INSTRUCTIONS.md +63 -0
- package/PUBLISHING.md +43 -29
- package/README.md +233 -441
- package/TESTING.md +15 -9
- package/TEST_PLAN.md +43 -17
- package/docs/INDEX.md +157 -0
- package/docs/api/eventbus-v1.md +166 -0
- package/docs/assets/architecture.svg +78 -0
- package/docs/assets/footer-dock.svg +47 -0
- package/docs/assets/logo.svg +49 -0
- package/docs/attestations.json +189 -0
- package/docs/choose-a-workflow.md +98 -0
- package/docs/commands/bg-clear.md +70 -0
- package/docs/commands/bg-update.md +82 -0
- package/docs/commands/bg.md +90 -0
- package/docs/commands/fusion-models.md +70 -0
- package/docs/commands/fusion.md +69 -0
- package/docs/commands/jobs.md +74 -0
- package/docs/commands/kill.md +82 -0
- package/docs/commands/logs.md +90 -0
- package/docs/commands/task-manager.md +109 -0
- package/docs/concepts/completion-delivery.md +66 -0
- package/docs/concepts/context-projection-and-budgeting.md +79 -0
- package/docs/getting-started.md +122 -0
- package/docs/manifest.json +1825 -0
- package/docs/operations/configuration.md +110 -0
- package/docs/operations/releasing.md +67 -0
- package/docs/operations/testing.md +101 -0
- package/docs/operations/troubleshooting.md +38 -0
- package/docs/read-before-edit.md +94 -0
- package/docs/reference/runtime-contracts.md +213 -0
- package/docs/reference/shortcuts-and-dock.md +70 -0
- package/docs/subsystems/attested-pi-runs.md +141 -0
- package/docs/subsystems/background-task-runtime.md +85 -0
- package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
- package/docs/subsystems/delegation.md +190 -0
- package/docs/subsystems/docs-freshness-gate.md +26 -0
- package/docs/subsystems/fusion.md +121 -0
- package/docs/subsystems/host-ui-and-telemetry.md +83 -0
- package/docs/tools/bg_delegate.md +193 -0
- package/docs/tools/bg_kill.md +114 -0
- package/docs/tools/bg_logs.md +133 -0
- package/docs/tools/bg_result.md +120 -0
- package/docs/tools/bg_run.md +168 -0
- package/docs/tools/bg_run_pi_attested.md +170 -0
- package/docs/tools/bg_status.md +111 -0
- package/docs/tools/fusion_investigate.md +116 -0
- package/docs/tools/fusion_reason.md +75 -0
- package/docs/tools/fusion_research.md +162 -0
- package/docs/tools/fusion_validate.md +206 -0
- package/logo.png +0 -0
- package/package.json +25 -7
- package/src/core/delegate/budget.ts +1 -1
- package/src/core/delegate/launch.ts +5 -0
- package/src/core/fusion/artifacts.ts +34 -4
- package/src/core/fusion/budget.ts +112 -20
- package/src/core/fusion/child-protocol.ts +82 -0
- package/src/core/fusion/clean-context.ts +91 -0
- package/src/core/fusion/config.ts +124 -35
- package/src/core/fusion/context.ts +29 -7
- package/src/core/fusion/evaluation.ts +392 -15
- package/src/core/fusion/orchestrator.ts +217 -23
- package/src/core/fusion/pi-child.ts +183 -23
- package/src/core/fusion/prompts.ts +39 -26
- package/src/core/fusion/source-policy.ts +257 -0
- package/src/core/fusion/types.ts +156 -11
- package/src/core/fusion/web-fetch.ts +104 -15
- package/src/core/fusion/workflows.ts +119 -65
- package/src/extension.ts +3 -3
- package/src/fusion-child-extension.ts +159 -120
- package/src/fusion-extension.ts +585 -240
- package/src/testing/normalize.ts +0 -22
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { closeSync, fsyncSync, openSync, writeSync } from 'node:fs';
|
|
3
|
-
import type { Usage } from '@earendil-works/pi-ai';
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, openSync, readFileSync, writeSync } from 'node:fs';
|
|
4
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
5
4
|
import { Type, type Static } from 'typebox';
|
|
6
5
|
import {
|
|
@@ -13,22 +12,40 @@ import {
|
|
|
13
12
|
FusionWebFetchError,
|
|
14
13
|
FUSION_WEB_FETCH_TIMEOUT_MS,
|
|
15
14
|
} from './core/fusion/web-fetch.js';
|
|
15
|
+
import {
|
|
16
|
+
canonicalizeFusionPublicUrl,
|
|
17
|
+
parseFusionSourcePolicy,
|
|
18
|
+
} from './core/fusion/source-policy.js';
|
|
19
|
+
import {
|
|
20
|
+
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
21
|
+
FUSION_CHILD_RESULT_PREFIX,
|
|
22
|
+
FUSION_RESEARCH_ENABLED_ENV,
|
|
23
|
+
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
24
|
+
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
25
|
+
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
26
|
+
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
27
|
+
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
28
|
+
buildFusionChildResultMetadata,
|
|
29
|
+
type FusionChildResultMetadata,
|
|
30
|
+
} from './core/fusion/child-protocol.js';
|
|
16
31
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
export {
|
|
33
|
+
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
34
|
+
FUSION_CHILD_RESULT_PREFIX,
|
|
35
|
+
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
36
|
+
FUSION_RESEARCH_ENABLED_ENV,
|
|
37
|
+
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
38
|
+
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
39
|
+
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
40
|
+
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
41
|
+
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
42
|
+
buildFusionChildResultMetadata,
|
|
43
|
+
type FusionChildResultMetadata,
|
|
44
|
+
type FusionChildResultUsageMetadata,
|
|
45
|
+
type FusionChildTextBlockMetadata,
|
|
46
|
+
} from './core/fusion/child-protocol.js';
|
|
22
47
|
|
|
23
|
-
|
|
24
|
-
* Aggregate ceiling on tool-result bytes a single candidate child may accumulate.
|
|
25
|
-
*
|
|
26
|
-
* v1 deliberately has no tool-CALL cap, so this byte budget is the only bound on how much
|
|
27
|
-
* a read-only candidate can pull into its context. 8 MiB is generous for targeted
|
|
28
|
-
* grep/read investigation while still preventing an unbounded read loop from degrading
|
|
29
|
-
* into an opaque provider-side context failure.
|
|
30
|
-
*/
|
|
31
|
-
export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 8 * 1024 * 1024;
|
|
48
|
+
const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
32
49
|
|
|
33
50
|
const FusionWebFetchParams = Type.Object(
|
|
34
51
|
{
|
|
@@ -58,30 +75,14 @@ interface FusionWebFetchDetails {
|
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
interface FusionWebFetchAuditMetadata {
|
|
61
|
-
url
|
|
78
|
+
url?: string | undefined;
|
|
79
|
+
rejected_url_sha256?: string | undefined;
|
|
62
80
|
final_url?: string | undefined;
|
|
63
81
|
http_status?: number | undefined;
|
|
64
82
|
response_bytes?: number | undefined;
|
|
65
83
|
content_sha256?: string | undefined;
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
export interface FusionChildTextBlockMetadata {
|
|
69
|
-
utf8_bytes: number;
|
|
70
|
-
sha256: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type FusionChildResultUsageMetadata = Usage;
|
|
74
|
-
|
|
75
|
-
export interface FusionChildResultMetadata {
|
|
76
|
-
schema_version: typeof FUSION_CHILD_RESULT_SCHEMA_VERSION;
|
|
77
|
-
provider: string;
|
|
78
|
-
model: string;
|
|
79
|
-
stop_reason: string;
|
|
80
|
-
text_blocks: FusionChildTextBlockMetadata[];
|
|
81
|
-
text_sha256: string;
|
|
82
|
-
usage: FusionChildResultUsageMetadata;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
86
|
function sha256(value: string | Buffer): string {
|
|
86
87
|
return createHash('sha256').update(value).digest('hex');
|
|
87
88
|
}
|
|
@@ -119,42 +120,32 @@ function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): v
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
provider: message.provider,
|
|
149
|
-
model: message.model,
|
|
150
|
-
stop_reason: message.stopReason,
|
|
151
|
-
text_blocks: textBlocks.map((text) => ({
|
|
152
|
-
utf8_bytes: Buffer.byteLength(text, 'utf8'),
|
|
153
|
-
sha256: sha256(text),
|
|
154
|
-
})),
|
|
155
|
-
text_sha256: sha256(textBlocks.join('')),
|
|
156
|
-
usage,
|
|
157
|
-
};
|
|
123
|
+
function writeToolCallLogSeal(
|
|
124
|
+
path: string,
|
|
125
|
+
recordCount: number,
|
|
126
|
+
totalResultBytes: number,
|
|
127
|
+
complete: boolean,
|
|
128
|
+
): void {
|
|
129
|
+
const logBytes = readFileSync(path);
|
|
130
|
+
const seal = {
|
|
131
|
+
schema_version: FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
132
|
+
status: complete ? 'complete' : 'failed',
|
|
133
|
+
record_count: recordCount,
|
|
134
|
+
total_result_bytes: totalResultBytes,
|
|
135
|
+
log_sha256: sha256(logBytes),
|
|
136
|
+
} as const;
|
|
137
|
+
const bytes = Buffer.from(`${JSON.stringify(seal)}\n`, 'utf8');
|
|
138
|
+
let fd: number | undefined;
|
|
139
|
+
try {
|
|
140
|
+
fd = openSync(`${path}${FUSION_TOOL_CALL_SEAL_SUFFIX}`, 'wx', 0o600);
|
|
141
|
+
const written = writeSync(fd, bytes);
|
|
142
|
+
if (written !== bytes.length) {
|
|
143
|
+
throw new Error(`fusion tool-call seal short write: ${String(written)} of ${String(bytes.length)} bytes`);
|
|
144
|
+
}
|
|
145
|
+
fsyncSync(fd);
|
|
146
|
+
} finally {
|
|
147
|
+
if (fd !== undefined) closeSync(fd);
|
|
148
|
+
}
|
|
158
149
|
}
|
|
159
150
|
|
|
160
151
|
async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
|
|
@@ -199,7 +190,9 @@ function stringField(value: object, key: string): string | undefined {
|
|
|
199
190
|
}
|
|
200
191
|
|
|
201
192
|
function fetchAuditMetadataFromObject(value: object, fallbackUrl: string): FusionWebFetchAuditMetadata {
|
|
202
|
-
const metadata: FusionWebFetchAuditMetadata = {
|
|
193
|
+
const metadata: FusionWebFetchAuditMetadata = {
|
|
194
|
+
url: stringField(value, 'url') ?? canonicalizeFusionPublicUrl(fallbackUrl),
|
|
195
|
+
};
|
|
203
196
|
const finalUrl = stringField(value, 'final_url');
|
|
204
197
|
if (finalUrl !== undefined) metadata.final_url = finalUrl;
|
|
205
198
|
const status = numberField(value, 'status');
|
|
@@ -211,15 +204,48 @@ function fetchAuditMetadataFromObject(value: object, fallbackUrl: string): Fusio
|
|
|
211
204
|
return metadata;
|
|
212
205
|
}
|
|
213
206
|
|
|
214
|
-
function fetchAuditMetadataFromError(error: unknown,
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
function fetchAuditMetadataFromError(error: unknown, attemptedUrl: string): FusionWebFetchAuditMetadata {
|
|
208
|
+
const metadata: FusionWebFetchAuditMetadata = { rejected_url_sha256: sha256(attemptedUrl) };
|
|
209
|
+
if (error instanceof FusionWebFetchError && typeof error === 'object' && error !== null) {
|
|
210
|
+
const status = numberField(error, 'status');
|
|
211
|
+
if (status !== undefined) metadata.http_status = status;
|
|
212
|
+
}
|
|
213
|
+
return metadata;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
function readRegularFileNoSymlinkSync(path: string, label: string): Buffer {
|
|
218
|
+
let fd: number | undefined;
|
|
219
|
+
try {
|
|
220
|
+
fd = openSync(path, constants.O_RDONLY | FUSION_CHILD_O_NOFOLLOW);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (typeof error === 'object' && error !== null && Reflect.get(error, 'code') === 'ELOOP') {
|
|
223
|
+
throw new Error(`${label} at ${path} is a symlink; refusing to follow it`);
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const stats = fstatSync(fd);
|
|
229
|
+
if (!stats.isFile()) throw new Error(`${label} at ${path} is not a regular file`);
|
|
230
|
+
return readFileSync(fd);
|
|
231
|
+
} finally {
|
|
232
|
+
if (fd !== undefined) closeSync(fd);
|
|
217
233
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function loadDeclaredResearchUrls(): ReadonlySet<string> {
|
|
237
|
+
const policyPath = process.env[FUSION_SOURCE_POLICY_PATH_ENV];
|
|
238
|
+
const expectedHash = process.env[FUSION_SOURCE_POLICY_SHA256_ENV];
|
|
239
|
+
if (policyPath === undefined || expectedHash === undefined) {
|
|
240
|
+
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires source policy path and sha256`);
|
|
221
241
|
}
|
|
222
|
-
|
|
242
|
+
if (!/^[0-9a-f]{64}$/.test(expectedHash)) throw new Error('fusion source policy hash is malformed');
|
|
243
|
+
const bytes = readRegularFileNoSymlinkSync(policyPath, 'fusion source policy');
|
|
244
|
+
if (sha256(bytes) !== expectedHash) throw new Error('fusion source policy hash mismatch');
|
|
245
|
+
const text = bytes.toString('utf8');
|
|
246
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) throw new Error('fusion source policy is not UTF-8');
|
|
247
|
+
const parsed = parseFusionSourcePolicy(JSON.parse(text));
|
|
248
|
+
return new Set(parsed.sources.map((source) => source.canonical_url));
|
|
223
249
|
}
|
|
224
250
|
|
|
225
251
|
function fusionWebFetchResultText(result: Awaited<ReturnType<typeof fusionWebFetch>>): string {
|
|
@@ -255,6 +281,7 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
255
281
|
if (researchEnabled === '1' && toolCallLogPath === undefined) {
|
|
256
282
|
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires ${FUSION_TOOL_CALL_LOG_PATH_ENV}`);
|
|
257
283
|
}
|
|
284
|
+
const declaredResearchUrls = researchEnabled === '1' ? loadDeclaredResearchUrls() : undefined;
|
|
258
285
|
const fetchAuditMetadata = new Map<string, FusionWebFetchAuditMetadata>();
|
|
259
286
|
if (toolCallLogPath !== undefined) {
|
|
260
287
|
// Create the log immediately, before tools can run. Without this, an absent file
|
|
@@ -264,54 +291,59 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
264
291
|
closeSync(openSync(toolCallLogPath, 'a', 0o600));
|
|
265
292
|
let ordinal = 0;
|
|
266
293
|
let totalToolResultBytes = 0;
|
|
294
|
+
let auditFailed = false;
|
|
267
295
|
const starts = new Map<string, number>();
|
|
268
296
|
pi.on('tool_call', (event) => {
|
|
269
297
|
starts.set(event.toolCallId, Date.now());
|
|
270
298
|
});
|
|
271
299
|
pi.on('tool_result', (event) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const fetchMetadata = fetchAuditMetadata.get(event.toolCallId);
|
|
288
|
-
fetchAuditMetadata.delete(event.toolCallId);
|
|
289
|
-
const record: FusionToolCallLogRecord = {
|
|
290
|
-
schema_version: FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
291
|
-
ordinal,
|
|
292
|
-
tool_name: event.toolName,
|
|
293
|
-
arguments_sha256: sha256(argumentsBytes),
|
|
294
|
-
arguments_bytes: argumentsBytes.length,
|
|
295
|
-
result_bytes: resultBytes.length,
|
|
296
|
-
result_sha256: sha256(resultBytes),
|
|
297
|
-
status: event.isError === true ? 'error' : 'ok',
|
|
298
|
-
duration_ms: Math.max(0, Date.now() - start),
|
|
299
|
-
...(fetchMetadata === undefined ? {} : fetchMetadata),
|
|
300
|
-
};
|
|
301
|
-
ordinal += 1;
|
|
302
|
-
appendToolCallLogLine(toolCallLogPath, record);
|
|
303
|
-
// Aggregate output ceiling. There is no tool-CALL cap in v1 by design, so bytes are
|
|
304
|
-
// the only bound on how much a read-only candidate can pull into its context. The
|
|
305
|
-
// record is durable before this check, so the offending call stays auditable; the
|
|
306
|
-
// failure is loud rather than a truncation, because a silently shortened tool result
|
|
307
|
-
// would corrupt the candidate's reasoning with no signal at all.
|
|
308
|
-
totalToolResultBytes += resultBytes.length;
|
|
309
|
-
if (totalToolResultBytes > FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES) {
|
|
310
|
-
throw new Error(
|
|
311
|
-
`fusion candidate exceeded the aggregate tool-output budget: ${String(totalToolResultBytes)} bytes across ${String(ordinal)} calls exceeds ${String(FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES)}`,
|
|
300
|
+
try {
|
|
301
|
+
const start = starts.get(event.toolCallId);
|
|
302
|
+
if (start === undefined) {
|
|
303
|
+
throw new Error(`fusion tool-call log missing start for ${event.toolCallId}`);
|
|
304
|
+
}
|
|
305
|
+
starts.delete(event.toolCallId);
|
|
306
|
+
const argumentsBytes = utf8JsonBytes(event.input, 'arguments');
|
|
307
|
+
const resultBytes = utf8JsonBytes(
|
|
308
|
+
{
|
|
309
|
+
content: event.content,
|
|
310
|
+
details: event.details,
|
|
311
|
+
isError: event.isError,
|
|
312
|
+
usage: event.usage,
|
|
313
|
+
},
|
|
314
|
+
'result',
|
|
312
315
|
);
|
|
316
|
+
const fetchMetadata = fetchAuditMetadata.get(event.toolCallId);
|
|
317
|
+
fetchAuditMetadata.delete(event.toolCallId);
|
|
318
|
+
const record: FusionToolCallLogRecord = {
|
|
319
|
+
schema_version: FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
320
|
+
ordinal,
|
|
321
|
+
tool_name: event.toolName,
|
|
322
|
+
arguments_sha256: sha256(argumentsBytes),
|
|
323
|
+
arguments_bytes: argumentsBytes.length,
|
|
324
|
+
result_bytes: resultBytes.length,
|
|
325
|
+
result_sha256: sha256(resultBytes),
|
|
326
|
+
status: event.isError === true ? 'error' : 'ok',
|
|
327
|
+
duration_ms: Math.max(0, Date.now() - start),
|
|
328
|
+
...(fetchMetadata === undefined ? {} : fetchMetadata),
|
|
329
|
+
};
|
|
330
|
+
ordinal += 1;
|
|
331
|
+
appendToolCallLogLine(toolCallLogPath, record);
|
|
332
|
+
totalToolResultBytes += resultBytes.length;
|
|
333
|
+
if (totalToolResultBytes > FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`fusion candidate exceeded the aggregate tool-output budget: ${String(totalToolResultBytes)} bytes across ${String(ordinal)} calls exceeds ${String(FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES)}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
auditFailed = true;
|
|
340
|
+
throw error;
|
|
313
341
|
}
|
|
314
342
|
});
|
|
343
|
+
pi.on('agent_end', () => {
|
|
344
|
+
const complete = !auditFailed && starts.size === 0;
|
|
345
|
+
writeToolCallLogSeal(toolCallLogPath, ordinal, totalToolResultBytes, complete);
|
|
346
|
+
});
|
|
315
347
|
}
|
|
316
348
|
|
|
317
349
|
if (researchEnabled === '1') {
|
|
@@ -332,10 +364,17 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
332
364
|
},
|
|
333
365
|
async execute(toolCallId, params) {
|
|
334
366
|
try {
|
|
367
|
+
const canonicalUrl = canonicalizeFusionPublicUrl(params.url);
|
|
368
|
+
if (params.url !== canonicalUrl) {
|
|
369
|
+
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} URL must exactly match its declared canonical URL`);
|
|
370
|
+
}
|
|
371
|
+
if (declaredResearchUrls === undefined || !declaredResearchUrls.has(canonicalUrl)) {
|
|
372
|
+
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} URL was not declared in the research source policy`);
|
|
373
|
+
}
|
|
335
374
|
const result = await fusionWebFetch(
|
|
336
375
|
params.extract === undefined
|
|
337
|
-
? { url:
|
|
338
|
-
: { url:
|
|
376
|
+
? { url: canonicalUrl }
|
|
377
|
+
: { url: canonicalUrl, extract: params.extract },
|
|
339
378
|
);
|
|
340
379
|
fetchAuditMetadata.set(toolCallId, fetchAuditMetadataFromObject(result, params.url));
|
|
341
380
|
return {
|