@pi-unipi/background-tasks 2.16.0 → 2.17.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.
- package/README.md +21 -27
- package/package.json +3 -4
- package/src/cards.ts +76 -0
- package/src/child-process.ts +1 -1
- package/src/config.ts +0 -42
- package/src/context-visible-conversation-v2.ts +1 -1
- package/src/delegate/artifacts.ts +1 -1
- package/src/delegate/launch.ts +17 -30
- package/src/delegate/result-package.ts +1 -1
- package/src/delegate/runner.ts +1 -20
- package/src/delegate/seed.ts +1 -1
- package/src/delegate-extension.ts +16 -168
- package/src/index.ts +53 -25
- package/src/json-utils.ts +56 -0
- package/src/package-assets.ts +51 -0
- package/src/registry.ts +8 -459
- package/src/task-manager.ts +13 -2
- package/src/tools.ts +4 -189
- package/src/types.ts +17 -70
- package/extensions/anthropic-attribution.ts +0 -1
- package/extensions/fusion-child.ts +0 -1
- package/src/anthropic-attribution-path.ts +0 -21
- package/src/anthropic-attribution.ts +0 -1983
- package/src/attested-pi-run.ts +0 -612
- package/src/fixtures/fusion-golden-bytes.json +0 -310
- package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
- package/src/fusion/artifacts.ts +0 -967
- package/src/fusion/budget.ts +0 -1162
- package/src/fusion/child-protocol.ts +0 -305
- package/src/fusion/claude-cache.ts +0 -207
- package/src/fusion/clean-context.ts +0 -91
- package/src/fusion/config.ts +0 -449
- package/src/fusion/context.ts +0 -265
- package/src/fusion/evaluation.ts +0 -800
- package/src/fusion/orchestrator.ts +0 -1288
- package/src/fusion/output-contract.ts +0 -34
- package/src/fusion/pi-child.ts +0 -2373
- package/src/fusion/prompts.ts +0 -345
- package/src/fusion/result-package.ts +0 -959
- package/src/fusion/source-policy.ts +0 -257
- package/src/fusion/types.ts +0 -1139
- package/src/fusion/web-fetch.ts +0 -1060
- package/src/fusion/workflows.ts +0 -184
- package/src/fusion-child-extension.ts +0 -1052
- package/src/fusion-extension.ts +0 -1293
- package/src/ui/fusion-model-selector.ts +0 -322
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { open } from 'node:fs/promises';
|
|
4
|
-
import { isIP } from 'node:net';
|
|
5
|
-
import { canonicalJson } from '../attested-pi-run.js';
|
|
6
|
-
import { isJsonObject, parseJsonText } from '../types.js';
|
|
7
|
-
import {
|
|
8
|
-
FUSION_SOURCE_POLICY_SCHEMA_VERSION,
|
|
9
|
-
FusionError,
|
|
10
|
-
type FusionDeclaredSourceV1,
|
|
11
|
-
type FusionSourcePolicyV1,
|
|
12
|
-
} from './types.js';
|
|
13
|
-
|
|
14
|
-
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
15
|
-
const O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
16
|
-
|
|
17
|
-
function sha256Text(value: string): string {
|
|
18
|
-
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function stripIpv6Brackets(hostname: string): string {
|
|
22
|
-
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isBlockedHostname(hostname: string): boolean {
|
|
26
|
-
const lower = hostname.toLowerCase();
|
|
27
|
-
return (
|
|
28
|
-
lower === 'localhost' ||
|
|
29
|
-
lower.endsWith('.localhost') ||
|
|
30
|
-
lower === 'metadata' ||
|
|
31
|
-
lower === 'metadata.local' ||
|
|
32
|
-
lower === 'metadata.google.internal' ||
|
|
33
|
-
lower === 'metadata.goog' ||
|
|
34
|
-
lower === 'instance-data' ||
|
|
35
|
-
lower === 'instance-data.ec2.internal'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isBlockedIpv4(host: string): boolean {
|
|
40
|
-
const parts = host.split('.').map((part) => Number.parseInt(part, 10));
|
|
41
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
42
|
-
return true;
|
|
43
|
-
const [a = 0, b = 0, c = 0] = parts;
|
|
44
|
-
if (a === 0 || a === 10 || a === 127) return true;
|
|
45
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
46
|
-
if (a === 169 && b === 254) return true;
|
|
47
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
48
|
-
if (a === 192 && b === 168) return true;
|
|
49
|
-
if (a === 192 && b === 0 && c === 0) return true;
|
|
50
|
-
if (a === 192 && b === 0 && c === 2) return true;
|
|
51
|
-
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
52
|
-
if (a === 198 && b === 51 && c === 100) return true;
|
|
53
|
-
if (a === 203 && b === 0 && c === 113) return true;
|
|
54
|
-
if (a >= 224) return true;
|
|
55
|
-
return host === '255.255.255.255' || host === '168.63.129.16';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isBlockedIpv6(host: string): boolean {
|
|
59
|
-
const lower = host.toLowerCase();
|
|
60
|
-
return (
|
|
61
|
-
lower === '::' ||
|
|
62
|
-
lower === '::1' ||
|
|
63
|
-
lower.startsWith('::ffff:') ||
|
|
64
|
-
lower.startsWith('64:ff9b::') ||
|
|
65
|
-
lower.startsWith('64:ff9b:1:') ||
|
|
66
|
-
lower.startsWith('100:') ||
|
|
67
|
-
lower.startsWith('2001:2:') ||
|
|
68
|
-
lower.startsWith('2001:db8:') ||
|
|
69
|
-
lower.startsWith('2002:') ||
|
|
70
|
-
lower.startsWith('fc') ||
|
|
71
|
-
lower.startsWith('fd') ||
|
|
72
|
-
lower.startsWith('fe8') ||
|
|
73
|
-
lower.startsWith('fe9') ||
|
|
74
|
-
lower.startsWith('fea') ||
|
|
75
|
-
lower.startsWith('feb') ||
|
|
76
|
-
lower.startsWith('ff')
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function canonicalizeFusionPublicUrl(value: string): string {
|
|
81
|
-
let url: URL;
|
|
82
|
-
try {
|
|
83
|
-
url = new URL(value);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
throw new FusionError(
|
|
86
|
-
`fusion research declared source URL is malformed: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
if (url.username !== '' || url.password !== '') {
|
|
91
|
-
throw new FusionError('fusion research source URL must not contain credentials', {
|
|
92
|
-
code: 'orchestration_failed',
|
|
93
|
-
childCreated: false,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
97
|
-
throw new FusionError('fusion research source URL must use http or https', {
|
|
98
|
-
code: 'orchestration_failed',
|
|
99
|
-
childCreated: false,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
url.username = '';
|
|
103
|
-
url.password = '';
|
|
104
|
-
url.hash = '';
|
|
105
|
-
const normalizedHost = stripIpv6Brackets(url.hostname.toLowerCase().replace(/\.+$/u, ''));
|
|
106
|
-
url.hostname = normalizedHost;
|
|
107
|
-
if (isBlockedHostname(normalizedHost)) {
|
|
108
|
-
throw new FusionError('fusion research source URL must be public, not localhost/metadata', {
|
|
109
|
-
code: 'orchestration_failed',
|
|
110
|
-
childCreated: false,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
const ipKind = isIP(normalizedHost);
|
|
114
|
-
if ((ipKind === 4 && isBlockedIpv4(normalizedHost)) || (ipKind === 6 && isBlockedIpv6(normalizedHost))) {
|
|
115
|
-
throw new FusionError('fusion research source URL must be public, not private/reserved', {
|
|
116
|
-
code: 'orchestration_failed',
|
|
117
|
-
childCreated: false,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if ((url.protocol === 'http:' && url.port === '80') || (url.protocol === 'https:' && url.port === '443')) {
|
|
121
|
-
url.port = '';
|
|
122
|
-
}
|
|
123
|
-
return url.toString();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface DeclaredFusionSourceInput {
|
|
127
|
-
url: string;
|
|
128
|
-
purpose: string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function normalizeFusionDeclaredSources(
|
|
132
|
-
sources: readonly DeclaredFusionSourceInput[] = [],
|
|
133
|
-
): readonly FusionDeclaredSourceV1[] {
|
|
134
|
-
const seen = new Map<string, number>();
|
|
135
|
-
return sources.map((source, index) => {
|
|
136
|
-
if (typeof source.url !== 'string' || source.url.trim().length === 0) {
|
|
137
|
-
throw new FusionError(`fusion research source ${String(index)} requires non-blank URL`, {
|
|
138
|
-
code: 'orchestration_failed',
|
|
139
|
-
childCreated: false,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
if (typeof source.purpose !== 'string' || source.purpose.trim().length === 0) {
|
|
143
|
-
throw new FusionError(`fusion research source ${String(index)} requires non-blank purpose`, {
|
|
144
|
-
code: 'orchestration_failed',
|
|
145
|
-
childCreated: false,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
const canonicalUrl = canonicalizeFusionPublicUrl(source.url.trim());
|
|
149
|
-
const previous = seen.get(canonicalUrl);
|
|
150
|
-
if (previous !== undefined) {
|
|
151
|
-
throw new FusionError(
|
|
152
|
-
`fusion research source ${String(index)} duplicates canonical URL from source ${String(previous)}: ${canonicalUrl}`,
|
|
153
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
seen.set(canonicalUrl, index);
|
|
157
|
-
const purpose = source.purpose.trim();
|
|
158
|
-
return {
|
|
159
|
-
url: canonicalUrl,
|
|
160
|
-
canonical_url: canonicalUrl,
|
|
161
|
-
purpose,
|
|
162
|
-
sha256: sha256Text(`${canonicalUrl}\u0000${purpose}`),
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function buildFusionSourcePolicy(
|
|
168
|
-
cwd: string,
|
|
169
|
-
sources: readonly FusionDeclaredSourceV1[],
|
|
170
|
-
): FusionSourcePolicyV1 {
|
|
171
|
-
const body = {
|
|
172
|
-
schema_version: FUSION_SOURCE_POLICY_SCHEMA_VERSION,
|
|
173
|
-
workflow: 'research' as const,
|
|
174
|
-
cwd,
|
|
175
|
-
sources,
|
|
176
|
-
} as const;
|
|
177
|
-
return { ...body, root_sha256: sha256Text(canonicalJson(body)) };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function sourcePolicyCanonicalBytes(policy: FusionSourcePolicyV1): string {
|
|
181
|
-
return canonicalJson(policy);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function requireString(record: Record<PropertyKey, unknown>, key: string, label: string): string {
|
|
185
|
-
const value = record[key];
|
|
186
|
-
if (typeof value !== 'string' || value.length === 0) throw new Error(`${label}.${key} must be non-blank string`);
|
|
187
|
-
return value;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function parseFusionSourcePolicy(value: unknown): FusionSourcePolicyV1 {
|
|
191
|
-
if (!isJsonObject(value) || Array.isArray(value)) throw new Error('fusion source policy must be object');
|
|
192
|
-
const keys = Object.keys(value).sort();
|
|
193
|
-
const expected = ['cwd', 'root_sha256', 'schema_version', 'sources', 'workflow'];
|
|
194
|
-
if (keys.join('\0') !== expected.join('\0')) throw new Error('fusion source policy keys mismatch');
|
|
195
|
-
if (value['schema_version'] !== FUSION_SOURCE_POLICY_SCHEMA_VERSION) throw new Error('fusion source policy schema_version mismatch');
|
|
196
|
-
if (value['workflow'] !== 'research') throw new Error('fusion source policy workflow must be research');
|
|
197
|
-
const cwd = requireString(value, 'cwd', 'fusion source policy');
|
|
198
|
-
const rootSha256 = requireString(value, 'root_sha256', 'fusion source policy');
|
|
199
|
-
if (!SHA256_HEX.test(rootSha256)) throw new Error('fusion source policy.root_sha256 must be sha256');
|
|
200
|
-
if (!Array.isArray(value['sources'])) throw new Error('fusion source policy.sources must be array');
|
|
201
|
-
const sources = value['sources'].map((item, index): FusionDeclaredSourceV1 => {
|
|
202
|
-
const label = `fusion source policy.sources[${String(index)}]`;
|
|
203
|
-
if (!isJsonObject(item) || Array.isArray(item)) throw new Error(`${label} must be object`);
|
|
204
|
-
const itemKeys = Object.keys(item).sort();
|
|
205
|
-
const itemExpected = ['canonical_url', 'purpose', 'sha256', 'url'];
|
|
206
|
-
if (itemKeys.join('\0') !== itemExpected.join('\0')) throw new Error(`${label} keys mismatch`);
|
|
207
|
-
const url = requireString(item, 'url', label);
|
|
208
|
-
const purpose = requireString(item, 'purpose', label);
|
|
209
|
-
const canonical_url = requireString(item, 'canonical_url', label);
|
|
210
|
-
const sha256 = requireString(item, 'sha256', label);
|
|
211
|
-
if (!SHA256_HEX.test(sha256)) throw new Error(`${label}.sha256 must be sha256`);
|
|
212
|
-
if (canonicalizeFusionPublicUrl(url) !== canonical_url) throw new Error(`${label}.canonical_url mismatch`);
|
|
213
|
-
if (url !== canonical_url) throw new Error(`${label}.url must equal canonical_url`);
|
|
214
|
-
if (purpose.trim() !== purpose) throw new Error(`${label}.purpose must be trimmed`);
|
|
215
|
-
if (sha256Text(`${canonical_url}\u0000${purpose}`) !== sha256) throw new Error(`${label}.sha256 mismatch`);
|
|
216
|
-
return { url, canonical_url, purpose, sha256 };
|
|
217
|
-
});
|
|
218
|
-
const seen = new Set<string>();
|
|
219
|
-
for (const [index, source] of sources.entries()) {
|
|
220
|
-
if (seen.has(source.canonical_url)) {
|
|
221
|
-
throw new Error(`fusion source policy.sources[${String(index)}].canonical_url duplicate`);
|
|
222
|
-
}
|
|
223
|
-
seen.add(source.canonical_url);
|
|
224
|
-
}
|
|
225
|
-
const body = { schema_version: FUSION_SOURCE_POLICY_SCHEMA_VERSION, workflow: 'research' as const, cwd, sources } as const;
|
|
226
|
-
if (sha256Text(canonicalJson(body)) !== rootSha256) throw new Error('fusion source policy root_sha256 mismatch');
|
|
227
|
-
return { ...body, root_sha256: rootSha256 };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function readRegularFileNoSymlink(path: string, label: string): Promise<Buffer> {
|
|
231
|
-
let handle: Awaited<ReturnType<typeof open>>;
|
|
232
|
-
try {
|
|
233
|
-
handle = await open(path, constants.O_RDONLY | O_NOFOLLOW);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (isJsonObject(error) && error['code'] === 'ELOOP') {
|
|
236
|
-
throw new Error(`${label} at ${path} is a symlink; refusing to follow it`);
|
|
237
|
-
}
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
const stats = await handle.stat();
|
|
242
|
-
if (!stats.isFile()) throw new Error(`${label} at ${path} is not a regular file`);
|
|
243
|
-
return await handle.readFile();
|
|
244
|
-
} finally {
|
|
245
|
-
await handle.close();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export async function readFusionSourcePolicyFile(path: string, expectedSha256: string): Promise<FusionSourcePolicyV1> {
|
|
250
|
-
if (!SHA256_HEX.test(expectedSha256)) throw new Error('fusion source policy expected hash is malformed');
|
|
251
|
-
const bytes = await readRegularFileNoSymlink(path, 'fusion source policy');
|
|
252
|
-
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
253
|
-
if (actual !== expectedSha256) throw new Error('fusion source policy artifact hash mismatch');
|
|
254
|
-
const text = bytes.toString('utf8');
|
|
255
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) throw new Error('fusion source policy is not UTF-8');
|
|
256
|
-
return parseFusionSourcePolicy(parseJsonText(text));
|
|
257
|
-
}
|