@vellumai/assistant 0.4.45 → 0.4.48
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/ARCHITECTURE.md +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- package/src/media/managed-avatar-client.ts +0 -225
|
@@ -23,19 +23,30 @@ import {
|
|
|
23
23
|
generateAllowlistOptions,
|
|
24
24
|
generateScopeOptions,
|
|
25
25
|
} from "../../permissions/checker.js";
|
|
26
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
26
27
|
import { getSecureKey } from "../../security/secure-keys.js";
|
|
27
28
|
import { parseToolManifestFile } from "../../skills/tool-manifest.js";
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
assertMetadataWritable,
|
|
31
|
+
getCredentialMetadata,
|
|
32
|
+
} from "../../tools/credentials/metadata-store.js";
|
|
29
33
|
import {
|
|
30
34
|
type ManifestOverride,
|
|
31
35
|
resolveExecutionTarget,
|
|
32
36
|
} from "../../tools/execution-target.js";
|
|
33
37
|
import { getAllTools, getTool } from "../../tools/registry.js";
|
|
38
|
+
import {
|
|
39
|
+
injectReasonField,
|
|
40
|
+
REASON_SKIP_SET,
|
|
41
|
+
} from "../../tools/schema-transforms.js";
|
|
34
42
|
import { isSideEffectTool } from "../../tools/side-effects.js";
|
|
35
43
|
import { setAvatarTool } from "../../tools/system/avatar-generator.js";
|
|
36
44
|
import { pathExists } from "../../util/fs.js";
|
|
37
45
|
import { getLogger } from "../../util/logger.js";
|
|
38
46
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
47
|
+
import { buildAssistantEvent } from "../assistant-event.js";
|
|
48
|
+
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
49
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
39
50
|
import { httpError } from "../http-errors.js";
|
|
40
51
|
import type { RouteDefinition } from "../http-router.js";
|
|
41
52
|
import { resolveWorkspacePath } from "./workspace-utils.js";
|
|
@@ -134,9 +145,9 @@ function getClientSecret(
|
|
|
134
145
|
rawService: string,
|
|
135
146
|
): string | undefined {
|
|
136
147
|
return (
|
|
137
|
-
getSecureKey(
|
|
148
|
+
getSecureKey(credentialKey(resolvedService, "client_secret")) ??
|
|
138
149
|
(resolvedService !== rawService
|
|
139
|
-
? getSecureKey(
|
|
150
|
+
? getSecureKey(credentialKey(rawService, "client_secret"))
|
|
140
151
|
: undefined) ??
|
|
141
152
|
undefined
|
|
142
153
|
);
|
|
@@ -162,9 +173,16 @@ async function handleOAuthConnectStart(body: {
|
|
|
162
173
|
|
|
163
174
|
const resolvedService = resolveService(body.service);
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
// client_id is stored in metadata (oauth2ClientId), not the secure store.
|
|
177
|
+
let clientId = getCredentialMetadata(
|
|
178
|
+
resolvedService,
|
|
179
|
+
"access_token",
|
|
180
|
+
)?.oauth2ClientId;
|
|
166
181
|
if (!clientId && resolvedService !== body.service) {
|
|
167
|
-
clientId =
|
|
182
|
+
clientId = getCredentialMetadata(
|
|
183
|
+
body.service,
|
|
184
|
+
"access_token",
|
|
185
|
+
)?.oauth2ClientId;
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
if (!clientId) {
|
|
@@ -203,6 +221,36 @@ async function handleOAuthConnectStart(body: {
|
|
|
203
221
|
openUrl: (url: string) => {
|
|
204
222
|
authUrl = url;
|
|
205
223
|
},
|
|
224
|
+
onDeferredComplete: (deferredResult) => {
|
|
225
|
+
// Emit oauth_connect_result to all connected SSE clients so the
|
|
226
|
+
// UI can update immediately when the deferred browser flow completes.
|
|
227
|
+
assistantEventHub
|
|
228
|
+
.publish(
|
|
229
|
+
buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, {
|
|
230
|
+
type: "oauth_connect_result",
|
|
231
|
+
success: deferredResult.success,
|
|
232
|
+
service: deferredResult.service,
|
|
233
|
+
accountInfo: deferredResult.accountInfo,
|
|
234
|
+
error: deferredResult.error,
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
.catch((err) => {
|
|
238
|
+
log.warn(
|
|
239
|
+
{ err, service: deferredResult.service },
|
|
240
|
+
"Failed to publish oauth_connect_result event",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!deferredResult.success) {
|
|
245
|
+
log.warn(
|
|
246
|
+
{
|
|
247
|
+
service: deferredResult.service,
|
|
248
|
+
err: deferredResult.error,
|
|
249
|
+
},
|
|
250
|
+
"Deferred OAuth connect failed",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
206
254
|
});
|
|
207
255
|
|
|
208
256
|
if (!result.success) {
|
|
@@ -309,23 +357,34 @@ function resolveManifestOverride(
|
|
|
309
357
|
function handleToolNamesList(): Response {
|
|
310
358
|
const tools = getAllTools();
|
|
311
359
|
const nameSet = new Set(tools.map((t) => t.name));
|
|
312
|
-
|
|
313
|
-
string
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
360
|
+
type SchemaShape = {
|
|
361
|
+
type: string;
|
|
362
|
+
properties?: Record<string, unknown>;
|
|
363
|
+
required?: string[];
|
|
364
|
+
};
|
|
365
|
+
const schemas: Record<string, SchemaShape> = {};
|
|
366
|
+
|
|
367
|
+
// Collect raw definitions from the registry so we can transform them.
|
|
368
|
+
const rawDefs: import("../../providers/types.js").ToolDefinition[] = [];
|
|
320
369
|
for (const tool of tools) {
|
|
321
370
|
try {
|
|
322
|
-
|
|
323
|
-
schemas[tool.name] = def.input_schema as (typeof schemas)[string];
|
|
371
|
+
rawDefs.push(tool.getDefinition());
|
|
324
372
|
} catch {
|
|
325
373
|
// Skip tools whose definitions can't be resolved
|
|
326
374
|
}
|
|
327
375
|
}
|
|
328
376
|
|
|
377
|
+
// Apply reason injection so settings/debug schemas match runtime behavior.
|
|
378
|
+
const transformedDefs = injectReasonField(rawDefs, REASON_SKIP_SET);
|
|
379
|
+
for (const def of transformedDefs) {
|
|
380
|
+
schemas[def.name] = def.input_schema as SchemaShape;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Skill manifest schemas are served raw (untransformed). Unlike runtime tool
|
|
384
|
+
// schemas which have `reason` injected via injectReasonField(), skill manifests
|
|
385
|
+
// reflect the original TOOLS.json content. This is intentional: skill tools are
|
|
386
|
+
// invoked through skill_execute (which has its own reason field), so their
|
|
387
|
+
// individual schemas are never sent to the LLM directly.
|
|
329
388
|
try {
|
|
330
389
|
const catalog = loadSkillCatalog();
|
|
331
390
|
for (const skill of catalog) {
|
|
@@ -337,8 +396,7 @@ function handleToolNamesList(): Response {
|
|
|
337
396
|
for (const entry of manifest.tools) {
|
|
338
397
|
if (nameSet.has(entry.name)) continue;
|
|
339
398
|
nameSet.add(entry.name);
|
|
340
|
-
schemas[entry.name] =
|
|
341
|
-
entry.input_schema as unknown as (typeof schemas)[string];
|
|
399
|
+
schemas[entry.name] = entry.input_schema as unknown as SchemaShape;
|
|
342
400
|
}
|
|
343
401
|
} catch {
|
|
344
402
|
// Skip skills whose manifests can't be parsed
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import type { ApprovalUIMetadata } from "./channel-approval-types.js";
|
|
3
4
|
import type { ChannelDeliveryResult } from "./gateway-client.js";
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -494,6 +495,137 @@ describe("TelegramStreamingDelivery", () => {
|
|
|
494
495
|
expect(delivery.finishSucceeded).toBe(true);
|
|
495
496
|
});
|
|
496
497
|
|
|
498
|
+
// ── Test 5j: no-messageId + tool_use_start + finish delivers post-tool text ─
|
|
499
|
+
test("no-messageId + tool_use_start + finish delivers post-tool text", async () => {
|
|
500
|
+
// Scenario from Devin review: initial send succeeds without messageId,
|
|
501
|
+
// more deltas arrive, tool_use_start fires, finish() must deliver post-tool text.
|
|
502
|
+
mockDeliverChannelReply.mockReset();
|
|
503
|
+
let localCallCount = 0;
|
|
504
|
+
mockDeliverChannelReply.mockImplementation(
|
|
505
|
+
async (): Promise<ChannelDeliveryResult> => {
|
|
506
|
+
localCallCount++;
|
|
507
|
+
if (localCallCount === 1) return { ok: true }; // no messageId
|
|
508
|
+
return { ok: true, messageId: 400 };
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const delivery = createDelivery();
|
|
513
|
+
|
|
514
|
+
// Step 1: initial send (>= 20 chars), succeeds without messageId
|
|
515
|
+
delivery.onEvent({
|
|
516
|
+
type: "assistant_text_delta",
|
|
517
|
+
text: "a".repeat(25),
|
|
518
|
+
});
|
|
519
|
+
await flushPromises();
|
|
520
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(1);
|
|
521
|
+
|
|
522
|
+
// Step 2: more deltas arrive — stuck in buffer (onTextDelta skips both branches)
|
|
523
|
+
delivery.onEvent({
|
|
524
|
+
type: "assistant_text_delta",
|
|
525
|
+
text: "post-tool text",
|
|
526
|
+
});
|
|
527
|
+
await flushPromises();
|
|
528
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(1); // no new call
|
|
529
|
+
|
|
530
|
+
// Step 3: tool_use_start — buffer should NOT be moved to currentMessageText
|
|
531
|
+
delivery.onEvent({
|
|
532
|
+
type: "tool_use_start",
|
|
533
|
+
toolName: "some_tool",
|
|
534
|
+
input: {},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Step 4: finish() — should deliver the post-tool text as a new message
|
|
538
|
+
await delivery.finish();
|
|
539
|
+
|
|
540
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(2);
|
|
541
|
+
const secondPayload = callPayload(1);
|
|
542
|
+
expect(secondPayload.text).toBe("post-tool text");
|
|
543
|
+
expect(secondPayload.messageId).toBeUndefined(); // new message, not edit
|
|
544
|
+
expect(delivery.finishSucceeded).toBe(true);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// ── Test 5k: no-messageId + finish with approval sends approval as new message ─
|
|
548
|
+
test("no-messageId + finish with approval sends approval as new message", async () => {
|
|
549
|
+
// Scenario from Codex review: initial send succeeds without messageId,
|
|
550
|
+
// no additional buffer, but finish(approval) must still deliver approval buttons.
|
|
551
|
+
mockDeliverChannelReply.mockReset();
|
|
552
|
+
mockDeliverChannelReply.mockImplementation(
|
|
553
|
+
async (): Promise<ChannelDeliveryResult> => {
|
|
554
|
+
return { ok: true }; // no messageId
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const delivery = createDelivery();
|
|
559
|
+
|
|
560
|
+
// Initial send succeeds without messageId
|
|
561
|
+
delivery.onEvent({
|
|
562
|
+
type: "assistant_text_delta",
|
|
563
|
+
text: "a".repeat(25),
|
|
564
|
+
});
|
|
565
|
+
await flushPromises();
|
|
566
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(1);
|
|
567
|
+
|
|
568
|
+
// finish() with approval — approval must not be silently dropped
|
|
569
|
+
const approval: ApprovalUIMetadata = {
|
|
570
|
+
requestId: "test-req",
|
|
571
|
+
actions: [{ id: "approve_once", label: "Approve" }],
|
|
572
|
+
plainTextFallback: "Reply APPROVE or REJECT",
|
|
573
|
+
};
|
|
574
|
+
await delivery.finish(approval);
|
|
575
|
+
|
|
576
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(2);
|
|
577
|
+
const secondPayload = callPayload(1);
|
|
578
|
+
// Approval buttons sent as a new message
|
|
579
|
+
expect(secondPayload.approval).toEqual(approval);
|
|
580
|
+
expect(secondPayload.messageId).toBeUndefined();
|
|
581
|
+
expect(delivery.finishSucceeded).toBe(true);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ── Test 5l: no-messageId + buffer + finish with approval delivers both ─
|
|
585
|
+
test("no-messageId + buffer + finish with approval delivers both text and approval", async () => {
|
|
586
|
+
// Combined scenario: no-messageId initial send, buffered text, and approval buttons.
|
|
587
|
+
mockDeliverChannelReply.mockReset();
|
|
588
|
+
let localCallCount = 0;
|
|
589
|
+
mockDeliverChannelReply.mockImplementation(
|
|
590
|
+
async (): Promise<ChannelDeliveryResult> => {
|
|
591
|
+
localCallCount++;
|
|
592
|
+
if (localCallCount === 1) return { ok: true }; // no messageId
|
|
593
|
+
return { ok: true, messageId: 500 };
|
|
594
|
+
},
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const delivery = createDelivery();
|
|
598
|
+
|
|
599
|
+
// Initial send succeeds without messageId
|
|
600
|
+
delivery.onEvent({
|
|
601
|
+
type: "assistant_text_delta",
|
|
602
|
+
text: "a".repeat(25),
|
|
603
|
+
});
|
|
604
|
+
await flushPromises();
|
|
605
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(1);
|
|
606
|
+
|
|
607
|
+
// More deltas arrive
|
|
608
|
+
delivery.onEvent({
|
|
609
|
+
type: "assistant_text_delta",
|
|
610
|
+
text: "remainder",
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// finish() with approval — should deliver buffer text + approval together
|
|
614
|
+
const approval: ApprovalUIMetadata = {
|
|
615
|
+
requestId: "test-req",
|
|
616
|
+
actions: [{ id: "approve_once", label: "Approve" }],
|
|
617
|
+
plainTextFallback: "Reply APPROVE or REJECT",
|
|
618
|
+
};
|
|
619
|
+
await delivery.finish(approval);
|
|
620
|
+
|
|
621
|
+
expect(mockDeliverChannelReply).toHaveBeenCalledTimes(2);
|
|
622
|
+
const secondPayload = callPayload(1);
|
|
623
|
+
expect(secondPayload.text).toBe("remainder");
|
|
624
|
+
expect(secondPayload.approval).toEqual(approval);
|
|
625
|
+
expect(secondPayload.messageId).toBeUndefined();
|
|
626
|
+
expect(delivery.finishSucceeded).toBe(true);
|
|
627
|
+
});
|
|
628
|
+
|
|
497
629
|
// ── Test 6: skips final edit when text hasn't changed ───────────────
|
|
498
630
|
test("skips final edit when text hasn't changed", async () => {
|
|
499
631
|
const delivery = createDelivery();
|
|
@@ -51,11 +51,13 @@ export class TelegramStreamingDelivery {
|
|
|
51
51
|
// Flush buffer and send an edit so the message is up-to-date before the tool runs
|
|
52
52
|
if (this.buffer.length > 0 && this.currentMessageId) {
|
|
53
53
|
this.flushEdit();
|
|
54
|
-
} else if (this.buffer.length > 0) {
|
|
54
|
+
} else if (this.buffer.length > 0 && !this.textDelivered) {
|
|
55
55
|
// No message sent yet — just move buffer to currentMessageText
|
|
56
56
|
this.currentMessageText += this.buffer;
|
|
57
57
|
this.buffer = "";
|
|
58
58
|
}
|
|
59
|
+
// When textDelivered is true but currentMessageId is null (no-messageId
|
|
60
|
+
// response), leave buffer as-is so finish() sends it as a new message.
|
|
59
61
|
break;
|
|
60
62
|
case "message_complete":
|
|
61
63
|
// Don't finalize here — let finish() handle it
|
|
@@ -121,6 +123,14 @@ export class TelegramStreamingDelivery {
|
|
|
121
123
|
return;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// Text was delivered but no messageId was returned, and there are approval
|
|
127
|
+
// buttons to attach. Send them as a new message so they aren't silently dropped.
|
|
128
|
+
if (!this.currentMessageId && this.textDelivered && approval) {
|
|
129
|
+
await this.sendNewMessage("", approval);
|
|
130
|
+
this.finishOk = true;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
// Final edit with approval buttons if present.
|
|
125
135
|
// Skip the edit when text hasn't changed since the last successful
|
|
126
136
|
// delivery and there are no approval buttons to attach — sending the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasTwilioCredentials } from "../calls/twilio-rest.js";
|
|
2
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
2
3
|
import { getSecureKey } from "../security/secure-keys.js";
|
|
3
4
|
|
|
4
5
|
interface IntegrationProbe {
|
|
@@ -13,13 +14,13 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
13
14
|
name: "Gmail",
|
|
14
15
|
category: "email",
|
|
15
16
|
isConnected: () =>
|
|
16
|
-
!!getSecureKey("
|
|
17
|
+
!!getSecureKey(credentialKey("integration:gmail", "access_token")),
|
|
17
18
|
},
|
|
18
19
|
{
|
|
19
20
|
name: "Slack",
|
|
20
21
|
category: "messaging",
|
|
21
22
|
isConnected: () =>
|
|
22
|
-
!!getSecureKey("
|
|
23
|
+
!!getSecureKey(credentialKey("integration:slack", "access_token")),
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
26
|
name: "Twilio",
|
|
@@ -30,8 +31,8 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
30
31
|
name: "Telegram",
|
|
31
32
|
category: "messaging",
|
|
32
33
|
isConnected: () =>
|
|
33
|
-
!!getSecureKey("
|
|
34
|
-
!!getSecureKey("
|
|
34
|
+
!!getSecureKey(credentialKey("telegram", "bot_token")) &&
|
|
35
|
+
!!getSecureKey(credentialKey("telegram", "webhook_secret")),
|
|
35
36
|
},
|
|
36
37
|
];
|
|
37
38
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for credential key format in the secure store.
|
|
3
|
+
*
|
|
4
|
+
* Keys follow the pattern: credential/{service}/{field}
|
|
5
|
+
*
|
|
6
|
+
* Previously, keys used colons as delimiters (credential:service:field),
|
|
7
|
+
* which was ambiguous when service names contained colons (e.g.
|
|
8
|
+
* "integration:gmail"). The slash-delimited format avoids this.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { listCredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
12
|
+
import { getLogger } from "../util/logger.js";
|
|
13
|
+
import {
|
|
14
|
+
deleteSecureKey,
|
|
15
|
+
getSecureKey,
|
|
16
|
+
listSecureKeys,
|
|
17
|
+
setSecureKey,
|
|
18
|
+
} from "./secure-keys.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("credential-key");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a credential key for the secure store.
|
|
24
|
+
*
|
|
25
|
+
* @returns A key of the form `credential/{service}/{field}`
|
|
26
|
+
*/
|
|
27
|
+
export function credentialKey(service: string, field: string): string {
|
|
28
|
+
return `credential/${service}/${field}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Migration from colon-delimited keys
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
let migrated = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Migrate any legacy colon-delimited credential keys to the new
|
|
39
|
+
* slash-delimited format. Idempotent: skips keys that already exist
|
|
40
|
+
* under the new format, and only runs once per process (guarded by a
|
|
41
|
+
* module-level flag).
|
|
42
|
+
*
|
|
43
|
+
* Legacy key format: `credential:<service>:<field>`
|
|
44
|
+
* New key format: `credential/<service>/<field>`
|
|
45
|
+
*
|
|
46
|
+
* The old colon-delimited format is ambiguous when either the service
|
|
47
|
+
* or field name contains colons — for `credential:A:B:C:D`, you can't
|
|
48
|
+
* tell where the service ends and the field begins without external
|
|
49
|
+
* context.
|
|
50
|
+
*
|
|
51
|
+
* To resolve this, the function first consults the credential metadata
|
|
52
|
+
* store to find which (service, field) pair matches a valid split.
|
|
53
|
+
* If no metadata match is found, it falls back to splitting on the
|
|
54
|
+
* **first** colon after the prefix — this handles the common case
|
|
55
|
+
* where service names are simple (e.g. "doordash.com") and field
|
|
56
|
+
* names may contain colons (e.g. "session:cookies").
|
|
57
|
+
*/
|
|
58
|
+
export function migrateKeys(): void {
|
|
59
|
+
if (migrated) return;
|
|
60
|
+
migrated = true;
|
|
61
|
+
|
|
62
|
+
let allKeys: string[];
|
|
63
|
+
try {
|
|
64
|
+
allKeys = listSecureKeys();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
log.warn({ err }, "Failed to list secure keys during migration");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const colonKeys = allKeys.filter(
|
|
71
|
+
(k) => k.startsWith("credential:") && !k.startsWith("credential/"),
|
|
72
|
+
);
|
|
73
|
+
if (colonKeys.length === 0) return;
|
|
74
|
+
|
|
75
|
+
log.info(
|
|
76
|
+
{ count: colonKeys.length },
|
|
77
|
+
"Migrating colon-delimited credential keys to slash-delimited format",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Build a set of known (service, field) pairs from credential metadata
|
|
81
|
+
// to disambiguate colon-delimited keys.
|
|
82
|
+
const knownPairs = new Set<string>();
|
|
83
|
+
try {
|
|
84
|
+
for (const meta of listCredentialMetadata()) {
|
|
85
|
+
knownPairs.add(`${meta.service}\0${meta.field}`);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// If metadata is unavailable, we'll rely on the first-colon fallback.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const oldKey of colonKeys) {
|
|
92
|
+
// Strip the "credential:" prefix — `rest` is "service:field" with
|
|
93
|
+
// potential colons in either part.
|
|
94
|
+
const rest = oldKey.slice("credential:".length);
|
|
95
|
+
|
|
96
|
+
const parsed = parseServiceField(rest, knownPairs);
|
|
97
|
+
if (parsed === undefined) {
|
|
98
|
+
log.warn({ key: oldKey }, "Skipping malformed credential key");
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { service, field } = parsed;
|
|
103
|
+
const newKey = credentialKey(service, field);
|
|
104
|
+
|
|
105
|
+
// Skip if the new key already exists (idempotent)
|
|
106
|
+
if (getSecureKey(newKey) !== undefined) {
|
|
107
|
+
// Clean up old key
|
|
108
|
+
deleteSecureKey(oldKey);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const value = getSecureKey(oldKey);
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ok = setSecureKey(newKey, value);
|
|
118
|
+
if (ok) {
|
|
119
|
+
deleteSecureKey(oldKey);
|
|
120
|
+
} else {
|
|
121
|
+
log.warn(
|
|
122
|
+
{ oldKey, newKey },
|
|
123
|
+
"Failed to write migrated key; keeping old key",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse a "service:field" string, using known metadata pairs to
|
|
131
|
+
* disambiguate when colons appear in either part.
|
|
132
|
+
*
|
|
133
|
+
* Strategy:
|
|
134
|
+
* 1. Try every possible split position and check against metadata.
|
|
135
|
+
* 2. If no metadata match, fall back to splitting on the first colon
|
|
136
|
+
* (field names with colons are more common than service names with colons).
|
|
137
|
+
*
|
|
138
|
+
* Returns undefined for malformed keys that have no colon.
|
|
139
|
+
*/
|
|
140
|
+
function parseServiceField(
|
|
141
|
+
rest: string,
|
|
142
|
+
knownPairs: Set<string>,
|
|
143
|
+
): { service: string; field: string } | undefined {
|
|
144
|
+
const firstColon = rest.indexOf(":");
|
|
145
|
+
if (firstColon <= 0) return undefined;
|
|
146
|
+
|
|
147
|
+
// Try each possible split position against metadata
|
|
148
|
+
if (knownPairs.size > 0) {
|
|
149
|
+
for (let i = firstColon; i < rest.length; i++) {
|
|
150
|
+
if (rest[i] !== ":") continue;
|
|
151
|
+
const service = rest.slice(0, i);
|
|
152
|
+
const field = rest.slice(i + 1);
|
|
153
|
+
if (field.length > 0 && knownPairs.has(`${service}\0${field}`)) {
|
|
154
|
+
return { service, field };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fallback: split on first colon — handles simple services with
|
|
160
|
+
// compound field names (e.g. "doordash.com:session:cookies").
|
|
161
|
+
return {
|
|
162
|
+
service: rest.slice(0, firstColon),
|
|
163
|
+
field: rest.slice(firstColon + 1),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @internal Test-only: reset the migration guard so migrateKeys() runs again. */
|
|
168
|
+
export function _resetMigrationFlag(): void {
|
|
169
|
+
migrated = false;
|
|
170
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
upsertCredentialMetadata,
|
|
12
12
|
} from "../tools/credentials/metadata-store.js";
|
|
13
13
|
import { getLogger } from "../util/logger.js";
|
|
14
|
+
import { credentialKey, migrateKeys } from "./credential-key.js";
|
|
14
15
|
import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
|
|
15
16
|
import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
|
|
16
17
|
|
|
@@ -106,11 +107,34 @@ function recordRefreshFailure(service: string): void {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
// ── Per-service refresh deduplication ─────────────────────────────────
|
|
111
|
+
// When multiple concurrent `withValidToken` calls detect an expired or
|
|
112
|
+
// 401-rejected token for the same service, only one actual refresh
|
|
113
|
+
// attempt is made. Other callers join the in-flight promise.
|
|
114
|
+
|
|
115
|
+
const inflightRefreshes = new Map<string, Promise<string>>();
|
|
116
|
+
|
|
117
|
+
function deduplicatedRefresh(service: string): Promise<string> {
|
|
118
|
+
const existing = inflightRefreshes.get(service);
|
|
119
|
+
if (existing) return existing;
|
|
120
|
+
|
|
121
|
+
const promise = doRefresh(service).finally(() => {
|
|
122
|
+
inflightRefreshes.delete(service);
|
|
123
|
+
});
|
|
124
|
+
inflightRefreshes.set(service, promise);
|
|
125
|
+
return promise;
|
|
126
|
+
}
|
|
127
|
+
|
|
109
128
|
/** @internal Test-only: reset all circuit breaker state */
|
|
110
129
|
export function _resetRefreshBreakers(): void {
|
|
111
130
|
refreshBreakers.clear();
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
/** @internal Test-only: reset in-flight refresh deduplication state */
|
|
134
|
+
export function _resetInflightRefreshes(): void {
|
|
135
|
+
inflightRefreshes.clear();
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
/** @internal Test-only: get breaker state for a service */
|
|
115
139
|
export function _getRefreshBreakerState(
|
|
116
140
|
service: string,
|
|
@@ -148,7 +172,7 @@ function isTokenExpired(service: string): boolean {
|
|
|
148
172
|
* Throws `TokenExpiredError` if refresh is not possible.
|
|
149
173
|
*/
|
|
150
174
|
async function doRefresh(service: string): Promise<string> {
|
|
151
|
-
const refreshToken = getSecureKey(
|
|
175
|
+
const refreshToken = getSecureKey(credentialKey(service, "refresh_token"));
|
|
152
176
|
if (!refreshToken) {
|
|
153
177
|
throw new TokenExpiredError(
|
|
154
178
|
service,
|
|
@@ -175,7 +199,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
175
199
|
);
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
const clientSecret =
|
|
202
|
+
const clientSecret = getSecureKey(credentialKey(service, "client_secret"));
|
|
179
203
|
const authMethod = meta?.oauth2TokenEndpointAuthMethod as
|
|
180
204
|
| TokenEndpointAuthMethod
|
|
181
205
|
| undefined;
|
|
@@ -219,7 +243,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
219
243
|
|
|
220
244
|
if (
|
|
221
245
|
!(await setSecureKeyAsync(
|
|
222
|
-
|
|
246
|
+
credentialKey(service, "access_token"),
|
|
223
247
|
result.accessToken,
|
|
224
248
|
))
|
|
225
249
|
) {
|
|
@@ -232,7 +256,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
232
256
|
if (result.refreshToken) {
|
|
233
257
|
if (
|
|
234
258
|
!(await setSecureKeyAsync(
|
|
235
|
-
|
|
259
|
+
credentialKey(service, "refresh_token"),
|
|
236
260
|
result.refreshToken,
|
|
237
261
|
))
|
|
238
262
|
) {
|
|
@@ -265,12 +289,17 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
265
289
|
* 1. Retrieves the stored access token (throws if none exists).
|
|
266
290
|
* 2. If the token is expired or near-expiry, refreshes it before calling the callback.
|
|
267
291
|
* 3. If the callback throws with a 401 status, attempts one refresh-and-retry cycle.
|
|
292
|
+
*
|
|
293
|
+
* @deprecated Use `resolveOAuthConnection(service).request()` instead.
|
|
294
|
+
* Retained only for BYO connection internals.
|
|
268
295
|
*/
|
|
269
296
|
export async function withValidToken<T>(
|
|
270
297
|
service: string,
|
|
271
298
|
callback: (token: string) => Promise<T>,
|
|
272
299
|
): Promise<T> {
|
|
273
|
-
|
|
300
|
+
migrateKeys();
|
|
301
|
+
|
|
302
|
+
let token = getSecureKey(credentialKey(service, "access_token"));
|
|
274
303
|
if (!token) {
|
|
275
304
|
throw new TokenExpiredError(
|
|
276
305
|
service,
|
|
@@ -280,14 +309,14 @@ export async function withValidToken<T>(
|
|
|
280
309
|
|
|
281
310
|
// Proactively refresh if expired or about to expire.
|
|
282
311
|
if (isTokenExpired(service)) {
|
|
283
|
-
token = await
|
|
312
|
+
token = await deduplicatedRefresh(service);
|
|
284
313
|
}
|
|
285
314
|
|
|
286
315
|
try {
|
|
287
316
|
return await callback(token);
|
|
288
317
|
} catch (err: unknown) {
|
|
289
318
|
if (is401Error(err)) {
|
|
290
|
-
token = await
|
|
319
|
+
token = await deduplicatedRefresh(service);
|
|
291
320
|
return callback(token);
|
|
292
321
|
}
|
|
293
322
|
throw err;
|
|
@@ -51,11 +51,6 @@ const appOpenTool: Tool = {
|
|
|
51
51
|
description:
|
|
52
52
|
"Display mode. 'preview' shows an inline preview card in chat. 'workspace' opens the full app in a workspace panel. Defaults to 'workspace'.",
|
|
53
53
|
},
|
|
54
|
-
reason: {
|
|
55
|
-
type: "string",
|
|
56
|
-
description:
|
|
57
|
-
"Brief non-technical explanation of what you are opening and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
|
|
58
|
-
},
|
|
59
54
|
},
|
|
60
55
|
required: ["app_id"],
|
|
61
56
|
},
|
|
@@ -95,11 +95,6 @@ const definition: ToolDefinition = {
|
|
|
95
95
|
description:
|
|
96
96
|
"Path where the file should be written, relative to (or inside) the sandbox working directory.",
|
|
97
97
|
},
|
|
98
|
-
reason: {
|
|
99
|
-
type: "string",
|
|
100
|
-
description:
|
|
101
|
-
"Brief non-technical explanation of what you are saving and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
|
|
102
|
-
},
|
|
103
98
|
},
|
|
104
99
|
required: ["attachment_id", "destination_path"],
|
|
105
100
|
},
|
|
@@ -298,11 +298,6 @@ const definition: ToolDefinition = {
|
|
|
298
298
|
type: "number",
|
|
299
299
|
description: `Maximum results to return (default ${DEFAULT_LIMIT}, max ${MAX_RESULTS}).`,
|
|
300
300
|
},
|
|
301
|
-
reason: {
|
|
302
|
-
type: "string",
|
|
303
|
-
description:
|
|
304
|
-
"Brief non-technical explanation of what you are searching for and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
|
|
305
|
-
},
|
|
306
301
|
},
|
|
307
302
|
required: [],
|
|
308
303
|
},
|