@vellumai/assistant 0.4.11 → 0.4.13

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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Shared types for the runtime HTTP server and its route handlers.
3
3
  */
4
- import type { ChannelId, InterfaceId } from '../channels/types.js';
5
- import type { Session } from '../daemon/session.js';
6
- import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
7
- import type { ApprovalMessageContext, ComposeApprovalMessageGenerativeOptions } from './approval-message-composer.js';
8
- import type { AssistantEventHub } from './assistant-event-hub.js';
4
+ import type { ChannelId, InterfaceId } from "../channels/types.js";
5
+ import type { Session } from "../daemon/session.js";
6
+ import type { GuardianRuntimeContext } from "../daemon/session-runtime-assembly.js";
7
+ import type {
8
+ ApprovalMessageContext,
9
+ ComposeApprovalMessageGenerativeOptions,
10
+ } from "./approval-message-composer.js";
11
+ import type { AssistantEventHub } from "./assistant-event-hub.js";
9
12
  import type {
10
13
  ComposeGuardianActionMessageOptions,
11
14
  GuardianActionMessageContext,
12
- } from './guardian-action-message-composer.js';
15
+ } from "./guardian-action-message-composer.js";
13
16
  /**
14
17
  * Daemon-injected function that generates approval copy using a provider.
15
18
  * Returns generated text or `null` on failure (caller falls back to deterministic text).
@@ -25,10 +28,10 @@ export type ApprovalCopyGenerator = (
25
28
 
26
29
  /** The disposition returned by the approval conversation engine. */
27
30
  export type ApprovalConversationDisposition =
28
- | 'keep_pending'
29
- | 'approve_once'
30
- | 'approve_always'
31
- | 'reject';
31
+ | "keep_pending"
32
+ | "approve_once"
33
+ | "approve_always"
34
+ | "reject";
32
35
 
33
36
  /** Structured result from a single turn of the approval conversation. */
