vellum 0.2.12 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +171 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +402 -5
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +28 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +96 -8
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +50 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +26 -2
- package/src/config/schema.ts +178 -9
- package/src/config/types.ts +3 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/defaults.ts +11 -0
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -5,10 +5,21 @@ import { resolveSkillSelector } from '../config/skills.js';
|
|
|
5
5
|
import { computeSkillVersionHash } from '../skills/version-hash.js';
|
|
6
6
|
import { getTool } from '../tools/registry.js';
|
|
7
7
|
import { getConfig } from '../config/loader.js';
|
|
8
|
+
import { getLogger } from '../util/logger.js';
|
|
8
9
|
import { dirname, resolve } from 'node:path';
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
11
|
import { looksLikeHostPortShorthand, looksLikePathOnlyInput } from '../tools/network/url-safety.js';
|
|
11
12
|
import { normalizeFilePath, isSkillSourcePath } from '../skills/path-classifier.js';
|
|
13
|
+
import { isWorkspaceScopedInvocation } from './workspace-policy.js';
|
|
14
|
+
import { buildShellCommandCandidates, buildShellAllowlistOptions, type ParsedCommand } from './shell-identity.js';
|
|
15
|
+
|
|
16
|
+
// Ensures the legacy mode deprecation warning fires at most once per process.
|
|
17
|
+
let _legacyDeprecationWarned = false;
|
|
18
|
+
|
|
19
|
+
/** @internal — exposed only for tests to reset the one-time warning flag. */
|
|
20
|
+
export function _resetLegacyDeprecationWarning(): void {
|
|
21
|
+
_legacyDeprecationWarned = false;
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
// Low-risk shell programs that are read-only / informational
|
|
14
25
|
const LOW_RISK_PROGRAMS = new Set([
|
|
@@ -143,9 +154,9 @@ function escapeMinimatchLiteral(value: string): string {
|
|
|
143
154
|
return value.replace(/([\\*?[\]{}()!+@|])/g, '\\$1');
|
|
144
155
|
}
|
|
145
156
|
|
|
146
|
-
function buildCommandCandidates(toolName: string, input: Record<string, unknown>, workingDir: string): string[] {
|
|
157
|
+
async function buildCommandCandidates(toolName: string, input: Record<string, unknown>, workingDir: string, preParsed?: ParsedCommand): Promise<string[]> {
|
|
147
158
|
if (toolName === 'bash' || toolName === 'host_bash') {
|
|
148
|
-
return
|
|
159
|
+
return buildShellCommandCandidates(getStringField(input, 'command'), preParsed);
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
if (toolName === 'skill_load') {
|
|
@@ -233,7 +244,7 @@ function buildCommandCandidates(toolName: string, input: Record<string, unknown>
|
|
|
233
244
|
return [...new Set(candidates)];
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
export async function classifyRisk(toolName: string, input: Record<string, unknown>, workingDir?: string): Promise<RiskLevel> {
|
|
247
|
+
export async function classifyRisk(toolName: string, input: Record<string, unknown>, workingDir?: string, preParsed?: ParsedCommand): Promise<RiskLevel> {
|
|
237
248
|
if (toolName === 'file_read') return RiskLevel.Low;
|
|
238
249
|
if (toolName === 'file_write' || toolName === 'file_edit') {
|
|
239
250
|
const filePath = getStringField(input, 'path', 'file_path');
|
|
@@ -273,7 +284,7 @@ export async function classifyRisk(toolName: string, input: Record<string, unkno
|
|
|
273
284
|
const command = (input.command as string) ?? '';
|
|
274
285
|
if (!command.trim()) return RiskLevel.Low;
|
|
275
286
|
|
|
276
|
-
const parsed = await parse(command);
|
|
287
|
+
const parsed = preParsed ?? await parse(command);
|
|
277
288
|
|
|
278
289
|
// Dangerous patterns → High
|
|
279
290
|
if (parsed.dangerousPatterns.length > 0) return RiskLevel.High;
|
|
@@ -341,10 +352,19 @@ export async function check(
|
|
|
341
352
|
workingDir: string,
|
|
342
353
|
policyContext?: PolicyContext,
|
|
343
354
|
): Promise<PermissionCheckResult> {
|
|
344
|
-
|
|
355
|
+
// For shell tools, parse once and share the result to avoid duplicate tree-sitter work.
|
|
356
|
+
let shellParsed: ParsedCommand | undefined;
|
|
357
|
+
if (toolName === 'bash' || toolName === 'host_bash') {
|
|
358
|
+
const command = ((input.command as string) ?? '').trim();
|
|
359
|
+
if (command) {
|
|
360
|
+
shellParsed = await parse(command);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const risk = await classifyRisk(toolName, input, workingDir, shellParsed);
|
|
345
365
|
|
|
346
366
|
// Build command string candidates for rule matching
|
|
347
|
-
const commandCandidates = buildCommandCandidates(toolName, input, workingDir);
|
|
367
|
+
const commandCandidates = await buildCommandCandidates(toolName, input, workingDir, shellParsed);
|
|
348
368
|
|
|
349
369
|
// Find the highest-priority matching rule across all candidates
|
|
350
370
|
const matchedRule = findHighestPriorityRule(toolName, commandCandidates, workingDir, policyContext);
|
|
@@ -399,10 +419,28 @@ export async function check(
|
|
|
399
419
|
// agent new capabilities, so in strict mode users must approve each
|
|
400
420
|
// skill load via an exact-version or wildcard trust rule.
|
|
401
421
|
const permissionsMode = getConfig().permissions.mode;
|
|
422
|
+
|
|
423
|
+
if (permissionsMode === 'legacy' && !_legacyDeprecationWarned) {
|
|
424
|
+
_legacyDeprecationWarned = true;
|
|
425
|
+
getLogger('checker').warn('Permissions mode "legacy" is deprecated and will be removed in a future release. Switch to "workspace" (default) or "strict".');
|
|
426
|
+
}
|
|
427
|
+
|
|
402
428
|
if (permissionsMode === 'strict' && !matchedRule) {
|
|
403
429
|
return { decision: 'prompt', reason: `Strict mode: no matching rule, requires approval` };
|
|
404
430
|
}
|
|
405
431
|
|
|
432
|
+
// Workspace mode: auto-allow workspace-scoped operations that don't have
|
|
433
|
+
// an explicit rule. Non-workspace operations fall through to risk-based policy.
|
|
434
|
+
if (permissionsMode === 'workspace' && !matchedRule) {
|
|
435
|
+
// When sandbox is disabled, bash runs on the host — don't auto-allow
|
|
436
|
+
const sandboxEnabled = getConfig().sandbox.enabled;
|
|
437
|
+
if (toolName === 'bash' && !sandboxEnabled) {
|
|
438
|
+
// Fall through to risk-based policy below
|
|
439
|
+
} else if (isWorkspaceScopedInvocation(toolName, input, workingDir)) {
|
|
440
|
+
return { decision: 'allow', reason: 'Workspace mode: workspace-scoped operation auto-allowed' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
406
444
|
// Auto-allow low-risk bundled skill tools even without explicit trust rules.
|
|
407
445
|
// These are first-party tools with a vetted risk declaration — applying the
|
|
408
446
|
// same policy as the per-tool default allow rules for browser tools, but
|
|
@@ -448,38 +486,10 @@ function friendlyHostname(url: URL): string {
|
|
|
448
486
|
return url.hostname.replace(/^www\./, '');
|
|
449
487
|
}
|
|
450
488
|
|
|
451
|
-
export function generateAllowlistOptions(toolName: string, input: Record<string, unknown>): AllowlistOption[] {
|
|
489
|
+
export async function generateAllowlistOptions(toolName: string, input: Record<string, unknown>): Promise<AllowlistOption[]> {
|
|
452
490
|
if (toolName === 'bash' || toolName === 'host_bash') {
|
|
453
491
|
const command = ((input.command as string) ?? '').trim();
|
|
454
|
-
|
|
455
|
-
const program = parts[0] ?? command;
|
|
456
|
-
const options: AllowlistOption[] = [];
|
|
457
|
-
|
|
458
|
-
// Exact match
|
|
459
|
-
options.push({ label: command, description: 'This exact command', pattern: command });
|
|
460
|
-
|
|
461
|
-
if (parts.length >= 2) {
|
|
462
|
-
// Subcommand wildcard: "npm install *"
|
|
463
|
-
const sub = parts.slice(0, -1).join(' ');
|
|
464
|
-
options.push({
|
|
465
|
-
label: `${sub} *`,
|
|
466
|
-
description: `Any "${sub}" command`,
|
|
467
|
-
pattern: `${sub} *`,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (parts.length >= 1) {
|
|
472
|
-
// Program wildcard: "npm *"
|
|
473
|
-
options.push({ label: `${program} *`, description: `Any ${program} command`, pattern: `${program} *` });
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Deduplicate
|
|
477
|
-
const seen = new Set<string>();
|
|
478
|
-
return options.filter((o) => {
|
|
479
|
-
if (seen.has(o.pattern)) return false;
|
|
480
|
-
seen.add(o.pattern);
|
|
481
|
-
return true;
|
|
482
|
-
});
|
|
492
|
+
return buildShellAllowlistOptions(command);
|
|
483
493
|
}
|
|
484
494
|
|
|
485
495
|
if (
|
|
@@ -604,7 +614,7 @@ export function generateAllowlistOptions(toolName: string, input: Record<string,
|
|
|
604
614
|
return [{ label: '*', description: 'Everything', pattern: '*' }];
|
|
605
615
|
}
|
|
606
616
|
|
|
607
|
-
export function generateScopeOptions(workingDir: string,
|
|
617
|
+
export function generateScopeOptions(workingDir: string, _toolName?: string): ScopeOption[] {
|
|
608
618
|
const home = homedir();
|
|
609
619
|
const options: ScopeOption[] = [];
|
|
610
620
|
|
|
@@ -626,11 +636,5 @@ export function generateScopeOptions(workingDir: string, toolName?: string): Sco
|
|
|
626
636
|
// Everywhere
|
|
627
637
|
options.push({ label: 'everywhere', scope: 'everywhere' });
|
|
628
638
|
|
|
629
|
-
|
|
630
|
-
return options;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const everywhere = options.find((option) => option.scope === 'everywhere');
|
|
634
|
-
const scoped = options.filter((option) => option.scope !== 'everywhere');
|
|
635
|
-
return everywhere ? [everywhere, ...scoped] : options;
|
|
639
|
+
return options;
|
|
636
640
|
}
|
|
@@ -225,6 +225,16 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
225
225
|
priority: 100,
|
|
226
226
|
};
|
|
227
227
|
|
|
228
|
+
// memory_search is a read-only tool — always allow without prompting.
|
|
229
|
+
const memorySearchRule: DefaultRuleTemplate = {
|
|
230
|
+
id: 'default:allow-memory_search-global',
|
|
231
|
+
tool: 'memory_search',
|
|
232
|
+
pattern: 'memory_search:*',
|
|
233
|
+
scope: 'everywhere',
|
|
234
|
+
decision: 'allow',
|
|
235
|
+
priority: 100,
|
|
236
|
+
};
|
|
237
|
+
|
|
228
238
|
return [
|
|
229
239
|
...hostFileRules,
|
|
230
240
|
hostShellRule,
|
|
@@ -239,5 +249,6 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
239
249
|
...browserToolRules,
|
|
240
250
|
...uiSurfaceRules,
|
|
241
251
|
viewImageRule,
|
|
252
|
+
memorySearchRule,
|
|
242
253
|
];
|
|
243
254
|
}
|
|
@@ -37,7 +37,6 @@ export class PermissionPrompter {
|
|
|
37
37
|
sandboxed?: boolean,
|
|
38
38
|
sessionId?: string,
|
|
39
39
|
executionTarget?: ExecutionTarget,
|
|
40
|
-
principal?: { kind?: string; id?: string; version?: string },
|
|
41
40
|
persistentDecisionsAllowed?: boolean,
|
|
42
41
|
): Promise<{ decision: UserDecision; selectedPattern?: string; selectedScope?: string }> {
|
|
43
42
|
const requestId = uuid();
|
|
@@ -64,9 +63,6 @@ export class PermissionPrompter {
|
|
|
64
63
|
sandboxed,
|
|
65
64
|
sessionId,
|
|
66
65
|
executionTarget,
|
|
67
|
-
principalKind: principal?.kind,
|
|
68
|
-
principalId: principal?.id,
|
|
69
|
-
principalVersion: principal?.version,
|
|
70
66
|
persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
|
|
71
67
|
});
|
|
72
68
|
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { parse, type ParsedCommand, type CommandSegment, type DangerousPattern } from '../tools/terminal/parser.js';
|
|
2
|
+
import type { AllowlistOption } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type { ParsedCommand };
|
|
5
|
+
|
|
6
|
+
export interface ShellActionKey {
|
|
7
|
+
/** e.g. "action:gh", "action:gh pr", "action:gh pr view" */
|
|
8
|
+
key: string;
|
|
9
|
+
/** How many tokens deep this key goes */
|
|
10
|
+
depth: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ShellIdentityAnalysis {
|
|
14
|
+
/** The parsed segments from the shell parser */
|
|
15
|
+
segments: CommandSegment[];
|
|
16
|
+
/** The operator sequence between segments (e.g. ['&&', '|']) */
|
|
17
|
+
operators: string[];
|
|
18
|
+
/** Whether the command contains opaque constructs (eval, heredocs, etc.) */
|
|
19
|
+
hasOpaqueConstructs: boolean;
|
|
20
|
+
/** Dangerous patterns detected by the parser */
|
|
21
|
+
dangerousPatterns: DangerousPattern[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ActionKeyResult {
|
|
25
|
+
/** The derived action keys from narrowest to broadest */
|
|
26
|
+
keys: ShellActionKey[];
|
|
27
|
+
/** Whether this command has a "simple action" shape (setup prefix + single action) */
|
|
28
|
+
isSimpleAction: boolean;
|
|
29
|
+
/** The primary action segment (the non-setup-prefix action command) */
|
|
30
|
+
primarySegment?: CommandSegment;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Programs that are considered setup prefixes (not the main action) */
|
|
34
|
+
const SETUP_PREFIX_PROGRAMS = new Set(['cd', 'pushd', 'export', 'unset', 'set']);
|
|
35
|
+
|
|
36
|
+
const MAX_ACTION_KEY_DEPTH = 3;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Analyze a shell command using the tree-sitter parser to extract
|
|
40
|
+
* identity information for permission decisions.
|
|
41
|
+
*/
|
|
42
|
+
export async function analyzeShellCommand(command: string, preParsed?: ParsedCommand): Promise<ShellIdentityAnalysis> {
|
|
43
|
+
const parsed = preParsed ?? await parse(command);
|
|
44
|
+
|
|
45
|
+
const operators: string[] = [];
|
|
46
|
+
for (const seg of parsed.segments) {
|
|
47
|
+
if (seg.operator) {
|
|
48
|
+
operators.push(seg.operator);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
segments: parsed.segments,
|
|
54
|
+
operators,
|
|
55
|
+
hasOpaqueConstructs: parsed.hasOpaqueConstructs,
|
|
56
|
+
dangerousPatterns: parsed.dangerousPatterns,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derive canonical action keys from a shell command analysis.
|
|
62
|
+
*
|
|
63
|
+
* Action keys identify the "family" of a command for allowlist purposes.
|
|
64
|
+
* For example, `cd repo && gh pr view 5525 --json title` derives:
|
|
65
|
+
* - action:gh pr view
|
|
66
|
+
* - action:gh pr
|
|
67
|
+
* - action:gh
|
|
68
|
+
*
|
|
69
|
+
* Only "simple action" commands (optional setup prefix + one action) get
|
|
70
|
+
* action keys. Pipelines and complex chains are marked non-simple.
|
|
71
|
+
*/
|
|
72
|
+
export function deriveShellActionKeys(analysis: ShellIdentityAnalysis): ActionKeyResult {
|
|
73
|
+
const { segments } = analysis;
|
|
74
|
+
|
|
75
|
+
if (segments.length === 0) {
|
|
76
|
+
return { keys: [], isSimpleAction: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For multi-segment commands, only allow simple-action classification if
|
|
80
|
+
// ALL inter-segment operators are explicitly &&. Any other operator (|, ||,
|
|
81
|
+
// ;, &, empty/missing) means the separator is unknown or unsafe.
|
|
82
|
+
// This safely handles cases where the parser doesn't capture certain
|
|
83
|
+
// separators (;, newline, &) and leaves them as empty operators.
|
|
84
|
+
if (segments.length > 1) {
|
|
85
|
+
for (const seg of segments) {
|
|
86
|
+
const op = seg.operator;
|
|
87
|
+
// Non-empty operator that isn't && → definitely complex
|
|
88
|
+
if (op && op !== '&&') {
|
|
89
|
+
return { keys: [], isSimpleAction: false };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Also check: if there are multiple segments but no operators at all
|
|
93
|
+
// between them (e.g. newline-separated), that's suspicious.
|
|
94
|
+
// The first segment always has operator '' (no preceding operator).
|
|
95
|
+
// If any non-first segment also has operator '', the separator was
|
|
96
|
+
// not captured — treat as complex for safety.
|
|
97
|
+
for (let i = 1; i < segments.length; i++) {
|
|
98
|
+
if (!segments[i].operator) {
|
|
99
|
+
return { keys: [], isSimpleAction: false };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Separate setup-prefix segments from action segments
|
|
105
|
+
const actionSegments: CommandSegment[] = [];
|
|
106
|
+
let foundNonPrefix = false;
|
|
107
|
+
|
|
108
|
+
for (const seg of segments) {
|
|
109
|
+
if (!foundNonPrefix && SETUP_PREFIX_PROGRAMS.has(seg.program)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
foundNonPrefix = true;
|
|
113
|
+
actionSegments.push(seg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Simple action: exactly one non-prefix action segment
|
|
117
|
+
if (actionSegments.length !== 1) {
|
|
118
|
+
return { keys: [], isSimpleAction: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const primarySegment = actionSegments[0];
|
|
122
|
+
const tokens: string[] = [primarySegment.program];
|
|
123
|
+
|
|
124
|
+
// Add non-flag, non-path stable subcommand tokens (up to MAX_ACTION_KEY_DEPTH)
|
|
125
|
+
for (const arg of primarySegment.args) {
|
|
126
|
+
if (tokens.length >= MAX_ACTION_KEY_DEPTH) break;
|
|
127
|
+
if (arg.startsWith('-')) continue;
|
|
128
|
+
if (arg.includes('/') || arg.startsWith('.')) continue;
|
|
129
|
+
if (/^\d+$/.test(arg)) continue;
|
|
130
|
+
if (arg.includes('$') || arg.includes('"') || arg.includes("'")) continue;
|
|
131
|
+
tokens.push(arg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build action keys from narrowest to broadest
|
|
135
|
+
const keys: ShellActionKey[] = [];
|
|
136
|
+
for (let depth = tokens.length; depth >= 1; depth--) {
|
|
137
|
+
keys.push({
|
|
138
|
+
key: `action:${tokens.slice(0, depth).join(' ')}`,
|
|
139
|
+
depth,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { keys, isSimpleAction: true, primarySegment };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build an ordered list of command candidates for trust-rule matching.
|
|
148
|
+
*
|
|
149
|
+
* Candidate ordering:
|
|
150
|
+
* 1. Raw command (backward compatibility — existing rules match as before)
|
|
151
|
+
* 2. Canonical primary command (if simple action) — the full primary segment text
|
|
152
|
+
* 3. Action keys from narrowest to broadest (if simple action)
|
|
153
|
+
*
|
|
154
|
+
* Complex commands (pipelines, multi-action chains) only return the raw candidate.
|
|
155
|
+
*/
|
|
156
|
+
export async function buildShellCommandCandidates(command: string, preParsed?: ParsedCommand): Promise<string[]> {
|
|
157
|
+
const trimmed = command.trim();
|
|
158
|
+
if (!trimmed) return [trimmed];
|
|
159
|
+
|
|
160
|
+
const analysis = await analyzeShellCommand(trimmed, preParsed);
|
|
161
|
+
const actionResult = deriveShellActionKeys(analysis);
|
|
162
|
+
|
|
163
|
+
const candidates: string[] = [trimmed];
|
|
164
|
+
|
|
165
|
+
if (actionResult.isSimpleAction && actionResult.primarySegment) {
|
|
166
|
+
// Add canonical primary command text (the actual segment, not the full command with setup prefixes)
|
|
167
|
+
const canonical = actionResult.primarySegment.command;
|
|
168
|
+
if (canonical !== trimmed) {
|
|
169
|
+
candidates.push(canonical);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add action keys
|
|
173
|
+
for (const actionKey of actionResult.keys) {
|
|
174
|
+
candidates.push(actionKey.key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Deduplicate while preserving order
|
|
179
|
+
return [...new Set(candidates)];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build allowlist options for shell commands using parser-derived identity.
|
|
184
|
+
*
|
|
185
|
+
* For simple actions (optional setup prefix + one action), options are:
|
|
186
|
+
* 1. Exact canonical primary command
|
|
187
|
+
* 2. Deepest action key (e.g. "action:gh pr view")
|
|
188
|
+
* 3. Broader action keys (e.g. "action:gh pr", "action:gh")
|
|
189
|
+
*
|
|
190
|
+
* For complex commands (pipelines, multi-action chains), only the exact
|
|
191
|
+
* command is offered (no broad options).
|
|
192
|
+
*/
|
|
193
|
+
export async function buildShellAllowlistOptions(command: string): Promise<AllowlistOption[]> {
|
|
194
|
+
const trimmed = command.trim();
|
|
195
|
+
if (!trimmed) return [];
|
|
196
|
+
|
|
197
|
+
const analysis = await analyzeShellCommand(trimmed);
|
|
198
|
+
const actionResult = deriveShellActionKeys(analysis);
|
|
199
|
+
|
|
200
|
+
if (!actionResult.isSimpleAction || !actionResult.primarySegment) {
|
|
201
|
+
// Complex command — exact only
|
|
202
|
+
return [{ label: trimmed, description: 'This exact compound command', pattern: trimmed }];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const options: AllowlistOption[] = [];
|
|
206
|
+
|
|
207
|
+
// Full original command text — "this exact command" means exactly what the user approved
|
|
208
|
+
options.push({ label: trimmed, description: 'This exact command', pattern: trimmed });
|
|
209
|
+
|
|
210
|
+
// Action keys from narrowest to broadest
|
|
211
|
+
for (const actionKey of actionResult.keys) {
|
|
212
|
+
const keyTokens = actionKey.key.replace(/^action:/, '');
|
|
213
|
+
options.push({
|
|
214
|
+
label: `${keyTokens} *`,
|
|
215
|
+
description: `Any "${keyTokens}" command`,
|
|
216
|
+
pattern: actionKey.key,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Deduplicate by pattern
|
|
221
|
+
const seen = new Set<string>();
|
|
222
|
+
return options.filter((o) => {
|
|
223
|
+
if (seen.has(o.pattern)) return false;
|
|
224
|
+
seen.add(o.pattern);
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, chmodSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
|
-
import {
|
|
4
|
+
import { Minimatch } from 'minimatch';
|
|
5
5
|
import { getRootDir } from '../util/platform.js';
|
|
6
6
|
import { getLogger } from '../util/logger.js';
|
|
7
7
|
import { getDefaultRuleTemplates } from './defaults.js';
|
|
@@ -21,6 +21,50 @@ interface TrustFile {
|
|
|
21
21
|
let cachedRules: TrustRule[] | null = null;
|
|
22
22
|
let cachedStarterBundleAccepted: boolean | null = null;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Cache of pre-compiled Minimatch objects keyed by pattern string.
|
|
26
|
+
* Rebuilt whenever cachedRules changes. Avoids re-parsing glob patterns
|
|
27
|
+
* on every tool-call permission check.
|
|
28
|
+
*/
|
|
29
|
+
const compiledPatterns = new Map<string, Minimatch>();
|
|
30
|
+
|
|
31
|
+
/** Get or compile a Minimatch object for the given pattern. Returns null if the pattern is invalid. */
|
|
32
|
+
function getCompiledPattern(pattern: string): Minimatch | null {
|
|
33
|
+
let compiled = compiledPatterns.get(pattern);
|
|
34
|
+
if (!compiled) {
|
|
35
|
+
if (typeof pattern !== 'string') {
|
|
36
|
+
log.warn({ pattern }, 'Cannot compile non-string pattern');
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
compiled = new Minimatch(pattern);
|
|
41
|
+
compiledPatterns.set(pattern, compiled);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log.warn({ pattern, err }, 'Failed to compile pattern');
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return compiled;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Rebuild the compiled pattern cache from the current rule set. */
|
|
51
|
+
function rebuildPatternCache(rules: TrustRule[]): void {
|
|
52
|
+
compiledPatterns.clear();
|
|
53
|
+
for (const rule of rules) {
|
|
54
|
+
if (typeof rule.pattern !== 'string') {
|
|
55
|
+
log.warn({ ruleId: rule.id, pattern: rule.pattern }, 'Skipping rule with non-string pattern during cache rebuild');
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!compiledPatterns.has(rule.pattern)) {
|
|
59
|
+
try {
|
|
60
|
+
compiledPatterns.set(rule.pattern, new Minimatch(rule.pattern));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn({ ruleId: rule.id, pattern: rule.pattern, err }, 'Skipping rule with invalid pattern during cache rebuild');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
24
68
|
function getTrustPath(): string {
|
|
25
69
|
return join(getRootDir(), 'protected', 'trust.json');
|
|
26
70
|
}
|
|
@@ -201,6 +245,22 @@ function loadFromDisk(): TrustRule[] {
|
|
|
201
245
|
log.info({ ruleCount: rules.length }, 'Migrated v2 trust rules to v3 (principal fields)');
|
|
202
246
|
} else if (data.version === TRUST_FILE_VERSION) {
|
|
203
247
|
rules = rawRules;
|
|
248
|
+
|
|
249
|
+
// Strip legacy principal-scoped fields from persisted v3 rules.
|
|
250
|
+
// Before the principal concept was removed, rules could carry
|
|
251
|
+
// principalKind/principalId/principalVersion which acted as scope
|
|
252
|
+
// constraints. Now that matching ignores those fields, leaving them
|
|
253
|
+
// on loaded rules would silently widen their scope to global
|
|
254
|
+
// wildcards. Stripping them and re-saving prevents scope escalation.
|
|
255
|
+
for (const rule of rules) {
|
|
256
|
+
const r = rule as unknown as Record<string, unknown>;
|
|
257
|
+
if ('principalKind' in r || 'principalId' in r || 'principalVersion' in r) {
|
|
258
|
+
delete r.principalKind;
|
|
259
|
+
delete r.principalId;
|
|
260
|
+
delete r.principalVersion;
|
|
261
|
+
needsSave = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
204
264
|
} else if (data.version !== 1) {
|
|
205
265
|
log.warn({ version: data.version }, 'Unknown trust file version, applying defaults in-memory only');
|
|
206
266
|
// Apply default deny rules in-memory so the assistant is still
|
|
@@ -262,6 +322,7 @@ function saveToDisk(rules: TrustRule[]): void {
|
|
|
262
322
|
function getRules(): TrustRule[] {
|
|
263
323
|
if (cachedRules === null) {
|
|
264
324
|
cachedRules = loadFromDisk();
|
|
325
|
+
rebuildPatternCache(cachedRules);
|
|
265
326
|
}
|
|
266
327
|
return cachedRules;
|
|
267
328
|
}
|
|
@@ -274,9 +335,6 @@ export function addRule(
|
|
|
274
335
|
priority: number = 100,
|
|
275
336
|
options?: {
|
|
276
337
|
allowHighRisk?: boolean;
|
|
277
|
-
principalKind?: string;
|
|
278
|
-
principalId?: string;
|
|
279
|
-
principalVersion?: string;
|
|
280
338
|
executionTarget?: string;
|
|
281
339
|
},
|
|
282
340
|
): TrustRule {
|
|
@@ -296,21 +354,13 @@ export function addRule(
|
|
|
296
354
|
if (options?.allowHighRisk != null) {
|
|
297
355
|
rule.allowHighRisk = options.allowHighRisk;
|
|
298
356
|
}
|
|
299
|
-
if (options?.principalKind != null) {
|
|
300
|
-
rule.principalKind = options.principalKind;
|
|
301
|
-
}
|
|
302
|
-
if (options?.principalId != null) {
|
|
303
|
-
rule.principalId = options.principalId;
|
|
304
|
-
}
|
|
305
|
-
if (options?.principalVersion != null) {
|
|
306
|
-
rule.principalVersion = options.principalVersion;
|
|
307
|
-
}
|
|
308
357
|
if (options?.executionTarget != null) {
|
|
309
358
|
rule.executionTarget = options.executionTarget;
|
|
310
359
|
}
|
|
311
360
|
rules.push(rule);
|
|
312
361
|
rules.sort(ruleOrder);
|
|
313
362
|
cachedRules = rules;
|
|
363
|
+
rebuildPatternCache(rules);
|
|
314
364
|
saveToDisk(rules);
|
|
315
365
|
log.info({ rule }, 'Added trust rule');
|
|
316
366
|
return rule;
|
|
@@ -337,6 +387,7 @@ export function updateRule(
|
|
|
337
387
|
rules[index] = rule;
|
|
338
388
|
rules.sort(ruleOrder);
|
|
339
389
|
cachedRules = rules;
|
|
390
|
+
rebuildPatternCache(rules);
|
|
340
391
|
saveToDisk(rules);
|
|
341
392
|
log.info({ rule }, 'Updated trust rule');
|
|
342
393
|
return rule;
|
|
@@ -353,6 +404,7 @@ export function removeRule(id: string): boolean {
|
|
|
353
404
|
if (index === -1) return false;
|
|
354
405
|
rules.splice(index, 1);
|
|
355
406
|
cachedRules = rules;
|
|
407
|
+
rebuildPatternCache(rules);
|
|
356
408
|
saveToDisk(rules);
|
|
357
409
|
log.info({ id }, 'Removed trust rule');
|
|
358
410
|
return true;
|
|
@@ -372,47 +424,14 @@ function findRuleByDecision(tool: string, command: string, scope: string, decisi
|
|
|
372
424
|
for (const rule of rules) {
|
|
373
425
|
if (rule.tool !== tool) continue;
|
|
374
426
|
if (rule.decision !== decision) continue;
|
|
375
|
-
|
|
427
|
+
const compiled = getCompiledPattern(rule.pattern);
|
|
428
|
+
if (!compiled || !compiled.match(command)) continue;
|
|
376
429
|
if (!matchesScope(rule.scope, scope)) continue;
|
|
377
430
|
return rule;
|
|
378
431
|
}
|
|
379
432
|
return null;
|
|
380
433
|
}
|
|
381
434
|
|
|
382
|
-
/**
|
|
383
|
-
* Check whether a rule's principal constraints match the given policy context.
|
|
384
|
-
*
|
|
385
|
-
* A missing field on the rule acts as a wildcard — it matches any value
|
|
386
|
-
* (or absence) in the context. When a rule specifies a principal field,
|
|
387
|
-
* the context must provide a matching value for the rule to apply.
|
|
388
|
-
*/
|
|
389
|
-
function matchesPrincipal(rule: TrustRule, ctx?: PolicyContext): boolean {
|
|
390
|
-
// If the rule has no principal constraints it matches everything (wildcard).
|
|
391
|
-
if (rule.principalKind == null && rule.principalId == null && rule.principalVersion == null) {
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const principal = ctx?.principal;
|
|
396
|
-
|
|
397
|
-
// Rule specifies a principalKind — context must supply one that matches.
|
|
398
|
-
if (rule.principalKind != null) {
|
|
399
|
-
if (principal?.kind !== rule.principalKind) return false;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Rule specifies a principalId — context must supply one that matches.
|
|
403
|
-
if (rule.principalId != null) {
|
|
404
|
-
if (principal?.id !== rule.principalId) return false;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Rule specifies a principalVersion — context must supply one that matches.
|
|
408
|
-
// If the rule omits principalVersion, any version (or none) is accepted.
|
|
409
|
-
if (rule.principalVersion != null) {
|
|
410
|
-
if (principal?.version !== rule.principalVersion) return false;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return true;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
435
|
/**
|
|
417
436
|
* Check whether a rule's executionTarget constraint matches the context.
|
|
418
437
|
*
|
|
@@ -428,9 +447,9 @@ function matchesExecutionTarget(rule: TrustRule, ctx?: PolicyContext): boolean {
|
|
|
428
447
|
* Find the highest-priority rule that matches any of the command candidates.
|
|
429
448
|
* Rules are pre-sorted by priority descending, so the first match wins.
|
|
430
449
|
*
|
|
431
|
-
* When a `PolicyContext` is provided, rules that specify
|
|
432
|
-
*
|
|
433
|
-
*
|
|
450
|
+
* When a `PolicyContext` is provided, rules that specify executionTarget
|
|
451
|
+
* constraints are filtered accordingly. Rules without those constraints
|
|
452
|
+
* act as wildcards and match any context.
|
|
434
453
|
*/
|
|
435
454
|
export function findHighestPriorityRule(tool: string, commands: string[], scope: string, ctx?: PolicyContext): TrustRule | null {
|
|
436
455
|
// Check ephemeral (task-scoped) rules first — they take precedence over
|
|
@@ -449,10 +468,11 @@ export function findHighestPriorityRule(tool: string, commands: string[], scope:
|
|
|
449
468
|
for (const rule of allRules) {
|
|
450
469
|
if (rule.tool !== tool) continue;
|
|
451
470
|
if (!matchesScope(rule.scope, scope)) continue;
|
|
452
|
-
if (!matchesPrincipal(rule, ctx)) continue;
|
|
453
471
|
if (!matchesExecutionTarget(rule, ctx)) continue;
|
|
472
|
+
const compiled = getCompiledPattern(rule.pattern);
|
|
473
|
+
if (!compiled) continue;
|
|
454
474
|
for (const command of commands) {
|
|
455
|
-
if (
|
|
475
|
+
if (compiled.match(command)) {
|
|
456
476
|
return rule;
|
|
457
477
|
}
|
|
458
478
|
}
|
|
@@ -480,6 +500,7 @@ export function clearAllRules(): void {
|
|
|
480
500
|
backfillDefaults(rules);
|
|
481
501
|
rules.sort(ruleOrder);
|
|
482
502
|
cachedRules = rules;
|
|
503
|
+
rebuildPatternCache(rules);
|
|
483
504
|
saveToDisk(rules);
|
|
484
505
|
log.info('Cleared all user trust rules (default rules preserved)');
|
|
485
506
|
}
|
|
@@ -487,6 +508,7 @@ export function clearAllRules(): void {
|
|
|
487
508
|
export function clearCache(): void {
|
|
488
509
|
cachedRules = null;
|
|
489
510
|
cachedStarterBundleAccepted = null;
|
|
511
|
+
compiledPatterns.clear();
|
|
490
512
|
}
|
|
491
513
|
|
|
492
514
|
// ─── Starter approval bundle ────────────────────────────────────────────────
|
|
@@ -577,6 +599,7 @@ export function acceptStarterBundle(): AcceptStarterBundleResult {
|
|
|
577
599
|
cachedStarterBundleAccepted = true;
|
|
578
600
|
rules.sort(ruleOrder);
|
|
579
601
|
cachedRules = rules;
|
|
602
|
+
rebuildPatternCache(rules);
|
|
580
603
|
saveToDisk(rules);
|
|
581
604
|
log.info({ rulesAdded: added }, 'Starter approval bundle accepted');
|
|
582
605
|
|