@try-works/dsh-model-as-tool 0.1.3 → 0.4.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 (260) hide show
  1. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  2. package/bin/dsh-tool-mode.mjs +202 -0
  3. package/catalog/desert-ant-models.json +693 -693
  4. package/client/client.model-tools.js +614 -0
  5. package/client/index.js +1717 -0
  6. package/lib/adapters/index.d.ts.map +1 -1
  7. package/lib/adapters/index.js +4 -4
  8. package/lib/bindings.d.ts.map +1 -1
  9. package/lib/bindings.js +26 -25
  10. package/lib/capability.d.ts.map +1 -1
  11. package/lib/capability.js +17 -0
  12. package/lib/capture-path.d.ts +97 -0
  13. package/lib/capture-path.d.ts.map +1 -0
  14. package/lib/capture-path.js +152 -0
  15. package/lib/diagnostics.d.ts +166 -0
  16. package/lib/diagnostics.d.ts.map +1 -0
  17. package/lib/diagnostics.js +135 -0
  18. package/lib/extension-dependencies.d.ts +325 -0
  19. package/lib/extension-dependencies.d.ts.map +1 -0
  20. package/lib/extension-dependencies.js +858 -0
  21. package/lib/extension-dispatch.d.ts +117 -0
  22. package/lib/extension-dispatch.d.ts.map +1 -0
  23. package/lib/extension-dispatch.js +124 -0
  24. package/lib/extension-registrar.d.ts +100 -0
  25. package/lib/extension-registrar.d.ts.map +1 -0
  26. package/lib/extension-registrar.js +134 -0
  27. package/lib/extensions-api.d.ts +221 -0
  28. package/lib/extensions-api.d.ts.map +1 -0
  29. package/lib/extensions-api.js +17 -0
  30. package/lib/extensions-remote.d.ts +54 -0
  31. package/lib/extensions-remote.d.ts.map +1 -0
  32. package/lib/extensions-remote.js +66 -0
  33. package/lib/extensions-service.d.ts +367 -0
  34. package/lib/extensions-service.d.ts.map +1 -0
  35. package/lib/extensions-service.js +1527 -0
  36. package/lib/harness-llm.d.ts +138 -0
  37. package/lib/harness-llm.d.ts.map +1 -0
  38. package/lib/harness-llm.js +218 -0
  39. package/lib/harness-serving.d.ts +95 -0
  40. package/lib/harness-serving.d.ts.map +1 -0
  41. package/lib/harness-serving.js +157 -0
  42. package/lib/headless.d.ts +97 -0
  43. package/lib/headless.d.ts.map +1 -0
  44. package/lib/headless.js +349 -0
  45. package/lib/http-acquisition.d.ts +27 -0
  46. package/lib/http-acquisition.d.ts.map +1 -0
  47. package/lib/http-acquisition.js +136 -0
  48. package/lib/index.d.ts +271 -7
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +2644 -70
  51. package/lib/invocation-accounting.d.ts +169 -0
  52. package/lib/invocation-accounting.d.ts.map +1 -0
  53. package/lib/invocation-accounting.js +148 -0
  54. package/lib/legacy-migration.d.ts +95 -0
  55. package/lib/legacy-migration.d.ts.map +1 -0
  56. package/lib/legacy-migration.js +429 -0
  57. package/lib/managed-workers.d.ts +151 -0
  58. package/lib/managed-workers.d.ts.map +1 -0
  59. package/lib/managed-workers.js +249 -0
  60. package/lib/mcp/client-grant.d.ts +74 -0
  61. package/lib/mcp/client-grant.d.ts.map +1 -0
  62. package/lib/mcp/client-grant.js +60 -0
  63. package/lib/mcp/credential-cli.d.ts +38 -0
  64. package/lib/mcp/credential-cli.d.ts.map +1 -0
  65. package/lib/mcp/credential-cli.js +148 -0
  66. package/lib/mcp/credential-store.d.ts +143 -0
  67. package/lib/mcp/credential-store.d.ts.map +1 -0
  68. package/lib/mcp/credential-store.js +454 -0
  69. package/lib/mcp/host-binding.d.ts +25 -0
  70. package/lib/mcp/host-binding.d.ts.map +1 -1
  71. package/lib/mcp/host-binding.js +107 -14
  72. package/lib/mcp/http.d.ts +68 -18
  73. package/lib/mcp/http.d.ts.map +1 -1
  74. package/lib/mcp/http.js +390 -62
  75. package/lib/mcp/oauth.d.ts +149 -0
  76. package/lib/mcp/oauth.d.ts.map +1 -0
  77. package/lib/mcp/oauth.js +396 -0
  78. package/lib/mcp/protocol.d.ts +46 -7
  79. package/lib/mcp/protocol.d.ts.map +1 -1
  80. package/lib/mcp/protocol.js +63 -16
  81. package/lib/mcp/rate-limit.d.ts +68 -0
  82. package/lib/mcp/rate-limit.d.ts.map +1 -0
  83. package/lib/mcp/rate-limit.js +114 -0
  84. package/lib/mcp/sdk-v2.d.ts +17 -0
  85. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  86. package/lib/mcp/sdk-v2.js +181 -0
  87. package/lib/mcp/server.d.ts +11 -1
  88. package/lib/mcp/server.d.ts.map +1 -1
  89. package/lib/mcp/server.js +80 -13
  90. package/lib/mcp/stdio.d.ts.map +1 -1
  91. package/lib/mcp/stdio.js +9 -1
  92. package/lib/mcp/subscriptions.d.ts +43 -1
  93. package/lib/mcp/subscriptions.d.ts.map +1 -1
  94. package/lib/mcp/subscriptions.js +90 -9
  95. package/lib/model-qualification.d.ts +192 -0
  96. package/lib/model-qualification.d.ts.map +1 -0
  97. package/lib/model-qualification.js +178 -0
  98. package/lib/planning.d.ts +246 -0
  99. package/lib/planning.d.ts.map +1 -0
  100. package/lib/planning.js +457 -0
  101. package/lib/prepared-assets.d.ts +178 -0
  102. package/lib/prepared-assets.d.ts.map +1 -0
  103. package/lib/prepared-assets.js +286 -0
  104. package/lib/provider-bridge.d.ts +37 -0
  105. package/lib/provider-bridge.d.ts.map +1 -1
  106. package/lib/provider-bridge.js +189 -12
  107. package/lib/ptc-authority.d.ts +140 -0
  108. package/lib/ptc-authority.d.ts.map +1 -0
  109. package/lib/ptc-authority.js +216 -0
  110. package/lib/ptc-runtime.d.ts +156 -0
  111. package/lib/ptc-runtime.d.ts.map +1 -0
  112. package/lib/ptc-runtime.js +191 -0
  113. package/lib/publication.d.ts +2 -1
  114. package/lib/publication.d.ts.map +1 -1
  115. package/lib/publication.js +13 -5
  116. package/lib/receipt.d.ts.map +1 -1
  117. package/lib/receipt.js +8 -0
  118. package/lib/title-proposal.d.ts +194 -0
  119. package/lib/title-proposal.d.ts.map +1 -0
  120. package/lib/title-proposal.js +244 -0
  121. package/lib/tool-mode-run.d.ts +235 -0
  122. package/lib/tool-mode-run.d.ts.map +1 -0
  123. package/lib/tool-mode-run.js +283 -0
  124. package/lib/types.d.ts +3 -1
  125. package/lib/types.d.ts.map +1 -1
  126. package/lib/update-surface.d.ts +79 -0
  127. package/lib/update-surface.d.ts.map +1 -0
  128. package/lib/update-surface.js +224 -0
  129. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
  139. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
  140. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
  141. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
  142. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
  143. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
  144. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
  145. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
  146. package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
  147. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  148. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  149. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  150. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  151. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  152. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  153. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  154. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  155. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  156. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +32 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +32 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +243 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  213. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  214. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  215. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  216. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  217. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  218. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  219. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  220. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  221. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  222. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  223. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  241. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  242. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  243. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  244. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  245. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  246. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  247. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  248. package/package.json +43 -15
  249. package/remote/spec.mjs +462 -0
  250. package/remote/typert.host.mjs +14 -0
  251. package/remote/typert.remote-client.mjs +16 -0
  252. package/lib/adapters/tongue.d.ts +0 -31
  253. package/lib/adapters/tongue.d.ts.map +0 -1
  254. package/lib/adapters/tongue.js +0 -93
  255. package/lib/adapters/voz.d.ts +0 -28
  256. package/lib/adapters/voz.d.ts.map +0 -1
  257. package/lib/adapters/voz.js +0 -102
  258. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  259. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  260. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The operator-owned client credential store (SEC-01).
