@kya-os/mcp 1.6.1 → 1.9.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +228 -3
  3. package/dist/authz/index.d.ts +1 -0
  4. package/dist/authz/index.d.ts.map +1 -1
  5. package/dist/authz/index.js +1 -0
  6. package/dist/authz/index.js.map +1 -1
  7. package/dist/authz/oidc/oidc-adapter.d.ts +10 -2
  8. package/dist/authz/oidc/oidc-adapter.d.ts.map +1 -1
  9. package/dist/authz/oidc/oidc-adapter.js +18 -9
  10. package/dist/authz/oidc/oidc-adapter.js.map +1 -1
  11. package/dist/authz/oidc/pending-flow-store.d.ts +80 -0
  12. package/dist/authz/oidc/pending-flow-store.d.ts.map +1 -0
  13. package/dist/authz/oidc/pending-flow-store.js +82 -0
  14. package/dist/authz/oidc/pending-flow-store.js.map +1 -0
  15. package/dist/card/build.d.ts +41 -0
  16. package/dist/card/build.d.ts.map +1 -0
  17. package/dist/card/build.js +47 -0
  18. package/dist/card/build.js.map +1 -0
  19. package/dist/card/builder.d.ts +99 -0
  20. package/dist/card/builder.d.ts.map +1 -0
  21. package/dist/card/builder.js +147 -0
  22. package/dist/card/builder.js.map +1 -0
  23. package/dist/card/cimd.d.ts +92 -0
  24. package/dist/card/cimd.d.ts.map +1 -0
  25. package/dist/card/cimd.js +225 -0
  26. package/dist/card/cimd.js.map +1 -0
  27. package/dist/card/delegation.d.ts +145 -0
  28. package/dist/card/delegation.d.ts.map +1 -0
  29. package/dist/card/delegation.js +293 -0
  30. package/dist/card/delegation.js.map +1 -0
  31. package/dist/card/emit.d.ts +133 -0
  32. package/dist/card/emit.d.ts.map +1 -0
  33. package/dist/card/emit.js +127 -0
  34. package/dist/card/emit.js.map +1 -0
  35. package/dist/card/index.d.ts +43 -0
  36. package/dist/card/index.d.ts.map +1 -0
  37. package/dist/card/index.js +46 -0
  38. package/dist/card/index.js.map +1 -0
  39. package/dist/card/middleware.d.ts +112 -0
  40. package/dist/card/middleware.d.ts.map +1 -0
  41. package/dist/card/middleware.js +121 -0
  42. package/dist/card/middleware.js.map +1 -0
  43. package/dist/card/proof/build.d.ts +22 -0
  44. package/dist/card/proof/build.d.ts.map +1 -0
  45. package/dist/card/proof/build.js +55 -0
  46. package/dist/card/proof/build.js.map +1 -0
  47. package/dist/card/proof/canonical.d.ts +66 -0
  48. package/dist/card/proof/canonical.d.ts.map +1 -0
  49. package/dist/card/proof/canonical.js +131 -0
  50. package/dist/card/proof/canonical.js.map +1 -0
  51. package/dist/card/proof/http-sig.d.ts +69 -0
  52. package/dist/card/proof/http-sig.d.ts.map +1 -0
  53. package/dist/card/proof/http-sig.js +101 -0
  54. package/dist/card/proof/http-sig.js.map +1 -0
  55. package/dist/card/proof/index.d.ts +25 -0
  56. package/dist/card/proof/index.d.ts.map +1 -0
  57. package/dist/card/proof/index.js +25 -0
  58. package/dist/card/proof/index.js.map +1 -0
  59. package/dist/card/proof/nonce-cache.d.ts +67 -0
  60. package/dist/card/proof/nonce-cache.d.ts.map +1 -0
  61. package/dist/card/proof/nonce-cache.js +88 -0
  62. package/dist/card/proof/nonce-cache.js.map +1 -0
  63. package/dist/card/proof/signer.d.ts +32 -0
  64. package/dist/card/proof/signer.d.ts.map +1 -0
  65. package/dist/card/proof/signer.js +59 -0
  66. package/dist/card/proof/signer.js.map +1 -0
  67. package/dist/card/proof/types.d.ts +219 -0
  68. package/dist/card/proof/types.d.ts.map +1 -0
  69. package/dist/card/proof/types.js +87 -0
  70. package/dist/card/proof/types.js.map +1 -0
  71. package/dist/card/proof/verify.d.ts +27 -0
  72. package/dist/card/proof/verify.d.ts.map +1 -0
  73. package/dist/card/proof/verify.js +236 -0
  74. package/dist/card/proof/verify.js.map +1 -0
  75. package/dist/card/resolve.d.ts +97 -0
  76. package/dist/card/resolve.d.ts.map +1 -0
  77. package/dist/card/resolve.js +223 -0
  78. package/dist/card/resolve.js.map +1 -0
  79. package/dist/card/revocation.d.ts +96 -0
  80. package/dist/card/revocation.d.ts.map +1 -0
  81. package/dist/card/revocation.js +190 -0
  82. package/dist/card/revocation.js.map +1 -0
  83. package/dist/card/schema.d.ts +177 -0
  84. package/dist/card/schema.d.ts.map +1 -0
  85. package/dist/card/schema.js +119 -0
  86. package/dist/card/schema.js.map +1 -0
  87. package/dist/card/verify.d.ts +144 -0
  88. package/dist/card/verify.d.ts.map +1 -0
  89. package/dist/card/verify.js +132 -0
  90. package/dist/card/verify.js.map +1 -0
  91. package/dist/delegation/bitstring.d.ts +9 -7
  92. package/dist/delegation/bitstring.d.ts.map +1 -1
  93. package/dist/delegation/bitstring.js +70 -37
  94. package/dist/delegation/bitstring.js.map +1 -1
  95. package/dist/delegation/did-linkage.d.ts +23 -0
  96. package/dist/delegation/did-linkage.d.ts.map +1 -0
  97. package/dist/delegation/did-linkage.js +57 -0
  98. package/dist/delegation/did-linkage.js.map +1 -0
  99. package/dist/delegation/did-resolver-registry.d.ts +7 -0
  100. package/dist/delegation/did-resolver-registry.d.ts.map +1 -0
  101. package/dist/delegation/did-resolver-registry.js +20 -0
  102. package/dist/delegation/did-resolver-registry.js.map +1 -0
  103. package/dist/delegation/did-web-resolver.d.ts +29 -18
  104. package/dist/delegation/did-web-resolver.d.ts.map +1 -1
  105. package/dist/delegation/did-web-resolver.js +65 -35
  106. package/dist/delegation/did-web-resolver.js.map +1 -1
  107. package/dist/delegation/index.d.ts +3 -0
  108. package/dist/delegation/index.d.ts.map +1 -1
  109. package/dist/delegation/index.js +3 -0
  110. package/dist/delegation/index.js.map +1 -1
  111. package/dist/delegation/statuslist-manager.d.ts.map +1 -1
  112. package/dist/delegation/statuslist-manager.js +16 -1
  113. package/dist/delegation/statuslist-manager.js.map +1 -1
  114. package/dist/delegation/vc-jwt-verify.d.ts +61 -0
  115. package/dist/delegation/vc-jwt-verify.d.ts.map +1 -0
  116. package/dist/delegation/vc-jwt-verify.js +131 -0
  117. package/dist/delegation/vc-jwt-verify.js.map +1 -0
  118. package/dist/delegation/vc-verification-checks.d.ts +50 -0
  119. package/dist/delegation/vc-verification-checks.d.ts.map +1 -0
  120. package/dist/delegation/vc-verification-checks.js +212 -0
  121. package/dist/delegation/vc-verification-checks.js.map +1 -0
  122. package/dist/delegation/vc-verifier.d.ts +24 -61
  123. package/dist/delegation/vc-verifier.d.ts.map +1 -1
  124. package/dist/delegation/vc-verifier.js +57 -180
  125. package/dist/delegation/vc-verifier.js.map +1 -1
  126. package/dist/delegation/vc-verifier.types.d.ts +88 -0
  127. package/dist/delegation/vc-verifier.types.d.ts.map +1 -0
  128. package/dist/delegation/vc-verifier.types.js +9 -0
  129. package/dist/delegation/vc-verifier.types.js.map +1 -0
  130. package/dist/index.d.ts +8 -4
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +10 -3
  133. package/dist/index.js.map +1 -1
  134. package/dist/integrations/cheqd/dlr.d.ts +44 -0
  135. package/dist/integrations/cheqd/dlr.d.ts.map +1 -0
  136. package/dist/integrations/cheqd/dlr.js +86 -0
  137. package/dist/integrations/cheqd/dlr.js.map +1 -0
  138. package/dist/integrations/cheqd/index.d.ts +5 -0
  139. package/dist/integrations/cheqd/index.d.ts.map +1 -0
  140. package/dist/integrations/cheqd/index.js +5 -0
  141. package/dist/integrations/cheqd/index.js.map +1 -0
  142. package/dist/integrations/cheqd/linkage.d.ts +20 -0
  143. package/dist/integrations/cheqd/linkage.d.ts.map +1 -0
  144. package/dist/integrations/cheqd/linkage.js +32 -0
  145. package/dist/integrations/cheqd/linkage.js.map +1 -0
  146. package/dist/integrations/cheqd/registrar.d.ts +85 -0
  147. package/dist/integrations/cheqd/registrar.d.ts.map +1 -0
  148. package/dist/integrations/cheqd/registrar.js +304 -0
  149. package/dist/integrations/cheqd/registrar.js.map +1 -0
  150. package/dist/integrations/cheqd/resolver.d.ts +38 -0
  151. package/dist/integrations/cheqd/resolver.d.ts.map +1 -0
  152. package/dist/integrations/cheqd/resolver.js +156 -0
  153. package/dist/integrations/cheqd/resolver.js.map +1 -0
  154. package/dist/middleware/index.d.ts +2 -0
  155. package/dist/middleware/index.d.ts.map +1 -1
  156. package/dist/middleware/index.js +5 -0
  157. package/dist/middleware/index.js.map +1 -1
  158. package/dist/middleware/kya-os-transport.d.ts +2 -1
  159. package/dist/middleware/kya-os-transport.d.ts.map +1 -1
  160. package/dist/middleware/kya-os-transport.js +2 -1
  161. package/dist/middleware/kya-os-transport.js.map +1 -1
  162. package/dist/middleware/with-kya-os-server.d.ts +21 -4
  163. package/dist/middleware/with-kya-os-server.d.ts.map +1 -1
  164. package/dist/middleware/with-kya-os-server.js +4 -0
  165. package/dist/middleware/with-kya-os-server.js.map +1 -1
  166. package/dist/middleware/with-kya-os.config-types.d.ts +139 -0
  167. package/dist/middleware/with-kya-os.config-types.d.ts.map +1 -0
  168. package/dist/middleware/with-kya-os.config-types.js +9 -0
  169. package/dist/middleware/with-kya-os.config-types.js.map +1 -0
  170. package/dist/middleware/with-kya-os.d.ts +10 -238
  171. package/dist/middleware/with-kya-os.d.ts.map +1 -1
  172. package/dist/middleware/with-kya-os.delegation-gate.d.ts +19 -0
  173. package/dist/middleware/with-kya-os.delegation-gate.d.ts.map +1 -0
  174. package/dist/middleware/with-kya-os.delegation-gate.js +175 -0
  175. package/dist/middleware/with-kya-os.delegation-gate.js.map +1 -0
  176. package/dist/middleware/with-kya-os.delegation-verify.d.ts +51 -0
  177. package/dist/middleware/with-kya-os.delegation-verify.d.ts.map +1 -0
  178. package/dist/middleware/with-kya-os.delegation-verify.js +165 -0
  179. package/dist/middleware/with-kya-os.delegation-verify.js.map +1 -0
  180. package/dist/middleware/with-kya-os.deps.d.ts +40 -0
  181. package/dist/middleware/with-kya-os.deps.d.ts.map +1 -0
  182. package/dist/middleware/with-kya-os.deps.js +9 -0
  183. package/dist/middleware/with-kya-os.deps.js.map +1 -0
  184. package/dist/middleware/with-kya-os.grants.d.ts +30 -0
  185. package/dist/middleware/with-kya-os.grants.d.ts.map +1 -0
  186. package/dist/middleware/with-kya-os.grants.js +145 -0
  187. package/dist/middleware/with-kya-os.grants.js.map +1 -0
  188. package/dist/middleware/with-kya-os.helpers.d.ts +24 -0
  189. package/dist/middleware/with-kya-os.helpers.d.ts.map +1 -0
  190. package/dist/middleware/with-kya-os.helpers.js +57 -0
  191. package/dist/middleware/with-kya-os.helpers.js.map +1 -0
  192. package/dist/middleware/with-kya-os.js +65 -703
  193. package/dist/middleware/with-kya-os.js.map +1 -1
  194. package/dist/middleware/with-kya-os.policy-gate.d.ts +16 -0
  195. package/dist/middleware/with-kya-os.policy-gate.d.ts.map +1 -0
  196. package/dist/middleware/with-kya-os.policy-gate.js +98 -0
  197. package/dist/middleware/with-kya-os.policy-gate.js.map +1 -0
  198. package/dist/middleware/with-kya-os.protocol.d.ts +29 -0
  199. package/dist/middleware/with-kya-os.protocol.d.ts.map +1 -0
  200. package/dist/middleware/with-kya-os.protocol.js +121 -0
  201. package/dist/middleware/with-kya-os.protocol.js.map +1 -0
  202. package/dist/middleware/with-kya-os.session.d.ts +32 -0
  203. package/dist/middleware/with-kya-os.session.d.ts.map +1 -0
  204. package/dist/middleware/with-kya-os.session.js +201 -0
  205. package/dist/middleware/with-kya-os.session.js.map +1 -0
  206. package/dist/middleware/with-kya-os.types.d.ts +178 -0
  207. package/dist/middleware/with-kya-os.types.d.ts.map +1 -0
  208. package/dist/middleware/with-kya-os.types.js +13 -0
  209. package/dist/middleware/with-kya-os.types.js.map +1 -0
  210. package/dist/proof/generator.d.ts +21 -0
  211. package/dist/proof/generator.d.ts.map +1 -1
  212. package/dist/proof/generator.js +21 -0
  213. package/dist/proof/generator.js.map +1 -1
  214. package/dist/proof/index.d.ts +1 -1
  215. package/dist/proof/index.d.ts.map +1 -1
  216. package/dist/proof/index.js +1 -1
  217. package/dist/proof/index.js.map +1 -1
  218. package/dist/proof/verifier.d.ts +26 -3
  219. package/dist/proof/verifier.d.ts.map +1 -1
  220. package/dist/proof/verifier.js +54 -24
  221. package/dist/proof/verifier.js.map +1 -1
  222. package/dist/providers/grant-store.d.ts +10 -1
  223. package/dist/providers/grant-store.d.ts.map +1 -1
  224. package/dist/providers/grant-store.js.map +1 -1
  225. package/dist/providers/runtime-fetch.d.ts +8 -0
  226. package/dist/providers/runtime-fetch.d.ts.map +1 -1
  227. package/dist/providers/runtime-fetch.js +17 -0
  228. package/dist/providers/runtime-fetch.js.map +1 -1
  229. package/dist/providers/web-crypto.d.ts.map +1 -1
  230. package/dist/providers/web-crypto.js +15 -7
  231. package/dist/providers/web-crypto.js.map +1 -1
  232. package/dist/session/index.d.ts +1 -0
  233. package/dist/session/index.d.ts.map +1 -1
  234. package/dist/session/index.js +1 -0
  235. package/dist/session/index.js.map +1 -1
  236. package/dist/session/manager.d.ts +17 -6
  237. package/dist/session/manager.d.ts.map +1 -1
  238. package/dist/session/manager.js +16 -26
  239. package/dist/session/manager.js.map +1 -1
  240. package/dist/session/session-store.d.ts +78 -0
  241. package/dist/session/session-store.d.ts.map +1 -0
  242. package/dist/session/session-store.js +79 -0
  243. package/dist/session/session-store.js.map +1 -0
  244. package/dist/types/protocol.d.ts +9 -3
  245. package/dist/types/protocol.d.ts.map +1 -1
  246. package/dist/types/protocol.js.map +1 -1
  247. package/dist/utils/guards.d.ts +2 -0
  248. package/dist/utils/guards.d.ts.map +1 -0
  249. package/dist/utils/guards.js +4 -0
  250. package/dist/utils/guards.js.map +1 -0
  251. package/dist/utils/index.d.ts +3 -0
  252. package/dist/utils/index.d.ts.map +1 -1
  253. package/dist/utils/index.js +3 -0
  254. package/dist/utils/index.js.map +1 -1
  255. package/dist/utils/ip-classifier.d.ts +12 -0
  256. package/dist/utils/ip-classifier.d.ts.map +1 -0
  257. package/dist/utils/ip-classifier.js +153 -0
  258. package/dist/utils/ip-classifier.js.map +1 -0
  259. package/dist/utils/safe-fetch-transports.d.ts +40 -0
  260. package/dist/utils/safe-fetch-transports.d.ts.map +1 -0
  261. package/dist/utils/safe-fetch-transports.js +121 -0
  262. package/dist/utils/safe-fetch-transports.js.map +1 -0
  263. package/dist/utils/safe-fetch-types.d.ts +66 -0
  264. package/dist/utils/safe-fetch-types.d.ts.map +1 -0
  265. package/dist/utils/safe-fetch-types.js +10 -0
  266. package/dist/utils/safe-fetch-types.js.map +1 -0
  267. package/dist/utils/safe-fetch.d.ts +40 -0
  268. package/dist/utils/safe-fetch.d.ts.map +1 -0
  269. package/dist/utils/safe-fetch.js +188 -0
  270. package/dist/utils/safe-fetch.js.map +1 -0
  271. package/dist/utils/statuslist-purpose.d.ts +12 -0
  272. package/dist/utils/statuslist-purpose.d.ts.map +1 -0
  273. package/dist/utils/statuslist-purpose.js +19 -0
  274. package/dist/utils/statuslist-purpose.js.map +1 -0
  275. package/dist/utils/url.d.ts +2 -0
  276. package/dist/utils/url.d.ts.map +1 -0
  277. package/dist/utils/url.js +8 -0
  278. package/dist/utils/url.js.map +1 -0
  279. package/package.json +25 -5
  280. package/schemas/README.md +2 -1
  281. package/schemas/card-delegation-credential.json +239 -0
  282. package/schemas/kya-os-card.schema.json +284 -0
