@agenttool/telescope 0.1.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/CLAUDE.md +71 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +230 -0
- package/dist/archive.d.ts +22 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +477 -0
- package/dist/archive.js.map +1 -0
- package/dist/cli-core.d.ts +7 -0
- package/dist/cli-core.d.ts.map +1 -0
- package/dist/cli-core.js +154 -0
- package/dist/cli-core.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +69 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/agent-txt.d.ts +25 -0
- package/dist/parsers/agent-txt.d.ts.map +1 -0
- package/dist/parsers/agent-txt.js +86 -0
- package/dist/parsers/agent-txt.js.map +1 -0
- package/dist/parsers/cards.d.ts +18 -0
- package/dist/parsers/cards.d.ts.map +1 -0
- package/dist/parsers/cards.js +46 -0
- package/dist/parsers/cards.js.map +1 -0
- package/dist/parsers/common.d.ts +19 -0
- package/dist/parsers/common.d.ts.map +1 -0
- package/dist/parsers/common.js +69 -0
- package/dist/parsers/common.js.map +1 -0
- package/dist/parsers/love.d.ts +29 -0
- package/dist/parsers/love.d.ts.map +1 -0
- package/dist/parsers/love.js +247 -0
- package/dist/parsers/love.js.map +1 -0
- package/dist/parsers/pathways.d.ts +13 -0
- package/dist/parsers/pathways.d.ts.map +1 -0
- package/dist/parsers/pathways.js +47 -0
- package/dist/parsers/pathways.js.map +1 -0
- package/dist/plans.d.ts +14 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +131 -0
- package/dist/plans.js.map +1 -0
- package/dist/scan.d.ts +3 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1093 -0
- package/dist/scan.js.map +1 -0
- package/dist/target.d.ts +6 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +180 -0
- package/dist/target.js.map +1 -0
- package/dist/transport.d.ts +23 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +291 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +38 -0
- package/dist/verify.js.map +1 -0
- package/package.json +65 -0
- package/schema/agenttool-telescope-report-v0.1.schema.json +404 -0
package/dist/scan.js
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
import { DEFAULT_LIMITS, REPORT_SCHEMA, TOOL_NAME, TOOL_VERSION, } from "./constants.js";
|
|
2
|
+
import { TargetInputError } from "./errors.js";
|
|
3
|
+
import { buildLoveActions, buildNpmAction } from "./plans.js";
|
|
4
|
+
import { parseAgentTxt } from "./parsers/agent-txt.js";
|
|
5
|
+
import { parseA2aCard, parseMcpCard } from "./parsers/cards.js";
|
|
6
|
+
import { parseLoveDiscovery, parseLoveManifest, selectLoveManifest, } from "./parsers/love.js";
|
|
7
|
+
import { parsePathways } from "./parsers/pathways.js";
|
|
8
|
+
import { normalizeTarget, assertPublicHttpsUrl, defaultResolveHostname, } from "./target.js";
|
|
9
|
+
import { fetchDocument, ScanBudget, } from "./transport.js";
|
|
10
|
+
const JSON_ACCEPT = "application/json, application/*+json;q=0.9";
|
|
11
|
+
const AGENT_TXT_ACCEPT = "text/agent, text/plain;q=0.9";
|
|
12
|
+
const DIAGNOSTIC_MESSAGES = {
|
|
13
|
+
unexpected_media_type: "A response used a media type Telescope will not parse for this surface.",
|
|
14
|
+
invalid_utf8: "A discovery document was not valid UTF-8.",
|
|
15
|
+
invalid_json: "A discovery document was not valid JSON.",
|
|
16
|
+
json_complexity_limit: "A JSON document exceeded the configured structural limit.",
|
|
17
|
+
agent_txt_duplicate_key: "agent.txt repeated a key; entries were preserved and selected duplicates were not guessed.",
|
|
18
|
+
agent_txt_malformed_line: "agent.txt contained a malformed non-comment line.",
|
|
19
|
+
love_index_latest_ignored: "The LOVE index latest field was ignored for release selection.",
|
|
20
|
+
mcp_locator_invalid: "The advertised MCP card locator is not an absolute HTTPS URL within the URL policy.",
|
|
21
|
+
love_index_locator_invalid: "The LOVE index locator is not an absolute HTTPS URL within the URL policy.",
|
|
22
|
+
love_manifest_locator_invalid: "The selected LOVE manifest locator is not an absolute HTTPS URL within the URL policy.",
|
|
23
|
+
love_index_unavailable: "The advertised LOVE index could not be read, so the selected release chain is incomplete.",
|
|
24
|
+
love_manifest_unavailable: "The selected LOVE manifest could not be read, so the selected release chain is incomplete.",
|
|
25
|
+
love_no_safe_mirror: "No manifest mirror passed the public-HTTPS DNS preflight, so no download plan was produced.",
|
|
26
|
+
npm_authority_not_false: "The optional npm path was not explicitly declared non-authoritative, so no npm action was produced.",
|
|
27
|
+
agent_txt_pathways_conflict: "agent.txt advertises a Pathways locator different from the fixed origin probe.",
|
|
28
|
+
agent_txt_love_conflict: "agent.txt advertises a LOVE locator different from the fixed well-known probe.",
|
|
29
|
+
advertised_mcp_not_found: "agent.txt advertised an MCP card, but the observed locator returned not found.",
|
|
30
|
+
adapter_failed: "An explicitly supplied discovery adapter failed in isolation.",
|
|
31
|
+
unsafe_remote_locator_omitted: "A remote locator was omitted because it contained credentials, query data, or left the bounded HTTPS locator shape.",
|
|
32
|
+
};
|
|
33
|
+
function diagnostic(code, evidenceId, level = "warning") {
|
|
34
|
+
return {
|
|
35
|
+
code,
|
|
36
|
+
level,
|
|
37
|
+
message: DIAGNOSTIC_MESSAGES[code] ??
|
|
38
|
+
"The observation was rejected or qualified by a local Telescope boundary.",
|
|
39
|
+
evidence_id: evidenceId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function claim(input) {
|
|
43
|
+
return { ...input, taint: input.taint ?? "remote_untrusted" };
|
|
44
|
+
}
|
|
45
|
+
function sourceState(state) {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
function isJsonMediaType(value) {
|
|
49
|
+
return value === "application/json" || Boolean(value?.endsWith("+json"));
|
|
50
|
+
}
|
|
51
|
+
function isAgentTxtMediaType(value) {
|
|
52
|
+
return value === "text/plain" || value === "text/agent";
|
|
53
|
+
}
|
|
54
|
+
function canonicalHttpsLocator(value) {
|
|
55
|
+
if (value.length > 2_048)
|
|
56
|
+
return null;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(value);
|
|
59
|
+
if (url.protocol !== "https:" ||
|
|
60
|
+
url.username ||
|
|
61
|
+
url.password ||
|
|
62
|
+
url.hash ||
|
|
63
|
+
(url.port && url.port !== "443")) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
url.port = "";
|
|
67
|
+
return url.href.length <= 2_048 ? url.href : null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function reportLocator(value, preserveExactDidTemplate = false) {
|
|
74
|
+
if (value.length > 2_048 || /[\u0000-\u001f\u007f]/.test(value))
|
|
75
|
+
return null;
|
|
76
|
+
const marker = "{exact-DID}";
|
|
77
|
+
const parseValue = preserveExactDidTemplate
|
|
78
|
+
? value.replace(marker, "did:example:placeholder")
|
|
79
|
+
: value;
|
|
80
|
+
if (preserveExactDidTemplate && !value.includes(marker))
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(parseValue);
|
|
84
|
+
if (url.protocol !== "https:" ||
|
|
85
|
+
url.username ||
|
|
86
|
+
url.password ||
|
|
87
|
+
url.hash ||
|
|
88
|
+
(url.port && url.port !== "443")) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (preserveExactDidTemplate) {
|
|
92
|
+
const parameters = [...url.searchParams.entries()];
|
|
93
|
+
if (parameters.length !== 1 ||
|
|
94
|
+
parameters[0]?.[0] !== "resource" ||
|
|
95
|
+
parameters[0]?.[1] !== "did:example:placeholder") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
url.port = "";
|
|
101
|
+
if (url.search)
|
|
102
|
+
return null;
|
|
103
|
+
return url.href.length <= 2_048 ? url.href : null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function mergeLimits(partial) {
|
|
110
|
+
const limits = { ...DEFAULT_LIMITS, ...partial };
|
|
111
|
+
const integerFields = Object.entries(limits);
|
|
112
|
+
if (integerFields.some(([, value]) => !Number.isSafeInteger(value))) {
|
|
113
|
+
throw new TargetInputError("invalid_limits", "Every Telescope limit must be an integer.");
|
|
114
|
+
}
|
|
115
|
+
if (limits.timeout_ms < 100 ||
|
|
116
|
+
limits.timeout_ms > 120_000 ||
|
|
117
|
+
limits.max_response_bytes < 1_024 ||
|
|
118
|
+
limits.max_response_bytes > 4 * 1024 * 1024 ||
|
|
119
|
+
limits.max_total_bytes < limits.max_response_bytes ||
|
|
120
|
+
limits.max_total_bytes > 16 * 1024 * 1024 ||
|
|
121
|
+
limits.max_redirects < 0 ||
|
|
122
|
+
limits.max_redirects > 8 ||
|
|
123
|
+
limits.max_requests < 4 ||
|
|
124
|
+
limits.max_requests > 64 ||
|
|
125
|
+
limits.max_agent_txt_lines < 16 ||
|
|
126
|
+
limits.max_agent_txt_lines > 4_096 ||
|
|
127
|
+
limits.max_agent_txt_line_bytes < 256 ||
|
|
128
|
+
limits.max_agent_txt_line_bytes > 16_384 ||
|
|
129
|
+
limits.max_json_depth < 4 ||
|
|
130
|
+
limits.max_json_depth > 64 ||
|
|
131
|
+
limits.max_json_nodes < 100 ||
|
|
132
|
+
limits.max_json_nodes > 100_000) {
|
|
133
|
+
throw new TargetInputError("invalid_limits", "One or more Telescope limits are outside supported bounds.");
|
|
134
|
+
}
|
|
135
|
+
return limits;
|
|
136
|
+
}
|
|
137
|
+
function createDeadline(timeoutMs, parent) {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const abortFromParent = () => controller.abort();
|
|
140
|
+
if (parent?.aborted)
|
|
141
|
+
controller.abort();
|
|
142
|
+
else
|
|
143
|
+
parent?.addEventListener("abort", abortFromParent, { once: true });
|
|
144
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
145
|
+
return {
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
cleanup: () => {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
parent?.removeEventListener("abort", abortFromParent);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function probeUrl(origin, path) {
|
|
154
|
+
return new URL(path, `${origin}/`).href;
|
|
155
|
+
}
|
|
156
|
+
function parserWarnings(warnings, evidenceId, diagnostics) {
|
|
157
|
+
for (const code of [...new Set(warnings)]) {
|
|
158
|
+
if (code === "love_index_latest_ignored")
|
|
159
|
+
continue;
|
|
160
|
+
diagnostics.push(diagnostic(code, evidenceId));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function parseUnavailableSurface(id, source, boundaryCodes) {
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
state: sourceState(source.state),
|
|
167
|
+
schema_conformance: source.state === "present" ? "invalid" : "not_assessed",
|
|
168
|
+
evidence_ids: [source.id],
|
|
169
|
+
claims: [],
|
|
170
|
+
boundary_codes: boundaryCodes,
|
|
171
|
+
diagnostic_codes: source.error_code ? [source.error_code] : [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function addTransportDiagnostic(source, diagnostics) {
|
|
175
|
+
if (source.error_code) {
|
|
176
|
+
diagnostics.push(diagnostic(source.error_code, source.id, "error"));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function validateAdapters(adapters) {
|
|
180
|
+
if (adapters.length > 30) {
|
|
181
|
+
throw new TargetInputError("adapter_limit", "At most 30 caller-owned adapters may be supplied to one scan.");
|
|
182
|
+
}
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const adapter of adapters) {
|
|
185
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(adapter.id) || seen.has(adapter.id)) {
|
|
186
|
+
throw new TargetInputError("invalid_adapter", "Adapter IDs must be unique lowercase identifiers.");
|
|
187
|
+
}
|
|
188
|
+
seen.add(adapter.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function runAdapters(adapters, context, diagnostics) {
|
|
192
|
+
const normalizeResult = (id, value) => {
|
|
193
|
+
const states = new Set([
|
|
194
|
+
"not_configured",
|
|
195
|
+
"present",
|
|
196
|
+
"absent",
|
|
197
|
+
"invalid",
|
|
198
|
+
"error",
|
|
199
|
+
]);
|
|
200
|
+
if (!states.has(value.state) ||
|
|
201
|
+
typeof value.summary !== "string" ||
|
|
202
|
+
value.summary.length > 4_096 ||
|
|
203
|
+
typeof value.facts !== "object" ||
|
|
204
|
+
value.facts === null ||
|
|
205
|
+
Array.isArray(value.facts)) {
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
state: "invalid",
|
|
209
|
+
summary: "The caller-owned adapter returned an invalid bounded result.",
|
|
210
|
+
facts: {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const entries = Object.entries(value.facts);
|
|
214
|
+
if (entries.length > 128 ||
|
|
215
|
+
entries.some(([key, fact]) => key.length === 0 ||
|
|
216
|
+
key.length > 128 ||
|
|
217
|
+
!(fact === null ||
|
|
218
|
+
typeof fact === "boolean" ||
|
|
219
|
+
typeof fact === "number" ||
|
|
220
|
+
(typeof fact === "string" && fact.length <= 4_096)) ||
|
|
221
|
+
(typeof fact === "number" && !Number.isFinite(fact)))) {
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
state: "invalid",
|
|
225
|
+
summary: "The caller-owned adapter returned invalid or oversized facts.",
|
|
226
|
+
facts: {},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
id,
|
|
231
|
+
state: value.state,
|
|
232
|
+
summary: value.summary,
|
|
233
|
+
facts: Object.fromEntries(entries),
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const seen = new Set(adapters.map((adapter) => adapter.id));
|
|
237
|
+
const results = await Promise.all(adapters.map(async (adapter) => {
|
|
238
|
+
try {
|
|
239
|
+
if (context.signal.aborted)
|
|
240
|
+
throw new DOMException("Aborted", "AbortError");
|
|
241
|
+
const operation = adapter.discover(context);
|
|
242
|
+
const result = await new Promise((resolve, reject) => {
|
|
243
|
+
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
244
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
245
|
+
operation.then((value) => {
|
|
246
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
247
|
+
resolve(value);
|
|
248
|
+
}, (error) => {
|
|
249
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
250
|
+
reject(error);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
return normalizeResult(adapter.id, result);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
diagnostics.push(diagnostic("adapter_failed", null));
|
|
257
|
+
return {
|
|
258
|
+
id: adapter.id,
|
|
259
|
+
state: "error",
|
|
260
|
+
summary: "The caller-owned adapter failed; core HTTPS observations remain available.",
|
|
261
|
+
facts: {},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}));
|
|
265
|
+
for (const id of ["dns_aid", "pkarr"]) {
|
|
266
|
+
if (!seen.has(id)) {
|
|
267
|
+
results.push({
|
|
268
|
+
id,
|
|
269
|
+
state: "not_configured",
|
|
270
|
+
summary: id === "dns_aid"
|
|
271
|
+
? "DNS-AID is an opt-in adapter seam; core does not claim DNSSEC validation."
|
|
272
|
+
: "PKARR is an opt-in adapter seam; core does not contact default public relays.",
|
|
273
|
+
facts: {},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return results.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
278
|
+
}
|
|
279
|
+
export async function inspectTarget(input, options = {}) {
|
|
280
|
+
const subject = normalizeTarget(input);
|
|
281
|
+
const limits = mergeLimits(options.limits);
|
|
282
|
+
const adapters = options.adapters ?? [];
|
|
283
|
+
validateAdapters(adapters);
|
|
284
|
+
const observedAt = (options.clock ?? (() => new Date()))().toISOString();
|
|
285
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
286
|
+
const resolveHostname = options.resolve_hostname ?? defaultResolveHostname;
|
|
287
|
+
const budget = new ScanBudget(limits);
|
|
288
|
+
const deadline = createDeadline(limits.timeout_ms, options.signal);
|
|
289
|
+
const sources = [];
|
|
290
|
+
const surfaces = [];
|
|
291
|
+
const diagnostics = [];
|
|
292
|
+
const actions = [];
|
|
293
|
+
const get = (id, url, accept) => fetchDocument({
|
|
294
|
+
id,
|
|
295
|
+
url,
|
|
296
|
+
accept,
|
|
297
|
+
fetch: fetchImpl,
|
|
298
|
+
resolve_hostname: resolveHostname,
|
|
299
|
+
budget,
|
|
300
|
+
limits,
|
|
301
|
+
signal: deadline.signal,
|
|
302
|
+
});
|
|
303
|
+
try {
|
|
304
|
+
const [agentDocument, pathwaysDocument, loveDocument, a2aDocument] = await Promise.all([
|
|
305
|
+
get("agent_txt", probeUrl(subject.origin, "/.well-known/agent.txt"), AGENT_TXT_ACCEPT),
|
|
306
|
+
get("pathways", probeUrl(subject.origin, "/v1/pathways"), JSON_ACCEPT),
|
|
307
|
+
get("love_discovery", probeUrl(subject.origin, "/.well-known/love-packages"), JSON_ACCEPT),
|
|
308
|
+
get("a2a_card", probeUrl(subject.origin, "/.well-known/agent-card.json"), JSON_ACCEPT),
|
|
309
|
+
]);
|
|
310
|
+
sources.push(agentDocument.observation, pathwaysDocument.observation, loveDocument.observation, a2aDocument.observation);
|
|
311
|
+
for (const source of sources)
|
|
312
|
+
addTransportDiagnostic(source, diagnostics);
|
|
313
|
+
let parsedAgent = null;
|
|
314
|
+
if (agentDocument.observation.state === "present" && agentDocument.body) {
|
|
315
|
+
if (!isAgentTxtMediaType(agentDocument.observation.media_type)) {
|
|
316
|
+
diagnostics.push(diagnostic("unexpected_media_type", "agent_txt", "error"));
|
|
317
|
+
surfaces.push({
|
|
318
|
+
...parseUnavailableSurface("agent_txt", agentDocument.observation, [
|
|
319
|
+
"publisher_assertions_not_verified",
|
|
320
|
+
"discovery_grants_no_authority_or_consent",
|
|
321
|
+
]),
|
|
322
|
+
state: "invalid",
|
|
323
|
+
schema_conformance: "invalid",
|
|
324
|
+
diagnostic_codes: ["unexpected_media_type"],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const parsed = parseAgentTxt(agentDocument.body, limits);
|
|
329
|
+
if (!parsed.ok) {
|
|
330
|
+
diagnostics.push(diagnostic(parsed.code, "agent_txt", "error"));
|
|
331
|
+
surfaces.push({
|
|
332
|
+
...parseUnavailableSurface("agent_txt", agentDocument.observation, [
|
|
333
|
+
"publisher_assertions_not_verified",
|
|
334
|
+
"discovery_grants_no_authority_or_consent",
|
|
335
|
+
]),
|
|
336
|
+
state: "invalid",
|
|
337
|
+
schema_conformance: "invalid",
|
|
338
|
+
diagnostic_codes: [parsed.code],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
parsedAgent = parsed.value;
|
|
343
|
+
parserWarnings(parsed.warnings, "agent_txt", diagnostics);
|
|
344
|
+
const claims = [];
|
|
345
|
+
const selected = parsed.value.selected;
|
|
346
|
+
const addSelected = (key, role, locator = false, preserveExactDidTemplate = false) => {
|
|
347
|
+
const value = selected[key];
|
|
348
|
+
if (value !== null) {
|
|
349
|
+
const claimValue = locator
|
|
350
|
+
? reportLocator(value, preserveExactDidTemplate)
|
|
351
|
+
: value;
|
|
352
|
+
if (claimValue === null) {
|
|
353
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "agent_txt"));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
claims.push(claim({
|
|
357
|
+
key,
|
|
358
|
+
value: claimValue,
|
|
359
|
+
basis: "publisher_assertion",
|
|
360
|
+
role,
|
|
361
|
+
evidence_ids: ["agent_txt"],
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
addSelected("substrate", "capability_advertisement");
|
|
366
|
+
addSelected("convention", "capability_advertisement");
|
|
367
|
+
addSelected("pathways_url", "locator", true);
|
|
368
|
+
addSelected("mcp_card_url", "locator", true);
|
|
369
|
+
addSelected("webfinger_template", "locator", true, true);
|
|
370
|
+
addSelected("love_packages_url", "locator", true);
|
|
371
|
+
surfaces.push({
|
|
372
|
+
id: "agent_txt",
|
|
373
|
+
state: "present",
|
|
374
|
+
schema_conformance: "supported_shape_valid",
|
|
375
|
+
evidence_ids: ["agent_txt"],
|
|
376
|
+
claims,
|
|
377
|
+
boundary_codes: [
|
|
378
|
+
"publisher_assertions_not_verified",
|
|
379
|
+
"discovery_grants_no_authority_or_consent",
|
|
380
|
+
],
|
|
381
|
+
diagnostic_codes: parsed.warnings,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
surfaces.push(parseUnavailableSurface("agent_txt", agentDocument.observation, [
|
|
388
|
+
"absence_is_scoped_to_exact_path_and_observation_time",
|
|
389
|
+
]));
|
|
390
|
+
}
|
|
391
|
+
const fixedPathways = probeUrl(subject.origin, "/v1/pathways");
|
|
392
|
+
const advertisedPathways = parsedAgent?.selected.pathways_url;
|
|
393
|
+
if (advertisedPathways &&
|
|
394
|
+
canonicalHttpsLocator(advertisedPathways) !==
|
|
395
|
+
canonicalHttpsLocator(fixedPathways)) {
|
|
396
|
+
diagnostics.push(diagnostic("agent_txt_pathways_conflict", "agent_txt"));
|
|
397
|
+
}
|
|
398
|
+
const fixedLove = probeUrl(subject.origin, "/.well-known/love-packages");
|
|
399
|
+
const advertisedLove = parsedAgent?.selected.love_packages_url;
|
|
400
|
+
if (advertisedLove &&
|
|
401
|
+
canonicalHttpsLocator(advertisedLove) !== canonicalHttpsLocator(fixedLove)) {
|
|
402
|
+
diagnostics.push(diagnostic("agent_txt_love_conflict", "agent_txt"));
|
|
403
|
+
}
|
|
404
|
+
let parsedPathways = null;
|
|
405
|
+
if (pathwaysDocument.observation.state === "present" &&
|
|
406
|
+
pathwaysDocument.body) {
|
|
407
|
+
if (!isJsonMediaType(pathwaysDocument.observation.media_type)) {
|
|
408
|
+
diagnostics.push(diagnostic("unexpected_media_type", "pathways", "error"));
|
|
409
|
+
surfaces.push({
|
|
410
|
+
...parseUnavailableSurface("pathways", pathwaysDocument.observation, [
|
|
411
|
+
"tutorial_selection_is_publisher_scoped",
|
|
412
|
+
]),
|
|
413
|
+
state: "invalid",
|
|
414
|
+
schema_conformance: "invalid",
|
|
415
|
+
diagnostic_codes: ["unexpected_media_type"],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const parsed = parsePathways(pathwaysDocument.body, limits);
|
|
420
|
+
if (!parsed.ok) {
|
|
421
|
+
diagnostics.push(diagnostic(parsed.code, "pathways", "error"));
|
|
422
|
+
surfaces.push({
|
|
423
|
+
...parseUnavailableSurface("pathways", pathwaysDocument.observation, ["tutorial_selection_is_publisher_scoped"]),
|
|
424
|
+
state: "invalid",
|
|
425
|
+
schema_conformance: "invalid",
|
|
426
|
+
diagnostic_codes: [parsed.code],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
parsedPathways = parsed.value;
|
|
431
|
+
parserWarnings(parsed.warnings, "pathways", diagnostics);
|
|
432
|
+
surfaces.push({
|
|
433
|
+
id: "pathways",
|
|
434
|
+
state: "present",
|
|
435
|
+
schema_conformance: "supported_shape_valid",
|
|
436
|
+
evidence_ids: ["pathways"],
|
|
437
|
+
claims: [
|
|
438
|
+
claim({
|
|
439
|
+
key: "sdk_version",
|
|
440
|
+
value: parsed.value.sdk_version,
|
|
441
|
+
basis: "publisher_assertion",
|
|
442
|
+
role: "release_selection",
|
|
443
|
+
evidence_ids: ["pathways"],
|
|
444
|
+
}),
|
|
445
|
+
],
|
|
446
|
+
boundary_codes: [
|
|
447
|
+
"tutorial_selection_is_publisher_scoped",
|
|
448
|
+
"latest_and_dist_tags_not_used_for_selection",
|
|
449
|
+
],
|
|
450
|
+
diagnostic_codes: parsed.warnings,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
surfaces.push(parseUnavailableSurface("pathways", pathwaysDocument.observation, [
|
|
457
|
+
"absence_is_scoped_to_exact_path_and_observation_time",
|
|
458
|
+
]));
|
|
459
|
+
}
|
|
460
|
+
if (parsedPathways?.npm) {
|
|
461
|
+
const npm = parsedPathways.npm;
|
|
462
|
+
surfaces.push({
|
|
463
|
+
id: "npm",
|
|
464
|
+
state: "present",
|
|
465
|
+
schema_conformance: "supported_shape_valid",
|
|
466
|
+
evidence_ids: ["pathways"],
|
|
467
|
+
claims: [
|
|
468
|
+
claim({
|
|
469
|
+
key: "package",
|
|
470
|
+
value: npm.package,
|
|
471
|
+
basis: "publisher_assertion",
|
|
472
|
+
role: "locator",
|
|
473
|
+
evidence_ids: ["pathways"],
|
|
474
|
+
}),
|
|
475
|
+
claim({
|
|
476
|
+
key: "version",
|
|
477
|
+
value: parsedPathways.sdk_version,
|
|
478
|
+
basis: "publisher_assertion",
|
|
479
|
+
role: "release_selection",
|
|
480
|
+
evidence_ids: ["pathways"],
|
|
481
|
+
}),
|
|
482
|
+
claim({
|
|
483
|
+
key: "authority",
|
|
484
|
+
value: npm.authority,
|
|
485
|
+
basis: "publisher_assertion",
|
|
486
|
+
role: "authority_boundary",
|
|
487
|
+
evidence_ids: ["pathways"],
|
|
488
|
+
}),
|
|
489
|
+
],
|
|
490
|
+
boundary_codes: [
|
|
491
|
+
"npm_is_convenience_not_release_authority",
|
|
492
|
+
"npm_install_does_not_independently_verify_love_size_sha256",
|
|
493
|
+
"no_registry_query_performed",
|
|
494
|
+
],
|
|
495
|
+
diagnostic_codes: [],
|
|
496
|
+
});
|
|
497
|
+
if (npm.authority === false) {
|
|
498
|
+
actions.push(buildNpmAction({
|
|
499
|
+
package_name: npm.package,
|
|
500
|
+
version: parsedPathways.sdk_version,
|
|
501
|
+
evidence_ids: ["pathways"],
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
diagnostics.push(diagnostic("npm_authority_not_false", "pathways"));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
surfaces.push({
|
|
510
|
+
id: "npm",
|
|
511
|
+
state: "not_attempted",
|
|
512
|
+
schema_conformance: "not_assessed",
|
|
513
|
+
evidence_ids: pathwaysDocument.observation.state === "present" ? ["pathways"] : [],
|
|
514
|
+
claims: [],
|
|
515
|
+
boundary_codes: ["npm_not_advertised_by_valid_pathways"],
|
|
516
|
+
diagnostic_codes: [],
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
let parsedLove = null;
|
|
520
|
+
const loveEvidence = ["love_discovery"];
|
|
521
|
+
const loveClaims = [];
|
|
522
|
+
let loveDiagnosticCodes = [];
|
|
523
|
+
if (loveDocument.observation.state === "present" && loveDocument.body) {
|
|
524
|
+
if (!isJsonMediaType(loveDocument.observation.media_type)) {
|
|
525
|
+
diagnostics.push(diagnostic("unexpected_media_type", "love_discovery", "error"));
|
|
526
|
+
loveDiagnosticCodes = ["unexpected_media_type"];
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const parsed = parseLoveDiscovery(loveDocument.body, limits);
|
|
530
|
+
if (!parsed.ok) {
|
|
531
|
+
diagnostics.push(diagnostic(parsed.code, "love_discovery", "error"));
|
|
532
|
+
loveDiagnosticCodes = [parsed.code];
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
parsedLove = parsed.value;
|
|
536
|
+
parserWarnings(parsed.warnings, "love_discovery", diagnostics);
|
|
537
|
+
loveDiagnosticCodes = parsed.warnings;
|
|
538
|
+
const reportIndexUrl = reportLocator(parsed.value.index_url);
|
|
539
|
+
if (reportIndexUrl) {
|
|
540
|
+
loveClaims.push(claim({
|
|
541
|
+
key: "index_url",
|
|
542
|
+
value: reportIndexUrl,
|
|
543
|
+
basis: "publisher_assertion",
|
|
544
|
+
role: "locator",
|
|
545
|
+
evidence_ids: ["love_discovery"],
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "love_discovery"));
|
|
550
|
+
}
|
|
551
|
+
loveClaims.push(claim({
|
|
552
|
+
key: "registry_role",
|
|
553
|
+
value: parsed.value.registry_role,
|
|
554
|
+
basis: "publisher_assertion",
|
|
555
|
+
role: "authority_boundary",
|
|
556
|
+
evidence_ids: ["love_discovery"],
|
|
557
|
+
}));
|
|
558
|
+
if (parsed.value.npm_mirror) {
|
|
559
|
+
const registryUrl = reportLocator(parsed.value.npm_mirror.registry_url);
|
|
560
|
+
if (registryUrl) {
|
|
561
|
+
loveClaims.push(claim({
|
|
562
|
+
key: "npm_registry_url",
|
|
563
|
+
value: registryUrl,
|
|
564
|
+
basis: "publisher_assertion",
|
|
565
|
+
role: "locator",
|
|
566
|
+
evidence_ids: ["love_discovery"],
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "love_discovery"));
|
|
571
|
+
}
|
|
572
|
+
loveClaims.push(claim({
|
|
573
|
+
key: "npm_registry_authority",
|
|
574
|
+
value: parsed.value.npm_mirror.authority,
|
|
575
|
+
basis: "publisher_assertion",
|
|
576
|
+
role: "authority_boundary",
|
|
577
|
+
evidence_ids: ["love_discovery"],
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
let parsedManifest = null;
|
|
584
|
+
let safeMirror = null;
|
|
585
|
+
let loveChainInvalid = false;
|
|
586
|
+
if (parsedLove) {
|
|
587
|
+
const indexUrl = canonicalHttpsLocator(parsedLove.index_url);
|
|
588
|
+
if (!indexUrl) {
|
|
589
|
+
loveChainInvalid = true;
|
|
590
|
+
diagnostics.push(diagnostic("love_index_locator_invalid", "love_discovery", "error"));
|
|
591
|
+
loveDiagnosticCodes.push("love_index_locator_invalid");
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const indexDocument = await get("love_index", indexUrl, JSON_ACCEPT);
|
|
595
|
+
sources.push(indexDocument.observation);
|
|
596
|
+
addTransportDiagnostic(indexDocument.observation, diagnostics);
|
|
597
|
+
loveEvidence.push("love_index");
|
|
598
|
+
if (indexDocument.observation.state !== "present" ||
|
|
599
|
+
!indexDocument.body) {
|
|
600
|
+
loveChainInvalid = true;
|
|
601
|
+
diagnostics.push(diagnostic("love_index_unavailable", "love_index", "error"));
|
|
602
|
+
loveDiagnosticCodes.push("love_index_unavailable");
|
|
603
|
+
}
|
|
604
|
+
else if (parsedPathways) {
|
|
605
|
+
if (!isJsonMediaType(indexDocument.observation.media_type)) {
|
|
606
|
+
loveChainInvalid = true;
|
|
607
|
+
diagnostics.push(diagnostic("unexpected_media_type", "love_index", "error"));
|
|
608
|
+
loveDiagnosticCodes.push("unexpected_media_type");
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const packageName = parsedPathways.npm?.package ?? "@agenttool/sdk";
|
|
612
|
+
const selection = selectLoveManifest(indexDocument.body, limits, packageName, parsedPathways.sdk_version);
|
|
613
|
+
if (!selection.ok) {
|
|
614
|
+
loveChainInvalid = true;
|
|
615
|
+
diagnostics.push(diagnostic(selection.code, "love_index", "error"));
|
|
616
|
+
loveDiagnosticCodes.push(selection.code);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
const manifestUrl = canonicalHttpsLocator(selection.value.manifest_url);
|
|
620
|
+
if (!manifestUrl) {
|
|
621
|
+
loveChainInvalid = true;
|
|
622
|
+
diagnostics.push(diagnostic("love_manifest_locator_invalid", "love_index", "error"));
|
|
623
|
+
loveDiagnosticCodes.push("love_manifest_locator_invalid");
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
const manifestDocument = await get("love_sdk_manifest", manifestUrl, JSON_ACCEPT);
|
|
627
|
+
sources.push(manifestDocument.observation);
|
|
628
|
+
addTransportDiagnostic(manifestDocument.observation, diagnostics);
|
|
629
|
+
loveEvidence.push("love_sdk_manifest");
|
|
630
|
+
if (manifestDocument.observation.state !== "present" ||
|
|
631
|
+
!manifestDocument.body) {
|
|
632
|
+
loveChainInvalid = true;
|
|
633
|
+
diagnostics.push(diagnostic("love_manifest_unavailable", "love_sdk_manifest", "error"));
|
|
634
|
+
loveDiagnosticCodes.push("love_manifest_unavailable");
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
if (!isJsonMediaType(manifestDocument.observation.media_type)) {
|
|
638
|
+
loveChainInvalid = true;
|
|
639
|
+
diagnostics.push(diagnostic("unexpected_media_type", "love_sdk_manifest", "error"));
|
|
640
|
+
loveDiagnosticCodes.push("unexpected_media_type");
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
const manifest = parseLoveManifest(manifestDocument.body, limits, packageName, parsedPathways.sdk_version);
|
|
644
|
+
if (!manifest.ok) {
|
|
645
|
+
loveChainInvalid = true;
|
|
646
|
+
diagnostics.push(diagnostic(manifest.code, "love_sdk_manifest", "error"));
|
|
647
|
+
loveDiagnosticCodes.push(manifest.code);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
parsedManifest = manifest.value;
|
|
651
|
+
loveClaims.push(claim({
|
|
652
|
+
key: "selected_package",
|
|
653
|
+
value: manifest.value.name,
|
|
654
|
+
basis: "publisher_assertion",
|
|
655
|
+
role: "release_selection",
|
|
656
|
+
evidence_ids: [
|
|
657
|
+
"pathways",
|
|
658
|
+
"love_index",
|
|
659
|
+
"love_sdk_manifest",
|
|
660
|
+
],
|
|
661
|
+
}), claim({
|
|
662
|
+
key: "selected_version",
|
|
663
|
+
value: manifest.value.version,
|
|
664
|
+
basis: "publisher_assertion",
|
|
665
|
+
role: "release_selection",
|
|
666
|
+
evidence_ids: [
|
|
667
|
+
"pathways",
|
|
668
|
+
"love_index",
|
|
669
|
+
"love_sdk_manifest",
|
|
670
|
+
],
|
|
671
|
+
}), claim({
|
|
672
|
+
key: "artifact_size",
|
|
673
|
+
value: manifest.value.artifact.size,
|
|
674
|
+
basis: "publisher_assertion",
|
|
675
|
+
role: "content_commitment",
|
|
676
|
+
evidence_ids: ["love_sdk_manifest"],
|
|
677
|
+
}), claim({
|
|
678
|
+
key: "artifact_sha256",
|
|
679
|
+
value: manifest.value.artifact.sha256,
|
|
680
|
+
basis: "publisher_assertion",
|
|
681
|
+
role: "content_commitment",
|
|
682
|
+
evidence_ids: ["love_sdk_manifest"],
|
|
683
|
+
}), claim({
|
|
684
|
+
key: "runtime_compatibility",
|
|
685
|
+
value: "not_evaluated",
|
|
686
|
+
basis: "local_derivation",
|
|
687
|
+
role: "capability_advertisement",
|
|
688
|
+
taint: "local",
|
|
689
|
+
evidence_ids: ["love_sdk_manifest"],
|
|
690
|
+
}));
|
|
691
|
+
for (const mirror of manifest.value.artifact.mirrors) {
|
|
692
|
+
const canonical = canonicalHttpsLocator(mirror);
|
|
693
|
+
if (!canonical)
|
|
694
|
+
continue;
|
|
695
|
+
try {
|
|
696
|
+
if (new URL(canonical).search)
|
|
697
|
+
continue;
|
|
698
|
+
safeMirror = (await assertPublicHttpsUrl(canonical, resolveHostname, deadline.signal)).href;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// Try the next declared mirror; no raw failure is emitted.
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!safeMirror) {
|
|
706
|
+
diagnostics.push(diagnostic("love_no_safe_mirror", "love_sdk_manifest", "error"));
|
|
707
|
+
loveDiagnosticCodes.push("love_no_safe_mirror");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (loveDocument.observation.state !== "present") {
|
|
719
|
+
surfaces.push(parseUnavailableSurface("love_packages", loveDocument.observation, [
|
|
720
|
+
"love_discovery_is_a_locator_not_publisher_authentication",
|
|
721
|
+
]));
|
|
722
|
+
}
|
|
723
|
+
else if (!parsedLove) {
|
|
724
|
+
surfaces.push({
|
|
725
|
+
id: "love_packages",
|
|
726
|
+
state: "invalid",
|
|
727
|
+
schema_conformance: "invalid",
|
|
728
|
+
evidence_ids: loveEvidence,
|
|
729
|
+
claims: loveClaims,
|
|
730
|
+
boundary_codes: [
|
|
731
|
+
"love_discovery_is_a_locator_not_publisher_authentication",
|
|
732
|
+
"manifest_digest_is_an_expectation_until_artifact_bytes_are_checked",
|
|
733
|
+
],
|
|
734
|
+
diagnostic_codes: [...new Set(loveDiagnosticCodes)],
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
surfaces.push({
|
|
739
|
+
id: "love_packages",
|
|
740
|
+
state: loveChainInvalid ? "invalid" : "present",
|
|
741
|
+
schema_conformance: loveChainInvalid
|
|
742
|
+
? "invalid"
|
|
743
|
+
: "supported_shape_valid",
|
|
744
|
+
evidence_ids: loveEvidence,
|
|
745
|
+
claims: loveClaims,
|
|
746
|
+
boundary_codes: [
|
|
747
|
+
"love_index_and_mirrors_are_locators_not_authority",
|
|
748
|
+
"manifest_digest_is_an_expectation_until_artifact_bytes_are_checked",
|
|
749
|
+
"love_v1_does_not_authenticate_a_publisher",
|
|
750
|
+
],
|
|
751
|
+
diagnostic_codes: [...new Set(loveDiagnosticCodes)],
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (parsedManifest && safeMirror) {
|
|
755
|
+
actions.push(...buildLoveActions({
|
|
756
|
+
manifest: parsedManifest,
|
|
757
|
+
mirror_url: safeMirror,
|
|
758
|
+
evidence_ids: ["pathways", "love_index", "love_sdk_manifest"],
|
|
759
|
+
}));
|
|
760
|
+
}
|
|
761
|
+
const mcpLocator = parsedAgent?.selected.mcp_card_url ?? null;
|
|
762
|
+
if (mcpLocator) {
|
|
763
|
+
const mcpUrl = canonicalHttpsLocator(mcpLocator);
|
|
764
|
+
if (!mcpUrl) {
|
|
765
|
+
diagnostics.push(diagnostic("mcp_locator_invalid", "agent_txt", "error"));
|
|
766
|
+
surfaces.push({
|
|
767
|
+
id: "mcp",
|
|
768
|
+
state: "invalid",
|
|
769
|
+
schema_conformance: "invalid",
|
|
770
|
+
evidence_ids: ["agent_txt"],
|
|
771
|
+
claims: [],
|
|
772
|
+
boundary_codes: ["advertisement_does_not_prove_callable_mcp"],
|
|
773
|
+
diagnostic_codes: ["mcp_locator_invalid"],
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
const mcpDocument = await get("mcp_card", mcpUrl, JSON_ACCEPT);
|
|
778
|
+
sources.push(mcpDocument.observation);
|
|
779
|
+
addTransportDiagnostic(mcpDocument.observation, diagnostics);
|
|
780
|
+
if (mcpDocument.observation.state === "not_found") {
|
|
781
|
+
diagnostics.push(diagnostic("advertised_mcp_not_found", "mcp_card"));
|
|
782
|
+
}
|
|
783
|
+
if (mcpDocument.observation.state === "present" && mcpDocument.body) {
|
|
784
|
+
if (!isJsonMediaType(mcpDocument.observation.media_type)) {
|
|
785
|
+
diagnostics.push(diagnostic("unexpected_media_type", "mcp_card", "error"));
|
|
786
|
+
surfaces.push({
|
|
787
|
+
id: "mcp",
|
|
788
|
+
state: "invalid",
|
|
789
|
+
schema_conformance: "invalid",
|
|
790
|
+
evidence_ids: ["agent_txt", "mcp_card"],
|
|
791
|
+
claims: [],
|
|
792
|
+
boundary_codes: [
|
|
793
|
+
"experimental_publisher_advertisement",
|
|
794
|
+
"card_presence_does_not_prove_initialization_tools_or_authentication",
|
|
795
|
+
],
|
|
796
|
+
diagnostic_codes: ["unexpected_media_type"],
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
const parsed = parseMcpCard(mcpDocument.body, limits);
|
|
801
|
+
if (!parsed.ok) {
|
|
802
|
+
diagnostics.push(diagnostic(parsed.code, "mcp_card", "error"));
|
|
803
|
+
surfaces.push({
|
|
804
|
+
id: "mcp",
|
|
805
|
+
state: "invalid",
|
|
806
|
+
schema_conformance: "invalid",
|
|
807
|
+
evidence_ids: ["agent_txt", "mcp_card"],
|
|
808
|
+
claims: [],
|
|
809
|
+
boundary_codes: [
|
|
810
|
+
"experimental_publisher_advertisement",
|
|
811
|
+
"card_presence_does_not_prove_initialization_tools_or_authentication",
|
|
812
|
+
],
|
|
813
|
+
diagnostic_codes: [parsed.code],
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
const mcpEndpoint = reportLocator(parsed.value.endpoint);
|
|
818
|
+
if (!mcpEndpoint) {
|
|
819
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "mcp_card"));
|
|
820
|
+
}
|
|
821
|
+
surfaces.push({
|
|
822
|
+
id: "mcp",
|
|
823
|
+
state: "present",
|
|
824
|
+
schema_conformance: "not_assessed",
|
|
825
|
+
evidence_ids: ["agent_txt", "mcp_card"],
|
|
826
|
+
claims: [
|
|
827
|
+
claim({
|
|
828
|
+
key: "name",
|
|
829
|
+
value: parsed.value.name,
|
|
830
|
+
basis: "publisher_assertion",
|
|
831
|
+
role: "capability_advertisement",
|
|
832
|
+
evidence_ids: ["mcp_card"],
|
|
833
|
+
}),
|
|
834
|
+
claim({
|
|
835
|
+
key: "protocol_version",
|
|
836
|
+
value: parsed.value.protocol_version,
|
|
837
|
+
basis: "publisher_assertion",
|
|
838
|
+
role: "capability_advertisement",
|
|
839
|
+
evidence_ids: ["mcp_card"],
|
|
840
|
+
}),
|
|
841
|
+
...(mcpEndpoint
|
|
842
|
+
? [
|
|
843
|
+
claim({
|
|
844
|
+
key: "endpoint",
|
|
845
|
+
value: mcpEndpoint,
|
|
846
|
+
basis: "publisher_assertion",
|
|
847
|
+
role: "locator",
|
|
848
|
+
evidence_ids: ["mcp_card"],
|
|
849
|
+
}),
|
|
850
|
+
]
|
|
851
|
+
: []),
|
|
852
|
+
claim({
|
|
853
|
+
key: "authentication",
|
|
854
|
+
value: parsed.value.authentication,
|
|
855
|
+
basis: "publisher_assertion",
|
|
856
|
+
role: "authority_boundary",
|
|
857
|
+
evidence_ids: ["mcp_card"],
|
|
858
|
+
}),
|
|
859
|
+
],
|
|
860
|
+
boundary_codes: [
|
|
861
|
+
"experimental_publisher_advertisement",
|
|
862
|
+
"schema_conformance_not_assessed",
|
|
863
|
+
"card_presence_does_not_prove_initialization_tools_or_authentication",
|
|
864
|
+
"endpoint_not_invoked",
|
|
865
|
+
],
|
|
866
|
+
diagnostic_codes: [],
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
surfaces.push(parseUnavailableSurface("mcp", mcpDocument.observation, [
|
|
873
|
+
"advertisement_does_not_prove_callable_mcp",
|
|
874
|
+
"endpoint_not_invoked",
|
|
875
|
+
]));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
surfaces.push({
|
|
881
|
+
id: "mcp",
|
|
882
|
+
state: "not_attempted",
|
|
883
|
+
schema_conformance: "not_assessed",
|
|
884
|
+
evidence_ids: parsedAgent ? ["agent_txt"] : [],
|
|
885
|
+
claims: [],
|
|
886
|
+
boundary_codes: ["mcp_not_advertised_by_valid_agent_txt"],
|
|
887
|
+
diagnostic_codes: [],
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (a2aDocument.observation.state === "present" && a2aDocument.body) {
|
|
891
|
+
if (!isJsonMediaType(a2aDocument.observation.media_type)) {
|
|
892
|
+
diagnostics.push(diagnostic("unexpected_media_type", "a2a_card", "error"));
|
|
893
|
+
surfaces.push({
|
|
894
|
+
id: "a2a",
|
|
895
|
+
state: "invalid",
|
|
896
|
+
schema_conformance: "invalid",
|
|
897
|
+
evidence_ids: ["a2a_card"],
|
|
898
|
+
claims: [],
|
|
899
|
+
boundary_codes: ["card_advertisement_does_not_prove_task_transport"],
|
|
900
|
+
diagnostic_codes: ["unexpected_media_type"],
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
const parsed = parseA2aCard(a2aDocument.body, limits);
|
|
905
|
+
if (!parsed.ok) {
|
|
906
|
+
diagnostics.push(diagnostic(parsed.code, "a2a_card", "error"));
|
|
907
|
+
surfaces.push({
|
|
908
|
+
id: "a2a",
|
|
909
|
+
state: "invalid",
|
|
910
|
+
schema_conformance: "invalid",
|
|
911
|
+
evidence_ids: ["a2a_card"],
|
|
912
|
+
claims: [],
|
|
913
|
+
boundary_codes: [
|
|
914
|
+
"card_advertisement_does_not_prove_task_transport",
|
|
915
|
+
],
|
|
916
|
+
diagnostic_codes: [parsed.code],
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
const a2aEndpoint = parsed.value.endpoint
|
|
921
|
+
? reportLocator(parsed.value.endpoint)
|
|
922
|
+
: null;
|
|
923
|
+
if (parsed.value.endpoint && !a2aEndpoint) {
|
|
924
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "a2a_card"));
|
|
925
|
+
}
|
|
926
|
+
surfaces.push({
|
|
927
|
+
id: "a2a",
|
|
928
|
+
state: "present",
|
|
929
|
+
schema_conformance: "not_assessed",
|
|
930
|
+
evidence_ids: ["a2a_card"],
|
|
931
|
+
claims: [
|
|
932
|
+
claim({
|
|
933
|
+
key: "name",
|
|
934
|
+
value: parsed.value.name,
|
|
935
|
+
basis: "publisher_assertion",
|
|
936
|
+
role: "capability_advertisement",
|
|
937
|
+
evidence_ids: ["a2a_card"],
|
|
938
|
+
}),
|
|
939
|
+
...(a2aEndpoint
|
|
940
|
+
? [
|
|
941
|
+
claim({
|
|
942
|
+
key: "endpoint",
|
|
943
|
+
value: a2aEndpoint,
|
|
944
|
+
basis: "publisher_assertion",
|
|
945
|
+
role: "locator",
|
|
946
|
+
evidence_ids: ["a2a_card"],
|
|
947
|
+
}),
|
|
948
|
+
]
|
|
949
|
+
: []),
|
|
950
|
+
],
|
|
951
|
+
boundary_codes: [
|
|
952
|
+
"schema_conformance_not_assessed",
|
|
953
|
+
"card_advertisement_does_not_prove_task_transport",
|
|
954
|
+
"endpoint_not_invoked",
|
|
955
|
+
],
|
|
956
|
+
diagnostic_codes: [],
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
const a2aMissingBoundaries = a2aDocument.observation.state === "not_found"
|
|
963
|
+
? [
|
|
964
|
+
"not_found_means_only_exact_standard_path_at_observation_time",
|
|
965
|
+
"no_private_or_alternate_a2a_absence_inferred",
|
|
966
|
+
]
|
|
967
|
+
: ["no_a2a_absence_inferred_from_inconclusive_observation"];
|
|
968
|
+
surfaces.push(parseUnavailableSurface("a2a", a2aDocument.observation, a2aMissingBoundaries));
|
|
969
|
+
}
|
|
970
|
+
const rawWebfingerTemplate = parsedAgent?.selected.webfinger_template ?? null;
|
|
971
|
+
const webfingerTemplate = rawWebfingerTemplate
|
|
972
|
+
? reportLocator(rawWebfingerTemplate, true)
|
|
973
|
+
: null;
|
|
974
|
+
if (rawWebfingerTemplate && !webfingerTemplate) {
|
|
975
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "agent_txt"));
|
|
976
|
+
}
|
|
977
|
+
surfaces.push({
|
|
978
|
+
id: "webfinger",
|
|
979
|
+
state: webfingerTemplate ? "present" : "not_attempted",
|
|
980
|
+
schema_conformance: "not_assessed",
|
|
981
|
+
evidence_ids: parsedAgent ? ["agent_txt"] : [],
|
|
982
|
+
claims: webfingerTemplate
|
|
983
|
+
? [
|
|
984
|
+
claim({
|
|
985
|
+
key: "template",
|
|
986
|
+
value: webfingerTemplate,
|
|
987
|
+
basis: "publisher_assertion",
|
|
988
|
+
role: "locator",
|
|
989
|
+
evidence_ids: ["agent_txt"],
|
|
990
|
+
}),
|
|
991
|
+
]
|
|
992
|
+
: [],
|
|
993
|
+
boundary_codes: [
|
|
994
|
+
"locator_not_identity_authority",
|
|
995
|
+
"not_queried_without_an_exact_did",
|
|
996
|
+
],
|
|
997
|
+
diagnostic_codes: [],
|
|
998
|
+
});
|
|
999
|
+
const offer = parsedAgent?.selected;
|
|
1000
|
+
const offerClaims = [];
|
|
1001
|
+
if (offer) {
|
|
1002
|
+
for (const [key, value, role] of [
|
|
1003
|
+
["atom_url", offer.offer_bus_atom_url, "locator"],
|
|
1004
|
+
["rss_url", offer.offer_bus_rss_url, "locator"],
|
|
1005
|
+
["json_url", offer.offer_bus_json_url, "locator"],
|
|
1006
|
+
["boundary", offer.offer_bus_boundary, "authority_boundary"],
|
|
1007
|
+
["websub", offer.websub, "capability_advertisement"],
|
|
1008
|
+
]) {
|
|
1009
|
+
if (value) {
|
|
1010
|
+
const claimValue = role === "locator" ? reportLocator(value) : value;
|
|
1011
|
+
if (claimValue === null) {
|
|
1012
|
+
diagnostics.push(diagnostic("unsafe_remote_locator_omitted", "agent_txt"));
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
offerClaims.push(claim({
|
|
1016
|
+
key,
|
|
1017
|
+
value: claimValue,
|
|
1018
|
+
basis: "publisher_assertion",
|
|
1019
|
+
role,
|
|
1020
|
+
evidence_ids: ["agent_txt"],
|
|
1021
|
+
}));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
surfaces.push({
|
|
1026
|
+
id: "offer_bus",
|
|
1027
|
+
state: offerClaims.length > 0 ? "present" : "not_attempted",
|
|
1028
|
+
schema_conformance: "not_assessed",
|
|
1029
|
+
evidence_ids: parsedAgent ? ["agent_txt"] : [],
|
|
1030
|
+
claims: offerClaims,
|
|
1031
|
+
boundary_codes: [
|
|
1032
|
+
"feed_discovery_grants_no_authority_or_settlement",
|
|
1033
|
+
"feeds_not_fetched",
|
|
1034
|
+
"no_automatic_action",
|
|
1035
|
+
],
|
|
1036
|
+
diagnostic_codes: [],
|
|
1037
|
+
});
|
|
1038
|
+
const extensions = await runAdapters(adapters, {
|
|
1039
|
+
subject,
|
|
1040
|
+
observed_at: observedAt,
|
|
1041
|
+
signal: deadline.signal,
|
|
1042
|
+
}, diagnostics);
|
|
1043
|
+
const coreIds = new Set([
|
|
1044
|
+
"agent_txt",
|
|
1045
|
+
"pathways",
|
|
1046
|
+
"love_packages",
|
|
1047
|
+
"a2a",
|
|
1048
|
+
]);
|
|
1049
|
+
const corePresent = surfaces.some((surface) => coreIds.has(surface.id) && surface.state === "present");
|
|
1050
|
+
const degraded = surfaces.some((surface) => surface.state === "invalid") ||
|
|
1051
|
+
sources.some((source) => ["restricted", "unreachable", "blocked", "too_large"].includes(source.state)) ||
|
|
1052
|
+
diagnostics.some((entry) => entry.level === "error" || entry.code.includes("ambiguous"));
|
|
1053
|
+
diagnostics.sort((left, right) => {
|
|
1054
|
+
const leftKey = `${left.evidence_id ?? "~"}\u0000${left.code}\u0000${left.level}`;
|
|
1055
|
+
const rightKey = `${right.evidence_id ?? "~"}\u0000${right.code}\u0000${right.level}`;
|
|
1056
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
1057
|
+
});
|
|
1058
|
+
const status = !corePresent
|
|
1059
|
+
? "inconclusive"
|
|
1060
|
+
: degraded
|
|
1061
|
+
? "partial"
|
|
1062
|
+
: "discovered";
|
|
1063
|
+
return {
|
|
1064
|
+
schema: REPORT_SCHEMA,
|
|
1065
|
+
tool: { name: TOOL_NAME, version: TOOL_VERSION },
|
|
1066
|
+
subject,
|
|
1067
|
+
observed_at: observedAt,
|
|
1068
|
+
status,
|
|
1069
|
+
network_boundary: {
|
|
1070
|
+
mode: "public_https_read_only",
|
|
1071
|
+
http_transport: options.fetch ? "injected" : "native_fetch",
|
|
1072
|
+
dns_resolver: options.resolve_hostname ? "injected" : "system_lookup",
|
|
1073
|
+
methods: ["GET"],
|
|
1074
|
+
credentials: "omitted",
|
|
1075
|
+
redirects: "manual_revalidated",
|
|
1076
|
+
dns_preflight: true,
|
|
1077
|
+
connected_address_pinning: false,
|
|
1078
|
+
ambient_proxy_isolation: false,
|
|
1079
|
+
statement: "Core probes ask the selected resolver and transport to fail closed on non-global addresses, omit credentials, and revalidate redirects. Native fetch can re-resolve before connecting; the connection is not address-pinned, ambient proxy behavior is not controlled, and this is not a universal SSRF guarantee. Injected transports, resolvers, and adapters are caller-owned seams.",
|
|
1080
|
+
},
|
|
1081
|
+
effective_limits: limits,
|
|
1082
|
+
sources,
|
|
1083
|
+
surfaces,
|
|
1084
|
+
actions,
|
|
1085
|
+
extensions,
|
|
1086
|
+
diagnostics,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
deadline.cleanup();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
//# sourceMappingURL=scan.js.map
|