@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.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /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
|
|
5
|
-
import type { Session } from
|
|
6
|
-
import type { GuardianRuntimeContext } from
|
|
7
|
-
import type {
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
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:
|
|
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
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
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<{
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
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 ===
|
|
54
|
-
if (ch ===
|
|
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
|
|
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
|
|
10
|
-
import { credentialStoreTool } from
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
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
|
|
31
|
-
import
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
import
|
|
39
|
-
import
|
|
40
|
-
import
|
|
41
|
-
import
|
|
42
|
-
import
|
|
43
|
-
import
|
|
44
|
-
import
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
package/src/tools/types.ts
CHANGED
|
@@ -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 {
|