@@ -16,76 +16,21 @@
16
16
  */
17
17
  import { RuntimeFetchProvider, NoopFetchProvider } from "../providers/runtime-fetch.js";
18
18
  import { NoopAuditLogProvider } from "../providers/audit-log.js";
19
- import { SessionManager, } from "../session/manager.js";
19
+ import { MemoryGrantStore } from "../providers/grant-store.js";
20
+ import { SessionManager } from "../session/manager.js";
20
21
  import { ProofGenerator, } from "../proof/generator.js";
21
- import { validateHandshakeFormat } from "../session/manager.js";
22
- import { DelegationCredentialVerifier, } from "../delegation/vc-verifier.js";
23
- import { createDidKeyResolver } from "../delegation/did-key-resolver.js";
24
- import { assertHolderBinding, isHolderBindingApplicable, isKyaOsControlArg, toHolderBindingRequest, } from "../delegation/holder-binding.js";
25
22
  import { ProofVerifier } from "../proof/verifier.js";
26
23
  import { SystemClockProvider } from "../providers/system-clock.js";
27
24
  import { MemoryNonceCacheProvider } from "../providers/memory.js";
28
- import { scopeSatisfies } from "../delegation/scope-matcher.js";
29
- import { createDidWebResolver } from "../delegation/did-web-resolver.js";
30
- import { validateDelegationChain as validateDelegationChainCore, getDelegationScopes, } from "../delegation/chain-enforcement.js";
31
- import { createNeedsAuthorizationError, createNeedsApprovalError, } from "../types/protocol.js";
32
25
  import { logger } from "../logging/index.js";
