@vellumai/assistant 0.4.50 → 0.4.51
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/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +19 -18
- package/src/__tests__/credential-vault.test.ts +17 -17
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +121 -32
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/oauth-cli.test.ts +286 -60
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +243 -11
- package/src/__tests__/relay-server.test.ts +9 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +71 -16
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +58 -9
- package/src/calls/call-domain.ts +3 -4
- package/src/calls/relay-server.ts +1 -1
- package/src/calls/twilio-config.ts +4 -3
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/oauth/apps.ts +40 -11
- package/src/cli/commands/oauth/connections.ts +66 -30
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +3 -3
- package/src/cli.ts +16 -12
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/loader.ts +6 -42
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/daemon/config-watcher.ts +8 -13
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/message-types/sessions.ts +0 -42
- package/src/daemon/server.ts +0 -6
- package/src/daemon/session-slash.ts +3 -5
- package/src/email/providers/index.ts +2 -2
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/conversation-queries.ts +3 -80
- package/src/memory/db-init.ts +4 -0
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +1 -1
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +32 -24
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +83 -17
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +208 -8
- package/src/oauth/token-persistence.ts +20 -13
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +71 -29
- package/src/runtime/invite-service.ts +40 -22
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +47 -10
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/workspace-routes.ts +7 -3
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +19 -16
- package/src/security/token-manager.ts +13 -6
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/tools/credentials/vault.ts +22 -5
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +2 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
|
@@ -20,6 +20,17 @@ import { httpError } from "../http-errors.js";
|
|
|
20
20
|
import type { RouteDefinition } from "../http-router.js";
|
|
21
21
|
|
|
22
22
|
const log = getLogger("runtime-http");
|
|
23
|
+
const MANAGED_PROXY_CREDENTIAL = {
|
|
24
|
+
service: "vellum",
|
|
25
|
+
field: "assistant_api_key",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function isManagedProxyCredential(service: string, field: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
service === MANAGED_PROXY_CREDENTIAL.service &&
|
|
31
|
+
field === MANAGED_PROXY_CREDENTIAL.field
|
|
32
|
+
);
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
export async function handleAddSecret(req: Request): Promise<Response> {
|
|
25
36
|
const body = (await req.json()) as {
|
|
@@ -89,6 +100,9 @@ export async function handleAddSecret(req: Request): Promise<Response> {
|
|
|
89
100
|
);
|
|
90
101
|
}
|
|
91
102
|
upsertCredentialMetadata(service, field, {});
|
|
103
|
+
if (isManagedProxyCredential(service, field)) {
|
|
104
|
+
initializeProviders(getConfig());
|
|
105
|
+
}
|
|
92
106
|
log.info({ service, field }, "Credential added via HTTP");
|
|
93
107
|
return Response.json({ success: true, type, name }, { status: 201 });
|
|
94
108
|
}
|
|
@@ -181,6 +195,9 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
181
195
|
);
|
|
182
196
|
}
|
|
183
197
|
deleteCredentialMetadata(service, field);
|
|
198
|
+
if (isManagedProxyCredential(service, field)) {
|
|
199
|
+
initializeProviders(getConfig());
|
|
200
|
+
}
|
|
184
201
|
log.info({ service, field }, "Credential deleted via HTTP");
|
|
185
202
|
return Response.json({ success: true, type, name });
|
|
186
203
|
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
generateAllowlistOptions,
|
|
30
30
|
generateScopeOptions,
|
|
31
31
|
} from "../../permissions/checker.js";
|
|
32
|
-
import {
|
|
32
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
33
33
|
import { parseToolManifestFile } from "../../skills/tool-manifest.js";
|
|
34
34
|
import {
|
|
35
35
|
type ManifestOverride,
|
|
@@ -160,7 +160,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
160
160
|
const app = getApp(conn.oauthAppId);
|
|
161
161
|
if (app) {
|
|
162
162
|
clientId = app.clientId;
|
|
163
|
-
clientSecret =
|
|
163
|
+
clientSecret = await getSecureKeyAsync(app.clientSecretCredentialPath);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -170,7 +170,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
170
170
|
if (dbApp) {
|
|
171
171
|
clientId = dbApp.clientId;
|
|
172
172
|
if (!clientSecret) {
|
|
173
|
-
clientSecret =
|
|
173
|
+
clientSecret = await getSecureKeyAsync(dbApp.clientSecretCredentialPath);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -34,7 +34,9 @@ interface TreeEntry {
|
|
|
34
34
|
function handleWorkspaceTree(ctx: RouteContext): Response {
|
|
35
35
|
const requestedPath = ctx.url.searchParams.get("path") ?? "";
|
|
36
36
|
const showHidden = ctx.url.searchParams.get("showHidden") === "true";
|
|
37
|
-
const resolved = resolveWorkspacePath(requestedPath
|
|
37
|
+
const resolved = resolveWorkspacePath(requestedPath, {
|
|
38
|
+
allowHidden: showHidden,
|
|
39
|
+
});
|
|
38
40
|
if (resolved === undefined) {
|
|
39
41
|
return httpError("BAD_REQUEST", "Invalid path", 400);
|
|
40
42
|
}
|
|
@@ -96,7 +98,8 @@ function handleWorkspaceFile(ctx: RouteContext): Response {
|
|
|
96
98
|
return httpError("BAD_REQUEST", "path query parameter is required", 400);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
const
|
|
101
|
+
const showHidden = ctx.url.searchParams.get("showHidden") === "true";
|
|
102
|
+
const resolved = resolveWorkspacePath(path, { allowHidden: showHidden });
|
|
100
103
|
if (resolved === undefined) {
|
|
101
104
|
return httpError("BAD_REQUEST", "Invalid path", 400);
|
|
102
105
|
}
|
|
@@ -146,7 +149,8 @@ function handleWorkspaceFileContent(ctx: RouteContext): Response {
|
|
|
146
149
|
);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
const
|
|
152
|
+
const showHidden = ctx.url.searchParams.get("showHidden") === "true";
|
|
153
|
+
const resolved = resolveWorkspacePath(path, { allowHidden: showHidden });
|
|
150
154
|
if (resolved === undefined) {
|
|
151
155
|
return httpError("BAD_REQUEST", "Invalid path", 400);
|
|
152
156
|
}
|
|
@@ -7,10 +7,16 @@ import { getWorkspaceDir } from "../../util/platform.js";
|
|
|
7
7
|
* Resolves a user-provided relative path to an absolute path within the workspace.
|
|
8
8
|
* Returns the resolved absolute path, or undefined if the path escapes the workspace root.
|
|
9
9
|
*/
|
|
10
|
-
export function resolveWorkspacePath(
|
|
10
|
+
export function resolveWorkspacePath(
|
|
11
|
+
relativePath: string,
|
|
12
|
+
options?: { allowHidden?: boolean },
|
|
13
|
+
): string | undefined {
|
|
11
14
|
// Reject paths containing hidden (dot-prefixed) segments like .env, .git, .hidden/foo
|
|
12
15
|
const segments = relativePath.split(/[/\\]/);
|
|
13
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
!options?.allowHidden &&
|
|
18
|
+
segments.some((s) => s.startsWith(".") && s !== "." && s !== "..")
|
|
19
|
+
) {
|
|
14
20
|
return undefined;
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
interface IntegrationProbe {
|
|
8
8
|
name: string;
|
|
9
9
|
category: string;
|
|
10
|
-
isConnected: () => boolean
|
|
10
|
+
isConnected: () => Promise<boolean>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Registry — add new integrations here:
|
|
@@ -15,7 +15,7 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
15
15
|
{
|
|
16
16
|
name: "Gmail",
|
|
17
17
|
category: "email",
|
|
18
|
-
isConnected: () => isProviderConnected("integration:
|
|
18
|
+
isConnected: () => isProviderConnected("integration:google"),
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: "Slack",
|
|
@@ -25,39 +25,46 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
25
25
|
{
|
|
26
26
|
name: "Twilio",
|
|
27
27
|
category: "telephony",
|
|
28
|
-
isConnected: () => hasTwilioCredentials(),
|
|
28
|
+
isConnected: async () => hasTwilioCredentials(),
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: "Telegram",
|
|
32
32
|
category: "messaging",
|
|
33
|
-
isConnected: () => {
|
|
33
|
+
isConnected: async () => {
|
|
34
34
|
const conn = getConnectionByProvider("telegram");
|
|
35
35
|
return !!(conn && conn.status === "active");
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
export function getIntegrationSummary():
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
export async function getIntegrationSummary(): Promise<
|
|
41
|
+
Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
category: string;
|
|
44
|
+
connected: boolean;
|
|
45
|
+
}>
|
|
46
|
+
> {
|
|
47
|
+
return Promise.all(
|
|
48
|
+
INTEGRATION_PROBES.map(async (probe) => ({
|
|
49
|
+
name: probe.name,
|
|
50
|
+
category: probe.category,
|
|
51
|
+
connected: await probe.isConnected(),
|
|
52
|
+
})),
|
|
53
|
+
);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
export function formatIntegrationSummary(): string {
|
|
53
|
-
const summary = getIntegrationSummary();
|
|
56
|
+
export async function formatIntegrationSummary(): Promise<string> {
|
|
57
|
+
const summary = await getIntegrationSummary();
|
|
54
58
|
return summary
|
|
55
59
|
.map((s) => `${s.name} ${s.connected ? "\u2713" : "\u2717"}`)
|
|
56
60
|
.join(" | ");
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export function hasCapability(category: string): boolean {
|
|
60
|
-
|
|
61
|
-
(probe) => probe.category === category
|
|
63
|
+
export async function hasCapability(category: string): Promise<boolean> {
|
|
64
|
+
const results = await Promise.all(
|
|
65
|
+
INTEGRATION_PROBES.filter((probe) => probe.category === category).map(
|
|
66
|
+
(probe) => probe.isConnected(),
|
|
67
|
+
),
|
|
62
68
|
);
|
|
69
|
+
return results.some(Boolean);
|
|
63
70
|
}
|
package/src/security/oauth2.ts
CHANGED
|
@@ -420,18 +420,17 @@ export interface OAuth2PreparedFlow {
|
|
|
420
420
|
* URL directly in chat and the callback arrives asynchronously via the gateway.
|
|
421
421
|
*
|
|
422
422
|
* Supports two transports:
|
|
423
|
-
* - **
|
|
424
|
-
*
|
|
425
|
-
* - **
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* (channel) sessions.
|
|
423
|
+
* - **loopback** (default): starts a temporary localhost server to receive the
|
|
424
|
+
* callback. Works without any public URL or tunnel.
|
|
425
|
+
* - **gateway**: routes callbacks through the public ingress URL.
|
|
426
|
+
* Requires `ingress.publicBaseUrl` to be configured. Used for providers that
|
|
427
|
+
* don't support localhost redirects (e.g. Twitter, Notion).
|
|
429
428
|
*/
|
|
430
429
|
export async function prepareOAuth2Flow(
|
|
431
430
|
config: OAuth2Config,
|
|
432
431
|
options?: OAuth2FlowOptions,
|
|
433
432
|
): Promise<OAuth2PreparedFlow> {
|
|
434
|
-
const transport = options?.callbackTransport ?? "
|
|
433
|
+
const transport = options?.callbackTransport ?? "loopback";
|
|
435
434
|
|
|
436
435
|
if (transport === "loopback") {
|
|
437
436
|
return prepareLoopbackFlow(config, options?.loopbackPort);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* available (macOS app embedded), with transparent fallback to the
|
|
4
4
|
* encrypted-at-rest file store.
|
|
5
5
|
*
|
|
6
|
-
* Async variants try the
|
|
6
|
+
* Async variants try the encrypted store first; sync variants always use the
|
|
7
7
|
* encrypted store (startup code paths cannot do async I/O).
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -82,30 +82,33 @@ export function isDowngradedFromKeychain(): boolean {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
|
-
// Async variants — try
|
|
85
|
+
// Async variants — try encrypted store first, fall back to broker
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Async version of `getSecureKey`.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* while the broker was unavailable or via sync `setSecureKey`).
|
|
89
|
+
* Async version of `getSecureKey`. Checks the encrypted store first
|
|
90
|
+
* (instant) since `setSecureKeyAsync` always writes to both stores.
|
|
91
|
+
* Falls back to the broker for keys that may exist only in the macOS
|
|
92
|
+
* Keychain. Returns `undefined` if the key is not found in either store.
|
|
94
93
|
*/
|
|
95
94
|
export async function getSecureKeyAsync(
|
|
96
95
|
account: string,
|
|
97
96
|
): Promise<string | undefined> {
|
|
97
|
+
// Check encrypted store first (sync, instant). Since setSecureKeyAsync
|
|
98
|
+
// always writes to both broker and encrypted store, a hit here is
|
|
99
|
+
// authoritative and avoids the broker IPC round-trip.
|
|
100
|
+
const encResult = encryptedStore.getKey(account);
|
|
101
|
+
if (encResult != null && encResult.length > 0) return encResult;
|
|
102
|
+
|
|
103
|
+
// Not in encrypted store — try broker as fallback for keys that may
|
|
104
|
+
// exist only in the macOS Keychain (e.g. written by the app directly).
|
|
98
105
|
const broker = getBroker();
|
|
99
106
|
if (broker.isAvailable()) {
|
|
100
107
|
const result = await broker.get(account);
|
|
101
|
-
|
|
102
|
-
if (result == null) return encryptedStore.getKey(account);
|
|
103
|
-
// Broker found the key — use it
|
|
104
|
-
if (result.found) return result.value;
|
|
105
|
-
// Broker says not found — check encrypted store as fallback
|
|
106
|
-
return encryptedStore.getKey(account);
|
|
108
|
+
if (result?.found) return result.value;
|
|
107
109
|
}
|
|
108
|
-
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -115,7 +118,7 @@ export async function getSecureKeyAsync(
|
|
|
115
118
|
*
|
|
116
119
|
* If the broker is available but `broker.set()` fails we return `false`
|
|
117
120
|
* immediately — falling through to an encrypted-store-only write would
|
|
118
|
-
* leave the
|
|
121
|
+
* leave the two stores out of sync.
|
|
119
122
|
*/
|
|
120
123
|
export async function setSecureKeyAsync(
|
|
121
124
|
account: string,
|
|
@@ -161,7 +164,7 @@ export async function setSecureKeyAsync(
|
|
|
161
164
|
*
|
|
162
165
|
* If the broker is available but `broker.del()` fails we return `"error"`
|
|
163
166
|
* immediately — falling through to an encrypted-store-only delete would
|
|
164
|
-
* leave the broker with the key,
|
|
167
|
+
* leave the broker with the key, causing stale reads on broker fallback.
|
|
165
168
|
*/
|
|
166
169
|
export async function deleteSecureKeyAsync(
|
|
167
170
|
account: string,
|
|
@@ -16,11 +16,15 @@ import {
|
|
|
16
16
|
} from "../oauth/oauth-store.js";
|
|
17
17
|
import { getLogger } from "../util/logger.js";
|
|
18
18
|
import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getSecureKey,
|
|
21
|
+
getSecureKeyAsync,
|
|
22
|
+
setSecureKeyAsync,
|
|
23
|
+
} from "./secure-keys.js";
|
|
20
24
|
|
|
21
25
|
const log = getLogger("token-manager");
|
|
22
26
|
|
|
23
|
-
const MESSAGING_SERVICES = new Set(["integration:
|
|
27
|
+
const MESSAGING_SERVICES = new Set(["integration:google", "integration:slack"]);
|
|
24
28
|
|
|
25
29
|
function recoveryHint(service: string): string {
|
|
26
30
|
const shortName = service.startsWith("integration:")
|
|
@@ -185,7 +189,10 @@ interface RefreshConfig {
|
|
|
185
189
|
* authMethod. Throws `TokenExpiredError` if the connection is not found
|
|
186
190
|
* or incomplete.
|
|
187
191
|
*/
|
|
188
|
-
function resolveRefreshConfig(
|
|
192
|
+
async function resolveRefreshConfig(
|
|
193
|
+
service: string,
|
|
194
|
+
connId: string,
|
|
195
|
+
): Promise<RefreshConfig> {
|
|
189
196
|
const conn = getConnection(connId);
|
|
190
197
|
if (!conn) {
|
|
191
198
|
throw new TokenExpiredError(
|
|
@@ -219,9 +226,9 @@ function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
|
|
|
219
226
|
);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
const secret =
|
|
229
|
+
const secret = await getSecureKeyAsync(app.clientSecretCredentialPath);
|
|
223
230
|
|
|
224
|
-
const refreshToken =
|
|
231
|
+
const refreshToken = await getSecureKeyAsync(
|
|
225
232
|
`oauth_connection/${conn.id}/refresh_token`,
|
|
226
233
|
);
|
|
227
234
|
|
|
@@ -249,7 +256,7 @@ function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
|
|
|
249
256
|
* Throws `TokenExpiredError` if refresh is not possible.
|
|
250
257
|
*/
|
|
251
258
|
async function doRefresh(service: string, connId: string): Promise<string> {
|
|
252
|
-
const refreshConfig = resolveRefreshConfig(service, connId);
|
|
259
|
+
const refreshConfig = await resolveRefreshConfig(service, connId);
|
|
253
260
|
const {
|
|
254
261
|
tokenUrl,
|
|
255
262
|
clientId: resolvedClientId,
|
|
@@ -55,27 +55,3 @@ export async function deployHtmlToVercel(opts: {
|
|
|
55
55
|
|
|
56
56
|
return { url: publicUrl, deploymentId: data.id };
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
export async function deleteVercelDeployment(
|
|
60
|
-
deploymentId: string,
|
|
61
|
-
token: string,
|
|
62
|
-
): Promise<void> {
|
|
63
|
-
const response = await fetch(
|
|
64
|
-
`https://api.vercel.com/v13/deployments/${deploymentId}`,
|
|
65
|
-
{
|
|
66
|
-
method: "DELETE",
|
|
67
|
-
headers: {
|
|
68
|
-
Authorization: `Bearer ${token}`,
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const text = await response.text();
|
|
75
|
-
throw new ProviderError(
|
|
76
|
-
`Vercel delete deployment failed (${response.status}): ${text}`,
|
|
77
|
-
"vercel",
|
|
78
|
-
response.status,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle confirmation decisions delivered via signal files from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* The built-in CLI writes JSON to `signals/confirm` instead of making an
|
|
5
|
+
* HTTP POST to `/v1/confirm`. The daemon's ConfigWatcher detects the file
|
|
6
|
+
* change and invokes {@link handleConfirmationSignal}, which reads the
|
|
7
|
+
* payload and resolves the pending interaction in-process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import type { UserDecision } from "../permissions/types.js";
|
|
14
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
16
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
17
|
+
|
|
18
|
+
const log = getLogger("signal:confirm");
|
|
19
|
+
|
|
20
|
+
const VALID_DECISIONS: ReadonlySet<string> = new Set<string>([
|
|
21
|
+
"allow",
|
|
22
|
+
"allow_10m",
|
|
23
|
+
"allow_thread",
|
|
24
|
+
"always_allow",
|
|
25
|
+
"always_allow_high_risk",
|
|
26
|
+
"deny",
|
|
27
|
+
"always_deny",
|
|
28
|
+
"temporary_override",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function isUserDecision(value: string): value is UserDecision {
|
|
32
|
+
return VALID_DECISIONS.has(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the `signals/confirm` file and resolve the pending interaction.
|
|
37
|
+
* Called by ConfigWatcher when the signal file is written or modified.
|
|
38
|
+
*/
|
|
39
|
+
export function handleConfirmationSignal(): void {
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(
|
|
42
|
+
join(getWorkspaceDir(), "signals", "confirm"),
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
const parsed = JSON.parse(content) as {
|
|
46
|
+
requestId?: string;
|
|
47
|
+
decision?: string;
|
|
48
|
+
};
|
|
49
|
+
const { requestId, decision } = parsed;
|
|
50
|
+
|
|
51
|
+
if (!requestId || typeof requestId !== "string") {
|
|
52
|
+
log.warn("Confirmation signal missing requestId");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!decision || !isUserDecision(decision)) {
|
|
56
|
+
log.warn({ decision }, "Confirmation signal has invalid decision");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const interaction = pendingInteractions.resolve(requestId);
|
|
61
|
+
if (!interaction) {
|
|
62
|
+
log.warn({ requestId }, "No pending interaction for confirmation signal");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interaction.session.handleConfirmationResponse(
|
|
67
|
+
requestId,
|
|
68
|
+
decision,
|
|
69
|
+
undefined,
|
|
70
|
+
undefined,
|
|
71
|
+
undefined,
|
|
72
|
+
{ source: "button" },
|
|
73
|
+
);
|
|
74
|
+
log.info({ requestId, decision }, "Confirmation resolved via signal file");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.error({ err }, "Failed to handle confirmation signal");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle MCP reload signals from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* When the CLI writes to `signals/mcp-reload`, the daemon's ConfigWatcher
|
|
5
|
+
* detects the file change and invokes {@link handleMcpReloadSignal} to
|
|
6
|
+
* restart MCP servers with the latest configuration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
|
|
10
|
+
import { getLogger } from "../util/logger.js";
|
|
11
|
+
|
|
12
|
+
const log = getLogger("signal:mcp-reload");
|
|
13
|
+
|
|
14
|
+
export function handleMcpReloadSignal(): void {
|
|
15
|
+
reloadMcpServers().catch((err: unknown) => {
|
|
16
|
+
log.error({ err }, "MCP reload triggered by signal file failed");
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -3,6 +3,7 @@ import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
|
|
|
3
3
|
import {
|
|
4
4
|
disconnectOAuthProvider,
|
|
5
5
|
getAppByProviderAndClientId,
|
|
6
|
+
getConnectionByProviderAndAccount,
|
|
6
7
|
getMostRecentAppByProvider,
|
|
7
8
|
getProvider,
|
|
8
9
|
} from "../../oauth/oauth-store.js";
|
|
@@ -19,7 +20,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
|
|
|
19
20
|
import { credentialKey } from "../../security/credential-key.js";
|
|
20
21
|
import {
|
|
21
22
|
deleteSecureKeyAsync,
|
|
22
|
-
|
|
23
|
+
getSecureKeyAsync,
|
|
23
24
|
listSecureKeys,
|
|
24
25
|
setSecureKeyAsync,
|
|
25
26
|
} from "../../security/secure-keys.js";
|
|
@@ -72,6 +73,11 @@ class CredentialStoreTool implements Tool {
|
|
|
72
73
|
type: "string",
|
|
73
74
|
description: "Service name, e.g. gmail, github",
|
|
74
75
|
},
|
|
76
|
+
account: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description:
|
|
79
|
+
"Account identifier (e.g. email address) to target a specific connection when multiple accounts are connected for the same service. If omitted, uses the most recently connected account.",
|
|
80
|
+
},
|
|
75
81
|
field: {
|
|
76
82
|
type: "string",
|
|
77
83
|
description: "Field name, e.g. password, username, recovery_email",
|
|
@@ -453,7 +459,19 @@ class CredentialStoreTool implements Tool {
|
|
|
453
459
|
}
|
|
454
460
|
// Also clean up any OAuth connection for this service (best-effort)
|
|
455
461
|
try {
|
|
456
|
-
const
|
|
462
|
+
const accountHint = input.account as string | undefined;
|
|
463
|
+
let oauthResult: "disconnected" | "not-found" | "error";
|
|
464
|
+
if (accountHint) {
|
|
465
|
+
const targetConn = getConnectionByProviderAndAccount(
|
|
466
|
+
service,
|
|
467
|
+
accountHint,
|
|
468
|
+
);
|
|
469
|
+
oauthResult = targetConn
|
|
470
|
+
? await disconnectOAuthProvider(service, undefined, targetConn.id)
|
|
471
|
+
: "not-found";
|
|
472
|
+
} else {
|
|
473
|
+
oauthResult = await disconnectOAuthProvider(service);
|
|
474
|
+
}
|
|
457
475
|
if (oauthResult === "error") {
|
|
458
476
|
log.warn(
|
|
459
477
|
{ service },
|
|
@@ -723,7 +741,7 @@ class CredentialStoreTool implements Tool {
|
|
|
723
741
|
isError: true,
|
|
724
742
|
};
|
|
725
743
|
|
|
726
|
-
// Resolve aliases (e.g. "gmail" → "integration:
|
|
744
|
+
// Resolve aliases (e.g. "gmail" → "integration:google")
|
|
727
745
|
const service = resolveService(rawService);
|
|
728
746
|
|
|
729
747
|
// Code-side behavioral fields (identityVerifier, setup, etc.)
|
|
@@ -747,7 +765,7 @@ class CredentialStoreTool implements Tool {
|
|
|
747
765
|
if (dbApp) {
|
|
748
766
|
if (!clientId) clientId = dbApp.clientId;
|
|
749
767
|
if (!clientSecret) {
|
|
750
|
-
clientSecret =
|
|
768
|
+
clientSecret = await getSecureKeyAsync(dbApp.clientSecretCredentialPath);
|
|
751
769
|
}
|
|
752
770
|
}
|
|
753
771
|
}
|
|
@@ -794,7 +812,6 @@ class CredentialStoreTool implements Tool {
|
|
|
794
812
|
clientSecret,
|
|
795
813
|
isInteractive: !!context.isInteractive,
|
|
796
814
|
sendToClient: context.sendToClient,
|
|
797
|
-
allowedTools: input.allowed_tools as string[] | undefined,
|
|
798
815
|
...(inputScopes ? { requestedScopes: inputScopes } : {}),
|
|
799
816
|
onDeferredComplete: (deferredResult) => {
|
|
800
817
|
// Emit oauth_connect_result to all connected SSE clients so the
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
routeConnection,
|
|
20
20
|
stripQueryString,
|
|
21
21
|
} from "../../../outbound-proxy/index.js";
|
|
22
|
-
import {
|
|
22
|
+
import { getSecureKeyAsync } from "../../../security/secure-keys.js";
|
|
23
23
|
import { getLogger } from "../../../util/logger.js";
|
|
24
24
|
import { silentlyWithLog } from "../../../util/silently.js";
|
|
25
25
|
import {
|
|
@@ -97,10 +97,10 @@ const acquireLocks = new Map<string, Promise<ProxySession>>();
|
|
|
97
97
|
* Handles optional composition with a second credential and value transforms.
|
|
98
98
|
* Returns null if any referenced credential cannot be resolved.
|
|
99
99
|
*/
|
|
100
|
-
function buildInjectedValue(
|
|
100
|
+
async function buildInjectedValue(
|
|
101
101
|
tpl: CredentialInjectionTemplate,
|
|
102
102
|
primaryValue: string,
|
|
103
|
-
): string | null {
|
|
103
|
+
): Promise<string | null> {
|
|
104
104
|
let value = primaryValue;
|
|
105
105
|
|
|
106
106
|
if (tpl.composeWith) {
|
|
@@ -109,7 +109,7 @@ function buildInjectedValue(
|
|
|
109
109
|
tpl.composeWith.field,
|
|
110
110
|
);
|
|
111
111
|
if (!composed) return null;
|
|
112
|
-
const composedValue =
|
|
112
|
+
const composedValue = await getSecureKeyAsync(composed.storageKey);
|
|
113
113
|
if (!composedValue) return null;
|
|
114
114
|
value = `${value}${tpl.composeWith.separator}${composedValue}`;
|
|
115
115
|
}
|
|
@@ -311,10 +311,10 @@ export async function startSession(
|
|
|
311
311
|
if (tpl.injectionType === "header" && tpl.headerName) {
|
|
312
312
|
const resolved = resolveById(credId);
|
|
313
313
|
if (!resolved) return req.headers;
|
|
314
|
-
const value =
|
|
314
|
+
const value = await getSecureKeyAsync(resolved.storageKey);
|
|
315
315
|
if (!value) return req.headers;
|
|
316
316
|
|
|
317
|
-
const headerValue = buildInjectedValue(tpl, value);
|
|
317
|
+
const headerValue = await buildInjectedValue(tpl, value);
|
|
318
318
|
if (!headerValue) {
|
|
319
319
|
log.warn(
|
|
320
320
|
{ host: req.hostname, credentialId: credId },
|
|
@@ -399,11 +399,11 @@ export async function startSession(
|
|
|
399
399
|
const { credentialId, template } = decision;
|
|
400
400
|
const resolved = resolveById(credentialId);
|
|
401
401
|
if (!resolved) return {};
|
|
402
|
-
const value =
|
|
402
|
+
const value = await getSecureKeyAsync(resolved.storageKey);
|
|
403
403
|
if (!value) return {};
|
|
404
404
|
|
|
405
405
|
if (template.injectionType === "header" && template.headerName) {
|
|
406
|
-
const headerValue = buildInjectedValue(template, value);
|
|
406
|
+
const headerValue = await buildInjectedValue(template, value);
|
|
407
407
|
if (!headerValue) {
|
|
408
408
|
log.warn(
|
|
409
409
|
{ hostname, credentialId },
|
|
@@ -115,7 +115,7 @@ export async function executeScheduleCreate(
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
const fireDate = formatLocalDate(job.nextRunAt);
|
|
118
|
-
const integrations = formatIntegrationSummary();
|
|
118
|
+
const integrations = await formatIntegrationSummary();
|
|
119
119
|
return {
|
|
120
120
|
content: [
|
|
121
121
|
`One-shot schedule created successfully.`,
|
|
@@ -197,7 +197,7 @@ export async function executeScheduleCreate(
|
|
|
197
197
|
: describeCronExpression(job.cronExpression);
|
|
198
198
|
|
|
199
199
|
const nextRunDate = formatLocalDate(job.nextRunAt);
|
|
200
|
-
const integrations = formatIntegrationSummary();
|
|
200
|
+
const integrations = await formatIntegrationSummary();
|
|
201
201
|
return {
|
|
202
202
|
content: [
|
|
203
203
|
`Recurring schedule created successfully.`,
|
|
@@ -28,7 +28,7 @@ export interface WatcherProvider {
|
|
|
28
28
|
id: string;
|
|
29
29
|
/** Human-readable name. */
|
|
30
30
|
displayName: string;
|
|
31
|
-
/** Credential service required (e.g. 'integration:
|
|
31
|
+
/** Credential service required (e.g. 'integration:google'). */
|
|
32
32
|
requiredCredentialService: string;
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -130,7 +130,7 @@ export const githubProvider: WatcherProvider = {
|
|
|
130
130
|
_config: Record<string, unknown>,
|
|
131
131
|
_watcherKey: string,
|
|
132
132
|
): Promise<FetchResult> {
|
|
133
|
-
const connection = resolveOAuthConnection(credentialService);
|
|
133
|
+
const connection = await resolveOAuthConnection(credentialService);
|
|
134
134
|
const since = watermark ?? new Date().toISOString();
|
|
135
135
|
const items: WatcherItem[] = [];
|
|
136
136
|
let page = 1;
|