@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport every sibling toolkit needs, once — REQ-052 (#224), task #225.
|
|
3
|
+
*
|
|
4
|
+
* `tools-github` grew one of these in #223 and it turned out to be four-fifths vendor-neutral: resolve the
|
|
5
|
+
* credential *per call*, pin the auth header to one validated host, parse JSON with an empty body tolerated,
|
|
6
|
+
* and map a failure onto the platform's closed error union. Only the last part is really vendor-specific, and
|
|
7
|
+
* only in its wording.
|
|
8
|
+
*
|
|
9
|
+
* Writing it twice more for Jira and Confluence — with nineteen toolkits after them — would mean copying four
|
|
10
|
+
* bugs along with it. #223 found two of them the hard way:
|
|
11
|
+
*
|
|
12
|
+
* - `JSON.parse("")` on a `204`. Two endpoints returned empty bodies on success and both tools failed outright
|
|
13
|
+
* while reporting a parse error about a correct response.
|
|
14
|
+
* - A plain-text body handled by catching the parse failure, which discarded the text and returned a
|
|
15
|
+
* placeholder. The tool typechecked and was entirely non-functional.
|
|
16
|
+
*
|
|
17
|
+
* Both are fixed here, so a toolkit gets them right by not writing them.
|
|
18
|
+
*
|
|
19
|
+
* **What stays with the vendor**: the failure vocabulary (`classify`), the base URL, and the fixed headers.
|
|
20
|
+
* A vendor that answers `200` with an error envelope — Slack's `ok: false`, GraphQL's `errors` — reads that
|
|
21
|
+
* envelope in its own module, because only it knows the shape.
|
|
22
|
+
*/
|
|
23
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
24
|
+
import { credentialHeader } from "../tools/credentials.js";
|
|
25
|
+
import { createHttpClient } from "./http.js";
|
|
26
|
+
/**
|
|
27
|
+
* The default classification, and why each arm is what it is.
|
|
28
|
+
*
|
|
29
|
+
* A model reads these to decide whether trying again is sensible, so the distinction that matters is
|
|
30
|
+
* *retryable*, not how precisely the words describe the HTTP status. `unauthorized` on a `403` is the important
|
|
31
|
+
* one: told "forbidden", a model retries with different arguments, which is never the fix for a missing scope.
|
|
32
|
+
*/
|
|
33
|
+
const defaultClassification = (failure, vendor) => {
|
|
34
|
+
const transport = failure.kind === "timeout" || failure.kind === "unreachable";
|
|
35
|
+
if (transport) {
|
|
36
|
+
return {
|
|
37
|
+
code: "provider_unavailable",
|
|
38
|
+
message: `${vendor} request failed (${failure.kind}): ${failure.reason}`,
|
|
39
|
+
retryable: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (failure.status === 429) {
|
|
43
|
+
/**
|
|
44
|
+
* `Retry-After` is honoured when the vendor sent one — the default backoff is a guess, and a vendor that
|
|
45
|
+
* told you the number has removed the need to guess. Meta, X and Reddit all send it.
|
|
46
|
+
*/
|
|
47
|
+
return {
|
|
48
|
+
code: "rate_limited",
|
|
49
|
+
message: `${vendor} rate limit reached: ${failure.reason}` +
|
|
50
|
+
(failure.retryAfterMs === undefined ? "" : ` Retry after ${Math.ceil(failure.retryAfterMs / 1000)}s.`),
|
|
51
|
+
retryable: true,
|
|
52
|
+
...(failure.retryAfterMs === undefined ? {} : { retryAfterMs: failure.retryAfterMs }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (failure.status === 401 || failure.status === 403) {
|
|
56
|
+
return {
|
|
57
|
+
code: "unauthorized",
|
|
58
|
+
message: `${vendor} refused the credential (${failure.status}): ${failure.reason}. The credential may lack the permission this tool needs.`,
|
|
59
|
+
retryable: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { code: "provider_error", message: `${vendor} request failed (${failure.kind}): ${failure.reason}`, retryable: false };
|
|
63
|
+
};
|
|
64
|
+
export const createVendorTransport = (config) => {
|
|
65
|
+
const base = config.baseUrl.replace(/\/$/, "");
|
|
66
|
+
const host = new URL(base).host;
|
|
67
|
+
/**
|
|
68
|
+
* One request, with the credential resolved now rather than at construction.
|
|
69
|
+
*
|
|
70
|
+
* Per call so a rotated token takes effect without a restart — a credential read once at startup is one that
|
|
71
|
+
* survives its own rotation, and the failure looks like the vendor rejecting a token that "has not changed".
|
|
72
|
+
*
|
|
73
|
+
* The header goes in through `headersFor`, which the runtime calls with the **validated** hostname only: a
|
|
74
|
+
* credential issued for one host cannot be sent to another by asking for a URL that merely mentions it.
|
|
75
|
+
*/
|
|
76
|
+
const request = async (context, path, init = {}) => {
|
|
77
|
+
const credential = await config.resolver.resolve({ ref: config.credentialRef, context });
|
|
78
|
+
// One helper, so twenty toolkits do not each write their own base64 and get the padding wrong — #260.
|
|
79
|
+
const [headerName, headerValue] = credentialHeader(credential);
|
|
80
|
+
const client = createHttpClient({
|
|
81
|
+
...(config.fetchImpl === undefined ? {} : { fetchImpl: config.fetchImpl }),
|
|
82
|
+
headersFor: (requested) => requested === host
|
|
83
|
+
? {
|
|
84
|
+
[headerName.toLowerCase()]: headerValue,
|
|
85
|
+
...(config.headers ?? {}),
|
|
86
|
+
// Overrides the vendor's default, which is JSON for every other call this transport makes.
|
|
87
|
+
...(init.contentType === undefined ? {} : { "content-type": init.contentType }),
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
const outcome = await client.request({
|
|
92
|
+
url: `${base}${path}`,
|
|
93
|
+
...(init.method === undefined ? {} : { method: init.method }),
|
|
94
|
+
...(init.rawBody !== undefined
|
|
95
|
+
? { body: init.rawBody }
|
|
96
|
+
: init.body === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { body: JSON.stringify(init.body) }),
|
|
99
|
+
// Parsed here and never shown to the model verbatim, so the untrusted-content envelope would only corrupt
|
|
100
|
+
// the JSON. Anything rendered as prose keeps the default fence.
|
|
101
|
+
fence: false,
|
|
102
|
+
});
|
|
103
|
+
if (outcome.ok)
|
|
104
|
+
return outcome.body;
|
|
105
|
+
const failure = outcome;
|
|
106
|
+
const described = config.classify?.(failure) ?? defaultClassification(failure, config.vendor);
|
|
107
|
+
/**
|
|
108
|
+
* `Retry-After`, carried through — and it was being dropped.
|
|
109
|
+
*
|
|
110
|
+
* `HttpFailure` parses the header and `PlatformError` has a field for it, and this transport joined them by
|
|
111
|
+
* building an error that mentioned neither. Every toolkit on it therefore ignored a vendor that had said
|
|
112
|
+
* *exactly* how long to wait, and fell back to a generic backoff — which is both slower than necessary and,
|
|
113
|
+
* against a service that counts requests during the window, a way to stay throttled.
|
|
114
|
+
*
|
|
115
|
+
* A classifier may override it; otherwise the server's own number wins over any default, because the server
|
|
116
|
+
* is the only party that knows.
|
|
117
|
+
*/
|
|
118
|
+
const retryAfterMs = described.retryAfterMs ?? failure.retryAfterMs;
|
|
119
|
+
/**
|
|
120
|
+
* `AgentPlatformError`, not a decorated `Error`.
|
|
121
|
+
*
|
|
122
|
+
* `toPlatformError` maps anything else to `{ code: "internal", retryable: false }` — so a rate limit thrown
|
|
123
|
+
* as `Object.assign(new Error(…), { retryable: true })` arrives at the model as permanently broken. The
|
|
124
|
+
* extra properties simply vanish and nothing warns you.
|
|
125
|
+
*/
|
|
126
|
+
throw new AgentPlatformError(retryAfterMs === undefined ? described : { ...described, retryAfterMs });
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
async json(context, path, init) {
|
|
130
|
+
const body = await request(context, path, init);
|
|
131
|
+
// **An empty body is a success.** A `204` from a delete, or a `201` from a fire-and-forget POST, has
|
|
132
|
+
// nothing to parse — and `JSON.parse("")` throws, which reported two correct responses as parse failures.
|
|
133
|
+
if (body.trim() === "")
|
|
134
|
+
return undefined;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(body);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new AgentPlatformError({
|
|
140
|
+
code: "provider_error",
|
|
141
|
+
message: `${config.vendor} returned a body that is not JSON`,
|
|
142
|
+
retryable: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
text(context, path, init) {
|
|
147
|
+
// Not `json` with the parse failure caught: that discards the body, which is how a log-reading tool
|
|
148
|
+
// shipped returning a placeholder on every success.
|
|
149
|
+
return request(context, path, init ?? {});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=vendor.js.map
|
package/dist/toolkit/web.d.ts
CHANGED
|
@@ -87,6 +87,19 @@ export type SearchProvider = {
|
|
|
87
87
|
/** Build the request URL for a query. The key belongs in `headers`, not here, wherever the provider allows it. */
|
|
88
88
|
readonly endpoint: (query: string, limit: number) => string;
|
|
89
89
|
readonly headers?: Readonly<Record<string, string>>;
|
|
90
|
+
/**
|
|
91
|
+
* `POST`, for the providers that require it.
|
|
92
|
+
*
|
|
93
|
+
* Added when the first real adapters were written (#214): this contract was GET-only, and three of the four
|
|
94
|
+
* most-used providers — Tavily, Serper, Exa — are POST with a JSON body. A GET-only seam would have limited the
|
|
95
|
+
* catalogue to Brave and self-hosted SearXNG, which is not a "one contract, several providers" rule so much as
|
|
96
|
+
* a rule with two providers.
|
|
97
|
+
*
|
|
98
|
+
* Optional, so every existing GET provider is unchanged.
|
|
99
|
+
*/
|
|
100
|
+
readonly method?: "GET" | "POST";
|
|
101
|
+
/** The request body for a `POST` provider. Ignored for `GET`. */
|
|
102
|
+
readonly body?: (query: string, limit: number) => unknown;
|
|
90
103
|
/** Read the provider's JSON into hits. Returning `[]` means "searched, found nothing". */
|
|
91
104
|
readonly parse: (payload: unknown) => readonly SearchHit[];
|
|
92
105
|
};
|
package/dist/toolkit/web.js
CHANGED
|
@@ -97,10 +97,16 @@ export const createWebSearch = (config = {}) => {
|
|
|
97
97
|
"directly.",
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
+
const method = provider.method ?? "GET";
|
|
100
101
|
const outcome = await client.request({
|
|
101
102
|
url: provider.endpoint(query, limit),
|
|
102
|
-
headers: provider.headers,
|
|
103
|
+
...(provider.headers === undefined ? {} : { headers: provider.headers }),
|
|
104
|
+
...(method === "POST"
|
|
105
|
+
? { method, body: JSON.stringify(provider.body?.(query, limit) ?? { query }) }
|
|
106
|
+
: {}),
|
|
103
107
|
accept: "application/json",
|
|
108
|
+
// Parsed by the provider's own `parse` and never shown to the model verbatim, so the untrusted-content
|
|
109
|
+
// envelope would only corrupt the JSON. The *hits* are fenced downstream, where they are read as prose.
|
|
104
110
|
fence: false,
|
|
105
111
|
});
|
|
106
112
|
if (!outcome.ok) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool costs a catalogue — REQ-045 (#204), task #210, AC-3.
|
|
3
|
+
*
|
|
4
|
+
* The budget itself is in `core/budget.ts`, shared with the skill catalogue. What is here is the part that knows
|
|
5
|
+
* about tools: how many tokens one entry is, which is the only thing the generic algorithm cannot know.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolCatalogEntry, ToolDescriptor } from "./index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Deliberately **not** re-exported here.
|
|
10
|
+
*
|
|
11
|
+
* `applyTokenBudget`, `TokenBudget` and `BudgetOutcome` live in `core/` and reach consumers through
|
|
12
|
+
* `@retinue/agentkit/runtime`. Re-exporting them from `./tools` as well gave one name two homes, which
|
|
13
|
+
* `public-surface.test.ts` fails on — and it is right to: a consumer reading two import paths for one function
|
|
14
|
+
* has to guess which is canonical, and the two will not stay in step.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* What a compact catalogue entry costs.
|
|
18
|
+
*
|
|
19
|
+
* The fields a model actually reads, plus a small per-entry allowance for the JSON or Markdown scaffolding the
|
|
20
|
+
* assembler wraps them in — measured at ~35 tokens per entry in #221's harness, which is what this reproduces.
|
|
21
|
+
* An estimate rather than a tokenizer call: the point is a stable ceiling, and a budget that shifted with the
|
|
22
|
+
* model's tokenizer would make one deployment's 4,000 tokens another's 4,600.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ENTRY_OVERHEAD_TOKENS = 6;
|
|
25
|
+
export declare const entryTokens: (entry: ToolCatalogEntry) => number;
|
|
26
|
+
/** A preloaded tool carries its schemas, which is usually most of its cost. */
|
|
27
|
+
export declare const descriptorTokens: (descriptor: ToolDescriptor) => number;
|
|
28
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool costs a catalogue — REQ-045 (#204), task #210, AC-3.
|
|
3
|
+
*
|
|
4
|
+
* The budget itself is in `core/budget.ts`, shared with the skill catalogue. What is here is the part that knows
|
|
5
|
+
* about tools: how many tokens one entry is, which is the only thing the generic algorithm cannot know.
|
|
6
|
+
*/
|
|
7
|
+
import { estimateTokens } from "../core/tokens.js";
|
|
8
|
+
/**
|
|
9
|
+
* Deliberately **not** re-exported here.
|
|
10
|
+
*
|
|
11
|
+
* `applyTokenBudget`, `TokenBudget` and `BudgetOutcome` live in `core/` and reach consumers through
|
|
12
|
+
* `@retinue/agentkit/runtime`. Re-exporting them from `./tools` as well gave one name two homes, which
|
|
13
|
+
* `public-surface.test.ts` fails on — and it is right to: a consumer reading two import paths for one function
|
|
14
|
+
* has to guess which is canonical, and the two will not stay in step.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* What a compact catalogue entry costs.
|
|
18
|
+
*
|
|
19
|
+
* The fields a model actually reads, plus a small per-entry allowance for the JSON or Markdown scaffolding the
|
|
20
|
+
* assembler wraps them in — measured at ~35 tokens per entry in #221's harness, which is what this reproduces.
|
|
21
|
+
* An estimate rather than a tokenizer call: the point is a stable ceiling, and a budget that shifted with the
|
|
22
|
+
* model's tokenizer would make one deployment's 4,000 tokens another's 4,600.
|
|
23
|
+
*/
|
|
24
|
+
export const ENTRY_OVERHEAD_TOKENS = 6;
|
|
25
|
+
export const entryTokens = (entry) => estimateTokens(`${entry.name} ${entry.label} ${entry.category} ${entry.effect} ${entry.description}`) +
|
|
26
|
+
ENTRY_OVERHEAD_TOKENS;
|
|
27
|
+
/** A preloaded tool carries its schemas, which is usually most of its cost. */
|
|
28
|
+
export const descriptorTokens = (descriptor) => entryTokens({
|
|
29
|
+
name: descriptor.name,
|
|
30
|
+
label: descriptor.label,
|
|
31
|
+
description: descriptor.description,
|
|
32
|
+
category: descriptor.category,
|
|
33
|
+
effect: descriptor.effect,
|
|
34
|
+
}) + estimateTokens(JSON.stringify(descriptor.inputSchema ?? {}));
|
|
35
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1,316 @@
|
|
|
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 type { ExecutionContext } from "../core/context.js";
|
|
47
|
+
import { type PlatformError } from "../core/errors.js";
|
|
48
|
+
/** An opaque handle. Its meaning belongs to the resolver, and no tool interprets it. */
|
|
49
|
+
export type CredentialRef = string;
|
|
50
|
+
/** How a secret is presented on the wire. */
|
|
51
|
+
export declare const CREDENTIAL_SCHEMES: readonly ["bearer", "basic", "custom-header"];
|
|
52
|
+
export type CredentialScheme = (typeof CREDENTIAL_SCHEMES)[number];
|
|
53
|
+
/**
|
|
54
|
+
* How a tenant supplied the credential.
|
|
55
|
+
*
|
|
56
|
+
* Not the same question as the scheme: an OAuth access token is presented as a bearer, so the two would collapse
|
|
57
|
+
* if they shared a type — and the collapse would lose exactly the fact #264 needs, which is whether there is a
|
|
58
|
+
* login URL to send someone to.
|
|
59
|
+
*/
|
|
60
|
+
export declare const AUTH_MODES: readonly ["token", "oauth2"];
|
|
61
|
+
export type AuthMode = (typeof AUTH_MODES)[number];
|
|
62
|
+
/**
|
|
63
|
+
* Non-secret vendor identifiers that travel with the credential.
|
|
64
|
+
*
|
|
65
|
+
* Atlassian's cloud id, WhatsApp's phone number id, Slack's team id — discovered at connection time, needed on
|
|
66
|
+
* every request, and **not secrets**. They live here rather than in a toolkit's configuration because they are
|
|
67
|
+
* per *connection*: two tenants using the same toolkit have different ones, which is the same reason the token
|
|
68
|
+
* is not configuration either.
|
|
69
|
+
*/
|
|
70
|
+
export type CredentialMetadata = Readonly<Record<string, string>>;
|
|
71
|
+
type WithMetadata = {
|
|
72
|
+
readonly metadata?: CredentialMetadata;
|
|
73
|
+
readonly mode?: AuthMode;
|
|
74
|
+
};
|
|
75
|
+
export type Credential = (WithMetadata & {
|
|
76
|
+
readonly scheme: "bearer";
|
|
77
|
+
readonly token: string;
|
|
78
|
+
}) | (WithMetadata & {
|
|
79
|
+
readonly scheme: "basic";
|
|
80
|
+
readonly username: string;
|
|
81
|
+
readonly password: string;
|
|
82
|
+
}) | (WithMetadata & {
|
|
83
|
+
readonly scheme: "custom-header";
|
|
84
|
+
readonly header: string;
|
|
85
|
+
readonly value: string;
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Builds a credential whose secret is **not enumerable** — AC-7.
|
|
89
|
+
*
|
|
90
|
+
* A typed object is far more likely to reach a log line than a bare string was: it gets spread into an error's
|
|
91
|
+
* `details`, passed to a structured logger, or serialised into an audit row, and every one of those uses
|
|
92
|
+
* `JSON.stringify` or an object spread. So the secret is defined non-enumerably and `toJSON`/`toString`/
|
|
93
|
+
* `util.inspect` are overridden. `credential.token` still reads normally; `{ ...credential }`,
|
|
94
|
+
* `JSON.stringify(credential)` and `console.log(credential)` do not.
|
|
95
|
+
*
|
|
96
|
+
* This is defence in depth, not a licence: a caller that reads `.token` and logs *that* is still logging a
|
|
97
|
+
* secret, and no type can stop it.
|
|
98
|
+
*/
|
|
99
|
+
export declare const createCredential: (input: Credential) => Credential;
|
|
100
|
+
/** A bearer credential, which is what most vendors want and what a plain string used to mean. */
|
|
101
|
+
export declare const bearer: (token: string, metadata?: CredentialMetadata, mode?: AuthMode) => Credential;
|
|
102
|
+
export interface CredentialResolver {
|
|
103
|
+
/**
|
|
104
|
+
* The credential behind a reference, for this caller.
|
|
105
|
+
*
|
|
106
|
+
* Takes the context because a reference is resolved *per tenant*: two tenants using the same toolkit name the
|
|
107
|
+
* same credential and mean different secrets, and a resolver that ignored the caller would hand one tenant
|
|
108
|
+
* another's token.
|
|
109
|
+
*/
|
|
110
|
+
resolve(input: {
|
|
111
|
+
readonly ref: CredentialRef;
|
|
112
|
+
readonly context: ExecutionContext;
|
|
113
|
+
}): Promise<Credential>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Told about every resolution — `docs/21`'s Connections section, "an audit record of every resolution".
|
|
117
|
+
*
|
|
118
|
+
* A sink rather than a return value, so a toolkit cannot forget to report and a host cannot be surprised by
|
|
119
|
+
* one. **Both outcomes are reported**: a refused resolution is the more interesting audit event, because a
|
|
120
|
+
* successful one is the normal case and a refused one is somebody asking for something they do not have.
|
|
121
|
+
*
|
|
122
|
+
* It never receives the credential. An audit trail that carries the secret is a second copy of the secret in a
|
|
123
|
+
* place designed to be kept for a long time.
|
|
124
|
+
*/
|
|
125
|
+
export interface CredentialAudit {
|
|
126
|
+
onResolved(input: {
|
|
127
|
+
readonly ref: CredentialRef;
|
|
128
|
+
readonly context: ExecutionContext;
|
|
129
|
+
readonly scheme: CredentialScheme;
|
|
130
|
+
}): Promise<void> | void;
|
|
131
|
+
onRefused(input: {
|
|
132
|
+
readonly ref: CredentialRef;
|
|
133
|
+
readonly context: ExecutionContext;
|
|
134
|
+
readonly reason: string;
|
|
135
|
+
}): Promise<void> | void;
|
|
136
|
+
}
|
|
137
|
+
export declare const credentialMissing: (ref: CredentialRef, detail?: string) => PlatformError;
|
|
138
|
+
/**
|
|
139
|
+
* The credential resolved is not the shape this toolkit can present — #260 AC-2.
|
|
140
|
+
*
|
|
141
|
+
* Raised at **construction** where possible, and at resolution otherwise. A misconfiguration discovered as a
|
|
142
|
+
* vendor 401 an hour later is the failure this exists to prevent: the vendor's message says the token is
|
|
143
|
+
* invalid, which sends an operator to rotate a token that was never the problem.
|
|
144
|
+
*/
|
|
145
|
+
export declare const credentialSchemeMismatch: (ref: CredentialRef, expected: readonly CredentialScheme[], got: CredentialScheme) => PlatformError;
|
|
146
|
+
/**
|
|
147
|
+
* A resolver over a plain map, for a single-tenant deployment and for tests.
|
|
148
|
+
*
|
|
149
|
+
* Shipped because the alternative is every host writing the same six lines, and the sixth one writing it with a
|
|
150
|
+
* fallback to `process.env` — which is the thing this module exists to prevent. Explicitly *not* environment
|
|
151
|
+
* backed: a host that wants that passes `{ github: process.env.GITHUB_TOKEN ?? "" }` and can see it doing so.
|
|
152
|
+
*
|
|
153
|
+
* **A bare string still works**, and stays the common case: it means a bearer token, which is what it meant
|
|
154
|
+
* before #260. The single-tenant path must not get harder because multi-tenant got possible.
|
|
155
|
+
*/
|
|
156
|
+
export declare const createStaticCredentialResolver: (secrets: Readonly<Record<string, string | Credential>>) => CredentialResolver;
|
|
157
|
+
/**
|
|
158
|
+
* Wraps a resolver so every resolution is audited, and so a scheme mismatch is caught here rather than by the
|
|
159
|
+
* vendor — #260 AC-2 and AC-8.
|
|
160
|
+
*
|
|
161
|
+
* A wrapper rather than a change to every resolver: a host's own resolver stays a four-line object, and the
|
|
162
|
+
* cross-cutting obligations are added once, where they cannot be forgotten per toolkit.
|
|
163
|
+
*/
|
|
164
|
+
export declare const withCredentialAudit: (resolver: CredentialResolver, audit: CredentialAudit, expected?: readonly CredentialScheme[]) => CredentialResolver;
|
|
165
|
+
/**
|
|
166
|
+
* The `Authorization`-style header a credential presents as.
|
|
167
|
+
*
|
|
168
|
+
* One function, so twenty toolkits do not each write their own base64 and get the padding wrong. Returns the
|
|
169
|
+
* header **name and value**, because `custom-header` does not use `Authorization`.
|
|
170
|
+
*/
|
|
171
|
+
export declare const credentialHeader: (credential: Credential) => readonly [string, string];
|
|
172
|
+
/**
|
|
173
|
+
* What a toolkit accepts — #260 AC-2.
|
|
174
|
+
*
|
|
175
|
+
* Declared by the toolkit and checked at **construction**, so wiring a Basic-auth vendor with a bearer token is
|
|
176
|
+
* a startup error naming both, rather than a vendor 401 an hour later whose message says the token is invalid.
|
|
177
|
+
*
|
|
178
|
+
* `modes` is the other axis and is not the platform's business to validate — it is a fact about the vendor that
|
|
179
|
+
* a deployment reads: "GitHub takes a PAT or OAuth", "Google is OAuth only". #262 uses it to decide which
|
|
180
|
+
* connection flows to offer, and #264 uses it to decide whether an unconnected tool can pause a run for consent
|
|
181
|
+
* or must simply fail, since a token has no login URL.
|
|
182
|
+
*/
|
|
183
|
+
export type ToolkitAuth = {
|
|
184
|
+
/** How a tenant may obtain the credential. */
|
|
185
|
+
readonly modes: readonly AuthMode[];
|
|
186
|
+
/** The scheme(s) this toolkit can present. A credential of any other scheme is refused. */
|
|
187
|
+
readonly schemes: readonly CredentialScheme[];
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Refuses a toolkit configuration whose credential cannot be presented — at construction.
|
|
191
|
+
*
|
|
192
|
+
* Takes the *declared* scheme rather than resolving, because resolution needs a context and construction has
|
|
193
|
+
* none. A host that wires a static map can therefore be told immediately; a host whose resolver is dynamic is
|
|
194
|
+
* caught by `withCredentialAudit` at the first call instead. Both are before the vendor sees anything.
|
|
195
|
+
*/
|
|
196
|
+
export declare const assertToolkitAuth: (ref: CredentialRef, auth: ToolkitAuth, declared: CredentialScheme | undefined) => void;
|
|
197
|
+
/**
|
|
198
|
+
* A credential that stops working at a known time.
|
|
199
|
+
*
|
|
200
|
+
* Additive by construction: `RefreshableCredential` is a `Credential` with one more field, so a resolver that
|
|
201
|
+
* returns a plain one is unchanged and the eight shipped toolkits compile untouched. That is AC-1, and it is a
|
|
202
|
+
* property of the *type* rather than a claim about the code.
|
|
203
|
+
*
|
|
204
|
+
* The refresh token is deliberately **not here.** It lives wherever the host keeps it — `ConnectionStore` seals
|
|
205
|
+
* it (#261) — and only the refresher ever sees it. Putting it on the credential would mean the longest-lived
|
|
206
|
+
* secret in an OAuth grant travelling through every toolkit that only needed the short-lived one.
|
|
207
|
+
*/
|
|
208
|
+
export type RefreshableCredential = Credential & {
|
|
209
|
+
/** ISO 8601. When the vendor stops accepting this. */
|
|
210
|
+
readonly expiresAt: string;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Adds an expiry to a credential **without losing its secret protection** — AC-6.
|
|
214
|
+
*
|
|
215
|
+
* This exists because the obvious way to build one is wrong, and wrong invisibly. A host writing a
|
|
216
|
+
* `CredentialRefresher` reaches for:
|
|
217
|
+
*
|
|
218
|
+
* ```ts
|
|
219
|
+
* return { ...bearer(accessToken), expiresAt }; // ← the secret is now enumerable
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* `createCredential` defines the secret **non-enumerably**, which is precisely what makes it survive a
|
|
223
|
+
* `JSON.stringify` into a log line — and precisely what a spread drops. The result looks identical, works
|
|
224
|
+
* identically, and serialises the token into the first structured log that touches it.
|
|
225
|
+
*
|
|
226
|
+
* Found by the AC-6 test failing against this repository's own test helper, which had made exactly that
|
|
227
|
+
* mistake. If the helper made it, a host will.
|
|
228
|
+
*/
|
|
229
|
+
export declare const refreshable: (credential: Credential, expiresAt: string) => RefreshableCredential;
|
|
230
|
+
export declare const isRefreshable: (credential: Credential) => credential is RefreshableCredential;
|
|
231
|
+
/**
|
|
232
|
+
* How early a token is replaced.
|
|
233
|
+
*
|
|
234
|
+
* Sixty seconds, and the number is a *commitment* rather than a guess: a tool call can take tens of seconds —
|
|
235
|
+
* a slow vendor, a large upload, a retry — and a token that was valid when the call started must still be
|
|
236
|
+
* valid when it arrives. Refreshing exactly at expiry makes "expired mid-flight" the common case rather than
|
|
237
|
+
* the rare one, and that failure looks like an intermittent authentication bug.
|
|
238
|
+
*
|
|
239
|
+
* AC-5. Configurable because a deployment whose calls are slower than this needs more.
|
|
240
|
+
*/
|
|
241
|
+
export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
|
|
242
|
+
/** Whether a credential is expired, or close enough that it should be replaced before use. */
|
|
243
|
+
export declare const isExpiring: (credential: Credential, skewMs: number, now: number) => boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Obtains a fresh credential for a reference.
|
|
246
|
+
*
|
|
247
|
+
* A port, because how a token is renewed differs entirely by vendor and by where the grant is stored. The
|
|
248
|
+
* host's implementation reads its own `ConnectionStore`, calls the vendor's token endpoint, re-seals the new
|
|
249
|
+
* refresh token and returns the new access credential — none of which the runtime should know about.
|
|
250
|
+
*
|
|
251
|
+
* It is given the ref and the context, and **not** the expired credential: it has to look the grant up anyway,
|
|
252
|
+
* and handing it a dead secret would be one more copy of one for no purpose.
|
|
253
|
+
*/
|
|
254
|
+
export interface CredentialRefresher {
|
|
255
|
+
refresh(input: {
|
|
256
|
+
readonly ref: CredentialRef;
|
|
257
|
+
readonly context: ExecutionContext;
|
|
258
|
+
}): Promise<RefreshableCredential>;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A refresh that failed because the grant is gone, rather than because the network was.
|
|
262
|
+
*
|
|
263
|
+
* The distinction is the whole of AC-3, and it is not cosmetic: `invalid_grant` means a person must consent
|
|
264
|
+
* again and no amount of retrying will help, while a timeout means try again in a second. A runtime that
|
|
265
|
+
* conflates them either retries a dead grant forever or asks a user to re-authorise because of a blip.
|
|
266
|
+
*/
|
|
267
|
+
export declare const REFRESH_GRANT_ERRORS: readonly ["invalid_grant", "invalid_client", "unauthorized_client", "access_denied"];
|
|
268
|
+
export declare const isGrantError: (error: unknown) => boolean;
|
|
269
|
+
export type RefreshingResolverOptions = {
|
|
270
|
+
readonly skewMs?: number;
|
|
271
|
+
/** Injectable so a test can move time without waiting for it. */
|
|
272
|
+
readonly now?: () => number;
|
|
273
|
+
/**
|
|
274
|
+
* Told that a refresh happened. Never told the token — see `CredentialAudit`, same reasoning.
|
|
275
|
+
*
|
|
276
|
+
* `expiresAt` is included because it is not a secret and it is the one thing an operator debugging a
|
|
277
|
+
* refresh loop actually needs.
|
|
278
|
+
*/
|
|
279
|
+
readonly onRefreshed?: (input: {
|
|
280
|
+
readonly ref: CredentialRef;
|
|
281
|
+
readonly tenantId: string;
|
|
282
|
+
readonly expiresAt: string;
|
|
283
|
+
}) => void;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Wraps a resolver so an expiring credential is renewed before it is handed out.
|
|
287
|
+
*
|
|
288
|
+
* A wrapper, like `withCredentialAudit`, and for the same reason: the eight shipped toolkits already resolve
|
|
289
|
+
* **per call**, so they pick this up without a line changing. A toolkit that cached a credential at
|
|
290
|
+
* construction would defeat it, which is why `createGitHubToolkit` and every sibling resolve inside `call()`.
|
|
291
|
+
*
|
|
292
|
+
* ## Time-driven, never 401-driven — AC-7
|
|
293
|
+
*
|
|
294
|
+
* The obvious design is to refresh when the vendor returns 401. It is wrong, and worth stating plainly because
|
|
295
|
+
* it is what most integrations do:
|
|
296
|
+
*
|
|
297
|
+
* A 401 is what a vendor returns for an expired token, a **revoked** grant, a token for the wrong tenant, and a
|
|
298
|
+
* scope the grant never had. Refreshing on 401 therefore turns a revoked grant into an infinite refresh loop
|
|
299
|
+
* against the vendor's token endpoint, and turns a missing scope into a refresh that succeeds and a call that
|
|
300
|
+
* fails again identically. Neither is diagnosable from the outside.
|
|
301
|
+
*
|
|
302
|
+
* Time is the only signal that means what it says: a token with an expiry in the past is expired, and nothing
|
|
303
|
+
* else is inferred from it. A 401 on a freshly-refreshed token is a real error and is surfaced as one.
|
|
304
|
+
*
|
|
305
|
+
* ## One refresh, not N — AC-2
|
|
306
|
+
*
|
|
307
|
+
* Twenty concurrent tool calls hitting an expired token must produce **one** refresh. Refresh endpoints rate
|
|
308
|
+
* limit, and — worse — several vendors invalidate the previous refresh token when one is used, so N concurrent
|
|
309
|
+
* refreshes race to invalidate each other and log the deployment out permanently.
|
|
310
|
+
*
|
|
311
|
+
* The in-flight promise is stored *before* the first await, so a second caller entering the function
|
|
312
|
+
* synchronously after the first still finds it.
|
|
313
|
+
*/
|
|
314
|
+
export declare const withRefreshingCredentials: (resolver: CredentialResolver, refresher: CredentialRefresher, options?: RefreshingResolverOptions) => CredentialResolver;
|
|
315
|
+
export {};
|
|
316
|
+
//# sourceMappingURL=credentials.d.ts.map
|