@vellumai/assistant 0.3.19 → 0.3.21
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -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 +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -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 +300 -32
- 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 +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- 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 +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -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 +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- 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__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- 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 +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- 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 +1 -1
- 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/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- 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 +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -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/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/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-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- 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 +258 -432
- 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/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- 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 +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- 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/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 +92 -35
- 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 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- 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 +1 -1
- 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/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- 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 +10 -2
- 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation guard tests for the unified feature flag registry.
|
|
3
|
+
*
|
|
4
|
+
* Ensures structural invariants hold so that both the TS and Swift loaders
|
|
5
|
+
* can safely consume the registry without runtime surprises.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function getRepoRoot(): string {
|
|
18
|
+
return join(process.cwd(), '..');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getRegistryPath(): string {
|
|
22
|
+
return join(getRepoRoot(), 'meta', 'feature-flags', 'feature-flag-registry.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadRegistry(): Record<string, unknown> {
|
|
26
|
+
const raw = readFileSync(getRegistryPath(), 'utf-8');
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VALID_SCOPES = new Set(['assistant', 'macos']);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tests
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
describe('unified feature flag registry guard', () => {
|
|
37
|
+
const registry = loadRegistry();
|
|
38
|
+
const flags = registry.flags as Record<string, unknown>[];
|
|
39
|
+
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
// version
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
test('version is a positive integer', () => {
|
|
45
|
+
expect(typeof registry.version).toBe('number');
|
|
46
|
+
expect(Number.isInteger(registry.version)).toBe(true);
|
|
47
|
+
expect(registry.version as number).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// -----------------------------------------------------------------------
|
|
51
|
+
// required fields and types
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test('all flags have required fields with correct types', () => {
|
|
55
|
+
const violations: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < flags.length; i++) {
|
|
58
|
+
const flag = flags[i];
|
|
59
|
+
const prefix = `flags[${i}]`;
|
|
60
|
+
|
|
61
|
+
if (typeof flag !== 'object' || !flag || Array.isArray(flag)) {
|
|
62
|
+
violations.push(`${prefix}: entry is not an object`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof flag.id !== 'string' || flag.id.length === 0) {
|
|
67
|
+
violations.push(`${prefix}: missing or non-string 'id'`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof flag.scope !== 'string' || flag.scope.length === 0) {
|
|
70
|
+
violations.push(`${prefix}: missing or non-string 'scope'`);
|
|
71
|
+
}
|
|
72
|
+
if (typeof flag.key !== 'string' || flag.key.length === 0) {
|
|
73
|
+
violations.push(`${prefix}: missing or non-string 'key'`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof flag.label !== 'string' || flag.label.length === 0) {
|
|
76
|
+
violations.push(`${prefix}: missing or non-string 'label'`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof flag.description !== 'string' || flag.description.length === 0) {
|
|
79
|
+
violations.push(`${prefix}: missing or non-string 'description'`);
|
|
80
|
+
}
|
|
81
|
+
if (typeof flag.defaultEnabled !== 'boolean') {
|
|
82
|
+
violations.push(`${prefix}: missing or non-boolean 'defaultEnabled'`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (violations.length > 0) {
|
|
87
|
+
const message = [
|
|
88
|
+
'Found flags with missing or incorrectly-typed required fields.',
|
|
89
|
+
'',
|
|
90
|
+
'Violations:',
|
|
91
|
+
...violations.map((v) => ` - ${v}`),
|
|
92
|
+
].join('\n');
|
|
93
|
+
|
|
94
|
+
expect(violations, message).toEqual([]);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// -----------------------------------------------------------------------
|
|
99
|
+
// unique ids
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
test('all id values are unique', () => {
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
const duplicates: string[] = [];
|
|
105
|
+
|
|
106
|
+
for (const flag of flags) {
|
|
107
|
+
const id = flag.id as string;
|
|
108
|
+
if (seen.has(id)) {
|
|
109
|
+
duplicates.push(id);
|
|
110
|
+
}
|
|
111
|
+
seen.add(id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (duplicates.length > 0) {
|
|
115
|
+
const message = [
|
|
116
|
+
'Found duplicate flag id values in the registry.',
|
|
117
|
+
'',
|
|
118
|
+
'Duplicates:',
|
|
119
|
+
...duplicates.map((d) => ` - ${d}`),
|
|
120
|
+
].join('\n');
|
|
121
|
+
|
|
122
|
+
expect(duplicates, message).toEqual([]);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
// unique keys
|
|
128
|
+
// -----------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
test('all key values are unique', () => {
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
const duplicates: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (const flag of flags) {
|
|
135
|
+
const key = flag.key as string;
|
|
136
|
+
if (seen.has(key)) {
|
|
137
|
+
duplicates.push(key);
|
|
138
|
+
}
|
|
139
|
+
seen.add(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (duplicates.length > 0) {
|
|
143
|
+
const message = [
|
|
144
|
+
'Found duplicate flag key values in the registry.',
|
|
145
|
+
'',
|
|
146
|
+
'Duplicates:',
|
|
147
|
+
...duplicates.map((d) => ` - ${d}`),
|
|
148
|
+
].join('\n');
|
|
149
|
+
|
|
150
|
+
expect(duplicates, message).toEqual([]);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// -----------------------------------------------------------------------
|
|
155
|
+
// valid scopes
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
test('all scope values are valid', () => {
|
|
159
|
+
const violations: string[] = [];
|
|
160
|
+
|
|
161
|
+
for (const flag of flags) {
|
|
162
|
+
const scope = flag.scope as string;
|
|
163
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
164
|
+
violations.push(`flag '${flag.id}' has invalid scope '${scope}' (expected 'assistant' or 'macos')`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (violations.length > 0) {
|
|
169
|
+
const message = [
|
|
170
|
+
'Found flags with invalid scope values.',
|
|
171
|
+
'',
|
|
172
|
+
'Violations:',
|
|
173
|
+
...violations.map((v) => ` - ${v}`),
|
|
174
|
+
].join('\n');
|
|
175
|
+
|
|
176
|
+
expect(violations, message).toEqual([]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical assistant feature-flag resolver.
|
|
3
|
+
*
|
|
4
|
+
* Loads default flag values from the unified registry at
|
|
5
|
+
* `meta/feature-flags/feature-flag-registry.json` and resolves the effective
|
|
6
|
+
* enabled/disabled state for each declared assistant-scope flag by consulting
|
|
7
|
+
* (in priority order):
|
|
8
|
+
* 1. `config.assistantFeatureFlagValues[key]` (explicit override)
|
|
9
|
+
* 2. defaults registry `defaultEnabled` (for declared keys)
|
|
10
|
+
* 3. `true` (for undeclared keys)
|
|
11
|
+
*
|
|
12
|
+
* Key format:
|
|
13
|
+
* Canonical: `feature_flags.<id>.enabled`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import type { AssistantConfig } from './schema.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface FeatureFlagDefault {
|
|
26
|
+
defaultEnabled: boolean;
|
|
27
|
+
description: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type FeatureFlagDefaultsRegistry = Record<string, FeatureFlagDefault>;
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Registry loading (singleton, loaded once)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
let cachedDefaults: FeatureFlagDefaultsRegistry | undefined;
|
|
38
|
+
|
|
39
|
+
const REGISTRY_FILENAME = 'feature-flag-registry.json';
|
|
40
|
+
|
|
41
|
+
function loadDefaultsRegistry(): FeatureFlagDefaultsRegistry {
|
|
42
|
+
if (cachedDefaults) return cachedDefaults;
|
|
43
|
+
|
|
44
|
+
const thisDir = import.meta.dirname ?? __dirname;
|
|
45
|
+
const envPath = process.env.FEATURE_FLAG_DEFAULTS_PATH?.trim();
|
|
46
|
+
const candidates = [
|
|
47
|
+
// Explicit override (primarily for tests / controlled environments)
|
|
48
|
+
...(envPath ? [envPath] : []),
|
|
49
|
+
// Bundled: co-located copy in the same directory as this source file.
|
|
50
|
+
// Works in Docker / packaged builds where the repo-root `meta/` dir
|
|
51
|
+
// is not available.
|
|
52
|
+
join(thisDir, REGISTRY_FILENAME),
|
|
53
|
+
// Packaged macOS app layout: the daemon binary lives at
|
|
54
|
+
// <App>.app/Contents/MacOS/vellum-daemon and the registry is copied
|
|
55
|
+
// to <App>.app/Contents/Resources/ by build.sh. In bun --compile
|
|
56
|
+
// binaries, import.meta.dirname resolves to /$bunfs/root (virtual),
|
|
57
|
+
// so we need to resolve relative to the real executable path.
|
|
58
|
+
join(dirname(process.execPath), '..', 'Resources', REGISTRY_FILENAME),
|
|
59
|
+
// Development: relative to this source file's directory, walking up
|
|
60
|
+
// to the repo root to reach `meta/feature-flags/`.
|
|
61
|
+
join(thisDir, '..', '..', '..', 'meta', 'feature-flags', REGISTRY_FILENAME),
|
|
62
|
+
// Alternate: from repo root via cwd
|
|
63
|
+
join(process.cwd(), 'meta', 'feature-flags', REGISTRY_FILENAME),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (existsSync(candidate)) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(candidate, 'utf-8');
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
cachedDefaults = parseRegistryToDefaults(parsed);
|
|
72
|
+
return cachedDefaults;
|
|
73
|
+
} catch {
|
|
74
|
+
// Malformed file — fall through to next candidate
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
cachedDefaults = {};
|
|
80
|
+
return cachedDefaults;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse the unified registry JSON into a flat key -> default map,
|
|
85
|
+
* filtering to assistant-scope flags only.
|
|
86
|
+
*/
|
|
87
|
+
function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
|
|
88
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
89
|
+
|
|
90
|
+
const registry = parsed as { version?: number; flags?: unknown[] };
|
|
91
|
+
if (!Array.isArray(registry.flags)) return {};
|
|
92
|
+
|
|
93
|
+
const result: FeatureFlagDefaultsRegistry = {};
|
|
94
|
+
for (const flag of registry.flags) {
|
|
95
|
+
if (!flag || typeof flag !== 'object' || Array.isArray(flag)) continue;
|
|
96
|
+
const entry = flag as Record<string, unknown>;
|
|
97
|
+
if (entry.scope !== 'assistant') continue;
|
|
98
|
+
if (typeof entry.key !== 'string') continue;
|
|
99
|
+
if (typeof entry.defaultEnabled !== 'boolean') continue;
|
|
100
|
+
|
|
101
|
+
result[entry.key as string] = {
|
|
102
|
+
defaultEnabled: entry.defaultEnabled,
|
|
103
|
+
description: typeof entry.description === 'string' ? entry.description : '',
|
|
104
|
+
label: typeof entry.label === 'string' ? entry.label : '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Public API
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve whether an assistant feature flag is enabled.
|
|
116
|
+
*
|
|
117
|
+
* Resolution order:
|
|
118
|
+
* 1. `config.assistantFeatureFlagValues[key]` (explicit override)
|
|
119
|
+
* 2. defaults registry `defaultEnabled` (for declared assistant-scope keys)
|
|
120
|
+
* 3. `true` (for undeclared keys with no override)
|
|
121
|
+
*/
|
|
122
|
+
export function isAssistantFeatureFlagEnabled(key: string, config: AssistantConfig): boolean {
|
|
123
|
+
const defaults = loadDefaultsRegistry();
|
|
124
|
+
const declared = defaults[key];
|
|
125
|
+
|
|
126
|
+
// 1. Check canonical section
|
|
127
|
+
const newValues = (config as AssistantConfigWithFeatureFlags).assistantFeatureFlagValues;
|
|
128
|
+
if (newValues) {
|
|
129
|
+
const explicit = newValues[key];
|
|
130
|
+
if (typeof explicit === 'boolean') return explicit;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. For declared keys, use the registry default
|
|
134
|
+
if (declared) {
|
|
135
|
+
return declared.defaultEnabled;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3. Undeclared keys with no persisted override default to enabled
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Return the loaded defaults registry (for introspection/tooling).
|
|
144
|
+
*/
|
|
145
|
+
export function getAssistantFeatureFlagDefaults(): FeatureFlagDefaultsRegistry {
|
|
146
|
+
return loadDefaultsRegistry();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reset the cached defaults registry. Intended for tests only.
|
|
151
|
+
*/
|
|
152
|
+
export function _resetDefaultsCache(): void {
|
|
153
|
+
cachedDefaults = undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Internal type augmentation for the new config field
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface AssistantConfigWithFeatureFlags extends AssistantConfig {
|
|
161
|
+
assistantFeatureFlagValues?: Record<string, boolean>;
|
|
162
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="16" height="16" fill="#0f172a"/>
|
|
3
|
+
<rect x="2" y="2" width="12" height="12" fill="#1e293b"/>
|
|
4
|
+
<rect x="3" y="3" width="10" height="10" fill="#0f172a" stroke="#3b82f6" stroke-width="1"/>
|
|
5
|
+
<rect x="4" y="4" width="2" height="2" fill="#3b82f6"/>
|
|
6
|
+
<rect x="10" y="4" width="2" height="2" fill="#3b82f6"/>
|
|
7
|
+
<rect x="4" y="10" width="2" height="2" fill="#3b82f6"/>
|
|
8
|
+
<rect x="10" y="10" width="2" height="2" fill="#3b82f6"/>
|
|
9
|
+
<line x1="5" y1="5" x2="11" y2="5" stroke="#10b981" stroke-width="1"/>
|
|
10
|
+
<line x1="5" y1="5" x2="5" y2="11" stroke="#10b981" stroke-width="1"/>
|
|
11
|
+
<line x1="11" y1="5" x2="11" y2="11" stroke="#10b981" stroke-width="1"/>
|
|
12
|
+
<line x1="5" y1="11" x2="11" y2="11" stroke="#10b981" stroke-width="1"/>
|
|
13
|
+
<rect x="7" y="7" width="2" height="2" fill="#f59e0b"/>
|
|
14
|
+
<line x1="5" y1="8" x2="7" y2="8" stroke="#60a5fa" stroke-width="1"/>
|
|
15
|
+
<line x1="9" y1="8" x2="11" y2="8" stroke="#60a5fa" stroke-width="1"/>
|
|
16
|
+
<line x1="8" y1="5" x2="8" y2="7" stroke="#60a5fa" stroke-width="1"/>
|
|
17
|
+
<line x1="8" y1="9" x2="8" y2="11" stroke="#60a5fa" stroke-width="1"/>
|
|
18
|
+
</svg>
|
|
@@ -256,6 +256,36 @@
|
|
|
256
256
|
"executor": "tools/slack-add-reaction.ts",
|
|
257
257
|
"execution_target": "host"
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"name": "slack_delete_message",
|
|
261
|
+
"description": "Delete a Slack message posted by the bot. Include a confidence score (0-1).",
|
|
262
|
+
"category": "messaging",
|
|
263
|
+
"risk": "high",
|
|
264
|
+
"input_schema": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"channel": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"description": "Slack channel ID"
|
|
270
|
+
},
|
|
271
|
+
"timestamp": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "Message timestamp (ts) to delete"
|
|
274
|
+
},
|
|
275
|
+
"confidence": {
|
|
276
|
+
"type": "number",
|
|
277
|
+
"description": "Confidence score (0-1) for this action"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"required": [
|
|
281
|
+
"channel",
|
|
282
|
+
"timestamp",
|
|
283
|
+
"confidence"
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
"executor": "tools/slack-delete-message.ts",
|
|
287
|
+
"execution_target": "host"
|
|
288
|
+
},
|
|
259
289
|
{
|
|
260
290
|
"name": "slack_leave_channel",
|
|
261
291
|
"description": "Leave a Slack channel. Include a confidence score (0-1).",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { deleteMessage } from '../../../../messaging/providers/slack/client.js';
|
|
2
|
+
import { getMessagingProvider } from '../../../../messaging/registry.js';
|
|
3
|
+
import { withValidToken } from '../../../../security/token-manager.js';
|
|
4
|
+
import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
|
|
5
|
+
import { err, ok } from './shared.js';
|
|
6
|
+
|
|
7
|
+
export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
|
|
8
|
+
const channel = input.channel as string;
|
|
9
|
+
const timestamp = input.timestamp as string;
|
|
10
|
+
|
|
11
|
+
if (!channel || !timestamp) {
|
|
12
|
+
return err('channel and timestamp are both required.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const provider = getMessagingProvider('slack');
|
|
17
|
+
return withValidToken(provider.credentialService, async (token) => {
|
|
18
|
+
await deleteMessage(token, channel, timestamp);
|
|
19
|
+
return ok(`Message deleted.`);
|
|
20
|
+
});
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -14,7 +14,7 @@ Use `send_notification` for user-facing alerts and notifications. This tool rout
|
|
|
14
14
|
|
|
15
15
|
## Deduplication (`dedupe_key`)
|
|
16
16
|
|
|
17
|
-
- `dedupe_key` suppresses duplicate signals
|
|
17
|
+
- `dedupe_key` suppresses duplicate signals **permanently**. A second notification with the same key is **dropped entirely** for the lifetime of the assistant's event store. Once a key has been used, it cannot be reused — any future notification with the same key will be silently discarded.
|
|
18
18
|
- Never reuse a `dedupe_key` across logically distinct notifications, even if they are related. The key means "this exact event already fired," not "these events are in the same category."
|
|
19
19
|
- If you omit `dedupe_key`, the LLM decision engine may generate one automatically based on signal context. This means even keyless signals can be deduplicated if the engine considers them duplicates of a recent event.
|
|
20
20
|
|
|
@@ -15,12 +15,34 @@ Create, list, and cancel one-time reminders. Reminders fire at a specific future
|
|
|
15
15
|
|
|
16
16
|
Control how the reminder is delivered at trigger time with `routing_intent`:
|
|
17
17
|
|
|
18
|
-
- **single_channel**
|
|
18
|
+
- **single_channel** — deliver to one best channel
|
|
19
19
|
- **multi_channel** — deliver to a subset of channels
|
|
20
|
-
- **all_channels** — deliver to every available channel
|
|
20
|
+
- **all_channels** (default) — deliver to every available channel
|
|
21
21
|
|
|
22
22
|
Optionally pass `routing_hints` (a JSON object) to influence routing decisions (e.g. preferred channels, exclusions). When omitted, defaults to `{}`.
|
|
23
23
|
|
|
24
|
+
### Routing Defaults
|
|
25
|
+
|
|
26
|
+
Use the following heuristics to pick `routing_intent`:
|
|
27
|
+
|
|
28
|
+
- **Default to `all_channels`** for most reminders. Users setting reminders usually want to be notified wherever they are, and redundant notifications are less harmful than missed ones.
|
|
29
|
+
- **Use `single_channel`** only when the user explicitly specifies a single channel (e.g. "remind me on Telegram") or the reminder is low-stakes and noise reduction matters.
|
|
30
|
+
- **Check `user_message_channel`** from the turn context. If the user is currently active on a specific channel (e.g. `vellum`), always include that channel. Pass it as a routing hint:
|
|
31
|
+
```
|
|
32
|
+
routing_hints: { preferred_channels: ["vellum"] }
|
|
33
|
+
routing_intent: "all_channels"
|
|
34
|
+
```
|
|
35
|
+
- **Never use `single_channel` as a passive default.** If you haven't thought about which channel to use, use `all_channels`.
|
|
36
|
+
|
|
37
|
+
### Examples
|
|
38
|
+
|
|
39
|
+
| Scenario | routing_intent | routing_hints |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| User sets reminder from desktop app | `all_channels` | `{ preferred_channels: ["vellum"] }` |
|
|
42
|
+
| User says "remind me on Telegram" | `single_channel` | `{ preferred_channels: ["telegram"] }` |
|
|
43
|
+
| User sets reminder from Telegram | `all_channels` | `{ preferred_channels: ["telegram"] }` |
|
|
44
|
+
| No channel preference expressed | `all_channels` | `{}` |
|
|
45
|
+
|
|
24
46
|
## Usage Notes
|
|
25
47
|
|
|
26
48
|
- Use reminders ONLY for time-triggered notifications (e.g. "remind me at 3pm", "remind me in 2 hours").
|
|
@@ -28,3 +50,28 @@ Optionally pass `routing_hints` (a JSON object) to influence routing decisions (
|
|
|
28
50
|
- For task tracking ("add to my tasks", "add to my queue"), use task_list_add instead.
|
|
29
51
|
- `fire_at` must be a strict ISO 8601 timestamp with timezone offset or Z (e.g. `2025-03-15T09:00:00-05:00` or `2025-03-15T09:00:00Z`). Ambiguous timestamps without timezone info will be rejected.
|
|
30
52
|
- `label` is a short human-readable summary shown in the notification.
|
|
53
|
+
|
|
54
|
+
### Anchored & Ambiguous Relative Time
|
|
55
|
+
|
|
56
|
+
Phrases like "at the 45 minute mark", "at the top of the hour", "on the half-hour", "at noon", "20 minutes in", or "when I hit an hour" are **clock-position or anchored relative time** expressions. Do NOT treat them as offsets from now.
|
|
57
|
+
|
|
58
|
+
**Resolution rules (in priority order):**
|
|
59
|
+
|
|
60
|
+
1. **Clock-position expressions** — map directly to a wall-clock time:
|
|
61
|
+
- "top of the hour" / "on the hour" → next :00 (e.g. 10:00 AM)
|
|
62
|
+
- "the X minute mark" / "at :XX" → current hour's :XX; if already past, advance one hour
|
|
63
|
+
- "the half-hour mark" / "half past" → nearest upcoming :30
|
|
64
|
+
- "noon" / "midnight" → 12:00 PM or 12:00 AM today; if past, tomorrow
|
|
65
|
+
- "quarter past" / "quarter to" → :15 or :45 of current or next hour
|
|
66
|
+
|
|
67
|
+
2. **Session-anchored expressions** — if the user mentioned a start time earlier in conversation ("I got here at 9", "meeting started at 2pm"), compute `start_time + offset`.
|
|
68
|
+
|
|
69
|
+
3. **Ask only if truly ambiguous** — if neither rule 1 nor rule 2 resolves, ask: "Do you mean [clock time] or [X minutes from now]?" Never silently default to "from now."
|
|
70
|
+
|
|
71
|
+
**Examples:**
|
|
72
|
+
- "at the 45 min mark" (now: 9:39) → 9:45 AM
|
|
73
|
+
- "at the 45 min mark" (now: 9:50) → 10:45 AM
|
|
74
|
+
- "top of the hour" (now: 9:39) → 10:00 AM
|
|
75
|
+
- "at noon" → 12:00 PM today
|
|
76
|
+
- "20 minutes in, I started at 2pm" → 2:20 PM
|
|
77
|
+
- "at the hour mark" with no start time → ask for clarification
|
|
@@ -55,6 +55,31 @@ When the user says "in X minutes/hours", compute the ISO 8601 timestamp yourself
|
|
|
55
55
|
- Format as ISO 8601 with timezone: `2025-03-15T09:05:00-05:00`
|
|
56
56
|
- Pass to `reminder_create` as `fire_at`
|
|
57
57
|
|
|
58
|
+
### Anchored & Ambiguous Relative Time
|
|
59
|
+
|
|
60
|
+
Phrases like "at the 45 minute mark", "at the top of the hour", "on the half-hour", "at noon", "20 minutes in", or "when I hit an hour" are **clock-position or anchored relative time** expressions. Do NOT treat them as offsets from now.
|
|
61
|
+
|
|
62
|
+
**Resolution rules (in priority order):**
|
|
63
|
+
|
|
64
|
+
1. **Clock-position expressions** — map directly to a wall-clock time:
|
|
65
|
+
- "top of the hour" / "on the hour" → next :00 (e.g. 10:00 AM)
|
|
66
|
+
- "the X minute mark" / "at :XX" → current hour's :XX; if already past, advance one hour
|
|
67
|
+
- "the half-hour mark" / "half past" → nearest upcoming :30
|
|
68
|
+
- "noon" / "midnight" → 12:00 PM or 12:00 AM today; if past, tomorrow
|
|
69
|
+
- "quarter past" / "quarter to" → :15 or :45 of current or next hour
|
|
70
|
+
|
|
71
|
+
2. **Session-anchored expressions** — if the user mentioned a start time earlier in conversation ("I got here at 9", "meeting started at 2pm"), compute `start_time + offset`.
|
|
72
|
+
|
|
73
|
+
3. **Ask only if truly ambiguous** — if neither rule 1 nor rule 2 resolves, ask: "Do you mean [clock time] or [X minutes from now]?" Never silently default to "from now."
|
|
74
|
+
|
|
75
|
+
**Examples:**
|
|
76
|
+
- "at the 45 min mark" (now: 9:39) → 9:45 AM
|
|
77
|
+
- "at the 45 min mark" (now: 9:50) → 10:45 AM
|
|
78
|
+
- "top of the hour" (now: 9:39) → 10:00 AM
|
|
79
|
+
- "at noon" → 12:00 PM today
|
|
80
|
+
- "20 minutes in, I started at 2pm" → 2:20 PM
|
|
81
|
+
- "at the hour mark" with no start time → ask for clarification
|
|
82
|
+
|
|
58
83
|
## "Remind me to X" Disambiguation
|
|
59
84
|
|
|
60
85
|
The word "remind" is ambiguous. Route based on whether a time is specified:
|
|
@@ -77,12 +102,34 @@ Use `notify` for simple alerts. Use `execute` when the reminder should trigger t
|
|
|
77
102
|
## Reminder Routing
|
|
78
103
|
|
|
79
104
|
`reminder_create` supports a `routing_intent` parameter that controls how the reminder is delivered at trigger time:
|
|
80
|
-
- **`single_channel`**
|
|
105
|
+
- **`single_channel`** — deliver to one best channel
|
|
81
106
|
- **`multi_channel`** — deliver to a subset of channels
|
|
82
|
-
- **`all_channels`** — deliver to every available channel
|
|
107
|
+
- **`all_channels`** (default) — deliver to every available channel
|
|
83
108
|
|
|
84
109
|
You can also pass `routing_hints` (a JSON object) to influence routing decisions (e.g. preferred channels, exclusions).
|
|
85
110
|
|
|
111
|
+
### Routing Defaults
|
|
112
|
+
|
|
113
|
+
Use the following heuristics to pick `routing_intent`:
|
|
114
|
+
|
|
115
|
+
- **Default to `all_channels`** for most reminders. Users setting reminders usually want to be notified wherever they are, and redundant notifications are less harmful than missed ones.
|
|
116
|
+
- **Use `single_channel`** only when the user explicitly specifies a single channel (e.g. "remind me on Telegram") or the reminder is low-stakes and noise reduction matters.
|
|
117
|
+
- **Check `user_message_channel`** from the turn context. If the user is currently active on a specific channel (e.g. `vellum`), always include that channel. Pass it as a routing hint:
|
|
118
|
+
```
|
|
119
|
+
routing_hints: { preferred_channels: ["vellum"] }
|
|
120
|
+
routing_intent: "all_channels"
|
|
121
|
+
```
|
|
122
|
+
- **Never use `single_channel` as a passive default.** If you haven't thought about which channel to use, use `all_channels`.
|
|
123
|
+
|
|
124
|
+
### Examples
|
|
125
|
+
|
|
126
|
+
| Scenario | routing_intent | routing_hints |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| User sets reminder from desktop app | `all_channels` | `{ preferred_channels: ["vellum"] }` |
|
|
129
|
+
| User says "remind me on Telegram" | `single_channel` | `{ preferred_channels: ["telegram"] }` |
|
|
130
|
+
| User sets reminder from Telegram | `all_channels` | `{ preferred_channels: ["telegram"] }` |
|
|
131
|
+
| No channel preference expressed | `all_channels` | `{}` |
|
|
132
|
+
|
|
86
133
|
## Tool Summary
|
|
87
134
|
|
|
88
135
|
| Tool | Timing | Recurrence | Purpose |
|