@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.
- package/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +614 -0
- package/client/index.js +1717 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +325 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +858 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +221 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +367 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1527 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +271 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2644 -70
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
- package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +32 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +32 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +243 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +43 -15
- package/remote/spec.mjs +462 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /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;
|
|
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"}
|