@vellumai/assistant 0.4.48 → 0.4.49
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 +2 -2
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +15 -6
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-metadata-store.test.ts +64 -73
- package/src/__tests__/credential-security-invariants.test.ts +13 -7
- package/src/__tests__/credential-vault-unit.test.ts +280 -49
- package/src/__tests__/credential-vault.test.ts +138 -16
- package/src/__tests__/credentials-cli.test.ts +71 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +32 -51
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +1 -1
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-channel-config.test.ts +67 -3
- package/src/__tests__/slack-share-routes.test.ts +17 -19
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -16
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +1 -6
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +2 -7
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +18 -12
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +101 -15
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/loader.ts +0 -6
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +49 -46
- package/src/daemon/handlers/config-telegram.ts +32 -16
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +36 -68
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +2 -9
- package/src/daemon/session.ts +138 -15
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/logfire.ts +16 -5
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +4 -4
- package/src/messaging/providers/gmail/client.ts +82 -2
- package/src/messaging/providers/gmail/people-client.ts +10 -10
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
- package/src/messaging/providers/whatsapp/adapter.ts +11 -8
- package/src/messaging/registry.ts +2 -32
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +126 -25
- package/src/oauth/byo-connection.ts +22 -6
- package/src/oauth/connect-orchestrator.ts +113 -57
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +35 -11
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +29 -0
- package/src/oauth/platform-connection.ts +6 -5
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +28 -40
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/retry.ts +1 -27
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/http-server.ts +8 -6
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -7
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/settings-routes.ts +55 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -9
- package/src/security/credential-key.ts +0 -156
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +119 -46
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +1 -2
- package/src/tools/credentials/metadata-store.ts +17 -121
- package/src/tools/credentials/vault.ts +94 -167
- package/src/tools/registry.ts +2 -7
- package/src/tools/skills/load.ts +62 -3
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/watcher/providers/google-calendar.ts +2 -1
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1026
- package/src/daemon/ride-shotgun-handler.ts +0 -569
- package/src/oauth/provider-base-urls.ts +0 -21
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
- package/src/runtime/telegram-streaming-delivery.ts +0 -393
- package/src/tools/computer-use/request-computer-control.ts +0 -56
|
@@ -14,22 +14,23 @@ import { loadSkillCatalog } from "../../config/skills.js";
|
|
|
14
14
|
import { normalizeActivationKey } from "../../daemon/handlers/config-voice.js";
|
|
15
15
|
import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
getApp,
|
|
18
|
+
getConnectionByProvider,
|
|
19
|
+
getMostRecentAppByProvider,
|
|
20
|
+
getProvider,
|
|
21
|
+
} from "../../oauth/oauth-store.js";
|
|
22
|
+
import {
|
|
23
|
+
getProviderBehavior,
|
|
18
24
|
resolveService,
|
|
19
|
-
} from "../../oauth/provider-
|
|
25
|
+
} from "../../oauth/provider-behaviors.js";
|
|
20
26
|
import {
|
|
21
27
|
check,
|
|
22
28
|
classifyRisk,
|
|
23
29
|
generateAllowlistOptions,
|
|
24
30
|
generateScopeOptions,
|
|
25
31
|
} from "../../permissions/checker.js";
|
|
26
|
-
import { credentialKey } from "../../security/credential-key.js";
|
|
27
32
|
import { getSecureKey } from "../../security/secure-keys.js";
|
|
28
33
|
import { parseToolManifestFile } from "../../skills/tool-manifest.js";
|
|
29
|
-
import {
|
|
30
|
-
assertMetadataWritable,
|
|
31
|
-
getCredentialMetadata,
|
|
32
|
-
} from "../../tools/credentials/metadata-store.js";
|
|
33
34
|
import {
|
|
34
35
|
type ManifestOverride,
|
|
35
36
|
resolveExecutionTarget,
|
|
@@ -139,50 +140,39 @@ function sanitizeOAuthError(message: string): string {
|
|
|
139
140
|
return "OAuth authentication failed. Please try again.";
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
/** Resolve client_secret from the keychain, checking canonical then alias service name. */
|
|
143
|
-
function getClientSecret(
|
|
144
|
-
resolvedService: string,
|
|
145
|
-
rawService: string,
|
|
146
|
-
): string | undefined {
|
|
147
|
-
return (
|
|
148
|
-
getSecureKey(credentialKey(resolvedService, "client_secret")) ??
|
|
149
|
-
(resolvedService !== rawService
|
|
150
|
-
? getSecureKey(credentialKey(rawService, "client_secret"))
|
|
151
|
-
: undefined) ??
|
|
152
|
-
undefined
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
143
|
async function handleOAuthConnectStart(body: {
|
|
157
144
|
service?: string;
|
|
158
145
|
requestedScopes?: string[];
|
|
159
146
|
}): Promise<Response> {
|
|
160
|
-
try {
|
|
161
|
-
assertMetadataWritable();
|
|
162
|
-
} catch {
|
|
163
|
-
return httpError(
|
|
164
|
-
"UNPROCESSABLE_ENTITY",
|
|
165
|
-
"Credential metadata file has an unrecognized version. Cannot store OAuth credentials.",
|
|
166
|
-
422,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
147
|
if (!body.service) {
|
|
171
148
|
return httpError("BAD_REQUEST", "Missing required field: service", 400);
|
|
172
149
|
}
|
|
173
150
|
|
|
174
151
|
const resolvedService = resolveService(body.service);
|
|
175
152
|
|
|
176
|
-
// client_id
|
|
177
|
-
let clientId
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
153
|
+
// Resolve client_id and client_secret from oauth-store.
|
|
154
|
+
let clientId: string | undefined;
|
|
155
|
+
let clientSecret: string | undefined;
|
|
156
|
+
|
|
157
|
+
// Try existing connection first (re-auth flow)
|
|
158
|
+
const conn = getConnectionByProvider(resolvedService);
|
|
159
|
+
if (conn) {
|
|
160
|
+
const app = getApp(conn.oauthAppId);
|
|
161
|
+
if (app) {
|
|
162
|
+
clientId = app.clientId;
|
|
163
|
+
clientSecret = getSecureKey(`oauth_app/${app.id}/client_secret`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fall back to most recent app for this provider (first-time connect with stored app)
|
|
168
|
+
if (!clientId) {
|
|
169
|
+
const dbApp = getMostRecentAppByProvider(resolvedService);
|
|
170
|
+
if (dbApp) {
|
|
171
|
+
clientId = dbApp.clientId;
|
|
172
|
+
if (!clientSecret) {
|
|
173
|
+
clientSecret = getSecureKey(`oauth_app/${dbApp.id}/client_secret`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
186
176
|
}
|
|
187
177
|
|
|
188
178
|
if (!clientId) {
|
|
@@ -193,12 +183,11 @@ async function handleOAuthConnectStart(body: {
|
|
|
193
183
|
);
|
|
194
184
|
}
|
|
195
185
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const profile = getProviderProfile(resolvedService);
|
|
186
|
+
const behavior = getProviderBehavior(resolvedService);
|
|
187
|
+
const providerRow = getProvider(resolvedService);
|
|
199
188
|
const requiresSecret =
|
|
200
|
-
|
|
201
|
-
!!(
|
|
189
|
+
behavior?.setup?.requiresClientSecret ??
|
|
190
|
+
!!(providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams);
|
|
202
191
|
if (requiresSecret && !clientSecret) {
|
|
203
192
|
return httpError(
|
|
204
193
|
"BAD_REQUEST",
|
|
@@ -222,6 +211,15 @@ async function handleOAuthConnectStart(body: {
|
|
|
222
211
|
authUrl = url;
|
|
223
212
|
},
|
|
224
213
|
onDeferredComplete: (deferredResult) => {
|
|
214
|
+
// Prefer accountInfo from oauth-store when available.
|
|
215
|
+
let accountInfo = deferredResult.accountInfo;
|
|
216
|
+
try {
|
|
217
|
+
const conn = getConnectionByProvider(resolvedService);
|
|
218
|
+
if (conn?.accountInfo) accountInfo = conn.accountInfo;
|
|
219
|
+
} catch {
|
|
220
|
+
// DB not ready — use orchestrator value
|
|
221
|
+
}
|
|
222
|
+
|
|
225
223
|
// Emit oauth_connect_result to all connected SSE clients so the
|
|
226
224
|
// UI can update immediately when the deferred browser flow completes.
|
|
227
225
|
assistantEventHub
|
|
@@ -230,7 +228,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
230
228
|
type: "oauth_connect_result",
|
|
231
229
|
success: deferredResult.success,
|
|
232
230
|
service: deferredResult.service,
|
|
233
|
-
accountInfo
|
|
231
|
+
accountInfo,
|
|
234
232
|
error: deferredResult.error,
|
|
235
233
|
}),
|
|
236
234
|
)
|
|
@@ -273,10 +271,19 @@ async function handleOAuthConnectStart(body: {
|
|
|
273
271
|
});
|
|
274
272
|
}
|
|
275
273
|
|
|
274
|
+
// Prefer accountInfo from oauth-store when available.
|
|
275
|
+
let responseAccountInfo = result.accountInfo;
|
|
276
|
+
try {
|
|
277
|
+
const conn = getConnectionByProvider(resolvedService);
|
|
278
|
+
if (conn?.accountInfo) responseAccountInfo = conn.accountInfo;
|
|
279
|
+
} catch {
|
|
280
|
+
// DB not ready — use orchestrator value
|
|
281
|
+
}
|
|
282
|
+
|
|
276
283
|
return Response.json({
|
|
277
284
|
ok: true,
|
|
278
285
|
grantedScopes: result.grantedScopes,
|
|
279
|
-
accountInfo:
|
|
286
|
+
accountInfo: responseAccountInfo,
|
|
280
287
|
...(authUrl ? { authUrl } : {}),
|
|
281
288
|
});
|
|
282
289
|
} catch (err) {
|
|
@@ -10,7 +10,7 @@ import type { RouteDefinition } from "../http-router.js";
|
|
|
10
10
|
|
|
11
11
|
const log = getLogger("surface-action-routes");
|
|
12
12
|
|
|
13
|
-
/** Any object that can handle a surface action
|
|
13
|
+
/** Any object that can handle a surface action. */
|
|
14
14
|
interface SurfaceActionTarget {
|
|
15
15
|
handleSurfaceAction(
|
|
16
16
|
surfaceId: string,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route handler for watch (ambient observation) functionality.
|
|
3
|
+
*
|
|
4
|
+
* Decoupled from computer-use routes so that the watch endpoint has
|
|
5
|
+
* zero dependency on CU session state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from "../../util/logger.js";
|
|
9
|
+
import { httpError } from "../http-errors.js";
|
|
10
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
11
|
+
|
|
12
|
+
const log = getLogger("watch-routes");
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Dependency injection interface
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface for watch observation handling.
|
|
20
|
+
* The daemon wires a concrete implementation at startup.
|
|
21
|
+
*/
|
|
22
|
+
export interface WatchDeps {
|
|
23
|
+
/** Handle a watch observation. */
|
|
24
|
+
handleWatchObservation: (params: {
|
|
25
|
+
watchId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
ocrText: string;
|
|
28
|
+
appName?: string;
|
|
29
|
+
windowTitle?: string;
|
|
30
|
+
bundleIdentifier?: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
captureIndex: number;
|
|
33
|
+
totalExpected: number;
|
|
34
|
+
}) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Route handler
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* POST /v1/computer-use/watch — send a watch observation.
|
|
43
|
+
*
|
|
44
|
+
* Body: { watchId, sessionId, ocrText, appName?, windowTitle?,
|
|
45
|
+
* bundleIdentifier?, timestamp, captureIndex, totalExpected }
|
|
46
|
+
*/
|
|
47
|
+
async function handleWatchObservationRoute(
|
|
48
|
+
req: Request,
|
|
49
|
+
deps: WatchDeps,
|
|
50
|
+
): Promise<Response> {
|
|
51
|
+
const body = (await req.json()) as {
|
|
52
|
+
watchId?: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
ocrText?: string;
|
|
55
|
+
appName?: string;
|
|
56
|
+
windowTitle?: string;
|
|
57
|
+
bundleIdentifier?: string;
|
|
58
|
+
timestamp?: number;
|
|
59
|
+
captureIndex?: number;
|
|
60
|
+
totalExpected?: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!body.watchId || typeof body.watchId !== "string") {
|
|
64
|
+
return httpError("BAD_REQUEST", "watchId is required", 400);
|
|
65
|
+
}
|
|
66
|
+
if (!body.sessionId || typeof body.sessionId !== "string") {
|
|
67
|
+
return httpError("BAD_REQUEST", "sessionId is required", 400);
|
|
68
|
+
}
|
|
69
|
+
if (!body.ocrText || typeof body.ocrText !== "string") {
|
|
70
|
+
return httpError("BAD_REQUEST", "ocrText is required", 400);
|
|
71
|
+
}
|
|
72
|
+
if (typeof body.timestamp !== "number") {
|
|
73
|
+
return httpError("BAD_REQUEST", "timestamp is required", 400);
|
|
74
|
+
}
|
|
75
|
+
if (typeof body.captureIndex !== "number") {
|
|
76
|
+
return httpError("BAD_REQUEST", "captureIndex is required", 400);
|
|
77
|
+
}
|
|
78
|
+
if (typeof body.totalExpected !== "number") {
|
|
79
|
+
return httpError("BAD_REQUEST", "totalExpected is required", 400);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await deps.handleWatchObservation({
|
|
84
|
+
watchId: body.watchId,
|
|
85
|
+
sessionId: body.sessionId,
|
|
86
|
+
ocrText: body.ocrText,
|
|
87
|
+
appName: body.appName,
|
|
88
|
+
windowTitle: body.windowTitle,
|
|
89
|
+
bundleIdentifier: body.bundleIdentifier,
|
|
90
|
+
timestamp: body.timestamp,
|
|
91
|
+
captureIndex: body.captureIndex,
|
|
92
|
+
totalExpected: body.totalExpected,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return Response.json({ ok: true });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
+
log.error(
|
|
99
|
+
{ err, watchId: body.watchId },
|
|
100
|
+
"Failed to handle watch observation via HTTP",
|
|
101
|
+
);
|
|
102
|
+
return httpError("INTERNAL_ERROR", message, 500);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Route definitions
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export function watchRouteDefinitions(deps: {
|
|
111
|
+
getWatchDeps?: () => WatchDeps;
|
|
112
|
+
}): RouteDefinition[] {
|
|
113
|
+
const getDeps = (): WatchDeps => {
|
|
114
|
+
if (!deps.getWatchDeps) {
|
|
115
|
+
throw new Error("Watch deps not available");
|
|
116
|
+
}
|
|
117
|
+
return deps.getWatchDeps();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
endpoint: "computer-use/watch",
|
|
123
|
+
method: "POST",
|
|
124
|
+
policyKey: "computer-use/watch",
|
|
125
|
+
handler: async ({ req }) => handleWatchObservationRoute(req, getDeps()),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { hasTwilioCredentials } from "../calls/twilio-rest.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
getConnectionByProvider,
|
|
4
|
+
isProviderConnected,
|
|
5
|
+
} from "../oauth/oauth-store.js";
|
|
4
6
|
|
|
5
7
|
interface IntegrationProbe {
|
|
6
8
|
name: string;
|
|
@@ -13,14 +15,12 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
13
15
|
{
|
|
14
16
|
name: "Gmail",
|
|
15
17
|
category: "email",
|
|
16
|
-
isConnected: () =>
|
|
17
|
-
!!getSecureKey(credentialKey("integration:gmail", "access_token")),
|
|
18
|
+
isConnected: () => isProviderConnected("integration:gmail"),
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
21
|
name: "Slack",
|
|
21
22
|
category: "messaging",
|
|
22
|
-
isConnected: () =>
|
|
23
|
-
!!getSecureKey(credentialKey("integration:slack", "access_token")),
|
|
23
|
+
isConnected: () => isProviderConnected("integration:slack"),
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: "Twilio",
|
|
@@ -30,9 +30,10 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
30
30
|
{
|
|
31
31
|
name: "Telegram",
|
|
32
32
|
category: "messaging",
|
|
33
|
-
isConnected: () =>
|
|
34
|
-
|
|
35
|
-
!!
|
|
33
|
+
isConnected: () => {
|
|
34
|
+
const conn = getConnectionByProvider("telegram");
|
|
35
|
+
return !!(conn && conn.status === "active");
|
|
36
|
+
},
|
|
36
37
|
},
|
|
37
38
|
];
|
|
38
39
|
|
|
@@ -2,23 +2,8 @@
|
|
|
2
2
|
* Single source of truth for credential key format in the secure store.
|
|
3
3
|
*
|
|
4
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
5
|
*/
|
|
10
6
|
|
|
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
7
|
/**
|
|
23
8
|
* Build a credential key for the secure store.
|
|
24
9
|
*
|
|
@@ -27,144 +12,3 @@ const log = getLogger("credential-key");
|
|
|
27
12
|
export function credentialKey(service: string, field: string): string {
|
|
28
13
|
return `credential/${service}/${field}`;
|
|
29
14
|
}
|
|
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
|
-
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* provides a graceful-fallback interface: every public method returns a
|
|
7
7
|
* safe default on failure and never throws.
|
|
8
8
|
*
|
|
9
|
-
* Socket path:
|
|
9
|
+
* Socket path: derived from getRootDir() as `~/.vellum/keychain-broker.sock`.
|
|
10
10
|
* Auth token: read from ~/.vellum/protected/keychain-broker.token on first
|
|
11
11
|
* connection, cached for process lifetime.
|
|
12
12
|
*/
|
|
@@ -70,8 +70,8 @@ function getTokenPath(): string {
|
|
|
70
70
|
return join(getRootDir(), "protected", "keychain-broker.token");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function getSocketPath(): string
|
|
74
|
-
return
|
|
73
|
+
function getSocketPath(): string {
|
|
74
|
+
return join(getRootDir(), "keychain-broker.sock");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
@@ -172,7 +172,7 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
172
172
|
function connect(): Promise<Socket> {
|
|
173
173
|
return new Promise((resolve, reject) => {
|
|
174
174
|
const socketPath = getSocketPath();
|
|
175
|
-
if (!socketPath) {
|
|
175
|
+
if (!pathExists(socketPath)) {
|
|
176
176
|
reject(new Error("No socket path"));
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
@@ -328,8 +328,7 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
328
328
|
return {
|
|
329
329
|
isAvailable(): boolean {
|
|
330
330
|
if (permanentlyUnavailable) return false;
|
|
331
|
-
|
|
332
|
-
if (!socketPath) return false;
|
|
331
|
+
if (!pathExists(getSocketPath())) return false;
|
|
333
332
|
return pathExists(getTokenPath());
|
|
334
333
|
},
|
|
335
334
|
|
package/src/security/oauth2.ts
CHANGED
|
@@ -691,7 +691,7 @@ export async function startOAuth2Flow(
|
|
|
691
691
|
if (transport === "gateway") {
|
|
692
692
|
if (!hasPublicUrl) {
|
|
693
693
|
throw new Error(
|
|
694
|
-
"Gateway transport requires a public ingress URL. Set ingress.publicBaseUrl
|
|
694
|
+
"Gateway transport requires a public ingress URL. Set ingress.publicBaseUrl, or use loopback transport.",
|
|
695
695
|
);
|
|
696
696
|
}
|
|
697
697
|
log.debug({ transport: "gateway" }, "OAuth2 flow starting");
|