34
37
  export interface ApprovalConversationResult {
@@ -42,7 +45,7 @@ export interface ApprovalConversationResult {
42
45
  export interface ApprovalConversationContext {
43
46
  toolName: string;
44
47
  allowedActions: string[];
45
- role: 'requester' | 'guardian';
48
+ role: "requester" | "guardian";
46
49
  pendingApprovals: Array<{ requestId: string; toolName: string }>;
47
50
  userMessage: string;
48
51
  }
@@ -70,10 +73,10 @@ export type GuardianActionCopyGenerator = (
70
73
 
71
74
  /** The disposition returned by the guardian follow-up conversation engine. */
72
75
  export type GuardianFollowUpDisposition =
73
- | 'call_back'
74
- | 'message_back'
75
- | 'decline'
76
- | 'keep_pending';
76
+ | "call_back"
77
+ | "message_back"
78
+ | "decline"
79
+ | "keep_pending";
77
80
 
78
81
  /** Structured result from a single turn of the guardian follow-up conversation. */
79
82
  export interface GuardianFollowUpTurnResult {
@@ -180,6 +183,8 @@ export interface RuntimeHttpServerOptions {
180
183
  guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator;
181
184
  /** Dependencies for the POST /v1/messages queue-if-busy handler. */
182
185
  sendMessageDeps?: SendMessageDeps;
186
+ /** Lookup an active session by ID (for surface actions). Returns undefined if not found. */
187
+ findSession?: (sessionId: string) => Session | undefined;
183
188
  }
184
189
 
185
190
  export interface RuntimeAttachmentMetadata {
@@ -196,6 +201,11 @@ export interface RuntimeMessagePayload {
196
201
  content: string;
197
202
  timestamp: string;
198
203
  attachments: RuntimeAttachmentMetadata[];
199
- toolCalls?: Array<{ name: string; input: Record<string, unknown>; result?: string; isError?: boolean }>;
204
+ toolCalls?: Array<{
205
+ name: string;
206
+ input: Record<string, unknown>;
207
+ result?: string;
208
+ isError?: boolean;
209
+ }>;
200
210
  interfaces?: string[];
201
211
  }
@@ -1,7 +1,7 @@
1
1
  import { API_KEY_PROVIDERS, getConfig, invalidateConfigCache } from '../../config/loader.js';
2
2
  import { initializeProviders } from '../../providers/registry.js';
3
- import { setSecureKey } from '../../security/secure-keys.js';
4
- import { assertMetadataWritable, upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
3
+ import { deleteSecureKey, setSecureKey } from '../../security/secure-keys.js';
4
+ import { assertMetadataWritable, deleteCredentialMetadata, upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
5
5
  import { getLogger } from '../../util/logger.js';
6
6
  import { httpError } from '../http-errors.js';
7
7
 
@@ -66,3 +66,58 @@ export async function handleAddSecret(req: Request): Promise<Response> {
66
66
  return httpError('INTERNAL_ERROR', message, 500);
67
67
  }
68
68
  }
69
+
70
+ export async function handleDeleteSecret(req: Request): Promise<Response> {
71
+ const body = await req.json() as {
72
+ type?: string;
73
+ name?: string;
74
+ };
75
+
76
+ const { type, name } = body;
77
+
78
+ if (!type || typeof type !== 'string') {
79
+ return httpError('BAD_REQUEST', 'type is required', 400);
80
+ }
81
+ if (!name || typeof name !== 'string') {
82
+ return httpError('BAD_REQUEST', 'name is required', 400);
83
+ }
84
+
85
+ try {
86
+ if (type === 'api_key') {
87
+ if (!API_KEY_PROVIDERS.includes(name as typeof API_KEY_PROVIDERS[number])) {
88
+ return httpError('BAD_REQUEST', `Unknown API key provider: ${name}. Valid providers: ${API_KEY_PROVIDERS.join(', ')}`, 400);
89
+ }
90
+ const deleted = deleteSecureKey(name);
91
+ if (!deleted) {
92
+ return httpError('NOT_FOUND', `API key not found: ${name}`, 404);
93
+ }
94
+ invalidateConfigCache();
95
+ initializeProviders(getConfig());
96
+ log.info({ provider: name }, 'API key deleted via HTTP');
97
+ return Response.json({ success: true, type, name });
98
+ }
99
+
100
+ if (type === 'credential') {
101
+ const colonIdx = name.indexOf(':');
102
+ if (colonIdx < 1 || colonIdx === name.length - 1) {
103
+ return httpError('BAD_REQUEST', 'For credential type, name must be in "service:field" format (e.g. "github:api_token")', 400);
104
+ }
105
+ const service = name.slice(0, colonIdx);
106
+ const field = name.slice(colonIdx + 1);
107
+ const key = `credential:${service}:${field}`;
108
+ const deleted = deleteSecureKey(key);
109
+ if (!deleted) {
110
+ return httpError('NOT_FOUND', `Credential not found: ${name}`, 404);
111
+ }
112
+ deleteCredentialMetadata(service, field);
113
+ log.info({ service, field }, 'Credential deleted via HTTP');
114
+ return Response.json({ success: true, type, name });
115
+ }
116
+
117
+ return httpError('BAD_REQUEST', `Unknown secret type: ${type}. Valid types: api_key, credential`, 400);
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ log.error({ err, type, name }, 'Failed to delete secret via HTTP');
121
+ return httpError('INTERNAL_ERROR', message, 500);
122
+ }
123
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Route handler for surface action operations.
3
+ *
4
+ * POST /v1/surface-actions — dispatch a surface action to an active session.
5
+ * Requires the session to already exist (does not create new sessions).
6
+ */
7
+ import type { Session } from "../../daemon/session.js";
8
+ import { getLogger } from "../../util/logger.js";
9
+ import { httpError } from "../http-errors.js";
10
+
11
+ const log = getLogger("surface-action-routes");
12
+
13
+ export type SessionLookup = (sessionId: string) => Session | undefined;
14
+
15
+ /**
16
+ * POST /v1/surface-actions — handle a UI surface action.
17
+ *
18
+ * Body: { sessionId, surfaceId, actionId, data? }
19
+ */
20
+ export async function handleSurfaceAction(
21
+ req: Request,
22
+ findSession: SessionLookup,
23
+ ): Promise<Response> {
24
+ const body = (await req.json()) as {
25
+ sessionId?: string;
26
+ surfaceId?: string;
27
+ actionId?: string;
28
+ data?: Record<string, unknown>;
29
+ };
30
+
31
+ const { sessionId, surfaceId, actionId, data } = body;
32
+
33
+ if (!sessionId || typeof sessionId !== "string") {
34
+ return httpError("BAD_REQUEST", "sessionId is required", 400);
35
+ }
36
+ if (!surfaceId || typeof surfaceId !== "string") {
37
+ return httpError("BAD_REQUEST", "surfaceId is required", 400);
38
+ }
39
+ if (!actionId || typeof actionId !== "string") {
40
+ return httpError("BAD_REQUEST", "actionId is required", 400);
41
+ }
42
+
43
+ const session = findSession(sessionId);
44
+ if (!session) {
45
+ return httpError(
46
+ "NOT_FOUND",
47
+ "No active session found for this sessionId",
48
+ 404,
49
+ );
50
+ }
51
+
52
+ try {
53
+ session.handleSurfaceAction(surfaceId, actionId, data);
54
+ log.info(
55
+ { sessionId, surfaceId, actionId },
56
+ "Surface action handled via HTTP",
57
+ );
58
+ return Response.json({ ok: true });
59
+ } catch (err) {
60
+ log.error(
61
+ { err, sessionId, surfaceId, actionId },
62
+ "Failed to handle surface action via HTTP",
63
+ );
64
+ return httpError("INTERNAL_ERROR", "Failed to handle surface action", 500);
65
+ }
66
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Route handlers for trust rule CRUD operations.
3
+ *
4
+ * These endpoints manage persistent trust rules independently of
5
+ * the approval-flow trust-rule endpoint in approval-routes.ts.
6
+ * All endpoints are bearer-token authenticated (standard runtime auth).
7
+ */
8
+ import {
9
+ addRule,
10
+ getAllRules,
11
+ removeRule,
12
+ updateRule,
13
+ } from "../../permissions/trust-store.js";
14
+ import { getLogger } from "../../util/logger.js";
15
+ import { httpError } from "../http-errors.js";
16
+
17
+ const log = getLogger("trust-rules-routes");
18
+
19
+ /**
20
+ * GET /v1/trust-rules/manage — list all trust rules.
21
+ */
22
+ export function handleListTrustRules(): Response {
23
+ const rules = getAllRules();
24
+ return Response.json({ type: "trust_rules_list_response", rules });
25
+ }
26
+
27
+ /**
28
+ * POST /v1/trust-rules/manage — add a trust rule (standalone, not approval-flow).
29
+ *
30
+ * Body: { toolName, pattern, scope, decision, allowHighRisk?, executionTarget? }
31
+ */
32
+ export async function handleAddTrustRuleManage(
33
+ req: Request,
34
+ ): Promise<Response> {
35
+ const body = (await req.json()) as {
36
+ toolName?: string;
37
+ pattern?: string;
38
+ scope?: string;
39
+ decision?: string;
40
+ allowHighRisk?: boolean;
41
+ executionTarget?: string;
42
+ };
43
+
44
+ const { toolName, pattern, scope, decision, allowHighRisk, executionTarget } =
45
+ body;
46
+
47
+ if (!toolName || typeof toolName !== "string") {
48
+ return httpError("BAD_REQUEST", "toolName is required", 400);
49
+ }
50
+ if (!pattern || typeof pattern !== "string") {
51
+ return httpError("BAD_REQUEST", "pattern is required", 400);
52
+ }
53
+ if (!scope || typeof scope !== "string") {
54
+ return httpError("BAD_REQUEST", "scope is required", 400);
55
+ }
56
+ const validDecisions = ["allow", "deny", "ask"] as const;
57
+ if (
58
+ !decision ||
59
+ !validDecisions.includes(decision as (typeof validDecisions)[number])
60
+ ) {
61
+ return httpError(
62
+ "BAD_REQUEST",
63
+ "decision must be one of: allow, deny, ask",
64
+ 400,
65
+ );
66
+ }
67
+
68
+ try {
69
+ const hasMetadata = allowHighRisk != null || executionTarget != null;
70
+ addRule(
71
+ toolName,
72
+ pattern,
73
+ scope,
74
+ decision as "allow" | "deny" | "ask",
75
+ undefined,
76
+ hasMetadata ? { allowHighRisk, executionTarget } : undefined,
77
+ );
78
+ log.info(
79
+ { toolName, pattern, scope, decision },
80
+ "Trust rule added via HTTP",
81
+ );
82
+ return Response.json({ ok: true });
83
+ } catch (err) {
84
+ log.error(
85
+ { err, toolName, pattern, scope },
86
+ "Failed to add trust rule via HTTP",
87
+ );
88
+ return httpError("INTERNAL_ERROR", "Failed to add trust rule", 500);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * DELETE /v1/trust-rules/manage/:id — remove a trust rule by ID.
94
+ */
95
+ export function handleRemoveTrustRuleManage(id: string): Response {
96
+ try {
97
+ const removed = removeRule(id);
98
+ if (!removed) {
99
+ return httpError("NOT_FOUND", "Trust rule not found", 404);
100
+ }
101
+ log.info({ id }, "Trust rule removed via HTTP");
102
+ return Response.json({ ok: true });
103
+ } catch (err) {
104
+ log.error({ err, id }, "Failed to remove trust rule via HTTP");
105
+ return httpError("INTERNAL_ERROR", "Failed to remove trust rule", 500);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * PATCH /v1/trust-rules/manage/:id — update fields on an existing trust rule.
111
+ *
112
+ * Body: { tool?, pattern?, scope?, decision?, priority? }
113
+ */
114
+ export async function handleUpdateTrustRuleManage(
115
+ req: Request,
116
+ id: string,
117
+ ): Promise<Response> {
118
+ const body = (await req.json()) as {
119
+ tool?: string;
120
+ pattern?: string;
121
+ scope?: string;
122
+ decision?: string;
123
+ priority?: number;
124
+ };
125
+
126
+ try {
127
+ updateRule(id, {
128
+ tool: body.tool,
129
+ pattern: body.pattern,
130
+ scope: body.scope,
131
+ decision: body.decision as "allow" | "deny" | "ask" | undefined,
132
+ priority: body.priority,
133
+ });
134
+ log.info({ id }, "Trust rule updated via HTTP");
135
+ return Response.json({ ok: true });
136
+ } catch (err) {
137
+ log.error({ err, id }, "Failed to update trust rule via HTTP");
138
+ return httpError("INTERNAL_ERROR", "Failed to update trust rule", 500);
139
+ }
140
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * One-time migration: copies existing macOS keychain items into the
3
+ * encrypted file store so the daemon can stop using the keychain CLI.
4
+ * Runs once on first startup after the change, then skips via a marker key.
5
+ */
6
+
7
+ import { API_KEY_PROVIDERS } from "../config/loader.js";
8
+ import { getLogger } from "../util/logger.js";
9
+ import { isMacOS } from "../util/platform.js";
10
+ import * as encryptedStore from "./encrypted-store.js";
11
+ import * as keychain from "./keychain.js";
12
+
13
+ const log = getLogger("keychain-migration");
14
+ const MIGRATION_MARKER = "keychain-to-encrypted-migrated";
15
+
16
+ /** Known credential keys that the daemon may have stored in the keychain. */
17
+ const CREDENTIAL_KEYS = [
18
+ "credential:twilio:account_sid",
19
+ "credential:twilio:auth_token",
20
+ "credential:twilio:phone_number",
21
+ "credential:twilio:user_phone_number",
22
+ "credential:telegram:bot_token",
23
+ "credential:telegram:webhook_secret",
24
+ "credential:elevenlabs:api_key",
25
+ "credential:integration:gmail:access_token",
26
+ "credential:integration:gmail:refresh_token",
27
+ "credential:integration:twitter:access_token",
28
+ "credential:integration:twitter:refresh_token",
29
+ "credential:integration:slack:access_token",
30
+ "credential:integration:slack:refresh_token",
31
+ ];
32
+
33
+ export function migrateKeychainToEncrypted(): void {
34
+ if (!isMacOS()) return;
35
+ if (encryptedStore.getKey(MIGRATION_MARKER) === "true") return;
36
+
37
+ let migrated = 0;
38
+ const allKeys = [...API_KEY_PROVIDERS, ...CREDENTIAL_KEYS];
39
+
40
+ for (const account of allKeys) {
41
+ try {
42
+ const value = keychain.getKey(account);
43
+ if (value != null && !encryptedStore.getKey(account)) {
44
+ encryptedStore.setKey(account, value);
45
+ migrated++;
46
+ }
47
+ } catch {
48
+ // Keychain unavailable or locked -- skip silently
49
+ }
50
+ }
51
+
52
+ encryptedStore.setKey(MIGRATION_MARKER, "true");
53
+ if (migrated > 0) {
54
+ log.info(
55
+ { count: migrated },
56
+ "Migrated keys from keychain to encrypted store",
57
+ );
58
+ }
59
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { getLogger } from '../util/logger.js';
10
+ import { isMacOS } from '../util/platform.js';
10
11
  import * as encryptedStore from './encrypted-store.js';
11
12
  import * as keychain from './keychain.js';
12
13
 
@@ -20,6 +21,14 @@ let downgradedFromKeychain = false;
20
21
  function getBackend(): Backend {
21
22
  if (resolvedBackend !== undefined) return resolvedBackend;
22
23
 
24
+ // On macOS, skip keychain probing and use encrypted file storage directly
25
+ // to avoid repeated Keychain Access authorization prompts.
26
+ if (isMacOS()) {
27
+ log.debug('macOS detected, using encrypted file storage (skipping keychain)');
28
+ resolvedBackend = 'encrypted';
29
+ return resolvedBackend;
30
+ }
31
+
23
32
  if (keychain.isKeychainAvailable()) {
24
33
  log.debug('Using OS keychain for secure key storage');
25
34
  resolvedBackend = 'keychain';
@@ -33,6 +42,14 @@ function getBackend(): Backend {
33
42
  async function getBackendAsync(): Promise<Backend> {
34
43
  if (resolvedBackend !== undefined) return resolvedBackend;
35
44
 
45
+ // On macOS, skip keychain probing and use encrypted file storage directly
46
+ // to avoid repeated Keychain Access authorization prompts.
47
+ if (isMacOS()) {
48
+ log.debug('macOS detected, using encrypted file storage (skipping keychain)');
49
+ resolvedBackend = 'encrypted';
50
+ return resolvedBackend;
51
+ }
52
+
36
53
  if (await keychain.isKeychainAvailableAsync()) {
37
54
  log.debug('Using OS keychain for secure key storage');
38
55
  resolvedBackend = 'keychain';
@@ -2,8 +2,8 @@
2
2
  * Shared frontmatter parsing for SKILL.md files.
3
3
  *
4
4
  * Frontmatter is a YAML-like block delimited by `---` at the top of a file.
5
- * This module provides a single implementation used by the skill catalog loader,
6
- * the Vellum catalog installer, and the CC command registry.
5
+ * This module provides a single implementation used by the skill catalog loader
6
+ * and the CC command registry.
7
7
  */
8
8
 
9
9
  /** Matches a `---` delimited frontmatter block at the start of a file. */
@@ -25,7 +25,9 @@ export interface FrontmatterParseResult {
25
25
  *
26
26
  * Returns `null` if no frontmatter block is found.
27
27
  */
28
- export function parseFrontmatterFields(content: string): FrontmatterParseResult | null {
28
+ export function parseFrontmatterFields(
29
+ content: string,
30
+ ): FrontmatterParseResult | null {
29
31
  const match = content.match(FRONTMATTER_REGEX);
30
32
  if (!match) return null;
31
33
 
@@ -34,8 +36,8 @@ export function parseFrontmatterFields(content: string): FrontmatterParseResult
34
36
 
35
37
  for (const line of frontmatter.split(/\r?\n/)) {
36
38
  const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith('#')) continue;
38
- const separatorIndex = trimmed.indexOf(':');
39
+ if (!trimmed || trimmed.startsWith("#")) continue;
40
+ const separatorIndex = trimmed.indexOf(":");
39
41
  if (separatorIndex === -1) continue;
40
42
 
41
43
  const key = trimmed.slice(0, separatorIndex).trim();
@@ -50,8 +52,8 @@ export function parseFrontmatterFields(content: string): FrontmatterParseResult
50
52
  // Only for double-quoted values — single-quoted YAML treats backslashes literally.
51
53
  // Single-pass to avoid misinterpreting \\n (escaped backslash + n) as a newline.
52
54
  value = value.replace(/\\(["\\nr])/g, (_, ch) => {
53
- if (ch === 'n') return '\n';
54
- if (ch === 'r') return '\r';
55
+ if (ch === "n") return "\n";
56
+ if (ch === "r") return "\r";
55
57
  return ch; // handles \\ → \ and \" → "
56
58
  });
57
59
  }
@@ -125,7 +125,8 @@ export async function executeAppCreate(
125
125
 
126
126
  // Auto-open the app via the shared open-proxy helper
127
127
  if (autoOpen && proxyToolResolver) {
128
- const extraInput = preview ? { preview } : undefined;
128
+ const createPreview = { ...(preview ?? {}), context: 'app_create' as const };
129
+ const extraInput = { preview: createPreview };
129
130
  const openResultText = await openAppViaSurface(app.id, proxyToolResolver, extraInput);
130
131
 
131
132
  // Determine whether the open succeeded by checking for the fallback text
@@ -6,17 +6,20 @@
6
6
  * so adding/removing tools only requires editing this manifest.
7
7
  */
8
8
 
9
- import { accountManageTool } from './credentials/account-registry.js';
10
- import { credentialStoreTool } from './credentials/vault.js';
11
- import { memorySaveTool, memorySearchTool, memoryUpdateTool } from './memory/register.js';
12
- import type { LazyToolDescriptor } from './registry.js';
13
- import { vellumSkillsCatalogTool } from './skills/vellum-catalog.js';
14
- import { setAvatarTool } from './system/avatar-generator.js';
15
- import { navigateSettingsTabTool } from './system/navigate-settings.js';
16
- import { openSystemSettingsTool } from './system/open-system-settings.js';
17
- import { voiceConfigUpdateTool } from './system/voice-config.js';
18
- import type { Tool } from './types.js';
19
- import { screenWatchTool } from './watch/screen-watch.js';
9
+ import { accountManageTool } from "./credentials/account-registry.js";
10
+ import { credentialStoreTool } from "./credentials/vault.js";
11
+ import {
12
+ memorySaveTool,
13
+ memorySearchTool,
14
+ memoryUpdateTool,
15
+ } from "./memory/register.js";
16
+ import type { LazyToolDescriptor } from "./registry.js";
17
+ import { setAvatarTool } from "./system/avatar-generator.js";
18
+ import { navigateSettingsTabTool } from "./system/navigate-settings.js";
19
+ import { openSystemSettingsTool } from "./system/open-system-settings.js";
20
+ import { voiceConfigUpdateTool } from "./system/voice-config.js";
21
+ import type { Tool } from "./types.js";
22
+ import { screenWatchTool } from "./watch/screen-watch.js";
20
23
 
21
24
  // ── Eager side-effect modules ───────────────────────────────────────
22
25
  // These static imports trigger top-level `registerTool()` side effects.
@@ -27,21 +30,21 @@ import { screenWatchTool } from './watch/screen-watch.js';
27
30
  // filesystem root rather than the module's own directory, causing
28
31
  // "Cannot find module './filesystem/read.js'" crashes in production builds.
29
32
  // Static imports are resolved at bundle time and are always safe.
30
- import './assets/materialize.js';
31
- import './assets/search.js';
32
- import './filesystem/edit.js';
33
- import './filesystem/read.js';
34
- import './filesystem/view-image.js';
35
- import './filesystem/write.js';
36
- import './network/web-fetch.js';
37
- import './network/web-search.js';
38
- import './skills/delete-managed.js';
39
- import './skills/load.js';
40
- import './skills/scaffold-managed.js';
41
- import './swarm/delegate.js';
42
- import './system/request-permission.js';
43
- import './system/version.js';
44
- import './terminal/shell.js';
33
+ import "./assets/materialize.js";
34
+ import "./assets/search.js";
35
+ import "./filesystem/edit.js";
36
+ import "./filesystem/read.js";
37
+ import "./filesystem/view-image.js";
38
+ import "./filesystem/write.js";
39
+ import "./network/web-fetch.js";
40
+ import "./network/web-search.js";
41
+ import "./skills/delete-managed.js";
42
+ import "./skills/load.js";
43
+ import "./skills/scaffold-managed.js";
44
+ import "./swarm/delegate.js";
45
+ import "./system/request-permission.js";
46
+ import "./system/version.js";
47
+ import "./terminal/shell.js";
45
48
 
46
49
  // loadEagerModules is a no-op now that all eager registrations happen via
47
50
  // static imports above. Kept for API compatibility with registry.ts callers.
@@ -54,21 +57,21 @@ export function loadEagerModules(): Promise<void> {
54
57
  // already in the registry before init ran (e.g. when a test file imports
55
58
  // an eager module at the top level).
56
59
  export const eagerModuleToolNames: string[] = [
57
- 'bash',
58
- 'file_read',
59
- 'file_write',
60
- 'file_edit',
61
- 'web_search',
62
- 'web_fetch',
63
- 'skill_load',
64
- 'scaffold_managed_skill',
65
- 'delete_managed_skill',
66
- 'request_system_permission',
67
- 'asset_search',
68
- 'asset_materialize',
69
- 'swarm_delegate',
70
- 'view_image',
71
- 'version',
60
+ "bash",
61
+ "file_read",
62
+ "file_write",
63
+ "file_edit",
64
+ "web_search",
65
+ "web_fetch",
66
+ "skill_load",
67
+ "scaffold_managed_skill",
68
+ "delete_managed_skill",
69
+ "request_system_permission",
70
+ "asset_search",
71
+ "asset_materialize",
72
+ "swarm_delegate",
73
+ "view_image",
74
+ "version",
72
75
  ];
73
76
 
74
77
  // ── Explicit tool instances ─────────────────────────────────────────
@@ -82,7 +85,6 @@ export const explicitTools: Tool[] = [
82
85
  credentialStoreTool,
83
86
  accountManageTool,
84
87
  screenWatchTool,
85
- vellumSkillsCatalogTool,
86
88
  voiceConfigUpdateTool,
87
89
  setAvatarTool,
88
90
  openSystemSettingsTool,
@@ -143,6 +143,8 @@ export interface ToolContext {
143
143
  executionChannel?: string;
144
144
  /** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
145
145
  callSessionId?: string;
146
+ /** True when the tool invocation was triggered by a user clicking a surface action button (not a regular message). */
147
+ triggeredBySurfaceAction?: boolean;
146
148
  /** External user ID of the requester (non-guardian actor). Used for scoped grant consumption. */
147
149
  requesterExternalUserId?: string;
148
150
  /** Chat ID of the requester (non-guardian actor). Used for tool grant request escalation notifications. */
@@ -172,6 +174,13 @@ export interface ToolExecutionResult {
172
174
  * replacement. MUST NOT be emitted in client-facing events or logs.
173
175
  */
174
176
  sensitiveBindings?: SensitiveOutputBinding[];
177
+ /**
178
+ * When true, the agent loop should yield control back to the user after
179
+ * returning this result. Used by interactive surfaces (tables with action
180
+ * buttons, file uploads) to force-stop the loop so the LLM cannot bypass
181
+ * the "wait for user action" instruction.
182
+ */
183
+ yieldToUser?: boolean;
175
184
  }
176
185
 
177
186
  export interface Tool {