@kya-os/mcp-i 1.12.0 → 1.12.2
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 +7 -9
- package/dist/runtime/adapter-express.js +3 -3
- package/dist/runtime/adapter-nextjs.js +3 -3
- package/dist/runtime/http.js +3 -3
- package/dist/runtime/identity.d.ts +11 -0
- package/dist/runtime/identity.js +57 -8
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +3 -2
- package/dist/runtime/proof-batch-queue.d.ts +60 -6
- package/dist/runtime/proof-batch-queue.js +154 -21
- package/dist/runtime/stdio.js +3 -3
- package/dist/runtime/utils/tools.js +19 -2
- package/dist/test/examples/test-usage-example.d.ts +14 -14
- package/package.json +6 -6
|
@@ -94,6 +94,17 @@ export declare class IdentityManager {
|
|
|
94
94
|
* Requirements: 4.2, 4.3
|
|
95
95
|
*/
|
|
96
96
|
private loadProdIdentity;
|
|
97
|
+
/**
|
|
98
|
+
* Derive the real Ed25519 public key from a raw private key seed.
|
|
99
|
+
*
|
|
100
|
+
* Ed25519 public keys are a deterministic function of the private key, so
|
|
101
|
+
* this recovers the actual key rather than approximating one. A previous
|
|
102
|
+
* version of this method returned a SHA-256 digest of the private key as
|
|
103
|
+
* a "placeholder public key" — cryptographically meaningless, and wrong
|
|
104
|
+
* wherever an AgentIdentity's publicKey is later published or used to
|
|
105
|
+
* build a kid (e.g. migrate-identity.ts, well-known.ts).
|
|
106
|
+
*/
|
|
107
|
+
private derivePublicKey;
|
|
97
108
|
/**
|
|
98
109
|
* Validate identity configuration
|
|
99
110
|
*/
|
package/dist/runtime/identity.js
CHANGED
|
@@ -13,6 +13,11 @@ const fs_1 = require("fs");
|
|
|
13
13
|
const path_1 = require("path");
|
|
14
14
|
const jose_1 = require("jose");
|
|
15
15
|
const crypto_1 = require("crypto");
|
|
16
|
+
// Minimal DER envelopes Node's crypto module needs to recognize a raw
|
|
17
|
+
// 32-byte Ed25519 key as PKCS8 (private) / SPKI (public). Mirrors the same
|
|
18
|
+
// prefixes used for signing/verification in providers/node-providers.ts.
|
|
19
|
+
const ED25519_PKCS8_PREFIX_HEX = "302e020100300506032b657004220420";
|
|
20
|
+
const ED25519_SPKI_HEADER_BYTES = 12;
|
|
16
21
|
/**
|
|
17
22
|
* Error codes for identity management
|
|
18
23
|
*/
|
|
@@ -232,14 +237,21 @@ class IdentityManager {
|
|
|
232
237
|
error.code = exports.IDENTITY_ERRORS.ENOIDENTITY;
|
|
233
238
|
throw error;
|
|
234
239
|
}
|
|
235
|
-
// For production,
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.
|
|
242
|
-
|
|
240
|
+
// For production, the private key is base64-encoded (raw 32-byte seed,
|
|
241
|
+
// or a 64-byte seed+public-key pair — see derivePublicKey below). Fail
|
|
242
|
+
// fast with a clear error on malformed input rather than silently
|
|
243
|
+
// producing an identity with unusable key material.
|
|
244
|
+
let publicKey;
|
|
245
|
+
try {
|
|
246
|
+
publicKey = this.derivePublicKey(env.AGENT_PRIVATE_KEY);
|
|
247
|
+
}
|
|
248
|
+
catch (cause) {
|
|
249
|
+
const error = new Error("AGENT_PRIVATE_KEY is not a valid Ed25519 private key " +
|
|
250
|
+
"(must be a base64-encoded 32-byte seed, or a 64-byte seed+public-key pair).");
|
|
251
|
+
error.code = exports.IDENTITY_ERRORS.ENOIDENTITY;
|
|
252
|
+
error.cause = cause;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
243
255
|
return {
|
|
244
256
|
did: env.AGENT_DID,
|
|
245
257
|
kid: env.AGENT_KEY_ID,
|
|
@@ -248,6 +260,43 @@ class IdentityManager {
|
|
|
248
260
|
createdAt: new Date().toISOString(), // We don't have creation time in prod
|
|
249
261
|
};
|
|
250
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Derive the real Ed25519 public key from a raw private key seed.
|
|
265
|
+
*
|
|
266
|
+
* Ed25519 public keys are a deterministic function of the private key, so
|
|
267
|
+
* this recovers the actual key rather than approximating one. A previous
|
|
268
|
+
* version of this method returned a SHA-256 digest of the private key as
|
|
269
|
+
* a "placeholder public key" — cryptographically meaningless, and wrong
|
|
270
|
+
* wherever an AgentIdentity's publicKey is later published or used to
|
|
271
|
+
* build a kid (e.g. migrate-identity.ts, well-known.ts).
|
|
272
|
+
*/
|
|
273
|
+
derivePublicKey(privateKeyBase64) {
|
|
274
|
+
const privateKeyBuffer = Buffer.from(privateKeyBase64, "base64");
|
|
275
|
+
// Handle both raw 32-byte and full 64-byte (seed + public key) Ed25519
|
|
276
|
+
// private keys, matching providers/node-providers.ts's sign().
|
|
277
|
+
const seed = privateKeyBuffer.length === 64
|
|
278
|
+
? privateKeyBuffer.subarray(0, 32)
|
|
279
|
+
: privateKeyBuffer;
|
|
280
|
+
// Wrap the raw seed in a minimal PKCS8 DER envelope so Node's crypto
|
|
281
|
+
// module recognizes it as an Ed25519 private key.
|
|
282
|
+
const pkcs8 = Buffer.concat([
|
|
283
|
+
Buffer.from(ED25519_PKCS8_PREFIX_HEX, "hex"),
|
|
284
|
+
seed,
|
|
285
|
+
]);
|
|
286
|
+
const privateKeyObject = (0, crypto_1.createPrivateKey)({
|
|
287
|
+
key: pkcs8,
|
|
288
|
+
format: "der",
|
|
289
|
+
type: "pkcs8",
|
|
290
|
+
});
|
|
291
|
+
const publicKeyDer = (0, crypto_1.createPublicKey)(privateKeyObject).export({
|
|
292
|
+
format: "der",
|
|
293
|
+
type: "spki",
|
|
294
|
+
});
|
|
295
|
+
// Strip the fixed-length SPKI header to get the raw 32-byte public key.
|
|
296
|
+
return publicKeyDer
|
|
297
|
+
.subarray(ED25519_SPKI_HEADER_BYTES)
|
|
298
|
+
.toString("base64");
|
|
299
|
+
}
|
|
251
300
|
/**
|
|
252
301
|
* Validate identity configuration
|
|
253
302
|
*/
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { type AuthHandshakeConfig, type VerifyOrHintsResult, type AgentReputatio
|
|
|
18
18
|
export { createResumeTokenStore, type ResumeTokenStoreConfig, } from "./resume-token-store";
|
|
19
19
|
export { CloudflareKVResumeTokenStore, type ResumeTokenKVNamespace, } from "./resume-token-store-kv";
|
|
20
20
|
export { ensureOutboundIdentityBridge } from "./outbound-identity-bridge";
|
|
21
|
-
export { type ProofDestination, type ProofBatchQueueConfig, ProofBatchQueue, KTAProofDestination, AgentShieldProofDestination, createProofBatchQueue, } from "./proof-batch-queue";
|
|
21
|
+
export { type ProofDestination, type ProofSubmission, type ProofBatchQueueConfig, ProofBatchQueue, KTAProofDestination, AgentShieldProofDestination, PartialProofSubmissionError, createProofBatchQueue, } from "./proof-batch-queue";
|
|
22
22
|
export { CloudflareKVDelegationVerifier, } from "./delegation-verifier-kv";
|
|
23
23
|
export { AgentShieldAPIDelegationVerifier, } from "./delegation-verifier-agentshield";
|
|
24
24
|
export { MemoryDelegationVerifier, } from "./delegation-verifier-memory";
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* canonical audit trails, and well-known endpoints.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
10
|
-
exports.getToolProtection = exports.isToolProtected = void 0;
|
|
9
|
+
exports.AgentShieldToolProtectionSource = exports.FileToolProtectionSource = exports.InlineToolProtectionSource = exports.createToolProtectionResolver = exports.ToolProtectionResolver = exports.MemoryDelegationVerifier = exports.AgentShieldAPIDelegationVerifier = exports.CloudflareKVDelegationVerifier = exports.createProofBatchQueue = exports.PartialProofSubmissionError = exports.AgentShieldProofDestination = exports.KTAProofDestination = exports.ProofBatchQueue = exports.ensureOutboundIdentityBridge = exports.CloudflareKVResumeTokenStore = exports.createResumeTokenStore = exports.MemoryResumeTokenStore = exports.hasSensitiveScopes = exports.verifyOrHints = exports.extractScopes = exports.validateDelegation = exports.checkScopes = exports.createDelegationVerifier = exports.formatVerifyLink = exports.DemoConsole = exports.createDemoManager = exports.DemoManager = exports.createDebugEndpoint = exports.DebugManager = exports.extractDIDFromPath = exports.validateAgentDocument = exports.validateDIDDocument = exports.createWellKnownHandler = exports.WellKnownManager = exports.extractCanonicalData = exports.createProofResponse = exports.ProofGenerator = exports.validateHandshakeFormat = exports.createHandshakeRequest = exports.defaultSessionManager = exports.SessionManager = exports.IDENTITY_ERRORS = exports.ensureIdentity = exports.defaultIdentityManager = exports.IdentityManager = exports.RUNTIME_ERRORS = exports.RuntimeFactory = exports.createMCPIRuntime = exports.MCPIRuntime = exports.MCPINodeRuntimeWrapper = void 0;
|
|
10
|
+
exports.getToolProtection = exports.isToolProtected = exports.toolProtectionRegistry = void 0;
|
|
11
11
|
// Main runtime - Node.js implementation with providers
|
|
12
12
|
var mcpi_runtime_wrapper_1 = require("./mcpi-runtime-wrapper");
|
|
13
13
|
Object.defineProperty(exports, "MCPINodeRuntimeWrapper", { enumerable: true, get: function () { return mcpi_runtime_wrapper_1.MCPINodeRuntimeWrapper; } });
|
|
@@ -75,6 +75,7 @@ var proof_batch_queue_1 = require("./proof-batch-queue");
|
|
|
75
75
|
Object.defineProperty(exports, "ProofBatchQueue", { enumerable: true, get: function () { return proof_batch_queue_1.ProofBatchQueue; } });
|
|
76
76
|
Object.defineProperty(exports, "KTAProofDestination", { enumerable: true, get: function () { return proof_batch_queue_1.KTAProofDestination; } });
|
|
77
77
|
Object.defineProperty(exports, "AgentShieldProofDestination", { enumerable: true, get: function () { return proof_batch_queue_1.AgentShieldProofDestination; } });
|
|
78
|
+
Object.defineProperty(exports, "PartialProofSubmissionError", { enumerable: true, get: function () { return proof_batch_queue_1.PartialProofSubmissionError; } });
|
|
78
79
|
Object.defineProperty(exports, "createProofBatchQueue", { enumerable: true, get: function () { return proof_batch_queue_1.createProofBatchQueue; } });
|
|
79
80
|
var delegation_verifier_kv_1 = require("./delegation-verifier-kv");
|
|
80
81
|
Object.defineProperty(exports, "CloudflareKVDelegationVerifier", { enumerable: true, get: function () { return delegation_verifier_kv_1.CloudflareKVDelegationVerifier; } });
|
|
@@ -17,24 +17,61 @@
|
|
|
17
17
|
* Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching)
|
|
18
18
|
*/
|
|
19
19
|
import type { DetachedProof } from "@kya-os/mcp/types" with { "resolution-mode": "import" };
|
|
20
|
+
/**
|
|
21
|
+
* A proof plus the tool-call context KTA's submission API requires
|
|
22
|
+
* alongside it. DetachedProof (proof.meta) carries did/kid/ts/nonce/
|
|
23
|
+
* audience/etc, but not which tool was called or how it turned out —
|
|
24
|
+
* that context only exists at the call site, so it has to be threaded
|
|
25
|
+
* through explicitly rather than derived from the proof.
|
|
26
|
+
*/
|
|
27
|
+
export interface ProofSubmission {
|
|
28
|
+
proof: DetachedProof;
|
|
29
|
+
toolName: string;
|
|
30
|
+
outcome: "success" | "failure" | "denied";
|
|
31
|
+
}
|
|
20
32
|
/**
|
|
21
33
|
* Proof submission destination
|
|
22
34
|
*/
|
|
23
35
|
export interface ProofDestination {
|
|
24
36
|
/** Destination name (for logging) */
|
|
25
37
|
name: string;
|
|
26
|
-
/** Submit batch of
|
|
27
|
-
submit(
|
|
38
|
+
/** Submit a batch of proof submissions */
|
|
39
|
+
submit(submissions: ProofSubmission[]): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by a ProofDestination whose submit() made independent per-item
|
|
43
|
+
* requests (rather than one atomic request for the whole array) when only
|
|
44
|
+
* some of them failed. Callers that retry on failure MUST retry only
|
|
45
|
+
* `failed`, not the original full array — retrying everything would
|
|
46
|
+
* resubmit proofs the destination already accepted.
|
|
47
|
+
*/
|
|
48
|
+
export declare class PartialProofSubmissionError extends Error {
|
|
49
|
+
readonly failed: ProofSubmission[];
|
|
50
|
+
readonly succeededCount: number;
|
|
51
|
+
/** The per-item rejection reason for each entry in `failed`, same order. */
|
|
52
|
+
readonly causes?: unknown[] | undefined;
|
|
53
|
+
constructor(message: string, failed: ProofSubmission[], succeededCount: number,
|
|
54
|
+
/** The per-item rejection reason for each entry in `failed`, same order. */
|
|
55
|
+
causes?: unknown[] | undefined);
|
|
28
56
|
}
|
|
29
57
|
/**
|
|
30
58
|
* KTA proof submission destination
|
|
59
|
+
*
|
|
60
|
+
* The live KTA API (POST /api/v1/proofs) accepts exactly one proof per
|
|
61
|
+
* request — there is no /batch endpoint — so a "batch" here means firing
|
|
62
|
+
* one request per submission, not one request for the whole array. Since
|
|
63
|
+
* each request is independent, submit() reports PARTIAL failure via
|
|
64
|
+
* PartialProofSubmissionError rather than treating the whole array as
|
|
65
|
+
* atomic — a caller that blindly retried everything on any rejection
|
|
66
|
+
* would resubmit proofs that already succeeded.
|
|
31
67
|
*/
|
|
32
68
|
export declare class KTAProofDestination implements ProofDestination {
|
|
33
69
|
name: string;
|
|
34
70
|
private apiUrl;
|
|
35
71
|
private apiKey?;
|
|
36
72
|
constructor(apiUrl: string, apiKey?: string);
|
|
37
|
-
submit(
|
|
73
|
+
submit(submissions: ProofSubmission[]): Promise<void>;
|
|
74
|
+
private submitOne;
|
|
38
75
|
}
|
|
39
76
|
/**
|
|
40
77
|
* AgentShield proof submission destination
|
|
@@ -47,7 +84,7 @@ export declare class AgentShieldProofDestination implements ProofDestination {
|
|
|
47
84
|
private apiUrl;
|
|
48
85
|
private apiKey;
|
|
49
86
|
constructor(apiUrl: string, apiKey: string);
|
|
50
|
-
submit(
|
|
87
|
+
submit(submissions: ProofSubmission[]): Promise<void>;
|
|
51
88
|
}
|
|
52
89
|
/**
|
|
53
90
|
* Proof batch queue configuration
|
|
@@ -79,9 +116,26 @@ export declare class ProofBatchQueue {
|
|
|
79
116
|
private stats;
|
|
80
117
|
constructor(config: ProofBatchQueueConfig);
|
|
81
118
|
/**
|
|
82
|
-
* Add proof to queue
|
|
119
|
+
* Add a proof submission to the queue.
|
|
120
|
+
*
|
|
121
|
+
* Accepts a bare DetachedProof for backward compatibility with the
|
|
122
|
+
* pre-#650 API: a plain-JS (or un-upgraded TS) consumer built against
|
|
123
|
+
* the old `enqueue(proof: DetachedProof)` signature would otherwise have
|
|
124
|
+
* `toolName`/`outcome` silently come through as `undefined`.
|
|
125
|
+
*/
|
|
126
|
+
enqueue(item: ProofSubmission | DetachedProof): void;
|
|
127
|
+
/**
|
|
128
|
+
* Normalize a bare DetachedProof (the pre-#650 enqueue() shape) into a
|
|
129
|
+
* ProofSubmission. Derives toolName/outcome from the proof's own signed
|
|
130
|
+
* meta when present, rather than a hardcoded literal -- a destination's
|
|
131
|
+
* server-side check compares the submitted toolName/outcome against what
|
|
132
|
+
* was actually signed into the JWS payload, so a literal that disagrees
|
|
133
|
+
* with the real signed value is guaranteed to fail verification. When the
|
|
134
|
+
* proof was never signed with these fields at all (a genuinely pre-#651
|
|
135
|
+
* proof), no fallback can make the submission succeed, so this warns
|
|
136
|
+
* rather than silently queueing a doomed submission.
|
|
83
137
|
*/
|
|
84
|
-
|
|
138
|
+
private submissionFromBareProof;
|
|
85
139
|
/**
|
|
86
140
|
* Flush queue immediately (submit all queued proofs)
|
|
87
141
|
*/
|
|
@@ -18,10 +18,40 @@
|
|
|
18
18
|
* Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching)
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = void 0;
|
|
21
|
+
exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = exports.PartialProofSubmissionError = void 0;
|
|
22
22
|
exports.createProofBatchQueue = createProofBatchQueue;
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by a ProofDestination whose submit() made independent per-item
|
|
25
|
+
* requests (rather than one atomic request for the whole array) when only
|
|
26
|
+
* some of them failed. Callers that retry on failure MUST retry only
|
|
27
|
+
* `failed`, not the original full array — retrying everything would
|
|
28
|
+
* resubmit proofs the destination already accepted.
|
|
29
|
+
*/
|
|
30
|
+
class PartialProofSubmissionError extends Error {
|
|
31
|
+
failed;
|
|
32
|
+
succeededCount;
|
|
33
|
+
causes;
|
|
34
|
+
constructor(message, failed, succeededCount,
|
|
35
|
+
/** The per-item rejection reason for each entry in `failed`, same order. */
|
|
36
|
+
causes) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.failed = failed;
|
|
39
|
+
this.succeededCount = succeededCount;
|
|
40
|
+
this.causes = causes;
|
|
41
|
+
this.name = "PartialProofSubmissionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.PartialProofSubmissionError = PartialProofSubmissionError;
|
|
23
45
|
/**
|
|
24
46
|
* KTA proof submission destination
|
|
47
|
+
*
|
|
48
|
+
* The live KTA API (POST /api/v1/proofs) accepts exactly one proof per
|
|
49
|
+
* request — there is no /batch endpoint — so a "batch" here means firing
|
|
50
|
+
* one request per submission, not one request for the whole array. Since
|
|
51
|
+
* each request is independent, submit() reports PARTIAL failure via
|
|
52
|
+
* PartialProofSubmissionError rather than treating the whole array as
|
|
53
|
+
* atomic — a caller that blindly retried everything on any rejection
|
|
54
|
+
* would resubmit proofs that already succeeded.
|
|
25
55
|
*/
|
|
26
56
|
class KTAProofDestination {
|
|
27
57
|
name = "KTA";
|
|
@@ -31,20 +61,61 @@ class KTAProofDestination {
|
|
|
31
61
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
32
62
|
this.apiKey = apiKey;
|
|
33
63
|
}
|
|
34
|
-
async submit(
|
|
64
|
+
async submit(submissions) {
|
|
65
|
+
const results = await Promise.allSettled(submissions.map((submission) => this.submitOne(submission)));
|
|
66
|
+
const failures = results
|
|
67
|
+
.map((result, index) => ({ result, submission: submissions[index] }))
|
|
68
|
+
.filter((entry) => entry.result.status === "rejected");
|
|
69
|
+
if (failures.length > 0) {
|
|
70
|
+
const causes = failures.map((entry) => entry.result.reason);
|
|
71
|
+
const reasons = causes
|
|
72
|
+
.map((reason) => reason instanceof Error ? reason.message : String(reason))
|
|
73
|
+
.join("; ");
|
|
74
|
+
throw new PartialProofSubmissionError(`KTA proof submission failed for ${failures.length}/${submissions.length} proof(s): ${reasons}`, failures.map((entry) => entry.submission), submissions.length - failures.length, causes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async submitOne(submission) {
|
|
78
|
+
const { proof, toolName, outcome } = submission;
|
|
35
79
|
const headers = {
|
|
36
80
|
"Content-Type": "application/json",
|
|
37
81
|
};
|
|
38
82
|
if (this.apiKey) {
|
|
39
|
-
headers["
|
|
83
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
40
84
|
}
|
|
41
|
-
const response = await fetch(`${this.apiUrl}/api/v1/proofs
|
|
85
|
+
const response = await fetch(`${this.apiUrl}/api/v1/proofs`, {
|
|
42
86
|
method: "POST",
|
|
43
87
|
headers,
|
|
44
|
-
body: JSON.stringify({
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
proof,
|
|
90
|
+
toolName,
|
|
91
|
+
outcome,
|
|
92
|
+
// The live API wants these as top-level fields; both are already
|
|
93
|
+
// present on the proof itself (ts is Unix-epoch seconds, audience
|
|
94
|
+
// is documented server-side as the business DID), so derive them
|
|
95
|
+
// rather than requiring the caller to pass duplicates.
|
|
96
|
+
timestamp: proof.meta.ts,
|
|
97
|
+
businessDid: proof.meta.audience,
|
|
98
|
+
}),
|
|
45
99
|
});
|
|
46
100
|
if (!response.ok) {
|
|
47
|
-
|
|
101
|
+
// The server overloads 409 for two distinct conditions: a duplicate
|
|
102
|
+
// proofJws (code CONFLICT) means this exact proof is already stored --
|
|
103
|
+
// almost always a client-side timeout after the server accepted a
|
|
104
|
+
// prior attempt, and is idempotent success. A replayed nonce (code
|
|
105
|
+
// NONCE_REUSED) is also a 409 but means the server REFUSED the proof;
|
|
106
|
+
// treating it as success would silently drop a real replay-protection
|
|
107
|
+
// signal. Distinguish via the response body's machine-readable code
|
|
108
|
+
// rather than assuming every 409 means "already stored".
|
|
109
|
+
let code;
|
|
110
|
+
if (response.status === 409) {
|
|
111
|
+
const body = await response.json().catch(() => ({}));
|
|
112
|
+
code = body.code;
|
|
113
|
+
if (code === "CONFLICT") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`KTA proof submission failed: ${response.status} ${response.statusText}` +
|
|
118
|
+
(code ? ` (${code})` : ""));
|
|
48
119
|
}
|
|
49
120
|
}
|
|
50
121
|
}
|
|
@@ -63,10 +134,11 @@ class AgentShieldProofDestination {
|
|
|
63
134
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
64
135
|
this.apiKey = apiKey;
|
|
65
136
|
}
|
|
66
|
-
async submit(
|
|
67
|
-
if (
|
|
137
|
+
async submit(submissions) {
|
|
138
|
+
if (submissions.length === 0) {
|
|
68
139
|
return;
|
|
69
140
|
}
|
|
141
|
+
const proofs = submissions.map((submission) => submission.proof);
|
|
70
142
|
// Extract session_id from first proof for AgentShield session grouping
|
|
71
143
|
// AgentShield uses this for analytics and detection monitoring
|
|
72
144
|
const sessionId = proofs[0]?.meta?.sessionId || "unknown";
|
|
@@ -127,14 +199,20 @@ class ProofBatchQueue {
|
|
|
127
199
|
this.startRetryTimer();
|
|
128
200
|
}
|
|
129
201
|
/**
|
|
130
|
-
* Add proof to queue
|
|
202
|
+
* Add a proof submission to the queue.
|
|
203
|
+
*
|
|
204
|
+
* Accepts a bare DetachedProof for backward compatibility with the
|
|
205
|
+
* pre-#650 API: a plain-JS (or un-upgraded TS) consumer built against
|
|
206
|
+
* the old `enqueue(proof: DetachedProof)` signature would otherwise have
|
|
207
|
+
* `toolName`/`outcome` silently come through as `undefined`.
|
|
131
208
|
*/
|
|
132
|
-
enqueue(
|
|
209
|
+
enqueue(item) {
|
|
210
|
+
const submission = "proof" in item ? item : this.submissionFromBareProof(item);
|
|
133
211
|
if (this.closed) {
|
|
134
212
|
console.warn("[ProofBatchQueue] Queue is closed, dropping proof");
|
|
135
213
|
return;
|
|
136
214
|
}
|
|
137
|
-
this.queue.push(
|
|
215
|
+
this.queue.push(submission);
|
|
138
216
|
this.stats.queued++;
|
|
139
217
|
if (this.config.debug) {
|
|
140
218
|
console.error(`[ProofBatchQueue] Enqueued proof (queue size: ${this.queue.length})`);
|
|
@@ -144,6 +222,48 @@ class ProofBatchQueue {
|
|
|
144
222
|
this.flush();
|
|
145
223
|
}
|
|
146
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Normalize a bare DetachedProof (the pre-#650 enqueue() shape) into a
|
|
227
|
+
* ProofSubmission. Derives toolName/outcome from the proof's own signed
|
|
228
|
+
* meta when present, rather than a hardcoded literal -- a destination's
|
|
229
|
+
* server-side check compares the submitted toolName/outcome against what
|
|
230
|
+
* was actually signed into the JWS payload, so a literal that disagrees
|
|
231
|
+
* with the real signed value is guaranteed to fail verification. When the
|
|
232
|
+
* proof was never signed with these fields at all (a genuinely pre-#651
|
|
233
|
+
* proof), no fallback can make the submission succeed, so this warns
|
|
234
|
+
* rather than silently queueing a doomed submission.
|
|
235
|
+
*/
|
|
236
|
+
submissionFromBareProof(proof) {
|
|
237
|
+
// The published @kya-os/mcp package's DetachedProof/ProofMeta types
|
|
238
|
+
// predate the toolName/submissionOutcome fields added to
|
|
239
|
+
// @kya-os/contracts/proof's ProofMeta (#651) -- read them structurally
|
|
240
|
+
// with a runtime guard rather than waiting on that package to catch up.
|
|
241
|
+
const meta = proof.meta;
|
|
242
|
+
const toolName = meta !== null &&
|
|
243
|
+
typeof meta === "object" &&
|
|
244
|
+
"toolName" in meta &&
|
|
245
|
+
typeof meta.toolName === "string"
|
|
246
|
+
? meta.toolName
|
|
247
|
+
: undefined;
|
|
248
|
+
const submissionOutcome = meta !== null &&
|
|
249
|
+
typeof meta === "object" &&
|
|
250
|
+
"submissionOutcome" in meta &&
|
|
251
|
+
(meta.submissionOutcome === "success" ||
|
|
252
|
+
meta.submissionOutcome === "failure" ||
|
|
253
|
+
meta.submissionOutcome === "denied")
|
|
254
|
+
? meta.submissionOutcome
|
|
255
|
+
: undefined;
|
|
256
|
+
if (toolName === undefined || submissionOutcome === undefined) {
|
|
257
|
+
console.warn("[ProofBatchQueue] Enqueued a DetachedProof with no signed toolName/submissionOutcome -- " +
|
|
258
|
+
"the destination's server-side JWS payload check will reject this submission. " +
|
|
259
|
+
"Generate the proof with { toolName, submissionOutcome } options instead.");
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
proof,
|
|
263
|
+
toolName: toolName ?? "unknown",
|
|
264
|
+
outcome: submissionOutcome ?? "success",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
147
267
|
/**
|
|
148
268
|
* Flush queue immediately (submit all queued proofs)
|
|
149
269
|
*/
|
|
@@ -151,14 +271,14 @@ class ProofBatchQueue {
|
|
|
151
271
|
if (this.queue.length === 0) {
|
|
152
272
|
return;
|
|
153
273
|
}
|
|
154
|
-
const
|
|
274
|
+
const submissions = this.queue.splice(0, this.config.maxBatchSize);
|
|
155
275
|
if (this.config.debug) {
|
|
156
|
-
console.error(`[ProofBatchQueue] Flushing ${
|
|
276
|
+
console.error(`[ProofBatchQueue] Flushing ${submissions.length} proofs to ${this.config.destinations.length} destinations`);
|
|
157
277
|
}
|
|
158
278
|
// Submit to each destination (fire-and-forget)
|
|
159
279
|
for (const destination of this.config.destinations) {
|
|
160
280
|
const batch = {
|
|
161
|
-
|
|
281
|
+
submissions,
|
|
162
282
|
destination,
|
|
163
283
|
retryCount: 0,
|
|
164
284
|
};
|
|
@@ -170,29 +290,42 @@ class ProofBatchQueue {
|
|
|
170
290
|
*/
|
|
171
291
|
async submitBatch(batch) {
|
|
172
292
|
try {
|
|
173
|
-
await batch.destination.submit(batch.
|
|
174
|
-
this.stats.submitted += batch.
|
|
293
|
+
await batch.destination.submit(batch.submissions);
|
|
294
|
+
this.stats.submitted += batch.submissions.length;
|
|
175
295
|
this.stats.batchesSubmitted++;
|
|
176
296
|
if (this.config.debug) {
|
|
177
|
-
console.error(`[ProofBatchQueue] Successfully submitted ${batch.
|
|
297
|
+
console.error(`[ProofBatchQueue] Successfully submitted ${batch.submissions.length} proofs to ${batch.destination.name}`);
|
|
178
298
|
}
|
|
179
299
|
}
|
|
180
300
|
catch (error) {
|
|
181
301
|
console.error(`[ProofBatchQueue] Failed to submit to ${batch.destination.name}:`, error);
|
|
302
|
+
// A destination that makes independent per-item requests (KTA) can
|
|
303
|
+
// fail partially. Only retry the submissions that actually failed —
|
|
304
|
+
// retrying the whole batch would resubmit ones already accepted,
|
|
305
|
+
// and would let one permanently-failing proof block good ones from
|
|
306
|
+
// ever being marked submitted.
|
|
307
|
+
const isPartialFailure = error instanceof PartialProofSubmissionError;
|
|
308
|
+
const submissionsToRetry = isPartialFailure
|
|
309
|
+
? error.failed
|
|
310
|
+
: batch.submissions;
|
|
311
|
+
if (isPartialFailure && error.succeededCount > 0) {
|
|
312
|
+
this.stats.submitted += error.succeededCount;
|
|
313
|
+
}
|
|
182
314
|
// Retry with exponential backoff
|
|
183
315
|
if (batch.retryCount < this.config.maxRetries) {
|
|
184
316
|
batch.retryCount++;
|
|
185
317
|
const backoffMs = Math.min(1000 * Math.pow(2, batch.retryCount - 1), 16000);
|
|
186
318
|
batch.nextRetryAt = Date.now() + backoffMs;
|
|
319
|
+
batch.submissions = submissionsToRetry;
|
|
187
320
|
this.pendingBatches.push(batch);
|
|
188
321
|
if (this.config.debug) {
|
|
189
|
-
console.error(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms`);
|
|
322
|
+
console.error(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms for ${batch.submissions.length} proof(s)`);
|
|
190
323
|
}
|
|
191
324
|
}
|
|
192
325
|
else {
|
|
193
|
-
// Max retries exceeded, drop
|
|
194
|
-
this.stats.failed +=
|
|
195
|
-
console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${
|
|
326
|
+
// Max retries exceeded, drop the still-failing submissions
|
|
327
|
+
this.stats.failed += submissionsToRetry.length;
|
|
328
|
+
console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${submissionsToRetry.length} proofs`);
|
|
196
329
|
}
|
|
197
330
|
}
|
|
198
331
|
}
|