@agentforge-io/core 2.0.10 → 2.0.14
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.
|
@@ -96,17 +96,27 @@ class AgentService {
|
|
|
96
96
|
* Returns the runtime `AgentDefinition` ready to feed the runner.
|
|
97
97
|
*/
|
|
98
98
|
async resolveAgent(params) {
|
|
99
|
+
// Public-chat slug lookup MUST gate on isActive — a deactivated
|
|
100
|
+
// agent shouldn't be reachable from a customer-facing widget.
|
|
99
101
|
if (params.agentSlug && params.tenantId && this.resolver) {
|
|
100
102
|
const record = await this.resolver.findBySlug(params.tenantId, params.agentSlug);
|
|
101
103
|
if (record && record.isActive)
|
|
102
104
|
return toAgentDefinition(record);
|
|
103
105
|
}
|
|
106
|
+
// Direct-id lookup does NOT gate on isActive. The caller has the
|
|
107
|
+
// opaque uuid because they already created a conversation against
|
|
108
|
+
// that agent (admin UI, system Agent Assist seed, internal job…).
|
|
109
|
+
// Refusing to stream the next turn just because someone toggled
|
|
110
|
+
// the agent inactive between turns surfaces as the opaque
|
|
111
|
+
// "Agent <uuid> not found" the admin saw in prod — actively
|
|
112
|
+
// hostile to operators. Leave inactive-filtering to the layer
|
|
113
|
+
// that handed out the agentId in the first place.
|
|
104
114
|
if (params.agentId) {
|
|
105
115
|
if (this.resolver) {
|
|
106
116
|
const record = await this.resolver
|
|
107
117
|
.findById(params.agentId)
|
|
108
118
|
.catch(() => null);
|
|
109
|
-
if (record
|
|
119
|
+
if (record)
|
|
110
120
|
return toAgentDefinition(record);
|
|
111
121
|
}
|
|
112
122
|
// Final fallback: hardcoded SDK array.
|
|
@@ -161,7 +171,9 @@ class AgentService {
|
|
|
161
171
|
// resolved agent declares one (e.g. a public-chat agent reusing the
|
|
162
172
|
// owner's Gmail authorization); otherwise they fall back to the
|
|
163
173
|
// caller's userId, which is the historical personal-agent path.
|
|
164
|
-
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;
|
|
165
177
|
const response = await this.runner.run(agent, messages, {
|
|
166
178
|
userId: params.userId,
|
|
167
179
|
conversationId: params.conversationId,
|
|
@@ -225,7 +237,9 @@ class AgentService {
|
|
|
225
237
|
// Same precedence as sendMessage — agent.connectorOwnerUserId takes
|
|
226
238
|
// priority so a public-chat agent always uses the owner's connector
|
|
227
239
|
// toolbelt regardless of which visitor session is streaming.
|
|
228
|
-
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;
|
|
229
243
|
for await (const chunk of this.runner.stream(agent, messages, {
|
|
230
244
|
userId: params.userId,
|
|
231
245
|
conversationId: params.conversationId,
|
|
@@ -19,6 +19,23 @@ export interface OAuth2ProviderConfig {
|
|
|
19
19
|
/** PKCE: defaults to true for security, but providers that don't support
|
|
20
20
|
* it (rare in 2026) can opt out. */
|
|
21
21
|
usePkce?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Optional extractor for non-standard token response shapes. The default
|
|
24
|
+
* reads `access_token` / `refresh_token` / etc. directly off the JSON
|
|
25
|
+
* body — RFC 6749 compliant providers (Google, HubSpot, ClickUp) don't
|
|
26
|
+
* need this.
|
|
27
|
+
*
|
|
28
|
+
* Slack's `oauth.v2.access` is the notable exception: when the consent
|
|
29
|
+
* grants only user scopes, the user token lands at
|
|
30
|
+
* `json.authed_user.access_token`, not at the root. Connectors with
|
|
31
|
+
* non-standard envelopes provide this hook to map their response into
|
|
32
|
+
* the canonical `TokenSet` the registry persists.
|
|
33
|
+
*
|
|
34
|
+
* If the extractor returns `accessToken: ''` (or any falsy string), the
|
|
35
|
+
* service treats the exchange as failed and surfaces a clear error
|
|
36
|
+
* instead of letting an empty string poison the encrypted-token row.
|
|
37
|
+
*/
|
|
38
|
+
tokenExtractor?: (json: unknown) => TokenSet;
|
|
22
39
|
}
|
|
23
40
|
export interface AuthorizeUrlResult {
|
|
24
41
|
url: string;
|
|
@@ -45,7 +45,7 @@ class OAuth2Service {
|
|
|
45
45
|
});
|
|
46
46
|
if (pkceVerifier)
|
|
47
47
|
body.set('code_verifier', pkceVerifier);
|
|
48
|
-
return this.postToken(cfg.tokenUrl, body);
|
|
48
|
+
return this.postToken(cfg.tokenUrl, body, cfg.tokenExtractor);
|
|
49
49
|
}
|
|
50
50
|
async refresh(cfg, refreshToken) {
|
|
51
51
|
const body = new URLSearchParams({
|
|
@@ -54,9 +54,9 @@ class OAuth2Service {
|
|
|
54
54
|
client_id: cfg.clientId,
|
|
55
55
|
client_secret: cfg.clientSecret,
|
|
56
56
|
});
|
|
57
|
-
return this.postToken(cfg.tokenUrl, body);
|
|
57
|
+
return this.postToken(cfg.tokenUrl, body, cfg.tokenExtractor);
|
|
58
58
|
}
|
|
59
|
-
async postToken(tokenUrl, body) {
|
|
59
|
+
async postToken(tokenUrl, body, extractor) {
|
|
60
60
|
const res = await this.fetchImpl(tokenUrl, {
|
|
61
61
|
method: 'POST',
|
|
62
62
|
headers: {
|
|
@@ -70,13 +70,30 @@ class OAuth2Service {
|
|
|
70
70
|
throw new Error(`OAuth2 token endpoint ${tokenUrl} returned ${res.status}: ${text}`);
|
|
71
71
|
}
|
|
72
72
|
const json = (await res.json());
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const tokens = extractor ? extractor(json) : defaultExtractor(json);
|
|
74
|
+
// Guard against accidentally persisting an empty-string token — that
|
|
75
|
+
// would crash the cipher with "Received undefined" downstream and the
|
|
76
|
+
// operator would chase a confusing stack trace. Better to fail fast
|
|
77
|
+
// here with a descriptive error so the connector author knows their
|
|
78
|
+
// extractor (or the default) missed the token.
|
|
79
|
+
if (!tokens.accessToken) {
|
|
80
|
+
throw new Error(`OAuth2 token endpoint ${tokenUrl} returned a response without an access token. ` +
|
|
81
|
+
'If the provider uses a non-standard envelope, supply `tokenExtractor` on the ConnectorDefinition.');
|
|
82
|
+
}
|
|
83
|
+
return tokens;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
exports.OAuth2Service = OAuth2Service;
|
|
87
|
+
/** RFC 6749 standard shape — used when the connector doesn't supply a
|
|
88
|
+
* custom extractor. Mirrors the original behavior before the
|
|
89
|
+
* `tokenExtractor` hook was added. */
|
|
90
|
+
function defaultExtractor(json) {
|
|
91
|
+
const j = json;
|
|
92
|
+
return {
|
|
93
|
+
accessToken: j.access_token ?? '',
|
|
94
|
+
refreshToken: j.refresh_token,
|
|
95
|
+
expiresIn: j.expires_in,
|
|
96
|
+
scope: j.scope,
|
|
97
|
+
tokenType: j.token_type,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -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.14",
|
|
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",
|