@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.
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -84
- package/src/__tests__/approval-primitive.test.ts +72 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -42
- package/src/__tests__/skill-feature-flags-integration.test.ts +0 -4
- package/src/__tests__/tool-approval-handler.test.ts +94 -5
- package/src/cli/mcp.ts +20 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +13 -8
- package/src/config/bundled-skills/reminder/SKILL.md +7 -6
- package/src/config/bundled-skills/reminder/TOOLS.json +1 -1
- package/src/config/bundled-skills/time-based-actions/SKILL.md +7 -6
- package/src/config/system-prompt.ts +0 -72
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +14 -6
- package/src/daemon/handlers/config.ts +0 -4
- package/src/daemon/handlers/navigate-settings.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +0 -10
- package/src/daemon/ipc-contract.ts +0 -4
- package/src/daemon/session-process.ts +2 -2
- package/src/permissions/checker.ts +4 -4
- package/src/runtime/routes/inbound-message-handler.ts +2 -2
- package/src/runtime/routes/ingress-routes.ts +7 -2
- package/src/tools/executor.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +1 -1
- package/src/tools/reminder/reminder.ts +1 -1
- package/src/tools/system/navigate-settings.ts +0 -1
- package/src/tools/tool-approval-handler.ts +2 -33
- package/src/daemon/handlers/config-parental.ts +0 -164
- package/src/daemon/ipc-contract/parental-control.ts +0 -109
- 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
|
-
}
|