@vellumai/assistant 0.3.18 → 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 +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -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 +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -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 +779 -0
- 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 +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- 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 +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -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 +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- 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__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- 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 +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- 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 +18 -0
- 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 +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- 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/config-channels.ts +18 -0
- 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/handlers/skills.ts +45 -2
- 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/skills.ts +1 -0
- 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 +260 -422
- 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 +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- 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/permissions/checker.ts +27 -0
- 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 +154 -0
- 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 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- 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 +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- 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
|
@@ -10,6 +10,11 @@ import { checkBrowserRuntime } from './runtime-check.js';
|
|
|
10
10
|
|
|
11
11
|
const log = getLogger('browser-manager');
|
|
12
12
|
|
|
13
|
+
// Screencast capture dimensions — used by coordinate math across the browser module
|
|
14
|
+
// to map between page coordinates and screencast-frame coordinates.
|
|
15
|
+
export const SCREENCAST_WIDTH = 800;
|
|
16
|
+
export const SCREENCAST_HEIGHT = 600;
|
|
17
|
+
|
|
13
18
|
function getDownloadsDir(): string {
|
|
14
19
|
const dir = join(getDataDir(), 'browser-downloads');
|
|
15
20
|
mkdirSync(dir, { recursive: true });
|
|
@@ -20,6 +25,7 @@ export type DownloadInfo = { path: string; filename: string };
|
|
|
20
25
|
|
|
21
26
|
type BrowserContext = {
|
|
22
27
|
newPage(): Promise<Page>;
|
|
28
|
+
pages?(): Page[];
|
|
23
29
|
close(): Promise<void>;
|
|
24
30
|
};
|
|
25
31
|
|
|
@@ -253,32 +259,11 @@ class BrowserManager {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
const pw2 = await import('playwright');
|
|
263
|
-
const headedBrowser = await pw2.chromium.launch({
|
|
264
|
-
channel: 'chrome',
|
|
265
|
-
headless: false,
|
|
266
|
-
args: [
|
|
267
|
-
'--window-position=-32000,-32000',
|
|
268
|
-
'--window-size=1,1',
|
|
269
|
-
'--disable-blink-features=AutomationControlled',
|
|
270
|
-
],
|
|
271
|
-
});
|
|
272
|
-
const ctx = headedBrowser.contexts()[0] || await headedBrowser.newContext();
|
|
273
|
-
this.cdpBrowser = headedBrowser as unknown as typeof this.cdpBrowser;
|
|
274
|
-
this._browserLaunched = true;
|
|
275
|
-
this.setBrowserMode('cdp');
|
|
276
|
-
await this.initBrowserCdpSession();
|
|
277
|
-
log.info('Launched headed Chromium (minimized) for interactive handoff support');
|
|
278
|
-
return ctx as unknown as BrowserContext;
|
|
279
|
-
} catch (err2) {
|
|
280
|
-
log.warn({ err: err2 }, 'Headed Chromium launch failed, falling back to headless');
|
|
281
|
-
}
|
|
262
|
+
if (invokingSessionId && this.sessionSenders.get(invokingSessionId) && this._browserMode === 'headless') {
|
|
263
|
+
log.info(
|
|
264
|
+
{ sessionId: invokingSessionId },
|
|
265
|
+
'CDP unavailable/declined; staying in headless mode (no visible browser window will be auto-launched)',
|
|
266
|
+
);
|
|
282
267
|
}
|
|
283
268
|
}
|
|
284
269
|
|
|
@@ -380,7 +365,26 @@ class BrowserManager {
|
|
|
380
365
|
this.snapshotMaps.delete(sessionId);
|
|
381
366
|
await this.stopScreencast(sessionId);
|
|
382
367
|
|
|
383
|
-
|
|
368
|
+
let page: Page | undefined;
|
|
369
|
+
|
|
370
|
+
// In connectOverCDP mode, Chrome often starts with a pre-opened blank tab.
|
|
371
|
+
// Only reuse blank/new-tab pages to avoid hijacking active user tabs, which
|
|
372
|
+
// could cause user-visible disruption or data loss when the session closes.
|
|
373
|
+
if (this._browserMode === 'cdp' && !this._browserLaunched && typeof context.pages === 'function') {
|
|
374
|
+
const BLANK_TAB_URLS = new Set(['about:blank', 'chrome://newtab/', 'chrome://new-tab-page/']);
|
|
375
|
+
const claimedPages = new Set(this.pages.values());
|
|
376
|
+
const reusable = context.pages().find((p) => {
|
|
377
|
+
if (p.isClosed() || claimedPages.has(p)) return false;
|
|
378
|
+
const url = p.url();
|
|
379
|
+
return BLANK_TAB_URLS.has(url) || url === '';
|
|
380
|
+
});
|
|
381
|
+
if (reusable) {
|
|
382
|
+
page = reusable;
|
|
383
|
+
log.debug({ sessionId, url: reusable.url() }, 'Reusing blank CDP tab instead of creating a new page');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
page ??= await context.newPage();
|
|
384
388
|
this.pages.set(sessionId, page);
|
|
385
389
|
this.rawPages.set(sessionId, page);
|
|
386
390
|
|
|
@@ -509,17 +513,27 @@ class BrowserManager {
|
|
|
509
513
|
this.cdpSessions.set(sessionId, cdp);
|
|
510
514
|
this.screencastCallbacks.set(sessionId, onFrame);
|
|
511
515
|
|
|
516
|
+
// Keep screencast intentionally low-frequency to avoid Chrome renderer /
|
|
517
|
+
// WindowServer spikes while users type in interactive auth flows.
|
|
518
|
+
const MIN_FRAME_INTERVAL_MS = 1000;
|
|
519
|
+
let lastFrameTime = 0;
|
|
520
|
+
|
|
512
521
|
cdp.on('Page.screencastFrame', (params) => {
|
|
513
|
-
|
|
522
|
+
const now = Date.now();
|
|
523
|
+
if (now - lastFrameTime >= MIN_FRAME_INTERVAL_MS) {
|
|
524
|
+
lastFrameTime = now;
|
|
525
|
+
onFrame({ data: params.data as string, metadata: params.metadata as ScreencastFrameMetadata });
|
|
526
|
+
}
|
|
527
|
+
// Always ack so CDP continues delivering frames (otherwise it stalls)
|
|
514
528
|
silentlyWithLog(cdp.send('Page.screencastFrameAck', { sessionId: params.sessionId }), 'screencast frame ack');
|
|
515
529
|
});
|
|
516
530
|
|
|
517
531
|
await cdp.send('Page.startScreencast', {
|
|
518
532
|
format: 'jpeg',
|
|
519
|
-
quality:
|
|
520
|
-
maxWidth:
|
|
521
|
-
maxHeight:
|
|
522
|
-
everyNthFrame:
|
|
533
|
+
quality: 30,
|
|
534
|
+
maxWidth: SCREENCAST_WIDTH,
|
|
535
|
+
maxHeight: SCREENCAST_HEIGHT,
|
|
536
|
+
everyNthFrame: 4,
|
|
523
537
|
});
|
|
524
538
|
}
|
|
525
539
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid';
|
|
2
2
|
|
|
3
3
|
import type { BrowserFrame, BrowserViewSurfaceData, ServerMessage } from '../../daemon/ipc-contract.js';
|
|
4
|
-
import { browserManager } from './browser-manager.js';
|
|
4
|
+
import { browserManager, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from './browser-manager.js';
|
|
5
5
|
|
|
6
6
|
// Track active screencast sessions
|
|
7
7
|
const activeScreencasts = new Map<string, { surfaceId: string }>();
|
|
@@ -161,7 +161,7 @@ export async function getElementBounds(
|
|
|
161
161
|
})()
|
|
162
162
|
`) as { x: number; y: number; w: number; h: number; vw: number; vh: number } | null;
|
|
163
163
|
if (!result) return null;
|
|
164
|
-
const scale = Math.min(
|
|
164
|
+
const scale = Math.min(SCREENCAST_WIDTH / result.vw, SCREENCAST_HEIGHT / result.vh);
|
|
165
165
|
return {
|
|
166
166
|
x: result.x * scale,
|
|
167
167
|
y: result.y * scale,
|
|
@@ -23,7 +23,7 @@ export async function executeCallStart(
|
|
|
23
23
|
return {
|
|
24
24
|
content: [
|
|
25
25
|
'Error: A guardian voice verification call is already active for this number.',
|
|
26
|
-
'Use the guardian outbound verification flow (`/v1/integrations/guardian/outbound/start` or `/resend`) and wait for completion before using `call_start`.',
|
|
26
|
+
'Use the guardian outbound verification flow via the gateway API (`/v1/integrations/guardian/outbound/start` or `/resend`) and wait for completion before using `call_start`.',
|
|
27
27
|
].join(' '),
|
|
28
28
|
isError: true,
|
|
29
29
|
};
|
package/src/tools/executor.ts
CHANGED
|
@@ -58,7 +58,7 @@ export class ToolExecutor {
|
|
|
58
58
|
|
|
59
59
|
// Run pre-execution approval gates (abort, parental controls, guardian
|
|
60
60
|
// policy, allowed-tool-set, task-run preflight, tool registry lookup).
|
|
61
|
-
const gateResult = this.approvalHandler.checkPreExecutionGates(
|
|
61
|
+
const gateResult = await this.approvalHandler.checkPreExecutionGates(
|
|
62
62
|
name, input, context, executionTarget, riskLevel, startTime,
|
|
63
63
|
(event) => emitLifecycleEvent(context, event),
|
|
64
64
|
);
|
|
@@ -70,24 +70,29 @@ export class ToolExecutor {
|
|
|
70
70
|
const tool = gateResult.tool;
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
// A consumed scoped grant is a complete authorization — skip the
|
|
74
|
+
// interactive permission/prompt flow so non-interactive sessions
|
|
75
|
+
// don't auto-deny prompt-gated tools and burn the one-time grant.
|
|
76
|
+
if (!gateResult.grantConsumed) {
|
|
77
|
+
// Check permissions via the extracted PermissionChecker
|
|
78
|
+
const permResult = await this.permissionChecker.checkPermission(
|
|
79
|
+
name,
|
|
80
|
+
input,
|
|
81
|
+
tool,
|
|
82
|
+
context,
|
|
83
|
+
executionTarget,
|
|
84
|
+
(event) => emitLifecycleEvent(context, event),
|
|
85
|
+
sanitizeToolInput,
|
|
86
|
+
startTime,
|
|
87
|
+
computePreviewDiff,
|
|
88
|
+
);
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
riskLevel = permResult.riskLevel;
|
|
91
|
+
decision = permResult.decision;
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
if (!permResult.allowed) {
|
|
94
|
+
return { content: permResult.content, isError: true };
|
|
95
|
+
}
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
const hookResult = await getHookManager().trigger('pre-tool-execute', {
|
|
@@ -373,7 +373,6 @@ export async function stopSession(sessionId: ProxySessionId): Promise<void> {
|
|
|
373
373
|
*/
|
|
374
374
|
export function getSessionEnv(
|
|
375
375
|
sessionId: ProxySessionId,
|
|
376
|
-
options?: { dockerMode?: boolean },
|
|
377
376
|
): ProxyEnvVars {
|
|
378
377
|
const managed = sessions.get(sessionId);
|
|
379
378
|
if (!managed) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -384,10 +383,7 @@ export function getSessionEnv(
|
|
|
384
383
|
// Touch the idle timer on access
|
|
385
384
|
resetIdleTimer(managed);
|
|
386
385
|
|
|
387
|
-
|
|
388
|
-
// host.docker.internal so traffic reaches the host-side proxy.
|
|
389
|
-
const host = options?.dockerMode ? 'host.docker.internal' : '127.0.0.1';
|
|
390
|
-
const proxyUrl = `http://${host}:${managed.session.port}`;
|
|
386
|
+
const proxyUrl = `http://127.0.0.1:${managed.session.port}`;
|
|
391
387
|
const env: ProxyEnvVars = {
|
|
392
388
|
HTTP_PROXY: proxyUrl,
|
|
393
389
|
HTTPS_PROXY: proxyUrl,
|
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();
|