@economic/agents 2.5.0 → 2.6.1
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 +33 -0
- package/dist/index.d.mts +88 -1
- package/dist/index.mjs +224 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -514,6 +514,39 @@ protected async getUserContext(jwtToken: string) {
|
|
|
514
514
|
}
|
|
515
515
|
```
|
|
516
516
|
|
|
517
|
+
If `getUserContext` throws, the socket closes with `1011` — or, when the thrown `Error` carries a numeric `closeCode` in the `4000`–`4999` range, with that code and an `"unauthorized"` status frame:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const error = new Error("No Eva access");
|
|
521
|
+
(error as Error & { closeCode: number }).closeCode = 4003;
|
|
522
|
+
throw error;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Token exchange
|
|
526
|
+
|
|
527
|
+
For agents whose clients connect with a _down-scoped_ token — one only valid for the client ↔ agent hop — override the static `getTokenExchangeConfig(env)` to exchange it (RFC 8693) at the identity server for a token issued to the agent's own OAuth client. Requires `getJwtAuthConfig`.
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
export class SupportAgent extends Agent {
|
|
531
|
+
static getTokenExchangeConfig(env: Cloudflare.Env) {
|
|
532
|
+
return {
|
|
533
|
+
tokenUrl: `${env.IDENTITY_ENDPOINT}/connect/token`,
|
|
534
|
+
clientId: env.OAUTH_CLIENT_ID,
|
|
535
|
+
clientSecret: env.OAUTH_CLIENT_SECRET, // string or Secrets Store binding
|
|
536
|
+
scopes: "api.read api.write", // always sent explicitly
|
|
537
|
+
tokenHeader: "X-EconomicToken", // default
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
When configured:
|
|
544
|
+
|
|
545
|
+
- The verified connect token is exchanged on every connect; the exchanged token is what `getUserContext` receives and what tools see as `ctx.tokens[tokenHeader]` each turn. **Client-sent request-body tokens for that key are ignored** — tool credentials always derive from the verified connection identity, so clients should stop sending them.
|
|
546
|
+
- Renewal is connection-based. The client token rides in the WebSocket protocols, so presenting a renewed token always means a reconnect, which re-verifies and re-exchanges. Keep the `authToken` passed to the hooks current (refresh it before expiry) and the socket rotates silently — the `unauthorized` close (`4001`) then only happens when the session is genuinely dead (logout, revocation).
|
|
547
|
+
- The exchanged token expires on the identity server's clock (with a 30s margin). A turn arriving after expiry closes the connection `4001` to force a reconnect; a client that refreshes its token on time never hits this.
|
|
548
|
+
- Exchange failures at connect: `invalid_grant` (dead session token) → status `"unauthorized"` + close `4001`; anything else (identity server down, misconfigured client) → status `"error"` + close `1011`. Partial configuration is the app's concern — throw from `getTokenExchangeConfig` if you consider it a deployment fault, or return `undefined` to disable the exchange.
|
|
549
|
+
|
|
517
550
|
## Observability
|
|
518
551
|
|
|
519
552
|
Emitted per turn from the `ai` SDK's OpenTelemetry spans, no setup beyond bindings:
|
package/dist/index.d.mts
CHANGED
|
@@ -26,6 +26,61 @@ type SqlFn = InstanceType<typeof Agent$1>["sql"];
|
|
|
26
26
|
type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
|
|
27
27
|
type AgentConnectionType = "agent" | "chat" | "assistant";
|
|
28
28
|
//#endregion
|
|
29
|
+
//#region src/server/features/auth/tokenExchange.d.ts
|
|
30
|
+
interface TokenExchangeConfig {
|
|
31
|
+
/** Identity server token endpoint, e.g. `${identityOrigin}/connect/token`. */
|
|
32
|
+
tokenUrl: string;
|
|
33
|
+
/** OAuth client the agent authenticates as. */
|
|
34
|
+
clientId: string;
|
|
35
|
+
/**
|
|
36
|
+
* Plain secret, or a binding resolved with `get()` (e.g. a Cloudflare
|
|
37
|
+
* Secrets Store binding). Resolved on every exchange so rotation is
|
|
38
|
+
* picked up without a redeploy.
|
|
39
|
+
*/
|
|
40
|
+
clientSecret: string | {
|
|
41
|
+
get(): Promise<string>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Space-separated scopes for the exchanged token. Always sent explicitly:
|
|
45
|
+
* omitting `scope` would make the identity server issue every scope the
|
|
46
|
+
* client is allowed, the opposite of least privilege.
|
|
47
|
+
*/
|
|
48
|
+
scopes: string;
|
|
49
|
+
/**
|
|
50
|
+
* Key under the request-context `tokens` record that tools read the token
|
|
51
|
+
* from. Defaults to {@link DEFAULT_TOKEN_HEADER}.
|
|
52
|
+
*/
|
|
53
|
+
tokenHeader?: string;
|
|
54
|
+
}
|
|
55
|
+
interface ExchangedToken {
|
|
56
|
+
accessToken: string;
|
|
57
|
+
/** Unix ms; already includes {@link EXPIRY_MARGIN_MS}. */
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
}
|
|
60
|
+
type TokenExchangeResult = ({
|
|
61
|
+
success: true;
|
|
62
|
+
} & ExchangedToken) | {
|
|
63
|
+
success: false;
|
|
64
|
+
status: number;
|
|
65
|
+
errorCode: string;
|
|
66
|
+
message: string;
|
|
67
|
+
/**
|
|
68
|
+
* True when the identity server rejected the subject token itself
|
|
69
|
+
* (RFC 6749 `invalid_grant`: expired or revoked). Re-authenticating is
|
|
70
|
+
* the only fix — other failures (`invalid_client`, `invalid_scope`,
|
|
71
|
+
* 5xx) are deployment or identity-server faults the user cannot fix by
|
|
72
|
+
* logging in again.
|
|
73
|
+
*/
|
|
74
|
+
subjectTokenRejected: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Exchange `subjectToken` for a token issued to the agent's own OAuth client.
|
|
78
|
+
*
|
|
79
|
+
* Expected identity-server rejections return a failure result; unexpected
|
|
80
|
+
* failures (network error, timeout, malformed success response) throw.
|
|
81
|
+
*/
|
|
82
|
+
declare function exchangeToken(subjectToken: string, config: TokenExchangeConfig): Promise<TokenExchangeResult>;
|
|
83
|
+
//#endregion
|
|
29
84
|
//#region src/server/util/tools.d.ts
|
|
30
85
|
type ToolSet = Record<string, Tool>;
|
|
31
86
|
type Tool<Context extends Record<string, unknown> = Record<string, unknown>, INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any> = Tool$1<INPUT, OUTPUT> & {
|
|
@@ -141,6 +196,38 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
|
|
|
141
196
|
* @returns JWT auth config or undefined to skip authentication
|
|
142
197
|
*/
|
|
143
198
|
static getJwtAuthConfig?(env: Cloudflare.Env): JwtAuthConfig<Record<string, unknown>> | undefined;
|
|
199
|
+
/**
|
|
200
|
+
* Override to exchange the verified connect token (RFC 8693) for a token
|
|
201
|
+
* issued to this agent's own OAuth client. Requires {@link getJwtAuthConfig}.
|
|
202
|
+
*
|
|
203
|
+
* When configured, the exchanged token is passed to {@link getUserContext}
|
|
204
|
+
* and injected into the tool context every turn under
|
|
205
|
+
* `tokens[config.tokenHeader]` — client-sent request-body tokens for that
|
|
206
|
+
* key are ignored. Renewal is connection-based: a client presenting a new
|
|
207
|
+
* token reconnects, and the connect flow re-verifies and re-exchanges.
|
|
208
|
+
*
|
|
209
|
+
* Do not add side effects to this method - return a single config from it,
|
|
210
|
+
* or undefined to disable the exchange.
|
|
211
|
+
*
|
|
212
|
+
* @param env - The worker environment variables
|
|
213
|
+
* @returns Token exchange config or undefined to disable
|
|
214
|
+
*/
|
|
215
|
+
static getTokenExchangeConfig?(env: Cloudflare.Env): TokenExchangeConfig | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* The exchanged token for this agent, refreshed on every authenticated
|
|
218
|
+
* connect. Kept in memory for the hot path and mirrored to DO storage so
|
|
219
|
+
* turns that run after hibernation (or without a connection at all) still
|
|
220
|
+
* have it. Short-lived by construction — at most the identity server's
|
|
221
|
+
* token lifetime.
|
|
222
|
+
*/
|
|
223
|
+
private _exchangedToken?;
|
|
224
|
+
private _storeExchangedToken;
|
|
225
|
+
private _getExchangedToken;
|
|
226
|
+
/**
|
|
227
|
+
* Swaps the client-sent token for the exchanged one in a copy of the turn
|
|
228
|
+
* body. No-op when token exchange is not configured.
|
|
229
|
+
*/
|
|
230
|
+
private _applyTokenExchange;
|
|
144
231
|
/**
|
|
145
232
|
* The user context for the session.
|
|
146
233
|
* Define getUserContext to set a user context.
|
|
@@ -365,4 +452,4 @@ declare function skill<Context extends Record<string, unknown> = Record<string,
|
|
|
365
452
|
//#region src/server/index.d.ts
|
|
366
453
|
declare function getCurrentToolContext(): any;
|
|
367
454
|
//#endregion
|
|
368
|
-
export { Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type FacetStub, type LegacyChatStub, type LegacyMessageFeedback, type MigrationDeps, type Skill, type SkillSource, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
|
|
455
|
+
export { Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type FacetStub, type LegacyChatStub, type LegacyMessageFeedback, type MigrationDeps, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, exchangeToken, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,178 @@ import { Think, skills } from "@cloudflare/think";
|
|
|
4
4
|
import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { createCompactFunction } from "agents/experimental/memory/utils";
|
|
7
|
+
//#region src/server/util/agent.ts
|
|
8
|
+
function sendConnectionStatus(connection, status) {
|
|
9
|
+
connection.send(JSON.stringify({
|
|
10
|
+
type: "connection_status",
|
|
11
|
+
status
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function sendAgentType(connection, agentType) {
|
|
15
|
+
connection.send(JSON.stringify({
|
|
16
|
+
type: "agent_type",
|
|
17
|
+
agentType
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function sendSubAgentName(connection, subAgentName) {
|
|
21
|
+
connection.send(JSON.stringify({
|
|
22
|
+
type: "sub_agent_name",
|
|
23
|
+
subAgentName
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Close code for a getUserContext failure. Subclasses signal an intentional
|
|
28
|
+
* auth-style rejection by throwing an Error carrying a numeric `closeCode`
|
|
29
|
+
* in the WebSocket application range (4000-4999); anything else is treated
|
|
30
|
+
* as an internal error (1011).
|
|
31
|
+
*/
|
|
32
|
+
function getConnectCloseCode(error) {
|
|
33
|
+
const closeCode = error?.closeCode;
|
|
34
|
+
if (typeof closeCode === "number" && closeCode >= 4e3 && closeCode <= 4999) return closeCode;
|
|
35
|
+
return 1011;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/server/features/auth/tokenExchange.ts
|
|
39
|
+
/**
|
|
40
|
+
* OAuth 2.0 Token Exchange (RFC 8693) against an identity server.
|
|
41
|
+
*
|
|
42
|
+
* The client connects with a token down-scoped for the client ↔ agent hop only.
|
|
43
|
+
* When an agent defines `static getTokenExchangeConfig`, the verified connect
|
|
44
|
+
* token is exchanged — authenticating as the agent's own OAuth client — for a
|
|
45
|
+
* token carrying the scopes the agent needs against backends. Backends then see
|
|
46
|
+
* the agent's `client_id` and can attribute calls to it, and the browser never
|
|
47
|
+
* holds a backend-capable credential.
|
|
48
|
+
*
|
|
49
|
+
* Expected failures (the identity server rejecting the request) return a result
|
|
50
|
+
* object; unexpected failures (network errors, malformed responses) throw. Same
|
|
51
|
+
* contract as `verifyJwt` in ./index.ts.
|
|
52
|
+
*/
|
|
53
|
+
const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
54
|
+
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
55
|
+
const TOKEN_EXCHANGE_TIMEOUT_MS = 3e4;
|
|
56
|
+
/** Exchanged tokens are treated as expired this long before they actually are. */
|
|
57
|
+
const EXPIRY_MARGIN_MS = 3e4;
|
|
58
|
+
/**
|
|
59
|
+
* Exchange `subjectToken` for a token issued to the agent's own OAuth client.
|
|
60
|
+
*
|
|
61
|
+
* Expected identity-server rejections return a failure result; unexpected
|
|
62
|
+
* failures (network error, timeout, malformed success response) throw.
|
|
63
|
+
*/
|
|
64
|
+
async function exchangeToken(subjectToken, config) {
|
|
65
|
+
const clientSecret = typeof config.clientSecret === "string" ? config.clientSecret : await config.clientSecret.get();
|
|
66
|
+
const body = new URLSearchParams({
|
|
67
|
+
client_id: config.clientId,
|
|
68
|
+
client_secret: clientSecret,
|
|
69
|
+
grant_type: TOKEN_EXCHANGE_GRANT_TYPE,
|
|
70
|
+
subject_token: subjectToken,
|
|
71
|
+
subject_token_type: ACCESS_TOKEN_TYPE,
|
|
72
|
+
scope: config.scopes
|
|
73
|
+
});
|
|
74
|
+
const response = await fetch(config.tokenUrl, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
77
|
+
body: body.toString(),
|
|
78
|
+
signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS)
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
let errorCode = "unknown_error";
|
|
82
|
+
let description;
|
|
83
|
+
try {
|
|
84
|
+
const json = await response.json();
|
|
85
|
+
errorCode = json.error ?? errorCode;
|
|
86
|
+
description = json.error_description;
|
|
87
|
+
} catch {}
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
status: response.status,
|
|
91
|
+
errorCode,
|
|
92
|
+
message: `Token exchange failed with status ${response.status} (${errorCode})${description ? `: ${description}` : ""}`,
|
|
93
|
+
subjectTokenRejected: response.status === 400 && errorCode === "invalid_grant"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
let json;
|
|
97
|
+
try {
|
|
98
|
+
json = await response.json();
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error("Token exchange response body was not valid JSON");
|
|
101
|
+
}
|
|
102
|
+
if (!json.access_token) throw new Error("Token exchange response contained no access_token");
|
|
103
|
+
if (typeof json.expires_in !== "number") throw new Error("Token exchange response contained no expires_in");
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
accessToken: json.access_token,
|
|
107
|
+
expiresAt: Date.now() + Math.max(0, json.expires_in * 1e3 - EXPIRY_MARGIN_MS)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns a copy of the request body with the exchanged token injected under
|
|
112
|
+
* `tokens[tokenHeader]`, overriding any client-sent value for that key.
|
|
113
|
+
*
|
|
114
|
+
* Never mutates `body`: think persists the original request body
|
|
115
|
+
* (`_lastBody`) and replays it on continuation turns, so a mutation would
|
|
116
|
+
* persist the exchanged token and replay it as a subject token later.
|
|
117
|
+
*/
|
|
118
|
+
function withExchangedToken(body, tokenHeader, accessToken) {
|
|
119
|
+
const tokens = body?.tokens ?? {};
|
|
120
|
+
return {
|
|
121
|
+
...body,
|
|
122
|
+
tokens: {
|
|
123
|
+
...tokens,
|
|
124
|
+
[tokenHeader]: accessToken
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Connect-time exchange. On failure, signals the client and closes the
|
|
130
|
+
* connection — `unauthorized`/4001 when the session token itself was rejected
|
|
131
|
+
* (only a re-login fixes that), `error`/1011 for identity-server or
|
|
132
|
+
* configuration faults — and returns undefined so the caller abandons the
|
|
133
|
+
* connect.
|
|
134
|
+
*/
|
|
135
|
+
async function exchangeConnectToken(subjectToken, config, connection) {
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = await exchangeToken(subjectToken, config);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
sendConnectionStatus(connection, "error");
|
|
141
|
+
console.error(`[Agent] Token exchange error - ${error}`);
|
|
142
|
+
connection.close(1011, "Token exchange failed");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
console.error(`[Agent] Token exchange failed - ${result.message}`);
|
|
147
|
+
if (result.subjectTokenRejected) {
|
|
148
|
+
sendConnectionStatus(connection, "unauthorized");
|
|
149
|
+
connection.close(4001, "Unauthorized");
|
|
150
|
+
} else {
|
|
151
|
+
sendConnectionStatus(connection, "error");
|
|
152
|
+
connection.close(1011, "Token exchange failed");
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
accessToken: result.accessToken,
|
|
158
|
+
expiresAt: result.expiresAt
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Turn-time injection: returns a body copy with the held token under
|
|
163
|
+
* `tokens[config.tokenHeader]`. When the held token is missing or expired the
|
|
164
|
+
* turn cannot run — only a re-authenticating reconnect delivers a fresh
|
|
165
|
+
* subject token — so the current connection (if any) is closed to force one,
|
|
166
|
+
* and this throws to fail the turn.
|
|
167
|
+
*/
|
|
168
|
+
function injectExchangedToken(body, config, token, connection) {
|
|
169
|
+
if (!token || token.expiresAt <= Date.now()) {
|
|
170
|
+
if (connection) {
|
|
171
|
+
sendConnectionStatus(connection, "unauthorized");
|
|
172
|
+
connection.close(4001, "Unauthorized");
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Exchanged token missing or expired - the client must reconnect");
|
|
175
|
+
}
|
|
176
|
+
return withExchangedToken(body, config.tokenHeader ?? "X-EconomicToken", token.accessToken);
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
7
179
|
//#region src/server/util/tools.ts
|
|
8
180
|
function tool(tool) {
|
|
9
181
|
return tool$1(tool);
|
|
@@ -92,26 +264,6 @@ async function touchAgentInstance(db, input) {
|
|
|
92
264
|
WHERE agent_name = ? AND durable_object_name = ?`).bind(input.updatedAt, input.agentName, input.durableObjectName).run();
|
|
93
265
|
}
|
|
94
266
|
//#endregion
|
|
95
|
-
//#region src/server/util/agent.ts
|
|
96
|
-
function sendConnectionStatus(connection, status) {
|
|
97
|
-
connection.send(JSON.stringify({
|
|
98
|
-
type: "connection_status",
|
|
99
|
-
status
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
function sendAgentType(connection, agentType) {
|
|
103
|
-
connection.send(JSON.stringify({
|
|
104
|
-
type: "agent_type",
|
|
105
|
-
agentType
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
|
-
function sendSubAgentName(connection, subAgentName) {
|
|
109
|
-
connection.send(JSON.stringify({
|
|
110
|
-
type: "sub_agent_name",
|
|
111
|
-
subAgentName
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
//#endregion
|
|
115
267
|
//#region src/server/util/llm.ts
|
|
116
268
|
/**
|
|
117
269
|
* Convention: tools can include a `sources` array in their return value to surface
|
|
@@ -148,6 +300,7 @@ function buildSourcesTransform(additional) {
|
|
|
148
300
|
}
|
|
149
301
|
//#endregion
|
|
150
302
|
//#region src/server/agents/Agent.ts
|
|
303
|
+
const EXCHANGED_TOKEN_STORAGE_KEY = "economic:exchangedToken";
|
|
151
304
|
var Agent = class extends Think {
|
|
152
305
|
agentType = "agent";
|
|
153
306
|
clientIp;
|
|
@@ -207,7 +360,24 @@ var Agent = class extends Think {
|
|
|
207
360
|
return;
|
|
208
361
|
}
|
|
209
362
|
const token = extractTokenFromConnectRequest(ctx.request);
|
|
210
|
-
|
|
363
|
+
let effectiveToken = token;
|
|
364
|
+
const getTokenExchangeConfig = this.constructor.getTokenExchangeConfig;
|
|
365
|
+
const exchangeConfig = getTokenExchangeConfig?.(this.env);
|
|
366
|
+
if (token && exchangeConfig) {
|
|
367
|
+
const exchanged = await exchangeConnectToken(token, exchangeConfig, connection);
|
|
368
|
+
if (!exchanged) return;
|
|
369
|
+
await this._storeExchangedToken(exchanged);
|
|
370
|
+
effectiveToken = exchanged.accessToken;
|
|
371
|
+
}
|
|
372
|
+
if (effectiveToken && this.getUserContext) try {
|
|
373
|
+
connection.setState({ userContext: await this.getUserContext(effectiveToken) });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const closeCode = getConnectCloseCode(error);
|
|
376
|
+
sendConnectionStatus(connection, closeCode === 1011 ? "error" : "unauthorized");
|
|
377
|
+
console.error(`[Agent] getUserContext error - ${error}`);
|
|
378
|
+
connection.close(closeCode, "Could not establish user context");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
211
381
|
}
|
|
212
382
|
}
|
|
213
383
|
sendConnectionStatus(connection, "connected");
|
|
@@ -231,7 +401,8 @@ var Agent = class extends Think {
|
|
|
231
401
|
*/
|
|
232
402
|
async beforeTurn(ctx) {
|
|
233
403
|
this.registerInstanceActivity();
|
|
234
|
-
|
|
404
|
+
const body = await this._applyTokenExchange(ctx.body);
|
|
405
|
+
this._toolContextForCurrentTurn = this.getToolContextForCurrentTurn(body);
|
|
235
406
|
this._toolsForCurrentTurn = ctx.tools;
|
|
236
407
|
const activeSkillName = this._getLastActivatedSkillName(ctx.messages);
|
|
237
408
|
const { tools, activeTools } = await this._getAuthorizedTools(ctx.tools, activeSkillName);
|
|
@@ -424,6 +595,36 @@ var Agent = class extends Think {
|
|
|
424
595
|
}
|
|
425
596
|
}
|
|
426
597
|
/**
|
|
598
|
+
* The exchanged token for this agent, refreshed on every authenticated
|
|
599
|
+
* connect. Kept in memory for the hot path and mirrored to DO storage so
|
|
600
|
+
* turns that run after hibernation (or without a connection at all) still
|
|
601
|
+
* have it. Short-lived by construction — at most the identity server's
|
|
602
|
+
* token lifetime.
|
|
603
|
+
*/
|
|
604
|
+
_exchangedToken;
|
|
605
|
+
async _storeExchangedToken(token) {
|
|
606
|
+
this._exchangedToken = token;
|
|
607
|
+
await this.ctx.storage.put(EXCHANGED_TOKEN_STORAGE_KEY, token);
|
|
608
|
+
}
|
|
609
|
+
async _getExchangedToken() {
|
|
610
|
+
if (!this._exchangedToken) this._exchangedToken = await this.ctx.storage.get(EXCHANGED_TOKEN_STORAGE_KEY);
|
|
611
|
+
if (this._exchangedToken && this._exchangedToken.expiresAt <= Date.now()) {
|
|
612
|
+
this._exchangedToken = void 0;
|
|
613
|
+
await this.ctx.storage.delete(EXCHANGED_TOKEN_STORAGE_KEY);
|
|
614
|
+
}
|
|
615
|
+
return this._exchangedToken;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Swaps the client-sent token for the exchanged one in a copy of the turn
|
|
619
|
+
* body. No-op when token exchange is not configured.
|
|
620
|
+
*/
|
|
621
|
+
async _applyTokenExchange(body) {
|
|
622
|
+
const getTokenExchangeConfig = this.constructor.getTokenExchangeConfig;
|
|
623
|
+
const exchangeConfig = getTokenExchangeConfig?.(this.env);
|
|
624
|
+
if (!exchangeConfig) return body;
|
|
625
|
+
return injectExchangedToken(body, exchangeConfig, await this._getExchangedToken(), getCurrentAgent().connection);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
427
628
|
* The user context for the session.
|
|
428
629
|
* Define getUserContext to set a user context.
|
|
429
630
|
*/
|
|
@@ -1378,4 +1579,4 @@ function getCurrentToolContext() {
|
|
|
1378
1579
|
return getCurrentAgent().agent._lastBody;
|
|
1379
1580
|
}
|
|
1380
1581
|
//#endregion
|
|
1381
|
-
export { Agent, Assistant, ChatAgent, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
|
|
1582
|
+
export { Agent, Assistant, ChatAgent, exchangeToken, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
|