33
- import { RiskClassifier } from "../policy/classifier.js";
34
- import { DefaultPolicyEngine } from "../policy/default-engine.js";
35
- import { verifyApprovalQuorum } from "../policy/approval.js";
36
- import { buildPolicyRequest } from "../policy/projection.js";
37
- import { KYA_OS_ERROR_CODES } from "../errors.js";
38
- import { canonicalizeJSON, parseVCJWT } from "../delegation/utils.js";
39
- import { base64urlDecodeToBytes, base64urlEncodeFromBytes, bytesToBase64 } from "../utils/base64.js";
40
- export const KYA_OS_ACTIONS = ["handshake", "identity", "reputation"];
41
- /**
42
- * Strip control characters and cap length on caller-derived values before they
43
- * are interpolated into client-facing reasons or log lines. A hostile
44
- * credential could otherwise embed newlines / control chars in an id or scope to
45
- * forge or corrupt log entries (log injection) or break a terminal. The fixed
46
- * parts of a reason contain no control chars, so sanitizing the whole assembled
47
- * string at the emission boundary is equivalent to sanitizing each interpolated
48
- * value, and is idempotent.
49
- */
50
- function sanitizeForMessage(value, maxLen = 256) {
51
- const s = typeof value === "string" ? value : String(value);
52
- // Replace C0/C1 control chars (incl. CR, LF, TAB, ESC, DEL) with U+FFFD so a
53
- // hostile id/scope cannot forge log lines or break a terminal. Filtered by
54
- // code point to keep this source ASCII-only (no literal control chars).
55
- let out = "";
56
- for (const ch of s) {
57
- const code = ch.codePointAt(0) ?? 0;
58
- out += code <= 0x1f || (code >= 0x7f && code <= 0x9f) ? "\uFFFD" : ch;
59
- }
60
- return out.length > maxLen ? `${out.slice(0, maxLen)}\u2026` : out;
61
- }
62
- /** Best-effort projection of a delegation VC into policy principal facts. */
63
- function extractPolicyPrincipal(del) {
64
- if (!del || typeof del !== "object") {
65
- return { agentDid: "unknown", delegatedScopes: [] };
66
- }
67
- try {
68
- const vc = del;
69
- const subject = vc.credentialSubject;
70
- const agentDid = subject?.delegation?.subjectDid ?? subject?.id ?? "unknown";
71
- const responsibleParty = subject?.delegation?.controller;
72
- let delegatedScopes = [];
73
- try {
74
- delegatedScopes = getDelegationScopes(vc);
75
- }
76
- catch {
77
- delegatedScopes = [];
78
- }
79
- return {
80
- agentDid,
81
- ...(responsibleParty ? { responsibleParty } : {}),
82
- delegatedScopes,
83
- };
84
- }
85
- catch {
86
- return { agentDid: "unknown", delegatedScopes: [] };
87
- }
88
- }
26
+ import { createGrantResolution } from "./with-kya-os.grants.js";
27
+ import { createPolicyGate } from "./with-kya-os.policy-gate.js";
28
+ import { createDelegationGate } from "./with-kya-os.delegation-gate.js";
29
+ import { createSessionProof } from "./with-kya-os.session.js";
30
+ import { createProtocol } from "./with-kya-os.protocol.js";
31
+ // The public type surface lives in `./with-kya-os.types.ts`; re-export it here so
32
+ // `import { KyaOsConfig, ... } from './with-kya-os'` continues to resolve unchanged.
33
+ export * from "./with-kya-os.types.js";
89
34
  /**
90
35
  * Create KYA-OS middleware for a standard MCP SDK Server.
91
36
  *
@@ -103,10 +48,13 @@ function extractPolicyPrincipal(del) {
103
48
  * @returns Middleware components for session management and proof generation
104
49
  *
105
50
  * @remarks
106
- * **Single-process only**: This middleware stores session state in memory using closure
107
- * variables (`activeSessionId`, `sessionNonces`). It is NOT suitable for multi-instance
108
- * deployments behind a load balancer. For distributed deployments, implement a custom
109
- * `SessionStore` backed by Redis, DynamoDB, or similar and pass it via `config.session`.
51
+ * **Session resolution**: proof generation resolves the session from the explicit
52
+ * `sessionId` threaded into each wrapper. A single-process `activeSessionId`
53
+ * fallback (the established handshake/auto session) is consulted ONLY when no
54
+ * sessionId was threaded e.g. non-KYA-OS clients (MCP Inspector) and the
55
+ * transport auto-proof path; a KYA-OS-aware call never depends on it. By default
56
+ * sessions live in an in-memory Map; for multi-instance deployments inject a
57
+ * durable `SessionStore` via `config.session.sessionStore`.
110
58
  */
