@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -24,79 +24,55 @@ mock.module("../util/logger.js", () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// Broker client mock — set up before importing secure-keys so the
|
|
28
|
+
// module-level `createBrokerClient()` call picks up our mock.
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
let mockBrokerAvailable = false;
|
|
32
|
+
let mockBrokerStore: Map<string, string> = new Map();
|
|
33
|
+
let mockBrokerGetError = false;
|
|
34
|
+
let mockBrokerSetError = false;
|
|
35
|
+
let mockBrokerDelError = false;
|
|
36
|
+
|
|
37
|
+
mock.module("../security/keychain-broker-client.js", () => ({
|
|
38
|
+
createBrokerClient: () => ({
|
|
39
|
+
isAvailable: () => mockBrokerAvailable,
|
|
40
|
+
ping: async () => (mockBrokerAvailable ? { pong: true } : null),
|
|
41
|
+
get: async (account: string) => {
|
|
42
|
+
// null = broker error (fall back to encrypted store)
|
|
43
|
+
if (mockBrokerGetError) return null;
|
|
44
|
+
const value = mockBrokerStore.get(account);
|
|
45
|
+
if (value !== undefined) return { found: true, value };
|
|
46
|
+
return { found: false };
|
|
47
|
+
},
|
|
48
|
+
set: async (account: string, value: string) => {
|
|
49
|
+
if (mockBrokerSetError) return false;
|
|
50
|
+
mockBrokerStore.set(account, value);
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
del: async (account: string) => {
|
|
54
|
+
if (mockBrokerDelError) return false;
|
|
55
|
+
const existed = mockBrokerStore.has(account);
|
|
56
|
+
mockBrokerStore.delete(account);
|
|
57
|
+
return existed;
|
|
58
|
+
},
|
|
59
|
+
list: async () => Array.from(mockBrokerStore.keys()),
|
|
60
|
+
}),
|
|
39
61
|
}));
|
|
40
62
|
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Keychain simulation via _overrideDeps — avoids process-global mock.module
|
|
43
|
-
// for keychain.js which leaks into keychain.test.ts.
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
import { _overrideDeps, _resetDeps } from "../security/keychain.js";
|
|
47
|
-
|
|
48
|
-
let keychainAvailable = false;
|
|
49
|
-
let keychainFailAtRuntime = false;
|
|
50
|
-
const keychainStore = new Map<string, string>();
|
|
51
|
-
|
|
52
|
-
function installKeychainDeps(): void {
|
|
53
|
-
_overrideDeps({
|
|
54
|
-
isMacOS: () => keychainAvailable,
|
|
55
|
-
isLinux: () => false,
|
|
56
|
-
execFileSync: ((cmd: string, args: string[]) => {
|
|
57
|
-
if (keychainFailAtRuntime) throw new Error("Keychain runtime error");
|
|
58
|
-
|
|
59
|
-
if (cmd === "security") {
|
|
60
|
-
if (args.includes("list-keychains")) return "";
|
|
61
|
-
|
|
62
|
-
if (args.includes("find-generic-password")) {
|
|
63
|
-
const aIdx = args.indexOf("-a");
|
|
64
|
-
const account = args[aIdx + 1];
|
|
65
|
-
const val = keychainStore.get(account);
|
|
66
|
-
if (!val) throw Object.assign(new Error("not found"), { status: 44 });
|
|
67
|
-
return val + "\n";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (args.includes("add-generic-password")) {
|
|
71
|
-
const aIdx = args.indexOf("-a");
|
|
72
|
-
const account = args[aIdx + 1];
|
|
73
|
-
const wIdx = args.indexOf("-w");
|
|
74
|
-
const value = args[wIdx + 1];
|
|
75
|
-
keychainStore.set(account, value);
|
|
76
|
-
return "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (args.includes("delete-generic-password")) {
|
|
80
|
-
const aIdx = args.indexOf("-a");
|
|
81
|
-
const account = args[aIdx + 1];
|
|
82
|
-
keychainStore.delete(account);
|
|
83
|
-
return "";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return "";
|
|
88
|
-
}) as typeof import("node:child_process").execFileSync,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
63
|
import { _setStorePath } from "../security/encrypted-store.js";
|
|
93
64
|
import {
|
|
94
65
|
_resetBackend,
|
|
95
66
|
_setBackend,
|
|
96
67
|
deleteSecureKey,
|
|
68
|
+
deleteSecureKeyAsync,
|
|
69
|
+
getBackendType,
|
|
97
70
|
getSecureKey,
|
|
71
|
+
getSecureKeyAsync,
|
|
72
|
+
isDowngradedFromKeychain,
|
|
98
73
|
listSecureKeys,
|
|
99
74
|
setSecureKey,
|
|
75
|
+
setSecureKeyAsync,
|
|
100
76
|
} from "../security/secure-keys.js";
|
|
101
77
|
|
|
102
78
|
// ---------------------------------------------------------------------------
|
|
@@ -111,13 +87,14 @@ const STORE_PATH = join(TEST_DIR, "keys.enc");
|
|
|
111
87
|
|
|
112
88
|
describe("secure-keys", () => {
|
|
113
89
|
beforeEach(() => {
|
|
114
|
-
// Clean state
|
|
115
|
-
keychainAvailable = false;
|
|
116
|
-
keychainFailAtRuntime = false;
|
|
117
|
-
mockIsMacOS = false;
|
|
118
|
-
keychainStore.clear();
|
|
119
90
|
_resetBackend();
|
|
120
|
-
|
|
91
|
+
|
|
92
|
+
// Reset broker mock state
|
|
93
|
+
mockBrokerAvailable = false;
|
|
94
|
+
mockBrokerStore = new Map();
|
|
95
|
+
mockBrokerGetError = false;
|
|
96
|
+
mockBrokerSetError = false;
|
|
97
|
+
mockBrokerDelError = false;
|
|
121
98
|
|
|
122
99
|
if (existsSync(TEST_DIR)) {
|
|
123
100
|
rmSync(TEST_DIR, { recursive: true });
|
|
@@ -132,7 +109,6 @@ describe("secure-keys", () => {
|
|
|
132
109
|
});
|
|
133
110
|
|
|
134
111
|
afterAll(() => {
|
|
135
|
-
_resetDeps();
|
|
136
112
|
if (existsSync(TEST_DIR)) {
|
|
137
113
|
rmSync(TEST_DIR, { recursive: true });
|
|
138
114
|
}
|
|
@@ -142,54 +118,24 @@ describe("secure-keys", () => {
|
|
|
142
118
|
// Backend selection
|
|
143
119
|
// -----------------------------------------------------------------------
|
|
144
120
|
describe("backend selection", () => {
|
|
145
|
-
test("
|
|
146
|
-
|
|
147
|
-
_resetBackend();
|
|
148
|
-
setSecureKey("anthropic", "sk-test-123");
|
|
149
|
-
expect(getSecureKey("anthropic")).toBe("sk-test-123");
|
|
150
|
-
// Should be in encrypted store, not keychain
|
|
151
|
-
expect(keychainStore.has("anthropic")).toBe(false);
|
|
152
|
-
expect(existsSync(STORE_PATH)).toBe(true);
|
|
121
|
+
test("returns encrypted when broker is unavailable", () => {
|
|
122
|
+
expect(getBackendType()).toBe("encrypted");
|
|
153
123
|
});
|
|
154
124
|
|
|
155
|
-
test("
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
setSecureKey("anthropic", "sk-test-456");
|
|
159
|
-
expect(getSecureKey("anthropic")).toBe("sk-test-456");
|
|
160
|
-
// Should be in keychain, not encrypted store
|
|
161
|
-
expect(keychainStore.get("anthropic")).toBe("sk-test-456");
|
|
162
|
-
expect(existsSync(STORE_PATH)).toBe(false);
|
|
125
|
+
test("returns broker when broker is available", () => {
|
|
126
|
+
mockBrokerAvailable = true;
|
|
127
|
+
expect(getBackendType()).toBe("broker");
|
|
163
128
|
});
|
|
164
129
|
|
|
165
|
-
test("
|
|
166
|
-
|
|
167
|
-
_resetBackend();
|
|
168
|
-
setSecureKey("test", "val1");
|
|
169
|
-
|
|
170
|
-
// Change availability — should still use encrypted store
|
|
171
|
-
keychainAvailable = true;
|
|
172
|
-
setSecureKey("test2", "val2");
|
|
173
|
-
expect(keychainStore.has("test2")).toBe(false);
|
|
174
|
-
expect(existsSync(STORE_PATH)).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("uses encrypted store on macOS even when keychain is available", () => {
|
|
178
|
-
mockIsMacOS = true;
|
|
179
|
-
keychainAvailable = true;
|
|
180
|
-
_resetBackend();
|
|
181
|
-
setSecureKey("anthropic", "sk-mac-test");
|
|
182
|
-
expect(getSecureKey("anthropic")).toBe("sk-mac-test");
|
|
183
|
-
// Should be in encrypted store, not keychain
|
|
184
|
-
expect(keychainStore.has("anthropic")).toBe(false);
|
|
185
|
-
expect(existsSync(STORE_PATH)).toBe(true);
|
|
130
|
+
test("isDowngradedFromKeychain always returns false", () => {
|
|
131
|
+
expect(isDowngradedFromKeychain()).toBe(false);
|
|
186
132
|
});
|
|
187
133
|
});
|
|
188
134
|
|
|
189
135
|
// -----------------------------------------------------------------------
|
|
190
|
-
// CRUD operations (via encrypted store backend)
|
|
136
|
+
// CRUD operations (via encrypted store backend — sync)
|
|
191
137
|
// -----------------------------------------------------------------------
|
|
192
|
-
describe("CRUD with encrypted backend", () => {
|
|
138
|
+
describe("CRUD with encrypted backend (sync)", () => {
|
|
193
139
|
test("set and get a key", () => {
|
|
194
140
|
setSecureKey("openai", "sk-openai-789");
|
|
195
141
|
expect(getSecureKey("openai")).toBe("sk-openai-789");
|
|
@@ -201,12 +147,12 @@ describe("secure-keys", () => {
|
|
|
201
147
|
|
|
202
148
|
test("delete removes a key", () => {
|
|
203
149
|
setSecureKey("gemini", "gem-key");
|
|
204
|
-
expect(deleteSecureKey("gemini")).toBe(
|
|
150
|
+
expect(deleteSecureKey("gemini")).toBe("deleted");
|
|
205
151
|
expect(getSecureKey("gemini")).toBeUndefined();
|
|
206
152
|
});
|
|
207
153
|
|
|
208
|
-
test("delete returns
|
|
209
|
-
expect(deleteSecureKey("missing")).toBe(
|
|
154
|
+
test("delete returns not-found for nonexistent key", () => {
|
|
155
|
+
expect(deleteSecureKey("missing")).toBe("not-found");
|
|
210
156
|
});
|
|
211
157
|
|
|
212
158
|
test("listSecureKeys returns all keys", () => {
|
|
@@ -220,143 +166,156 @@ describe("secure-keys", () => {
|
|
|
220
166
|
});
|
|
221
167
|
|
|
222
168
|
// -----------------------------------------------------------------------
|
|
223
|
-
//
|
|
169
|
+
// Sync variants always use encrypted store even when broker is available
|
|
224
170
|
// -----------------------------------------------------------------------
|
|
225
|
-
describe("
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
171
|
+
describe("sync variants ignore broker", () => {
|
|
172
|
+
test("getSecureKey uses encrypted store even when broker is available", () => {
|
|
173
|
+
mockBrokerAvailable = true;
|
|
174
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
175
|
+
// Sync getter should not see broker-only keys
|
|
176
|
+
expect(getSecureKey("api-key")).toBeUndefined();
|
|
177
|
+
// But encrypted store keys should work
|
|
178
|
+
setSecureKey("api-key", "encrypted-value");
|
|
179
|
+
expect(getSecureKey("api-key")).toBe("encrypted-value");
|
|
229
180
|
});
|
|
230
181
|
|
|
231
|
-
test("
|
|
232
|
-
|
|
233
|
-
|
|
182
|
+
test("setSecureKey uses encrypted store even when broker is available", () => {
|
|
183
|
+
mockBrokerAvailable = true;
|
|
184
|
+
setSecureKey("api-key", "encrypted-value");
|
|
185
|
+
expect(getSecureKey("api-key")).toBe("encrypted-value");
|
|
186
|
+
// Should not have written to broker
|
|
187
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
234
188
|
});
|
|
235
189
|
|
|
236
|
-
test("
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Keychain doesn't support listing
|
|
245
|
-
expect(listSecureKeys()).toEqual([]);
|
|
190
|
+
test("deleteSecureKey uses encrypted store even when broker is available", () => {
|
|
191
|
+
mockBrokerAvailable = true;
|
|
192
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
193
|
+
setSecureKey("api-key", "encrypted-value");
|
|
194
|
+
deleteSecureKey("api-key");
|
|
195
|
+
expect(getSecureKey("api-key")).toBeUndefined();
|
|
196
|
+
// Broker value should be untouched
|
|
197
|
+
expect(mockBrokerStore.has("api-key")).toBe(true);
|
|
246
198
|
});
|
|
247
199
|
});
|
|
248
200
|
|
|
249
201
|
// -----------------------------------------------------------------------
|
|
250
|
-
//
|
|
202
|
+
// Async variants — broker available path
|
|
251
203
|
// -----------------------------------------------------------------------
|
|
252
|
-
describe("
|
|
253
|
-
test("
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
expect(
|
|
204
|
+
describe("async variants with broker available", () => {
|
|
205
|
+
test("getSecureKeyAsync returns broker value when available", async () => {
|
|
206
|
+
mockBrokerAvailable = true;
|
|
207
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
208
|
+
setSecureKey("api-key", "encrypted-value");
|
|
209
|
+
expect(await getSecureKeyAsync("api-key")).toBe("broker-value");
|
|
258
210
|
});
|
|
259
211
|
|
|
260
|
-
test("
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
212
|
+
test("getSecureKeyAsync falls back to encrypted store when broker reports not-found", async () => {
|
|
213
|
+
mockBrokerAvailable = true;
|
|
214
|
+
// Broker has nothing for this key — returns { found: false }.
|
|
215
|
+
// Keys may exist only in the encrypted store (written while broker
|
|
216
|
+
// was unavailable or via sync setSecureKey), so we must fall back.
|
|
217
|
+
setSecureKey("api-key", "encrypted-value");
|
|
218
|
+
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
265
219
|
});
|
|
266
220
|
|
|
267
|
-
test("
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
expect(keychainStore.get("k1")).toBe("v1");
|
|
272
|
-
|
|
273
|
-
_setBackend(undefined); // reset
|
|
274
|
-
keychainAvailable = false;
|
|
275
|
-
setSecureKey("k2", "v2");
|
|
276
|
-
expect(keychainStore.has("k2")).toBe(false);
|
|
277
|
-
expect(getSecureKey("k2")).toBe("v2");
|
|
221
|
+
test("getSecureKeyAsync returns undefined when neither broker nor encrypted store has key", async () => {
|
|
222
|
+
mockBrokerAvailable = true;
|
|
223
|
+
// Neither store has the key — should return undefined
|
|
224
|
+
expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
|
|
278
225
|
});
|
|
279
|
-
});
|
|
280
226
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
keychainAvailable = true;
|
|
287
|
-
_resetBackend();
|
|
227
|
+
test("getSecureKeyAsync falls back to encrypted store on broker error", async () => {
|
|
228
|
+
mockBrokerAvailable = true;
|
|
229
|
+
mockBrokerGetError = true;
|
|
230
|
+
setSecureKey("api-key", "encrypted-value");
|
|
231
|
+
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
288
232
|
});
|
|
289
233
|
|
|
290
|
-
test("
|
|
291
|
-
|
|
292
|
-
const result =
|
|
234
|
+
test("setSecureKeyAsync writes to broker and encrypted store", async () => {
|
|
235
|
+
mockBrokerAvailable = true;
|
|
236
|
+
const result = await setSecureKeyAsync("api-key", "new-value");
|
|
293
237
|
expect(result).toBe(true);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
// Subsequent gets should also use encrypted store now
|
|
298
|
-
expect(getSecureKey("anthropic")).toBe("sk-test-fallback");
|
|
238
|
+
expect(mockBrokerStore.get("api-key")).toBe("new-value");
|
|
239
|
+
// Also persisted to encrypted store for sync callers
|
|
240
|
+
expect(getSecureKey("api-key")).toBe("new-value");
|
|
299
241
|
});
|
|
300
242
|
|
|
301
|
-
test("
|
|
302
|
-
|
|
303
|
-
|
|
243
|
+
test("setSecureKeyAsync returns false on broker set error (no silent fallback)", async () => {
|
|
244
|
+
mockBrokerAvailable = true;
|
|
245
|
+
mockBrokerSetError = true;
|
|
246
|
+
const result = await setSecureKeyAsync("api-key", "new-value");
|
|
247
|
+
// Must return false — falling through to encrypted-only write would
|
|
248
|
+
// leave the broker with stale data that async readers still see.
|
|
304
249
|
expect(result).toBe(false);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
expect(
|
|
308
|
-
expect(existsSync(STORE_PATH)).toBe(false);
|
|
250
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
251
|
+
// Encrypted store should NOT have been written either.
|
|
252
|
+
expect(getSecureKey("api-key")).toBeUndefined();
|
|
309
253
|
});
|
|
310
254
|
|
|
311
|
-
test("
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
setSecureKey("
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
expect(
|
|
318
|
-
expect(getSecureKey("
|
|
255
|
+
test("deleteSecureKeyAsync deletes from broker and encrypted store", async () => {
|
|
256
|
+
mockBrokerAvailable = true;
|
|
257
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
258
|
+
setSecureKey("api-key", "encrypted-value");
|
|
259
|
+
const result = await deleteSecureKeyAsync("api-key");
|
|
260
|
+
expect(result).toBe("deleted");
|
|
261
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
262
|
+
expect(getSecureKey("api-key")).toBeUndefined();
|
|
319
263
|
});
|
|
320
264
|
|
|
321
|
-
test("
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
265
|
+
test("deleteSecureKeyAsync returns error on broker del error (no silent fallback)", async () => {
|
|
266
|
+
mockBrokerAvailable = true;
|
|
267
|
+
mockBrokerDelError = true;
|
|
268
|
+
setSecureKey("api-key", "encrypted-value");
|
|
269
|
+
const result = await deleteSecureKeyAsync("api-key");
|
|
270
|
+
// Must return "error" — falling through to encrypted-only delete would
|
|
271
|
+
// leave the broker with the key, and async readers would still see it.
|
|
272
|
+
expect(result).toBe("error");
|
|
273
|
+
// Encrypted store should NOT have been modified either.
|
|
274
|
+
expect(getSecureKey("api-key")).toBe("encrypted-value");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
325
277
|
|
|
326
|
-
|
|
327
|
-
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
// Async variants — broker unavailable path
|
|
280
|
+
// -----------------------------------------------------------------------
|
|
281
|
+
describe("async variants with broker unavailable", () => {
|
|
282
|
+
test("getSecureKeyAsync uses encrypted store", async () => {
|
|
283
|
+
setSecureKey("api-key", "encrypted-value");
|
|
284
|
+
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
285
|
+
});
|
|
328
286
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// encrypted store doesn't have the key, so the overall result is false.
|
|
333
|
-
// But the important thing is that the backend was downgraded.
|
|
334
|
-
expect(result).toBe(false);
|
|
287
|
+
test("getSecureKeyAsync returns undefined for missing key", async () => {
|
|
288
|
+
expect(await getSecureKeyAsync("missing")).toBeUndefined();
|
|
289
|
+
});
|
|
335
290
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Should be in encrypted store, not keychain
|
|
341
|
-
expect(keychainStore.has("openai")).toBe(false);
|
|
342
|
-
expect(getSecureKey("openai")).toBe("sk-openai-new");
|
|
291
|
+
test("setSecureKeyAsync uses encrypted store", async () => {
|
|
292
|
+
const result = await setSecureKeyAsync("api-key", "new-value");
|
|
293
|
+
expect(result).toBe(true);
|
|
294
|
+
expect(getSecureKey("api-key")).toBe("new-value");
|
|
343
295
|
});
|
|
344
296
|
|
|
345
|
-
test("
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
expect(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
297
|
+
test("deleteSecureKeyAsync uses encrypted store", async () => {
|
|
298
|
+
setSecureKey("api-key", "value");
|
|
299
|
+
const result = await deleteSecureKeyAsync("api-key");
|
|
300
|
+
expect(result).toBe("deleted");
|
|
301
|
+
expect(getSecureKey("api-key")).toBeUndefined();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// -----------------------------------------------------------------------
|
|
306
|
+
// _setBackend / _resetBackend (no-ops kept for test compat)
|
|
307
|
+
// -----------------------------------------------------------------------
|
|
308
|
+
describe("_setBackend", () => {
|
|
309
|
+
test("_setBackend is a no-op but does not throw", () => {
|
|
310
|
+
_setBackend("encrypted");
|
|
311
|
+
setSecureKey("test", "value");
|
|
312
|
+
expect(existsSync(STORE_PATH)).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("_resetBackend is a no-op but does not throw", () => {
|
|
316
|
+
_resetBackend();
|
|
317
|
+
setSecureKey("test", "value");
|
|
318
|
+
expect(getSecureKey("test")).toBe("value");
|
|
360
319
|
});
|
|
361
320
|
});
|
|
362
321
|
});
|
|
@@ -85,7 +85,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
85
85
|
apiKeys: {},
|
|
86
86
|
skills: { entries: {}, allowBundled: true },
|
|
87
87
|
memory: { retrieval: { injectionStrategy: "inline" } },
|
|
88
|
-
permissions: { mode: "
|
|
88
|
+
permissions: { mode: "workspace" },
|
|
89
89
|
}),
|
|
90
90
|
loadRawConfig: () => ({}),
|
|
91
91
|
saveRawConfig: () => {},
|
|
@@ -26,7 +26,7 @@ const metadataByKey = new Map<
|
|
|
26
26
|
mock.module("../security/secure-keys.js", () => ({
|
|
27
27
|
getSecureKey: () => undefined,
|
|
28
28
|
setSecureKey: setSecureKeyMock,
|
|
29
|
-
deleteSecureKey: () =>
|
|
29
|
+
deleteSecureKey: () => "deleted",
|
|
30
30
|
listSecureKeys: () => [],
|
|
31
31
|
getBackendType: () => null,
|
|
32
32
|
isDowngradedFromKeychain: () => false,
|
|
@@ -76,7 +76,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
76
76
|
apiKeys: {},
|
|
77
77
|
skills: { entries: {}, allowBundled: true },
|
|
78
78
|
memory: { retrieval: { injectionStrategy: "inline" } },
|
|
79
|
-
permissions: { mode: "
|
|
79
|
+
permissions: { mode: "workspace" },
|
|
80
|
+
sandbox: { enabled: false },
|
|
80
81
|
daemon: {
|
|
81
82
|
startupSocketWaitMs: 5000,
|
|
82
83
|
stopTimeoutMs: 5000,
|
|
@@ -520,7 +520,7 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
520
520
|
// ── app_create side effects ─────────────────────────────────────────
|
|
521
521
|
|
|
522
522
|
describe("app_create side effects", () => {
|
|
523
|
-
test("broadcasts app_files_changed after app_create", async () => {
|
|
523
|
+
test("broadcasts app_files_changed immediately after app_create", async () => {
|
|
524
524
|
const ctx = makeCtx();
|
|
525
525
|
const executor = makeFakeExecutor({
|
|
526
526
|
content: JSON.stringify({ id: "new-app-1", name: "My App" }),
|
|
@@ -539,7 +539,7 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
539
539
|
|
|
540
540
|
await toolFn("app_create", { name: "My App", html: "<h1>hi</h1>" });
|
|
541
541
|
|
|
542
|
-
expect(broadcastSpy).
|
|
542
|
+
expect(broadcastSpy.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
543
543
|
expect((broadcastSpy.mock.calls as unknown[][])[0][0]).toEqual({
|
|
544
544
|
type: "app_files_changed",
|
|
545
545
|
appId: "new-app-1",
|
|
@@ -30,7 +30,10 @@ let currentConfig: Record<string, unknown> = {
|
|
|
30
30
|
const DECLARED_SKILL_ID = "hatch-new-assistant";
|
|
31
31
|
const DECLARED_FLAG_KEY = "feature_flags.hatch-new-assistant.enabled";
|
|
32
32
|
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const realPlatform = require("../util/platform.js");
|
|
33
35
|
mock.module("../util/platform.js", () => ({
|
|
36
|
+
...realPlatform,
|
|
34
37
|
getRootDir: () => TEST_DIR,
|
|
35
38
|
getDataDir: () => TEST_DIR,
|
|
36
39
|
getWorkspaceDir: () => TEST_DIR,
|
|
@@ -54,31 +57,53 @@ mock.module("../util/platform.js", () => ({
|
|
|
54
57
|
isWindows: () => false,
|
|
55
58
|
getPlatformName: () => "linux",
|
|
56
59
|
getClipboardCommand: () => null,
|
|
60
|
+
readSessionToken: () => null,
|
|
57
61
|
removeSocketFile: () => {},
|
|
58
62
|
migratePath: () => {},
|
|
59
63
|
migrateToWorkspaceLayout: () => {},
|
|
60
64
|
migrateToDataLayout: () => {},
|
|
61
65
|
}));
|
|
62
66
|
|
|
67
|
+
const noopLogger = new Proxy({} as Record<string, unknown>, {
|
|
68
|
+
get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
72
|
+
const realLogger = require("../util/logger.js");
|
|
63
73
|
mock.module("../util/logger.js", () => ({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}),
|
|
74
|
+
...realLogger,
|
|
75
|
+
getLogger: () => noopLogger,
|
|
76
|
+
getCliLogger: () => noopLogger,
|
|
68
77
|
isDebug: () => false,
|
|
69
78
|
truncateForLog: (v: string) => v,
|
|
79
|
+
initLogger: () => {},
|
|
80
|
+
pruneOldLogFiles: () => 0,
|
|
70
81
|
}));
|
|
71
82
|
|
|
72
83
|
mock.module("../config/loader.js", () => ({
|
|
73
84
|
getConfig: () => currentConfig,
|
|
85
|
+
loadConfig: () => currentConfig,
|
|
86
|
+
loadRawConfig: () => ({}),
|
|
87
|
+
saveConfig: () => {},
|
|
88
|
+
saveRawConfig: () => {},
|
|
89
|
+
invalidateConfigCache: () => {},
|
|
90
|
+
getNestedValue: () => undefined,
|
|
91
|
+
setNestedValue: () => {},
|
|
92
|
+
syncConfigToLockfile: () => {},
|
|
74
93
|
}));
|
|
75
94
|
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
96
|
+
const realUserReference = require("../config/user-reference.js");
|
|
76
97
|
mock.module("../config/user-reference.js", () => ({
|
|
98
|
+
...realUserReference,
|
|
77
99
|
resolveUserReference: () => "TestUser",
|
|
78
100
|
resolveUserPronouns: () => null,
|
|
79
101
|
}));
|
|
80
102
|
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
104
|
+
const realCredentialMetadataStore = require("../tools/credentials/metadata-store.js");
|
|
81
105
|
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
106
|
+
...realCredentialMetadataStore,
|
|
82
107
|
listCredentialMetadata: () => [],
|
|
83
108
|
}));
|
|
84
109
|
|