3
+ *
4
+ * SEC-01's generation clause: credentials are generated with high-entropy secrets,
5
+ * stored with suitable host protection (owner-only), and revocable independently per
6
+ * application. Before this module the OAuth client registry was in-memory only and the
7
+ * static-token mode accepted exactly one configured token — nothing could GENERATE a
8
+ * credential and nothing persisted it, so a restart lost every client and a generated
9
+ * secret could not exist.
10
+ *
11
+ * This store is the durable half. The security properties it now holds, each pinned
12
+ * by test:
13
+ *
14
+ * - one JSON document per deployment (`credentials.json`), written to a CRYPTO-RANDOM
15
+ * temp name opened EXCLUSIVELY (`wx`) — a predictable temp name is a hardlink target
16
+ * an attacker on the same account can pre-plant, so the name is unguessable and a
17
+ * pre-existing name FAILS instead of being overwritten — then fsync'd and atomically
18
+ * renamed over the document. A rename replaces a planted SYMLINK at the target rather
19
+ * than following it;
20
+ * - Windows: an EXPLICIT owner-only DACL is applied to the temp file BEFORE the rename
21
+ * (inheritance removed, the owner alone granted full control) and the store fails
22
+ * closed when the ACL cannot be secured. POSIX mode 0600 alone is not sufficient on
23
+ * Windows, and "the profile directory inherits it" is not a control;
24
+ * - MUTATIONS serialize through an in-process mutex AND an exclusive sidecar lock file,
25
+ * and re-read the document inside the critical section — two concurrent writers can
26
+ * no longer each persist a snapshot that silently drops the other's entry;
27
+ * - a CORRUPT or unreadable document makes MUTATIONS fail closed (a generation refused,
28
+ * the bytes untouched) while READS deny: authentication cannot succeed on a store
29
+ * the host cannot vouch for, and a generation can never overwrite a malformed
30
+ * document "as if empty". A MISSING document is the only empty start;
31
+ * - reads are served from the FILE, fresh on every lookup, so a revocation through any
32
+ * surface takes effect on the very next request;
33
+ * - `bySecret` matches STATIC-TOKEN credentials only: an OAuth resource-server secret
34
+ * presented as a static bearer must never authenticate through this path (scheme
35
+ * confusion);
36
+ * - the secret is compared CONSTANT-TIME (SHA-256 digests over equal-length input);
37
+ * - every mutation throws when it cannot persist: a generation that cannot be written
38
+ * must leave nothing usable rather than an in-memory-only credential that a restart
39
+ * silently forgets (fail-closed, SEC-01).
40
+ *
41
+ * @module @try-works/dsh-model-as-tool/mcp/credential-store
42
+ */
43
+ /** The scheme a stored credential authenticates under. */
44
+ export type CredentialScheme = 'static-token' | 'oauth-resource-server';
45
+ /** One persisted client credential. */
46
+ export interface StoredClientCredential {
47
+ /** The client identity, recorded in receipts and grants. */
48
+ readonly clientId: string;
49
+ /** The secret: crypto-random, shown once at generation, never re-echoed. */
50
+ readonly secret: string;
51
+ /** The scheme the secret is presented under. */
52
+ readonly scheme: CredentialScheme;
53
+ /** The scope the client may be granted (OAuth); empty for static tokens. */
54
+ readonly scope: readonly string[];
55
+ /** ISO timestamp of generation. */
56
+ readonly createdAt: string;
57
+ }
58
+ /** The store a generation writes and an auth layer reads. */
59
+ export interface ClientCredentialStore {
60
+ /** The file the credentials live in. */
61
+ readonly file: string;
62
+ /** Every live credential, WITHOUT a way to re-echo secrets beyond what the file holds. */
63
+ list(): readonly StoredClientCredential[];
64
+ /** One credential by client identity. */
65
+ get(clientId: string): StoredClientCredential | undefined;
66
+ /**
67
+ * Resolve a PRESENTED secret to its credential, in constant time.
68
+ *
69
+ * STATIC-TOKEN SCHEME ONLY: this is the static-bearer lookup, and an OAuth
70
+ * resource-server secret must never match here (scheme confusion). OAuth
71
+ * credentials authenticate through the OAuth verifier, never by raw secret.
72
+ */
73
+ bySecret(secret: string): StoredClientCredential | undefined;
74
+ /** Write a credential; throws when persistence fails, registering nothing usable. */
75
+ upsert(credential: StoredClientCredential): void;
76
+ /** Delete one credential; throws when the deletion cannot be persisted. */
77
+ delete(clientId: string): void;
78
+ }
79
+ /** A document that EXISTS but cannot be vouched for. Mutations refuse; reads deny. */
80
+ export declare class CorruptCredentialStoreError extends Error {
81
+ constructor(message: string);
82
+ }
83
+ /** A generated credential for the human-only CLI and tests (SEC-01, shown once). */
84
+ export interface GeneratedCredential {
85
+ readonly clientId: string;
86
+ readonly secret: string;
87
+ readonly scheme: CredentialScheme;
88
+ readonly scope: readonly string[];
89
+ readonly createdAt: string;
90
+ }
91
+ /**
92
+ * Generate a fresh client credential directly over the store (the primitive
93
+ * the human-only CLI and tests use — the model-visible `model_tools` surface
94
+ * refuses generation entirely, SEC-01: the secret must never reach a model).
95
+ * @param store - the durable store.
96
+ * @param options - clientId (≤64 chars), scheme, scope.
97
+ * @returns the credential with the secret exactly once.
98
+ */
99
+ export declare function generateCredential(store: ClientCredentialStore, options: {
100
+ readonly clientId: string;
101
+ readonly scheme: CredentialScheme;
102
+ readonly scope?: readonly string[];
103
+ }): GeneratedCredential;
104
+ /**
105
+ * Derive the default credential-store file, next to the host's own state
106
+ * (SEC-01: owner-only home protection).
107
+ * @param env - the environment to read; defaults to the process environment.
108
+ * @returns the absolute path.
109
+ */
110
+ export declare function defaultCredentialStoreFile(env?: NodeJS.ProcessEnv): string;
111
+ /**
112
+ * Apply an explicit owner-only DACL to one file on Windows (icacls: inheritance
113
+ * removed, the current owner alone granted full control), and fail closed when
114
+ * the ACL cannot be secured. On POSIX the caller's 0600 mode bits are the
115
+ * control and this is a no-op.
116
+ * @param file - the file to secure, BEFORE it is published (renamed) into place.
117
+ */
118
+ export declare function applyOwnerOnlyDacl(file: string): void;
119
+ /**
120
+ * TEST-ONLY hooks for the store's lock machinery. Production callers pass
121
+ * nothing: the hooks exist so the regressions can exercise a long-lived holder
122
+ * and the fail-closed liveness paths without multi-second waits.
123
+ */
124
+ export interface CredentialStoreTestHooks {
125
+ /** Sleep INSIDE the critical section, after the strict read (simulates a paused writer). */
126
+ readonly holdLockAfterReadMs?: number;
127
+ /** Override the stale-lock age threshold. */
128
+ readonly staleLockMs?: number;
129
+ /** Override how long a contender waits for the lock before failing closed. */
130
+ readonly lockDeadlineMs?: number;
131
+ /** Override the process-liveness probe. */
132
+ readonly isProcessAlive?: (pid: number) => boolean | undefined;
133
+ }
134
+ /**
135
+ * Create (or open) the file-backed credential store.
136
+ * @param file - the absolute path of the credentials document.
137
+ * @param hooks - TEST-ONLY lock hooks; production passes nothing.
138
+ * @returns the store.
139
+ */
140
+ export declare function createFileCredentialStore(file: string, hooks?: CredentialStoreTestHooks): ClientCredentialStore;
141
+ /** Remove a credential store file, for tests that need a fresh deployment. */
142
+ export declare function removeCredentialStoreFile(file: string): void;
143
+ //# sourceMappingURL=credential-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-store.d.ts","sourceRoot":"","sources":["../../src/mcp/credential-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAkBH,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,uBAAuB,CAAA;AAEvE,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,mCAAmC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACpC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0FAA0F;IAC1F,IAAI,IAAI,SAAS,sBAAsB,EAAE,CAAA;IACzC,yCAAyC;IACzC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CAAA;IACzD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CAAA;IAC5D,qFAAqF;IACrF,MAAM,CAAC,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAAA;IAChD,2EAA2E;IAC3E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAQD,sFAAsF;AACtF,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,qBAAqB,EAC5B,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC5G,mBAAmB,CAerB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAKvF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgBrD;AA8DD;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,4FAA4F;IAC5F,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;IACrC,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,CAAA;CAC/D;AA0CD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,wBAA6B,GAAG,qBAAqB,CAsOnH;AAED,8EAA8E;AAC9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAG5D"}
@@ -0,0 +1,454 @@
1
+ /**
2
+ * The operator-owned client credential store (SEC-01).
3
+ *
4
+ * SEC-01's generation clause: credentials are generated with high-entropy secrets,
5
+ * stored with suitable host protection (owner-only), and revocable independently per
6
+ * application. Before this module the OAuth client registry was in-memory only and the
7
+ * static-token mode accepted exactly one configured token — nothing could GENERATE a
8
+ * credential and nothing persisted it, so a restart lost every client and a generated
9
+ * secret could not exist.
10
+ *
11
+ * This store is the durable half. The security properties it now holds, each pinned
12
+ * by test:
13
+ *
14
+ * - one JSON document per deployment (`credentials.json`), written to a CRYPTO-RANDOM
15
+ * temp name opened EXCLUSIVELY (`wx`) — a predictable temp name is a hardlink target
16
+ * an attacker on the same account can pre-plant, so the name is unguessable and a
17
+ * pre-existing name FAILS instead of being overwritten — then fsync'd and atomically
18
+ * renamed over the document. A rename replaces a planted SYMLINK at the target rather
19
+ * than following it;
20
+ * - Windows: an EXPLICIT owner-only DACL is applied to the temp file BEFORE the rename
21
+ * (inheritance removed, the owner alone granted full control) and the store fails
22
+ * closed when the ACL cannot be secured. POSIX mode 0600 alone is not sufficient on
23
+ * Windows, and "the profile directory inherits it" is not a control;
24
+ * - MUTATIONS serialize through an in-process mutex AND an exclusive sidecar lock file,
25
+ * and re-read the document inside the critical section — two concurrent writers can
26
+ * no longer each persist a snapshot that silently drops the other's entry;
27
+ * - a CORRUPT or unreadable document makes MUTATIONS fail closed (a generation refused,
28
+ * the bytes untouched) while READS deny: authentication cannot succeed on a store
29
+ * the host cannot vouch for, and a generation can never overwrite a malformed
30
+ * document "as if empty". A MISSING document is the only empty start;
31
+ * - reads are served from the FILE, fresh on every lookup, so a revocation through any
32
+ * surface takes effect on the very next request;
33
+ * - `bySecret` matches STATIC-TOKEN credentials only: an OAuth resource-server secret
34
+ * presented as a static bearer must never authenticate through this path (scheme
35
+ * confusion);
36
+ * - the secret is compared CONSTANT-TIME (SHA-256 digests over equal-length input);
37
+ * - every mutation throws when it cannot persist: a generation that cannot be written
38
+ * must leave nothing usable rather than an in-memory-only credential that a restart
39
+ * silently forgets (fail-closed, SEC-01).
40
+ *
41
+ * @module @try-works/dsh-model-as-tool/mcp/credential-store
42
+ */
43
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
44
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
45
+ import { spawnSync } from 'node:child_process';
46
+ import { basename, dirname, join, parse } from 'node:path';
47
+ import { homedir } from 'node:os';
48
+ /** A document that EXISTS but cannot be vouched for. Mutations refuse; reads deny. */
49
+ export class CorruptCredentialStoreError extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = 'CorruptCredentialStoreError';
53
+ }
54
+ }
55
+ /**
56
+ * Generate a fresh client credential directly over the store (the primitive
57
+ * the human-only CLI and tests use — the model-visible `model_tools` surface
58
+ * refuses generation entirely, SEC-01: the secret must never reach a model).
59
+ * @param store - the durable store.
60
+ * @param options - clientId (≤64 chars), scheme, scope.
61
+ * @returns the credential with the secret exactly once.
62
+ */
63
+ export function generateCredential(store, options) {
64
+ if (typeof options.clientId !== 'string' || options.clientId.length === 0 || options.clientId.length > 64) {
65
+ throw new Error('clientId must be a non-empty string of at most 64 characters');
66
+ }
67
+ if (options.scheme !== 'static-token' && options.scheme !== 'oauth-resource-server') {
68
+ throw new Error('scheme must be static-token or oauth-resource-server');
69
+ }
70
+ if (store.get(options.clientId) !== undefined) {
71
+ throw new Error(`a credential for ${JSON.stringify(options.clientId)} already exists; revoke it first`);
72
+ }
73
+ const secret = randomBytes(32).toString('base64url');
74
+ const scope = options.scope === undefined || options.scope.length === 0 ? ['mcp:call'] : [...options.scope];
75
+ const createdAt = new Date().toISOString();
76
+ store.upsert({ clientId: options.clientId, secret, scheme: options.scheme, scope, createdAt });
77
+ return { clientId: options.clientId, secret, scheme: options.scheme, scope, createdAt };
78
+ }
79
+ /**
80
+ * Derive the default credential-store file, next to the host's own state
81
+ * (SEC-01: owner-only home protection).
82
+ * @param env - the environment to read; defaults to the process environment.
83
+ * @returns the absolute path.
84
+ */
85
+ export function defaultCredentialStoreFile(env = process.env) {
86
+ const home = env.DSH_HOME !== undefined && env.DSH_HOME.length > 0
87
+ ? env.DSH_HOME
88
+ : join(homedir(), '.dsh');
89
+ return join(home, 'model-as-tool', 'credentials.json');
90
+ }
91
+ /**
92
+ * Apply an explicit owner-only DACL to one file on Windows (icacls: inheritance
93
+ * removed, the current owner alone granted full control), and fail closed when
94
+ * the ACL cannot be secured. On POSIX the caller's 0600 mode bits are the
95
+ * control and this is a no-op.
96
+ * @param file - the file to secure, BEFORE it is published (renamed) into place.
97
+ */
98
+ export function applyOwnerOnlyDacl(file) {
99
+ if (process.platform !== 'win32')
100
+ return;
101
+ const owner = process.env.USERNAME;
102
+ if (owner === undefined || owner.length === 0) {
103
+ throw new Error('the credential file ACL cannot be secured: no Windows USERNAME to grant ownership to');
104
+ }
105
+ const result = spawnSync('icacls', [file, '/inheritance:r', '/grant:r', `${owner}:(F)`], {
106
+ windowsHide: true,
107
+ encoding: 'utf8',
108
+ });
109
+ if (result.status !== 0) {
110
+ throw new Error(`the credential file ACL cannot be secured: icacls failed with status ${result.status ?? 'unknown'}`
111
+ + `: ${(result.stderr ?? result.stdout ?? '').trim()}`);
112
+ }
113
+ }
114
+ /**
115
+ * Refuse a reparse point (symlink or Windows junction) at a path the store is
116
+ * about to write THROUGH: a junctioned credential home is an untrusted target,
117
+ * and the store fails closed rather than persisting plaintext secrets into a
118
+ * location the operator did not own by construction.
119
+ * @param path - the directory (or file) to check.
120
+ */
121
+ function assertNoReparsePoint(path) {
122
+ try {
123
+ if (lstatSync(path).isSymbolicLink()) {
124
+ throw new Error(`the credential store refuses ${path}: it is a symbolic link or junction (untrusted target)`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (error.code === 'ENOENT')
129
+ return;
130
+ throw error;
131
+ }
132
+ }
133
+ /**
134
+ * The trusted root the credential store lives under by default: DSH_HOME when
135
+ * set, otherwise `~/.dsh`. Ancestor validation walks from HERE down to the
136
+ * store directory — system junctions ABOVE the trusted root are never part of
137
+ * the trust question. A store outside the trusted root (an explicit --store)
138
+ * is validated from the filesystem root instead, and any reparse point in the
139
+ * chain fails it closed.
140
+ */
141
+ function trustedCredentialRoot(env = process.env) {
142
+ return env.DSH_HOME !== undefined && env.DSH_HOME.length > 0
143
+ ? env.DSH_HOME
144
+ : join(homedir(), '.dsh');
145
+ }
146
+ /**
147
+ * Walk EVERY ancestor component from the bounded trusted root down to the
148
+ * store directory and refuse any reparse point — not just the immediate parent:
149
+ * a junction higher in the chain redirects the whole write just as surely.
150
+ * @param file - the credential document path.
151
+ */
152
+ function assertTrustedStoreAncestors(file) {
153
+ const dir = dirname(file);
154
+ const root = trustedCredentialRoot();
155
+ const start = dir === root || dir.startsWith(root + '\\') || dir.startsWith(root + '/')
156
+ ? root
157
+ : parse(dir).root;
158
+ // Collect the ancestor chain from `start` down to `dir` (bounded by the path
159
+ // depth, which is small), then check each component that EXISTS.
160
+ const chain = [];
161
+ let cursor = dir;
162
+ while (cursor !== start && cursor !== parse(cursor).root && cursor.length > 0) {
163
+ chain.push(cursor);
164
+ const parent = dirname(cursor);
165
+ if (parent === cursor)
166
+ break;
167
+ cursor = parent;
168
+ }
169
+ chain.push(cursor);
170
+ for (const component of chain.reverse()) {
171
+ assertNoReparsePoint(component);
172
+ }
173
+ }
174
+ /** Is the pid alive? `undefined` means "cannot tell" — callers fail closed. */
175
+ function isProcessAlive(pid) {
176
+ if (!Number.isInteger(pid) || pid <= 0)
177
+ return undefined;
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ }
182
+ catch (error) {
183
+ if (error.code === 'ESRCH')
184
+ return false;
185
+ return undefined;
186
+ }
187
+ }
188
+ /** Read a lock record; undefined when the lock is unreadable or corrupt. */
189
+ function readLockRecord(lockPath) {
190
+ try {
191
+ const parsed = JSON.parse(readFileSync(lockPath, 'utf8'));
192
+ if (typeof parsed !== 'object' || parsed === null)
193
+ return undefined;
194
+ const candidate = parsed;
195
+ if (typeof candidate.token !== 'string' || candidate.token.length === 0
196
+ || typeof candidate.pid !== 'number' || typeof candidate.at !== 'number') {
197
+ return undefined;
198
+ }
199
+ return { token: candidate.token, pid: candidate.pid, at: candidate.at };
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
205
+ /** A real blocking sleep, without burning the CPU. */
206
+ function sleep(ms) {
207
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
208
+ }
209
+ /**
210
+ * Create (or open) the file-backed credential store.
211
+ * @param file - the absolute path of the credentials document.
212
+ * @param hooks - TEST-ONLY lock hooks; production passes nothing.
213
+ * @returns the store.
214
+ */
215
+ export function createFileCredentialStore(file, hooks = {}) {
216
+ const staleLockMs = hooks.staleLockMs ?? 30_000;
217
+ const lockDeadlineMs = hooks.lockDeadlineMs ?? 10_000;
218
+ const probeAlive = hooks.isProcessAlive ?? isProcessAlive;
219
+ /**
220
+ * The cross-process lock: an unguessable ownership token plus the holder's
221
+ * pid, so a stale lock is only ever stolen from a holder PROVEN DEAD. A live
222
+ * holder is never stolen from, unknown liveness fails closed (the contender
223
+ * keeps waiting, then refuses), and release removes ONLY a token-owned lock
224
+ * — a successor's lock is never touched.
225
+ */
226
+ const acquireFileLock = () => {
227
+ const lockPath = `${file}.lock`;
228
+ const token = randomBytes(16).toString('hex');
229
+ const deadline = Date.now() + lockDeadlineMs;
230
+ for (;;) {
231
+ try {
232
+ const fd = openSync(lockPath, 'wx');
233
+ try {
234
+ writeFileSync(fd, JSON.stringify({ token, pid: process.pid, at: Date.now() }), 'utf8');
235
+ }
236
+ finally {
237
+ closeSync(fd);
238
+ }
239
+ const release = () => {
240
+ // Release ONLY a token-owned lock: if the record no longer carries OUR
241
+ // token, the lock is a successor's (or was removed) — never remove it.
242
+ const held = readLockRecord(lockPath);
243
+ if (held === undefined || held.token !== token)
244
+ return;
245
+ try {
246
+ rmSync(lockPath, { force: true });
247
+ }
248
+ catch {
249
+ // Already gone: nothing to release.
250
+ }
251
+ };
252
+ return { token, release };
253
+ }
254
+ catch (error) {
255
+ const code = error.code;
256
+ if (code !== 'EEXIST')
257
+ throw error;
258
+ // A held lock: examine WHO holds it before even considering theft.
259
+ const held = readLockRecord(lockPath);
260
+ if (held !== undefined) {
261
+ const age = Date.now() - held.at;
262
+ const alive = probeAlive(held.pid);
263
+ if (alive === false && age > staleLockMs) {
264
+ // The holder is PROVEN DEAD and the lock is stale: take it over.
265
+ // (An unreadable/corrupt record is NOT this case — fail closed below.)
266
+ try {
267
+ rmSync(lockPath, { force: true });
268
+ }
269
+ catch {
270
+ // Released between the read and the removal: retry the acquire.
271
+ }
272
+ continue;
273
+ }
274
+ // A LIVE holder (or unknown liveness) is never stolen from — the
275
+ // contender waits, then fails closed at the deadline.
276
+ }
277
+ if (Date.now() > deadline) {
278
+ throw new Error('the credential store is locked by another writer; the mutation was not attempted');
279
+ }
280
+ sleep(25);
281
+ }
282
+ }
283
+ };
284
+ /** Read the CURRENT document. STRICT for mutations: corruption refuses them. */
285
+ const read = (strict) => {
286
+ let text;
287
+ try {
288
+ text = readFileSync(file, 'utf8');
289
+ }
290
+ catch (error) {
291
+ if (error.code === 'ENOENT') {
292
+ // A MISSING document is the only empty start. Nothing else is.
293
+ return new Map();
294
+ }
295
+ if (strict) {
296
+ throw new CorruptCredentialStoreError(`the credentials document cannot be read and no mutation will guess its contents: `
297
+ + `${error instanceof Error ? error.message : String(error)}`);
298
+ }
299
+ return new Map();
300
+ }
301
+ let parsed;
302
+ try {
303
+ parsed = JSON.parse(text);
304
+ }
305
+ catch (error) {
306
+ if (strict) {
307
+ throw new CorruptCredentialStoreError(`the credentials document is not valid JSON and will not be overwritten as if empty: `
308
+ + `${error instanceof Error ? error.message : String(error)}`);
309
+ }
310
+ return new Map();
311
+ }
312
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
313
+ if (strict)
314
+ throw new CorruptCredentialStoreError('the credentials document is not a JSON object');
315
+ return new Map();
316
+ }
317
+ const document = parsed;
318
+ if (!Array.isArray(document.clients)) {
319
+ if (strict)
320
+ throw new CorruptCredentialStoreError('the credentials document has no clients array');
321
+ return new Map();
322
+ }
323
+ const next = new Map();
324
+ for (const entry of document.clients) {
325
+ const candidate = entry;
326
+ if (typeof candidate.clientId !== 'string' || typeof candidate.secret !== 'string'
327
+ || (candidate.scheme !== 'static-token' && candidate.scheme !== 'oauth-resource-server')
328
+ || !Array.isArray(candidate.scope)) {
329
+ // A malformed ENTRY corrupts the document: a mutation would silently drop
330
+ // it on the next persist, and authentication cannot vouch for it either.
331
+ if (strict) {
332
+ throw new CorruptCredentialStoreError(`the credentials document carries a malformed client entry (id ${JSON.stringify(candidate.clientId)})`);
333
+ }
334
+ return new Map();
335
+ }
336
+ next.set(candidate.clientId, candidate);
337
+ }
338
+ return next;
339
+ };
340
+ const persist = (clients, token) => {
341
+ const document = {
342
+ version: 1,
343
+ clients: [...clients.values()],
344
+ };
345
+ // SEC-01: the temp name is crypto-random and opened EXCLUSIVELY — a planted
346
+ // hardlink/symlink at a predictable name cannot be overwritten, and a
347
+ // pre-existing name fails the write instead of clobbering an unrelated file.
348
+ const temp = join(dirname(file), `.${basename(file)}.tmp-${randomBytes(12).toString('hex')}`);
349
+ const fd = openSync(temp, 'wx', 0o600);
350
+ try {
351
+ // The file WE created must be a regular file: 'wx' already excludes
352
+ // pre-existing entries (hardlinks and junctions included); this is the
353
+ // belt-and-braces check.
354
+ const created = lstatSync(temp);
355
+ if (!created.isFile() || created.isSymbolicLink()) {
356
+ throw new Error(`the credential temp file is not a regular file: refusing to write plaintext secrets through it`);
357
+ }
358
+ // Owner-only BEFORE the first secret byte: the empty file's ACL is
359
+ // secured while it holds NOTHING, so no plaintext ever exists under an
360
+ // inherited broad ACL (the reviewer's inherited Everyone:R +
361
+ // Authenticated Users:M finding).
362
+ applyOwnerOnlyDacl(temp);
363
+ writeFileSync(fd, JSON.stringify(document, null, 2), 'utf8');
364
+ fsyncSync(fd);
365
+ }
366
+ finally {
367
+ closeSync(fd);
368
+ }
369
+ // The publish is refused unless OUR token still owns the lock: a writer
370
+ // that somehow lost its lock must never publish a stale snapshot over the
371
+ // successor's state.
372
+ const held = readLockRecord(`${file}.lock`);
373
+ if (held === undefined || held.token !== token) {
374
+ rmSync(temp, { force: true });
375
+ throw new Error('the credential store lost its write lock; the publish is refused');
376
+ }
377
+ renameSync(temp, file);
378
+ };
379
+ /**
380
+ * One serialized mutation: the ancestor chain is checked for reparse points
381
+ * BEFORE anything (lock file included) is written through it, the parent
382
+ * directory is created (a first-generation store starts from nothing), then
383
+ * the file lock excludes other processes and the read-modify-write runs
384
+ * fully SYNCHRONOUSLY inside it — Node's single thread makes the section
385
+ * atomic for this process too, so two concurrent writers cannot each persist
386
+ * a snapshot that drops the other's entry or resurrects a just-revoked
387
+ * credential.
388
+ */
389
+ const mutation = (body) => {
390
+ assertTrustedStoreAncestors(file);
391
+ mkdirSync(dirname(file), { recursive: true });
392
+ const { token, release } = acquireFileLock();
393
+ try {
394
+ const clients = read(true);
395
+ // TEST-ONLY: simulate a writer paused after its strict read, holding the
396
+ // lock while other processes contend (the reviewer's 31 s repro, without
397
+ // the wait).
398
+ if (hooks.holdLockAfterReadMs !== undefined)
399
+ sleep(hooks.holdLockAfterReadMs);
400
+ return body(clients, token);
401
+ }
402
+ finally {
403
+ release();
404
+ }
405
+ };
406
+ return {
407
+ file,
408
+ list() {
409
+ // Reads DENY on corruption (empty list) rather than crash the auth path.
410
+ return [...read(false).values()];
411
+ },
412
+ get(clientId) {
413
+ return read(false).get(clientId);
414
+ },
415
+ bySecret(secret) {
416
+ // CONSTANT-TIME over the STATIC-TOKEN entries: the digest equalizes lengths,
417
+ // and the loop never exits early on a match. OAuth resource-server secrets
418
+ // are NEVER matched here — scheme confusion would let an OAuth secret pass
419
+ // as a static bearer.
420
+ const digest = createHash('sha256').update(secret).digest();
421
+ let found;
422
+ for (const candidate of read(false).values()) {
423
+ if (candidate.scheme !== 'static-token')
424
+ continue;
425
+ const candidateDigest = createHash('sha256').update(candidate.secret).digest();
426
+ if (timingSafeEqual(digest, candidateDigest))
427
+ found = candidate;
428
+ }
429
+ return found;
430
+ },
431
+ upsert(credential) {
432
+ mutation((clients, token) => {
433
+ // PERSIST FIRST inside the critical section: a credential that cannot be
434
+ // written must never be registered as usable anywhere (SEC-01).
435
+ clients.set(credential.clientId, credential);
436
+ persist(clients, token);
437
+ });
438
+ },
439
+ delete(clientId) {
440
+ mutation((clients, token) => {
441
+ if (!clients.has(clientId))
442
+ return;
443
+ clients.delete(clientId);
444
+ persist(clients, token);
445
+ });
446
+ },
447
+ };
448
+ }
449
+ /** Remove a credential store file, for tests that need a fresh deployment. */
450
+ export function removeCredentialStoreFile(file) {
451
+ rmSync(file, { force: true });
452
+ rmSync(`${file}.lock`, { force: true });
453
+ }
454
+ //# sourceMappingURL=credential-store.js.map
@@ -23,7 +23,9 @@
23
23
  * @module @try-works/dsh-model-as-tool/mcp/host-binding
24
24
  */
25
25
  import type { ToolContract } from '@try-works/tool-mode-contracts';
26
+ import type { ExternalCaller } from './client-grant.ts';
26
27
  import type { McpHost } from './server.ts';
28
+ import { type InvocationAccounting } from '../invocation-accounting.ts';
27
29
  /**
28
30
  * Derive the payload schema from a contract's final output schema (§16.4).
29
31
  *
@@ -67,6 +69,14 @@ export interface HostSurface {
67
69
  readonly signal: AbortSignal;
68
70
  /** Server-issued. A caller-supplied id would let a caller forge a receipt. */
69
71
  readonly callId: string;
72
+ /**
73
+ * The AUTHENTICATED external caller and its derived grant (SEC-04).
74
+ *
75
+ * Absent for calls with no client identity (native paths). When present, the
76
+ * implementation MUST authorize the call against THIS caller's grant rather than
77
+ * the host's own — an external client is not the operator.
78
+ */
79
+ readonly caller?: ExternalCaller;
70
80
  }): Promise<{
71
81
  readonly isError: boolean;
72
82
  readonly value?: unknown;
@@ -87,5 +97,20 @@ export declare function createMcpHost(options: {
87
97
  };
88
98
  /** Called for each tool withheld, so a reason is recorded rather than lost. */
89
99
  readonly onWithheld?: (tool: PublishedTool) => void;
100
+ /**
101
+ * Called once per EXTERNAL invocation, with its accounting (OBS-02).
102
+ *
103
+ * Wired EXPLICITLY because an external call is not automatically an agent-loop
104
+ * transcript: DSH observes `ctx.tools.execute`, and an MCP caller reaches the same
105
+ * pipeline through a different door. Without this the external half of the traffic has
106
+ * no receipt at all.
107
+ *
108
+ * It receives TIMINGS, A CLASS and SIZES — never the payload. The result is deliberately
109
+ * NOT passed in, so an implementation of this hook cannot duplicate a large result into a
110
+ * second log, which is the other half of what OBS-02 forbids.
111
+ */
112
+ readonly onAccounted?: (accounting: InvocationAccounting) => void;
113
+ /** The engine that served the call, recorded WITH the measurement (OBS-05). */
114
+ readonly engineVersion?: string;
90
115
  }): McpHost;
91
116
  //# sourceMappingURL=host-binding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host-binding.d.ts","sourceRoot":"","sources":["../../src/mcp/host-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,KAAK,EAAE,OAAO,EAA4C,MAAM,aAAa,CAAA;AAKpF;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAA;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE;QACf,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;QACrD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;QAC5B,8EAA8E;QAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KACxB,GAAG,OAAO,CAAC;QACV,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAC,CAAA;CACH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACxE,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;CACpD,GAAG,OAAO,CAqEV"}
1
+ {"version":3,"file":"host-binding.d.ts","sourceRoot":"","sources":["../../src/mcp/host-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAElE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,OAAO,EAA4C,MAAM,aAAa,CAAA;AACpF,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAA;AAQpC;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AAsCD,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAA;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE;QACf,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;QACrD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;QAC5B,8EAA8E;QAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB;;;;;;WAMG;QACH,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;KACjC,GAAG,OAAO,CAAC;QACV,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAC,CAAA;CACH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACxE,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;IACnD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,oBAAoB,KAAK,IAAI,CAAA;IACjE,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC,GAAG,OAAO,CA6HV"}