unbrowse 2.12.2 → 2.12.7
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 +8 -44
- package/dist/cli.js +514 -20723
- package/package.json +4 -10
- package/runtime-src/api/routes.ts +15 -801
- package/runtime-src/auth/index.ts +32 -142
- package/runtime-src/capture/index.ts +101 -436
- package/runtime-src/cli.ts +371 -956
- package/runtime-src/client/index.ts +29 -622
- package/runtime-src/execution/index.ts +85 -345
- package/runtime-src/graph/index.ts +10 -128
- package/runtime-src/intent-match.ts +27 -27
- package/runtime-src/kuri/client.ts +82 -543
- package/runtime-src/orchestrator/index.ts +462 -2246
- package/runtime-src/reverse-engineer/index.ts +22 -220
- package/runtime-src/runtime/local-server.ts +16 -149
- package/runtime-src/runtime/paths.ts +5 -9
- package/runtime-src/runtime/setup.ts +1 -52
- package/runtime-src/server.ts +11 -6
- package/runtime-src/transform/schema-hints.ts +358 -0
- package/runtime-src/types/skill.ts +2 -49
- package/runtime-src/verification/index.ts +0 -15
- package/runtime-src/version.ts +13 -13
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/bin/unbrowse-wrapper.mjs +0 -39
- package/bin/unbrowse.js +0 -38
- package/runtime-src/analytics-session.ts +0 -33
- package/runtime-src/api/browse-index.ts +0 -254
- package/runtime-src/api/browse-session.ts +0 -179
- package/runtime-src/api/browse-submit.ts +0 -455
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -635
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/capture/prefetch.ts +0 -122
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/client/graph-client.ts +0 -99
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/indexer/index.ts +0 -480
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -256
- package/runtime-src/orchestrator/first-pass-action.ts +0 -362
- package/runtime-src/orchestrator/passive-publish.ts +0 -152
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -268
- package/runtime-src/payments/wallet.ts +0 -33
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
- package/runtime-src/router.ts +0 -17
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/single-binary.ts +0 -141
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/verification/matrix.ts +0 -30
- package/scripts/postinstall.mjs +0 -81
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
import { buildSkillOperationGraph } from "../graph/index.js";
|
|
2
|
-
import { validateManifest, publishSkill, cachePublishedSkill, publishGraphEdges } from "../client/index.js";
|
|
3
|
-
import { mergeEndpoints } from "../marketplace/index.js";
|
|
4
|
-
import {
|
|
5
|
-
writeSkillSnapshot,
|
|
6
|
-
domainSkillCache,
|
|
7
|
-
persistDomainCache,
|
|
8
|
-
getDomainReuseKey,
|
|
9
|
-
scopedCacheKey,
|
|
10
|
-
snapshotPathForCacheKey,
|
|
11
|
-
generateLocalDescription,
|
|
12
|
-
} from "../orchestrator/index.js";
|
|
13
|
-
import { getRegistrableDomain } from "../domain.js";
|
|
14
|
-
import type { SkillManifest, EndpointDescriptor } from "../types/index.js";
|
|
15
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
|
-
|
|
19
|
-
const UNBROWSE_CONFIG_PATH = join(homedir(), ".unbrowse", "config.json");
|
|
20
|
-
const SKILL_SNAPSHOT_DIR = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR
|
|
21
|
-
?? join(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
|
|
22
|
-
|
|
23
|
-
/** Read agent_id from local config — used for contributor attribution on publish. */
|
|
24
|
-
function getLocalAgentId(): string | undefined {
|
|
25
|
-
try {
|
|
26
|
-
const config = JSON.parse(readFileSync(UNBROWSE_CONFIG_PATH, "utf-8"));
|
|
27
|
-
return config.agent_id ?? undefined;
|
|
28
|
-
} catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Strip PII and user-specific data from endpoints before publishing to marketplace.
|
|
34
|
-
* Keeps: URL templates, method, schema structure, semantic metadata (action/resource kinds,
|
|
35
|
-
* requires/provides, field paths, descriptions).
|
|
36
|
-
* Strips: example response data, actual query values, sample URLs with query params,
|
|
37
|
-
* request bodies, header values.
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* Strip PII and user-specific data from endpoints before publishing to marketplace.
|
|
41
|
-
* Deterministic baseline — the agent sanitizer builds on top of this.
|
|
42
|
-
*/
|
|
43
|
-
// Patterns that identify secret/token values regardless of field name
|
|
44
|
-
const SECRET_VALUE_PATTERNS = [
|
|
45
|
-
/^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, // JWT tokens
|
|
46
|
-
/^Bearer\s+\S+/i, // Bearer tokens
|
|
47
|
-
/^Basic\s+[A-Za-z0-9+/=]+/i, // Basic auth
|
|
48
|
-
/^ghp_[A-Za-z0-9]{36}/, // GitHub PAT
|
|
49
|
-
/^sk-[A-Za-z0-9]{20,}/, // OpenAI/Stripe secret keys
|
|
50
|
-
/^pk_(live|test)_[A-Za-z0-9]+/, // Stripe publishable keys
|
|
51
|
-
/^xox[bsrp]-[A-Za-z0-9-]+/, // Slack tokens
|
|
52
|
-
/^AKIA[A-Z0-9]{16}/, // AWS access key
|
|
53
|
-
/^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 encoded secrets (long)
|
|
54
|
-
/^v2\.[A-Za-z0-9_-]{20,}/, // Various API v2 tokens
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// Field name patterns that indicate the value is a secret
|
|
58
|
-
const SECRET_KEY_PATTERNS = /^(api[_-]?key|access[_-]?token|auth[_-]?token|secret[_-]?key|private[_-]?key|password|passwd|session[_-]?id|session[_-]?token|csrf[_-]?token|client[_-]?secret|bearer|refresh[_-]?token|id[_-]?token|jwt|nonce|otp|pin|ssn|credit[_-]?card)$/i;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Returns true if a value looks like a secret/token/credential.
|
|
62
|
-
*/
|
|
63
|
-
export function looksLikeSecret(key: string, value: unknown): boolean {
|
|
64
|
-
if (typeof value !== "string" || value.length < 8) return false;
|
|
65
|
-
if (SECRET_KEY_PATTERNS.test(key)) return true;
|
|
66
|
-
return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Recursively walk an object and replace secret-looking values with "[REDACTED]".
|
|
71
|
-
* Returns a new object — does not mutate the input.
|
|
72
|
-
*/
|
|
73
|
-
export function redactSecrets(obj: unknown, parentKey = ""): unknown {
|
|
74
|
-
if (obj === null || obj === undefined) return obj;
|
|
75
|
-
if (typeof obj === "string") {
|
|
76
|
-
return looksLikeSecret(parentKey, obj) ? "[REDACTED]" : obj;
|
|
77
|
-
}
|
|
78
|
-
if (Array.isArray(obj)) {
|
|
79
|
-
return obj.map((item, i) => redactSecrets(item, parentKey));
|
|
80
|
-
}
|
|
81
|
-
if (typeof obj === "object") {
|
|
82
|
-
const result: Record<string, unknown> = {};
|
|
83
|
-
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
84
|
-
result[k] = redactSecrets(v, k);
|
|
85
|
-
}
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
return obj;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Strip PII and user-specific data from endpoints before publishing to marketplace.
|
|
93
|
-
* Deterministic baseline — the agent sanitizer builds on top of this.
|
|
94
|
-
*
|
|
95
|
-
* Layer 1: Redact secrets (tokens, keys, JWTs) from ALL string values
|
|
96
|
-
* Layer 2: Strip example responses, query defaults, sample URLs
|
|
97
|
-
*/
|
|
98
|
-
/**
|
|
99
|
-
* Synthesize a plausible placeholder value for a given real value.
|
|
100
|
-
* Preserves type and rough shape so agents can understand the endpoint.
|
|
101
|
-
*/
|
|
102
|
-
function synthesizePlaceholder(key: string, value: unknown): unknown {
|
|
103
|
-
if (value === null || value === undefined) return value;
|
|
104
|
-
if (typeof value === "number") return Number.isInteger(value) ? 12345 : 99.99;
|
|
105
|
-
if (typeof value === "boolean") return value;
|
|
106
|
-
if (typeof value === "string") {
|
|
107
|
-
if (looksLikeSecret(key, value)) return "[REDACTED]";
|
|
108
|
-
// Email-like
|
|
109
|
-
if (/@/.test(value)) return "user@example.com";
|
|
110
|
-
// URL-like
|
|
111
|
-
if (/^https?:\/\//.test(value)) return "https://example.com/item/123";
|
|
112
|
-
// UUID-like
|
|
113
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}/.test(value)) return "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
114
|
-
// Numeric string
|
|
115
|
-
if (/^\d+$/.test(value)) return "12345";
|
|
116
|
-
// Date-like
|
|
117
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "2026-01-15T00:00:00Z";
|
|
118
|
-
// Short identifier
|
|
119
|
-
if (value.length <= 8) return "abc123";
|
|
120
|
-
// General text — return a generic of similar length
|
|
121
|
-
if (value.length > 100) return "Example description text for this item.";
|
|
122
|
-
return "example-value";
|
|
123
|
-
}
|
|
124
|
-
if (Array.isArray(value)) {
|
|
125
|
-
return value.length > 0 ? [synthesizeExample(value[0], 0)] : [];
|
|
126
|
-
}
|
|
127
|
-
if (typeof value === "object") {
|
|
128
|
-
return synthesizeExample(value, 0);
|
|
129
|
-
}
|
|
130
|
-
return value;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Recursively synthesize a structurally similar example with placeholder values.
|
|
135
|
-
* Keeps keys and types, replaces actual data with generic equivalents.
|
|
136
|
-
*/
|
|
137
|
-
export function synthesizeExample(obj: unknown, depth = 0): unknown {
|
|
138
|
-
if (depth > 5) return null; // prevent infinite recursion
|
|
139
|
-
if (obj === null || obj === undefined) return obj;
|
|
140
|
-
if (typeof obj !== "object") return synthesizePlaceholder("", obj);
|
|
141
|
-
if (Array.isArray(obj)) {
|
|
142
|
-
// Keep at most 2 items to show the shape
|
|
143
|
-
return obj.slice(0, 2).map((item) => synthesizeExample(item, depth + 1));
|
|
144
|
-
}
|
|
145
|
-
const result: Record<string, unknown> = {};
|
|
146
|
-
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
147
|
-
result[k] = typeof v === "object" && v !== null
|
|
148
|
-
? synthesizeExample(v, depth + 1)
|
|
149
|
-
: synthesizePlaceholder(k, v);
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Strip PII and user-specific data from endpoints before publishing to marketplace.
|
|
156
|
-
* Replaces real data with structurally similar synthetic examples so agents
|
|
157
|
-
* can still understand the endpoint shape.
|
|
158
|
-
*
|
|
159
|
-
* Layer 1: Redact secrets (tokens, keys, JWTs) from ALL string values
|
|
160
|
-
* Layer 2: Synthesize placeholder examples, sanitize query defaults
|
|
161
|
-
*/
|
|
162
|
-
export function sanitizeForPublish(endpoints: EndpointDescriptor[]): EndpointDescriptor[] {
|
|
163
|
-
return endpoints.map((ep) => {
|
|
164
|
-
const clean = { ...ep };
|
|
165
|
-
|
|
166
|
-
// Layer 1: Redact any secret values in headers, query, body
|
|
167
|
-
if (clean.headers_template) {
|
|
168
|
-
clean.headers_template = redactSecrets(clean.headers_template) as Record<string, string>;
|
|
169
|
-
}
|
|
170
|
-
if (clean.query) {
|
|
171
|
-
clean.query = redactSecrets(clean.query) as Record<string, unknown>;
|
|
172
|
-
}
|
|
173
|
-
if (clean.body) {
|
|
174
|
-
clean.body = redactSecrets(clean.body) as Record<string, unknown>;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Layer 2: Replace query defaults with generic placeholders
|
|
178
|
-
if (clean.query) {
|
|
179
|
-
clean.query = Object.fromEntries(
|
|
180
|
-
Object.entries(clean.query).map(([k, v]) => [k, typeof v === "string" ? "example" : v]),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Replace path_params with generic placeholders
|
|
185
|
-
if (clean.path_params) {
|
|
186
|
-
clean.path_params = Object.fromEntries(
|
|
187
|
-
Object.keys(clean.path_params).map((k) => [k, "example"]),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Synthesize body example (keep structure, replace values)
|
|
192
|
-
if (clean.body) clean.body = synthesizeExample(clean.body) as Record<string, unknown>;
|
|
193
|
-
if (clean.body_params) clean.body_params = synthesizeExample(clean.body_params) as Record<string, unknown>;
|
|
194
|
-
|
|
195
|
-
// Strip header values — keep keys only (headers are not useful as examples)
|
|
196
|
-
if (clean.headers_template) {
|
|
197
|
-
clean.headers_template = Object.fromEntries(
|
|
198
|
-
Object.keys(clean.headers_template).map((k) => [k, ""]),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Strip trigger_url query params (keep origin + path)
|
|
203
|
-
if (clean.trigger_url) {
|
|
204
|
-
try {
|
|
205
|
-
const u = new URL(clean.trigger_url);
|
|
206
|
-
clean.trigger_url = u.origin + u.pathname;
|
|
207
|
-
} catch { /* keep as-is if unparseable */ }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Sanitize semantic descriptor — synthesize examples instead of deleting
|
|
211
|
-
if (clean.semantic) {
|
|
212
|
-
const sem = { ...clean.semantic };
|
|
213
|
-
|
|
214
|
-
// Synthesize structurally similar examples
|
|
215
|
-
if (sem.example_response_compact) {
|
|
216
|
-
sem.example_response_compact = synthesizeExample(sem.example_response_compact);
|
|
217
|
-
}
|
|
218
|
-
if (sem.example_request) {
|
|
219
|
-
sem.example_request = synthesizeExample(sem.example_request);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Replace sample URL query params with placeholders
|
|
223
|
-
if (sem.sample_request_url) {
|
|
224
|
-
try {
|
|
225
|
-
const u = new URL(sem.sample_request_url);
|
|
226
|
-
for (const key of u.searchParams.keys()) {
|
|
227
|
-
u.searchParams.set(key, "example");
|
|
228
|
-
}
|
|
229
|
-
sem.sample_request_url = u.toString();
|
|
230
|
-
} catch { delete sem.sample_request_url; }
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Strip example_value from bindings (these are real captured values)
|
|
234
|
-
if (sem.requires) {
|
|
235
|
-
sem.requires = sem.requires.map((b) => {
|
|
236
|
-
const { example_value: _, ...rest } = b;
|
|
237
|
-
return rest;
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (sem.provides) {
|
|
241
|
-
sem.provides = sem.provides.map((b) => {
|
|
242
|
-
const { example_value: _, ...rest } = b;
|
|
243
|
-
return rest;
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
clean.semantic = sem;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return clean;
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Merge agent-reviewed endpoint metadata into sanitized endpoints.
|
|
256
|
-
* Called by the /v1/skills/:id/review route when an agent submits
|
|
257
|
-
* reviewed descriptions and synthetic examples for a skill's endpoints.
|
|
258
|
-
*/
|
|
259
|
-
export function mergeAgentReview(
|
|
260
|
-
endpoints: EndpointDescriptor[],
|
|
261
|
-
reviews: Array<{
|
|
262
|
-
endpoint_id: string;
|
|
263
|
-
description?: string;
|
|
264
|
-
action_kind?: string;
|
|
265
|
-
resource_kind?: string;
|
|
266
|
-
example_request?: unknown;
|
|
267
|
-
example_response?: unknown;
|
|
268
|
-
}>,
|
|
269
|
-
): EndpointDescriptor[] {
|
|
270
|
-
const reviewMap = new Map(reviews.map((r) => [r.endpoint_id, r]));
|
|
271
|
-
return endpoints.map((ep) => {
|
|
272
|
-
const reviewed = reviewMap.get(ep.endpoint_id);
|
|
273
|
-
if (!reviewed) return ep;
|
|
274
|
-
return {
|
|
275
|
-
...ep,
|
|
276
|
-
description: reviewed.description || ep.description,
|
|
277
|
-
semantic: ep.semantic ? {
|
|
278
|
-
...ep.semantic,
|
|
279
|
-
action_kind: reviewed.action_kind || ep.semantic.action_kind,
|
|
280
|
-
resource_kind: reviewed.resource_kind || ep.semantic.resource_kind,
|
|
281
|
-
description_out: reviewed.description || ep.semantic.description_out,
|
|
282
|
-
...(reviewed.example_response ? { example_response_compact: reviewed.example_response } : {}),
|
|
283
|
-
...(reviewed.example_request ? { example_request: reviewed.example_request } : {}),
|
|
284
|
-
} : ep.semantic,
|
|
285
|
-
};
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Find existing domain snapshots and merge incoming endpoints into them.
|
|
290
|
-
* Returns a merged skill with all endpoints from both existing snapshots
|
|
291
|
-
* and the incoming skill, or null if no existing snapshot found.
|
|
292
|
-
*/
|
|
293
|
-
export function findAndMergeDomainSnapshot(
|
|
294
|
-
snapshotDir: string,
|
|
295
|
-
domain: string,
|
|
296
|
-
incoming: SkillManifest,
|
|
297
|
-
): SkillManifest | null {
|
|
298
|
-
if (!existsSync(snapshotDir)) return null;
|
|
299
|
-
const targetDomain = getRegistrableDomain(domain);
|
|
300
|
-
|
|
301
|
-
let bestExisting: SkillManifest | null = null;
|
|
302
|
-
let bestEndpointCount = 0;
|
|
303
|
-
|
|
304
|
-
for (const entry of readdirSync(snapshotDir)) {
|
|
305
|
-
if (!entry.endsWith(".json")) continue;
|
|
306
|
-
try {
|
|
307
|
-
const candidate = JSON.parse(readFileSync(join(snapshotDir, entry), "utf-8")) as SkillManifest;
|
|
308
|
-
if (getRegistrableDomain(candidate.domain) !== targetDomain) continue;
|
|
309
|
-
if (candidate.execution_type !== "http") continue;
|
|
310
|
-
const epCount = candidate.endpoints?.length ?? 0;
|
|
311
|
-
if (epCount > bestEndpointCount) {
|
|
312
|
-
bestExisting = candidate;
|
|
313
|
-
bestEndpointCount = epCount;
|
|
314
|
-
}
|
|
315
|
-
} catch { /* skip corrupt */ }
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!bestExisting) return null;
|
|
319
|
-
|
|
320
|
-
const merged = mergeEndpoints(bestExisting.endpoints, incoming.endpoints);
|
|
321
|
-
if (merged.length <= bestEndpointCount) return null; // no new endpoints to add
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
...bestExisting,
|
|
325
|
-
endpoints: merged,
|
|
326
|
-
intents: Array.from(new Set([
|
|
327
|
-
...(bestExisting.intents ?? []),
|
|
328
|
-
...(incoming.intents ?? []),
|
|
329
|
-
incoming.intent_signature,
|
|
330
|
-
])),
|
|
331
|
-
updated_at: new Date().toISOString(),
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
const indexInFlight = new Map<string, Promise<void>>();
|
|
335
|
-
|
|
336
|
-
export interface BackgroundIndexJob {
|
|
337
|
-
skill: SkillManifest;
|
|
338
|
-
domain: string;
|
|
339
|
-
intent: string;
|
|
340
|
-
contextUrl?: string;
|
|
341
|
-
clientScope?: string;
|
|
342
|
-
cacheKey: string;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Queue a skill for background processing: graph building, marketplace
|
|
347
|
-
* validation, and publishing. Non-blocking — returns immediately.
|
|
348
|
-
* Per-domain dedup: only one job per domain runs at a time.
|
|
349
|
-
*/
|
|
350
|
-
export function queueBackgroundIndex(job: BackgroundIndexJob): void {
|
|
351
|
-
const key = job.domain;
|
|
352
|
-
if (indexInFlight.has(key)) {
|
|
353
|
-
console.log(`[background-index] skipped for ${key}: already in flight`);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const work = processIndexJob(job)
|
|
358
|
-
.catch(err =>
|
|
359
|
-
console.error(`[background-index] failed for ${key}: ${(err as Error).message}`)
|
|
360
|
-
)
|
|
361
|
-
.finally(() => indexInFlight.delete(key));
|
|
362
|
-
|
|
363
|
-
indexInFlight.set(key, work);
|
|
364
|
-
console.log(`[background-index] queued for ${key}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function processIndexJob(job: BackgroundIndexJob): Promise<void> {
|
|
368
|
-
let { skill, domain, clientScope } = job;
|
|
369
|
-
const scope = clientScope ?? "global";
|
|
370
|
-
const scopedKey = scopedCacheKey(scope, job.cacheKey);
|
|
371
|
-
|
|
372
|
-
// 0. Merge with existing domain snapshot (accumulate endpoints across captures)
|
|
373
|
-
const merged = findAndMergeDomainSnapshot(SKILL_SNAPSHOT_DIR, domain, skill);
|
|
374
|
-
if (merged) {
|
|
375
|
-
console.log(`[background-index] merged ${skill.endpoints.length} new endpoint(s) into existing ${merged.endpoints.length - skill.endpoints.length} for ${domain}`);
|
|
376
|
-
skill = merged;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 1. Build operation graph from ALL accumulated endpoints
|
|
380
|
-
skill.operation_graph = buildSkillOperationGraph(skill.endpoints);
|
|
381
|
-
|
|
382
|
-
// 2. Generate local descriptions for BM25 ranking
|
|
383
|
-
for (const ep of skill.endpoints) {
|
|
384
|
-
if (!ep.description) {
|
|
385
|
-
ep.description = generateLocalDescription(ep);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 3. Update local snapshot with merged skill + graph + descriptions
|
|
390
|
-
|
|
391
|
-
// 4. Sanitize + validate + publish to marketplace (remote, ~1.5s total)
|
|
392
|
-
const publishable = skill.endpoints.filter(ep => ep.method !== "WS");
|
|
393
|
-
if (publishable.length === 0) {
|
|
394
|
-
console.log(`[background-index] no publishable endpoints for ${domain}`);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Deterministic PII sanitization — secrets redacted, values replaced with synthetic placeholders.
|
|
399
|
-
// The calling agent can later POST to /v1/skills/:id/review with better descriptions and examples.
|
|
400
|
-
const sanitized = sanitizeForPublish(publishable);
|
|
401
|
-
|
|
402
|
-
const { operation_graph: _g, ...base } = skill;
|
|
403
|
-
const draft: SkillManifest = { ...base, endpoints: sanitized, indexer_id: getLocalAgentId() };
|
|
404
|
-
const validation = await validateManifest({ ...draft, skill_id: "__validate__" });
|
|
405
|
-
if (!validation.valid) {
|
|
406
|
-
console.warn(
|
|
407
|
-
`[background-index] validation failed for ${domain}: ${validation.hardErrors.join("; ")}`
|
|
408
|
-
);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const publishStart = Date.now();
|
|
413
|
-
const published = await publishSkill(draft);
|
|
414
|
-
const publishMs = Date.now() - publishStart;
|
|
415
|
-
console.log(`[background-index] publish latency: ${publishMs}ms for ${domain}`);
|
|
416
|
-
|
|
417
|
-
const publishedSkill: SkillManifest = {
|
|
418
|
-
...published,
|
|
419
|
-
endpoints: skill.endpoints,
|
|
420
|
-
operation_graph: skill.operation_graph,
|
|
421
|
-
...(skill.auth_profile_ref ? { auth_profile_ref: skill.auth_profile_ref } : {}),
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// 5. Update caches with published version (has backend descriptions)
|
|
425
|
-
cachePublishedSkill(publishedSkill, clientScope);
|
|
426
|
-
writeSkillSnapshot(scopedKey, publishedSkill);
|
|
427
|
-
|
|
428
|
-
// 6. Publish graph edges via dedicated endpoint (fire-and-forget)
|
|
429
|
-
if (skill.operation_graph?.operations) {
|
|
430
|
-
for (const op of skill.operation_graph.operations) {
|
|
431
|
-
const opEdges = (skill.operation_graph.edges ?? [])
|
|
432
|
-
.filter(e => e.from_operation_id === op.operation_id)
|
|
433
|
-
.map(e => ({
|
|
434
|
-
target_endpoint_id: skill.operation_graph!.operations.find(
|
|
435
|
-
t => t.operation_id === e.to_operation_id
|
|
436
|
-
)?.endpoint_id ?? e.to_operation_id,
|
|
437
|
-
kind: e.kind,
|
|
438
|
-
confidence: e.confidence,
|
|
439
|
-
}));
|
|
440
|
-
if (opEdges.length > 0) {
|
|
441
|
-
publishGraphEdges(domain, {
|
|
442
|
-
endpoint_id: op.endpoint_id,
|
|
443
|
-
method: op.method,
|
|
444
|
-
url_template: op.url_template,
|
|
445
|
-
}, opEdges).catch(() => {});
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// 7. Update domain cache so cross-intent reuse works
|
|
451
|
-
const domainKey = getDomainReuseKey(job.contextUrl ?? domain);
|
|
452
|
-
if (domainKey) {
|
|
453
|
-
domainSkillCache.set(domainKey, {
|
|
454
|
-
skillId: publishedSkill.skill_id,
|
|
455
|
-
localSkillPath: snapshotPathForCacheKey(scopedKey),
|
|
456
|
-
ts: Date.now(),
|
|
457
|
-
});
|
|
458
|
-
persistDomainCache();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
console.log(`[background-index] completed for ${domain} -> ${published.skill_id}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** Check if a domain has an indexing job running. */
|
|
465
|
-
export function isIndexingInFlight(domain: string): boolean {
|
|
466
|
-
return indexInFlight.has(domain);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/** Await all in-flight background index jobs. Call before process exit. */
|
|
470
|
-
export async function drainPendingIndexJobs(): Promise<void> {
|
|
471
|
-
const pending = [...indexInFlight.values()];
|
|
472
|
-
if (pending.length === 0) return;
|
|
473
|
-
console.log(`[background-index] draining ${pending.length} pending job(s)...`);
|
|
474
|
-
await Promise.allSettled(pending);
|
|
475
|
-
console.log(`[background-index] all jobs drained`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
export function resetIndexQueueForTests(): void {
|
|
479
|
-
indexInFlight.clear();
|
|
480
|
-
}
|