@vellumai/assistant 0.3.23 → 0.3.25

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 (30) 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__/ipc-snapshot.test.ts +0 -42
  6. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -4
  7. package/src/__tests__/tool-approval-handler.test.ts +94 -5
  8. package/src/cli/mcp.ts +20 -0
  9. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +13 -8
  10. package/src/config/bundled-skills/reminder/SKILL.md +7 -6
  11. package/src/config/bundled-skills/reminder/TOOLS.json +1 -1
  12. package/src/config/bundled-skills/time-based-actions/SKILL.md +7 -6
  13. package/src/config/system-prompt.ts +0 -72
  14. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +14 -6
  15. package/src/daemon/handlers/config.ts +0 -4
  16. package/src/daemon/handlers/navigate-settings.ts +0 -1
  17. package/src/daemon/ipc-contract-inventory.json +0 -10
  18. package/src/daemon/ipc-contract.ts +0 -4
  19. package/src/daemon/session-process.ts +2 -2
  20. package/src/permissions/checker.ts +4 -4
  21. package/src/runtime/routes/inbound-message-handler.ts +2 -2
  22. package/src/runtime/routes/ingress-routes.ts +7 -2
  23. package/src/tools/executor.ts +2 -2
  24. package/src/tools/reminder/reminder-store.ts +1 -1
  25. package/src/tools/reminder/reminder.ts +1 -1
  26. package/src/tools/system/navigate-settings.ts +0 -1
  27. package/src/tools/tool-approval-handler.ts +2 -33
  28. package/src/daemon/handlers/config-parental.ts +0 -164
  29. package/src/daemon/ipc-contract/parental-control.ts +0 -109
  30. package/src/security/parental-control-store.ts +0 -184
@@ -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
- }