@retinue/agentkit 0.1.0 → 0.3.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/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials, referenced rather than held — REQ-047 (#206), task #214, AC-5; widened by REQ-063 (#259), #260.
|
|
3
|
+
*
|
|
4
|
+
* A toolkit needs a secret. The question is who holds it and when it is read, and getting that wrong is the
|
|
5
|
+
* decision that makes a multi-tenant deployment a rewrite of every package rather than a configuration change.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a tool must not read the environment
|
|
8
|
+
*
|
|
9
|
+
* `process.env.GITHUB_TOKEN` inside a tool works perfectly for exactly one tenant. It is also the shape that
|
|
10
|
+
* twenty more toolkits will copy, because the first one did — and each of them then has to be rewritten when a
|
|
11
|
+
* second customer arrives, with their own token, for the same tool.
|
|
12
|
+
*
|
|
13
|
+
* So a tool takes a **reference** and the host resolves it. What the reference means — a row in a credential
|
|
14
|
+
* table, a secret manager path, an env var name in a single-tenant deployment — is the host's business and
|
|
15
|
+
* changes nothing about the tool.
|
|
16
|
+
*
|
|
17
|
+
* ## Why resolution happens at the point of use
|
|
18
|
+
*
|
|
19
|
+
* Not at construction. A credential read once at startup is a credential that survives its own rotation: the
|
|
20
|
+
* secret changes, every request keeps sending the old one, and the failure looks like the vendor rejecting a
|
|
21
|
+
* token that "has not changed". Resolving per call costs a lookup — cacheable by the host, which knows its own
|
|
22
|
+
* rotation window — and means a rotated secret takes effect without a restart.
|
|
23
|
+
*
|
|
24
|
+
* The corollary: **a resolved secret is never stored on the tool.** It lives in the local scope of one call.
|
|
25
|
+
*
|
|
26
|
+
* ## Why a miss throws
|
|
27
|
+
*
|
|
28
|
+
* Returning an empty string would send an unauthenticated request and surface as a vendor 401 several layers
|
|
29
|
+
* away, where the actual problem — nobody wired the credential — is invisible. A typed failure names the ref.
|
|
30
|
+
*
|
|
31
|
+
* ## Why a string was not enough — #260
|
|
32
|
+
*
|
|
33
|
+
* `resolve()` returned `Promise<string>`, which covers a bearer token and nothing else. Jira and Confluence
|
|
34
|
+
* want an account email *and* an API token as Basic auth; Atlassian's OAuth needs an access token *and* a cloud
|
|
35
|
+
* id discovered after consent; WhatsApp needs a token *and* a phone number id; a vendor using `X-Api-Key` needs
|
|
36
|
+
* a header name. Four of the fourteen integrations specified in `docs/23` cannot be expressed as a string, and
|
|
37
|
+
* every one of them would have grown its own side-channel.
|
|
38
|
+
*
|
|
39
|
+
* Two axes, deliberately kept apart, because they are answered by different people:
|
|
40
|
+
*
|
|
41
|
+
* - **`CredentialScheme`** — how the secret is presented on the wire. The toolkit knows this.
|
|
42
|
+
* - **`AuthMode`** — how the tenant *obtained* it: pasted a token, or completed an OAuth flow. The deployment
|
|
43
|
+
* knows this, and it is what decides whether an unconnected tool can pause a run for consent (#264) or must
|
|
44
|
+
* simply fail, since a token has no login URL to redirect to.
|
|
45
|
+
*/
|
|
46
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
47
|
+
/** How a secret is presented on the wire. */
|
|
48
|
+
export const CREDENTIAL_SCHEMES = ["bearer", "basic", "custom-header"];
|
|
49
|
+
/**
|
|
50
|
+
* How a tenant supplied the credential.
|
|
51
|
+
*
|
|
52
|
+
* Not the same question as the scheme: an OAuth access token is presented as a bearer, so the two would collapse
|
|
53
|
+
* if they shared a type — and the collapse would lose exactly the fact #264 needs, which is whether there is a
|
|
54
|
+
* login URL to send someone to.
|
|
55
|
+
*/
|
|
56
|
+
export const AUTH_MODES = ["token", "oauth2"];
|
|
57
|
+
/** The secret-bearing property of each scheme — the ones that must never be enumerable. */
|
|
58
|
+
const SECRET_KEYS = {
|
|
59
|
+
bearer: ["token"],
|
|
60
|
+
basic: ["password"],
|
|
61
|
+
"custom-header": ["value"],
|
|
62
|
+
};
|
|
63
|
+
const REDACTED = "[credential redacted]";
|
|
64
|
+
/**
|
|
65
|
+
* Builds a credential whose secret is **not enumerable** — AC-7.
|
|
66
|
+
*
|
|
67
|
+
* A typed object is far more likely to reach a log line than a bare string was: it gets spread into an error's
|
|
68
|
+
* `details`, passed to a structured logger, or serialised into an audit row, and every one of those uses
|
|
69
|
+
* `JSON.stringify` or an object spread. So the secret is defined non-enumerably and `toJSON`/`toString`/
|
|
70
|
+
* `util.inspect` are overridden. `credential.token` still reads normally; `{ ...credential }`,
|
|
71
|
+
* `JSON.stringify(credential)` and `console.log(credential)` do not.
|
|
72
|
+
*
|
|
73
|
+
* This is defence in depth, not a licence: a caller that reads `.token` and logs *that* is still logging a
|
|
74
|
+
* secret, and no type can stop it.
|
|
75
|
+
*/
|
|
76
|
+
export const createCredential = (input) => {
|
|
77
|
+
const secrets = SECRET_KEYS[input.scheme];
|
|
78
|
+
const credential = { ...input };
|
|
79
|
+
for (const key of secrets) {
|
|
80
|
+
const value = credential[key];
|
|
81
|
+
delete credential[key];
|
|
82
|
+
Object.defineProperty(credential, key, { value, enumerable: false, writable: false, configurable: false });
|
|
83
|
+
}
|
|
84
|
+
Object.defineProperty(credential, "toJSON", {
|
|
85
|
+
value: () => ({ scheme: input.scheme, ...(input.mode === undefined ? {} : { mode: input.mode }), secret: REDACTED }),
|
|
86
|
+
enumerable: false,
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(credential, "toString", { value: () => REDACTED, enumerable: false });
|
|
89
|
+
// Node's `console.log` and `util.inspect` ignore `toString`; this is the hook they do read.
|
|
90
|
+
Object.defineProperty(credential, Symbol.for("nodejs.util.inspect.custom"), {
|
|
91
|
+
value: () => `Credential(${input.scheme}) ${REDACTED}`,
|
|
92
|
+
enumerable: false,
|
|
93
|
+
});
|
|
94
|
+
return Object.freeze(credential);
|
|
95
|
+
};
|
|
96
|
+
/** A bearer credential, which is what most vendors want and what a plain string used to mean. */
|
|
97
|
+
export const bearer = (token, metadata, mode) => createCredential({ scheme: "bearer", token, ...(metadata === undefined ? {} : { metadata }), ...(mode === undefined ? {} : { mode }) });
|
|
98
|
+
export const credentialMissing = (ref, detail) => ({
|
|
99
|
+
code: "capability_unavailable",
|
|
100
|
+
message: `No credential is wired for reference "${ref}"` +
|
|
101
|
+
(detail === undefined ? "" : `: ${detail}`) +
|
|
102
|
+
". A tool cannot read one from the environment — supply a CredentialResolver to the host.",
|
|
103
|
+
// Retrying an unwired credential cannot help; something has to be configured.
|
|
104
|
+
retryable: false,
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* The credential resolved is not the shape this toolkit can present — #260 AC-2.
|
|
108
|
+
*
|
|
109
|
+
* Raised at **construction** where possible, and at resolution otherwise. A misconfiguration discovered as a
|
|
110
|
+
* vendor 401 an hour later is the failure this exists to prevent: the vendor's message says the token is
|
|
111
|
+
* invalid, which sends an operator to rotate a token that was never the problem.
|
|
112
|
+
*/
|
|
113
|
+
export const credentialSchemeMismatch = (ref, expected, got) => ({
|
|
114
|
+
code: "capability_unavailable",
|
|
115
|
+
message: `Credential "${ref}" is a ${got} credential and this toolkit presents ${expected.join(" or ")}. ` +
|
|
116
|
+
"The vendor would answer 401 and the message would say the token is invalid, which is not the problem.",
|
|
117
|
+
retryable: false,
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* A resolver over a plain map, for a single-tenant deployment and for tests.
|
|
121
|
+
*
|
|
122
|
+
* Shipped because the alternative is every host writing the same six lines, and the sixth one writing it with a
|
|
123
|
+
* fallback to `process.env` — which is the thing this module exists to prevent. Explicitly *not* environment
|
|
124
|
+
* backed: a host that wants that passes `{ github: process.env.GITHUB_TOKEN ?? "" }` and can see it doing so.
|
|
125
|
+
*
|
|
126
|
+
* **A bare string still works**, and stays the common case: it means a bearer token, which is what it meant
|
|
127
|
+
* before #260. The single-tenant path must not get harder because multi-tenant got possible.
|
|
128
|
+
*/
|
|
129
|
+
export const createStaticCredentialResolver = (secrets) => ({
|
|
130
|
+
async resolve({ ref }) {
|
|
131
|
+
const secret = secrets[ref];
|
|
132
|
+
if (secret === undefined || secret === "")
|
|
133
|
+
throw credentialMissing(ref, "not present in the static map");
|
|
134
|
+
return typeof secret === "string" ? bearer(secret) : secret;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* Wraps a resolver so every resolution is audited, and so a scheme mismatch is caught here rather than by the
|
|
139
|
+
* vendor — #260 AC-2 and AC-8.
|
|
140
|
+
*
|
|
141
|
+
* A wrapper rather than a change to every resolver: a host's own resolver stays a four-line object, and the
|
|
142
|
+
* cross-cutting obligations are added once, where they cannot be forgotten per toolkit.
|
|
143
|
+
*/
|
|
144
|
+
export const withCredentialAudit = (resolver, audit, expected) => ({
|
|
145
|
+
async resolve(input) {
|
|
146
|
+
let credential;
|
|
147
|
+
try {
|
|
148
|
+
credential = await resolver.resolve(input);
|
|
149
|
+
}
|
|
150
|
+
catch (thrown) {
|
|
151
|
+
const reason = thrown instanceof Error ? thrown.message : String(thrown);
|
|
152
|
+
await audit.onRefused({ ref: input.ref, context: input.context, reason });
|
|
153
|
+
throw thrown;
|
|
154
|
+
}
|
|
155
|
+
if (expected !== undefined && !expected.includes(credential.scheme)) {
|
|
156
|
+
const error = credentialSchemeMismatch(input.ref, expected, credential.scheme);
|
|
157
|
+
await audit.onRefused({ ref: input.ref, context: input.context, reason: error.message });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
await audit.onResolved({ ref: input.ref, context: input.context, scheme: credential.scheme });
|
|
161
|
+
return credential;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* The `Authorization`-style header a credential presents as.
|
|
166
|
+
*
|
|
167
|
+
* One function, so twenty toolkits do not each write their own base64 and get the padding wrong. Returns the
|
|
168
|
+
* header **name and value**, because `custom-header` does not use `Authorization`.
|
|
169
|
+
*/
|
|
170
|
+
export const credentialHeader = (credential) => {
|
|
171
|
+
switch (credential.scheme) {
|
|
172
|
+
case "bearer":
|
|
173
|
+
return ["Authorization", `Bearer ${credential.token}`];
|
|
174
|
+
case "basic":
|
|
175
|
+
return ["Authorization", `Basic ${Buffer.from(`${credential.username}:${credential.password}`).toString("base64")}`];
|
|
176
|
+
case "custom-header":
|
|
177
|
+
return [credential.header, credential.value];
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Refuses a toolkit configuration whose credential cannot be presented — at construction.
|
|
182
|
+
*
|
|
183
|
+
* Takes the *declared* scheme rather than resolving, because resolution needs a context and construction has
|
|
184
|
+
* none. A host that wires a static map can therefore be told immediately; a host whose resolver is dynamic is
|
|
185
|
+
* caught by `withCredentialAudit` at the first call instead. Both are before the vendor sees anything.
|
|
186
|
+
*/
|
|
187
|
+
export const assertToolkitAuth = (ref, auth, declared) => {
|
|
188
|
+
if (auth.schemes.length === 0)
|
|
189
|
+
throw new Error(`toolkit auth for "${ref}" declares no schemes, so no credential could ever satisfy it`);
|
|
190
|
+
if (declared !== undefined && !auth.schemes.includes(declared)) {
|
|
191
|
+
const error = credentialSchemeMismatch(ref, auth.schemes, declared);
|
|
192
|
+
throw Object.assign(new Error(error.message), error);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Adds an expiry to a credential **without losing its secret protection** — AC-6.
|
|
197
|
+
*
|
|
198
|
+
* This exists because the obvious way to build one is wrong, and wrong invisibly. A host writing a
|
|
199
|
+
* `CredentialRefresher` reaches for:
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* return { ...bearer(accessToken), expiresAt }; // ← the secret is now enumerable
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* `createCredential` defines the secret **non-enumerably**, which is precisely what makes it survive a
|
|
206
|
+
* `JSON.stringify` into a log line — and precisely what a spread drops. The result looks identical, works
|
|
207
|
+
* identically, and serialises the token into the first structured log that touches it.
|
|
208
|
+
*
|
|
209
|
+
* Found by the AC-6 test failing against this repository's own test helper, which had made exactly that
|
|
210
|
+
* mistake. If the helper made it, a host will.
|
|
211
|
+
*/
|
|
212
|
+
export const refreshable = (credential, expiresAt) => {
|
|
213
|
+
const secrets = SECRET_KEYS[credential.scheme];
|
|
214
|
+
/**
|
|
215
|
+
* Rebuilt **through** `createCredential`, with the expiry passed in rather than added after.
|
|
216
|
+
*
|
|
217
|
+
* Two reasons it has to be this way round. The protection must be *applied* rather than copied — a copy of a
|
|
218
|
+
* non-enumerable property is an enumerable one — and `createCredential` freezes what it returns, so there is
|
|
219
|
+
* no "after" to add a field in. The secrets are read back explicitly because the spread above cannot see
|
|
220
|
+
* them, which is the whole point of them.
|
|
221
|
+
*/
|
|
222
|
+
return createCredential({
|
|
223
|
+
...credential,
|
|
224
|
+
...Object.fromEntries(secrets.map((key) => [key, credential[key]])),
|
|
225
|
+
expiresAt,
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
export const isRefreshable = (credential) => typeof credential.expiresAt === "string";
|
|
229
|
+
/**
|
|
230
|
+
* How early a token is replaced.
|
|
231
|
+
*
|
|
232
|
+
* Sixty seconds, and the number is a *commitment* rather than a guess: a tool call can take tens of seconds —
|
|
233
|
+
* a slow vendor, a large upload, a retry — and a token that was valid when the call started must still be
|
|
234
|
+
* valid when it arrives. Refreshing exactly at expiry makes "expired mid-flight" the common case rather than
|
|
235
|
+
* the rare one, and that failure looks like an intermittent authentication bug.
|
|
236
|
+
*
|
|
237
|
+
* AC-5. Configurable because a deployment whose calls are slower than this needs more.
|
|
238
|
+
*/
|
|
239
|
+
export const DEFAULT_REFRESH_SKEW_MS = 60_000;
|
|
240
|
+
/** Whether a credential is expired, or close enough that it should be replaced before use. */
|
|
241
|
+
export const isExpiring = (credential, skewMs, now) => {
|
|
242
|
+
if (!isRefreshable(credential))
|
|
243
|
+
return false;
|
|
244
|
+
const expiresAt = Date.parse(credential.expiresAt);
|
|
245
|
+
// An unparseable expiry is treated as expiring. The alternative is using a credential whose lifetime is
|
|
246
|
+
// unknown, and the cost of an unnecessary refresh is one call.
|
|
247
|
+
return Number.isNaN(expiresAt) || expiresAt - now <= skewMs;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* A refresh that failed because the grant is gone, rather than because the network was.
|
|
251
|
+
*
|
|
252
|
+
* The distinction is the whole of AC-3, and it is not cosmetic: `invalid_grant` means a person must consent
|
|
253
|
+
* again and no amount of retrying will help, while a timeout means try again in a second. A runtime that
|
|
254
|
+
* conflates them either retries a dead grant forever or asks a user to re-authorise because of a blip.
|
|
255
|
+
*/
|
|
256
|
+
export const REFRESH_GRANT_ERRORS = ["invalid_grant", "invalid_client", "unauthorized_client", "access_denied"];
|
|
257
|
+
export const isGrantError = (error) => {
|
|
258
|
+
const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
259
|
+
return REFRESH_GRANT_ERRORS.some((code) => text.includes(code));
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Wraps a resolver so an expiring credential is renewed before it is handed out.
|
|
263
|
+
*
|
|
264
|
+
* A wrapper, like `withCredentialAudit`, and for the same reason: the eight shipped toolkits already resolve
|
|
265
|
+
* **per call**, so they pick this up without a line changing. A toolkit that cached a credential at
|
|
266
|
+
* construction would defeat it, which is why `createGitHubToolkit` and every sibling resolve inside `call()`.
|
|
267
|
+
*
|
|
268
|
+
* ## Time-driven, never 401-driven — AC-7
|
|
269
|
+
*
|
|
270
|
+
* The obvious design is to refresh when the vendor returns 401. It is wrong, and worth stating plainly because
|
|
271
|
+
* it is what most integrations do:
|
|
272
|
+
*
|
|
273
|
+
* A 401 is what a vendor returns for an expired token, a **revoked** grant, a token for the wrong tenant, and a
|
|
274
|
+
* scope the grant never had. Refreshing on 401 therefore turns a revoked grant into an infinite refresh loop
|
|
275
|
+
* against the vendor's token endpoint, and turns a missing scope into a refresh that succeeds and a call that
|
|
276
|
+
* fails again identically. Neither is diagnosable from the outside.
|
|
277
|
+
*
|
|
278
|
+
* Time is the only signal that means what it says: a token with an expiry in the past is expired, and nothing
|
|
279
|
+
* else is inferred from it. A 401 on a freshly-refreshed token is a real error and is surfaced as one.
|
|
280
|
+
*
|
|
281
|
+
* ## One refresh, not N — AC-2
|
|
282
|
+
*
|
|
283
|
+
* Twenty concurrent tool calls hitting an expired token must produce **one** refresh. Refresh endpoints rate
|
|
284
|
+
* limit, and — worse — several vendors invalidate the previous refresh token when one is used, so N concurrent
|
|
285
|
+
* refreshes race to invalidate each other and log the deployment out permanently.
|
|
286
|
+
*
|
|
287
|
+
* The in-flight promise is stored *before* the first await, so a second caller entering the function
|
|
288
|
+
* synchronously after the first still finds it.
|
|
289
|
+
*/
|
|
290
|
+
export const withRefreshingCredentials = (resolver, refresher, options = {}) => {
|
|
291
|
+
const skewMs = Math.max(0, options.skewMs ?? DEFAULT_REFRESH_SKEW_MS);
|
|
292
|
+
const now = options.now ?? (() => Date.now());
|
|
293
|
+
/**
|
|
294
|
+
* Keyed by tenant **and** ref — AC-4.
|
|
295
|
+
*
|
|
296
|
+
* Two tenants naming the same credential `"google"` mean different grants, and a cache keyed by ref alone
|
|
297
|
+
* would hand one tenant the other's token after a refresh. A space separates them: a tenant id cannot
|
|
298
|
+
* contain one, so `a` + `b c` and `a b` + `c` cannot collide.
|
|
299
|
+
*/
|
|
300
|
+
const cached = new Map();
|
|
301
|
+
const inFlight = new Map();
|
|
302
|
+
const keyOf = (tenantId, ref) => `${tenantId} ${ref}`;
|
|
303
|
+
const refreshOnce = (key, input) => {
|
|
304
|
+
const existing = inFlight.get(key);
|
|
305
|
+
if (existing !== undefined)
|
|
306
|
+
return existing;
|
|
307
|
+
// Built and stored before the first await, so a synchronous second caller joins this one rather than
|
|
308
|
+
// starting a second refresh.
|
|
309
|
+
const attempt = (async () => {
|
|
310
|
+
try {
|
|
311
|
+
const fresh = await refresher.refresh({ ref: input.ref, context: input.context });
|
|
312
|
+
cached.set(key, fresh);
|
|
313
|
+
options.onRefreshed?.({
|
|
314
|
+
ref: input.ref,
|
|
315
|
+
tenantId: String(input.context.tenantId),
|
|
316
|
+
expiresAt: fresh.expiresAt,
|
|
317
|
+
});
|
|
318
|
+
return fresh;
|
|
319
|
+
}
|
|
320
|
+
catch (thrown) {
|
|
321
|
+
/**
|
|
322
|
+
* Belt-and-braces, and **not** load-bearing — worth saying rather than implying otherwise.
|
|
323
|
+
*
|
|
324
|
+
* A cached credential that is expiring already fails the freshness guard in `resolve`, so the next
|
|
325
|
+
* caller would attempt a refresh whether or not this line ran. Removing it breaks no test, which is
|
|
326
|
+
* exactly what one should expect. It stays because the invariant it states — a credential known to be
|
|
327
|
+
* dead is never held — is one a future edit could otherwise quietly rely on being false.
|
|
328
|
+
*/
|
|
329
|
+
cached.delete(key);
|
|
330
|
+
throw new AgentPlatformError(isGrantError(thrown)
|
|
331
|
+
? {
|
|
332
|
+
code: "unauthorized",
|
|
333
|
+
message: `The stored authorisation for "${input.ref}" is no longer valid and could not be renewed. ` +
|
|
334
|
+
"Someone needs to connect the account again — retrying will not help.",
|
|
335
|
+
retryable: false,
|
|
336
|
+
}
|
|
337
|
+
: {
|
|
338
|
+
code: "provider_unavailable",
|
|
339
|
+
message: `Could not renew the authorisation for "${input.ref}" right now.`,
|
|
340
|
+
retryable: true,
|
|
341
|
+
}, { cause: thrown });
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
inFlight.delete(key);
|
|
345
|
+
}
|
|
346
|
+
})();
|
|
347
|
+
inFlight.set(key, attempt);
|
|
348
|
+
return attempt;
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
async resolve(input) {
|
|
352
|
+
const key = keyOf(String(input.context.tenantId), input.ref);
|
|
353
|
+
const held = cached.get(key);
|
|
354
|
+
if (held !== undefined && !isExpiring(held, skewMs, now()))
|
|
355
|
+
return held;
|
|
356
|
+
// Ask the underlying resolver first: it is the source of truth, and on the common path the credential it
|
|
357
|
+
// returns is a plain one with no expiry, which is passed straight through.
|
|
358
|
+
const resolved = await resolver.resolve(input);
|
|
359
|
+
if (!isRefreshable(resolved))
|
|
360
|
+
return resolved;
|
|
361
|
+
if (!isExpiring(resolved, skewMs, now())) {
|
|
362
|
+
cached.set(key, resolved);
|
|
363
|
+
return resolved;
|
|
364
|
+
}
|
|
365
|
+
return refreshOnce(key, input);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
//# sourceMappingURL=credentials.js.map
|
package/dist/tools/define.d.ts
CHANGED
|
@@ -16,10 +16,55 @@ export type ToolSpec<I = unknown, O = unknown> = {
|
|
|
16
16
|
readonly inputSchema?: unknown;
|
|
17
17
|
readonly outputSchema?: unknown;
|
|
18
18
|
readonly requiresIdempotencyKey?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The provider scopes this tool needs — #260's field, made settable by #234.
|
|
21
|
+
*
|
|
22
|
+
* It was declared on `ToolDescriptor` and had **no way to be set**: `ToolSpec` did not carry it, so nothing
|
|
23
|
+
* in the repository could populate it and nothing read it. A field that cannot be written is a field that
|
|
24
|
+
* documents an intention rather than expressing one, which is the defect class `check:reachability` exists
|
|
25
|
+
* for — it was missed because the check asks whether declared fields are *read*, and an unwritable field is
|
|
26
|
+
* never read either.
|
|
27
|
+
*
|
|
28
|
+
* Google is the first vendor where it matters: `gmail.send` and `gmail.readonly` are different consents, and
|
|
29
|
+
* asking for the wrong one is the difference between a security team approving an integration and refusing
|
|
30
|
+
* it.
|
|
31
|
+
*/
|
|
32
|
+
readonly requiredScopes?: readonly string[];
|
|
19
33
|
execute(input: I, context: ExecutionContext): Promise<O> | O;
|
|
20
34
|
};
|
|
21
35
|
/** Build a `Tool` from a plain spec: `execute` returns data (or throws); the envelope is handled here. */
|
|
22
36
|
export declare const defineTool: <I = unknown, O = unknown>(spec: ToolSpec<I, O>) => Tool<O>;
|
|
23
37
|
/** A `ToolProvider` serving a fixed set of tools. For dynamic per-tenant tools, implement the port. */
|
|
24
38
|
export declare const toolProvider: (id: string, tools: readonly Tool[]) => ToolProvider;
|
|
39
|
+
/**
|
|
40
|
+
* The spec a confirmation wrapper accepts — the three classification fields removed.
|
|
41
|
+
*
|
|
42
|
+
* Removed rather than defaulted, so passing one is a **compile error** instead of a silent override. "Visible in
|
|
43
|
+
* review" was the weaker version of this requirement: a reviewer reading `confirms({ …, approvalPolicy: "never" })`
|
|
44
|
+
* has to notice a contradiction, and the type system can refuse it instead.
|
|
45
|
+
*/
|
|
46
|
+
export type GatedToolSpec<I, O> = Omit<ToolSpec<I, O>, "effect" | "approvalPolicy" | "requiresIdempotencyKey">;
|
|
47
|
+
/**
|
|
48
|
+
* A tool that writes somewhere outside this system, and therefore waits for a person.
|
|
49
|
+
*
|
|
50
|
+
* `effect`, `approvalPolicy` and `requiresIdempotencyKey` are one decision, and this is the word for it. Setting
|
|
51
|
+
* them individually is three chances to get it wrong, and the failure is silent: `effect: "read"` on a tool that
|
|
52
|
+
* posts a message skips the approval gate, carries no idempotency key, and nothing in the build objects, because
|
|
53
|
+
* `read` is a valid value and the compiler cannot know what the function does.
|
|
54
|
+
*
|
|
55
|
+
* Across one package that is a code-review problem. Across the twenty-one toolkit packages `docs/23` plans, some
|
|
56
|
+
* contributed, it is a systemic one — so the safe thing is now shorter to write than the unsafe thing.
|
|
57
|
+
*
|
|
58
|
+
* Use `defineTool` directly for a read, or for the rare combination this does not express.
|
|
59
|
+
*/
|
|
60
|
+
export declare const confirms: <I = unknown, O = unknown>(spec: GatedToolSpec<I, O>) => Tool<O>;
|
|
61
|
+
/**
|
|
62
|
+
* A tool whose effect cannot be undone — a delete, a merge, a send that cannot be recalled.
|
|
63
|
+
*
|
|
64
|
+
* Distinct from `confirms` because the *classification* differs and downstream policy reads it: a shadow run
|
|
65
|
+
* suppresses `destructive` and `external-write` alike, but an operator reviewing what an agent may do wants to
|
|
66
|
+
* know which of the two a tool is. Collapsing them would lose that, and the answer to "what can this agent
|
|
67
|
+
* irreversibly do" would stop being answerable from the catalogue.
|
|
68
|
+
*/
|
|
69
|
+
export declare const destroys: <I = unknown, O = unknown>(spec: GatedToolSpec<I, O>) => Tool<O>;
|
|
25
70
|
//# sourceMappingURL=define.d.ts.map
|
package/dist/tools/define.js
CHANGED
|
@@ -22,6 +22,9 @@ export const defineTool = (spec) => {
|
|
|
22
22
|
effect,
|
|
23
23
|
approvalPolicy: spec.approvalPolicy ?? (effect === "external-write" || effect === "destructive" ? "always" : "never"),
|
|
24
24
|
requiresIdempotencyKey: spec.requiresIdempotencyKey ?? (effect === "external-write" || effect === "destructive"),
|
|
25
|
+
// Omitted rather than defaulted to `[]`: absent means "this vendor has no scopes", which is true of
|
|
26
|
+
// every wave-1 tool, and an empty array would read as "needs none" — a different claim.
|
|
27
|
+
...(spec.requiredScopes === undefined ? {} : { requiredScopes: spec.requiredScopes }),
|
|
25
28
|
};
|
|
26
29
|
return {
|
|
27
30
|
descriptor,
|
|
@@ -42,4 +45,27 @@ export const toolProvider = (id, tools) => ({
|
|
|
42
45
|
return tools;
|
|
43
46
|
},
|
|
44
47
|
});
|
|
48
|
+
/**
|
|
49
|
+
* A tool that writes somewhere outside this system, and therefore waits for a person.
|
|
50
|
+
*
|
|
51
|
+
* `effect`, `approvalPolicy` and `requiresIdempotencyKey` are one decision, and this is the word for it. Setting
|
|
52
|
+
* them individually is three chances to get it wrong, and the failure is silent: `effect: "read"` on a tool that
|
|
53
|
+
* posts a message skips the approval gate, carries no idempotency key, and nothing in the build objects, because
|
|
54
|
+
* `read` is a valid value and the compiler cannot know what the function does.
|
|
55
|
+
*
|
|
56
|
+
* Across one package that is a code-review problem. Across the twenty-one toolkit packages `docs/23` plans, some
|
|
57
|
+
* contributed, it is a systemic one — so the safe thing is now shorter to write than the unsafe thing.
|
|
58
|
+
*
|
|
59
|
+
* Use `defineTool` directly for a read, or for the rare combination this does not express.
|
|
60
|
+
*/
|
|
61
|
+
export const confirms = (spec) => defineTool({ ...spec, effect: "external-write", approvalPolicy: "always", requiresIdempotencyKey: true });
|
|
62
|
+
/**
|
|
63
|
+
* A tool whose effect cannot be undone — a delete, a merge, a send that cannot be recalled.
|
|
64
|
+
*
|
|
65
|
+
* Distinct from `confirms` because the *classification* differs and downstream policy reads it: a shadow run
|
|
66
|
+
* suppresses `destructive` and `external-write` alike, but an operator reviewing what an agent may do wants to
|
|
67
|
+
* know which of the two a tool is. Collapsing them would lose that, and the answer to "what can this agent
|
|
68
|
+
* irreversibly do" would stop being answerable from the catalogue.
|
|
69
|
+
*/
|
|
70
|
+
export const destroys = (spec) => defineTool({ ...spec, effect: "destructive", approvalPolicy: "always", requiresIdempotencyKey: true });
|
|
45
71
|
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `find_tools` — REQ-045 (#204), task #210, AC-1 and AC-2.
|
|
3
|
+
*
|
|
4
|
+
* Search over tool descriptors, so a catalogue can be *findable* rather than resident. The two-tier loading
|
|
5
|
+
* already keeps schemas out of context; this keeps the compact entries out too, which is what makes a budget
|
|
6
|
+
* (AC-3) something other than a way to hide tools from the model permanently.
|
|
7
|
+
*
|
|
8
|
+
* ## What is reused, and the one thing that is not
|
|
9
|
+
*
|
|
10
|
+
* **Fusion is `fuseByRank`** — the same reciprocal rank fusion, the same `K = 60`, the same normalisation and
|
|
11
|
+
* the same relevance floor as knowledge retrieval. There is one implementation and both callers use it.
|
|
12
|
+
*
|
|
13
|
+
* **Embeddings are the same port.** `EmbeddingProvider`, injected. No second embedding path, no second client,
|
|
14
|
+
* no second cache format.
|
|
15
|
+
*
|
|
16
|
+
* **The keyword signal is not `KeywordIndex`, and that is a finding rather than a shortcut.** `KeywordIndex` is
|
|
17
|
+
* a Postgres full-text index over `knowledge_chunks`; tools are not rows in that table and putting them there
|
|
18
|
+
* would mean a write path, a migration, and an index to keep in sync with a registry that is assembled per
|
|
19
|
+
* request from providers. A corpus of a few hundred short documents that already lives in memory does not need
|
|
20
|
+
* an index — it needs a scan, and the scan is deterministic and free. AC-2's "no second implementation" is about
|
|
21
|
+
* not having two rankers; this is one ranker over a corpus the other one cannot hold.
|
|
22
|
+
*
|
|
23
|
+
* ## Why the semantic signal is optional
|
|
24
|
+
*
|
|
25
|
+
* Without an `EmbeddingProvider` this is keyword-only, and it says so in the result rather than pretending. Most
|
|
26
|
+
* deployments will not wire embeddings *for tools*: #221 measured selection accuracy as flat from 20 to 200
|
|
27
|
+
* tools, so the case for `find_tools` is context cost, not accuracy — and a deployment that pays for embeddings
|
|
28
|
+
* to solve a problem it does not have is exactly the cost this repository keeps refusing to impose by default.
|
|
29
|
+
*/
|
|
30
|
+
import type { EmbeddingProvider } from "../knowledge/index.js";
|
|
31
|
+
import type { ToolCatalogEntry, ToolDescriptor } from "./index.js";
|
|
32
|
+
/** Which signals found a tool. Reported, because "keyword only" is a different confidence from "both agreed". */
|
|
33
|
+
export type ToolSearchSignal = "semantic" | "keyword";
|
|
34
|
+
export type ToolSearchHit = {
|
|
35
|
+
readonly entry: ToolCatalogEntry;
|
|
36
|
+
/** 0–1 relative to the best hit for this query. Never comparable across queries. */
|
|
37
|
+
readonly score: number;
|
|
38
|
+
readonly signals: readonly ToolSearchSignal[];
|
|
39
|
+
};
|
|
40
|
+
export type ToolSearchOutcome = {
|
|
41
|
+
readonly hits: readonly ToolSearchHit[];
|
|
42
|
+
/** The signals actually used. Keyword-only when no embedding provider is wired. */
|
|
43
|
+
readonly modes: readonly ToolSearchSignal[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The absolute floor, and why a relative one is not enough.
|
|
47
|
+
*
|
|
48
|
+
* Fused scores are normalised against the best hit, so a query that matched exactly one tool badly gives that
|
|
49
|
+
* tool a score of 1.0 — the relative floor cannot reject a uniformly poor result set, because something is
|
|
50
|
+
* always the best of it. Knowledge retrieval solves this with an absolute `semanticFloor` handed to the vector
|
|
51
|
+
* index; this is the same idea for the keyword signal.
|
|
52
|
+
*
|
|
53
|
+
* **2 is one name or category hit, or two words of prose.** A single common word appearing somewhere in a
|
|
54
|
+
* description is not evidence: "translate this into Welsh" found `parse_csv`, whose description happens to say
|
|
55
|
+
* "into", and returned it with a perfect score. That was a real result from the first run of this file's tests,
|
|
56
|
+
* and it is the least-bad-match failure the floor exists to prevent — a model handed the least-bad tool calls it.
|
|
57
|
+
*/
|
|
58
|
+
export declare const MIN_KEYWORD_SCORE = 2;
|
|
59
|
+
export type ToolSearchDeps = {
|
|
60
|
+
/** Absent means keyword-only — see the note above on why that is the honest default. */
|
|
61
|
+
readonly embeddings?: EmbeddingProvider;
|
|
62
|
+
readonly candidates?: number;
|
|
63
|
+
readonly relevanceFloor?: number;
|
|
64
|
+
/** The absolute keyword floor. See `MIN_KEYWORD_SCORE`. */
|
|
65
|
+
readonly minKeywordScore?: number;
|
|
66
|
+
/** The absolute semantic floor, for the same reason. Defaults to the platform's `SEMANTIC_RELEVANCE_FLOOR`. */
|
|
67
|
+
readonly minSimilarity?: number;
|
|
68
|
+
};
|
|
69
|
+
/** The compact entry a catalogue shows, derived once so search and catalogue cannot disagree about a tool. */
|
|
70
|
+
export declare const compactEntry: (d: ToolDescriptor) => ToolCatalogEntry;
|
|
71
|
+
export declare const termsOf: (text: string) => readonly string[];
|
|
72
|
+
/** What a tool is embedded and matched as. One string, so both signals read the same document. */
|
|
73
|
+
export declare const documentFor: (entry: ToolCatalogEntry) => string;
|
|
74
|
+
/**
|
|
75
|
+
* The keyword signal: how many of the query's terms this tool mentions, and where.
|
|
76
|
+
*
|
|
77
|
+
* Weighted by field, because a term in the *name* is a different claim from a term buried in prose — a query for
|
|
78
|
+
* "issue" should rank `github_create_issue` above a tool whose description mentions issues in passing. Weights
|
|
79
|
+
* are small integers rather than tuned constants: RRF uses only the rank, so their exact values decide ordering
|
|
80
|
+
* and nothing else, and a fused score cannot be dominated by a badly chosen one.
|
|
81
|
+
*/
|
|
82
|
+
export declare const keywordScore: (entry: ToolCatalogEntry, queryTerms: readonly string[]) => number;
|
|
83
|
+
/**
|
|
84
|
+
* The same score, divided by document length — and this is what fixed a real ranking defect.
|
|
85
|
+
*
|
|
86
|
+
* The 200-tool measurement found `find_tools` returning `archive_post_metrics` above `get_post_metrics`. Both
|
|
87
|
+
* match the query's terms identically, so the raw scores tied and the tie-break — alphabetical by name — decided
|
|
88
|
+
* it. Alphabetical order is arbitrary with respect to relevance, and in a catalogue whose near-duplicates are
|
|
89
|
+
* `<verb>_<object>` it is *systematically* wrong: it prefers whichever verb sorts earliest.
|
|
90
|
+
*
|
|
91
|
+
* Length normalisation is the standard answer and it is the correct one here rather than a fitted one: the
|
|
92
|
+
* distractors are the base tool's description **plus** a qualifier, so they say the same thing less densely. A
|
|
93
|
+
* short, focused description that matches your query is a better match than a long one that matches it
|
|
94
|
+
* incidentally. `sqrt` rather than a linear divisor, as in BM25, so a genuinely detailed description is not
|
|
95
|
+
* punished out of contention.
|
|
96
|
+
*
|
|
97
|
+
* The raw score still decides the *floor* (see `MIN_KEYWORD_SCORE`); this decides the *order*. Two values,
|
|
98
|
+
* because "is this a match at all" and "which match is better" are different questions.
|
|
99
|
+
*/
|
|
100
|
+
export declare const weightedKeywordScore: (entry: ToolCatalogEntry, queryTerms: readonly string[]) => number;
|
|
101
|
+
export type ToolSearch = {
|
|
102
|
+
search(input: {
|
|
103
|
+
readonly query: string;
|
|
104
|
+
readonly tools: readonly ToolDescriptor[];
|
|
105
|
+
readonly limit: number;
|
|
106
|
+
}): Promise<ToolSearchOutcome>;
|
|
107
|
+
};
|
|
108
|
+
export declare const createToolSearch: (deps?: ToolSearchDeps) => ToolSearch;
|
|
109
|
+
//# sourceMappingURL=find.d.ts.map
|