@vellumai/assistant 0.9.0 → 0.9.1-staging.1
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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static-analysis guard for the AppControl Swift sources.
|
|
3
|
-
*
|
|
4
|
-
* The app-control surface targets a *specific* host process — events must
|
|
5
|
-
* be delivered with `CGEvent.postToPid(_:)` (Swift-bridged) or its C-symbol
|
|
6
|
-
* equivalent `CGEventPostToPid(...)`. The deprecated global form
|
|
7
|
-
* `CGEventPost(...)` posts to the system-wide event tap, which would leak
|
|
8
|
-
* input to whichever app currently has user focus. That defeats the whole
|
|
9
|
-
* point of per-process app control and is a security hazard, so we keep
|
|
10
|
-
* it out of `clients/macos/vellum-assistant/AppControl/` entirely.
|
|
11
|
-
*
|
|
12
|
-
* The guard flags any standalone `CGEventPost(...)` call (i.e. the parens
|
|
13
|
-
* follow the symbol directly, not preceded by `.`). Allowed forms:
|
|
14
|
-
* - `CGEvent.postToPid(_:)` (Swift-bridged, modern idiom)
|
|
15
|
-
* - `CGEventPostToPid(...)` (C-symbol, process-scoped)
|
|
16
|
-
* - any line carrying a `// allow: CGEventPost` suppression comment
|
|
17
|
-
*
|
|
18
|
-
* If a real call site ever needs the global form, the suppression comment
|
|
19
|
-
* makes the intent explicit.
|
|
20
|
-
*/
|
|
21
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
22
|
-
import { join } from "node:path";
|
|
23
|
-
import { describe, expect, test } from "bun:test";
|
|
24
|
-
|
|
25
|
-
const APP_CONTROL_DIR = join(
|
|
26
|
-
process.cwd(),
|
|
27
|
-
"..",
|
|
28
|
-
"clients",
|
|
29
|
-
"macos",
|
|
30
|
-
"vellum-assistant",
|
|
31
|
-
"AppControl",
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
/** Recursively collect `.swift` files under `dir`. */
|
|
35
|
-
function collectSwiftFiles(dir: string): string[] {
|
|
36
|
-
const out: string[] = [];
|
|
37
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
-
const full = join(dir, entry.name);
|
|
39
|
-
if (entry.isDirectory()) {
|
|
40
|
-
out.push(...collectSwiftFiles(full));
|
|
41
|
-
} else if (entry.isFile() && entry.name.endsWith(".swift")) {
|
|
42
|
-
out.push(full);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return out;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Match a global `CGEventPost(` call: the literal symbol followed
|
|
50
|
-
* immediately by `(`. We use a negative lookbehind to exclude:
|
|
51
|
-
* - `.CGEventPost(` (member access, not a real Swift form but harmless)
|
|
52
|
-
* - `CGEventPostToPid(` (process-scoped C symbol — allowed)
|
|
53
|
-
* - `CGEvent.postToPid(` (Swift-bridged form — does not match anyway,
|
|
54
|
-
* the symbol there is `postToPid`).
|
|
55
|
-
*
|
|
56
|
-
* The regex is intentionally narrow: `\bCGEventPost\(` matches only
|
|
57
|
-
* `CGEventPost(`. `CGEventPostToPid(` does not match because the substring
|
|
58
|
-
* after `CGEventPost` is `T`, not `(`.
|
|
59
|
-
*/
|
|
60
|
-
const GLOBAL_CGEVENT_POST = /\bCGEventPost\(/;
|
|
61
|
-
|
|
62
|
-
/** Suppression comment that whitelists a single line. */
|
|
63
|
-
const ALLOW_COMMENT = /\/\/\s*allow:\s*CGEventPost/i;
|
|
64
|
-
|
|
65
|
-
describe("app-control: no global CGEventPost in Swift sources", () => {
|
|
66
|
-
test("CGEventPost(...) is forbidden in clients/macos/vellum-assistant/AppControl/", () => {
|
|
67
|
-
const files = collectSwiftFiles(APP_CONTROL_DIR);
|
|
68
|
-
expect(files.length).toBeGreaterThan(0);
|
|
69
|
-
|
|
70
|
-
const violations: string[] = [];
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const content = readFileSync(file, "utf-8");
|
|
73
|
-
const lines = content.split("\n");
|
|
74
|
-
for (let i = 0; i < lines.length; i++) {
|
|
75
|
-
const line = lines[i]!;
|
|
76
|
-
if (!GLOBAL_CGEVENT_POST.test(line)) continue;
|
|
77
|
-
if (ALLOW_COMMENT.test(line)) continue;
|
|
78
|
-
violations.push(`${file}:${i + 1}: ${line.trim()}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (violations.length > 0) {
|
|
83
|
-
const message = [
|
|
84
|
-
"Found global CGEventPost(...) calls in AppControl Swift sources.",
|
|
85
|
-
"App-control input must be process-scoped — use CGEvent.postToPid(_:)",
|
|
86
|
-
"(Swift-bridged form) or CGEventPostToPid(...) (C-symbol form). The",
|
|
87
|
-
"global form leaks events to whichever app currently has user focus.",
|
|
88
|
-
"",
|
|
89
|
-
"If a specific call site genuinely needs the global form, append a",
|
|
90
|
-
"`// allow: CGEventPost` comment to that line to suppress this guard.",
|
|
91
|
-
"",
|
|
92
|
-
"Violations:",
|
|
93
|
-
...violations.map((v) => ` - ${v}`),
|
|
94
|
-
].join("\n");
|
|
95
|
-
expect(violations, message).toEqual([]);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Mocks — declared before importing modules under test
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
const mockConfig = {
|
|
8
|
-
secretDetection: {
|
|
9
|
-
enabled: true,
|
|
10
|
-
allowOneTimeSend: false,
|
|
11
|
-
},
|
|
12
|
-
timeouts: { permissionTimeoutSec: 300 },
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function setMockNestedValue(
|
|
16
|
-
obj: Record<string, unknown>,
|
|
17
|
-
path: string,
|
|
18
|
-
value: unknown,
|
|
19
|
-
): void {
|
|
20
|
-
const keys = path.split(".");
|
|
21
|
-
let current = obj;
|
|
22
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
23
|
-
const key = keys[i];
|
|
24
|
-
if (current[key] == null || typeof current[key] !== "object") {
|
|
25
|
-
current[key] = {};
|
|
26
|
-
}
|
|
27
|
-
current = current[key] as Record<string, unknown>;
|
|
28
|
-
}
|
|
29
|
-
current[keys[keys.length - 1]] = value;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
mock.module("../config/loader.js", () => ({
|
|
33
|
-
getConfig: () => mockConfig,
|
|
34
|
-
loadConfig: () => mockConfig,
|
|
35
|
-
loadRawConfig: () => ({}),
|
|
36
|
-
saveRawConfig: () => {},
|
|
37
|
-
setNestedValue: setMockNestedValue,
|
|
38
|
-
invalidateConfigCache: () => {},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
mock.module("../util/logger.js", () => ({
|
|
42
|
-
getLogger: () =>
|
|
43
|
-
new Proxy({} as Record<string, unknown>, {
|
|
44
|
-
get: () => () => {},
|
|
45
|
-
}),
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
// Track credential store writes
|
|
49
|
-
const storedKeys = new Map<string, string>();
|
|
50
|
-
mock.module("../security/secure-keys.js", () => {
|
|
51
|
-
const syncSet = (key: string, value: string) => {
|
|
52
|
-
storedKeys.set(key, value);
|
|
53
|
-
return true;
|
|
54
|
-
};
|
|
55
|
-
const syncDelete = (key: string) => {
|
|
56
|
-
if (storedKeys.has(key)) {
|
|
57
|
-
storedKeys.delete(key);
|
|
58
|
-
return "deleted" as const;
|
|
59
|
-
}
|
|
60
|
-
return "not-found" as const;
|
|
61
|
-
};
|
|
62
|
-
return {
|
|
63
|
-
getSecureKeyAsync: async (key: string) => storedKeys.get(key) ?? undefined,
|
|
64
|
-
getSecureKeyResultAsync: async (account: string) => ({
|
|
65
|
-
value: storedKeys.get(account),
|
|
66
|
-
unreachable: false,
|
|
67
|
-
}),
|
|
68
|
-
setSecureKeyAsync: async (key: string, value: string) =>
|
|
69
|
-
syncSet(key, value),
|
|
70
|
-
deleteSecureKeyAsync: async (key: string) => syncDelete(key),
|
|
71
|
-
listSecureKeysAsync: async () => ({
|
|
72
|
-
accounts: [...storedKeys.keys()],
|
|
73
|
-
unreachable: false,
|
|
74
|
-
}),
|
|
75
|
-
getProviderKeyAsync: async () => undefined,
|
|
76
|
-
getMaskedProviderKey: async () => null,
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// In-memory metadata store that mirrors storedKeys for list/get operations
|
|
81
|
-
const metadataStore = new Map<
|
|
82
|
-
string,
|
|
83
|
-
{ credentialId: string; service: string; field: string }
|
|
84
|
-
>();
|
|
85
|
-
|
|
86
|
-
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
87
|
-
upsertCredentialMetadata: (
|
|
88
|
-
service: string,
|
|
89
|
-
field: string,
|
|
90
|
-
_policy?: Record<string, unknown>,
|
|
91
|
-
) => {
|
|
92
|
-
const key = `${service}:${field}`;
|
|
93
|
-
metadataStore.set(key, {
|
|
94
|
-
credentialId: `cred-${service}-${field}`,
|
|
95
|
-
service,
|
|
96
|
-
field,
|
|
97
|
-
});
|
|
98
|
-
},
|
|
99
|
-
deleteCredentialMetadata: (service: string, field: string) => {
|
|
100
|
-
metadataStore.delete(`${service}:${field}`);
|
|
101
|
-
},
|
|
102
|
-
getCredentialMetadata: (service: string, field: string) => {
|
|
103
|
-
return metadataStore.get(`${service}:${field}`) ?? null;
|
|
104
|
-
},
|
|
105
|
-
getCredentialMetadataById: (id: string) => {
|
|
106
|
-
for (const m of metadataStore.values()) {
|
|
107
|
-
if (m.credentialId === id) return m;
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
},
|
|
111
|
-
listCredentialMetadata: () => [...metadataStore.values()],
|
|
112
|
-
assertMetadataWritable: () => {},
|
|
113
|
-
_setMetadataPath: () => {},
|
|
114
|
-
}));
|
|
115
|
-
|
|
116
|
-
mock.module("../tools/credentials/policy-validate.js", () => ({
|
|
117
|
-
validatePolicyInput: () => ({ valid: true, errors: [] }),
|
|
118
|
-
toPolicyFromInput: (input: Record<string, unknown>) => ({
|
|
119
|
-
allowedTools: input.allowed_tools ?? [],
|
|
120
|
-
allowedDomains: input.allowed_domains ?? [],
|
|
121
|
-
usageDescription: input.usage_description,
|
|
122
|
-
}),
|
|
123
|
-
}));
|
|
124
|
-
|
|
125
|
-
// Import modules under test
|
|
126
|
-
const { credentialStoreTool } = await import("../tools/credentials/vault.js");
|
|
127
|
-
const { isToolAllowed } = await import("../tools/credentials/tool-policy.js");
|
|
128
|
-
const { isDomainAllowed } =
|
|
129
|
-
await import("../tools/credentials/domain-policy.js");
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Helpers
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
function makeContext(overrides: Record<string, unknown> = {}) {
|
|
136
|
-
return {
|
|
137
|
-
workingDir: "/tmp",
|
|
138
|
-
conversationId: "c1",
|
|
139
|
-
trustClass: "guardian" as const,
|
|
140
|
-
...overrides,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// E2E Scenario 1 — Normal secure store + list
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
describe("E2E: secure store and list lifecycle", () => {
|
|
149
|
-
beforeEach(() => {
|
|
150
|
-
storedKeys.clear();
|
|
151
|
-
metadataStore.clear();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("store persists credential and returns metadata-only confirmation", async () => {
|
|
155
|
-
const result = await credentialStoreTool.execute(
|
|
156
|
-
{
|
|
157
|
-
action: "store",
|
|
158
|
-
service: "github",
|
|
159
|
-
field: "token",
|
|
160
|
-
value: "ghp_abc123",
|
|
161
|
-
},
|
|
162
|
-
makeContext(),
|
|
163
|
-
);
|
|
164
|
-
expect(result.isError).toBe(false);
|
|
165
|
-
expect(result.content).toContain("github");
|
|
166
|
-
// Value must NOT appear in tool output (invariant 1)
|
|
167
|
-
expect(result.content).not.toContain("ghp_abc123");
|
|
168
|
-
// Value must be in credential store
|
|
169
|
-
expect(storedKeys.get("credential/github/token")).toBe("ghp_abc123");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("list returns service/field pairs without secret values", async () => {
|
|
173
|
-
storedKeys.set("credential/github/token", "secret1");
|
|
174
|
-
storedKeys.set("credential/aws/access_key", "secret2");
|
|
175
|
-
metadataStore.set("github:token", {
|
|
176
|
-
credentialId: "cred-github-token",
|
|
177
|
-
service: "github",
|
|
178
|
-
field: "token",
|
|
179
|
-
});
|
|
180
|
-
metadataStore.set("aws:access_key", {
|
|
181
|
-
credentialId: "cred-aws-access_key",
|
|
182
|
-
service: "aws",
|
|
183
|
-
field: "access_key",
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const result = await credentialStoreTool.execute(
|
|
187
|
-
{ action: "list" },
|
|
188
|
-
makeContext(),
|
|
189
|
-
);
|
|
190
|
-
expect(result.isError).toBe(false);
|
|
191
|
-
expect(result.content).toContain("github");
|
|
192
|
-
expect(result.content).toContain("token");
|
|
193
|
-
// Secret values must NOT appear
|
|
194
|
-
expect(result.content).not.toContain("secret1");
|
|
195
|
-
expect(result.content).not.toContain("secret2");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("delete removes credential from credential store", async () => {
|
|
199
|
-
storedKeys.set("credential/github/token", "secret1");
|
|
200
|
-
|
|
201
|
-
const result = await credentialStoreTool.execute(
|
|
202
|
-
{ action: "delete", service: "github", field: "token" },
|
|
203
|
-
makeContext(),
|
|
204
|
-
);
|
|
205
|
-
expect(result.isError).toBe(false);
|
|
206
|
-
expect(storedKeys.has("credential/github/token")).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// E2E Scenario 3 — One-time send override path
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
describe("E2E: one-time send override", () => {
|
|
215
|
-
beforeEach(() => {
|
|
216
|
-
storedKeys.clear();
|
|
217
|
-
metadataStore.clear();
|
|
218
|
-
mockConfig.secretDetection.allowOneTimeSend = false;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("rejects transient_send when config gate is off", async () => {
|
|
222
|
-
const ctx = makeContext({
|
|
223
|
-
requestSecret: async () => ({
|
|
224
|
-
value: "tmp1",
|
|
225
|
-
delivery: "transient_send" as const,
|
|
226
|
-
}),
|
|
227
|
-
});
|
|
228
|
-
const result = await credentialStoreTool.execute(
|
|
229
|
-
{ action: "prompt", service: "svc", field: "key", label: "Key" },
|
|
230
|
-
ctx,
|
|
231
|
-
);
|
|
232
|
-
expect(result.isError).toBe(true);
|
|
233
|
-
expect(result.content).toContain("not enabled");
|
|
234
|
-
expect(storedKeys.has("credential/svc/key")).toBe(false);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test("accepts transient_send when config gate is on", async () => {
|
|
238
|
-
mockConfig.secretDetection.allowOneTimeSend = true;
|
|
239
|
-
const ctx = makeContext({
|
|
240
|
-
requestSecret: async () => ({
|
|
241
|
-
value: "tmp1",
|
|
242
|
-
delivery: "transient_send" as const,
|
|
243
|
-
}),
|
|
244
|
-
});
|
|
245
|
-
const result = await credentialStoreTool.execute(
|
|
246
|
-
{ action: "prompt", service: "svc", field: "key", label: "Key" },
|
|
247
|
-
ctx,
|
|
248
|
-
);
|
|
249
|
-
expect(result.isError).toBe(false);
|
|
250
|
-
expect(result.content).toContain("NOT saved");
|
|
251
|
-
// Value must NOT be in credential store
|
|
252
|
-
expect(storedKeys.has("credential/svc/key")).toBe(false);
|
|
253
|
-
// Value must NOT appear in output
|
|
254
|
-
expect(result.content).not.toContain("tmp1");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("store delivery always persists regardless of config gate", async () => {
|
|
258
|
-
mockConfig.secretDetection.allowOneTimeSend = true;
|
|
259
|
-
const ctx = makeContext({
|
|
260
|
-
requestSecret: async () => ({
|
|
261
|
-
value: "perm1",
|
|
262
|
-
delivery: "store" as const,
|
|
263
|
-
}),
|
|
264
|
-
});
|
|
265
|
-
const result = await credentialStoreTool.execute(
|
|
266
|
-
{ action: "prompt", service: "svc", field: "key", label: "Key" },
|
|
267
|
-
ctx,
|
|
268
|
-
);
|
|
269
|
-
expect(result.isError).toBe(false);
|
|
270
|
-
expect(storedKeys.has("credential/svc/key")).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
// E2E Scenario 4 — Tool policy enforcement
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
|
|
278
|
-
describe("E2E: tool policy enforcement", () => {
|
|
279
|
-
test("allows tool when listed in allowedTools", () => {
|
|
280
|
-
expect(
|
|
281
|
-
isToolAllowed("browser_fill_credential", ["browser_fill_credential"]),
|
|
282
|
-
).toBe(true);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("denies tool when not listed", () => {
|
|
286
|
-
expect(isToolAllowed("browser_fill_credential", ["other_tool"])).toBe(
|
|
287
|
-
false,
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("denies all tools when allowedTools is empty (fail-closed)", () => {
|
|
292
|
-
expect(isToolAllowed("browser_fill_credential", [])).toBe(false);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("denies when toolName is empty", () => {
|
|
296
|
-
expect(isToolAllowed("", ["browser_fill_credential"])).toBe(false);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// E2E Scenario 5 — Domain policy enforcement
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
describe("E2E: domain policy enforcement", () => {
|
|
305
|
-
test("allows exact domain match", () => {
|
|
306
|
-
expect(isDomainAllowed("github.com", ["github.com"])).toBe(true);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("allows subdomain of registrable domain", () => {
|
|
310
|
-
expect(isDomainAllowed("login.github.com", ["github.com"])).toBe(true);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("denies mismatched domain", () => {
|
|
314
|
-
expect(isDomainAllowed("evil.com", ["github.com"])).toBe(false);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("denies when allowedDomains is empty (fail-closed)", () => {
|
|
318
|
-
expect(isDomainAllowed("github.com", [])).toBe(false);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("denies localhost and IP addresses", () => {
|
|
322
|
-
expect(isDomainAllowed("localhost", ["localhost"])).toBe(false);
|
|
323
|
-
expect(isDomainAllowed("127.0.0.1", ["127.0.0.1"])).toBe(false);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
// Cross-cutting — output never leaks secrets
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
describe("E2E: cross-cutting secret leak prevention", () => {
|
|
332
|
-
beforeEach(() => {
|
|
333
|
-
storedKeys.clear();
|
|
334
|
-
mockConfig.secretDetection.enabled = true;
|
|
335
|
-
mockConfig.secretDetection.allowOneTimeSend = false;
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("store output never contains the stored value", async () => {
|
|
339
|
-
const secret = ["sk", "-proj-", "abc123xyz"].join("");
|
|
340
|
-
const result = await credentialStoreTool.execute(
|
|
341
|
-
{ action: "store", service: "openai", field: "api_key", value: secret },
|
|
342
|
-
makeContext(),
|
|
343
|
-
);
|
|
344
|
-
expect(result.content).not.toContain(secret);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("prompt output never contains the secret value", async () => {
|
|
348
|
-
mockConfig.secretDetection.allowOneTimeSend = true;
|
|
349
|
-
const secret = ["tok", "_", "sensitive99"].join("");
|
|
350
|
-
const ctx = makeContext({
|
|
351
|
-
requestSecret: async () => ({
|
|
352
|
-
value: secret,
|
|
353
|
-
delivery: "transient_send" as const,
|
|
354
|
-
}),
|
|
355
|
-
});
|
|
356
|
-
const result = await credentialStoreTool.execute(
|
|
357
|
-
{ action: "prompt", service: "svc", field: "key", label: "Key" },
|
|
358
|
-
ctx,
|
|
359
|
-
);
|
|
360
|
-
expect(result.content).not.toContain(secret);
|
|
361
|
-
});
|
|
362
|
-
});
|