@sourcey/catalog-verifier 1.1.21 → 1.1.23
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 +4 -1
- package/SOURCEY-CLOSURE.json +265 -121
- package/dist/contracts/agent-readiness/src/binding.d.ts +28 -1
- package/dist/contracts/agent-readiness/src/binding.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/binding.js +75 -16
- package/dist/contracts/agent-readiness/src/binding.js.map +1 -1
- package/dist/contracts/agent-readiness/src/client.d.ts +30 -0
- package/dist/contracts/agent-readiness/src/client.d.ts.map +1 -0
- package/dist/contracts/agent-readiness/src/client.js +25 -0
- package/dist/contracts/agent-readiness/src/client.js.map +1 -0
- package/dist/contracts/agent-readiness/src/declaration-acquisition.d.ts +91 -0
- package/dist/contracts/agent-readiness/src/declaration-acquisition.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/declaration-acquisition.js +6 -0
- package/dist/contracts/agent-readiness/src/declaration-acquisition.js.map +1 -1
- package/dist/contracts/agent-readiness/src/declaration.d.ts +358 -0
- package/dist/contracts/agent-readiness/src/declaration.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/declaration.js +34 -79
- package/dist/contracts/agent-readiness/src/declaration.js.map +1 -1
- package/dist/contracts/agent-readiness/src/{operate-policy.d.ts → grade-policy.d.ts} +31 -79
- package/dist/contracts/agent-readiness/src/grade-policy.d.ts.map +1 -0
- package/dist/contracts/agent-readiness/src/grade-policy.js +93 -0
- package/dist/contracts/agent-readiness/src/grade-policy.js.map +1 -0
- package/dist/contracts/agent-readiness/src/index.d.ts +3 -1
- package/dist/contracts/agent-readiness/src/index.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/index.js +3 -1
- package/dist/contracts/agent-readiness/src/index.js.map +1 -1
- package/dist/contracts/agent-readiness/src/jobs.d.ts +12 -3
- package/dist/contracts/agent-readiness/src/jobs.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/jobs.js +8 -1
- package/dist/contracts/agent-readiness/src/jobs.js.map +1 -1
- package/dist/contracts/agent-readiness/src/offer-relations.d.ts +89 -0
- package/dist/contracts/agent-readiness/src/offer-relations.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/operation.d.ts +106 -0
- package/dist/contracts/agent-readiness/src/operation.d.ts.map +1 -0
- package/dist/contracts/agent-readiness/src/operation.js +228 -0
- package/dist/contracts/agent-readiness/src/operation.js.map +1 -0
- package/dist/contracts/agent-readiness/src/release-index.d.ts +277 -89
- package/dist/contracts/agent-readiness/src/release-index.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/revision.d.ts +500 -166
- package/dist/contracts/agent-readiness/src/revision.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/revision.js +100 -37
- package/dist/contracts/agent-readiness/src/revision.js.map +1 -1
- package/dist/contracts/agent-readiness/src/run.d.ts +386 -98
- package/dist/contracts/agent-readiness/src/run.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/run.js +333 -52
- package/dist/contracts/agent-readiness/src/run.js.map +1 -1
- package/dist/contracts/agent-readiness/src/shared.d.ts +51 -17
- package/dist/contracts/agent-readiness/src/shared.d.ts.map +1 -1
- package/dist/contracts/agent-readiness/src/shared.js +52 -23
- package/dist/contracts/agent-readiness/src/shared.js.map +1 -1
- package/dist/contracts/agent-readiness/src/surfaces.d.ts +25 -0
- package/dist/contracts/agent-readiness/src/surfaces.d.ts.map +1 -0
- package/dist/contracts/agent-readiness/src/surfaces.js +76 -0
- package/dist/contracts/agent-readiness/src/surfaces.js.map +1 -0
- package/dist/contracts/api/src/endpoints.d.ts +885 -254
- package/dist/contracts/api/src/endpoints.d.ts.map +1 -1
- package/dist/contracts/api/src/read.d.ts +354 -109
- package/dist/contracts/api/src/read.d.ts.map +1 -1
- package/dist/contracts/api/src/read.js +1 -1
- package/dist/contracts/api/src/routes.d.ts +27 -23
- package/dist/contracts/api/src/routes.d.ts.map +1 -1
- package/dist/contracts/api/src/routes.js +2 -3
- package/dist/contracts/api/src/routes.js.map +1 -1
- package/dist/contracts/api/src/submissions.d.ts +91 -0
- package/dist/contracts/api/src/submissions.d.ts.map +1 -1
- package/dist/contracts/datasets/src/index.d.ts +13 -10
- package/dist/contracts/datasets/src/index.d.ts.map +1 -1
- package/dist/contracts/startup-credits-commercial/src/product.js +1 -1
- package/dist/contracts/startup-credits-commercial/src/product.js.map +1 -1
- package/dist/modules/agent-readiness-engine/src/assertions.d.ts.map +1 -1
- package/dist/modules/agent-readiness-engine/src/assertions.js +25 -0
- package/dist/modules/agent-readiness-engine/src/assertions.js.map +1 -1
- package/dist/modules/agent-readiness-engine/src/authority.d.ts +81 -0
- package/dist/modules/agent-readiness-engine/src/authority.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/authority.js +189 -0
- package/dist/modules/agent-readiness-engine/src/authority.js.map +1 -0
- package/dist/modules/agent-readiness-engine/src/challenges.d.ts +13 -0
- package/dist/modules/agent-readiness-engine/src/challenges.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/challenges.js +67 -0
- package/dist/modules/agent-readiness-engine/src/challenges.js.map +1 -0
- package/dist/modules/agent-readiness-engine/src/documents.d.ts +21 -0
- package/dist/modules/agent-readiness-engine/src/documents.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/documents.js +69 -0
- package/dist/modules/agent-readiness-engine/src/documents.js.map +1 -0
- package/dist/modules/agent-readiness-engine/src/naming.d.ts +19 -0
- package/dist/modules/agent-readiness-engine/src/naming.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/naming.js +28 -0
- package/dist/modules/agent-readiness-engine/src/naming.js.map +1 -0
- package/dist/modules/agent-readiness-engine/src/path.d.ts +68 -15
- package/dist/modules/agent-readiness-engine/src/path.d.ts.map +1 -1
- package/dist/modules/agent-readiness-engine/src/path.js +467 -153
- package/dist/modules/agent-readiness-engine/src/path.js.map +1 -1
- package/dist/modules/agent-readiness-engine/src/profile.d.ts +85 -0
- package/dist/modules/agent-readiness-engine/src/profile.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/profile.js +176 -0
- package/dist/modules/agent-readiness-engine/src/profile.js.map +1 -0
- package/dist/modules/agent-readiness-engine/src/rate.d.ts +10 -20
- package/dist/modules/agent-readiness-engine/src/rate.d.ts.map +1 -1
- package/dist/modules/agent-readiness-engine/src/rate.js +29 -47
- package/dist/modules/agent-readiness-engine/src/rate.js.map +1 -1
- package/dist/modules/agent-readiness-engine/src/records.d.ts +30 -0
- package/dist/modules/agent-readiness-engine/src/records.d.ts.map +1 -0
- package/dist/modules/agent-readiness-engine/src/records.js +59 -0
- package/dist/modules/agent-readiness-engine/src/records.js.map +1 -0
- package/dist/modules/agent-readiness-jobs/src/index.d.ts +16 -8
- package/dist/modules/agent-readiness-jobs/src/index.d.ts.map +1 -1
- package/dist/modules/agent-readiness-jobs/src/index.js +12 -7
- package/dist/modules/agent-readiness-jobs/src/index.js.map +1 -1
- package/dist/modules/agent-readiness-policy/src/current-policy.d.ts +13 -17
- package/dist/modules/agent-readiness-policy/src/current-policy.d.ts.map +1 -1
- package/dist/modules/agent-readiness-policy/src/current-policy.js +1 -1
- package/dist/modules/agent-readiness-policy/src/current-policy.js.map +1 -1
- package/dist/modules/agent-readiness-policy/src/{operate-policy.d.ts → grade-policy.d.ts} +3 -3
- package/dist/modules/agent-readiness-policy/src/grade-policy.d.ts.map +1 -0
- package/dist/modules/agent-readiness-policy/src/grade-policy.js +82 -0
- package/dist/modules/agent-readiness-policy/src/grade-policy.js.map +1 -0
- package/dist/modules/agent-readiness-policy/src/index.d.ts +4 -4
- package/dist/modules/agent-readiness-policy/src/index.d.ts.map +1 -1
- package/dist/modules/agent-readiness-policy/src/index.js +35 -22
- package/dist/modules/agent-readiness-policy/src/index.js.map +1 -1
- package/dist/modules/agent-readiness-policy/src/revision.d.ts +3 -3
- package/dist/modules/agent-readiness-policy/src/revision.d.ts.map +1 -1
- package/dist/modules/agent-readiness-policy/src/revision.js +40 -32
- package/dist/modules/agent-readiness-policy/src/revision.js.map +1 -1
- package/dist/modules/artifact/src/policy-closure.js +1 -1
- package/dist/modules/artifact/src/policy-closure.js.map +1 -1
- package/dist/modules/catalog-admission/src/publication-dependencies.d.ts +0 -4
- package/dist/modules/catalog-admission/src/publication-dependencies.d.ts.map +1 -1
- package/dist/modules/catalog-admission/src/publication-dependencies.js +1 -1
- package/dist/modules/catalog-admission/src/publication-dependencies.js.map +1 -1
- package/dist/modules/release/src/identity.d.ts +0 -1
- package/dist/modules/release/src/identity.d.ts.map +1 -1
- package/dist/modules/release/src/identity.js +1 -1
- package/dist/modules/release/src/identity.js.map +1 -1
- package/dist/modules/release/src/verification-policies.d.ts +13 -17
- package/dist/modules/release/src/verification-policies.d.ts.map +1 -1
- package/dist/modules/release/src/verification-policies.js +1 -1
- package/dist/modules/release/src/verification-policies.js.map +1 -1
- package/dist/modules/sourcey-publication/src/verifier.d.ts +5 -0
- package/dist/modules/sourcey-publication/src/verifier.d.ts.map +1 -1
- package/dist/modules/sourcey-publication/src/verifier.js +13 -0
- package/dist/modules/sourcey-publication/src/verifier.js.map +1 -1
- package/package.json +1 -1
- package/dist/contracts/agent-readiness/src/operate-policy.d.ts.map +0 -1
- package/dist/contracts/agent-readiness/src/operate-policy.js +0 -86
- package/dist/contracts/agent-readiness/src/operate-policy.js.map +0 -1
- package/dist/modules/agent-readiness-policy/src/operate-policy.d.ts.map +0 -1
- package/dist/modules/agent-readiness-policy/src/operate-policy.js +0 -110
- package/dist/modules/agent-readiness-policy/src/operate-policy.js.map +0 -1
|
@@ -1,59 +1,325 @@
|
|
|
1
1
|
import { compareInstants } from "provenry/primitives";
|
|
2
2
|
import { AGENT_READINESS_STEPS, } from "../../../contracts/agent-readiness/src/index.js";
|
|
3
|
+
import { consentReached } from "./authority.js";
|
|
4
|
+
import { agentReadinessFactNamesEndpoint, agentReadinessNamesEndpoint } from "./naming.js";
|
|
5
|
+
import { probeUnanswered } from "./records.js";
|
|
3
6
|
const MAXIMUM_EVIDENCE = 32;
|
|
4
|
-
|
|
5
|
-
const NAMING_FACTS = new Set([
|
|
6
|
-
"mcp_endpoint",
|
|
7
|
-
"ard_entry",
|
|
8
|
-
"api_catalog_link",
|
|
9
|
-
"openapi_server",
|
|
10
|
-
"oauth_protected_resource",
|
|
11
|
-
]);
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
12
8
|
/**
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* The probed path of one interface (north star §5.6). The latest probe run that
|
|
10
|
+
* answered the operation decides its steps; a run that left its request, its
|
|
11
|
+
* malformed request, its spec, its authority chain or a declared page
|
|
12
|
+
* unanswered decides nothing, since residue is never a fact. Discovery's
|
|
13
|
+
* latest run decides Discover and the payment manifests, unless an unanswered
|
|
14
|
+
* read could change them: then the latest run whose reads were all answered,
|
|
15
|
+
* or left unanswered only the service's standing answers, does. Earlier runs
|
|
16
|
+
* count only to reproduce a page answer or a standing answer.
|
|
17
|
+
*/
|
|
18
|
+
export function evaluateProbePath(input) {
|
|
19
|
+
const runs = oldestFirst(input.runs);
|
|
20
|
+
const latest = runs.at(-1);
|
|
21
|
+
if (!latest)
|
|
22
|
+
throw new Error("A probed path needs at least one probe run.");
|
|
23
|
+
const { operation } = input;
|
|
24
|
+
const id = operation.operation_id;
|
|
25
|
+
const answeredRuns = runs.filter((run) => !probeUnanswered(run, id));
|
|
26
|
+
const current = answeredRuns.at(-1);
|
|
27
|
+
const probe = current?.probes.find(({ operation_id }) => operation_id === id);
|
|
28
|
+
if (!current || !probe) {
|
|
29
|
+
return {
|
|
30
|
+
steps: AGENT_READINESS_STEPS.map(notAssessed),
|
|
31
|
+
job: NOT_ASSESSED_JOB,
|
|
32
|
+
reproducible: false,
|
|
33
|
+
discoveryRun: null,
|
|
34
|
+
verifiedRun: null,
|
|
35
|
+
standingRuns: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const answer = probe.request?.answer ?? null;
|
|
39
|
+
const of = (purpose) => exchangeEvidence(current, (entry) => entry.call_id === id && entry.purpose === purpose);
|
|
40
|
+
const requested = of("probe_operation");
|
|
41
|
+
const malformed = of("probe_malformed");
|
|
42
|
+
const metadata = of("probe_authorization_server");
|
|
43
|
+
const chainEvidence = [
|
|
44
|
+
...of("probe_resource_metadata"),
|
|
45
|
+
...metadata,
|
|
46
|
+
...of("probe_client_registration"),
|
|
47
|
+
...of("probe_authorization"),
|
|
48
|
+
];
|
|
49
|
+
const pagesOf = (include) => {
|
|
50
|
+
const ids = new Set(current.pages.filter(include).map(({ page_id }) => page_id));
|
|
51
|
+
return exchangeEvidence(current, (entry) => entry.purpose === "probe_page" && ids.has(entry.call_id));
|
|
52
|
+
};
|
|
53
|
+
const spec = probe.spec;
|
|
54
|
+
const specRead = spec?.fetched === "read" ? spec : null;
|
|
55
|
+
const covered = specRead?.covers === true ? specRead : null;
|
|
56
|
+
const specEvidence = spec
|
|
57
|
+
? [{ kind: "discovery", run_digest: current.run_digest, attempt_digest: spec.attempt_digest }]
|
|
58
|
+
: [];
|
|
59
|
+
const declaration = () => [{ kind: "declaration", run_digest: current.run_digest }];
|
|
60
|
+
const chain = probe.authority;
|
|
61
|
+
const refusal = answer === "offer" || answer === "authority" || answer === "error";
|
|
62
|
+
// No account is needed: the request itself answered, or the spec requires no security.
|
|
63
|
+
const anonymous = answer === "success" ||
|
|
64
|
+
answer === "offer" ||
|
|
65
|
+
((answer === null || answer === "error" || answer === "page") &&
|
|
66
|
+
covered !== null &&
|
|
67
|
+
covered.security.includes("none"));
|
|
68
|
+
// An agent registration path: the chain's auth.md registration, or a sign-up API the listing names.
|
|
69
|
+
// A sign-up API answers the probe's GET as an API that asks nothing of a person: a success, an
|
|
70
|
+
// offer, or a typed 405 (it signs up by POST). A CAPTCHA or a credential demanded is for people.
|
|
71
|
+
const pageStatus = (page) => {
|
|
72
|
+
const entry = current.exchanges.find(({ purpose, call_id }) => purpose === "probe_page" && call_id === page.page_id);
|
|
73
|
+
return entry?.record.outcome === "responded" ? entry.record.response.status : null;
|
|
74
|
+
};
|
|
75
|
+
const signUpApi = (page) => page.role === "registration" &&
|
|
76
|
+
page.found &&
|
|
77
|
+
(page.answer === "success" ||
|
|
78
|
+
page.answer === "offer" ||
|
|
79
|
+
(page.answer === "error" && pageStatus(page) === 405));
|
|
80
|
+
const signedUp = current.pages.some(signUpApi);
|
|
81
|
+
const registration = chain?.registration === "agent" || signedUp ? "agent" : (chain?.registration ?? null);
|
|
82
|
+
const registrationEvidence = [
|
|
83
|
+
...(chain?.registration ? chainEvidence : []),
|
|
84
|
+
...(signedUp ? pagesOf(signUpApi) : []),
|
|
85
|
+
];
|
|
86
|
+
const target = operation.kind === "http" ? operation.url : input.endpoint;
|
|
87
|
+
// A read unanswered on every probe since it last answered, the first of them the policy's
|
|
88
|
+
// interval before the latest, is the service's standing answer. A run that answered it, or did
|
|
89
|
+
// not read it, starts the count again.
|
|
90
|
+
const stretches = latest.discovery.unanswered.map((url) => {
|
|
91
|
+
const since = runs.findLastIndex(({ discovery }) => !discovery.unanswered.includes(url));
|
|
92
|
+
return { url, runs: reproducing(runs.slice(since + 1), input.policy.reproduction) };
|
|
93
|
+
});
|
|
94
|
+
const standing = new Set(stretches.flatMap(({ url, runs: found }) => (found.length > 0 ? [url] : [])));
|
|
95
|
+
const settled = (run) => run.discovery.unanswered.every((url) => standing.has(url));
|
|
96
|
+
const settledDiscovery = [...runs].reverse().find(settled);
|
|
97
|
+
/** The discovery run that decides: the latest, unless a read there still unanswered could change the reading. */
|
|
98
|
+
const discoveryFor = (decided) => decided(latest) || settled(latest) ? latest : (settledDiscovery ?? null);
|
|
99
|
+
// A server that answers for authority before listing its tools describes them to the agent it
|
|
100
|
+
// authorizes: it asked before any tool was called. A list it refused or cut short describes none.
|
|
101
|
+
const toolCalled = current.exchanges.some((entry) => entry.call_id === id && entry.purpose === "probe_operation" && entry.role === "request");
|
|
102
|
+
const toolDescribed = operation.kind === "mcp" &&
|
|
103
|
+
(probe.tool_listed === true ||
|
|
104
|
+
(probe.tool_listed === null && answer === "authority" && !toolCalled));
|
|
105
|
+
const finds = (run) => discoverStep(run, current, operation, target, covered, specEvidence, toolDescribed);
|
|
106
|
+
const discoveryRun = discoveryFor((candidate) => finds(candidate).outcome === "machine");
|
|
107
|
+
const discover = discoveryRun ? finds(discoveryRun) : notAssessed("discover");
|
|
108
|
+
const authority = (() => {
|
|
109
|
+
if (anonymous) {
|
|
110
|
+
return result("authority", "not_applicable", null, answer === null ? specEvidence : requested);
|
|
111
|
+
}
|
|
112
|
+
if (registration !== null) {
|
|
113
|
+
return result("authority", "machine", null, [...requested, ...registrationEvidence]);
|
|
114
|
+
}
|
|
115
|
+
const authorization = current.exchanges.find((entry) => entry.call_id === id && entry.purpose === "probe_authorization");
|
|
116
|
+
if (chain?.client && authorization && consentReached(authorization.record)) {
|
|
117
|
+
return result("authority", "approval", "setup", [...requested, ...chainEvidence]);
|
|
118
|
+
}
|
|
119
|
+
// A key or token scheme, an OAuth flow the agent's own client cannot start,
|
|
120
|
+
// or nothing machine-readable: a person hands over a credential.
|
|
121
|
+
return result("authority", "workaround", "setup", [
|
|
122
|
+
...requested,
|
|
123
|
+
...chainEvidence,
|
|
124
|
+
...specEvidence,
|
|
125
|
+
]);
|
|
126
|
+
})();
|
|
127
|
+
const onboard = (() => {
|
|
128
|
+
if (anonymous)
|
|
129
|
+
return result("onboard", "not_applicable", null, authority.evidence);
|
|
130
|
+
// The vendor's decision stands before any way in: whatever registration exists, it decides.
|
|
131
|
+
const accessPage = pagesOf(({ page_id, found }) => input.accessGates.includes(page_id) && found);
|
|
132
|
+
if (accessPage.length > 0)
|
|
133
|
+
return result("onboard", "gated", null, accessPage);
|
|
134
|
+
// A setting a person on the account turns on once before the agent can use this interface (an
|
|
135
|
+
// organization's MCP write access): a decision made once, whatever way in the agent has.
|
|
136
|
+
const turnedOn = pagesOf(({ page_id, found }) => input.accessSettings.includes(page_id) && found);
|
|
137
|
+
const agentGetsIn = (evidence) => turnedOn.length > 0
|
|
138
|
+
? result("onboard", "approval", "setup", [...evidence, ...turnedOn])
|
|
139
|
+
: result("onboard", "machine", null, evidence);
|
|
140
|
+
if (registration === "agent")
|
|
141
|
+
return agentGetsIn(registrationEvidence);
|
|
142
|
+
if (registration === "claimable") {
|
|
143
|
+
return result("onboard", "approval", "setup", [...registrationEvidence, ...turnedOn]);
|
|
144
|
+
}
|
|
145
|
+
// The agent's own OAuth client gets it in once the server takes it to consent (the shape's A).
|
|
146
|
+
if (chain?.client && authority.outcome === "approval")
|
|
147
|
+
return agentGetsIn(chainEvidence);
|
|
148
|
+
// No path for the agent: what the probes found, or the listing itself when they sent nothing.
|
|
149
|
+
const evidence = [
|
|
150
|
+
...requested,
|
|
151
|
+
...pagesOf(({ role }) => role === "registration"),
|
|
152
|
+
...chainEvidence,
|
|
153
|
+
...specEvidence,
|
|
154
|
+
];
|
|
155
|
+
return result("onboard", "workaround", "setup", evidence.length > 0 ? evidence : declaration());
|
|
156
|
+
})();
|
|
157
|
+
const pay = (() => {
|
|
158
|
+
if (answer === "offer")
|
|
159
|
+
return result("pay", "machine", null, requested);
|
|
160
|
+
if (covered?.payment)
|
|
161
|
+
return result("pay", "machine", null, specEvidence);
|
|
162
|
+
const manifests = (run) => target === null
|
|
163
|
+
? []
|
|
164
|
+
: run.discovery.facts.filter((found) => found.fact === "payment_manifest" &&
|
|
165
|
+
ownDomain(found.descriptor_url, run.discovery.domains) &&
|
|
166
|
+
agentReadinessNamesEndpoint(found.value, target));
|
|
167
|
+
const run = discoveryFor((candidate) => manifests(candidate).length > 0);
|
|
168
|
+
const named = run ? manifests(run) : [];
|
|
169
|
+
if (named.length > 0 && run) {
|
|
170
|
+
return result("pay", "machine", null, named.map(({ attempt_digest }) => ({
|
|
171
|
+
kind: "discovery",
|
|
172
|
+
run_digest: run.run_digest,
|
|
173
|
+
attempt_digest,
|
|
174
|
+
})));
|
|
175
|
+
}
|
|
176
|
+
if (anonymous) {
|
|
177
|
+
return result("pay", "not_applicable", null, answer === null ? specEvidence : requested);
|
|
178
|
+
}
|
|
179
|
+
if (input.checkoutRuledOut)
|
|
180
|
+
return result("pay", "not_applicable", null, declaration());
|
|
181
|
+
// An unanswered read could have held a manifest that names the endpoint.
|
|
182
|
+
if (!run)
|
|
183
|
+
return notAssessed("pay");
|
|
184
|
+
// A dead or invented page sets nothing: the checkout counts once it is found.
|
|
185
|
+
const checkout = pagesOf(({ role, found }) => role === "checkout" && found);
|
|
186
|
+
return checkout.length > 0
|
|
187
|
+
? result("pay", "workaround", "setup", checkout)
|
|
188
|
+
: notAssessed("pay");
|
|
189
|
+
})();
|
|
190
|
+
const pages = answeredRuns.filter((run) => run.probes.find(({ operation_id }) => operation_id === id)?.request?.answer === "page");
|
|
191
|
+
const pageRuns = answer === "page" ? reproducing(pages, input.policy.reproduction) : [];
|
|
192
|
+
const pageReproduced = pageRuns.length > 0;
|
|
193
|
+
const job = (() => {
|
|
194
|
+
// The job's tool answered, or asked for the authority it needs: whatever an anonymous list
|
|
195
|
+
// says, the server performs it for an agent with that authority.
|
|
196
|
+
if (answer === "success" || answer === "offer" || answer === "authority") {
|
|
197
|
+
return jobResult("reachable", requested);
|
|
198
|
+
}
|
|
199
|
+
// The server lists its tools, the job's is not among them, and its call is refused with an error.
|
|
200
|
+
if (operation.kind === "mcp" && probe.tool_listed === false && answer === "error") {
|
|
201
|
+
return jobResult("unavailable", requested);
|
|
202
|
+
}
|
|
203
|
+
if (refusal)
|
|
204
|
+
return jobResult("reachable", requested);
|
|
205
|
+
if (covered)
|
|
206
|
+
return jobResult("reachable", specEvidence);
|
|
207
|
+
if (answer === null)
|
|
208
|
+
return jobResult("unavailable", specEvidence);
|
|
209
|
+
if (pageReproduced) {
|
|
210
|
+
return jobResult("unavailable", pageRuns.flatMap((run) => exchangeEvidence(run, (entry) => entry.call_id === id && entry.purpose === "probe_operation")));
|
|
211
|
+
}
|
|
212
|
+
return NOT_ASSESSED_JOB;
|
|
213
|
+
})();
|
|
214
|
+
const confirm = (() => {
|
|
215
|
+
const read = refusal ? probe.request : answer === "success" ? probe.malformed : null;
|
|
216
|
+
if (read) {
|
|
217
|
+
const typedRefusal = (read.answer === "offer" || read.answer === "authority" || read.answer === "error") &&
|
|
218
|
+
read.typed === true;
|
|
219
|
+
const evidence = refusal ? requested : [...requested, ...malformed];
|
|
220
|
+
return typedRefusal
|
|
221
|
+
? result("confirm", "machine", null, evidence)
|
|
222
|
+
: result("confirm", "workaround", "recurring", evidence);
|
|
223
|
+
}
|
|
224
|
+
// A page or a spec operation: the errors the spec declares for the job.
|
|
225
|
+
return covered?.typed_errors
|
|
226
|
+
? result("confirm", "machine", null, specEvidence)
|
|
227
|
+
: result("confirm", "workaround", "recurring", [...requested, ...specEvidence]);
|
|
228
|
+
})();
|
|
229
|
+
const sustain = (() => {
|
|
230
|
+
if (authority.outcome === "not_applicable") {
|
|
231
|
+
return result("sustain", "not_applicable", null, authority.evidence);
|
|
232
|
+
}
|
|
233
|
+
// Either alternative keeps the agent's authority alive: its key operations, or the chain's
|
|
234
|
+
// grants, whoever obtained the first token.
|
|
235
|
+
if (covered?.key_operations)
|
|
236
|
+
return result("sustain", "machine", null, specEvidence);
|
|
237
|
+
if (chain !== null) {
|
|
238
|
+
if (!chain.renewal)
|
|
239
|
+
return result("sustain", "workaround", "recurring", metadata);
|
|
240
|
+
return chain.revocation
|
|
241
|
+
? result("sustain", "machine", null, metadata)
|
|
242
|
+
: result("sustain", "workaround", "setup", metadata);
|
|
243
|
+
}
|
|
244
|
+
return result("sustain", "workaround", "setup", [...authority.evidence, ...specEvidence]);
|
|
245
|
+
})();
|
|
246
|
+
return {
|
|
247
|
+
steps: [discover, authority, onboard, pay, confirm, sustain],
|
|
248
|
+
job,
|
|
249
|
+
reproducible: answer === "page" && !pageReproduced && covered === null,
|
|
250
|
+
discoveryRun,
|
|
251
|
+
verifiedRun: null,
|
|
252
|
+
standingRuns: stretches.flatMap(({ runs: found }) => found),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The verified path of one binding: what performing the job with its authority
|
|
257
|
+
* establishes (north star §5.6, precedence). Only the vendor's own runs can
|
|
258
|
+
* lower a letter: a refusal of authority a vendor run proved valid counts when
|
|
259
|
+
* the vendor's latest run holds it and its earlier runs reproduce it (the
|
|
260
|
+
* job's refusal makes the job `unavailable`, Authority's makes that step
|
|
261
|
+
* `gated`, and a 402 is a balance a person tops up: Pay's recurring
|
|
262
|
+
* `workaround`), and so do failed assertions reproduced the same way (the job
|
|
263
|
+
* `unavailable`). Otherwise the latest run that did the job decides while it
|
|
264
|
+
* is fresh; a failing run from anyone else never lowers a letter.
|
|
18
265
|
*/
|
|
19
266
|
export function evaluatePath(input) {
|
|
20
|
-
const runs =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error("A path needs at least one run.");
|
|
267
|
+
const runs = oldestFirst(input.runs);
|
|
268
|
+
const latestRun = runs.at(-1);
|
|
269
|
+
if (!latestRun)
|
|
270
|
+
throw new Error("A verified path needs at least one run.");
|
|
25
271
|
const { job, binding } = input;
|
|
272
|
+
const { reproduction: rule } = input.policy;
|
|
26
273
|
const succeeded = (run) => run.assertions.length === job.assertions.length &&
|
|
27
274
|
job.assertions.every(({ name }) => run.assertions.some(({ assertion, holds }) => assertion === name && holds));
|
|
28
|
-
const
|
|
275
|
+
const fresh = (run) => Date.parse(input.asOf) - Date.parse(run.finished_at) <= input.policy.fresh_for_days * DAY_MS;
|
|
276
|
+
const vendorRuns = runs.filter(({ label }) => label === "vendor_run_verified");
|
|
277
|
+
const holds = (found) => found?.latest === true && reproduced(found.runs, rule);
|
|
278
|
+
// Runs whose job completed and an assertion did not hold: the API exists but does not do the job.
|
|
279
|
+
const failedRun = (run) => run.assertions.length > 0 && run.assertions.some(({ holds: held }) => !held);
|
|
280
|
+
const vendorLatest = vendorRuns.at(-1);
|
|
281
|
+
const jobRefusals = refusals(vendorRuns, "job", rule.statuses);
|
|
282
|
+
const payRefusals = refusals(vendorRuns, "job", [402]);
|
|
283
|
+
const authorityRefusals = refusals(vendorRuns, "delegation", rule.statuses);
|
|
284
|
+
const failures = vendorRuns.filter(failedRun);
|
|
285
|
+
const failing = vendorLatest !== undefined && failedRun(vendorLatest);
|
|
286
|
+
const lowered = holds(jobRefusals) ||
|
|
287
|
+
holds(payRefusals) ||
|
|
288
|
+
holds(authorityRefusals) ||
|
|
289
|
+
(failing && reproduced(failures, rule));
|
|
290
|
+
// The run that decides: the vendor's, when its runs reproduce a failure; else the latest fresh success.
|
|
291
|
+
const current = (lowered ? vendorLatest : [...runs].reverse().find((run) => succeeded(run) && fresh(run))) ??
|
|
292
|
+
vendorLatest ??
|
|
293
|
+
latestRun;
|
|
294
|
+
const ok = !lowered && succeeded(current) && fresh(current);
|
|
29
295
|
const jobEvidence = exchangeEvidence(current, (entry) => entry.purpose === "job");
|
|
30
|
-
const
|
|
31
|
-
const jobRefusal = ok ? null : refusal("job");
|
|
32
|
-
const jobStep = (() => {
|
|
296
|
+
const jobOutcome = (() => {
|
|
33
297
|
if (ok)
|
|
34
|
-
return
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
298
|
+
return jobResult("verified", jobEvidence);
|
|
299
|
+
if (holds(jobRefusals))
|
|
300
|
+
return jobResult("unavailable", jobRefusals.evidence);
|
|
301
|
+
if (failing && reproduced(failures, rule)) {
|
|
302
|
+
return jobResult("unavailable", failures.flatMap((run) => exchangeEvidence(run, (entry) => entry.purpose === "job")));
|
|
37
303
|
}
|
|
38
|
-
return
|
|
304
|
+
return NOT_ASSESSED_JOB;
|
|
39
305
|
})();
|
|
40
306
|
const usedRoles = [...new Set(binding.calls.flatMap(({ credentials }) => credentials))];
|
|
41
|
-
const
|
|
307
|
+
const authorityStep = (() => {
|
|
42
308
|
if (binding.delegation.kind === "none") {
|
|
43
309
|
return ok && usedRoles.length === 0
|
|
44
|
-
? result("
|
|
45
|
-
: notAssessed("
|
|
310
|
+
? result("authority", "not_applicable", null, jobEvidence)
|
|
311
|
+
: notAssessed("authority");
|
|
312
|
+
}
|
|
313
|
+
if (holds(authorityRefusals)) {
|
|
314
|
+
return result("authority", "gated", null, authorityRefusals.evidence);
|
|
46
315
|
}
|
|
47
|
-
const delegationRefusal = refusal("delegation");
|
|
48
|
-
if (delegationRefusal)
|
|
49
|
-
return result("delegation", "blocked", null, delegationRefusal.evidence);
|
|
50
316
|
// The authority the job actually carried: the handles on its own requests.
|
|
51
317
|
const carried = new Set(current.exchanges.flatMap((entry) => entry.purpose === "job" && entry.role === "request"
|
|
52
318
|
? entry.record.request.credentials.map(({ handle_digest }) => handle_digest)
|
|
53
319
|
: []));
|
|
54
320
|
const held = current.credentials.filter(({ handle_digest }) => carried.has(handle_digest));
|
|
55
321
|
if (usedRoles.length === 0 || held.length === 0 || held.length !== carried.size) {
|
|
56
|
-
return notAssessed("
|
|
322
|
+
return notAssessed("authority");
|
|
57
323
|
}
|
|
58
324
|
const evidence = held.map((source) => ({
|
|
59
325
|
kind: "credential",
|
|
@@ -63,19 +329,27 @@ export function evaluatePath(input) {
|
|
|
63
329
|
const issuedBy = new Set(held.flatMap(({ source }) => (source.kind === "issued" ? [source.exchange_digest] : [])));
|
|
64
330
|
evidence.push(...exchangeEvidence(current, (entry) => issuedBy.has(entry.record.exchange_digest)));
|
|
65
331
|
if (held.some(({ source }) => source.kind === "entered")) {
|
|
66
|
-
return result("
|
|
332
|
+
return result("authority", "workaround", "setup", evidence);
|
|
67
333
|
}
|
|
68
334
|
return binding.delegation.kind === "oauth"
|
|
69
|
-
? result("
|
|
70
|
-
: result("
|
|
335
|
+
? result("authority", "approval", "setup", evidence)
|
|
336
|
+
: result("authority", "machine", null, evidence);
|
|
71
337
|
})();
|
|
72
338
|
const payStep = (() => {
|
|
73
|
-
if (
|
|
74
|
-
return result("pay", "
|
|
75
|
-
|
|
76
|
-
|
|
339
|
+
if (holds(payRefusals)) {
|
|
340
|
+
return result("pay", "workaround", "recurring", payRefusals.evidence);
|
|
341
|
+
}
|
|
342
|
+
if (!ok)
|
|
343
|
+
return notAssessed("pay");
|
|
344
|
+
switch (binding.payment.kind) {
|
|
345
|
+
case "none":
|
|
346
|
+
return result("pay", "not_applicable", null, jobEvidence);
|
|
347
|
+
case "per_call":
|
|
348
|
+
return result("pay", "machine", null, jobEvidence);
|
|
349
|
+
case "prepaid_balance":
|
|
350
|
+
// A balance a person set up: the run leaves Pay to the probes.
|
|
351
|
+
return notAssessed("pay");
|
|
77
352
|
}
|
|
78
|
-
return human(current, "pay", ok ? jobEvidence : []) ?? notAssessed("pay");
|
|
79
353
|
})();
|
|
80
354
|
const confirmStep = (() => {
|
|
81
355
|
if (!ok || current.error_probe === null)
|
|
@@ -88,61 +362,107 @@ export function evaluatePath(input) {
|
|
|
88
362
|
? result("confirm", "machine", null, evidence)
|
|
89
363
|
: result("confirm", "workaround", "recurring", evidence);
|
|
90
364
|
})();
|
|
91
|
-
const discoverStep = (() => {
|
|
92
|
-
const { attempts, facts } = current.discovery;
|
|
93
|
-
if (attempts.length === 0)
|
|
94
|
-
return notAssessed("discover");
|
|
95
|
-
const used = binding.calls.flatMap(({ endpoint_id }) => {
|
|
96
|
-
const uri = input.endpoints.get(endpoint_id);
|
|
97
|
-
return uri ? [uri] : [];
|
|
98
|
-
});
|
|
99
|
-
const named = facts.filter((fact) => NAMING_FACTS.has(fact.fact) && used.some((uri) => namesEndpoint(fact.value, uri)));
|
|
100
|
-
const evidence = (named.length > 0 ? named.map(({ attempt_digest }) => attempt_digest) : attempts).map((attempt) => ({
|
|
101
|
-
kind: "discovery",
|
|
102
|
-
run_digest: current.run_digest,
|
|
103
|
-
attempt_digest: attempt,
|
|
104
|
-
}));
|
|
105
|
-
return named.length > 0
|
|
106
|
-
? result("discover", "machine", null, evidence)
|
|
107
|
-
: result("discover", "workaround", "setup", evidence);
|
|
108
|
-
})();
|
|
109
365
|
const sustainStep = (() => {
|
|
110
366
|
if (binding.delegation.kind === "none") {
|
|
111
367
|
return ok ? result("sustain", "not_applicable", null, jobEvidence) : notAssessed("sustain");
|
|
112
368
|
}
|
|
113
369
|
const { rotation, revocation } = binding.sustain;
|
|
114
|
-
const handoffs = current.handoffs.flatMap((handoff, index) => handoff.step === "sustain" ? [index] : []);
|
|
115
|
-
const evidence = [
|
|
116
|
-
...exchangeEvidence(current, (entry) => entry.purpose.startsWith("sustain_")),
|
|
117
|
-
...handoffs.map((handoff) => ({ kind: "handoff", run_digest: current.run_digest, handoff })),
|
|
118
|
-
];
|
|
119
370
|
const witnessed = upkeepWitnesses(current);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
-
const parts = [
|
|
130
|
-
part(rotation.kind, "sustain_rotation"),
|
|
131
|
-
part(revocation.kind, "sustain_revocation"),
|
|
132
|
-
];
|
|
133
|
-
if (parts.includes("not_assessed"))
|
|
134
|
-
return notAssessed("sustain");
|
|
135
|
-
return parts.includes("workaround")
|
|
136
|
-
? result("sustain", "workaround", "recurring", evidence)
|
|
137
|
-
: result("sustain", "machine", null, evidence);
|
|
371
|
+
// Only an exchange witnesses upkeep: a manual or unwitnessed part leaves Sustain to the probes.
|
|
372
|
+
const machine = (rotation.kind === "refresh" || rotation.kind === "mint") &&
|
|
373
|
+
witnessed.rotation &&
|
|
374
|
+
revocation.kind === "call" &&
|
|
375
|
+
witnessed.revocation;
|
|
376
|
+
return machine
|
|
377
|
+
? result("sustain", "machine", null, exchangeEvidence(current, (entry) => entry.purpose.startsWith("sustain_")))
|
|
378
|
+
: notAssessed("sustain");
|
|
138
379
|
})();
|
|
380
|
+
const pending = (found) => found?.latest === true && !reproduced(found.runs, rule);
|
|
381
|
+
return {
|
|
382
|
+
// Discover and Onboard are the probes' alone: a verified run reads no descriptor and registers nothing.
|
|
383
|
+
steps: [
|
|
384
|
+
notAssessed("discover"),
|
|
385
|
+
authorityStep,
|
|
386
|
+
notAssessed("onboard"),
|
|
387
|
+
payStep,
|
|
388
|
+
confirmStep,
|
|
389
|
+
sustainStep,
|
|
390
|
+
],
|
|
391
|
+
job: jobOutcome,
|
|
392
|
+
reproducible: pending(jobRefusals) ||
|
|
393
|
+
pending(payRefusals) ||
|
|
394
|
+
pending(authorityRefusals) ||
|
|
395
|
+
(failing && !reproduced(failures, rule)),
|
|
396
|
+
// A verified run reads no descriptor.
|
|
397
|
+
discoveryRun: null,
|
|
398
|
+
verifiedRun: current,
|
|
399
|
+
standingRuns: [],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Verified outcomes replace probed ones for the steps the verified run
|
|
404
|
+
* assessed, and for the job (north star §5.6, precedence); a step it left
|
|
405
|
+
* unassessed keeps its probed outcome.
|
|
406
|
+
*/
|
|
407
|
+
export function combinePaths(probed, verified) {
|
|
139
408
|
return {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
409
|
+
steps: probed.steps.map((step, index) => {
|
|
410
|
+
const replacement = verified.steps[index];
|
|
411
|
+
if (replacement?.step !== step.step)
|
|
412
|
+
throw new Error("Paths list steps in path order.");
|
|
413
|
+
return replacement.outcome === "not_assessed" ? step : replacement;
|
|
414
|
+
}),
|
|
415
|
+
job: verified.job.outcome === "not_assessed" ? probed.job : verified.job,
|
|
416
|
+
reproducible: probed.reproducible || verified.reproducible,
|
|
417
|
+
discoveryRun: probed.discoveryRun,
|
|
418
|
+
verifiedRun: verified.verifiedRun,
|
|
419
|
+
standingRuns: probed.standingRuns,
|
|
144
420
|
};
|
|
145
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Discover (north star §5.6): `machine` when the agent finds the interface
|
|
424
|
+
* from the vendor's own domains and a machine-readable description covers the
|
|
425
|
+
* job. Found: a descriptor fetched on an Entity domain names the job's
|
|
426
|
+
* endpoint, or a document written for agents names its origin (the route).
|
|
427
|
+
* Covered: the named spec covers the job, the MCP server lists the job's tool
|
|
428
|
+
* (or lists tools only after authority), or that document for agents. A spec
|
|
429
|
+
* operation has no endpoint to name: its spec, covering the job, is the
|
|
430
|
+
* machine-readable interface. Otherwise a setup workaround: a person tells the
|
|
431
|
+
* agent where the interface is or how to call it.
|
|
432
|
+
*/
|
|
433
|
+
function discoverStep(run, current, operation, target, covered, specEvidence,
|
|
434
|
+
/** An MCP server listed the job's tool, or lists tools only to the agent it authorizes. */
|
|
435
|
+
toolDescribed) {
|
|
436
|
+
const { attempts, facts, domains } = run.discovery;
|
|
437
|
+
const discovery = (attempt) => ({
|
|
438
|
+
kind: "discovery",
|
|
439
|
+
run_digest: run.run_digest,
|
|
440
|
+
attempt_digest: attempt,
|
|
441
|
+
});
|
|
442
|
+
if (operation.kind === "spec") {
|
|
443
|
+
return covered
|
|
444
|
+
? result("discover", "machine", null, specEvidence)
|
|
445
|
+
: result("discover", "workaround", "setup", specEvidence);
|
|
446
|
+
}
|
|
447
|
+
const named = target
|
|
448
|
+
? facts.filter((fact) => agentReadinessFactNamesEndpoint(fact, target) && ownDomain(fact.descriptor_url, domains))
|
|
449
|
+
: [];
|
|
450
|
+
const described = covered !== null || toolDescribed;
|
|
451
|
+
const routed = named.some(({ fact }) => fact === "documentation_route");
|
|
452
|
+
if (routed || (named.length > 0 && described)) {
|
|
453
|
+
return result("discover", "machine", null, [
|
|
454
|
+
...named.map(({ attempt_digest }) => discovery(attempt_digest)),
|
|
455
|
+
...specEvidence,
|
|
456
|
+
]);
|
|
457
|
+
}
|
|
458
|
+
const evidence = [...attempts.map(discovery), ...specEvidence];
|
|
459
|
+
return result("discover", "workaround", "setup", evidence.length > 0 ? evidence : [{ kind: "declaration", run_digest: current.run_digest }]);
|
|
460
|
+
}
|
|
461
|
+
/** Whether a URL is on one of the Entity's domains, or a subdomain of one. */
|
|
462
|
+
function ownDomain(url, domains) {
|
|
463
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
464
|
+
return domains.some((domain) => host === domain || host.endsWith(`.${domain}`));
|
|
465
|
+
}
|
|
146
466
|
/** Accepted maintenance requests are not effects: correlate their readbacks. */
|
|
147
467
|
function upkeepWitnesses(run) {
|
|
148
468
|
const at = (purpose) => run.exchanges.findIndex((entry) => entry.purpose === purpose && entry.role === "request");
|
|
@@ -158,9 +478,6 @@ function upkeepWitnesses(run) {
|
|
|
158
478
|
const revoke = run.exchanges[revokeAt];
|
|
159
479
|
const refused = run.exchanges[refusedAt];
|
|
160
480
|
const control = run.exchanges[controlAt];
|
|
161
|
-
const succeeded = (entry) => entry?.record.outcome === "responded" &&
|
|
162
|
-
entry.record.response.status >= 200 &&
|
|
163
|
-
entry.record.response.status <= 299;
|
|
164
481
|
const handles = (entry) => entry?.record.request.credentials.map(({ handle_digest }) => handle_digest) ?? [];
|
|
165
482
|
const sameHandles = (a, b) => {
|
|
166
483
|
const left = handles(a);
|
|
@@ -207,85 +524,79 @@ function upkeepWitnesses(run) {
|
|
|
207
524
|
handles(control).every((handle) => !handles(refused).includes(handle));
|
|
208
525
|
return { rotation: renewed, revocation: revoked };
|
|
209
526
|
}
|
|
210
|
-
/** A listing no binding has run: every step not assessed, so no letter. */
|
|
211
|
-
export function unexercisedPath() {
|
|
212
|
-
return AGENT_READINESS_STEPS.map(notAssessed);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Every human step the runner recorded at `step`. A workaround dominates
|
|
216
|
-
* approval, and recurrence is decided among handoffs of that outcome: a
|
|
217
|
-
* recurring consent must not turn a one-time mechanical setup into recurring
|
|
218
|
-
* work. All handoffs remain evidence, independent of their input order.
|
|
219
|
-
*/
|
|
220
|
-
function human(run, step, evidence, inferred) {
|
|
221
|
-
const handoffs = run.handoffs.flatMap((handoff, index) => handoff.step === step ? [{ handoff, index }] : []);
|
|
222
|
-
if (handoffs.length === 0)
|
|
223
|
-
return null;
|
|
224
|
-
const kind = inferred?.outcome === "workaround" ||
|
|
225
|
-
handoffs.some(({ handoff }) => handoff.kind === "workaround")
|
|
226
|
-
? "workaround"
|
|
227
|
-
: "approval";
|
|
228
|
-
const timing = (inferred?.outcome === kind && inferred.timing === "recurring") ||
|
|
229
|
-
handoffs.some(({ handoff }) => handoff.kind === kind && handoff.timing === "recurring")
|
|
230
|
-
? "recurring"
|
|
231
|
-
: "setup";
|
|
232
|
-
return result(step, kind, timing, [
|
|
233
|
-
...evidence,
|
|
234
|
-
...handoffs
|
|
235
|
-
.filter(({ index }) => !evidence.some((entry) => entry.kind === "handoff" &&
|
|
236
|
-
entry.run_digest === run.run_digest &&
|
|
237
|
-
entry.handoff === index))
|
|
238
|
-
.map(({ index }) => ({ kind: "handoff", run_digest: run.run_digest, handoff: index })),
|
|
239
|
-
]);
|
|
240
|
-
}
|
|
241
527
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
528
|
+
* Each run's refusal at `purpose` with one of `statuses`, holding authority
|
|
529
|
+
* the same run proved valid: at least one credential, each issued in the run
|
|
530
|
+
* by an exchange that succeeded or carried by another exchange that succeeded.
|
|
531
|
+
* A request with no credential proves nothing about authority, so it is never
|
|
532
|
+
* a refusal (missing authority is residue). Null when no run refused.
|
|
246
533
|
*/
|
|
247
|
-
function
|
|
248
|
-
const
|
|
534
|
+
function refusals(runs, purpose, statuses) {
|
|
535
|
+
const found = runs.flatMap((run) => {
|
|
249
536
|
const refused = run.exchanges.find((entry) => entry.purpose === purpose &&
|
|
250
537
|
entry.record.outcome === "responded" &&
|
|
251
|
-
|
|
538
|
+
statuses.includes(entry.record.response.status));
|
|
252
539
|
if (refused?.record.outcome !== "responded")
|
|
253
|
-
return
|
|
540
|
+
return [];
|
|
254
541
|
const credentials = refused.record.request.credentials.map(({ handle_digest }) => handle_digest);
|
|
255
|
-
const succeeded = (entry) => entry.record.outcome === "responded" &&
|
|
256
|
-
entry.record.response.status >= 200 &&
|
|
257
|
-
entry.record.response.status <= 299;
|
|
258
|
-
// Authority is proved valid by the run itself: the service issued it in this
|
|
259
|
-
// run, or it succeeded on another exchange of this run.
|
|
260
542
|
const proved = (handle) => run.credentials.some(({ handle_digest, source }) => handle_digest === handle &&
|
|
261
543
|
source.kind === "issued" &&
|
|
262
544
|
run.exchanges.some((entry) => entry.record.exchange_digest === source.exchange_digest && succeeded(entry))) ||
|
|
263
545
|
run.exchanges.some((entry) => entry !== refused &&
|
|
264
546
|
succeeded(entry) &&
|
|
265
547
|
entry.record.request.credentials.some(({ handle_digest }) => handle_digest === handle));
|
|
266
|
-
|
|
267
|
-
|
|
548
|
+
return credentials.length > 0 && credentials.every(proved)
|
|
549
|
+
? [{ run, entry: refused, status: refused.record.response.status }]
|
|
550
|
+
: [];
|
|
268
551
|
});
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
552
|
+
const last = found.at(-1);
|
|
553
|
+
if (!last)
|
|
271
554
|
return null;
|
|
272
|
-
const same =
|
|
273
|
-
const first = same[0];
|
|
274
|
-
if (!first ||
|
|
275
|
-
same.length < rule.attempts ||
|
|
276
|
-
Date.parse(latest.run.finished_at) - Date.parse(first.run.finished_at) <
|
|
277
|
-
rule.minimum_interval_seconds * 1_000) {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
555
|
+
const same = found.filter(({ status }) => status === last.status);
|
|
280
556
|
return {
|
|
281
|
-
|
|
557
|
+
runs: same.map(({ run }) => run),
|
|
282
558
|
evidence: same.slice(-MAXIMUM_EVIDENCE).map(({ run, entry }) => ({
|
|
283
559
|
kind: "exchange",
|
|
284
560
|
run_digest: run.run_digest,
|
|
285
561
|
exchange_digest: entry.record.exchange_digest,
|
|
286
562
|
})),
|
|
563
|
+
latest: last.run === runs.at(-1),
|
|
287
564
|
};
|
|
288
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* The latest of `found` that reproduce a finding, the fewest that do, or none:
|
|
568
|
+
* a revision cites only these (north star §5.4).
|
|
569
|
+
*/
|
|
570
|
+
function reproducing(found, rule) {
|
|
571
|
+
for (let start = found.length - rule.attempts; start >= 0; start -= 1) {
|
|
572
|
+
const stretch = found.slice(start);
|
|
573
|
+
if (reproduced(stretch, rule))
|
|
574
|
+
return stretch;
|
|
575
|
+
}
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* A finding reproduced as the policy asks: in the latest run and in earlier
|
|
580
|
+
* ones, at least `attempts` runs whose first and last are the interval apart.
|
|
581
|
+
*/
|
|
582
|
+
function reproduced(found, rule) {
|
|
583
|
+
const first = found[0];
|
|
584
|
+
const last = found.at(-1);
|
|
585
|
+
return (first !== undefined &&
|
|
586
|
+
last !== undefined &&
|
|
587
|
+
found.length >= rule.attempts &&
|
|
588
|
+
Date.parse(last.finished_at) - Date.parse(first.finished_at) >=
|
|
589
|
+
rule.minimum_interval_seconds * 1_000);
|
|
590
|
+
}
|
|
591
|
+
function succeeded(entry) {
|
|
592
|
+
return (entry?.record.outcome === "responded" &&
|
|
593
|
+
entry.record.response.status >= 200 &&
|
|
594
|
+
entry.record.response.status <= 299);
|
|
595
|
+
}
|
|
596
|
+
function oldestFirst(runs) {
|
|
597
|
+
return [...runs].sort((left, right) => compareInstants(left.finished_at, right.finished_at) ||
|
|
598
|
+
(left.run_digest < right.run_digest ? -1 : 1));
|
|
599
|
+
}
|
|
289
600
|
function exchangeEvidence(run, include) {
|
|
290
601
|
return run.exchanges.filter(include).map((entry) => ({
|
|
291
602
|
kind: "exchange",
|
|
@@ -293,16 +604,19 @@ function exchangeEvidence(run, include) {
|
|
|
293
604
|
exchange_digest: entry.record.exchange_digest,
|
|
294
605
|
}));
|
|
295
606
|
}
|
|
296
|
-
/** A descriptor names an endpoint when it gives its exact URI or a base the URI sits under. */
|
|
297
|
-
function namesEndpoint(value, uri) {
|
|
298
|
-
const base = value.replace(/\/+$/u, "");
|
|
299
|
-
const endpoint = uri.replace(/\/+$/u, "");
|
|
300
|
-
return endpoint === base || endpoint.startsWith(`${base}/`);
|
|
301
|
-
}
|
|
302
607
|
function result(step, outcome, timing, evidence) {
|
|
303
608
|
return { step, outcome, timing, evidence: evidence.slice(0, MAXIMUM_EVIDENCE) };
|
|
304
609
|
}
|
|
305
|
-
function notAssessed(step) {
|
|
610
|
+
export function notAssessed(step) {
|
|
306
611
|
return { step, outcome: "not_assessed", timing: null, evidence: [] };
|
|
307
612
|
}
|
|
613
|
+
/** The job not yet settled by any run. */
|
|
614
|
+
export const NOT_ASSESSED_JOB = { outcome: "not_assessed", evidence: [] };
|
|
615
|
+
function jobResult(outcome, evidence) {
|
|
616
|
+
return { outcome, evidence: evidence.slice(0, MAXIMUM_EVIDENCE) };
|
|
617
|
+
}
|
|
618
|
+
/** The job unavailable on the run's declaration revision alone: it names no interface for the job. */
|
|
619
|
+
export function declaredUnavailable(run) {
|
|
620
|
+
return jobResult("unavailable", [{ kind: "declaration", run_digest: run.run_digest }]);
|
|
621
|
+
}
|
|
308
622
|
//# sourceMappingURL=path.js.map
|