@agentforge-io/core 2.0.11 → 2.0.15
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.
|
@@ -171,7 +171,9 @@ class AgentService {
|
|
|
171
171
|
// resolved agent declares one (e.g. a public-chat agent reusing the
|
|
172
172
|
// owner's Gmail authorization); otherwise they fall back to the
|
|
173
173
|
// caller's userId, which is the historical personal-agent path.
|
|
174
|
-
const
|
|
174
|
+
const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
|
|
175
|
+
const filter = params.overrides?.extraToolsFilter;
|
|
176
|
+
const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
175
177
|
const response = await this.runner.run(agent, messages, {
|
|
176
178
|
userId: params.userId,
|
|
177
179
|
conversationId: params.conversationId,
|
|
@@ -235,7 +237,9 @@ class AgentService {
|
|
|
235
237
|
// Same precedence as sendMessage — agent.connectorOwnerUserId takes
|
|
236
238
|
// priority so a public-chat agent always uses the owner's connector
|
|
237
239
|
// toolbelt regardless of which visitor session is streaming.
|
|
238
|
-
const
|
|
240
|
+
const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
|
|
241
|
+
const filter = params.overrides?.extraToolsFilter;
|
|
242
|
+
const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
239
243
|
for await (const chunk of this.runner.stream(agent, messages, {
|
|
240
244
|
userId: params.userId,
|
|
241
245
|
conversationId: params.conversationId,
|
|
@@ -36,6 +36,24 @@ export interface OAuth2ProviderConfig {
|
|
|
36
36
|
* instead of letting an empty string poison the encrypted-token row.
|
|
37
37
|
*/
|
|
38
38
|
tokenExtractor?: (json: unknown) => TokenSet;
|
|
39
|
+
/**
|
|
40
|
+
* Where to put the client credentials on the token exchange request.
|
|
41
|
+
*
|
|
42
|
+
* - `'body'` (default) — appends `client_id` + `client_secret` to the
|
|
43
|
+
* form-encoded body. Works for the majority of providers (Google,
|
|
44
|
+
* HubSpot, ClickUp, Slack, GitHub).
|
|
45
|
+
*
|
|
46
|
+
* - `'basic'` — sends them as `Authorization: Basic
|
|
47
|
+
* base64(client_id:client_secret)` instead. Required by Notion
|
|
48
|
+
* (their `/v1/oauth/token` rejects body credentials with
|
|
49
|
+
* `invalid_client`), Spotify, and a handful of other providers
|
|
50
|
+
* whose docs say "Basic Auth" explicitly.
|
|
51
|
+
*
|
|
52
|
+
* RFC 6749 §2.3.1 allows both forms but recommends Basic when both are
|
|
53
|
+
* supported. We default to body because it was the legacy behavior and
|
|
54
|
+
* every existing connector relies on it.
|
|
55
|
+
*/
|
|
56
|
+
tokenAuth?: 'body' | 'basic';
|
|
39
57
|
}
|
|
40
58
|
export interface AuthorizeUrlResult {
|
|
41
59
|
url: string;
|
|
@@ -40,44 +40,62 @@ class OAuth2Service {
|
|
|
40
40
|
grant_type: 'authorization_code',
|
|
41
41
|
code,
|
|
42
42
|
redirect_uri: redirectUri,
|
|
43
|
-
client_id: cfg.clientId,
|
|
44
|
-
client_secret: cfg.clientSecret,
|
|
45
43
|
});
|
|
44
|
+
// Body-auth providers carry credentials in the form payload; basic-auth
|
|
45
|
+
// providers (Notion, Spotify) carry them in the Authorization header
|
|
46
|
+
// and reject body credentials with `invalid_client`. See
|
|
47
|
+
// OAuth2ProviderConfig.tokenAuth for the per-provider toggle.
|
|
48
|
+
if ((cfg.tokenAuth ?? 'body') === 'body') {
|
|
49
|
+
body.set('client_id', cfg.clientId);
|
|
50
|
+
body.set('client_secret', cfg.clientSecret);
|
|
51
|
+
}
|
|
46
52
|
if (pkceVerifier)
|
|
47
53
|
body.set('code_verifier', pkceVerifier);
|
|
48
|
-
return this.postToken(cfg
|
|
54
|
+
return this.postToken(cfg, body);
|
|
49
55
|
}
|
|
50
56
|
async refresh(cfg, refreshToken) {
|
|
51
57
|
const body = new URLSearchParams({
|
|
52
58
|
grant_type: 'refresh_token',
|
|
53
59
|
refresh_token: refreshToken,
|
|
54
|
-
client_id: cfg.clientId,
|
|
55
|
-
client_secret: cfg.clientSecret,
|
|
56
60
|
});
|
|
57
|
-
|
|
61
|
+
if ((cfg.tokenAuth ?? 'body') === 'body') {
|
|
62
|
+
body.set('client_id', cfg.clientId);
|
|
63
|
+
body.set('client_secret', cfg.clientSecret);
|
|
64
|
+
}
|
|
65
|
+
return this.postToken(cfg, body);
|
|
58
66
|
}
|
|
59
|
-
async postToken(
|
|
60
|
-
const
|
|
67
|
+
async postToken(cfg, body) {
|
|
68
|
+
const headers = {
|
|
69
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
70
|
+
accept: 'application/json',
|
|
71
|
+
};
|
|
72
|
+
// Basic-auth providers need credentials in the Authorization header
|
|
73
|
+
// instead of the body. RFC 6749 §2.3.1 — both forms are spec-legal
|
|
74
|
+
// but providers vary on which they accept.
|
|
75
|
+
if (cfg.tokenAuth === 'basic') {
|
|
76
|
+
const credentials = Buffer.from(`${cfg.clientId}:${cfg.clientSecret}`).toString('base64');
|
|
77
|
+
headers.authorization = `Basic ${credentials}`;
|
|
78
|
+
}
|
|
79
|
+
const res = await this.fetchImpl(cfg.tokenUrl, {
|
|
61
80
|
method: 'POST',
|
|
62
|
-
headers
|
|
63
|
-
'content-type': 'application/x-www-form-urlencoded',
|
|
64
|
-
accept: 'application/json',
|
|
65
|
-
},
|
|
81
|
+
headers,
|
|
66
82
|
body: body.toString(),
|
|
67
83
|
});
|
|
68
84
|
if (!res.ok) {
|
|
69
85
|
const text = await res.text().catch(() => '');
|
|
70
|
-
throw new Error(`OAuth2 token endpoint ${tokenUrl} returned ${res.status}: ${text}`);
|
|
86
|
+
throw new Error(`OAuth2 token endpoint ${cfg.tokenUrl} returned ${res.status}: ${text}`);
|
|
71
87
|
}
|
|
72
88
|
const json = (await res.json());
|
|
73
|
-
const tokens =
|
|
89
|
+
const tokens = cfg.tokenExtractor
|
|
90
|
+
? cfg.tokenExtractor(json)
|
|
91
|
+
: defaultExtractor(json);
|
|
74
92
|
// Guard against accidentally persisting an empty-string token — that
|
|
75
93
|
// would crash the cipher with "Received undefined" downstream and the
|
|
76
94
|
// operator would chase a confusing stack trace. Better to fail fast
|
|
77
95
|
// here with a descriptive error so the connector author knows their
|
|
78
96
|
// extractor (or the default) missed the token.
|
|
79
97
|
if (!tokens.accessToken) {
|
|
80
|
-
throw new Error(`OAuth2 token endpoint ${tokenUrl} returned a response without an access token. ` +
|
|
98
|
+
throw new Error(`OAuth2 token endpoint ${cfg.tokenUrl} returned a response without an access token. ` +
|
|
81
99
|
'If the provider uses a non-standard envelope, supply `tokenExtractor` on the ConnectorDefinition.');
|
|
82
100
|
}
|
|
83
101
|
return tokens;
|
|
@@ -45,6 +45,28 @@ export interface AgentOverrides {
|
|
|
45
45
|
* routed through a per-call map (the registry doesn't see them).
|
|
46
46
|
*/
|
|
47
47
|
extraTools?: AgentToolDefinition[];
|
|
48
|
+
/**
|
|
49
|
+
* Optional post-resolution filter applied to the tools that
|
|
50
|
+
* `AgentService` derives from the connector registry (the
|
|
51
|
+
* `resolveExtraTools(userId)` output). The caller receives the full
|
|
52
|
+
* list and returns the subset that should actually be exposed to the
|
|
53
|
+
* model for this turn.
|
|
54
|
+
*
|
|
55
|
+
* Use cases:
|
|
56
|
+
* - The prompt-designer assistant exposes ONLY read-only tools to
|
|
57
|
+
* itself even though the operator's tenant has write tools
|
|
58
|
+
* authorized (see PROMPT_DESIGNER_TOOL_USE_SDD §4.2).
|
|
59
|
+
* - A throttling layer dropping tools that have hit a rate limit.
|
|
60
|
+
*
|
|
61
|
+
* If omitted, the resolved list passes through unchanged — the
|
|
62
|
+
* historical behavior.
|
|
63
|
+
*
|
|
64
|
+
* Note: this filter runs AFTER `resolveExtraTools` and BEFORE the
|
|
65
|
+
* shallow merge with `overrides.extraTools`. Tools explicitly
|
|
66
|
+
* provided via `extraTools` are NOT filtered; the caller is
|
|
67
|
+
* responsible for vetting those.
|
|
68
|
+
*/
|
|
69
|
+
extraToolsFilter?: (tools: AgentToolDefinition[]) => AgentToolDefinition[];
|
|
48
70
|
}
|
|
49
71
|
export interface AgentResponse {
|
|
50
72
|
messageId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"description": "Framework-free AI runtime SDK. Owns: agent loop (Anthropic), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules — not here.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|