111
59
  export function createKyaOsMiddleware(config, cryptoProvider) {
112
60
  const identity = {
@@ -126,6 +74,19 @@ export function createKyaOsMiddleware(config, cryptoProvider) {
126
74
  const proofGenerator = new ProofGenerator(identity, cryptoProvider);
127
75
  const delegationConfig = config.delegation;
128
76
  const auditLog = config.auditLog ?? new NoopAuditLogProvider();
77
+ // Emit the proof under the legacy bare key as well, by default, for pre-1.1
78
+ // back-compat. The value is identical under both keys and `_meta` is never
79
+ // hashed (§7.6), so the mirror is purely additive.
80
+ const emitLegacyProofKey = config.emitLegacyProofKey ?? true;
81
+ // Durable grant store for the no-paste retry. Defaults to in-memory; inject a
82
+ // shared, durable store for multi-instance / restart survival (mirrors the
83
+ // nonceCache precedent and its production warning).
84
+ const grantStore = config.grantStore ?? new MemoryGrantStore();
85
+ if (!config.grantStore) {
86
+ logger.warn("[kya-os] Using MemoryGrantStore — grants are lost on restart and not " +
87
+ "shared across instances. Inject a Redis / Durable Object / DB-backed " +
88
+ "GrantStore via config.grantStore for production / multi-instance use.");
89
+ }
129
90
  // Holder binding (spec §11.8). Built once so all tools share one replay cache.
130
91
  // The verifier derives the subject key from the did:key DID directly, so its
131
92
  // fetchProvider is unused on the phase-1 path — a never-called stub keeps
@@ -142,645 +103,46 @@ export function createKyaOsMiddleware(config, cryptoProvider) {
142
103
  ? new RuntimeFetchProvider()
143
104
  : new NoopFetchProvider()),
144
105
  });
145
- // Session map: sessionId last nonce (for proof generation)
146
- const sessionNonces = new Map();
147
- // Active session tracking — set after handshake (manual or auto)
148
- let activeSessionId;
149
- const handshakeTool = {
150
- name: "_kyaos_handshake",
151
- description: "KYA-OS identity handshake — establishes a cryptographic session",
152
- inputSchema: {
153
- type: "object",
154
- properties: {
155
- nonce: { type: "string", description: "Client-generated unique nonce" },
156
- audience: {
157
- type: "string",
158
- description: "Intended audience (server DID or URL)",
159
- },
160
- timestamp: { type: "number", description: "Unix epoch seconds" },
161
- agentDid: {
162
- type: "string",
163
- description: "Client agent DID (optional)",
164
- },
165
- },
166
- required: ["nonce", "audience", "timestamp"],
167
- },
168
- };
169
- const kyaOsTool = {
170
- name: "_kyaos",
171
- description: "KYA-OS protocol — identity verification, session handshake, and server metadata",
172
- inputSchema: {
173
- type: "object",
174
- properties: {
175
- action: {
176
- type: "string",
177
- enum: [...KYA_OS_ACTIONS],
178
- description: "Protocol operation to perform",
179
- },
180
- nonce: { type: "string", description: "Client-generated unique nonce" },
181
- audience: {
182
- type: "string",
183
- description: "Intended audience (server DID or URL)",
184
- },
185
- timestamp: { type: "number", description: "Unix epoch seconds" },
186
- agentDid: {
187
- type: "string",
188
- description: "Client agent DID (optional)",
189
- },
190
- },
191
- required: ["action"],
192
- },
106
+ // The constructed collaborators, threaded into each sub-factory.
107
+ const deps = {
108
+ identity,
109
+ config,
110
+ cryptoProvider,
111
+ sessionManager,
112
+ proofGenerator,
113
+ auditLog,
114
+ grantStore,
115
+ delegationConfig,
116
+ holderBindingMode,
117
+ holderBindingVerifier,
118
+ emitLegacyProofKey,
193
119
  };
194
- async function handleHandshake(args) {
195
- if (!validateHandshakeFormat(args)) {
196
- return {
197
- content: [
198
- {
199
- type: "text",
200
- text: JSON.stringify({
201
- success: false,
202
- error: {
203
- code: KYA_OS_ERROR_CODES.handshake_failed,
204
- message: "Invalid handshake format: requires nonce (string), audience (string), and timestamp (positive integer)",
205
- },
206
- }),
207
- },
208
- ],
209
- isError: true,
210
- };
211
- }
212
- const result = await sessionManager.validateHandshake(args);
213
- if (result.success && result.session) {
214
- sessionNonces.set(result.session.sessionId, result.session.nonce);
215
- activeSessionId = result.session.sessionId;
216
- }
217
- return {
218
- content: [
219
- {
220
- type: "text",
221
- text: JSON.stringify({
222
- success: result.success,
223
- ...(result.session && {
224
- sessionId: result.session.sessionId,
225
- serverDid: identity.did,
226
- serverKid: identity.kid,
227
- }),
228
- ...(result.error && { error: result.error }),
229
- }),
230
- },
231
- ],
232
- ...(result.error && { isError: true }),
233
- };
234
- }
235
- async function handleIdentity() {
236
- return {
237
- content: [
238
- {
239
- type: "text",
240
- text: JSON.stringify({
241
- did: identity.did,
242
- kid: identity.kid,
243
- name: config.identity.agentName ?? identity.did,
244
- capabilities: ["handshake", "signing", "verification"],
245
- protocolVersion: "1.0.0",
246
- clockSkewSeconds: sessionManager.getStats().config.timestampSkewSeconds,
247
- }),
248
- },
249
- ],
250
- };
251
- }
252
- async function handleKyaOs(args) {
253
- const action = typeof args.action === "string"
254
- ? args.action
255
- : undefined;
256
- switch (action) {
257
- case "handshake":
258
- return handleHandshake(args);
259
- case "identity":
260
- return handleIdentity();
261
- case "reputation":
262
- return {
263
- content: [
264
- {
265
- type: "text",
266
- text: JSON.stringify({
267
- success: false,
268
- error: {
269
- code: KYA_OS_ERROR_CODES.runtime_error,
270
- message: 'action: "reputation" is not yet implemented.',
271
- },
272
- }),
273
- },
274
- ],
275
- isError: true,
276
- };
277
- default:
278
- return {
279
- content: [
280
- {
281
- type: "text",
282
- text: JSON.stringify({
283
- success: false,
284
- error: {
285
- code: KYA_OS_ERROR_CODES.invalid_request,
286
- message: `Unknown _kyaos action: "${action ?? "(missing)"}". Valid actions: ${KYA_OS_ACTIONS.join(", ")}`,
287
- },
288
- }),
289
- },
290
- ],
291
- isError: true,
292
- };
293
- }
294
- }
295
- /**
296
- * Auto-create a session for proof generation when no handshake has occurred.
297
- * In production, KYA-OS-aware runtimes should execute handshake before tool calls.
298
- * This convenience mode allows non-KYA-OS clients (like MCP Inspector) to
299
- * still see proofs without manual handshake.
300
- */
301
- async function ensureSession() {
302
- if (activeSessionId) {
303
- const existing = await sessionManager.getSession(activeSessionId);
304
- if (existing)
305
- return activeSessionId;
306
- }
307
- if (!config.autoSession)
308
- return undefined;
309
- // Generate a server-side session with cryptographically random nonce (SPEC.md §4)
310
- const nonceBytes = await cryptoProvider.randomBytes(16);
311
- const nonce = base64urlEncodeFromBytes(nonceBytes);
312
- const timestamp = Math.floor(Date.now() / 1000);
313
- const result = await sessionManager.validateHandshake({
314
- nonce,
315
- audience: identity.did,
316
- timestamp,
317
- });
318
- if (result.success && result.session) {
319
- activeSessionId = result.session.sessionId;
320
- sessionNonces.set(result.session.sessionId, result.session.nonce);
321
- return activeSessionId;
322
- }
323
- return undefined;
324
- }
325
- function wrapWithProof(toolName, handler) {
326
- return async (args, sessionId, context) => {
327
- const result = await handler(args, sessionId, context);
328
- if (result.isError) {
329
- return result;
330
- }
331
- // Resolve session: explicit param → active session → auto-create
332
- const resolvedSessionId = sessionId ?? await ensureSession();
333
- if (!resolvedSessionId) {
334
- return result;
335
- }
336
- const session = await sessionManager.getSession(resolvedSessionId);
337
- if (!session) {
338
- return result;
339
- }
340
- try {
341
- const request = { method: toolName, params: args };
342
- const response = { data: result.content };
343
- const proof = await proofGenerator.generateProof(request, response, session, { scopeId: context?.scopeId });
344
- // Attach proof as _meta (rendered by MCP Inspector, invisible to LLMs)
345
- result._meta = { proof };
346
- // Hand the verified call to the audit sink. A sink failure MUST NOT
347
- // break the tool response, so it is logged and swallowed.
348
- try {
349
- await auditLog.logAuditRecord({
350
- identity: { did: identity.did, kid: identity.kid },
351
- session: { sessionId: session.sessionId, audience: session.audience },
352
- requestHash: proof.meta.requestHash,
353
- responseHash: proof.meta.responseHash,
354
- verified: "yes",
355
- scopeId: proof.meta.scopeId,
356
- });
357
- }
358
- catch (auditError) {
359
- logger.error("[kya-os] Audit log failed", {
360
- tool: toolName,
361
- error: auditError instanceof Error
362
- ? auditError.message
363
- : String(auditError),
364
- });
365
- }
366
- }
367
- catch (error) {
368
- logger.error("[kya-os] Proof generation failed", {
369
- tool: toolName,
370
- error: error instanceof Error ? error.message : String(error),
371
- });
372
- result._meta = {
373
- proofError: "Proof generation failed — response is unproven",
374
- };
375
- }
376
- return result;
377
- };
378
- }
379
- /**
380
- * Attach a signed proof recording an authorization OUTCOME to a response, so
381
- * rejected or pending privileged attempts leave a verifiable, non-repudiable
382
- * forensic record (these previously produced no proof). Used for `denied`,
383
- * `step_up_required`, and `needs_authorization` outcomes.
384
- *
385
- * When `responseData` is provided (e.g. the `needs_authorization` challenge
386
- * content), the proof binds a `responseHash` over it — so the signed proof
387
- * also attests the response body (notably the consent `authorizationUrl`),
388
- * letting a verifier detect a tampered/MITM-swapped URL. Pure denials and
389
- * step-ups pass no `responseData` (there is no response body to bind).
390
- *
391
- * Best-effort: if no session can be resolved or proof generation fails, the
392
- * original response is returned unchanged.
393
- */
394
- async function attachOutcomeProof(response, toolName, args, sessionId, reason, outcome = "denied", paramsOverride, responseData) {
395
- try {
396
- const resolvedSessionId = sessionId ?? (await ensureSession());
397
- if (!resolvedSessionId)
398
- return response;
399
- const session = await sessionManager.getSession(resolvedSessionId);
400
- if (!session)
401
- return response;
402
- // Prefer the caller's already-stripped args so the signed requestHash
403
- // matches the needs_approval / resumeToken requestHash exactly.
404
- let cleanArgs;
405
- if (paramsOverride !== undefined) {
406
- cleanArgs = paramsOverride;
407
- }
408
- else {
409
- cleanArgs = {};
410
- for (const [k, v] of Object.entries(args)) {
411
- if (k !== "_kyaos_delegation")
412
- cleanArgs[k] = v;
413
- }
414
- }
415
- const request = { method: toolName, params: cleanArgs };
416
- const proofResponse = responseData !== undefined ? { data: responseData } : undefined;
417
- const proof = await proofGenerator.generateProof(request, proofResponse, session, {
418
- outcome,
419
- reason: sanitizeForMessage(reason),
420
- });
421
- response._meta = { ...(response._meta ?? {}), proof };
422
- }
423
- catch (error) {
424
- logger.error("[kya-os] Outcome proof generation failed", {
425
- tool: toolName,
426
- error: error instanceof Error ? error.message : String(error),
427
- });
428
- }
429
- return response;
430
- }
431
- function wrapWithDelegation(toolName, config, handler) {
432
- const didKeyResolver = createDidKeyResolver();
433
- const fetchProvider = delegationConfig?.fetchProvider ??
434
- (typeof globalThis.fetch === "function"
435
- ? new RuntimeFetchProvider()
436
- : undefined);
437
- const didWebResolver = fetchProvider
438
- ? createDidWebResolver(fetchProvider)
439
- : undefined;
440
- const didResolver = {
441
- async resolve(did) {
442
- const customResolver = delegationConfig?.didResolver;
443
- if (customResolver) {
444
- const resolved = await customResolver.resolve(did);
445
- if (resolved) {
446
- return resolved;
447
- }
448
- }
449
- if (did.startsWith("did:key:")) {
450
- return didKeyResolver.resolve(did);
451
- }
452
- if (did.startsWith("did:web:")) {
453
- return didWebResolver?.resolve(did) ?? null;
454
- }
455
- return null;
456
- },
457
- };
458
- const signatureVerifier = async (vc, publicKeyJwk) => {
459
- const proof = vc.proof;
460
- if (!proof) {
461
- return { valid: false, reason: "Missing proof" };
462
- }
463
- const proofValue = proof["proofValue"];
464
- if (!proofValue) {
465
- return { valid: false, reason: "Missing proofValue in proof" };
466
- }
467
- // Reconstruct the unsigned VC (without proof) for signature verification
468
- const vcRecord = vc;
469
- const vcWithoutProof = {};
470
- for (const [k, v] of Object.entries(vcRecord)) {
471
- if (k !== "proof")
472
- vcWithoutProof[k] = v;
473
- }
474
- const canonical = canonicalizeJSON(vcWithoutProof);
475
- const data = new TextEncoder().encode(canonical);
476
- // Decode signature from base64url proof value
477
- const sigBytes = base64urlDecodeToBytes(proofValue);
478
- // Get public key from JWK (x is base64url-encoded raw key bytes)
479
- const jwk = publicKeyJwk;
480
- if (!jwk.x) {
481
- return { valid: false, reason: "No x field in publicKeyJwk" };
482
- }
483
- // Convert base64url key to standard base64 for the crypto provider
484
- const pubKeyBytes = base64urlDecodeToBytes(jwk.x);
485
- const pubKeyBase64 = bytesToBase64(pubKeyBytes);
486
- const valid = await cryptoProvider.verify(data, sigBytes, pubKeyBase64);
487
- return {
488
- valid,
489
- reason: valid ? undefined : "Signature verification failed",
490
- };
491
- };
492
- const verifier = new DelegationCredentialVerifier({
493
- didResolver,
494
- signatureVerifier,
495
- statusListResolver: delegationConfig?.statusListResolver,
496
- });
497
- const buildDelegationErrorResponse = (error, reason) => ({
498
- content: [
499
- {
500
- type: "text",
501
- text: JSON.stringify({ error, reason: sanitizeForMessage(reason) }),
502
- },
503
- ],
504
- isError: true,
505
- });
506
- // Thin adapter over the framework-agnostic core (E3.1 · chain-enforcement.ts).
507
- // The walk/attenuation/audience/§11.6/revocation rules live in one place;
508
- // this binds the host's injected verifier + server DID + resolver + the
509
- // optional graph-backed RevocationChecker.
510
- const validateDelegationChain = (leafCredential, options) => validateDelegationChainCore(leafCredential, {
511
- serverDid: identity.did,
512
- verifier,
513
- resolveDelegationChain: delegationConfig?.resolveDelegationChain,
514
- statusListConfigured: !!delegationConfig?.statusListResolver,
515
- revocationChecker: delegationConfig?.revocationChecker,
516
- }, options);
517
- return async (args, sessionId) => {
518
- const delegationArg = args["_kyaos_delegation"];
519
- if (delegationArg === undefined || delegationArg === null) {
520
- // No delegation provided — return needs_authorization response
521
- const tokenBytes = await cryptoProvider.randomBytes(16);
522
- const hex = Array.from(tokenBytes)
523
- .map((b) => b.toString(16).padStart(2, "0"))
524
- .join("");
525
- const resumeToken = [
526
- hex.slice(0, 8),
527
- hex.slice(8, 12),
528
- hex.slice(12, 16),
529
- hex.slice(16, 20),
530
- hex.slice(20),
531
- ].join("-");
532
- const expiresAt = Math.floor(Date.now() / 1000) + 300;
533
- const authError = createNeedsAuthorizationError({
534
- message: `Tool "${toolName}" requires delegation with scope: ${config.scopeId}`,
535
- authorizationUrl: config.consentUrl,
536
- resumeToken,
537
- expiresAt,
538
- scopes: [config.scopeId],
539
- });
540
- // Sign the challenge (outcome=needs_authorization). The proof binds a
541
- // responseHash over the EMITTED challenge content — including the
542
- // authorizationUrl. A verifier that recomputes the response hash over the
543
- // content it received (ProofVerifier.verifyProof(proof, jwk, { request,
544
- // response })) thereby detects a tampered/MITM-swapped consent URL; the
545
- // signature alone proves authenticity, not content-match. config.format-
546
- // Challenge lets a server render the challenge (e.g. a markdown link for
547
- // LLM clients) BEFORE signing, so the proof binds exactly what the client
548
- // receives. A throwing hook falls back to the default challenge (never
549
- // -32603). Best-effort: attachOutcomeProof no-ops if no session resolves.
550
- const defaultChallengeContent = [
551
- { type: "text", text: JSON.stringify(authError) },
552
- ];
553
- let challengeContent = defaultChallengeContent;
554
- if (config.formatChallenge) {
555
- try {
556
- challengeContent = config.formatChallenge(authError);
557
- }
558
- catch (error) {
559
- logger.error("[kya-os] formatChallenge threw; using the default challenge", {
560
- tool: toolName,
561
- error: error instanceof Error ? error.message : String(error),
562
- });
563
- challengeContent = defaultChallengeContent;
564
- }
565
- }
566
- return attachOutcomeProof({ content: challengeContent }, toolName, args, sessionId, authError.message, "needs_authorization", undefined, challengeContent);
567
- }
568
- // Accept delegation as either a JSON object (embedded proof) or a
569
- // VC-JWT string (compact JWT). The Cloudflare consent service issues
570
- // VC-JWTs; examples use embedded proofs. Support both transparently.
571
- let vc;
572
- let isVCJWT = false;
573
- if (typeof delegationArg === "string") {
574
- const parsed = parseVCJWT(delegationArg);
575
- if (!parsed || !parsed.payload.vc) {
576
- return attachOutcomeProof(buildDelegationErrorResponse(KYA_OS_ERROR_CODES.delegation_invalid, "Invalid VC-JWT format"), toolName, args, sessionId, "Invalid VC-JWT format");
577
- }
578
- vc = parsed.payload.vc;
579
- // VC-JWTs don't have an embedded proof — the JWT signature is the
580
- // proof. Add a marker so basic validation (which checks for proof
581
- // presence) passes. The actual signature is in the JWT envelope.
582
- if (!vc.proof) {
583
- vc = { ...vc, proof: { type: 'JwtProof2020', jwt: delegationArg } };
584
- }
585
- isVCJWT = true;
586
- }
587
- else {
588
- vc = delegationArg;
589
- }
590
- // For VC-JWTs the embedded-signature check is skipped (the JWT envelope
591
- // signature is the proof); schema/expiry/status/scope checks still apply.
592
- // validateDelegationChain performs the shape check and returns
593
- // { valid, reason } for malformed input, never throwing on normal or
594
- // structurally-malformed credentials. This try/catch is therefore a PURE
595
- // BACKSTOP — it fires only on a truly unexpected throw (e.g. a hostile
596
- // getter/Proxy accessor or a provider fault): it logs the detail
597
- // server-side and returns a GENERIC reason, so no internal/implementation
598
- // detail (stack, raw error text) leaks to the client or the signed proof.
599
- const verificationResult = await (async () => {
600
- try {
601
- return await validateDelegationChain(vc, { skipSignature: isVCJWT });
602
- }
603
- catch (error) {
604
- logger.error("[kya-os] Unexpected error verifying delegation", {
605
- tool: toolName,
606
- error: error instanceof Error ? error.message : String(error),
607
- stack: error instanceof Error ? error.stack : undefined,
608
- });
609
- return { valid: false, reason: "Delegation credential could not be verified" };
610
- }
611
- })();
612
- if (!verificationResult.valid) {
613
- const reason = verificationResult.reason ?? "Unknown delegation validation error";
614
- logger.warn(`[kya-os] Delegation verification failed for "${toolName}": ${sanitizeForMessage(reason)}`);
615
- return attachOutcomeProof(buildDelegationErrorResponse(KYA_OS_ERROR_CODES.delegation_invalid, reason), toolName, args, sessionId, reason);
616
- }
617
- // Holder binding (spec §11.8): the delegation is valid, but a valid
618
- // *credential* is a bearer token until we also prove the caller holds the
619
- // delegation SUBJECT's key. Opt-in via `delegation.holderBinding`. did:key
620
- // subjects are bound here; did:web is deferred to cnf binding (phase 2) and
621
- // logged, never rejected. Runs after identity is established, before scope.
622
- if (holderBindingMode !== "off" && holderBindingVerifier) {
623
- const subjectDid = vc.credentialSubject?.id;
624
- if (subjectDid && isHolderBindingApplicable(subjectDid)) {
625
- const proofArg = args["_kyaos_proof"];
626
- if (proofArg === undefined) {
627
- const reason = "Holder-of-key proof (_kyaos_proof) is required for this delegation subject";
628
- logger.warn(`[kya-os] Holder binding: "${toolName}" called without _kyaos_proof`);
629
- if (holderBindingMode === "enforce") {
630
- return attachOutcomeProof(buildDelegationErrorResponse(KYA_OS_ERROR_CODES.holder_binding_failed, reason), toolName, args, sessionId, reason);
631
- }
632
- }
633
- else {
634
- let parsedProof = proofArg;
635
- if (typeof proofArg === "string") {
636
- try {
637
- parsedProof = JSON.parse(proofArg);
638
- }
639
- catch {
640
- parsedProof = {};
641
- }
642
- }
643
- const binding = await assertHolderBinding({
644
- proof: parsedProof,
645
- subjectDid,
646
- request: toHolderBindingRequest(toolName, args),
647
- expectedAudience: identity.did,
648
- proofVerifier: holderBindingVerifier,
649
- });
650
- if (binding.status !== "bound") {
651
- const reason = binding.reason ??
652
- "Holder-of-key proof did not bind the delegation subject";
653
- logger.warn(`[kya-os] Holder binding ${binding.status} for "${toolName}": ${sanitizeForMessage(reason)}`);
654
- if (holderBindingMode === "enforce") {
655
- return attachOutcomeProof(buildDelegationErrorResponse(KYA_OS_ERROR_CODES.holder_binding_failed, reason), toolName, args, sessionId, reason);
656
- }
657
- }
658
- }
659
- }
660
- else if (subjectDid) {
661
- // Non-did:key subject — phase 1 cannot pin its key; defer to cnf
662
- // binding (phase 2) rather than reject legitimate traffic.
663
- logger.warn(`[kya-os] Holder binding: subject "${subjectDid}" is not did:key; deferring to cnf binding (phase 2)`);
664
- }
665
- }
666
- // Safe to call directly: the structural guard + validateDelegationChain
667
- // above guarantee a well-formed credential here, and scopeSatisfies is
668
- // bounded (ReDoS-guarded) and returns rather than throws.
669
- const scopeResult = scopeSatisfies(config.scopeId, vc);
670
- if (scopeResult.usedNonExactMatcher) {
671
- logger.warn(`[kya-os] Scope "${config.scopeId}" for "${toolName}" granted via a non-exact ` +
672
- `(prefix/regex) matcher. Verify this is intended — non-exact matchers widen authority.`);
673
- }
674
- if (!scopeResult.satisfied) {
675
- const reason = `Required scope "${config.scopeId}" not in delegation scopes`;
676
- logger.warn(`[kya-os] Delegation missing required scope "${config.scopeId}" for "${toolName}"`);
677
- return attachOutcomeProof(buildDelegationErrorResponse(KYA_OS_ERROR_CODES.insufficient_scope, reason), toolName, args, sessionId, reason);
678
- }
679
- // Strip the reserved _kyaos* control namespace before passing to the
680
- // handler — same predicate the bound request hash uses, so the handler
681
- // receives exactly the call the subject signed (no smuggled control arg).
682
- const cleanArgs = {};
683
- for (const [k, v] of Object.entries(args)) {
684
- if (!isKyaOsControlArg(k))
685
- cleanArgs[k] = v;
686
- }
687
- logger.debug(`[kya-os] Delegation verified for "${toolName}", scope "${config.scopeId}"`);
688
- return handler(cleanArgs, sessionId, { scopeId: config.scopeId });
689
- };
690
- }
691
- const defaultRiskClassifier = new RiskClassifier();
692
- const defaultPolicyEngine = new DefaultPolicyEngine();
693
- /**
694
- * Per-action policy / step-up gate. Composed AFTER wrapWithDelegation (which
695
- * enforces identity + scope); this wrapper adds the PROPORTIONALITY layer:
696
- * classify the action's risk, ask a pluggable PolicyEngine, and either allow,
697
- * deny (with a signed denial proof), or require N-of-M human approval
698
- * (needs_approval) before the handler runs. It forces a decision point — it
699
- * does not itself supply judgment.
700
- */
701
- function withPolicyGate(toolName, handler, opts = {}) {
702
- const engine = opts.engine ?? defaultPolicyEngine;
703
- const classifier = opts.classifier ?? defaultRiskClassifier;
704
- const approvalsKey = opts.approvalsArgKey ?? "_kyaos_approvals";
705
- const isValidApprovalSignature = opts.isValidApprovalSignature ?? (async () => false);
706
- return async (args, sessionId) => {
707
- // Drop the reserved _kyaos* namespace plus the (possibly custom) approvals
708
- // key, so no control arg reaches the handler.
709
- const cleanArgs = {};
710
- for (const [k, v] of Object.entries(args)) {
711
- if (!isKyaOsControlArg(k) && k !== approvalsKey)
712
- cleanArgs[k] = v;
713
- }
714
- const namespace = opts.resolveNamespace?.(args) ?? toolName;
715
- const risk = classifier.classify({ toolName, namespace });
716
- const principal = extractPolicyPrincipal(args["_kyaos_delegation"]);
717
- const policyRequest = buildPolicyRequest({
718
- principal: {
719
- agentDid: principal.agentDid,
720
- ...(principal.responsibleParty
721
- ? { responsibleParty: principal.responsibleParty }
722
- : {}),
723
- },
724
- action: { toolName },
725
- resource: { namespace },
726
- delegatedScopes: principal.delegatedScopes,
727
- scopeMatched: opts.scopeMatched ?? false,
728
- risk,
729
- });
730
- const decision = await engine.evaluate(policyRequest);
731
- if (decision.decision === "allow") {
732
- return handler(cleanArgs, sessionId);
733
- }
734
- if (decision.decision === "deny") {
735
- const denied = {
736
- content: [
737
- {
738
- type: "text",
739
- text: JSON.stringify({
740
- error: KYA_OS_ERROR_CODES.policy_denied,
741
- reason: sanitizeForMessage(decision.reason),
742
- }),
743
- },
744
- ],
745
- isError: true,
746
- };
747
- return attachOutcomeProof(denied, toolName, args, sessionId, decision.reason, "denied", cleanArgs);
748
- }
749
- // step_up: verify any supplied approval grants, bound to this exact action.
750
- const requestHash = await proofGenerator.hashRequest({
751
- method: toolName,
752
- params: cleanArgs,
753
- });
754
- const grants = Array.isArray(args[approvalsKey])
755
- ? args[approvalsKey]
756
- : [];
757
- const quorumResult = await verifyApprovalQuorum(grants, requestHash, decision.quorum, isValidApprovalSignature);
758
- if (quorumResult.satisfied) {
759
- return handler(cleanArgs, sessionId);
760
- }
761
- const needsApproval = createNeedsApprovalError({
762
- message: `Tool "${toolName}" requires ${decision.quorum.n}-of-N approval before it may proceed (${sanitizeForMessage(decision.reason)}).`,
763
- resumeToken: `step_up:${requestHash}`,
764
- expiresAt: Math.floor(Date.now() / 1000) + 300,
765
- requestHash,
766
- quorum: decision.quorum,
767
- });
768
- const stepUp = {
769
- content: [{ type: "text", text: JSON.stringify(needsApproval) }],
770
- isError: true,
771
- };
772
- return attachOutcomeProof(stepUp, toolName, args, sessionId, decision.reason, "step_up_required", cleanArgs);
773
- };
774
- }
120
+ // Session establishment + proof attachment (owns the activeSessionId fallback);
121
+ // the `_kyaos` protocol surface routes to its handshake handler.
122
+ const session = createSessionProof(deps);
123
+ const protocol = createProtocol(deps, {
124
+ handleHandshake: session.handleHandshake,
125
+ });
126
+ // Durable-grant resolution (the no-paste retry). Depends only on the immutable
127
+ // deps above, so it lifts out cleanly with no shared session state.
128
+ const { resolveExistingGrant, bindGrantOnSuccess } = createGrantResolution(deps);
129
+ const { wrapWithDelegation } = createDelegationGate(deps, {
130
+ attachOutcomeProof: session.attachOutcomeProof,
131
+ resolveExistingGrant,
132
+ bindGrantOnSuccess,
133
+ });
134
+ const { withPolicyGate } = createPolicyGate(deps, {
135
+ attachOutcomeProof: session.attachOutcomeProof,
136
+ });
775
137
  return {
776
138
  identity: config.identity,
777
139
  sessionManager,
778
140
  proofGenerator,
779
- kyaOsTool,
780
- handshakeTool,
781
- handleKyaOs,
782
- handleHandshake,
783
- wrapWithProof,
141
+ kyaOsTool: protocol.kyaOsTool,
142
+ handshakeTool: protocol.handshakeTool,
143
+ handleKyaOs: protocol.handleKyaOs,
144
+ handleHandshake: session.handleHandshake,
145
+ wrapWithProof: session.wrapWithProof,
784
146
  wrapWithDelegation,
785
147
  withPolicyGate,
786
148
  };