@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
|
@@ -1,390 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified secure key storage —
|
|
3
|
-
*
|
|
2
|
+
* Unified secure key storage — routes through the keychain broker when
|
|
3
|
+
* available (macOS app embedded), with transparent fallback to the
|
|
4
|
+
* encrypted-at-rest file store.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Async variants try the broker first; sync variants always use the
|
|
7
|
+
* encrypted store (startup code paths cannot do async I/O).
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { getLogger } from "../util/logger.js";
|
|
10
|
-
import { isMacOS } from "../util/platform.js";
|
|
11
10
|
import * as encryptedStore from "./encrypted-store.js";
|
|
12
|
-
import
|
|
11
|
+
import type { KeychainBrokerClient } from "./keychain-broker-client.js";
|
|
12
|
+
import { createBrokerClient } from "./keychain-broker-client.js";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
let _broker: KeychainBrokerClient | undefined;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let downgradedFromKeychain = false;
|
|
20
|
-
/** Keys known to not exist in keychain — avoids repeated subprocess calls on misses. */
|
|
21
|
-
const keychainMissCache = new Set<string>();
|
|
22
|
-
|
|
23
|
-
function getBackend(): Backend {
|
|
24
|
-
if (resolvedBackend !== undefined) return resolvedBackend;
|
|
25
|
-
|
|
26
|
-
// On macOS, skip keychain probing and use encrypted file storage directly
|
|
27
|
-
// to avoid repeated Keychain Access authorization prompts. Mark as
|
|
28
|
-
// downgraded so getSecureKey/getSecureKeyAsync still check keychain as a
|
|
29
|
-
// fallback for secrets stored before this switch.
|
|
30
|
-
if (isMacOS()) {
|
|
31
|
-
log.debug(
|
|
32
|
-
"macOS detected, using encrypted file storage (skipping keychain)",
|
|
33
|
-
);
|
|
34
|
-
resolvedBackend = "encrypted";
|
|
35
|
-
downgradedFromKeychain = true;
|
|
36
|
-
return resolvedBackend;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (keychain.isKeychainAvailable()) {
|
|
40
|
-
log.debug("Using OS keychain for secure key storage");
|
|
41
|
-
resolvedBackend = "keychain";
|
|
42
|
-
} else {
|
|
43
|
-
log.debug("OS keychain unavailable, using encrypted file storage");
|
|
44
|
-
resolvedBackend = "encrypted";
|
|
45
|
-
}
|
|
46
|
-
return resolvedBackend;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function getBackendAsync(): Promise<Backend> {
|
|
50
|
-
if (resolvedBackend !== undefined) return resolvedBackend;
|
|
51
|
-
|
|
52
|
-
// On macOS, skip keychain probing and use encrypted file storage directly
|
|
53
|
-
// to avoid repeated Keychain Access authorization prompts. Mark as
|
|
54
|
-
// downgraded so getSecureKey/getSecureKeyAsync still check keychain as a
|
|
55
|
-
// fallback for secrets stored before this switch.
|
|
56
|
-
if (isMacOS()) {
|
|
57
|
-
log.debug(
|
|
58
|
-
"macOS detected, using encrypted file storage (skipping keychain)",
|
|
59
|
-
);
|
|
60
|
-
resolvedBackend = "encrypted";
|
|
61
|
-
downgradedFromKeychain = true;
|
|
62
|
-
return resolvedBackend;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (await keychain.isKeychainAvailableAsync()) {
|
|
66
|
-
log.debug("Using OS keychain for secure key storage");
|
|
67
|
-
resolvedBackend = "keychain";
|
|
68
|
-
} else {
|
|
69
|
-
log.debug("OS keychain unavailable, using encrypted file storage");
|
|
70
|
-
resolvedBackend = "encrypted";
|
|
71
|
-
}
|
|
72
|
-
return resolvedBackend;
|
|
16
|
+
function getBroker(): KeychainBrokerClient {
|
|
17
|
+
if (!_broker) _broker = createBrokerClient();
|
|
18
|
+
return _broker;
|
|
73
19
|
}
|
|
74
20
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* but is unusable at runtime (headless/locked sessions).
|
|
79
|
-
*/
|
|
80
|
-
function withKeychainFallback<T>(
|
|
81
|
-
keychainFn: () => T,
|
|
82
|
-
encryptedFn: () => T,
|
|
83
|
-
fallbackValue: T,
|
|
84
|
-
): T {
|
|
85
|
-
const backend = getBackend();
|
|
86
|
-
if (backend === "encrypted") return encryptedFn();
|
|
87
|
-
if (backend !== "keychain") return fallbackValue;
|
|
88
|
-
|
|
89
|
-
const result = keychainFn();
|
|
90
|
-
// keychain.setKey/deleteKey return false on failure.
|
|
91
|
-
// We downgrade on failures (false) to switch to encrypted backend.
|
|
92
|
-
if (result === false) {
|
|
93
|
-
log.warn(
|
|
94
|
-
"Keychain operation failed at runtime, falling back to encrypted file storage",
|
|
95
|
-
);
|
|
96
|
-
resolvedBackend = "encrypted";
|
|
97
|
-
downgradedFromKeychain = true;
|
|
98
|
-
return encryptedFn();
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Sync variants — encrypted store only (startup / sync call sites)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
102
24
|
|
|
103
25
|
/**
|
|
104
|
-
* Retrieve a secret from secure storage.
|
|
26
|
+
* Retrieve a secret from secure storage (sync — encrypted store only).
|
|
105
27
|
* Returns `undefined` if the key doesn't exist or on error.
|
|
106
28
|
*/
|
|
107
29
|
export function getSecureKey(account: string): string | undefined {
|
|
108
|
-
|
|
109
|
-
if (backend === "keychain") {
|
|
110
|
-
try {
|
|
111
|
-
return keychain.getKey(account) ?? undefined;
|
|
112
|
-
} catch {
|
|
113
|
-
// Keychain runtime error on read — downgrade to encrypted store
|
|
114
|
-
log.warn(
|
|
115
|
-
"Keychain read failed at runtime, falling back to encrypted file storage",
|
|
116
|
-
);
|
|
117
|
-
resolvedBackend = "encrypted";
|
|
118
|
-
downgradedFromKeychain = true;
|
|
119
|
-
return encryptedStore.getKey(account);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (backend === "encrypted") {
|
|
123
|
-
const value = encryptedStore.getKey(account);
|
|
124
|
-
// After a runtime downgrade, keys may still exist in the keychain.
|
|
125
|
-
// Try keychain read as fallback so pre-downgrade keys remain accessible.
|
|
126
|
-
if (
|
|
127
|
-
value === undefined &&
|
|
128
|
-
downgradedFromKeychain &&
|
|
129
|
-
!keychainMissCache.has(account)
|
|
130
|
-
) {
|
|
131
|
-
try {
|
|
132
|
-
const keychainValue = keychain.getKey(account) ?? undefined;
|
|
133
|
-
if (keychainValue === undefined) {
|
|
134
|
-
keychainMissCache.add(account);
|
|
135
|
-
}
|
|
136
|
-
return keychainValue;
|
|
137
|
-
} catch {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return value;
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
30
|
+
return encryptedStore.getKey(account);
|
|
144
31
|
}
|
|
145
32
|
|
|
146
33
|
/**
|
|
147
|
-
* Store a secret in secure storage.
|
|
34
|
+
* Store a secret in secure storage (sync — encrypted store only).
|
|
148
35
|
* Returns `true` on success, `false` on failure.
|
|
149
36
|
*/
|
|
150
37
|
export function setSecureKey(account: string, value: string): boolean {
|
|
151
|
-
|
|
152
|
-
() => keychain.setKey(account, value),
|
|
153
|
-
() => encryptedStore.setKey(account, value),
|
|
154
|
-
false,
|
|
155
|
-
);
|
|
156
|
-
// When writing to the encrypted store after a keychain downgrade, clean up
|
|
157
|
-
// any stale keychain entry so the gateway's credential-reader (which tries
|
|
158
|
-
// keychain first) does not read an outdated value.
|
|
159
|
-
if (result && downgradedFromKeychain && getBackend() === "encrypted") {
|
|
160
|
-
keychainMissCache.delete(account);
|
|
161
|
-
try {
|
|
162
|
-
// Only attempt deletion if the key actually exists in keychain to
|
|
163
|
-
// avoid spawning a subprocess on every write.
|
|
164
|
-
if (keychain.getKey(account) != null) {
|
|
165
|
-
keychain.deleteKey(account);
|
|
166
|
-
}
|
|
167
|
-
} catch {
|
|
168
|
-
/* best-effort */
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return result;
|
|
38
|
+
return encryptedStore.setKey(account, value);
|
|
172
39
|
}
|
|
173
40
|
|
|
41
|
+
/** Result of a delete operation — distinguishes success, not-found, and error. */
|
|
42
|
+
export type DeleteResult = "deleted" | "not-found" | "error";
|
|
43
|
+
|
|
174
44
|
/**
|
|
175
|
-
* Delete a secret from secure storage.
|
|
176
|
-
* Returns `
|
|
45
|
+
* Delete a secret from secure storage (sync — encrypted store only).
|
|
46
|
+
* Returns `"deleted"` on success, `"not-found"` if key doesn't exist,
|
|
47
|
+
* or `"error"` on failure.
|
|
177
48
|
*/
|
|
178
|
-
export function deleteSecureKey(account: string):
|
|
179
|
-
|
|
180
|
-
if (backend === "encrypted") {
|
|
181
|
-
const result = encryptedStore.deleteKey(account);
|
|
182
|
-
// After a runtime downgrade, keys may still exist in the keychain.
|
|
183
|
-
// Attempt cleanup and return true if either backend had the key.
|
|
184
|
-
if (downgradedFromKeychain) {
|
|
185
|
-
keychainMissCache.delete(account);
|
|
186
|
-
const keychainResult = keychain.deleteKey(account);
|
|
187
|
-
return result || keychainResult;
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
if (backend !== "keychain") return false;
|
|
192
|
-
|
|
193
|
-
// keychain.deleteKey returns false for both "not found" and "runtime error".
|
|
194
|
-
// Check existence first so a missing key doesn't spuriously downgrade the
|
|
195
|
-
// backend — saveConfig routinely deletes keys for unset providers.
|
|
196
|
-
// getKey now returns null for "not found" and throws on runtime errors.
|
|
197
|
-
try {
|
|
198
|
-
if (keychain.getKey(account) == null) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
// Keychain runtime error — fall through to withKeychainFallback which
|
|
203
|
-
// will handle the downgrade when deleteKey also fails.
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return withKeychainFallback(
|
|
207
|
-
() => keychain.deleteKey(account),
|
|
208
|
-
() => encryptedStore.deleteKey(account),
|
|
209
|
-
false,
|
|
210
|
-
);
|
|
49
|
+
export function deleteSecureKey(account: string): DeleteResult {
|
|
50
|
+
return encryptedStore.deleteKey(account);
|
|
211
51
|
}
|
|
212
52
|
|
|
213
53
|
/**
|
|
214
|
-
* List all account names in secure storage.
|
|
215
|
-
*
|
|
216
|
-
* Throws if the store file exists but cannot be read (encrypted backend).
|
|
54
|
+
* List all account names in secure storage (sync — encrypted store only).
|
|
55
|
+
* Throws if the store file exists but cannot be read.
|
|
217
56
|
*/
|
|
218
57
|
export function listSecureKeys(): string[] {
|
|
219
|
-
|
|
220
|
-
if (backend === "encrypted") return encryptedStore.listKeys();
|
|
221
|
-
// OS keychains don't provide a list API scoped to our service
|
|
222
|
-
return [];
|
|
58
|
+
return encryptedStore.listKeys();
|
|
223
59
|
}
|
|
224
60
|
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Backend introspection
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
225
65
|
/**
|
|
226
66
|
* Return the currently resolved backend type.
|
|
227
|
-
*
|
|
67
|
+
* Returns `"broker"` when the keychain broker is reachable, `"encrypted"` otherwise.
|
|
228
68
|
*/
|
|
229
|
-
export function getBackendType(): "
|
|
230
|
-
return
|
|
69
|
+
export function getBackendType(): "broker" | "encrypted" | null {
|
|
70
|
+
return getBroker().isAvailable() ? "broker" : "encrypted";
|
|
231
71
|
}
|
|
232
72
|
|
|
233
73
|
/**
|
|
234
74
|
* Whether the backend was downgraded from keychain to encrypted at runtime.
|
|
235
|
-
*
|
|
236
|
-
* even though the active backend is encrypted.
|
|
75
|
+
* Always returns false now that keychain CLI is removed.
|
|
237
76
|
*/
|
|
238
77
|
export function isDowngradedFromKeychain(): boolean {
|
|
239
|
-
return
|
|
78
|
+
return false;
|
|
240
79
|
}
|
|
241
80
|
|
|
242
81
|
// ---------------------------------------------------------------------------
|
|
243
|
-
// Async variants —
|
|
244
|
-
// loop during keychain operations. Preferred for non-startup code paths.
|
|
82
|
+
// Async variants — try broker first, fall back to encrypted store
|
|
245
83
|
// ---------------------------------------------------------------------------
|
|
246
84
|
|
|
247
85
|
/**
|
|
248
|
-
* Async version of `getSecureKey
|
|
86
|
+
* Async version of `getSecureKey`. When the broker is available it is
|
|
87
|
+
* queried first. A `null` return from the broker means error (fall back
|
|
88
|
+
* to encrypted store). A `{ found: false }` also falls back to the
|
|
89
|
+
* encrypted store — keys may exist only in `keys.enc` (e.g. written
|
|
90
|
+
* while the broker was unavailable or via sync `setSecureKey`).
|
|
249
91
|
*/
|
|
250
92
|
export async function getSecureKeyAsync(
|
|
251
93
|
account: string,
|
|
252
94
|
): Promise<string | undefined> {
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (backend === "encrypted") {
|
|
267
|
-
const value = encryptedStore.getKey(account);
|
|
268
|
-
if (
|
|
269
|
-
value === undefined &&
|
|
270
|
-
downgradedFromKeychain &&
|
|
271
|
-
!keychainMissCache.has(account)
|
|
272
|
-
) {
|
|
273
|
-
try {
|
|
274
|
-
const keychainValue =
|
|
275
|
-
(await keychain.getKeyAsync(account)) ?? undefined;
|
|
276
|
-
if (keychainValue === undefined) {
|
|
277
|
-
keychainMissCache.add(account);
|
|
278
|
-
}
|
|
279
|
-
return keychainValue;
|
|
280
|
-
} catch {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return value;
|
|
285
|
-
}
|
|
286
|
-
return undefined;
|
|
95
|
+
const broker = getBroker();
|
|
96
|
+
if (broker.isAvailable()) {
|
|
97
|
+
const result = await broker.get(account);
|
|
98
|
+
// null = broker error, fall back to encrypted store
|
|
99
|
+
if (result == null) return encryptedStore.getKey(account);
|
|
100
|
+
// Broker found the key — use it
|
|
101
|
+
if (result.found) return result.value;
|
|
102
|
+
// Broker says not found — check encrypted store as fallback
|
|
103
|
+
return encryptedStore.getKey(account);
|
|
104
|
+
}
|
|
105
|
+
return encryptedStore.getKey(account);
|
|
287
106
|
}
|
|
288
107
|
|
|
289
108
|
/**
|
|
290
|
-
* Async version of `setSecureKey
|
|
109
|
+
* Async version of `setSecureKey`. When the broker is available the key
|
|
110
|
+
* is written there **and** to the encrypted store so that sync callers
|
|
111
|
+
* have a consistent view. Returns `true` only when both stores succeed.
|
|
112
|
+
*
|
|
113
|
+
* If the broker is available but `broker.set()` fails we return `false`
|
|
114
|
+
* immediately — falling through to an encrypted-store-only write would
|
|
115
|
+
* leave the broker with stale data that async readers would still see.
|
|
291
116
|
*/
|
|
292
117
|
export async function setSecureKeyAsync(
|
|
293
118
|
account: string,
|
|
294
119
|
value: string,
|
|
295
120
|
): Promise<boolean> {
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const exists = await keychain.getKeyAsync(account);
|
|
306
|
-
if (exists != null) {
|
|
307
|
-
await keychain.deleteKeyAsync(account);
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
/* best-effort */
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return result;
|
|
314
|
-
}
|
|
315
|
-
if (backend !== "keychain") return false;
|
|
316
|
-
|
|
317
|
-
const result = await keychain.setKeyAsync(account, value);
|
|
318
|
-
if (result === false) {
|
|
319
|
-
log.warn(
|
|
320
|
-
"Keychain operation failed at runtime, falling back to encrypted file storage",
|
|
321
|
-
);
|
|
322
|
-
resolvedBackend = "encrypted";
|
|
323
|
-
downgradedFromKeychain = true;
|
|
324
|
-
const fallbackResult = encryptedStore.setKey(account, value);
|
|
325
|
-
// Clean up stale keychain entry after runtime downgrade
|
|
326
|
-
if (fallbackResult) {
|
|
327
|
-
keychainMissCache.delete(account);
|
|
328
|
-
try {
|
|
329
|
-
const exists = await keychain.getKeyAsync(account);
|
|
330
|
-
if (exists != null) {
|
|
331
|
-
await keychain.deleteKeyAsync(account);
|
|
332
|
-
}
|
|
333
|
-
} catch {
|
|
334
|
-
/* best-effort */
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return fallbackResult;
|
|
338
|
-
}
|
|
339
|
-
return result;
|
|
121
|
+
const broker = getBroker();
|
|
122
|
+
if (broker.isAvailable()) {
|
|
123
|
+
const brokerOk = await broker.set(account, value);
|
|
124
|
+
if (!brokerOk) return false;
|
|
125
|
+
// Broker succeeded — also persist to encrypted store for sync callers.
|
|
126
|
+
const encOk = encryptedStore.setKey(account, value);
|
|
127
|
+
return encOk;
|
|
128
|
+
}
|
|
129
|
+
return encryptedStore.setKey(account, value);
|
|
340
130
|
}
|
|
341
131
|
|
|
342
132
|
/**
|
|
343
|
-
* Async version of `deleteSecureKey
|
|
133
|
+
* Async version of `deleteSecureKey`. When the broker is available the
|
|
134
|
+
* key is deleted there **and** from the encrypted store so that sync
|
|
135
|
+
* callers have a consistent view.
|
|
136
|
+
*
|
|
137
|
+
* Returns `"deleted"` when the key was removed, `"not-found"` when it
|
|
138
|
+
* didn't exist (idempotent), or `"error"` on a real backend failure.
|
|
139
|
+
*
|
|
140
|
+
* If the broker is available but `broker.del()` fails we return `"error"`
|
|
141
|
+
* immediately — falling through to an encrypted-store-only delete would
|
|
142
|
+
* leave the broker with the key, and async readers would still see it.
|
|
344
143
|
*/
|
|
345
|
-
export async function deleteSecureKeyAsync(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
} catch {
|
|
363
|
-
// fall through
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const result = await keychain.deleteKeyAsync(account);
|
|
367
|
-
if (result === false) {
|
|
368
|
-
log.warn(
|
|
369
|
-
"Keychain operation failed at runtime, falling back to encrypted file storage",
|
|
370
|
-
);
|
|
371
|
-
resolvedBackend = "encrypted";
|
|
372
|
-
downgradedFromKeychain = true;
|
|
373
|
-
return encryptedStore.deleteKey(account);
|
|
374
|
-
}
|
|
375
|
-
return result;
|
|
144
|
+
export async function deleteSecureKeyAsync(
|
|
145
|
+
account: string,
|
|
146
|
+
): Promise<DeleteResult> {
|
|
147
|
+
const broker = getBroker();
|
|
148
|
+
if (broker.isAvailable()) {
|
|
149
|
+
const brokerOk = await broker.del(account);
|
|
150
|
+
if (!brokerOk) return "error";
|
|
151
|
+
// Broker succeeded — also remove from encrypted store for sync callers.
|
|
152
|
+
const encResult = encryptedStore.deleteKey(account);
|
|
153
|
+
// Broker deletion succeeded; encrypted-store "not-found" is fine
|
|
154
|
+
// (key may only exist in the broker).
|
|
155
|
+
if (encResult === "error") return "error";
|
|
156
|
+
return "deleted";
|
|
157
|
+
}
|
|
158
|
+
return encryptedStore.deleteKey(account);
|
|
376
159
|
}
|
|
377
160
|
|
|
378
|
-
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Test helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/** @internal Test-only: reset the cached broker so it's re-created. */
|
|
379
166
|
export function _resetBackend(): void {
|
|
380
|
-
|
|
381
|
-
downgradedFromKeychain = false;
|
|
382
|
-
keychainMissCache.clear();
|
|
167
|
+
_broker = undefined;
|
|
383
168
|
}
|
|
384
169
|
|
|
385
170
|
/** @internal Test-only: force a specific backend. Pass `undefined` to reset. */
|
|
386
|
-
export function _setBackend(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
171
|
+
export function _setBackend(
|
|
172
|
+
_backend: "keychain" | "encrypted" | "broker" | null | undefined,
|
|
173
|
+
): void {
|
|
174
|
+
// No-op — kept for test compatibility.
|
|
390
175
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { setHomeBaseAppLink } from "../../home-base/app-link-store.js";
|
|
12
|
+
import { generateAppIcon } from "../../media/app-icon-generator.js";
|
|
12
13
|
import type { AppDefinition } from "../../memory/app-store.js";
|
|
13
14
|
import type { EditEngineResult } from "../../memory/app-store.js";
|
|
14
15
|
|
|
@@ -40,6 +41,7 @@ export interface AppStoreWriter {
|
|
|
40
41
|
createApp(params: {
|
|
41
42
|
name: string;
|
|
42
43
|
description?: string;
|
|
44
|
+
icon?: string;
|
|
43
45
|
schemaJson: string;
|
|
44
46
|
htmlDefinition: string;
|
|
45
47
|
pages?: Record<string, string>;
|
|
@@ -139,9 +141,15 @@ export async function executeAppCreate(
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
// Extract icon from preview if provided — only persist emoji-like values,
|
|
145
|
+
// not URLs which would render as raw strings in UI and bundle manifests.
|
|
146
|
+
const rawIcon = preview?.icon as string | undefined;
|
|
147
|
+
const icon = rawIcon && !rawIcon.startsWith("http") ? rawIcon : undefined;
|
|
148
|
+
|
|
142
149
|
const app = store.createApp({
|
|
143
150
|
name,
|
|
144
151
|
description,
|
|
152
|
+
icon,
|
|
145
153
|
schemaJson,
|
|
146
154
|
htmlDefinition,
|
|
147
155
|
pages,
|
|
@@ -405,3 +413,68 @@ export function executeAppFileWrite(
|
|
|
405
413
|
status: input.status,
|
|
406
414
|
};
|
|
407
415
|
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// app_generate_icon
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
export interface AppGenerateIconInput {
|
|
422
|
+
app_id: string;
|
|
423
|
+
description?: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function executeAppGenerateIcon(
|
|
427
|
+
input: AppGenerateIconInput,
|
|
428
|
+
store: AppStoreReader,
|
|
429
|
+
): Promise<ExecutorResult> {
|
|
430
|
+
const app = store.getApp(input.app_id);
|
|
431
|
+
if (!app) {
|
|
432
|
+
return {
|
|
433
|
+
content: JSON.stringify({ error: `App '${input.app_id}' not found` }),
|
|
434
|
+
isError: true,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Generate to a temp path first, then swap on success to avoid
|
|
439
|
+
// destroying an existing icon if generation fails.
|
|
440
|
+
const { existsSync, renameSync, unlinkSync } = await import("node:fs");
|
|
441
|
+
const { join } = await import("node:path");
|
|
442
|
+
const { getAppsDir } = await import("../../memory/app-store.js");
|
|
443
|
+
const iconPath = join(getAppsDir(), input.app_id, "icon.png");
|
|
444
|
+
const tempPath = join(getAppsDir(), input.app_id, "icon.tmp.png");
|
|
445
|
+
|
|
446
|
+
// Temporarily move existing icon aside so generateAppIcon doesn't skip
|
|
447
|
+
if (existsSync(iconPath)) {
|
|
448
|
+
renameSync(iconPath, tempPath);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
await generateAppIcon(
|
|
452
|
+
input.app_id,
|
|
453
|
+
app.name,
|
|
454
|
+
input.description ?? app.description,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (existsSync(iconPath)) {
|
|
458
|
+
// Success — clean up the old icon backup
|
|
459
|
+
if (existsSync(tempPath)) {
|
|
460
|
+
unlinkSync(tempPath);
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
content: JSON.stringify({ generated: true, appId: input.app_id }),
|
|
464
|
+
isError: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Generation failed — restore the previous icon if we had one
|
|
469
|
+
if (existsSync(tempPath)) {
|
|
470
|
+
renameSync(tempPath, iconPath);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
content: JSON.stringify({
|
|
475
|
+
error:
|
|
476
|
+
"Icon generation failed. Make sure a Gemini API key is configured in Settings.",
|
|
477
|
+
}),
|
|
478
|
+
isError: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -9,7 +9,7 @@ import { getLogger } from "../../util/logger.js";
|
|
|
9
9
|
|
|
10
10
|
const log = getLogger("auto-navigate");
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_CDP_BASE = "http://localhost:9222";
|
|
13
13
|
const MAX_PAGES = 10;
|
|
14
14
|
const PAGE_WAIT_MS = 2500;
|
|
15
15
|
const SCROLL_WAIT_MS = 1000;
|
|
@@ -80,23 +80,32 @@ export interface AutoNavProgress {
|
|
|
80
80
|
visitedCount?: number;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export interface AutoNavOptions {
|
|
84
|
+
abortSignal?: { aborted: boolean };
|
|
85
|
+
onProgress?: (p: AutoNavProgress) => void;
|
|
86
|
+
cdpBaseUrl?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
/**
|
|
84
90
|
* Navigate Chrome through a domain's pages to trigger API calls.
|
|
85
91
|
* Discovers internal links from the DOM and visits up to ~15 unique paths.
|
|
86
92
|
*
|
|
87
93
|
* @param domain The domain to crawl (e.g. "example.com").
|
|
88
|
-
* @param
|
|
89
|
-
* @param onProgress Optional callback for live progress updates.
|
|
94
|
+
* @param options Optional configuration for abort, progress, and CDP base URL.
|
|
90
95
|
* @returns List of visited page URLs.
|
|
91
96
|
*/
|
|
92
97
|
export async function autoNavigate(
|
|
93
98
|
domain: string,
|
|
94
|
-
|
|
95
|
-
onProgress?: (p: AutoNavProgress) => void,
|
|
99
|
+
options?: AutoNavOptions,
|
|
96
100
|
): Promise<string[]> {
|
|
101
|
+
const {
|
|
102
|
+
abortSignal,
|
|
103
|
+
onProgress,
|
|
104
|
+
cdpBaseUrl = DEFAULT_CDP_BASE,
|
|
105
|
+
} = options ?? {};
|
|
97
106
|
let wsUrl: string | null = null;
|
|
98
107
|
try {
|
|
99
|
-
const res = await fetch(`${
|
|
108
|
+
const res = await fetch(`${cdpBaseUrl}/json/list`);
|
|
100
109
|
if (!res.ok) {
|
|
101
110
|
log.warn("CDP not available for auto-navigation");
|
|
102
111
|
return [];
|