@knitli/openapi-mcp 0.0.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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +362 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +249 -0
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +132 -0
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +7 -0
- package/dist/conformance/fixtures.d.ts +24 -0
- package/dist/conformance/fixtures.js +119 -0
- package/dist/conformance/index.d.ts +2 -0
- package/dist/conformance/index.js +2 -0
- package/dist/conformance/runtime-suite.d.ts +31 -0
- package/dist/conformance/runtime-suite.js +343 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/load.d.ts +31 -0
- package/dist/load.js +20 -0
- package/dist/operations.d.ts +27 -0
- package/dist/operations.js +231 -0
- package/dist/permissions.d.ts +31 -0
- package/dist/permissions.js +89 -0
- package/dist/release/compile-release.d.ts +33 -0
- package/dist/release/compile-release.js +1620 -0
- package/dist/release/load-v4.d.ts +29 -0
- package/dist/release/load-v4.js +297 -0
- package/dist/release/manifest-builder.d.ts +19 -0
- package/dist/release/manifest-builder.js +28 -0
- package/dist/release/publish.d.ts +45 -0
- package/dist/release/publish.js +346 -0
- package/dist/release/reference-map.d.ts +53 -0
- package/dist/release/reference-map.js +347 -0
- package/dist/release/schema-v4.d.ts +5 -0
- package/dist/release/schema-v4.js +91 -0
- package/dist/runtime/action-permit.d.ts +11 -0
- package/dist/runtime/action-permit.js +1 -0
- package/dist/runtime/classify.d.ts +16 -0
- package/dist/runtime/classify.js +279 -0
- package/dist/runtime/credential-binding.d.ts +3 -0
- package/dist/runtime/credential-binding.js +101 -0
- package/dist/runtime/digest.d.ts +8 -0
- package/dist/runtime/digest.js +80 -0
- package/dist/runtime/errors.d.ts +14 -0
- package/dist/runtime/errors.js +15 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +14 -0
- package/dist/runtime/manifest.d.ts +33 -0
- package/dist/runtime/manifest.js +489 -0
- package/dist/runtime/prepared-call.d.ts +39 -0
- package/dist/runtime/prepared-call.js +566 -0
- package/dist/runtime/references.d.ts +7 -0
- package/dist/runtime/references.js +197 -0
- package/dist/runtime/runtime.d.ts +25 -0
- package/dist/runtime/runtime.js +1272 -0
- package/dist/runtime/schema-keywords.d.ts +2 -0
- package/dist/runtime/schema-keywords.js +30 -0
- package/dist/runtime/schema-resolver.d.ts +10 -0
- package/dist/runtime/schema-resolver.js +237 -0
- package/dist/runtime/serialize.d.ts +12 -0
- package/dist/runtime/serialize.js +877 -0
- package/dist/runtime/store.d.ts +52 -0
- package/dist/runtime/store.js +582 -0
- package/dist/runtime/strict-json.d.ts +19 -0
- package/dist/runtime/strict-json.js +539 -0
- package/dist/runtime/types.d.ts +457 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/verify-record.d.ts +6 -0
- package/dist/runtime/verify-record.js +492 -0
- package/dist/runtime/versions.d.ts +63 -0
- package/dist/runtime/versions.js +86 -0
- package/dist/safety.d.ts +28 -0
- package/dist/safety.js +60 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +52 -0
- package/dist/schemas.d.ts +9 -0
- package/dist/schemas.js +16 -0
- package/dist/sign.d.ts +20 -0
- package/dist/sign.js +55 -0
- package/dist/sqlite/auth.d.ts +33 -0
- package/dist/sqlite/auth.js +973 -0
- package/dist/sqlite/catalog-store.d.ts +20 -0
- package/dist/sqlite/catalog-store.js +370 -0
- package/dist/sqlite/destination-guard.d.ts +24 -0
- package/dist/sqlite/destination-guard.js +140 -0
- package/dist/sqlite/generation-store.d.ts +12 -0
- package/dist/sqlite/generation-store.js +721 -0
- package/dist/sqlite/guarded-fetch.d.ts +24 -0
- package/dist/sqlite/guarded-fetch.js +626 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.js +5 -0
- package/dist/sqlite/oauth-resource.d.ts +2 -0
- package/dist/sqlite/oauth-resource.js +15 -0
- package/dist/sqlite/release-completion.d.ts +5 -0
- package/dist/sqlite/release-completion.js +80 -0
- package/dist/stdio/action-broker.d.ts +15 -0
- package/dist/stdio/action-broker.js +136 -0
- package/dist/stdio/authorizer.d.ts +30 -0
- package/dist/stdio/authorizer.js +484 -0
- package/dist/stdio/config.d.ts +77 -0
- package/dist/stdio/config.js +144 -0
- package/dist/stdio/exact-policy.d.ts +41 -0
- package/dist/stdio/exact-policy.js +361 -0
- package/dist/stdio/index.d.ts +6 -0
- package/dist/stdio/index.js +148 -0
- package/dist/stdio/render.d.ts +8 -0
- package/dist/stdio/render.js +253 -0
- package/dist/stdio/request-state.d.ts +18 -0
- package/dist/stdio/request-state.js +79 -0
- package/dist/stdio/search.d.ts +10 -0
- package/dist/stdio/search.js +76 -0
- package/dist/stdio/server.d.ts +29 -0
- package/dist/stdio/server.js +309 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
import { classifyOperation } from "./classify.js";
|
|
2
|
+
import { sha256 } from "./digest.js";
|
|
3
|
+
import { OpenApiMcpError } from "./errors.js";
|
|
4
|
+
import { authenticateManifest, commitAuthenticatedManifestAtState, } from "./manifest.js";
|
|
5
|
+
import { createPreparedCall, snapshotPaginationTokenState, verifyAndSnapshotPreparedCall, } from "./prepared-call.js";
|
|
6
|
+
import { decodeOperationRef, encodeOperationRef, parseTypedRecordId, } from "./references.js";
|
|
7
|
+
import { collectReferences, resolveSchemaClosure } from "./schema-resolver.js";
|
|
8
|
+
import { serializeArguments } from "./serialize.js";
|
|
9
|
+
import { canonicalJson } from "./strict-json.js";
|
|
10
|
+
import { verifyStoredRecord } from "./verify-record.js";
|
|
11
|
+
import { DEFAULT_RUNTIME_LIMITS, PREPARED_CALL_VERSION, resolveRuntimeLimits, } from "./versions.js";
|
|
12
|
+
function inputInvalid(message) {
|
|
13
|
+
return new OpenApiMcpError("INPUT_INVALID", message);
|
|
14
|
+
}
|
|
15
|
+
function upstreamUnavailable(message) {
|
|
16
|
+
return new OpenApiMcpError("UPSTREAM_ERROR", message, { retryable: true });
|
|
17
|
+
}
|
|
18
|
+
class GenerationStoreUnavailable extends OpenApiMcpError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("UPSTREAM_ERROR", "Generation state is unavailable", {
|
|
21
|
+
retryable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function manifestKey(catalogId, releaseId) {
|
|
26
|
+
return `${catalogId}\0${releaseId}`;
|
|
27
|
+
}
|
|
28
|
+
const maximumSearchReleases = 8;
|
|
29
|
+
const maximumInventorySchemaBatch = 128;
|
|
30
|
+
// State churn may require reproof, but one search may spend no more than the
|
|
31
|
+
// same eight-release compatibility envelope used for manifest authentication.
|
|
32
|
+
const maximumCompleteReleaseProofs = maximumSearchReleases;
|
|
33
|
+
function cappedProduct(left, right) {
|
|
34
|
+
return Math.min(Number.MAX_SAFE_INTEGER, left * right);
|
|
35
|
+
}
|
|
36
|
+
function completeReleaseByteLimit(limits) {
|
|
37
|
+
return (limits.maxReleaseInventoryBytes ??
|
|
38
|
+
DEFAULT_RUNTIME_LIMITS.maxReleaseInventoryBytes);
|
|
39
|
+
}
|
|
40
|
+
function createCompleteVerificationBudget(limits) {
|
|
41
|
+
const perReleaseBytes = completeReleaseByteLimit(limits);
|
|
42
|
+
// Record verification, one schema-document traversal, references, and
|
|
43
|
+
// bounded closure visits are charged independently.
|
|
44
|
+
const perReleaseWork = limits.maxManifestRecords * (limits.maxSchemaRefHops + 3);
|
|
45
|
+
return {
|
|
46
|
+
proofsRemaining: maximumCompleteReleaseProofs,
|
|
47
|
+
// All releases share one inventory envelope. Authentication may consider
|
|
48
|
+
// eight releases, but search never receives eight independent 128 MiB
|
|
49
|
+
// proof allocations.
|
|
50
|
+
bytesRemaining: perReleaseBytes,
|
|
51
|
+
workRemaining: cappedProduct(perReleaseWork, maximumSearchReleases),
|
|
52
|
+
storeCallsRemaining: cappedProduct(limits.maxManifestRecords * 2, maximumSearchReleases),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function chargeCompleteVerification(budget, charge) {
|
|
56
|
+
const proofs = charge.proofs ?? 0;
|
|
57
|
+
const bytes = charge.bytes ?? 0;
|
|
58
|
+
const work = charge.work ?? 0;
|
|
59
|
+
const storeCalls = charge.storeCalls ?? 0;
|
|
60
|
+
if (proofs > budget.proofsRemaining ||
|
|
61
|
+
bytes > budget.bytesRemaining ||
|
|
62
|
+
work > budget.workRemaining ||
|
|
63
|
+
storeCalls > budget.storeCallsRemaining) {
|
|
64
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Search admission verification budget exhausted");
|
|
65
|
+
}
|
|
66
|
+
budget.proofsRemaining -= proofs;
|
|
67
|
+
budget.bytesRemaining -= bytes;
|
|
68
|
+
budget.workRemaining -= work;
|
|
69
|
+
budget.storeCallsRemaining -= storeCalls;
|
|
70
|
+
}
|
|
71
|
+
function sameGenerationState(left, right) {
|
|
72
|
+
return (left === right ||
|
|
73
|
+
(left !== null &&
|
|
74
|
+
right !== null &&
|
|
75
|
+
left.revision === right.revision &&
|
|
76
|
+
left.highestGeneration === right.highestGeneration &&
|
|
77
|
+
left.highestManifestDigest === right.highestManifestDigest &&
|
|
78
|
+
left.activeGeneration === right.activeGeneration &&
|
|
79
|
+
left.activeManifestDigest === right.activeManifestDigest));
|
|
80
|
+
}
|
|
81
|
+
function snapshotRows(value, maximum) {
|
|
82
|
+
try {
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
throw new Error();
|
|
85
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
86
|
+
if (lengthDescriptor === undefined ||
|
|
87
|
+
!("value" in lengthDescriptor) ||
|
|
88
|
+
!Number.isSafeInteger(lengthDescriptor.value) ||
|
|
89
|
+
lengthDescriptor.value < 0 ||
|
|
90
|
+
lengthDescriptor.value > maximum)
|
|
91
|
+
throw new Error();
|
|
92
|
+
const length = lengthDescriptor.value;
|
|
93
|
+
const keys = Reflect.ownKeys(value);
|
|
94
|
+
if (keys.length !== length + 1 ||
|
|
95
|
+
keys.some((key) => typeof key !== "string" ||
|
|
96
|
+
(key !== "length" && !/^(0|[1-9][0-9]*)$/.test(key))))
|
|
97
|
+
throw new Error();
|
|
98
|
+
const snapshot = [];
|
|
99
|
+
for (let index = 0; index < length; index += 1) {
|
|
100
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
101
|
+
if (descriptor === undefined ||
|
|
102
|
+
!("value" in descriptor) ||
|
|
103
|
+
!descriptor.enumerable)
|
|
104
|
+
throw new Error();
|
|
105
|
+
snapshot.push(descriptor.value);
|
|
106
|
+
}
|
|
107
|
+
return snapshot;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory lookup result is invalid");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function verifyCompleteRelease(store, authenticated, limits, searchBudget) {
|
|
114
|
+
chargeCompleteVerification(searchBudget, { proofs: 1 });
|
|
115
|
+
// CatalogStore addresses immutable release identities but has no snapshot
|
|
116
|
+
// handle. Verify the complete observed release view before admission; later
|
|
117
|
+
// use-time reads remain independently verified because availability can
|
|
118
|
+
// still change after this pass.
|
|
119
|
+
const operationIds = [];
|
|
120
|
+
const schemaIds = [];
|
|
121
|
+
for (const id of Object.keys(authenticated.manifest.records).sort()) {
|
|
122
|
+
const parsed = parseTypedRecordId(id);
|
|
123
|
+
if (parsed.startsWith("operation:"))
|
|
124
|
+
operationIds.push(parsed);
|
|
125
|
+
else
|
|
126
|
+
schemaIds.push(parsed);
|
|
127
|
+
}
|
|
128
|
+
const encoder = new TextEncoder();
|
|
129
|
+
const maximumBytes = completeReleaseByteLimit(limits);
|
|
130
|
+
const maximumWork = limits.maxManifestRecords * (limits.maxSchemaRefHops + 2);
|
|
131
|
+
let aggregateBytes = 0;
|
|
132
|
+
let aggregateWork = 0;
|
|
133
|
+
const accountRecord = (record) => {
|
|
134
|
+
const bytes = encoder.encode(canonicalJson(record)).byteLength;
|
|
135
|
+
chargeCompleteVerification(searchBudget, { bytes });
|
|
136
|
+
aggregateBytes += bytes;
|
|
137
|
+
aggregateWork += 1;
|
|
138
|
+
if (aggregateBytes > maximumBytes || aggregateWork > maximumWork) {
|
|
139
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory verification exceeds its aggregate limit");
|
|
140
|
+
}
|
|
141
|
+
return bytes;
|
|
142
|
+
};
|
|
143
|
+
const operationRoots = [];
|
|
144
|
+
for (const id of operationIds) {
|
|
145
|
+
chargeCompleteVerification(searchBudget, { work: 1, storeCalls: 1 });
|
|
146
|
+
const row = await store.getOperation(authenticated.manifest.catalogId, authenticated.manifest.releaseId, id);
|
|
147
|
+
if (row === null) {
|
|
148
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory operation is missing");
|
|
149
|
+
}
|
|
150
|
+
const operation = await verifyStoredRecord(authenticated, row, limits);
|
|
151
|
+
if (!operation.id.startsWith("operation:") || operation.id !== id) {
|
|
152
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Release inventory operation identity does not match");
|
|
153
|
+
}
|
|
154
|
+
accountRecord(operation);
|
|
155
|
+
operationRoots.push([...new Set(operation.schemaIds)].sort());
|
|
156
|
+
}
|
|
157
|
+
const schemaBatchSize = Math.max(1, Math.min(maximumInventorySchemaBatch, Math.floor(limits.maxSchemaClosureBytes / limits.maxRecordBytes)));
|
|
158
|
+
const schemas = new Map();
|
|
159
|
+
let offset = 0;
|
|
160
|
+
while (offset < schemaIds.length) {
|
|
161
|
+
const ids = [];
|
|
162
|
+
let requestBytes = 0;
|
|
163
|
+
while (offset < schemaIds.length && ids.length < schemaBatchSize) {
|
|
164
|
+
const id = schemaIds[offset];
|
|
165
|
+
const idBytes = encoder.encode(id).byteLength;
|
|
166
|
+
if (idBytes > limits.maxSchemaClosureBytes ||
|
|
167
|
+
(ids.length > 0 &&
|
|
168
|
+
requestBytes + idBytes > limits.maxSchemaClosureBytes))
|
|
169
|
+
break;
|
|
170
|
+
ids.push(id);
|
|
171
|
+
requestBytes += idBytes;
|
|
172
|
+
offset += 1;
|
|
173
|
+
}
|
|
174
|
+
if (ids.length === 0) {
|
|
175
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory schema request exceeds its byte limit");
|
|
176
|
+
}
|
|
177
|
+
chargeCompleteVerification(searchBudget, {
|
|
178
|
+
work: ids.length,
|
|
179
|
+
storeCalls: 1,
|
|
180
|
+
});
|
|
181
|
+
const result = await store.getSchemas(authenticated.manifest.catalogId, authenticated.manifest.releaseId, ids);
|
|
182
|
+
const rows = snapshotRows(result, ids.length);
|
|
183
|
+
const verified = await Promise.all(rows.map((row) => verifyStoredRecord(authenticated, row, limits)));
|
|
184
|
+
const returned = new Set(verified.map((record) => record.id));
|
|
185
|
+
if (returned.size !== ids.length ||
|
|
186
|
+
verified.length !== ids.length ||
|
|
187
|
+
ids.some((id) => !returned.has(id)) ||
|
|
188
|
+
verified.some((record) => !record.id.startsWith("schema:"))) {
|
|
189
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory schema is missing or ambiguous");
|
|
190
|
+
}
|
|
191
|
+
for (const record of verified) {
|
|
192
|
+
const schema = record;
|
|
193
|
+
schemas.set(schema.id, {
|
|
194
|
+
record: schema,
|
|
195
|
+
bytes: accountRecord(schema),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const schemaReferences = new Map();
|
|
200
|
+
for (const [id, { record }] of schemas) {
|
|
201
|
+
const references = collectReferences(record.schema);
|
|
202
|
+
chargeCompleteVerification(searchBudget, {
|
|
203
|
+
work: references.length + 1,
|
|
204
|
+
});
|
|
205
|
+
schemaReferences.set(id, references);
|
|
206
|
+
aggregateWork += references.length;
|
|
207
|
+
if (aggregateWork > maximumWork ||
|
|
208
|
+
references.some((reference) => !schemas.has(reference))) {
|
|
209
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release schema graph is incomplete or exceeds its work limit");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const verifiedClosures = new Set();
|
|
213
|
+
for (const roots of operationRoots) {
|
|
214
|
+
const cacheKey = roots.join("\0");
|
|
215
|
+
if (verifiedClosures.has(cacheKey))
|
|
216
|
+
continue;
|
|
217
|
+
let frontier = roots;
|
|
218
|
+
const visited = new Set();
|
|
219
|
+
let closureBytes = 0;
|
|
220
|
+
let hop = 0;
|
|
221
|
+
while (frontier.length > 0) {
|
|
222
|
+
if (hop > limits.maxSchemaRefHops) {
|
|
223
|
+
throw new OpenApiMcpError("SCHEMA_RESOLUTION_LIMIT", "Schema reference hop limit exceeded");
|
|
224
|
+
}
|
|
225
|
+
const next = new Set();
|
|
226
|
+
for (const id of frontier) {
|
|
227
|
+
if (visited.has(id))
|
|
228
|
+
continue;
|
|
229
|
+
const schema = schemas.get(id);
|
|
230
|
+
if (schema === undefined) {
|
|
231
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Operation schema root is not in the verified inventory");
|
|
232
|
+
}
|
|
233
|
+
visited.add(id);
|
|
234
|
+
closureBytes += schema.bytes;
|
|
235
|
+
aggregateWork += 1;
|
|
236
|
+
chargeCompleteVerification(searchBudget, { work: 1 });
|
|
237
|
+
if (closureBytes > limits.maxSchemaClosureBytes ||
|
|
238
|
+
aggregateWork > maximumWork) {
|
|
239
|
+
throw new OpenApiMcpError("SCHEMA_RESOLUTION_LIMIT", "Schema closure exceeds its aggregate limit");
|
|
240
|
+
}
|
|
241
|
+
for (const reference of schemaReferences.get(id) ?? []) {
|
|
242
|
+
if (!visited.has(reference))
|
|
243
|
+
next.add(reference);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
frontier = [...next].sort();
|
|
247
|
+
hop += 1;
|
|
248
|
+
}
|
|
249
|
+
verifiedClosures.add(cacheKey);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const markdownCharacters = new Map([
|
|
253
|
+
["#", "#"],
|
|
254
|
+
["`", "ˋ"],
|
|
255
|
+
["*", "*"],
|
|
256
|
+
[">", "›"],
|
|
257
|
+
["<", "‹"],
|
|
258
|
+
["[", "【"],
|
|
259
|
+
["]", "】"],
|
|
260
|
+
]);
|
|
261
|
+
const unsafeUnicodeCategory = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
262
|
+
function safeText(value, maximum) {
|
|
263
|
+
let safe = "";
|
|
264
|
+
// Strip control, format, and Unicode line/paragraph separator code points.
|
|
265
|
+
// Iteration is by code point so supplementary-plane format controls cannot
|
|
266
|
+
// hide behind a surrogate pair.
|
|
267
|
+
for (const character of value) {
|
|
268
|
+
const replacement = unsafeUnicodeCategory.test(character)
|
|
269
|
+
? " "
|
|
270
|
+
: (markdownCharacters.get(character) ?? character);
|
|
271
|
+
if (safe.length + replacement.length > maximum)
|
|
272
|
+
break;
|
|
273
|
+
safe += replacement;
|
|
274
|
+
}
|
|
275
|
+
return safe;
|
|
276
|
+
}
|
|
277
|
+
function safeSummary(value) {
|
|
278
|
+
return value === null ? null : safeText(value, 600);
|
|
279
|
+
}
|
|
280
|
+
function inputOutline(operation) {
|
|
281
|
+
const outline = {
|
|
282
|
+
path: [],
|
|
283
|
+
query: [],
|
|
284
|
+
headers: [],
|
|
285
|
+
body: operation.requestBody?.content
|
|
286
|
+
.slice(0, 16)
|
|
287
|
+
.map((entry) => safeText(entry.mediaType, 128)) ?? [],
|
|
288
|
+
};
|
|
289
|
+
for (const parameter of operation.parameters) {
|
|
290
|
+
const item = {
|
|
291
|
+
name: safeText(parameter.name, 128),
|
|
292
|
+
required: parameter.required,
|
|
293
|
+
};
|
|
294
|
+
if (parameter.in === "path" && outline.path.length < 32)
|
|
295
|
+
outline.path.push(item);
|
|
296
|
+
if (parameter.in === "query" && outline.query.length < 32)
|
|
297
|
+
outline.query.push(item);
|
|
298
|
+
if (parameter.in === "header" && outline.headers.length < 32)
|
|
299
|
+
outline.headers.push(item);
|
|
300
|
+
}
|
|
301
|
+
return outline;
|
|
302
|
+
}
|
|
303
|
+
function safeJsonValue(value, state, depth = 0) {
|
|
304
|
+
if (state.nodes >= 64 || depth > 4)
|
|
305
|
+
return undefined;
|
|
306
|
+
state.nodes += 1;
|
|
307
|
+
if (typeof value === "string")
|
|
308
|
+
return safeText(value, 128);
|
|
309
|
+
if (value === null || typeof value === "boolean" || typeof value === "number")
|
|
310
|
+
return value;
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
const result = [];
|
|
313
|
+
for (const entry of value.slice(0, 32)) {
|
|
314
|
+
const safe = safeJsonValue(entry, state, depth + 1);
|
|
315
|
+
if (safe !== undefined)
|
|
316
|
+
result.push(safe);
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
const result = {};
|
|
321
|
+
for (const key of Object.keys(value).sort().slice(0, 32)) {
|
|
322
|
+
const safe = safeJsonValue(value[key], state, depth + 1);
|
|
323
|
+
if (safe !== undefined)
|
|
324
|
+
result[safeText(key, 64)] = safe;
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
function advisoryPermissions(advisory) {
|
|
329
|
+
const safe = {};
|
|
330
|
+
const state = { nodes: 0 };
|
|
331
|
+
for (const key of [
|
|
332
|
+
"permissionConfidence",
|
|
333
|
+
"permissionIds",
|
|
334
|
+
"permissions",
|
|
335
|
+
"privilegeLevel",
|
|
336
|
+
"requiredPermissions",
|
|
337
|
+
"scopes",
|
|
338
|
+
]) {
|
|
339
|
+
if (Object.hasOwn(advisory, key)) {
|
|
340
|
+
const value = safeJsonValue(advisory[key], state);
|
|
341
|
+
if (value !== undefined)
|
|
342
|
+
safe[key] = value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return safe;
|
|
346
|
+
}
|
|
347
|
+
function warning(error) {
|
|
348
|
+
if (error instanceof OpenApiMcpError) {
|
|
349
|
+
if (error.code === "RECORD_DIGEST_MISMATCH") {
|
|
350
|
+
return {
|
|
351
|
+
code: "RECORD_DIGEST_MISMATCH",
|
|
352
|
+
message: "Search candidate rejected",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (error.code === "RECORD_NOT_ADMITTED") {
|
|
356
|
+
return {
|
|
357
|
+
code: "RECORD_NOT_ADMITTED",
|
|
358
|
+
message: "Search candidate not admitted",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (error.code.startsWith("MANIFEST_")) {
|
|
362
|
+
return {
|
|
363
|
+
code: error.code,
|
|
364
|
+
message: "Search candidate manifest rejected",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
code: "UPSTREAM_ERROR",
|
|
370
|
+
message: "Search candidate unavailable",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function normalizeCandidate(value) {
|
|
374
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
375
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate shape is invalid");
|
|
376
|
+
}
|
|
377
|
+
const prototype = Object.getPrototypeOf(value);
|
|
378
|
+
const expected = ["catalogId", "operationId", "releaseId"];
|
|
379
|
+
const keys = Reflect.ownKeys(value).sort();
|
|
380
|
+
if ((prototype !== null && prototype !== Object.prototype) ||
|
|
381
|
+
keys.length !== expected.length ||
|
|
382
|
+
keys.some((key, index) => key !== expected[index])) {
|
|
383
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate shape is invalid");
|
|
384
|
+
}
|
|
385
|
+
const snapshot = Object.create(null);
|
|
386
|
+
for (const key of expected) {
|
|
387
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
388
|
+
if (descriptor === undefined ||
|
|
389
|
+
!("value" in descriptor) ||
|
|
390
|
+
!descriptor.enumerable ||
|
|
391
|
+
typeof descriptor.value !== "string") {
|
|
392
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate fields are invalid");
|
|
393
|
+
}
|
|
394
|
+
snapshot[key] = descriptor.value;
|
|
395
|
+
}
|
|
396
|
+
// The operation-ref codec owns the public identity grammar. Encoding a
|
|
397
|
+
// detached snapshot validates every segment before it can drive a lookup.
|
|
398
|
+
encodeOperationRef({
|
|
399
|
+
catalogId: snapshot.catalogId,
|
|
400
|
+
releaseId: snapshot.releaseId,
|
|
401
|
+
operationId: snapshot.operationId,
|
|
402
|
+
manifestDigest: "0".repeat(64),
|
|
403
|
+
});
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
catalogId: snapshot.catalogId,
|
|
406
|
+
releaseId: snapshot.releaseId,
|
|
407
|
+
operationId: snapshot.operationId,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function snapshotCandidates(value, maximum) {
|
|
411
|
+
try {
|
|
412
|
+
if (!Array.isArray(value))
|
|
413
|
+
throw new Error();
|
|
414
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
415
|
+
if (lengthDescriptor === undefined ||
|
|
416
|
+
!("value" in lengthDescriptor) ||
|
|
417
|
+
!Number.isSafeInteger(lengthDescriptor.value) ||
|
|
418
|
+
lengthDescriptor.value < 0 ||
|
|
419
|
+
lengthDescriptor.value > maximum)
|
|
420
|
+
throw new Error();
|
|
421
|
+
const length = lengthDescriptor.value;
|
|
422
|
+
const keys = Reflect.ownKeys(value);
|
|
423
|
+
if (keys.length !== length + 1 ||
|
|
424
|
+
keys.some((key) => typeof key !== "string" ||
|
|
425
|
+
(key !== "length" && !/^(0|[1-9][0-9]*)$/.test(key))))
|
|
426
|
+
throw new Error();
|
|
427
|
+
const snapshot = [];
|
|
428
|
+
for (let index = 0; index < length; index += 1) {
|
|
429
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
430
|
+
if (descriptor === undefined ||
|
|
431
|
+
!("value" in descriptor) ||
|
|
432
|
+
!descriptor.enumerable)
|
|
433
|
+
throw new Error();
|
|
434
|
+
snapshot.push(descriptor.value);
|
|
435
|
+
}
|
|
436
|
+
return snapshot;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate result is invalid");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function snapshotSearchInput(value) {
|
|
443
|
+
try {
|
|
444
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
445
|
+
throw new Error();
|
|
446
|
+
const prototype = Object.getPrototypeOf(value);
|
|
447
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
448
|
+
throw new Error();
|
|
449
|
+
const keys = Reflect.ownKeys(value).sort();
|
|
450
|
+
const allowed = ["api", "limit", "query"];
|
|
451
|
+
if (keys.some((key) => typeof key !== "string" || !allowed.includes(key)) ||
|
|
452
|
+
!keys.includes("query"))
|
|
453
|
+
throw new Error();
|
|
454
|
+
const snapshot = Object.create(null);
|
|
455
|
+
for (const key of keys) {
|
|
456
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
457
|
+
if (descriptor === undefined ||
|
|
458
|
+
!("value" in descriptor) ||
|
|
459
|
+
!descriptor.enumerable)
|
|
460
|
+
throw new Error();
|
|
461
|
+
snapshot[key] = descriptor.value;
|
|
462
|
+
}
|
|
463
|
+
return Object.freeze(snapshot);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
throw inputInvalid("Search input is invalid");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function validateSearchInput(value, limits) {
|
|
470
|
+
const input = snapshotSearchInput(value);
|
|
471
|
+
if (typeof input.query !== "string" ||
|
|
472
|
+
input.query.trim().length === 0 ||
|
|
473
|
+
input.query.length > 1024)
|
|
474
|
+
throw inputInvalid("Search input is invalid");
|
|
475
|
+
if (input.api !== undefined &&
|
|
476
|
+
(typeof input.api !== "string" ||
|
|
477
|
+
input.api.length === 0 ||
|
|
478
|
+
input.api.length > 128 ||
|
|
479
|
+
input.api === "." ||
|
|
480
|
+
input.api === ".." ||
|
|
481
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(input.api)))
|
|
482
|
+
throw inputInvalid("Search input is invalid");
|
|
483
|
+
const limit = input.limit ??
|
|
484
|
+
Math.min(limits.defaultSearchResults, limits.maxSearchResults);
|
|
485
|
+
if (!Number.isSafeInteger(limit) ||
|
|
486
|
+
limit < 1 ||
|
|
487
|
+
limit > limits.maxSearchResults) {
|
|
488
|
+
throw inputInvalid("Search input is invalid");
|
|
489
|
+
}
|
|
490
|
+
return { api: input.api, limit, query: input.query };
|
|
491
|
+
}
|
|
492
|
+
function snapshotPrepareInput(value) {
|
|
493
|
+
try {
|
|
494
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
495
|
+
throw new Error();
|
|
496
|
+
const prototype = Object.getPrototypeOf(value);
|
|
497
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
498
|
+
throw new Error();
|
|
499
|
+
const keys = Reflect.ownKeys(value).sort();
|
|
500
|
+
const expected = Object.hasOwn(value, "pageToken")
|
|
501
|
+
? ["arguments", "operation", "pageToken"]
|
|
502
|
+
: ["arguments", "operation"];
|
|
503
|
+
if (keys.length !== expected.length ||
|
|
504
|
+
keys.some((key, index) => key !== expected[index]))
|
|
505
|
+
throw new Error();
|
|
506
|
+
const snapshot = Object.create(null);
|
|
507
|
+
for (const key of expected) {
|
|
508
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
509
|
+
if (descriptor === undefined ||
|
|
510
|
+
!("value" in descriptor) ||
|
|
511
|
+
!descriptor.enumerable)
|
|
512
|
+
throw new Error();
|
|
513
|
+
snapshot[key] = descriptor.value;
|
|
514
|
+
}
|
|
515
|
+
if (Object.hasOwn(snapshot, "pageToken") &&
|
|
516
|
+
(typeof snapshot.pageToken !== "string" ||
|
|
517
|
+
snapshot.pageToken.length === 0 ||
|
|
518
|
+
snapshot.pageToken.length > 512 ||
|
|
519
|
+
Array.from(snapshot.pageToken).some((character) => character.charCodeAt(0) <= 32 || character.charCodeAt(0) === 127)))
|
|
520
|
+
throw inputInvalid("Pagination token is invalid");
|
|
521
|
+
if (typeof snapshot.operation !== "string")
|
|
522
|
+
throw new Error();
|
|
523
|
+
return Object.freeze({
|
|
524
|
+
operation: snapshot.operation,
|
|
525
|
+
arguments: snapshot.arguments,
|
|
526
|
+
pageToken: typeof snapshot.pageToken === "string" ? snapshot.pageToken : null,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (error instanceof OpenApiMcpError)
|
|
531
|
+
throw error;
|
|
532
|
+
throw inputInvalid("Prepare input is invalid");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function snapshotCredentialSlots(value) {
|
|
536
|
+
try {
|
|
537
|
+
if (!Array.isArray(value) || value.length > 64)
|
|
538
|
+
throw new Error();
|
|
539
|
+
const keys = Reflect.ownKeys(value);
|
|
540
|
+
if (keys.length !== value.length + 1 ||
|
|
541
|
+
keys.some((key) => key !== "length" &&
|
|
542
|
+
(typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(key))))
|
|
543
|
+
throw new Error();
|
|
544
|
+
const slots = [];
|
|
545
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
546
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
547
|
+
if (descriptor === undefined ||
|
|
548
|
+
!("value" in descriptor) ||
|
|
549
|
+
!descriptor.enumerable)
|
|
550
|
+
throw new Error();
|
|
551
|
+
const raw = descriptor.value;
|
|
552
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
553
|
+
throw new Error();
|
|
554
|
+
const prototype = Object.getPrototypeOf(raw);
|
|
555
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
556
|
+
throw new Error();
|
|
557
|
+
const slotKeys = Reflect.ownKeys(raw).sort();
|
|
558
|
+
if (slotKeys.length !== 2 ||
|
|
559
|
+
slotKeys[0] !== "name" ||
|
|
560
|
+
slotKeys[1] !== "placement")
|
|
561
|
+
throw new Error();
|
|
562
|
+
const nameDescriptor = Object.getOwnPropertyDescriptor(raw, "name");
|
|
563
|
+
const placementDescriptor = Object.getOwnPropertyDescriptor(raw, "placement");
|
|
564
|
+
if (nameDescriptor === undefined ||
|
|
565
|
+
!("value" in nameDescriptor) ||
|
|
566
|
+
!nameDescriptor.enumerable ||
|
|
567
|
+
typeof nameDescriptor.value !== "string" ||
|
|
568
|
+
nameDescriptor.value.length === 0 ||
|
|
569
|
+
nameDescriptor.value.length > 256 ||
|
|
570
|
+
new TextEncoder().encode(nameDescriptor.value).length > 256 ||
|
|
571
|
+
placementDescriptor === undefined ||
|
|
572
|
+
!("value" in placementDescriptor) ||
|
|
573
|
+
!placementDescriptor.enumerable ||
|
|
574
|
+
(placementDescriptor.value !== "header" &&
|
|
575
|
+
placementDescriptor.value !== "query"))
|
|
576
|
+
throw new Error();
|
|
577
|
+
slots.push(Object.freeze({
|
|
578
|
+
name: nameDescriptor.value,
|
|
579
|
+
placement: placementDescriptor.value,
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
slots.sort((left, right) => {
|
|
583
|
+
const leftKey = `${left.placement}\0${left.placement === "header" ? left.name.toLowerCase() : left.name}`;
|
|
584
|
+
const rightKey = `${right.placement}\0${right.placement === "header" ? right.name.toLowerCase() : right.name}`;
|
|
585
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
586
|
+
});
|
|
587
|
+
return Object.freeze(slots);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential slot policy is invalid");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function snapshotCredentialProfileBinding(value) {
|
|
594
|
+
try {
|
|
595
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
596
|
+
throw new Error();
|
|
597
|
+
const prototype = Object.getPrototypeOf(value);
|
|
598
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
599
|
+
throw new Error();
|
|
600
|
+
const keys = Reflect.ownKeys(value).sort();
|
|
601
|
+
if (keys.length !== 3 ||
|
|
602
|
+
keys[0] !== "profileDigest" ||
|
|
603
|
+
keys[1] !== "profileId" ||
|
|
604
|
+
keys[2] !== "slots")
|
|
605
|
+
throw new Error();
|
|
606
|
+
const profileId = Object.getOwnPropertyDescriptor(value, "profileId");
|
|
607
|
+
const profileDigest = Object.getOwnPropertyDescriptor(value, "profileDigest");
|
|
608
|
+
const slots = Object.getOwnPropertyDescriptor(value, "slots");
|
|
609
|
+
if (profileId === undefined ||
|
|
610
|
+
!("value" in profileId) ||
|
|
611
|
+
!profileId.enumerable ||
|
|
612
|
+
typeof profileId.value !== "string" ||
|
|
613
|
+
profileId.value.length === 0 ||
|
|
614
|
+
profileId.value.length > 128 ||
|
|
615
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profileId.value) ||
|
|
616
|
+
profileId.value === "." ||
|
|
617
|
+
profileId.value === ".." ||
|
|
618
|
+
profileDigest === undefined ||
|
|
619
|
+
!("value" in profileDigest) ||
|
|
620
|
+
!profileDigest.enumerable ||
|
|
621
|
+
typeof profileDigest.value !== "string" ||
|
|
622
|
+
!/^[0-9a-f]{64}$/.test(profileDigest.value) ||
|
|
623
|
+
slots === undefined ||
|
|
624
|
+
!("value" in slots) ||
|
|
625
|
+
!slots.enumerable)
|
|
626
|
+
throw new Error();
|
|
627
|
+
return Object.freeze({
|
|
628
|
+
profileId: profileId.value,
|
|
629
|
+
profileDigest: profileDigest.value,
|
|
630
|
+
slots: snapshotCredentialSlots(slots.value),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential binding policy is invalid");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function destinationDenied() {
|
|
638
|
+
return new OpenApiMcpError("DESTINATION_DENIED", "Operation destination is denied");
|
|
639
|
+
}
|
|
640
|
+
function timingSafeEqual(left, right) {
|
|
641
|
+
if (left.length !== right.length)
|
|
642
|
+
return false;
|
|
643
|
+
let difference = 0;
|
|
644
|
+
for (let index = 0; index < left.length; index += 1)
|
|
645
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
646
|
+
return difference === 0;
|
|
647
|
+
}
|
|
648
|
+
/** Internal host preflight. Proves all members without admitting any generation. */
|
|
649
|
+
export async function verifyExecutableRelease(options, catalogId, releaseId) {
|
|
650
|
+
const limits = resolveRuntimeLimits(options.limits);
|
|
651
|
+
const authenticated = await authenticateManifest(await options.store.getManifest(catalogId, releaseId), options.trust, limits);
|
|
652
|
+
if (authenticated.manifest.catalogId !== catalogId ||
|
|
653
|
+
authenticated.manifest.releaseId !== releaseId)
|
|
654
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED");
|
|
655
|
+
await verifyCompleteRelease(options.store, authenticated, limits, createCompleteVerificationBudget(limits));
|
|
656
|
+
return authenticated;
|
|
657
|
+
}
|
|
658
|
+
/** Internal executable ingress; manifest-only admission retains its public contract. */
|
|
659
|
+
export async function admitExecutableRelease(options, preflight) {
|
|
660
|
+
const limits = resolveRuntimeLimits(options.limits);
|
|
661
|
+
const budget = createCompleteVerificationBudget(limits);
|
|
662
|
+
const { catalogId, releaseId, issuer, generation } = preflight.manifest;
|
|
663
|
+
let initialTransition;
|
|
664
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
665
|
+
const state = await options.generations.get(catalogId, issuer);
|
|
666
|
+
const transition = state !== null && generation < state.highestGeneration
|
|
667
|
+
? "rollback"
|
|
668
|
+
: "normal";
|
|
669
|
+
if (initialTransition !== undefined && initialTransition !== transition)
|
|
670
|
+
throw new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT");
|
|
671
|
+
initialTransition ??= transition;
|
|
672
|
+
const current = await authenticateManifest(await options.store.getManifest(catalogId, releaseId), options.trust, limits);
|
|
673
|
+
if (current.manifestDigest !== preflight.manifestDigest)
|
|
674
|
+
throw new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT");
|
|
675
|
+
await verifyCompleteRelease(options.store, current, limits, budget);
|
|
676
|
+
if (!sameGenerationState(state, await options.generations.get(catalogId, issuer)))
|
|
677
|
+
continue;
|
|
678
|
+
const admitted = await commitAuthenticatedManifestAtState(current, options.trust, options.generations, state);
|
|
679
|
+
if (admitted !== null)
|
|
680
|
+
return admitted;
|
|
681
|
+
}
|
|
682
|
+
throw new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT");
|
|
683
|
+
}
|
|
684
|
+
/** Construct the portable verified runtime. */
|
|
685
|
+
export function createOpenApiRuntime(options) {
|
|
686
|
+
return createRuntime(options);
|
|
687
|
+
}
|
|
688
|
+
/** Internal adapter seam. Source calls draw from the same budget as record proof. */
|
|
689
|
+
export function createRuntimeWithCandidateLookup(options, lookup) {
|
|
690
|
+
return createRuntime(options, lookup);
|
|
691
|
+
}
|
|
692
|
+
function createRuntime(options, lookup) {
|
|
693
|
+
const limits = resolveRuntimeLimits(options.limits);
|
|
694
|
+
const committingGenerations = {
|
|
695
|
+
get(catalogId, issuer) {
|
|
696
|
+
return options.generations.get(catalogId, issuer);
|
|
697
|
+
},
|
|
698
|
+
async accept(catalogId, issuer, transition) {
|
|
699
|
+
try {
|
|
700
|
+
return await options.generations.accept(catalogId, issuer, transition);
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
throw new GenerationStoreUnavailable();
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
const requireActiveManifest = async (authenticated) => {
|
|
708
|
+
let activeState;
|
|
709
|
+
try {
|
|
710
|
+
const state = await options.generations.get(authenticated.manifest.catalogId, authenticated.manifest.issuer);
|
|
711
|
+
activeState =
|
|
712
|
+
state === null
|
|
713
|
+
? null
|
|
714
|
+
: {
|
|
715
|
+
generation: state.activeGeneration,
|
|
716
|
+
digest: state.activeManifestDigest,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
throw upstreamUnavailable("Generation state is unavailable");
|
|
721
|
+
}
|
|
722
|
+
if (activeState === null ||
|
|
723
|
+
activeState.generation !== authenticated.manifest.generation ||
|
|
724
|
+
!timingSafeEqual(typeof activeState.digest === "string" ? activeState.digest : "", authenticated.manifestDigest))
|
|
725
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Prepared call release is not active");
|
|
726
|
+
};
|
|
727
|
+
const prepare = async (inputValue, expectedSafety) => {
|
|
728
|
+
const input = snapshotPrepareInput(inputValue);
|
|
729
|
+
if (input.pageToken !== null &&
|
|
730
|
+
(expectedSafety !== "read" || !options.paginationTokenCodec))
|
|
731
|
+
throw inputInvalid("Pagination token is not permitted");
|
|
732
|
+
const reference = decodeOperationRef(input.operation);
|
|
733
|
+
let envelope;
|
|
734
|
+
try {
|
|
735
|
+
envelope = await options.store.getManifest(reference.catalogId, reference.releaseId);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
throw upstreamUnavailable("Manifest lookup is unavailable");
|
|
739
|
+
}
|
|
740
|
+
const authenticated = await authenticateManifest(envelope, options.trust, limits);
|
|
741
|
+
if (authenticated.manifest.catalogId !== reference.catalogId ||
|
|
742
|
+
authenticated.manifest.releaseId !== reference.releaseId ||
|
|
743
|
+
!timingSafeEqual(authenticated.manifestDigest, reference.manifestDigest))
|
|
744
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Operation reference release is not admitted");
|
|
745
|
+
await requireActiveManifest(authenticated);
|
|
746
|
+
const destinationPolicy = options.destinationPolicy;
|
|
747
|
+
if (destinationPolicy === undefined)
|
|
748
|
+
throw destinationDenied();
|
|
749
|
+
const credentialBindingResolver = options.credentialBinding;
|
|
750
|
+
if (credentialBindingResolver === undefined)
|
|
751
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential binding policy is unavailable");
|
|
752
|
+
let storedOperation;
|
|
753
|
+
try {
|
|
754
|
+
storedOperation = await options.store.getOperation(reference.catalogId, reference.releaseId, reference.operationId);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
throw upstreamUnavailable("Operation lookup is unavailable");
|
|
758
|
+
}
|
|
759
|
+
if (storedOperation === null)
|
|
760
|
+
throw new OpenApiMcpError("OPERATION_NOT_FOUND", "Referenced operation was not found");
|
|
761
|
+
const operation = await verifyStoredRecord(authenticated, storedOperation, limits);
|
|
762
|
+
if (operation.id !== reference.operationId)
|
|
763
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Operation identity does not match its reference");
|
|
764
|
+
const operationDigest = authenticated.manifest.records[operation.id];
|
|
765
|
+
const schemas = await resolveSchemaClosure(options.store, authenticated, operation.schemaIds, limits);
|
|
766
|
+
let allowed = false;
|
|
767
|
+
try {
|
|
768
|
+
allowed = await destinationPolicy.allows(operation.origin);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
throw destinationDenied();
|
|
772
|
+
}
|
|
773
|
+
if (allowed !== true)
|
|
774
|
+
throw destinationDenied();
|
|
775
|
+
const slotContext = Object.freeze({
|
|
776
|
+
catalogId: reference.catalogId,
|
|
777
|
+
releaseId: reference.releaseId,
|
|
778
|
+
operationId: operation.id,
|
|
779
|
+
operationDigest,
|
|
780
|
+
manifestDigest: authenticated.manifestDigest,
|
|
781
|
+
method: operation.method,
|
|
782
|
+
origin: operation.origin,
|
|
783
|
+
});
|
|
784
|
+
let bindingResult;
|
|
785
|
+
try {
|
|
786
|
+
bindingResult = await credentialBindingResolver.resolve(slotContext);
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential binding policy failed");
|
|
790
|
+
}
|
|
791
|
+
const credentialBinding = snapshotCredentialProfileBinding(bindingResult);
|
|
792
|
+
const credentialSlots = credentialBinding.slots;
|
|
793
|
+
const serialized = serializeArguments(operation, schemas, input.arguments, {
|
|
794
|
+
limits,
|
|
795
|
+
reservedCredentialSlots: credentialSlots,
|
|
796
|
+
});
|
|
797
|
+
const operationClassification = classifyOperation(operation, [...schemas.values()], serialized.normalizedArguments);
|
|
798
|
+
if (operationClassification.safety !== expectedSafety)
|
|
799
|
+
throw new OpenApiMcpError("TOOL_SAFETY_MISMATCH", expectedSafety === "read"
|
|
800
|
+
? "Operation requires the action tool"
|
|
801
|
+
: "Operation requires the read tool");
|
|
802
|
+
const reservedSlotsDigest = await sha256("knitli.openapi-mcp.credential-slots.v1", credentialSlots);
|
|
803
|
+
let prepared = await createPreparedCall({
|
|
804
|
+
version: PREPARED_CALL_VERSION,
|
|
805
|
+
catalogId: reference.catalogId,
|
|
806
|
+
releaseId: reference.releaseId,
|
|
807
|
+
operationId: operation.id,
|
|
808
|
+
operationDigest,
|
|
809
|
+
manifestDigest: authenticated.manifestDigest,
|
|
810
|
+
credentialProfileId: credentialBinding.profileId,
|
|
811
|
+
credentialProfileDigest: credentialBinding.profileDigest,
|
|
812
|
+
reservedSlotsDigest,
|
|
813
|
+
method: operation.method,
|
|
814
|
+
origin: operation.origin,
|
|
815
|
+
relativeUrl: serialized.relativeUrl,
|
|
816
|
+
pageToken: null,
|
|
817
|
+
headers: serialized.headers,
|
|
818
|
+
body: serialized.body,
|
|
819
|
+
normalizedArguments: serialized.normalizedArguments,
|
|
820
|
+
safety: operationClassification.safety,
|
|
821
|
+
actionKind: operationClassification.actionKind,
|
|
822
|
+
cardinality: operationClassification.cardinality,
|
|
823
|
+
});
|
|
824
|
+
if (input.pageToken !== null) {
|
|
825
|
+
let token;
|
|
826
|
+
try {
|
|
827
|
+
if (!options.paginationTokenCodec)
|
|
828
|
+
throw inputInvalid("Pagination codec is unavailable");
|
|
829
|
+
token = snapshotPaginationTokenState(await options.paginationTokenCodec.decode(input.pageToken), limits);
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
if (error instanceof OpenApiMcpError &&
|
|
833
|
+
error.code === "PAGINATION_LIMIT_EXCEEDED")
|
|
834
|
+
throw error;
|
|
835
|
+
throw inputInvalid("Pagination token is invalid");
|
|
836
|
+
}
|
|
837
|
+
if (token.catalogId !== prepared.catalogId ||
|
|
838
|
+
token.releaseId !== prepared.releaseId ||
|
|
839
|
+
token.operationId !== prepared.operationId ||
|
|
840
|
+
token.origin !== prepared.origin ||
|
|
841
|
+
!timingSafeEqual(token.manifestDigest, prepared.manifestDigest) ||
|
|
842
|
+
!timingSafeEqual(token.inputDigest, prepared.inputDigest))
|
|
843
|
+
throw inputInvalid("Pagination token does not match the call");
|
|
844
|
+
const nextUrl = new URL(token.nextRelativeUrl, prepared.origin);
|
|
845
|
+
for (const slot of credentialSlots) {
|
|
846
|
+
if (slot.placement === "query" &&
|
|
847
|
+
[...nextUrl.searchParams.keys()].some((key) => key.toLowerCase() === slot.name.toLowerCase()))
|
|
848
|
+
throw inputInvalid("Continuation URL contains a reserved credential slot");
|
|
849
|
+
}
|
|
850
|
+
const { inputDigest: _inputDigest, preparedCallDigest: _preparedCallDigest, ...payload } = prepared;
|
|
851
|
+
prepared = await createPreparedCall({
|
|
852
|
+
...payload,
|
|
853
|
+
relativeUrl: token.nextRelativeUrl,
|
|
854
|
+
pageToken: input.pageToken,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
await requireActiveManifest(authenticated);
|
|
858
|
+
return prepared;
|
|
859
|
+
};
|
|
860
|
+
const runtime = {
|
|
861
|
+
async search(input) {
|
|
862
|
+
const query = validateSearchInput(input, limits);
|
|
863
|
+
const completeVerificationBudget = createCompleteVerificationBudget(limits);
|
|
864
|
+
const emptyBytes = new TextEncoder().encode(canonicalJson({ operations: [], warnings: [] })).length;
|
|
865
|
+
if (emptyBytes > limits.maxResponseBytes) {
|
|
866
|
+
throw new OpenApiMcpError("RESPONSE_LIMIT_EXCEEDED", "Search response limit is too small");
|
|
867
|
+
}
|
|
868
|
+
const candidateLimit = Math.min(query.limit * 3, 150);
|
|
869
|
+
let candidateResult;
|
|
870
|
+
let candidateLookupLimited = false;
|
|
871
|
+
try {
|
|
872
|
+
const input = { ...query, limit: candidateLimit };
|
|
873
|
+
if (lookup) {
|
|
874
|
+
const found = await lookup(input, () => {
|
|
875
|
+
try {
|
|
876
|
+
chargeCompleteVerification(completeVerificationBudget, {
|
|
877
|
+
storeCalls: 1,
|
|
878
|
+
work: 1,
|
|
879
|
+
});
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
candidateResult = found.candidates;
|
|
887
|
+
candidateLookupLimited = found.limited;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
chargeCompleteVerification(completeVerificationBudget, {
|
|
891
|
+
storeCalls: 1,
|
|
892
|
+
work: 1,
|
|
893
|
+
});
|
|
894
|
+
candidateResult = await options.store.searchCandidates(input);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
throw upstreamUnavailable("Search candidate lookup is unavailable");
|
|
899
|
+
}
|
|
900
|
+
const candidates = snapshotCandidates(candidateResult, candidateLimit);
|
|
901
|
+
const operations = [];
|
|
902
|
+
const warnings = [];
|
|
903
|
+
const seen = new Set();
|
|
904
|
+
const manifests = new Map();
|
|
905
|
+
const failedManifests = new Set();
|
|
906
|
+
let releaseAttempts = 0;
|
|
907
|
+
const authenticateCandidateRelease = async (catalogId, releaseId) => {
|
|
908
|
+
const key = manifestKey(catalogId, releaseId);
|
|
909
|
+
const cached = manifests.get(key);
|
|
910
|
+
if (cached !== undefined)
|
|
911
|
+
return cached;
|
|
912
|
+
if (failedManifests.has(key)) {
|
|
913
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Search candidate manifest was already rejected");
|
|
914
|
+
}
|
|
915
|
+
if (releaseAttempts >= maximumSearchReleases) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
releaseAttempts += 1;
|
|
919
|
+
try {
|
|
920
|
+
const envelope = await options.store.getManifest(catalogId, releaseId);
|
|
921
|
+
const authenticated = await authenticateManifest(envelope, options.trust, limits);
|
|
922
|
+
if (authenticated.manifest.catalogId !== catalogId ||
|
|
923
|
+
authenticated.manifest.releaseId !== releaseId) {
|
|
924
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Search candidate release identity does not match its manifest");
|
|
925
|
+
}
|
|
926
|
+
const entry = { authenticated };
|
|
927
|
+
manifests.set(key, entry);
|
|
928
|
+
return entry;
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
failedManifests.add(key);
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const responseFits = (items, candidateWarnings) => new TextEncoder().encode(canonicalJson({
|
|
936
|
+
operations: [...items],
|
|
937
|
+
warnings: [...candidateWarnings],
|
|
938
|
+
})).length <= limits.maxResponseBytes;
|
|
939
|
+
const limitWarning = {
|
|
940
|
+
code: "RESPONSE_LIMIT_EXCEEDED",
|
|
941
|
+
message: "Search response limit reached",
|
|
942
|
+
};
|
|
943
|
+
const addWarning = (candidateWarning) => {
|
|
944
|
+
if (!responseFits([], [...warnings, candidateWarning, limitWarning]))
|
|
945
|
+
return false;
|
|
946
|
+
warnings.push(candidateWarning);
|
|
947
|
+
return true;
|
|
948
|
+
};
|
|
949
|
+
let responseLimited = false;
|
|
950
|
+
let admissionLimited = candidateLookupLimited;
|
|
951
|
+
for (const [rank, rawCandidate] of candidates.entries()) {
|
|
952
|
+
let candidate;
|
|
953
|
+
try {
|
|
954
|
+
candidate = normalizeCandidate(rawCandidate);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
if (!addWarning(warning(error)))
|
|
958
|
+
responseLimited = true;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (query.api !== undefined &&
|
|
962
|
+
!candidate.operationId.startsWith(`operation:${query.api}:`)) {
|
|
963
|
+
if (!addWarning(warning(new OpenApiMcpError("RECORD_DIGEST_MISMATCH"))))
|
|
964
|
+
responseLimited = true;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const key = manifestKey(candidate.catalogId, candidate.releaseId);
|
|
968
|
+
const identity = `${key}\0${candidate.operationId}`;
|
|
969
|
+
if (seen.has(identity))
|
|
970
|
+
continue;
|
|
971
|
+
seen.add(identity);
|
|
972
|
+
try {
|
|
973
|
+
const entry = await authenticateCandidateRelease(candidate.catalogId, candidate.releaseId);
|
|
974
|
+
if (entry === null) {
|
|
975
|
+
admissionLimited = true;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const row = await options.store.getOperation(candidate.catalogId, candidate.releaseId, candidate.operationId);
|
|
979
|
+
if (row === null) {
|
|
980
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Search candidate record is missing");
|
|
981
|
+
}
|
|
982
|
+
const operation = await verifyStoredRecord(entry.authenticated, row, limits);
|
|
983
|
+
if (operation.id !== candidate.operationId) {
|
|
984
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate identity does not match its record");
|
|
985
|
+
}
|
|
986
|
+
if (query.api !== undefined && operation.api !== query.api) {
|
|
987
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Search candidate API does not match its filter");
|
|
988
|
+
}
|
|
989
|
+
const runtimeClassification = classifyOperation(operation);
|
|
990
|
+
const retained = {
|
|
991
|
+
admission: {
|
|
992
|
+
catalogId: entry.authenticated.manifest.catalogId,
|
|
993
|
+
releaseId: entry.authenticated.manifest.releaseId,
|
|
994
|
+
issuer: entry.authenticated.manifest.issuer,
|
|
995
|
+
generation: entry.authenticated.manifest.generation,
|
|
996
|
+
digest: entry.authenticated.manifestDigest,
|
|
997
|
+
},
|
|
998
|
+
id: `${candidate.catalogId}\0${candidate.releaseId}\0${operation.id}`,
|
|
999
|
+
rank,
|
|
1000
|
+
item: {
|
|
1001
|
+
operation: encodeOperationRef({
|
|
1002
|
+
catalogId: candidate.catalogId,
|
|
1003
|
+
releaseId: candidate.releaseId,
|
|
1004
|
+
operationId: operation.id,
|
|
1005
|
+
manifestDigest: entry.authenticated.manifestDigest,
|
|
1006
|
+
}),
|
|
1007
|
+
summary: safeSummary(operation.summary),
|
|
1008
|
+
inputOutline: inputOutline(operation),
|
|
1009
|
+
...runtimeClassification,
|
|
1010
|
+
deprecated: operation.deprecated,
|
|
1011
|
+
advisory: advisoryPermissions(operation.advisory),
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
operations.push(retained);
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
if (!addWarning(warning(error)))
|
|
1018
|
+
responseLimited = true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (admissionLimited) {
|
|
1022
|
+
if (!addWarning({
|
|
1023
|
+
code: "RECORD_NOT_ADMITTED",
|
|
1024
|
+
message: "Search release admission limit reached",
|
|
1025
|
+
}))
|
|
1026
|
+
responseLimited = true;
|
|
1027
|
+
}
|
|
1028
|
+
const stagedOperations = operations.splice(0);
|
|
1029
|
+
const warnedOperations = new Set();
|
|
1030
|
+
const warnOperations = (candidates, error) => {
|
|
1031
|
+
for (const candidate of candidates) {
|
|
1032
|
+
if (warnedOperations.has(candidate.id))
|
|
1033
|
+
continue;
|
|
1034
|
+
warnedOperations.add(candidate.id);
|
|
1035
|
+
if (!addWarning(warning(error)))
|
|
1036
|
+
responseLimited = true;
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
const groups = new Map();
|
|
1040
|
+
for (const staged of stagedOperations) {
|
|
1041
|
+
const groupKey = `${staged.admission.catalogId}\0${staged.admission.issuer}`;
|
|
1042
|
+
const group = groups.get(groupKey);
|
|
1043
|
+
if (group === undefined)
|
|
1044
|
+
groups.set(groupKey, [staged]);
|
|
1045
|
+
else
|
|
1046
|
+
group.push(staged);
|
|
1047
|
+
}
|
|
1048
|
+
for (const group of groups.values()) {
|
|
1049
|
+
const byGeneration = new Map();
|
|
1050
|
+
for (const staged of group) {
|
|
1051
|
+
let releases = byGeneration.get(staged.admission.generation);
|
|
1052
|
+
if (releases === undefined) {
|
|
1053
|
+
releases = new Map();
|
|
1054
|
+
byGeneration.set(staged.admission.generation, releases);
|
|
1055
|
+
}
|
|
1056
|
+
const release = releases.get(staged.admission.digest);
|
|
1057
|
+
if (release === undefined)
|
|
1058
|
+
releases.set(staged.admission.digest, [staged]);
|
|
1059
|
+
else
|
|
1060
|
+
release.push(staged);
|
|
1061
|
+
}
|
|
1062
|
+
let selected;
|
|
1063
|
+
const generations = [...byGeneration.keys()].sort((left, right) => right - left);
|
|
1064
|
+
let selectionSettled = false;
|
|
1065
|
+
const initialTransitionKinds = new Map();
|
|
1066
|
+
for (let selectionAttempt = 0; selectionAttempt < 32; selectionAttempt += 1) {
|
|
1067
|
+
let capturedState;
|
|
1068
|
+
try {
|
|
1069
|
+
capturedState = await options.generations.get(group[0].admission.catalogId, group[0].admission.issuer);
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
throw upstreamUnavailable("Generation state is unavailable");
|
|
1073
|
+
}
|
|
1074
|
+
const attemptWarnings = [];
|
|
1075
|
+
let restartSelection = false;
|
|
1076
|
+
for (const generation of generations) {
|
|
1077
|
+
const releases = byGeneration.get(generation);
|
|
1078
|
+
if (releases === undefined)
|
|
1079
|
+
continue;
|
|
1080
|
+
let alreadyActive = false;
|
|
1081
|
+
let candidates;
|
|
1082
|
+
if (releases.size !== 1) {
|
|
1083
|
+
const activeDigest = capturedState?.activeGeneration === generation
|
|
1084
|
+
? capturedState.activeManifestDigest
|
|
1085
|
+
: undefined;
|
|
1086
|
+
candidates =
|
|
1087
|
+
activeDigest === undefined
|
|
1088
|
+
? undefined
|
|
1089
|
+
: releases.get(activeDigest);
|
|
1090
|
+
alreadyActive = candidates !== undefined;
|
|
1091
|
+
if (candidates === undefined) {
|
|
1092
|
+
for (const conflicting of releases.values())
|
|
1093
|
+
attemptWarnings.push({
|
|
1094
|
+
candidates: conflicting,
|
|
1095
|
+
error: new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT"),
|
|
1096
|
+
});
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
candidates = releases.values().next().value;
|
|
1102
|
+
}
|
|
1103
|
+
if (candidates === undefined || candidates.length === 0)
|
|
1104
|
+
continue;
|
|
1105
|
+
const admission = candidates[0].admission;
|
|
1106
|
+
const key = manifestKey(admission.catalogId, admission.releaseId);
|
|
1107
|
+
const entry = manifests.get(key);
|
|
1108
|
+
if (entry === undefined)
|
|
1109
|
+
continue;
|
|
1110
|
+
try {
|
|
1111
|
+
if (alreadyActive) {
|
|
1112
|
+
await verifyCompleteRelease(options.store, entry.authenticated, limits, completeVerificationBudget);
|
|
1113
|
+
let currentState;
|
|
1114
|
+
try {
|
|
1115
|
+
currentState = await options.generations.get(admission.catalogId, admission.issuer);
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
throw new GenerationStoreUnavailable();
|
|
1119
|
+
}
|
|
1120
|
+
if (!sameGenerationState(capturedState, currentState)) {
|
|
1121
|
+
restartSelection = true;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
await verifyCompleteRelease(options.store, entry.authenticated, limits, completeVerificationBudget);
|
|
1127
|
+
const transitionKind = capturedState !== null &&
|
|
1128
|
+
admission.generation < capturedState.highestGeneration
|
|
1129
|
+
? "rollback"
|
|
1130
|
+
: "normal";
|
|
1131
|
+
const initialTransitionKind = initialTransitionKinds.get(admission.digest);
|
|
1132
|
+
if (initialTransitionKind === undefined) {
|
|
1133
|
+
initialTransitionKinds.set(admission.digest, transitionKind);
|
|
1134
|
+
}
|
|
1135
|
+
else if (initialTransitionKind !== transitionKind) {
|
|
1136
|
+
throw new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT", "Generation state changed the selected transition kind");
|
|
1137
|
+
}
|
|
1138
|
+
const admitted = await commitAuthenticatedManifestAtState(entry.authenticated, options.trust, committingGenerations, capturedState);
|
|
1139
|
+
if (admitted === null) {
|
|
1140
|
+
restartSelection = true;
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
selected = candidates;
|
|
1145
|
+
operations.push(...candidates);
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
if (error instanceof GenerationStoreUnavailable)
|
|
1150
|
+
throw error;
|
|
1151
|
+
failedManifests.add(key);
|
|
1152
|
+
attemptWarnings.push({ candidates, error });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (restartSelection) {
|
|
1156
|
+
selected = undefined;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (selected === undefined) {
|
|
1160
|
+
let currentState;
|
|
1161
|
+
try {
|
|
1162
|
+
currentState = await options.generations.get(group[0].admission.catalogId, group[0].admission.issuer);
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
throw upstreamUnavailable("Generation state is unavailable");
|
|
1166
|
+
}
|
|
1167
|
+
if (!sameGenerationState(capturedState, currentState))
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
for (const pending of attemptWarnings)
|
|
1171
|
+
warnOperations(pending.candidates, pending.error);
|
|
1172
|
+
selectionSettled = true;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
if (!selectionSettled) {
|
|
1176
|
+
warnOperations(group, new OpenApiMcpError("MANIFEST_GENERATION_CONFLICT", "Generation state changed too many times; retry search"));
|
|
1177
|
+
}
|
|
1178
|
+
const selectedSet = new Set(selected ?? []);
|
|
1179
|
+
for (const staged of group) {
|
|
1180
|
+
if (selectedSet.has(staged) || warnedOperations.has(staged.id))
|
|
1181
|
+
continue;
|
|
1182
|
+
warnOperations([staged], new OpenApiMcpError("RECORD_NOT_ADMITTED"));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const finalStates = new Map();
|
|
1186
|
+
for (const { admission } of operations) {
|
|
1187
|
+
const key = `${admission.catalogId}\0${admission.issuer}`;
|
|
1188
|
+
if (finalStates.has(key))
|
|
1189
|
+
continue;
|
|
1190
|
+
try {
|
|
1191
|
+
const state = await options.generations.get(admission.catalogId, admission.issuer);
|
|
1192
|
+
finalStates.set(key, state === null
|
|
1193
|
+
? null
|
|
1194
|
+
: {
|
|
1195
|
+
activeGeneration: state.activeGeneration,
|
|
1196
|
+
activeManifestDigest: state.activeManifestDigest,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
throw upstreamUnavailable("Generation state is unavailable");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
for (let index = operations.length - 1; index >= 0; index -= 1) {
|
|
1204
|
+
const admission = operations[index].admission;
|
|
1205
|
+
const state = finalStates.get(`${admission.catalogId}\0${admission.issuer}`);
|
|
1206
|
+
if (state === undefined ||
|
|
1207
|
+
state === null ||
|
|
1208
|
+
state.activeGeneration !== admission.generation ||
|
|
1209
|
+
state.activeManifestDigest !== admission.digest) {
|
|
1210
|
+
operations.splice(index, 1);
|
|
1211
|
+
if (!addWarning(warning(new OpenApiMcpError("RECORD_NOT_ADMITTED"))))
|
|
1212
|
+
responseLimited = true;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
operations.sort((left, right) => {
|
|
1216
|
+
if (left.item.deprecated !== right.item.deprecated) {
|
|
1217
|
+
return left.item.deprecated ? 1 : -1;
|
|
1218
|
+
}
|
|
1219
|
+
if (left.rank !== right.rank)
|
|
1220
|
+
return left.rank - right.rank;
|
|
1221
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
1222
|
+
});
|
|
1223
|
+
const returnedOperations = operations.slice(0, query.limit);
|
|
1224
|
+
if (!responseFits(returnedOperations.map(({ item }) => item), warnings))
|
|
1225
|
+
responseLimited = true;
|
|
1226
|
+
if (responseLimited) {
|
|
1227
|
+
while (!responseFits(returnedOperations.map(({ item }) => item), [...warnings, limitWarning])) {
|
|
1228
|
+
if (returnedOperations.pop() === undefined) {
|
|
1229
|
+
throw new OpenApiMcpError("RESPONSE_LIMIT_EXCEEDED", "Search response limit is too small");
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
warnings.push(limitWarning);
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
operations: returnedOperations.map(({ item }) => item),
|
|
1236
|
+
warnings,
|
|
1237
|
+
};
|
|
1238
|
+
},
|
|
1239
|
+
async prepareRead(input) {
|
|
1240
|
+
return await prepare(input, "read");
|
|
1241
|
+
},
|
|
1242
|
+
async prepareAction(input) {
|
|
1243
|
+
return await prepare(input, "action");
|
|
1244
|
+
},
|
|
1245
|
+
async revalidate(call) {
|
|
1246
|
+
const snapshot = await verifyAndSnapshotPreparedCall(call);
|
|
1247
|
+
const operation = encodeOperationRef({
|
|
1248
|
+
catalogId: snapshot.catalogId,
|
|
1249
|
+
releaseId: snapshot.releaseId,
|
|
1250
|
+
operationId: snapshot.operationId,
|
|
1251
|
+
manifestDigest: snapshot.manifestDigest,
|
|
1252
|
+
});
|
|
1253
|
+
const fresh = await prepare(Object.freeze({
|
|
1254
|
+
operation,
|
|
1255
|
+
arguments: snapshot.normalizedArguments,
|
|
1256
|
+
...(snapshot.pageToken === null
|
|
1257
|
+
? {}
|
|
1258
|
+
: { pageToken: snapshot.pageToken }),
|
|
1259
|
+
}), snapshot.safety);
|
|
1260
|
+
if (!timingSafeEqual(fresh.operationDigest, snapshot.operationDigest) ||
|
|
1261
|
+
!timingSafeEqual(fresh.manifestDigest, snapshot.manifestDigest) ||
|
|
1262
|
+
fresh.credentialProfileId !== snapshot.credentialProfileId ||
|
|
1263
|
+
!timingSafeEqual(fresh.credentialProfileDigest, snapshot.credentialProfileDigest) ||
|
|
1264
|
+
!timingSafeEqual(fresh.reservedSlotsDigest, snapshot.reservedSlotsDigest) ||
|
|
1265
|
+
!timingSafeEqual(fresh.inputDigest, snapshot.inputDigest) ||
|
|
1266
|
+
!timingSafeEqual(fresh.preparedCallDigest, snapshot.preparedCallDigest))
|
|
1267
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Prepared call no longer matches current policy and records");
|
|
1268
|
+
return fresh;
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
return runtime;
|
|
1272
|
+
}
|