@vellumai/assistant 0.3.22 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -84
  3. package/src/__tests__/approval-primitive.test.ts +72 -0
  4. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -4
  5. package/src/__tests__/host-shell-tool.test.ts +25 -0
  6. package/src/__tests__/ipc-snapshot.test.ts +0 -42
  7. package/src/__tests__/mcp-cli.test.ts +120 -3
  8. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -4
  9. package/src/__tests__/terminal-tools.test.ts +19 -1
  10. package/src/__tests__/tool-approval-handler.test.ts +94 -5
  11. package/src/__tests__/tool-executor.test.ts +1 -1
  12. package/src/cli/mcp.ts +25 -0
  13. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +13 -8
  14. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  15. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  16. package/src/config/bundled-skills/reminder/SKILL.md +7 -6
  17. package/src/config/bundled-skills/time-based-actions/SKILL.md +7 -6
  18. package/src/config/feature-flag-registry.json +8 -0
  19. package/src/config/schema.ts +10 -10
  20. package/src/config/system-prompt.ts +0 -72
  21. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +7 -7
  22. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +14 -6
  23. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  24. package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -10
  25. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  26. package/src/daemon/handlers/config.ts +0 -4
  27. package/src/daemon/handlers/navigate-settings.ts +0 -1
  28. package/src/daemon/ipc-contract-inventory.json +0 -10
  29. package/src/daemon/ipc-contract.ts +0 -4
  30. package/src/daemon/lifecycle.ts +14 -2
  31. package/src/daemon/session-process.ts +2 -2
  32. package/src/daemon/shutdown-handlers.ts +1 -1
  33. package/src/instrument.ts +15 -1
  34. package/src/mcp/client.ts +3 -3
  35. package/src/memory/conversation-crud.ts +26 -4
  36. package/src/memory/migrations/119-schema-indexes-and-columns.ts +46 -18
  37. package/src/permissions/checker.ts +4 -4
  38. package/src/runtime/routes/inbound-message-handler.ts +2 -2
  39. package/src/runtime/routes/ingress-routes.ts +7 -2
  40. package/src/tools/executor.ts +2 -2
  41. package/src/tools/host-terminal/host-shell.ts +4 -29
  42. package/src/tools/swarm/delegate.ts +3 -0
  43. package/src/tools/system/navigate-settings.ts +0 -1
  44. package/src/tools/terminal/safe-env.ts +9 -0
  45. package/src/tools/tool-approval-handler.ts +2 -33
  46. package/src/tools/tool-manifest.ts +33 -88
  47. package/src/daemon/handlers/config-parental.ts +0 -164
  48. package/src/daemon/ipc-contract/parental-control.ts +0 -109
  49. package/src/security/parental-control-store.ts +0 -184
