@vellumai/assistant 0.3.19 → 0.3.20
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +6 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
package/src/tools/skills/load.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isAssistantFeatureFlagEnabled } from '../../config/assistant-feature-flags.js';
|
|
2
|
+
import { getConfig } from '../../config/loader.js';
|
|
3
|
+
import { skillFlagKey } from '../../config/skill-state.js';
|
|
1
4
|
import type { SkillSummary } from '../../config/skills.js';
|
|
2
5
|
import { loadSkillBySelector, loadSkillCatalog } from '../../config/skills.js';
|
|
3
6
|
import { RiskLevel } from '../../permissions/types.js';
|
|
@@ -46,6 +49,15 @@ export class SkillLoadTool implements Tool {
|
|
|
46
49
|
|
|
47
50
|
const skill = loaded.skill;
|
|
48
51
|
|
|
52
|
+
// Assistant feature flag gate: reject loading if the skill's flag is OFF
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(skill.id), config)) {
|
|
55
|
+
return {
|
|
56
|
+
content: `Error: skill "${skill.id}" is currently unavailable (disabled by feature flag)`,
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
// Load catalog for include validation and child metadata output
|
|
50
62
|
let catalogIndex: Map<string, SkillSummary> | undefined;
|
|
51
63
|
if (skill.includes && skill.includes.length > 0) {
|
|
@@ -83,14 +95,15 @@ export class SkillLoadTool implements Tool {
|
|
|
83
95
|
const childLines: string[] = [];
|
|
84
96
|
for (const childId of skill.includes) {
|
|
85
97
|
const child = catalogIndex.get(childId);
|
|
86
|
-
if (child)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
if (!child) continue;
|
|
99
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(childId), config)) continue;
|
|
100
|
+
|
|
101
|
+
childLines.push(` - ${child.id}: ${child.name} — ${child.description} (${child.skillFilePath})`);
|
|
102
|
+
|
|
103
|
+
// Load the included skill's body content
|
|
104
|
+
const childLoaded = loadSkillBySelector(childId);
|
|
105
|
+
if (childLoaded.skill && childLoaded.skill.body.length > 0) {
|
|
106
|
+
includedBodies.push(`--- Included Skill: ${childLoaded.skill.name} (${childId}) ---\n${childLoaded.skill.body}`);
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
immediateChildrenSection = `Included Skills (immediate):\n${childLines.join('\n')}`;
|
|
@@ -113,6 +126,7 @@ export class SkillLoadTool implements Tool {
|
|
|
113
126
|
for (const childId of skill.includes) {
|
|
114
127
|
const child = catalogIndex.get(childId);
|
|
115
128
|
if (!child) continue;
|
|
129
|
+
if (!isAssistantFeatureFlagEnabled(skillFlagKey(childId), config)) continue;
|
|
116
130
|
let childHash: string | undefined;
|
|
117
131
|
try {
|
|
118
132
|
childHash = computeSkillVersionHash(child.directoryPath);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getConfig } from '../../config/loader.js';
|
|
5
|
+
import { generateImage, mapGeminiError } from '../../media/gemini-image-service.js';
|
|
6
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
7
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
8
|
+
import { getLogger } from '../../util/logger.js';
|
|
9
|
+
import { getWorkspaceDir } from '../../util/platform.js';
|
|
10
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
11
|
+
|
|
12
|
+
const log = getLogger('avatar-generator');
|
|
13
|
+
|
|
14
|
+
const TOOL_NAME = 'set_avatar';
|
|
15
|
+
|
|
16
|
+
/** Canonical path where the custom avatar PNG is stored. */
|
|
17
|
+
function getAvatarPath(): string {
|
|
18
|
+
return join(getWorkspaceDir(), 'data', 'avatar', 'custom-avatar.png');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const setAvatarTool: Tool = {
|
|
22
|
+
name: TOOL_NAME,
|
|
23
|
+
description:
|
|
24
|
+
'Generate a custom avatar image from a text description. ' +
|
|
25
|
+
'Saves the result as the assistant\'s avatar.',
|
|
26
|
+
category: 'system',
|
|
27
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
28
|
+
|
|
29
|
+
getDefinition(): ToolDefinition {
|
|
30
|
+
return {
|
|
31
|
+
name: TOOL_NAME,
|
|
32
|
+
description: this.description,
|
|
33
|
+
input_schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
description: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description:
|
|
39
|
+
'A text description of the desired avatar appearance, ' +
|
|
40
|
+
'e.g. "a friendly purple cat with green eyes wearing a tiny hat".',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['description'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async execute(
|
|
49
|
+
input: Record<string, unknown>,
|
|
50
|
+
_context: ToolContext,
|
|
51
|
+
): Promise<ToolExecutionResult> {
|
|
52
|
+
const description = input.description;
|
|
53
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
54
|
+
return {
|
|
55
|
+
content: 'Error: description is required and must be a non-empty string.',
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const config = getConfig();
|
|
61
|
+
const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
return {
|
|
64
|
+
content: 'No Gemini API key configured. Please add your Gemini API key in Settings → Models & Services, or set the GEMINI_API_KEY environment variable.',
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
log.info({ description: description.trim() }, 'Generating avatar via Gemini');
|
|
71
|
+
|
|
72
|
+
const prompt =
|
|
73
|
+
`Create an avatar image based on this description: ${description.trim()}\n\n` +
|
|
74
|
+
'Style: cute, friendly, work-safe illustration. ' +
|
|
75
|
+
'Vibrant but soft colors. Simple and recognizable at small sizes (28px). ' +
|
|
76
|
+
'Circular or rounded composition filling the canvas. ' +
|
|
77
|
+
'Subtle background color (not white or transparent).';
|
|
78
|
+
|
|
79
|
+
const result = await generateImage(apiKey, {
|
|
80
|
+
prompt,
|
|
81
|
+
mode: 'generate',
|
|
82
|
+
model: config.imageGenModel,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (result.images.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: 'Error: Gemini returned no image data. Please try again.',
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const image = result.images[0];
|
|
93
|
+
const pngBuffer = Buffer.from(image.dataBase64, 'base64');
|
|
94
|
+
|
|
95
|
+
const avatarPath = getAvatarPath();
|
|
96
|
+
const avatarDir = dirname(avatarPath);
|
|
97
|
+
|
|
98
|
+
mkdirSync(avatarDir, { recursive: true });
|
|
99
|
+
writeFileSync(avatarPath, pngBuffer);
|
|
100
|
+
|
|
101
|
+
log.info({ avatarPath }, 'Avatar saved successfully');
|
|
102
|
+
|
|
103
|
+
// Side-effect hook in tool-side-effects.ts broadcasts avatar_updated to all clients.
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content: 'Avatar updated! Your new avatar will appear shortly.',
|
|
107
|
+
isError: false,
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = mapGeminiError(error);
|
|
111
|
+
log.error({ error: message }, 'Avatar generation failed');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: `Avatar generation failed: ${message}`,
|
|
115
|
+
isError: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
2
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const SETTINGS_TABS = [
|
|
6
|
+
'Voice',
|
|
7
|
+
'Connect',
|
|
8
|
+
'Trust',
|
|
9
|
+
'Model',
|
|
10
|
+
'Scheduling',
|
|
11
|
+
'Parental',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type SettingsTab = (typeof SETTINGS_TABS)[number];
|
|
15
|
+
|
|
16
|
+
export class NavigateSettingsTabTool implements Tool {
|
|
17
|
+
name = 'navigate_settings_tab';
|
|
18
|
+
description =
|
|
19
|
+
'Open the Vellum settings panel to a specific tab (e.g. Voice, Connect, Trust). ' +
|
|
20
|
+
'Use this when the user needs to review or adjust settings visually.';
|
|
21
|
+
category = 'system';
|
|
22
|
+
defaultRiskLevel = RiskLevel.Low;
|
|
23
|
+
|
|
24
|
+
getDefinition(): ToolDefinition {
|
|
25
|
+
return {
|
|
26
|
+
name: this.name,
|
|
27
|
+
description: this.description,
|
|
28
|
+
input_schema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
tab: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: [...SETTINGS_TABS],
|
|
34
|
+
description: 'The settings tab to navigate to',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['tab'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
43
|
+
const tab = input.tab as string;
|
|
44
|
+
if (!SETTINGS_TABS.includes(tab as SettingsTab)) {
|
|
45
|
+
return {
|
|
46
|
+
content: `Error: unknown tab "${tab}". Valid tabs: ${SETTINGS_TABS.join(', ')}`,
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (context.sendToClient) {
|
|
52
|
+
context.sendToClient({
|
|
53
|
+
type: 'navigate_settings',
|
|
54
|
+
tab,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: `Opened settings to the ${tab} tab.`,
|
|
60
|
+
isError: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const navigateSettingsTabTool = new NavigateSettingsTabTool();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
2
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const PANES = {
|
|
6
|
+
microphone: {
|
|
7
|
+
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
|
|
8
|
+
label: 'Microphone privacy',
|
|
9
|
+
instruction: 'Please toggle Vellum Assistant on.',
|
|
10
|
+
},
|
|
11
|
+
speech_recognition: {
|
|
12
|
+
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition',
|
|
13
|
+
label: 'Speech Recognition privacy',
|
|
14
|
+
instruction: 'Please toggle Vellum Assistant on.',
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type PaneName = keyof typeof PANES;
|
|
19
|
+
|
|
20
|
+
const VALID_PANES = Object.keys(PANES) as PaneName[];
|
|
21
|
+
|
|
22
|
+
export class OpenSystemSettingsTool implements Tool {
|
|
23
|
+
name = 'open_system_settings';
|
|
24
|
+
description =
|
|
25
|
+
'Open a specific macOS System Settings pane (e.g. Microphone or Speech Recognition privacy). ' +
|
|
26
|
+
'Use this to guide the user through granting permissions that can only be toggled in System Settings.';
|
|
27
|
+
category = 'system';
|
|
28
|
+
defaultRiskLevel = RiskLevel.Low;
|
|
29
|
+
|
|
30
|
+
getDefinition(): ToolDefinition {
|
|
31
|
+
return {
|
|
32
|
+
name: this.name,
|
|
33
|
+
description: this.description,
|
|
34
|
+
input_schema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
pane: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: [...VALID_PANES],
|
|
40
|
+
description: 'The System Settings pane to open',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['pane'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
49
|
+
const pane = input.pane as string;
|
|
50
|
+
if (!VALID_PANES.includes(pane as PaneName)) {
|
|
51
|
+
return {
|
|
52
|
+
content: `Error: unknown pane "${pane}". Valid panes: ${VALID_PANES.join(', ')}`,
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const meta = PANES[pane as PaneName];
|
|
58
|
+
|
|
59
|
+
// Send open_url IPC to the client — the x-apple.systempreferences: scheme
|
|
60
|
+
// opens System Settings directly without a browser confirmation dialog.
|
|
61
|
+
if (context.sendToClient) {
|
|
62
|
+
context.sendToClient({
|
|
63
|
+
type: 'open_url',
|
|
64
|
+
url: meta.url,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: `Opened System Settings to ${meta.label}. ${meta.instruction}`,
|
|
70
|
+
isError: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const openSystemSettingsTool = new OpenSystemSettingsTool();
|
|
@@ -3,60 +3,149 @@ import { RiskLevel } from '../../permissions/types.js';
|
|
|
3
3
|
import type { ToolDefinition } from '../../providers/types.js';
|
|
4
4
|
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Valid voice config settings and their UserDefaults key mappings.
|
|
8
|
+
*/
|
|
9
|
+
const VOICE_SETTINGS = {
|
|
10
|
+
activation_key: { userDefaultsKey: 'pttActivationKey', type: 'string' as const },
|
|
11
|
+
wake_word_enabled: { userDefaultsKey: 'wakeWordEnabled', type: 'boolean' as const },
|
|
12
|
+
wake_word_keyword: { userDefaultsKey: 'wakeWordKeyword', type: 'string' as const },
|
|
13
|
+
wake_word_timeout: { userDefaultsKey: 'wakeWordTimeoutSeconds', type: 'number' as const },
|
|
14
|
+
} as const;
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
type VoiceSettingName = keyof typeof VOICE_SETTINGS;
|
|
17
|
+
|
|
18
|
+
const VALID_SETTINGS = Object.keys(VOICE_SETTINGS) as VoiceSettingName[];
|
|
19
|
+
|
|
20
|
+
const VALID_TIMEOUTS = [5, 10, 15, 30, 60];
|
|
21
|
+
|
|
22
|
+
function validateSetting(setting: string, value: unknown): { ok: true; coerced: string | boolean | number } | { ok: false; error: string } {
|
|
23
|
+
if (!VALID_SETTINGS.includes(setting as VoiceSettingName)) {
|
|
24
|
+
return { ok: false, error: `Unknown setting "${setting}". Valid settings: ${VALID_SETTINGS.join(', ')}` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (setting) {
|
|
28
|
+
case 'activation_key': {
|
|
29
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
30
|
+
return { ok: false, error: 'activation_key must be a non-empty string' };
|
|
31
|
+
}
|
|
32
|
+
// Use the canonical normalizer from config-voice handler
|
|
33
|
+
const result = normalizeActivationKey(value);
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
return { ok: false, error: result.reason };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, coerced: result.value };
|
|
38
|
+
}
|
|
39
|
+
case 'wake_word_enabled': {
|
|
40
|
+
if (typeof value === 'boolean') return { ok: true, coerced: value };
|
|
41
|
+
if (value === 'true') return { ok: true, coerced: true };
|
|
42
|
+
if (value === 'false') return { ok: true, coerced: false };
|
|
43
|
+
return { ok: false, error: 'wake_word_enabled must be a boolean (or "true"/"false" string)' };
|
|
44
|
+
}
|
|
45
|
+
case 'wake_word_keyword': {
|
|
46
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
47
|
+
return { ok: false, error: 'wake_word_keyword must be a non-empty string' };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, coerced: value.trim() };
|
|
50
|
+
}
|
|
51
|
+
case 'wake_word_timeout': {
|
|
52
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
53
|
+
if (Number.isNaN(num) || !VALID_TIMEOUTS.includes(num)) {
|
|
54
|
+
return { ok: false, error: `wake_word_timeout must be one of: ${VALID_TIMEOUTS.join(', ')}` };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, coerced: num };
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return { ok: false, error: `Unknown setting "${setting}"` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const FRIENDLY_NAMES: Record<VoiceSettingName, string> = {
|
|
64
|
+
activation_key: 'PTT activation key',
|
|
65
|
+
wake_word_enabled: 'Wake word',
|
|
66
|
+
wake_word_keyword: 'Wake word keyword',
|
|
67
|
+
wake_word_timeout: 'Wake word timeout',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class VoiceConfigUpdateTool implements Tool {
|
|
71
|
+
name = 'voice_config_update';
|
|
72
|
+
description =
|
|
73
|
+
'Update a voice configuration setting (PTT activation key, wake word enabled/keyword/timeout). ' +
|
|
74
|
+
'Changes take effect immediately via IPC broadcast to the desktop client.';
|
|
75
|
+
category = 'system';
|
|
76
|
+
defaultRiskLevel = RiskLevel.Low;
|
|
14
77
|
|
|
15
78
|
getDefinition(): ToolDefinition {
|
|
16
79
|
return {
|
|
17
|
-
name:
|
|
80
|
+
name: this.name,
|
|
18
81
|
description: this.description,
|
|
19
82
|
input_schema: {
|
|
20
83
|
type: 'object',
|
|
21
84
|
properties: {
|
|
85
|
+
setting: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
enum: [...VALID_SETTINGS],
|
|
88
|
+
description: 'The voice setting to change',
|
|
89
|
+
},
|
|
90
|
+
value: {
|
|
91
|
+
description: 'The new value for the setting (type depends on setting)',
|
|
92
|
+
},
|
|
93
|
+
// Backward compat: legacy schema used activation_key directly
|
|
22
94
|
activation_key: {
|
|
23
95
|
type: 'string',
|
|
24
|
-
description:
|
|
25
|
-
'The activation key to set. Accepts enum values (fn, ctrl, fn_shift, none) or natural language (e.g. "Control", "Fn+Shift", "Off").',
|
|
96
|
+
description: 'Deprecated — use setting: "activation_key" with value instead',
|
|
26
97
|
},
|
|
27
98
|
},
|
|
28
|
-
required: ['activation_key'],
|
|
29
99
|
},
|
|
30
100
|
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async execute(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
104
|
+
// Backward compat: if activation_key is provided without setting/value, treat as setting: "activation_key"
|
|
105
|
+
let setting = input.setting as string | undefined;
|
|
106
|
+
let value = input.value;
|
|
107
|
+
|
|
108
|
+
if (!setting && typeof input.activation_key === 'string') {
|
|
109
|
+
setting = 'activation_key';
|
|
110
|
+
value = input.activation_key;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!setting) {
|
|
114
|
+
return {
|
|
115
|
+
content: `Error: "setting" is required. Valid settings: ${VALID_SETTINGS.join(', ')}`,
|
|
116
|
+
isError: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (value === undefined) {
|
|
39
121
|
return {
|
|
40
|
-
content:
|
|
122
|
+
content: `Error: "value" is required for setting "${setting}".`,
|
|
41
123
|
isError: true,
|
|
42
124
|
};
|
|
43
125
|
}
|
|
44
126
|
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
47
|
-
return { content:
|
|
127
|
+
const validation = validateSetting(setting, value);
|
|
128
|
+
if (!validation.ok) {
|
|
129
|
+
return { content: `Error: ${validation.error}`, isError: true };
|
|
48
130
|
}
|
|
49
131
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
132
|
+
const meta = VOICE_SETTINGS[setting as VoiceSettingName];
|
|
133
|
+
const friendlyName = FRIENDLY_NAMES[setting as VoiceSettingName];
|
|
134
|
+
|
|
135
|
+
// Send client_settings_update IPC to write to UserDefaults
|
|
136
|
+
if (context.sendToClient) {
|
|
137
|
+
context.sendToClient({
|
|
138
|
+
type: 'client_settings_update',
|
|
139
|
+
key: meta.userDefaultsKey,
|
|
140
|
+
value: validation.coerced,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
56
143
|
|
|
57
144
|
return {
|
|
58
|
-
content:
|
|
145
|
+
content: `${friendlyName} updated to ${JSON.stringify(validation.coerced)}. The change has been broadcast to the desktop client.`,
|
|
59
146
|
isError: false,
|
|
60
147
|
};
|
|
61
|
-
}
|
|
62
|
-
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const voiceConfigUpdateTool = new VoiceConfigUpdateTool();
|
|
@@ -6,24 +6,31 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { ToolError } from '../../../util/errors.js';
|
|
7
7
|
import { getLogger } from '../../../util/logger.js';
|
|
8
8
|
import { isLinux,isMacOS } from '../../../util/platform.js';
|
|
9
|
-
import type { SandboxBackend, SandboxResult } from './types.js';
|
|
9
|
+
import type { SandboxBackend, SandboxResult, WrapOptions } from './types.js';
|
|
10
10
|
|
|
11
11
|
const log = getLogger('sandbox');
|
|
12
12
|
|
|
13
13
|
const HASH_DISPLAY_LENGTH = 12;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* macOS sandbox-exec profile
|
|
16
|
+
* Build a macOS sandbox-exec SBPL profile.
|
|
17
|
+
*
|
|
18
|
+
* The profile restricts shell commands:
|
|
17
19
|
* - Denies all by default
|
|
18
20
|
* - Allows read access to most of the filesystem (needed for toolchains)
|
|
19
21
|
* - Allows write access only to the working directory and temp dirs
|
|
20
|
-
* - Blocks outbound network access
|
|
22
|
+
* - Blocks outbound network access (unless proxied)
|
|
21
23
|
* - Blocks process debugging (ptrace)
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* When `allowNetwork` is true the `(deny network*)` rule is replaced with
|
|
26
|
+
* `(allow network*)` so the process can reach the local credential proxy.
|
|
25
27
|
*/
|
|
26
|
-
|
|
28
|
+
function buildSandboxProfile(allowNetwork: boolean): string {
|
|
29
|
+
const networkRule = allowNetwork
|
|
30
|
+
? ';; Allow network access (proxied mode — needed to reach the credential proxy)\n(allow network*)'
|
|
31
|
+
: ';; Block network access\n(deny network*)';
|
|
32
|
+
|
|
33
|
+
return `
|
|
27
34
|
(version 1)
|
|
28
35
|
(deny default)
|
|
29
36
|
|
|
@@ -54,12 +61,12 @@ const SANDBOX_PROFILE = `
|
|
|
54
61
|
;; Allow IOKit (needed for some system calls)
|
|
55
62
|
(allow iokit-open)
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
(deny network*)
|
|
64
|
+
${networkRule}
|
|
59
65
|
|
|
60
66
|
;; Block process debugging
|
|
61
67
|
(deny process-info-pidinfo (target others))
|
|
62
68
|
`.trim();
|
|
69
|
+
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* Escape a path for safe embedding inside an SBPL quoted string.
|
|
@@ -83,15 +90,18 @@ function escapeSBPL(path: string): string {
|
|
|
83
90
|
* a hash of the path) to avoid race conditions when concurrent commands
|
|
84
91
|
* use different working directories.
|
|
85
92
|
*/
|
|
86
|
-
function getProfilePath(workingDir: string): string {
|
|
93
|
+
function getProfilePath(workingDir: string, allowNetwork: boolean): string {
|
|
87
94
|
const dir = join(process.env.HOME ?? '/tmp', '.vellum');
|
|
88
95
|
if (!existsSync(dir)) {
|
|
89
96
|
mkdirSync(dir, { recursive: true });
|
|
90
97
|
}
|
|
91
|
-
|
|
98
|
+
// Include the network flag in the hash so proxied and non-proxied profiles
|
|
99
|
+
// for the same directory don't collide.
|
|
100
|
+
const hashInput = allowNetwork ? `${workingDir}:proxied` : workingDir;
|
|
101
|
+
const hash = createHash('sha256').update(hashInput).digest('hex').slice(0, HASH_DISPLAY_LENGTH);
|
|
92
102
|
const path = join(dir, `sandbox-profile-${hash}.sb`);
|
|
93
103
|
|
|
94
|
-
const profile =
|
|
104
|
+
const profile = buildSandboxProfile(allowNetwork).replace(/__WORKING_DIR__/g, () => escapeSBPL(workingDir));
|
|
95
105
|
writeFileSync(path, profile + '\n');
|
|
96
106
|
return path;
|
|
97
107
|
}
|
|
@@ -137,20 +147,29 @@ function isBwrapAvailable(): boolean {
|
|
|
137
147
|
* - Network access blocked (--unshare-net)
|
|
138
148
|
* - PID namespace isolated (--unshare-pid)
|
|
139
149
|
*/
|
|
140
|
-
function buildBwrapArgs(workingDir: string, command: string): string[] {
|
|
141
|
-
|
|
150
|
+
function buildBwrapArgs(workingDir: string, command: string, allowNetwork: boolean): string[] {
|
|
151
|
+
const args = [
|
|
142
152
|
// Filesystem: read-only root, writable working dir and temp
|
|
143
153
|
'--ro-bind', '/', '/',
|
|
144
154
|
'--bind', workingDir, workingDir,
|
|
145
155
|
'--bind', '/tmp', '/tmp',
|
|
146
156
|
'--dev', '/dev',
|
|
147
157
|
'--proc', '/proc',
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
// Only isolate the network namespace when network access is not needed.
|
|
161
|
+
// In proxied mode the process must be able to reach 127.0.0.1:<proxy-port>.
|
|
162
|
+
if (!allowNetwork) {
|
|
163
|
+
args.push('--unshare-net');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
args.push(
|
|
150
167
|
'--unshare-pid',
|
|
151
168
|
// Run bash inside the sandbox
|
|
152
169
|
'bash', '-c', '--', command,
|
|
153
|
-
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return args;
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
/**
|
|
@@ -158,9 +177,11 @@ function buildBwrapArgs(workingDir: string, command: string): string[] {
|
|
|
158
177
|
* macOS sandbox-exec (SBPL profiles) and Linux bwrap (bubblewrap).
|
|
159
178
|
*/
|
|
160
179
|
export class NativeBackend implements SandboxBackend {
|
|
161
|
-
wrap(command: string, workingDir: string,
|
|
180
|
+
wrap(command: string, workingDir: string, options?: WrapOptions): SandboxResult {
|
|
181
|
+
const allowNetwork = options?.networkMode === 'proxied';
|
|
182
|
+
|
|
162
183
|
if (isMacOS()) {
|
|
163
|
-
const profile = getProfilePath(workingDir);
|
|
184
|
+
const profile = getProfilePath(workingDir, allowNetwork);
|
|
164
185
|
return {
|
|
165
186
|
command: 'sandbox-exec',
|
|
166
187
|
args: ['-f', profile, 'bash', '-c', '--', command],
|
|
@@ -176,7 +197,7 @@ export class NativeBackend implements SandboxBackend {
|
|
|
176
197
|
}
|
|
177
198
|
return {
|
|
178
199
|
command: 'bwrap',
|
|
179
|
-
args: buildBwrapArgs(workingDir, command),
|
|
200
|
+
args: buildBwrapArgs(workingDir, command, allowNetwork),
|
|
180
201
|
sandboxed: true,
|
|
181
202
|
};
|
|
182
203
|
}
|
|
@@ -10,15 +10,15 @@ export interface SandboxResult {
|
|
|
10
10
|
export interface WrapOptions {
|
|
11
11
|
/**
|
|
12
12
|
* Network mode for this invocation.
|
|
13
|
-
* - 'off':
|
|
14
|
-
* - 'proxied':
|
|
13
|
+
* - 'off': network access is blocked (sandbox-exec deny network / bwrap --unshare-net). This is the default.
|
|
14
|
+
* - 'proxied': network access is allowed so the process can reach the local credential proxy.
|
|
15
15
|
*/
|
|
16
16
|
networkMode?: 'off' | 'proxied';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* A sandbox backend knows how to wrap a shell command so it runs
|
|
21
|
-
* inside an OS-level sandbox (macOS sandbox-exec, Linux bwrap,
|
|
21
|
+
* inside an OS-level sandbox (macOS sandbox-exec, Linux bwrap, etc.).
|
|
22
22
|
*/
|
|
23
23
|
export interface SandboxBackend {
|
|
24
24
|
/** Wrap a command for sandboxed execution in the given working directory. */
|
|
@@ -109,7 +109,7 @@ const initGuard = new PromiseGuard<void>();
|
|
|
109
109
|
* don't exist — fall back to:
|
|
110
110
|
* 1. `../Resources/<file>` (macOS .app bundle layout)
|
|
111
111
|
* 2. Next to the compiled binary (process.execPath)
|
|
112
|
-
* This matches the pattern used
|
|
112
|
+
* This matches the pattern used for compiled Bun binary asset resolution.
|
|
113
113
|
*/
|
|
114
114
|
function findWasmPath(pkg: string, file: string): string {
|
|
115
115
|
const dir = import.meta.dirname ?? __dirname;
|