@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeSync,
|
|
17
|
+
type BigIntStats,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { isAbsolute, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
resolveSafeNetworkTarget,
|
|
24
|
+
type ResolvedNetworkAddress,
|
|
25
|
+
type SafeNetworkTargetOptions,
|
|
26
|
+
} from "./network.js";
|
|
27
|
+
import {
|
|
28
|
+
normalizeSourceUrlIdentity,
|
|
29
|
+
parseMetadataSearchResponse,
|
|
30
|
+
type MetadataSearchResponse,
|
|
31
|
+
} from "./url-intelligence.js";
|
|
32
|
+
import { sanitizeArtifactUrl } from "./persist.js";
|
|
33
|
+
|
|
34
|
+
const REQUEST_SCHEMA_VERSION = 1;
|
|
35
|
+
const MIN_RESULTS = 1;
|
|
36
|
+
const MAX_RESULTS = 20;
|
|
37
|
+
const MIN_TIMEOUT_MS = 500;
|
|
38
|
+
const MAX_TIMEOUT_MS = 15_000;
|
|
39
|
+
const MAX_QUERY_BYTES = 4 * 1024;
|
|
40
|
+
const DEFAULT_RESULTS = 10;
|
|
41
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
42
|
+
const DEFAULT_PROCESS_GRACE_MS = 500;
|
|
43
|
+
const MAX_PROCESS_GRACE_MS = 2_000;
|
|
44
|
+
const DEFAULT_MAX_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
45
|
+
const DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
|
|
46
|
+
const DEFAULT_MAX_BINARY_BYTES = 64 * 1024 * 1024;
|
|
47
|
+
const MAX_CONFIGURED_BINARY_BYTES = 256 * 1024 * 1024;
|
|
48
|
+
const KILL_GRACE_MS = 100;
|
|
49
|
+
const HASH_BUFFER_BYTES = 64 * 1024;
|
|
50
|
+
const MAX_ENGINE_ADDRESSES = 8;
|
|
51
|
+
const METADATA_SEARCH_ENGINE_HOSTS = Object.freeze([
|
|
52
|
+
"html.duckduckgo.com",
|
|
53
|
+
"search.brave.com",
|
|
54
|
+
"www.startpage.com",
|
|
55
|
+
"search.yahoo.com",
|
|
56
|
+
] as const);
|
|
57
|
+
|
|
58
|
+
export type MetadataSearchNetworkResolver = (
|
|
59
|
+
url: URL,
|
|
60
|
+
options: SafeNetworkTargetOptions,
|
|
61
|
+
) => Promise<readonly ResolvedNetworkAddress[]>;
|
|
62
|
+
|
|
63
|
+
export type SearchProviderRequest = {
|
|
64
|
+
readonly query: string;
|
|
65
|
+
readonly maxResults?: number;
|
|
66
|
+
readonly timeoutMs?: number;
|
|
67
|
+
readonly signal?: AbortSignal;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SearchProviderErrorCategory =
|
|
71
|
+
| "invalid-request"
|
|
72
|
+
| "unavailable"
|
|
73
|
+
| "timeout"
|
|
74
|
+
| "aborted"
|
|
75
|
+
| "protocol"
|
|
76
|
+
| "process";
|
|
77
|
+
|
|
78
|
+
export type SearchProviderOutcome =
|
|
79
|
+
| {
|
|
80
|
+
readonly status: "success";
|
|
81
|
+
readonly response: MetadataSearchResponse;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
readonly status: "failure";
|
|
85
|
+
readonly category: SearchProviderErrorCategory;
|
|
86
|
+
readonly message: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Provider-neutral search boundary. Expected runtime failures are returned, not thrown. */
|
|
90
|
+
export type SearchProvider = (request: SearchProviderRequest) => Promise<SearchProviderOutcome>;
|
|
91
|
+
|
|
92
|
+
export type RustMetadataSearchProviderOptions = {
|
|
93
|
+
/** Absolute path to the reviewed binary. The file identity is pinned when the provider is created. */
|
|
94
|
+
readonly binaryPath: string;
|
|
95
|
+
readonly defaultMaxResults?: number;
|
|
96
|
+
readonly defaultTimeoutMs?: number;
|
|
97
|
+
readonly processGraceMs?: number;
|
|
98
|
+
readonly maxStdoutBytes?: number;
|
|
99
|
+
readonly maxStderrBytes?: number;
|
|
100
|
+
readonly maxBinaryBytes?: number;
|
|
101
|
+
/** Source environment to sanitize. Defaults to the current process environment. */
|
|
102
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
103
|
+
/** Test and composition seam. Production uses the pinned public-network resolver. */
|
|
104
|
+
readonly resolveNetworkTarget?: MetadataSearchNetworkResolver;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type BinaryIdentity = {
|
|
108
|
+
readonly realPath: string;
|
|
109
|
+
readonly device: bigint;
|
|
110
|
+
readonly inode: bigint;
|
|
111
|
+
readonly size: bigint;
|
|
112
|
+
readonly mode: bigint;
|
|
113
|
+
readonly owner: bigint;
|
|
114
|
+
readonly modifiedNanoseconds: bigint;
|
|
115
|
+
readonly changedNanoseconds: bigint;
|
|
116
|
+
readonly sha256: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type ValidatedRequest = {
|
|
120
|
+
readonly query: string;
|
|
121
|
+
readonly maxResults: number;
|
|
122
|
+
readonly timeoutMs: number;
|
|
123
|
+
readonly signal?: AbortSignal;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type TerminationReason = "timeout" | "aborted" | "output-limit";
|
|
127
|
+
|
|
128
|
+
type ProcessResult = {
|
|
129
|
+
readonly stdout: Buffer;
|
|
130
|
+
readonly exitCode: number | null;
|
|
131
|
+
readonly signal: NodeJS.Signals | null;
|
|
132
|
+
readonly spawnFailed: boolean;
|
|
133
|
+
readonly termination?: TerminationReason;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type OutputCapture = {
|
|
137
|
+
readonly chunks: Buffer[];
|
|
138
|
+
bytes: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type ResolvedEngineHost = {
|
|
142
|
+
readonly hostname: string;
|
|
143
|
+
readonly addresses: readonly ResolvedNetworkAddress[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function failure(category: SearchProviderErrorCategory, message: string): SearchProviderOutcome {
|
|
147
|
+
return Object.freeze({ status: "failure", category, message });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function signalAborted(signal: AbortSignal | undefined): boolean {
|
|
151
|
+
return signal?.aborted === true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkedInteger(
|
|
155
|
+
value: number | undefined,
|
|
156
|
+
fallback: number,
|
|
157
|
+
minimum: number,
|
|
158
|
+
maximum: number,
|
|
159
|
+
label: string,
|
|
160
|
+
): number {
|
|
161
|
+
const resolved = value ?? fallback;
|
|
162
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
163
|
+
throw new TypeError(`${label} must be an integer from ${minimum} through ${maximum}.`);
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function utf8Length(value: string): number {
|
|
169
|
+
return Buffer.byteLength(value, "utf8");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasUnsafeQueryCodeUnits(value: string): boolean {
|
|
173
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
174
|
+
const code = value.charCodeAt(index);
|
|
175
|
+
if (code === 0) return true;
|
|
176
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
177
|
+
const next = value.charCodeAt(index + 1);
|
|
178
|
+
if (next < 0xdc00 || next > 0xdfff) return true;
|
|
179
|
+
index += 1;
|
|
180
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function validateRequest(
|
|
188
|
+
request: SearchProviderRequest,
|
|
189
|
+
defaults: { readonly maxResults: number; readonly timeoutMs: number },
|
|
190
|
+
): ValidatedRequest | null {
|
|
191
|
+
if (
|
|
192
|
+
typeof request.query !== "string"
|
|
193
|
+
|| request.query.trim() === ""
|
|
194
|
+
|| utf8Length(request.query.trim()) > MAX_QUERY_BYTES
|
|
195
|
+
|| hasUnsafeQueryCodeUnits(request.query)
|
|
196
|
+
) return null;
|
|
197
|
+
const maxResults = request.maxResults ?? defaults.maxResults;
|
|
198
|
+
const timeoutMs = request.timeoutMs ?? defaults.timeoutMs;
|
|
199
|
+
if (
|
|
200
|
+
!Number.isSafeInteger(maxResults)
|
|
201
|
+
|| maxResults < MIN_RESULTS
|
|
202
|
+
|| maxResults > MAX_RESULTS
|
|
203
|
+
|| !Number.isSafeInteger(timeoutMs)
|
|
204
|
+
|| timeoutMs < MIN_TIMEOUT_MS
|
|
205
|
+
|| timeoutMs > MAX_TIMEOUT_MS
|
|
206
|
+
) return null;
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
query: request.query.trim(),
|
|
209
|
+
maxResults,
|
|
210
|
+
timeoutMs,
|
|
211
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sameOpenedIdentity(
|
|
216
|
+
left: BigIntStats,
|
|
217
|
+
right: BigIntStats,
|
|
218
|
+
): boolean {
|
|
219
|
+
return left.dev === right.dev
|
|
220
|
+
&& left.ino === right.ino
|
|
221
|
+
&& left.size === right.size
|
|
222
|
+
&& left.mode === right.mode
|
|
223
|
+
&& left.uid === right.uid
|
|
224
|
+
&& left.mtimeNs === right.mtimeNs
|
|
225
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function inspectBinary(binaryPath: string, maxBytes: number): BinaryIdentity {
|
|
229
|
+
const pathMetadata = lstatSync(binaryPath, { bigint: true });
|
|
230
|
+
if (pathMetadata.isSymbolicLink() || !pathMetadata.isFile() || pathMetadata.nlink !== 1n) {
|
|
231
|
+
throw new Error("The metadata search binary does not have a trusted file identity.");
|
|
232
|
+
}
|
|
233
|
+
const ownerExecutable = (pathMetadata.mode & 0o100n) !== 0n;
|
|
234
|
+
const writableByAnotherUser = (pathMetadata.mode & 0o022n) !== 0n;
|
|
235
|
+
const privilegedExecutable = (pathMetadata.mode & 0o6000n) !== 0n;
|
|
236
|
+
const processOwner = typeof process.getuid === "function" ? BigInt(process.getuid()) : pathMetadata.uid;
|
|
237
|
+
if (
|
|
238
|
+
!ownerExecutable
|
|
239
|
+
|| writableByAnotherUser
|
|
240
|
+
|| privilegedExecutable
|
|
241
|
+
|| pathMetadata.uid !== processOwner
|
|
242
|
+
|| pathMetadata.size <= 0n
|
|
243
|
+
|| pathMetadata.size > BigInt(maxBytes)
|
|
244
|
+
) throw new Error("The metadata search binary does not satisfy the executable policy.");
|
|
245
|
+
|
|
246
|
+
const descriptor = openSync(binaryPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
247
|
+
try {
|
|
248
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
249
|
+
if (!sameOpenedIdentity(pathMetadata, opened) || opened.nlink !== 1n || !opened.isFile()) {
|
|
250
|
+
throw new Error("The metadata search binary identity changed while it was inspected.");
|
|
251
|
+
}
|
|
252
|
+
const hash = createHash("sha256");
|
|
253
|
+
const buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);
|
|
254
|
+
let offset = 0;
|
|
255
|
+
while (offset <= maxBytes) {
|
|
256
|
+
const bytesRead = readSync(descriptor, buffer, 0, Math.min(buffer.byteLength, maxBytes + 1 - offset), offset);
|
|
257
|
+
if (bytesRead === 0) break;
|
|
258
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
259
|
+
offset += bytesRead;
|
|
260
|
+
}
|
|
261
|
+
if (offset > maxBytes) throw new Error("The metadata search binary exceeds its byte limit.");
|
|
262
|
+
const finished = fstatSync(descriptor, { bigint: true });
|
|
263
|
+
if (!sameOpenedIdentity(opened, finished) || BigInt(offset) !== finished.size) {
|
|
264
|
+
throw new Error("The metadata search binary identity changed while it was read.");
|
|
265
|
+
}
|
|
266
|
+
return Object.freeze({
|
|
267
|
+
realPath: realpathSync(binaryPath),
|
|
268
|
+
device: opened.dev,
|
|
269
|
+
inode: opened.ino,
|
|
270
|
+
size: opened.size,
|
|
271
|
+
mode: opened.mode,
|
|
272
|
+
owner: opened.uid,
|
|
273
|
+
modifiedNanoseconds: opened.mtimeNs,
|
|
274
|
+
changedNanoseconds: opened.ctimeNs,
|
|
275
|
+
sha256: hash.digest("hex"),
|
|
276
|
+
});
|
|
277
|
+
} finally {
|
|
278
|
+
closeSync(descriptor);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function identitiesEqual(left: BinaryIdentity, right: BinaryIdentity): boolean {
|
|
283
|
+
return left.realPath === right.realPath
|
|
284
|
+
&& left.device === right.device
|
|
285
|
+
&& left.inode === right.inode
|
|
286
|
+
&& left.size === right.size
|
|
287
|
+
&& left.mode === right.mode
|
|
288
|
+
&& left.owner === right.owner
|
|
289
|
+
&& left.modifiedNanoseconds === right.modifiedNanoseconds
|
|
290
|
+
&& left.changedNanoseconds === right.changedNanoseconds
|
|
291
|
+
&& left.sha256 === right.sha256;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function openedIdentityMatches(opened: BigIntStats, expected: BinaryIdentity): boolean {
|
|
295
|
+
return opened.dev === expected.device
|
|
296
|
+
&& opened.ino === expected.inode
|
|
297
|
+
&& opened.size === expected.size
|
|
298
|
+
&& opened.mode === expected.mode
|
|
299
|
+
&& opened.uid === expected.owner
|
|
300
|
+
&& opened.mtimeNs === expected.modifiedNanoseconds
|
|
301
|
+
&& opened.ctimeNs === expected.changedNanoseconds
|
|
302
|
+
&& opened.nlink === 1n
|
|
303
|
+
&& opened.isFile();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Copy reviewed bytes through an already-open descriptor into the private run
|
|
308
|
+
* directory. The task executes this copy, closing the path-replacement window
|
|
309
|
+
* between the final identity check and `spawn`.
|
|
310
|
+
*/
|
|
311
|
+
function materializePinnedBinary(expected: BinaryIdentity, runDirectory: string): string {
|
|
312
|
+
const source = openSync(expected.realPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
313
|
+
const destinationPath = join(runDirectory, "metadata-search");
|
|
314
|
+
let destination: number | null = null;
|
|
315
|
+
try {
|
|
316
|
+
if (!openedIdentityMatches(fstatSync(source, { bigint: true }), expected)) {
|
|
317
|
+
throw new Error("metadata search binary identity changed");
|
|
318
|
+
}
|
|
319
|
+
destination = openSync(
|
|
320
|
+
destinationPath,
|
|
321
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
322
|
+
0o500,
|
|
323
|
+
);
|
|
324
|
+
const hash = createHash("sha256");
|
|
325
|
+
const buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);
|
|
326
|
+
let offset = 0;
|
|
327
|
+
const expectedBytes = Number(expected.size);
|
|
328
|
+
while (offset < expectedBytes) {
|
|
329
|
+
const bytesRead = readSync(source, buffer, 0, Math.min(buffer.byteLength, expectedBytes - offset), offset);
|
|
330
|
+
if (bytesRead === 0) throw new Error("metadata search binary ended early");
|
|
331
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
332
|
+
let written = 0;
|
|
333
|
+
while (written < bytesRead) {
|
|
334
|
+
written += writeSync(destination, buffer, written, bytesRead - written, offset + written);
|
|
335
|
+
}
|
|
336
|
+
offset += bytesRead;
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
hash.digest("hex") !== expected.sha256
|
|
340
|
+
|| !openedIdentityMatches(fstatSync(source, { bigint: true }), expected)
|
|
341
|
+
) throw new Error("metadata search binary identity changed");
|
|
342
|
+
fsyncSync(destination);
|
|
343
|
+
return destinationPath;
|
|
344
|
+
} finally {
|
|
345
|
+
if (destination !== null) closeSync(destination);
|
|
346
|
+
closeSync(source);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function createPrivateRunDirectory(): string {
|
|
351
|
+
const directory = mkdtempSync(join(tmpdir(), "hraness-kb-metadata-search-"));
|
|
352
|
+
chmodSync(directory, 0o700);
|
|
353
|
+
for (const name of ["home", "config", "cache", "data", "tmp"]) {
|
|
354
|
+
mkdirSync(join(directory, name), { mode: 0o700 });
|
|
355
|
+
}
|
|
356
|
+
return directory;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Construct a minimal subprocess environment without ambient proxy, auth, provider, or startup state. */
|
|
360
|
+
export function isolatedMetadataSearchEnvironment(
|
|
361
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
362
|
+
runDirectory: string,
|
|
363
|
+
): Readonly<Record<string, string>> {
|
|
364
|
+
const environment: Record<string, string> = {};
|
|
365
|
+
for (const key of ["PATH", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT"]) {
|
|
366
|
+
const value = source[key];
|
|
367
|
+
if (value !== undefined) environment[key] = value;
|
|
368
|
+
}
|
|
369
|
+
environment.HOME = join(runDirectory, "home");
|
|
370
|
+
environment.XDG_CONFIG_HOME = join(runDirectory, "config");
|
|
371
|
+
environment.XDG_CACHE_HOME = join(runDirectory, "cache");
|
|
372
|
+
environment.XDG_DATA_HOME = join(runDirectory, "data");
|
|
373
|
+
environment.TMPDIR = join(runDirectory, "tmp");
|
|
374
|
+
environment.TMP = join(runDirectory, "tmp");
|
|
375
|
+
environment.TEMP = join(runDirectory, "tmp");
|
|
376
|
+
return Object.freeze(environment);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function appendBounded(
|
|
380
|
+
capture: OutputCapture,
|
|
381
|
+
chunk: Buffer | string,
|
|
382
|
+
maximumBytes: number,
|
|
383
|
+
onExceeded: () => void,
|
|
384
|
+
retain: boolean,
|
|
385
|
+
): void {
|
|
386
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
387
|
+
if (capture.bytes > maximumBytes) return;
|
|
388
|
+
capture.bytes += bytes.byteLength;
|
|
389
|
+
if (capture.bytes > maximumBytes) {
|
|
390
|
+
capture.chunks.length = 0;
|
|
391
|
+
onExceeded();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (retain) capture.chunks.push(bytes);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function runProcess(options: {
|
|
398
|
+
readonly binaryPath: string;
|
|
399
|
+
readonly cwd: string;
|
|
400
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
401
|
+
readonly stdin: string;
|
|
402
|
+
readonly timeoutMs: number;
|
|
403
|
+
readonly maxStdoutBytes: number;
|
|
404
|
+
readonly maxStderrBytes: number;
|
|
405
|
+
readonly signal?: AbortSignal;
|
|
406
|
+
}): Promise<ProcessResult> {
|
|
407
|
+
let child: ChildProcessWithoutNullStreams | null = null;
|
|
408
|
+
let closed: Promise<ProcessResult> | null = null;
|
|
409
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
410
|
+
let forceKill: ReturnType<typeof setTimeout> | null = null;
|
|
411
|
+
let termination: TerminationReason | undefined;
|
|
412
|
+
let onAbort: (() => void) | null = null;
|
|
413
|
+
try {
|
|
414
|
+
child = spawn(options.binaryPath, [], {
|
|
415
|
+
cwd: options.cwd,
|
|
416
|
+
env: options.environment,
|
|
417
|
+
shell: false,
|
|
418
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
419
|
+
windowsHide: true,
|
|
420
|
+
});
|
|
421
|
+
const runningChild = child;
|
|
422
|
+
const stdout: OutputCapture = { chunks: [], bytes: 0 };
|
|
423
|
+
const stderr: OutputCapture = { chunks: [], bytes: 0 };
|
|
424
|
+
let spawnFailed = false;
|
|
425
|
+
const terminate = (reason: TerminationReason): void => {
|
|
426
|
+
if (termination !== undefined) return;
|
|
427
|
+
termination = reason;
|
|
428
|
+
runningChild.kill("SIGTERM");
|
|
429
|
+
forceKill = setTimeout(() => runningChild.kill("SIGKILL"), KILL_GRACE_MS);
|
|
430
|
+
};
|
|
431
|
+
runningChild.stdout.on("data", (chunk: Buffer) => {
|
|
432
|
+
appendBounded(stdout, chunk, options.maxStdoutBytes, () => terminate("output-limit"), true);
|
|
433
|
+
});
|
|
434
|
+
runningChild.stderr.on("data", (chunk: Buffer) => {
|
|
435
|
+
appendBounded(stderr, chunk, options.maxStderrBytes, () => terminate("output-limit"), false);
|
|
436
|
+
});
|
|
437
|
+
runningChild.stdin.on("error", () => {
|
|
438
|
+
// The close event supplies the categorical process outcome.
|
|
439
|
+
});
|
|
440
|
+
closed = new Promise((resolve) => {
|
|
441
|
+
runningChild.once("error", () => {
|
|
442
|
+
spawnFailed = true;
|
|
443
|
+
});
|
|
444
|
+
runningChild.once("close", (exitCode, processSignal) => {
|
|
445
|
+
resolve(Object.freeze({
|
|
446
|
+
stdout: Buffer.concat(stdout.chunks),
|
|
447
|
+
exitCode,
|
|
448
|
+
signal: processSignal,
|
|
449
|
+
spawnFailed,
|
|
450
|
+
...(termination === undefined ? {} : { termination }),
|
|
451
|
+
}));
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
timeout = setTimeout(() => terminate("timeout"), options.timeoutMs);
|
|
455
|
+
if (options.signal !== undefined) {
|
|
456
|
+
onAbort = () => terminate("aborted");
|
|
457
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
458
|
+
if (options.signal.aborted) terminate("aborted");
|
|
459
|
+
}
|
|
460
|
+
runningChild.stdin.end(options.stdin, "utf8");
|
|
461
|
+
return await closed;
|
|
462
|
+
} finally {
|
|
463
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
464
|
+
if (forceKill !== null) clearTimeout(forceKill);
|
|
465
|
+
if (onAbort !== null && options.signal !== undefined) {
|
|
466
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
467
|
+
}
|
|
468
|
+
if (child !== null && child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
469
|
+
if (closed !== null) await closed;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function deterministicResponse(response: MetadataSearchResponse, maximumResults: number): MetadataSearchResponse {
|
|
474
|
+
const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|
|
475
|
+
const enginesQueried = Object.freeze([...response.enginesQueried].sort());
|
|
476
|
+
const results = response.results
|
|
477
|
+
.map((result) => Object.freeze({ ...result, engines: Object.freeze([...result.engines].sort()) }))
|
|
478
|
+
.sort((left, right) => right.score - left.score || compareText(left.title, right.title) || compareText(left.url, right.url))
|
|
479
|
+
.slice(0, maximumResults);
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
...response,
|
|
482
|
+
results: Object.freeze(results),
|
|
483
|
+
enginesQueried,
|
|
484
|
+
enginesFailed: Object.freeze([...response.enginesFailed].sort()),
|
|
485
|
+
engineStatus: response.engineStatus,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function resolveEngineHosts(
|
|
490
|
+
resolver: MetadataSearchNetworkResolver,
|
|
491
|
+
timeoutMs: number,
|
|
492
|
+
): Promise<readonly ResolvedEngineHost[]> {
|
|
493
|
+
const resolved = await Promise.all(METADATA_SEARCH_ENGINE_HOSTS.map(async (hostname) => {
|
|
494
|
+
const addresses = await resolver(new URL(`https://${hostname}/`), {
|
|
495
|
+
allowPrivateNetwork: false,
|
|
496
|
+
timeoutMs,
|
|
497
|
+
});
|
|
498
|
+
if (addresses.length === 0 || addresses.length > MAX_ENGINE_ADDRESSES) {
|
|
499
|
+
throw new Error("metadata search engine DNS answer count is invalid");
|
|
500
|
+
}
|
|
501
|
+
const unique = new Map<string, ResolvedNetworkAddress>();
|
|
502
|
+
for (const address of addresses) {
|
|
503
|
+
if (
|
|
504
|
+
(address.family !== 4 && address.family !== 6)
|
|
505
|
+
|| typeof address.address !== "string"
|
|
506
|
+
|| address.address.trim() !== address.address
|
|
507
|
+
|| address.address === ""
|
|
508
|
+
) throw new Error("metadata search engine DNS answer is invalid");
|
|
509
|
+
unique.set(`${address.family}:${address.address}`, Object.freeze({ ...address }));
|
|
510
|
+
}
|
|
511
|
+
if (unique.size !== addresses.length) throw new Error("metadata search engine DNS answer is duplicated");
|
|
512
|
+
return Object.freeze({
|
|
513
|
+
hostname,
|
|
514
|
+
addresses: Object.freeze([...unique.values()].sort((left, right) =>
|
|
515
|
+
left.family - right.family || (left.address < right.address ? -1 : left.address > right.address ? 1 : 0))),
|
|
516
|
+
});
|
|
517
|
+
}));
|
|
518
|
+
return Object.freeze(resolved);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Form an exact-match query after dropping the non-request fragment. Safe identity-bearing
|
|
523
|
+
* query parameters are preserved; credential-shaped URL data causes a closed rejection.
|
|
524
|
+
*/
|
|
525
|
+
export function createExactUrlSearchQuery(value: string | URL): string | null {
|
|
526
|
+
let url: URL;
|
|
527
|
+
try {
|
|
528
|
+
url = value instanceof URL ? new URL(value.href) : new URL(value);
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
if ((url.protocol !== "https:" && url.protocol !== "http:") || normalizeSourceUrlIdentity(url) === null) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
url.hash = "";
|
|
536
|
+
const exactUrl = url.href;
|
|
537
|
+
let sanitized: string;
|
|
538
|
+
try {
|
|
539
|
+
sanitized = sanitizeArtifactUrl(exactUrl);
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
if (sanitized !== exactUrl || utf8Length(exactUrl) > MAX_QUERY_BYTES - 2) return null;
|
|
544
|
+
return `"${exactUrl}"`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Create the isolated adapter for the reviewed Rust metadata-search helper. */
|
|
548
|
+
export function createRustMetadataSearchProvider(options: RustMetadataSearchProviderOptions): SearchProvider {
|
|
549
|
+
if (!isAbsolute(options.binaryPath)) {
|
|
550
|
+
throw new TypeError("The metadata search binary path must be absolute.");
|
|
551
|
+
}
|
|
552
|
+
const defaultMaxResults = checkedInteger(
|
|
553
|
+
options.defaultMaxResults,
|
|
554
|
+
DEFAULT_RESULTS,
|
|
555
|
+
MIN_RESULTS,
|
|
556
|
+
MAX_RESULTS,
|
|
557
|
+
"defaultMaxResults",
|
|
558
|
+
);
|
|
559
|
+
const defaultTimeoutMs = checkedInteger(
|
|
560
|
+
options.defaultTimeoutMs,
|
|
561
|
+
DEFAULT_TIMEOUT_MS,
|
|
562
|
+
MIN_TIMEOUT_MS,
|
|
563
|
+
MAX_TIMEOUT_MS,
|
|
564
|
+
"defaultTimeoutMs",
|
|
565
|
+
);
|
|
566
|
+
const processGraceMs = checkedInteger(
|
|
567
|
+
options.processGraceMs,
|
|
568
|
+
DEFAULT_PROCESS_GRACE_MS,
|
|
569
|
+
0,
|
|
570
|
+
MAX_PROCESS_GRACE_MS,
|
|
571
|
+
"processGraceMs",
|
|
572
|
+
);
|
|
573
|
+
const maxStdoutBytes = checkedInteger(
|
|
574
|
+
options.maxStdoutBytes,
|
|
575
|
+
DEFAULT_MAX_STDOUT_BYTES,
|
|
576
|
+
1,
|
|
577
|
+
16 * 1024 * 1024,
|
|
578
|
+
"maxStdoutBytes",
|
|
579
|
+
);
|
|
580
|
+
const maxStderrBytes = checkedInteger(
|
|
581
|
+
options.maxStderrBytes,
|
|
582
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
583
|
+
1,
|
|
584
|
+
2 * 1024 * 1024,
|
|
585
|
+
"maxStderrBytes",
|
|
586
|
+
);
|
|
587
|
+
const maxBinaryBytes = checkedInteger(
|
|
588
|
+
options.maxBinaryBytes,
|
|
589
|
+
DEFAULT_MAX_BINARY_BYTES,
|
|
590
|
+
1,
|
|
591
|
+
MAX_CONFIGURED_BINARY_BYTES,
|
|
592
|
+
"maxBinaryBytes",
|
|
593
|
+
);
|
|
594
|
+
let pinnedIdentity: BinaryIdentity;
|
|
595
|
+
try {
|
|
596
|
+
pinnedIdentity = inspectBinary(options.binaryPath, maxBinaryBytes);
|
|
597
|
+
} catch {
|
|
598
|
+
throw new Error("The metadata search binary is unavailable or untrusted.");
|
|
599
|
+
}
|
|
600
|
+
const sourceEnvironment = options.environment ?? process.env;
|
|
601
|
+
const resolveNetworkTarget = options.resolveNetworkTarget ?? resolveSafeNetworkTarget;
|
|
602
|
+
|
|
603
|
+
return async (request) => {
|
|
604
|
+
const validated = validateRequest(request, { maxResults: defaultMaxResults, timeoutMs: defaultTimeoutMs });
|
|
605
|
+
if (validated === null) return failure("invalid-request", "The metadata search request is invalid.");
|
|
606
|
+
if (signalAborted(validated.signal)) return failure("aborted", "Metadata search was aborted.");
|
|
607
|
+
|
|
608
|
+
let engineHosts: readonly ResolvedEngineHost[];
|
|
609
|
+
try {
|
|
610
|
+
engineHosts = await resolveEngineHosts(resolveNetworkTarget, validated.timeoutMs);
|
|
611
|
+
} catch {
|
|
612
|
+
return failure("unavailable", "Metadata search network targets are unavailable.");
|
|
613
|
+
}
|
|
614
|
+
if (signalAborted(validated.signal)) return failure("aborted", "Metadata search was aborted.");
|
|
615
|
+
|
|
616
|
+
let currentIdentity: BinaryIdentity;
|
|
617
|
+
try {
|
|
618
|
+
currentIdentity = inspectBinary(options.binaryPath, maxBinaryBytes);
|
|
619
|
+
} catch {
|
|
620
|
+
return failure("unavailable", "Metadata search is unavailable.");
|
|
621
|
+
}
|
|
622
|
+
if (!identitiesEqual(pinnedIdentity, currentIdentity)) {
|
|
623
|
+
return failure("unavailable", "Metadata search is unavailable.");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let runDirectory: string;
|
|
627
|
+
try {
|
|
628
|
+
runDirectory = createPrivateRunDirectory();
|
|
629
|
+
} catch {
|
|
630
|
+
return failure("unavailable", "Metadata search is unavailable.");
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const input = JSON.stringify({
|
|
634
|
+
schema_version: REQUEST_SCHEMA_VERSION,
|
|
635
|
+
query: validated.query,
|
|
636
|
+
max_results: validated.maxResults,
|
|
637
|
+
timeout_ms: validated.timeoutMs,
|
|
638
|
+
engine_hosts: engineHosts,
|
|
639
|
+
});
|
|
640
|
+
let result: ProcessResult;
|
|
641
|
+
try {
|
|
642
|
+
const executablePath = materializePinnedBinary(currentIdentity, runDirectory);
|
|
643
|
+
result = await runProcess({
|
|
644
|
+
binaryPath: executablePath,
|
|
645
|
+
cwd: runDirectory,
|
|
646
|
+
environment: isolatedMetadataSearchEnvironment(sourceEnvironment, runDirectory),
|
|
647
|
+
stdin: input,
|
|
648
|
+
timeoutMs: validated.timeoutMs + processGraceMs,
|
|
649
|
+
maxStdoutBytes,
|
|
650
|
+
maxStderrBytes,
|
|
651
|
+
...(validated.signal === undefined ? {} : { signal: validated.signal }),
|
|
652
|
+
});
|
|
653
|
+
} catch {
|
|
654
|
+
return failure("process", "Metadata search process failed.");
|
|
655
|
+
}
|
|
656
|
+
if (result.termination === "aborted") return failure("aborted", "Metadata search was aborted.");
|
|
657
|
+
if (result.termination === "timeout") return failure("timeout", "Metadata search timed out.");
|
|
658
|
+
if (result.termination === "output-limit") {
|
|
659
|
+
return failure("protocol", "Metadata search returned an invalid response.");
|
|
660
|
+
}
|
|
661
|
+
if (result.spawnFailed) return failure("unavailable", "Metadata search is unavailable.");
|
|
662
|
+
if (result.exitCode !== 0 || result.signal !== null) {
|
|
663
|
+
return failure("process", "Metadata search process failed.");
|
|
664
|
+
}
|
|
665
|
+
let decoded: string;
|
|
666
|
+
try {
|
|
667
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(result.stdout);
|
|
668
|
+
} catch {
|
|
669
|
+
return failure("protocol", "Metadata search returned an invalid response.");
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const parsed = parseMetadataSearchResponse(JSON.parse(decoded) as unknown);
|
|
673
|
+
if (parsed.query !== validated.query) {
|
|
674
|
+
return failure("protocol", "Metadata search returned an invalid response.");
|
|
675
|
+
}
|
|
676
|
+
return Object.freeze({
|
|
677
|
+
status: "success",
|
|
678
|
+
response: deterministicResponse(parsed, validated.maxResults),
|
|
679
|
+
});
|
|
680
|
+
} catch {
|
|
681
|
+
return failure("protocol", "Metadata search returned an invalid response.");
|
|
682
|
+
}
|
|
683
|
+
} finally {
|
|
684
|
+
rmSync(runDirectory, { recursive: true, force: true, maxRetries: 3 });
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|