@@ -1,164 +0,0 @@
1
- import * as net from 'node:net';
2
-
3
- import {
4
- clearPIN,
5
- getParentalControlSettings,
6
- hasPIN,
7
- setPIN,
8
- updateParentalControlSettings,
9
- verifyPIN,
10
- } from '../../security/parental-control-store.js';
11
- import { getLogger } from '../../util/logger.js';
12
- import type {
13
- ParentalControlGetRequest,
14
- ParentalControlSetPinRequest,
15
- ParentalControlUpdateRequest,
16
- ParentalControlVerifyPinRequest,
17
- } from '../ipc-protocol.js';
18
- import { defineHandlers, type HandlerContext } from './shared.js';
19
-
20
- const log = getLogger('parental-control');
21
-
22
- function sendGetResponse(socket: net.Socket, ctx: HandlerContext): void {
23
- const settings = getParentalControlSettings();
24
- ctx.send(socket, {
25
- type: 'parental_control_get_response',
26
- enabled: settings.enabled,
27
- has_pin: hasPIN(),
28
- content_restrictions: settings.contentRestrictions,
29
- blocked_tool_categories: settings.blockedToolCategories,
30
- });
31
- }
32
-
33
- export function handleParentalControlGet(
34
- _msg: ParentalControlGetRequest,
35
- socket: net.Socket,
36
- ctx: HandlerContext,
37
- ): void {
38
- sendGetResponse(socket, ctx);
39
- }
40
-
41
- export function handleParentalControlVerifyPin(
42
- msg: ParentalControlVerifyPinRequest,
43
- socket: net.Socket,
44
- ctx: HandlerContext,
45
- ): void {
46
- const verified = verifyPIN(msg.pin);
47
- ctx.send(socket, {
48
- type: 'parental_control_verify_pin_response',
49
- verified,
50
- });
51
- }
52
-
53
- export function handleParentalControlSetPin(
54
- msg: ParentalControlSetPinRequest,
55
- socket: net.Socket,
56
- ctx: HandlerContext,
57
- ): void {
58
- try {
59
- const pinExists = hasPIN();
60
-
61
- if (msg.clear) {
62
- // Clearing the PIN — must verify current PIN first
63
- if (pinExists) {
64
- if (!msg.current_pin || !verifyPIN(msg.current_pin)) {
65
- ctx.send(socket, {
66
- type: 'parental_control_set_pin_response',
67
- success: false,
68
- error: 'Current PIN is incorrect',
69
- });
70
- return;
71
- }
72
- }
73
- clearPIN();
74
- ctx.send(socket, {
75
- type: 'parental_control_set_pin_response',
76
- success: true,
77
- });
78
- return;
79
- }
80
-
81
- if (!msg.new_pin) {
82
- ctx.send(socket, {
83
- type: 'parental_control_set_pin_response',
84
- success: false,
85
- error: 'new_pin is required',
86
- });
87
- return;
88
- }
89
-
90
- if (pinExists) {
91
- // Changing existing PIN — must verify current PIN first
92
- if (!msg.current_pin || !verifyPIN(msg.current_pin)) {
93
- ctx.send(socket, {
94
- type: 'parental_control_set_pin_response',
95
- success: false,
96
- error: 'Current PIN is incorrect',
97
- });
98
- return;
99
- }
100
- }
101
-
102
- setPIN(msg.new_pin);
103
- ctx.send(socket, {
104
- type: 'parental_control_set_pin_response',
105
- success: true,
106
- });
107
- } catch (err) {
108
- log.error({ err }, 'Failed to set parental control PIN');
109
- ctx.send(socket, {
110
- type: 'parental_control_set_pin_response',
111
- success: false,
112
- error: err instanceof Error ? err.message : 'Internal error',
113
- });
114
- }
115
- }
116
-
117
- export function handleParentalControlUpdate(
118
- msg: ParentalControlUpdateRequest,
119
- socket: net.Socket,
120
- ctx: HandlerContext,
121
- ): void {
122
- const settings = getParentalControlSettings();
123
- const pinExists = hasPIN();
124
-
125
- // Require PIN verification when parental mode is already enabled
126
- if (settings.enabled && pinExists) {
127
- if (!msg.pin || !verifyPIN(msg.pin)) {
128
- ctx.send(socket, {
129
- type: 'parental_control_update_response',
130
- success: false,
131
- error: 'PIN required to change parental control settings',
132
- enabled: settings.enabled,
133
- has_pin: pinExists,
134
- content_restrictions: settings.contentRestrictions,
135
- blocked_tool_categories: settings.blockedToolCategories,
136
- });
137
- return;
138
- }
139
- }
140
-
141
- const updated = updateParentalControlSettings({
142
- enabled: msg.enabled,
143
- contentRestrictions: msg.content_restrictions,
144
- blockedToolCategories: msg.blocked_tool_categories,
145
- });
146
-
147
- log.info({ enabled: updated.enabled }, 'Parental control settings updated');
148
-
149
- ctx.send(socket, {
150
- type: 'parental_control_update_response',
151
- success: true,
152
- enabled: updated.enabled,
153
- has_pin: hasPIN(),
154
- content_restrictions: updated.contentRestrictions,
155
- blocked_tool_categories: updated.blockedToolCategories,
156
- });
157
- }
158
-
159
- export const parentalControlHandlers = defineHandlers({
160
- parental_control_get: handleParentalControlGet,
161
- parental_control_verify_pin: handleParentalControlVerifyPin,
162
- parental_control_set_pin: handleParentalControlSetPin,
163
- parental_control_update: handleParentalControlUpdate,
164
- });
@@ -1,109 +0,0 @@
1
- // Parental control IPC types.
2
- //
3
- // The parental control system lets a parent or guardian lock the assistant
4
- // behind a 6-digit PIN and configure per-topic content restrictions and
5
- // per-category tool blocks. All mutating operations require the PIN when
6
- // one has been set.
7
-
8
- // === Shared data types ===
9
-
10
- /**
11
- * Topics that can be individually blocked.
12
- * All unlisted topics are allowed.
13
- */
14
- export type ParentalContentTopic =
15
- | 'violence'
16
- | 'adult_content'
17
- | 'political'
18
- | 'gambling'
19
- | 'drugs';
20
-
21
- /**
22
- * Broad tool categories that can be disabled for age-appropriate use.
23
- * When a category is blocked, individual tool invocations within that
24
- * category are rejected before the permission pipeline runs.
25
- */
26
- export type ParentalToolCategory =
27
- | 'computer_use'
28
- | 'network'
29
- | 'shell'
30
- | 'file_write';
31
-
32
- // === Client → Server ===
33
-
34
- /** Retrieve the current parental control settings and PIN status. */
35
- export interface ParentalControlGetRequest {
36
- type: 'parental_control_get';
37
- }
38
-
39
- /** Verify a PIN attempt without changing any state. Useful to gate an unlock-settings flow before showing the full panel. */
40
- export interface ParentalControlVerifyPinRequest {
41
- type: 'parental_control_verify_pin';
42
- pin: string;
43
- }
44
-
45
- /** Set, change, or clear the parental control PIN. To set for the first time provide only new_pin. To change provide current_pin and new_pin. To clear provide current_pin and set clear:true. */
46
- export interface ParentalControlSetPinRequest {
47
- type: 'parental_control_set_pin';
48
- current_pin?: string;
49
- new_pin?: string;
50
- clear?: boolean;
51
- }
52
-
53
- /** Update parental control settings. Requires the PIN when parental mode is already enabled. */
54
- export interface ParentalControlUpdateRequest {
55
- type: 'parental_control_update';
56
- /** Current PIN — required when parental mode is already enabled. */
57
- pin?: string;
58
- /** Enable or disable parental control mode. */
59
- enabled?: boolean;
60
- /** Full replacement list of blocked content topics. */
61
- content_restrictions?: ParentalContentTopic[];
62
- /** Full replacement list of blocked tool categories. */
63
- blocked_tool_categories?: ParentalToolCategory[];
64
- }
65
-
66
- // === Server → Client ===
67
-
68
- export interface ParentalControlGetResponse {
69
- type: 'parental_control_get_response';
70
- enabled: boolean;
71
- has_pin: boolean;
72
- content_restrictions: ParentalContentTopic[];
73
- blocked_tool_categories: ParentalToolCategory[];
74
- }
75
-
76
- export interface ParentalControlVerifyPinResponse {
77
- type: 'parental_control_verify_pin_response';
78
- verified: boolean;
79
- }
80
-
81
- export interface ParentalControlSetPinResponse {
82
- type: 'parental_control_set_pin_response';
83
- success: boolean;
84
- error?: string;
85
- }
86
-
87
- export interface ParentalControlUpdateResponse {
88
- type: 'parental_control_update_response';
89
- success: boolean;
90
- error?: string;
91
- enabled: boolean;
92
- has_pin: boolean;
93
- content_restrictions: ParentalContentTopic[];
94
- blocked_tool_categories: ParentalToolCategory[];
95
- }
96
-
97
- // --- Domain-level union aliases (consumed by the barrel file) ---
98
-
99
- export type _ParentalControlClientMessages =
100
- | ParentalControlGetRequest
101
- | ParentalControlVerifyPinRequest
102
- | ParentalControlSetPinRequest
103
- | ParentalControlUpdateRequest;
104
-
105
- export type _ParentalControlServerMessages =
106
- | ParentalControlGetResponse
107
- | ParentalControlVerifyPinResponse
108
- | ParentalControlSetPinResponse
109
- | ParentalControlUpdateResponse;
@@ -1,184 +0,0 @@
1
- /**
2
- * Parental control settings and PIN management.
3
- *
4
- * Non-secret settings (enabled state, content restrictions, blocked tool
5
- * categories) are persisted to `~/.vellum/parental-control.json`.
6
- *
7
- * The PIN hash and salt are stored in the encrypted key store under the
8
- * account `parental:pin` as the hex string `"<salt>:<hash>"`.
9
- *
10
- * PIN hashing uses SHA-256 with a random 16-byte salt to prevent offline
11
- * dictionary attacks. Comparison uses timingSafeEqual to avoid timing leaks.
12
- */
13
-
14
- import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
15
- import { readFileSync, writeFileSync } from 'node:fs';
16
- import { dirname,join } from 'node:path';
17
-
18
- import type { ParentalContentTopic, ParentalToolCategory } from '../daemon/ipc-contract/parental-control.js';
19
- import { ensureDir,pathExists } from '../util/fs.js';
20
- import { getLogger } from '../util/logger.js';
21
- import { getRootDir } from '../util/platform.js';
22
- import { deleteKey,getKey, setKey } from './encrypted-store.js';
23
-
24
- const log = getLogger('parental-control');
25
-
26
- const PIN_ACCOUNT = 'parental:pin';
27
-
28
- export type { ParentalContentTopic, ParentalToolCategory };
29
-
30
- export interface ParentalControlSettings {
31
- enabled: boolean;
32
- contentRestrictions: ParentalContentTopic[];
33
- blockedToolCategories: ParentalToolCategory[];
34
- }
35
-
36
- const DEFAULT_SETTINGS: ParentalControlSettings = {
37
- enabled: false,
38
- contentRestrictions: [],
39
- blockedToolCategories: [],
40
- };
41
-
42
- function getSettingsPath(): string {
43
- return join(getRootDir(), 'parental-control.json');
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Settings I/O
48
- // ---------------------------------------------------------------------------
49
-
50
- export function getParentalControlSettings(): ParentalControlSettings {
51
- try {
52
- const file = getSettingsPath();
53
- if (!pathExists(file)) return { ...DEFAULT_SETTINGS };
54
- const raw = readFileSync(file, 'utf-8');
55
- const parsed = JSON.parse(raw) as Partial<ParentalControlSettings>;
56
- return {
57
- enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : false,
58
- contentRestrictions: Array.isArray(parsed.contentRestrictions) ? parsed.contentRestrictions : [],
59
- blockedToolCategories: Array.isArray(parsed.blockedToolCategories) ? parsed.blockedToolCategories : [],
60
- };
61
- } catch {
62
- return { ...DEFAULT_SETTINGS };
63
- }
64
- }
65
-
66
- function saveSettings(settings: ParentalControlSettings): void {
67
- const file = getSettingsPath();
68
- ensureDir(dirname(file));
69
- writeFileSync(file, JSON.stringify(settings, null, 2), { encoding: 'utf-8' });
70
- }
71
-
72
- export function updateParentalControlSettings(
73
- patch: Partial<ParentalControlSettings>,
74
- ): ParentalControlSettings {
75
- const current = getParentalControlSettings();
76
- const next: ParentalControlSettings = {
77
- enabled: patch.enabled !== undefined ? patch.enabled : current.enabled,
78
- contentRestrictions: patch.contentRestrictions !== undefined
79
- ? patch.contentRestrictions
80
- : current.contentRestrictions,
81
- blockedToolCategories: patch.blockedToolCategories !== undefined
82
- ? patch.blockedToolCategories
83
- : current.blockedToolCategories,
84
- };
85
- saveSettings(next);
86
- return next;
87
- }
88
-
89
- // ---------------------------------------------------------------------------
90
- // PIN management
91
- // ---------------------------------------------------------------------------
92
-
93
- /** Returns true if a parental control PIN has been configured. */
94
- export function hasPIN(): boolean {
95
- return getKey(PIN_ACCOUNT) !== undefined;
96
- }
97
-
98
- function hashPIN(pin: string, salt: Buffer): Buffer {
99
- return createHash('sha256').update(salt).update(pin).digest();
100
- }
101
-
102
- /**
103
- * Set a new PIN. Rejects if `pin` is not exactly 6 ASCII digits.
104
- * Throws if the store write fails.
105
- */
106
- export function setPIN(pin: string): void {
107
- if (!/^\d{6}$/.test(pin)) {
108
- throw new Error('PIN must be exactly 6 digits');
109
- }
110
- const salt = randomBytes(16);
111
- const hash = hashPIN(pin, salt);
112
- const stored = `${salt.toString('hex')}:${hash.toString('hex')}`;
113
- if (!setKey(PIN_ACCOUNT, stored)) {
114
- throw new Error('Failed to persist PIN — encrypted store write error');
115
- }
116
- log.info('Parental control PIN set');
117
- }
118
-
119
- /**
120
- * Verify a PIN attempt. Returns true on match, false on mismatch or if no
121
- * PIN has been configured. Uses constant-time comparison to prevent timing
122
- * attacks.
123
- */
124
- export function verifyPIN(pin: string): boolean {
125
- if (!/^\d{6}$/.test(pin)) return false;
126
- const stored = getKey(PIN_ACCOUNT);
127
- if (!stored) return false;
128
-
129
- const colonIdx = stored.indexOf(':');
130
- if (colonIdx === -1) return false;
131
-
132
- try {
133
- const salt = Buffer.from(stored.slice(0, colonIdx), 'hex');
134
- const expectedHash = Buffer.from(stored.slice(colonIdx + 1), 'hex');
135
- const actualHash = hashPIN(pin, salt);
136
- if (actualHash.length !== expectedHash.length) return false;
137
- return timingSafeEqual(actualHash, expectedHash);
138
- } catch {
139
- return false;
140
- }
141
- }
142
-
143
- /**
144
- * Remove the PIN. The caller is responsible for requiring PIN verification
145
- * before calling this.
146
- */
147
- export function clearPIN(): void {
148
- deleteKey(PIN_ACCOUNT);
149
- log.info('Parental control PIN cleared');
150
- }
151
-
152
- // ---------------------------------------------------------------------------
153
- // Tool category → tool name mapping
154
- // ---------------------------------------------------------------------------
155
-
156
- /**
157
- * Tool name prefixes that belong to each blocked category.
158
- * A tool is considered blocked if its name starts with any of the listed
159
- * prefixes (case-sensitive).
160
- */
161
- export const TOOL_CATEGORY_PREFIXES: Record<ParentalToolCategory, string[]> = {
162
- computer_use: ['cu_', 'computer_use', 'screenshot', 'accessibility_'],
163
- network: ['web_fetch', 'web_search', 'browser_'],
164
- shell: ['bash', 'terminal', 'host_shell'],
165
- file_write: ['file_write', 'file_edit', 'multi_edit', 'file_delete', 'git'],
166
- };
167
-
168
- /**
169
- * Returns true if the given tool name falls within any of the currently
170
- * blocked tool categories.
171
- */
172
- export function isToolBlocked(toolName: string): boolean {
173
- const { enabled, blockedToolCategories } = getParentalControlSettings();
174
- if (!enabled || blockedToolCategories.length === 0) return false;
175
-
176
- for (const category of blockedToolCategories) {
177
- const prefixes = TOOL_CATEGORY_PREFIXES[category];
178
- // Guard against unknown categories that may appear after deserialization of
179
- // settings written by a newer client version — skip rather than throw.
180
- if (!prefixes) continue;
181
- if (prefixes.some((p) => toolName.startsWith(p))) return true;
182
- }
183
- return false;
184
- }