@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Chrome CDP session management.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the duplicated launch / readiness / window-management logic
|
|
5
|
+
* that was previously copy-pasted across the Amazon and DoorDash CLIs.
|
|
6
|
+
* Callers get back a {@link CdpSession} with structured metadata so they can
|
|
7
|
+
* make cleanup decisions (e.g. only kill Chrome if *we* launched it).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join as pathJoin } from "node:path";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CDP_PORT = 9222;
|
|
19
|
+
const DEFAULT_CDP_BASE = `http://localhost:${DEFAULT_CDP_PORT}`;
|
|
20
|
+
const DEFAULT_USER_DATA_DIR = pathJoin(
|
|
21
|
+
homedir(),
|
|
22
|
+
"Library/Application Support/Google/Chrome-CDP",
|
|
23
|
+
);
|
|
24
|
+
const CHROME_APP_PATH =
|
|
25
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export interface CdpSession {
|
|
32
|
+
/** Base URL for the CDP HTTP endpoints (e.g. `http://localhost:9222`). */
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
/** Whether this helper launched Chrome (true) or it was already running (false). */
|
|
35
|
+
launchedByUs: boolean;
|
|
36
|
+
/** The `--user-data-dir` used for the Chrome instance. */
|
|
37
|
+
userDataDir: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EnsureChromeOptions {
|
|
41
|
+
/** CDP port. Defaults to `9222`. */
|
|
42
|
+
port?: number;
|
|
43
|
+
/** User data directory for Chrome. Defaults to `~/Library/Application Support/Google/Chrome-CDP`. */
|
|
44
|
+
userDataDir?: string;
|
|
45
|
+
/** Initial URL to open when launching Chrome. */
|
|
46
|
+
startUrl?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Readiness check
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns `true` when a CDP endpoint is responding at the given base URL.
|
|
55
|
+
*/
|
|
56
|
+
export async function isCdpReady(
|
|
57
|
+
cdpBase: string = DEFAULT_CDP_BASE,
|
|
58
|
+
): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${cdpBase}/json/version`);
|
|
61
|
+
return res.ok;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Launch / ensure
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ensure a Chrome instance with CDP is available. If one is already listening
|
|
73
|
+
* on the target port, returns immediately. Otherwise spawns a new detached
|
|
74
|
+
* Chrome process and waits for the CDP endpoint to become ready.
|
|
75
|
+
*
|
|
76
|
+
* Returns a {@link CdpSession} with metadata about the running instance.
|
|
77
|
+
*/
|
|
78
|
+
export async function ensureChromeWithCdp(
|
|
79
|
+
options: EnsureChromeOptions = {},
|
|
80
|
+
): Promise<CdpSession> {
|
|
81
|
+
const port = options.port ?? DEFAULT_CDP_PORT;
|
|
82
|
+
const baseUrl = `http://localhost:${port}`;
|
|
83
|
+
const userDataDir = options.userDataDir ?? DEFAULT_USER_DATA_DIR;
|
|
84
|
+
|
|
85
|
+
if (await isCdpReady(baseUrl)) {
|
|
86
|
+
return { baseUrl, launchedByUs: false, userDataDir };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const args = [
|
|
90
|
+
`--remote-debugging-port=${port}`,
|
|
91
|
+
`--force-renderer-accessibility`,
|
|
92
|
+
`--user-data-dir=${userDataDir}`,
|
|
93
|
+
];
|
|
94
|
+
if (options.startUrl) {
|
|
95
|
+
args.push(options.startUrl);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
spawnChild(CHROME_APP_PATH, args, {
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: "ignore",
|
|
101
|
+
}).unref();
|
|
102
|
+
|
|
103
|
+
// Poll until CDP responds (up to 15 s)
|
|
104
|
+
for (let i = 0; i < 30; i++) {
|
|
105
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
106
|
+
if (await isCdpReady(baseUrl)) {
|
|
107
|
+
return { baseUrl, launchedByUs: true, userDataDir };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error("Chrome started but CDP endpoint not responding after 15s");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Window management helpers
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Look up the first page target and return its WebSocket debugger URL.
|
|
120
|
+
*/
|
|
121
|
+
async function findPageTarget(cdpBase: string): Promise<string | null> {
|
|
122
|
+
const res = await fetch(`${cdpBase}/json/list`);
|
|
123
|
+
const targets = (await res.json()) as Array<{
|
|
124
|
+
type: string;
|
|
125
|
+
webSocketDebuggerUrl: string;
|
|
126
|
+
}>;
|
|
127
|
+
const page = targets.find((t) => t.type === "page");
|
|
128
|
+
return page?.webSocketDebuggerUrl ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Set the window state of the Chrome window owning the first page target.
|
|
133
|
+
* Used by both minimize and restore.
|
|
134
|
+
*/
|
|
135
|
+
async function setWindowState(
|
|
136
|
+
cdpBase: string,
|
|
137
|
+
windowState: "minimized" | "normal",
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const wsUrl = await findPageTarget(cdpBase);
|
|
140
|
+
if (!wsUrl) return;
|
|
141
|
+
|
|
142
|
+
const ws = new WebSocket(wsUrl);
|
|
143
|
+
|
|
144
|
+
await new Promise<void>((resolve, reject) => {
|
|
145
|
+
const timeout = setTimeout(() => {
|
|
146
|
+
ws.close();
|
|
147
|
+
reject(new Error(`CDP ${windowState} timed out`));
|
|
148
|
+
}, 5000);
|
|
149
|
+
|
|
150
|
+
ws.addEventListener("open", () => {
|
|
151
|
+
ws.send(JSON.stringify({ id: 1, method: "Browser.getWindowForTarget" }));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
ws.addEventListener("message", (event) => {
|
|
155
|
+
const msg = JSON.parse(String(event.data)) as {
|
|
156
|
+
id: number;
|
|
157
|
+
result?: { windowId: number };
|
|
158
|
+
error?: { message: string };
|
|
159
|
+
};
|
|
160
|
+
if (msg.id === 1 && msg.result) {
|
|
161
|
+
ws.send(
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
id: 2,
|
|
164
|
+
method: "Browser.setWindowBounds",
|
|
165
|
+
params: {
|
|
166
|
+
windowId: msg.result.windowId,
|
|
167
|
+
bounds: { windowState },
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
} else if (msg.id === 1) {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
ws.close();
|
|
174
|
+
reject(new Error("Browser.getWindowForTarget failed"));
|
|
175
|
+
} else if (msg.id === 2) {
|
|
176
|
+
clearTimeout(timeout);
|
|
177
|
+
ws.close();
|
|
178
|
+
if (msg.error) {
|
|
179
|
+
reject(
|
|
180
|
+
new Error(`Browser.setWindowBounds failed: ${msg.error.message}`),
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
resolve();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
ws.addEventListener("error", (err) => {
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
reject(err);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Minimize the Chrome window associated with the CDP session.
|
|
197
|
+
*/
|
|
198
|
+
export async function minimizeChromeWindow(
|
|
199
|
+
cdpBase: string = DEFAULT_CDP_BASE,
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
await setWindowState(cdpBase, "minimized");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Restore (un-minimize) the Chrome window associated with the CDP session.
|
|
206
|
+
*/
|
|
207
|
+
export async function restoreChromeWindow(
|
|
208
|
+
cdpBase: string = DEFAULT_CDP_BASE,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
await setWindowState(cdpBase, "normal");
|
|
211
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
mock,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
|
|
11
|
+
// Mock the logger to avoid side effects during tests
|
|
12
|
+
mock.module("../../util/logger.js", () => ({
|
|
13
|
+
getLogger: () => ({
|
|
14
|
+
info: () => {},
|
|
15
|
+
debug: () => {},
|
|
16
|
+
warn: () => {},
|
|
17
|
+
error: () => {},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { NetworkRecorder } = await import("./network-recorder.js");
|
|
22
|
+
|
|
23
|
+
describe("NetworkRecorder", () => {
|
|
24
|
+
describe("startDirect CDP URL passthrough", () => {
|
|
25
|
+
const originalFetch = globalThis.fetch;
|
|
26
|
+
let fetchCalls: string[];
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
fetchCalls = [];
|
|
30
|
+
// Mock fetch to capture the URL and return a valid CDP version response.
|
|
31
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
32
|
+
fetchCalls.push(String(url));
|
|
33
|
+
return new Response(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
webSocketDebuggerUrl: "ws://localhost:1234/devtools/browser/fake",
|
|
36
|
+
}),
|
|
37
|
+
{ status: 200 },
|
|
38
|
+
);
|
|
39
|
+
}) as unknown as typeof fetch;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
globalThis.fetch = originalFetch;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Safety net: restore fetch even if afterEach is skipped due to a test error
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
globalThis.fetch = originalFetch;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses constructor-provided cdpBaseUrl when called without arguments", async () => {
|
|
52
|
+
const customBase = "http://custom-host:9333";
|
|
53
|
+
const recorder = new NetworkRecorder(undefined, customBase);
|
|
54
|
+
|
|
55
|
+
// startDirect will fail at the WebSocket connect step, but we only
|
|
56
|
+
// care that fetch was called with the correct URL.
|
|
57
|
+
try {
|
|
58
|
+
await recorder.startDirect();
|
|
59
|
+
} catch {
|
|
60
|
+
// Expected — WebSocket connection will fail in test environment
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
expect(fetchCalls.length).toBeGreaterThanOrEqual(1);
|
|
64
|
+
expect(fetchCalls[0]).toBe(`${customBase}/json/version`);
|
|
65
|
+
expect(fetchCalls[0]).not.toContain("undefined");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses explicit cdpBaseUrl argument when provided", async () => {
|
|
69
|
+
const constructorBase = "http://constructor-host:9222";
|
|
70
|
+
const explicitBase = "http://explicit-host:9444";
|
|
71
|
+
const recorder = new NetworkRecorder(undefined, constructorBase);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await recorder.startDirect(explicitBase);
|
|
75
|
+
} catch {
|
|
76
|
+
// Expected — WebSocket connection will fail in test environment
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(fetchCalls.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(fetchCalls[0]).toBe(`${explicitBase}/json/version`);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -18,8 +18,8 @@ const log = getLogger("network-recorder");
|
|
|
18
18
|
/** Max response body size to capture (64 KB). */
|
|
19
19
|
const MAX_BODY_SIZE = 64 * 1024;
|
|
20
20
|
|
|
21
|
-
/** CDP endpoint
|
|
22
|
-
const
|
|
21
|
+
/** Default CDP endpoint — used when no base URL is injected. */
|
|
22
|
+
const DEFAULT_CDP_BASE = "http://localhost:9222";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Minimal CDP client over WebSocket — talks the Chrome DevTools Protocol directly
|
|
@@ -115,7 +115,7 @@ export class NetworkRecorder {
|
|
|
115
115
|
private entries = new Map<string, NetworkRecordedEntry>();
|
|
116
116
|
private targetDomain?: string;
|
|
117
117
|
private running = false;
|
|
118
|
-
private cdpBaseUrl =
|
|
118
|
+
private cdpBaseUrl = DEFAULT_CDP_BASE;
|
|
119
119
|
private attachedTargetIds = new Set<string>();
|
|
120
120
|
private targetPollTimer?: ReturnType<typeof setInterval>;
|
|
121
121
|
|
|
@@ -130,20 +130,21 @@ export class NetworkRecorder {
|
|
|
130
130
|
return this.entries.size;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
constructor(targetDomain?: string) {
|
|
133
|
+
constructor(targetDomain?: string, cdpBaseUrl?: string) {
|
|
134
134
|
this.targetDomain = targetDomain;
|
|
135
|
+
if (cdpBaseUrl) this.cdpBaseUrl = cdpBaseUrl;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
139
|
* Connect directly to Chrome's CDP endpoint and start recording network events.
|
|
139
140
|
* Attaches to the browser-level target so events from all tabs are captured.
|
|
140
141
|
*/
|
|
141
|
-
async startDirect(cdpBaseUrl
|
|
142
|
+
async startDirect(cdpBaseUrl?: string): Promise<void> {
|
|
142
143
|
if (this.running) return;
|
|
143
|
-
this.cdpBaseUrl = cdpBaseUrl;
|
|
144
|
+
if (cdpBaseUrl) this.cdpBaseUrl = cdpBaseUrl;
|
|
144
145
|
|
|
145
146
|
// Discover the browser's WebSocket debugger URL
|
|
146
|
-
const versionRes = await fetch(`${cdpBaseUrl}/json/version`);
|
|
147
|
+
const versionRes = await fetch(`${this.cdpBaseUrl}/json/version`);
|
|
147
148
|
const version = (await versionRes.json()) as {
|
|
148
149
|
webSocketDebuggerUrl: string;
|
|
149
150
|
};
|
|
@@ -9,7 +9,7 @@ import { getLogger } from "../../util/logger.js";
|
|
|
9
9
|
|
|
10
10
|
const log = getLogger("x-auto-navigate");
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_CDP_BASE = "http://localhost:9222";
|
|
13
13
|
|
|
14
14
|
interface NavStep {
|
|
15
15
|
label: string;
|
|
@@ -71,19 +71,25 @@ class MiniCDP {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export interface NavigateXPagesOptions {
|
|
75
|
+
abortSignal?: { aborted: boolean };
|
|
76
|
+
cdpBaseUrl?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
/**
|
|
75
80
|
* Navigate Chrome through X.com pages to trigger GraphQL calls.
|
|
76
81
|
* The NetworkRecorder should already be attached and capturing.
|
|
77
82
|
*
|
|
78
|
-
* @param
|
|
83
|
+
* @param options Optional configuration for abort and CDP base URL.
|
|
79
84
|
* @returns List of step labels that completed successfully.
|
|
80
85
|
*/
|
|
81
|
-
export async function navigateXPages(
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
export async function navigateXPages(
|
|
87
|
+
options?: NavigateXPagesOptions,
|
|
88
|
+
): Promise<string[]> {
|
|
89
|
+
const { abortSignal, cdpBaseUrl = DEFAULT_CDP_BASE } = options ?? {};
|
|
84
90
|
let wsUrl: string | null = null;
|
|
85
91
|
try {
|
|
86
|
-
const res = await fetch(`${
|
|
92
|
+
const res = await fetch(`${cdpBaseUrl}/json/list`);
|
|
87
93
|
if (!res.ok) {
|
|
88
94
|
log.warn("CDP not available for auto-navigation");
|
|
89
95
|
return [];
|
|
@@ -30,6 +30,19 @@ export interface CredentialPolicy {
|
|
|
30
30
|
/** How a credential value is injected into an outbound proxied request. */
|
|
31
31
|
export type CredentialInjectionType = "header" | "query";
|
|
32
32
|
|
|
33
|
+
/** Reference to another credential whose value is composed with the primary value. */
|
|
34
|
+
export interface CredentialComposeRef {
|
|
35
|
+
/** Service of the credential to compose with. */
|
|
36
|
+
service: string;
|
|
37
|
+
/** Field of the credential to compose with. */
|
|
38
|
+
field: string;
|
|
39
|
+
/** Separator between the primary and composed values (e.g. ":"). */
|
|
40
|
+
separator: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Transform applied to a credential value after composition. */
|
|
44
|
+
export type CredentialValueTransform = "base64";
|
|
45
|
+
|
|
33
46
|
/**
|
|
34
47
|
* Describes where and how to inject a credential into proxied requests
|
|
35
48
|
* matching a specific host pattern.
|
|
@@ -45,6 +58,17 @@ export interface CredentialInjectionTemplate {
|
|
|
45
58
|
valuePrefix?: string;
|
|
46
59
|
/** Query parameter name when injectionType is 'query'. */
|
|
47
60
|
queryParamName?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Compose this credential's value with another credential's value before injection.
|
|
63
|
+
* The result is `{primaryValue}{separator}{composedValue}`, optionally transformed
|
|
64
|
+
* by `valueTransform`.
|
|
65
|
+
*/
|
|
66
|
+
composeWith?: CredentialComposeRef;
|
|
67
|
+
/**
|
|
68
|
+
* Transform applied to the (possibly composed) value before prepending `valuePrefix`.
|
|
69
|
+
* Applied after composition.
|
|
70
|
+
*/
|
|
71
|
+
valueTransform?: CredentialValueTransform;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
/** Input fields for specifying policy when storing a credential. */
|
|
@@ -10,9 +10,7 @@ import type { ToolDefinition } from "../../providers/types.js";
|
|
|
10
10
|
import type { TokenEndpointAuthMethod } from "../../security/oauth2.js";
|
|
11
11
|
import {
|
|
12
12
|
deleteSecureKey,
|
|
13
|
-
getBackendType,
|
|
14
13
|
getSecureKey,
|
|
15
|
-
isDowngradedFromKeychain,
|
|
16
14
|
listSecureKeys,
|
|
17
15
|
setSecureKey,
|
|
18
16
|
} from "../../security/secure-keys.js";
|
|
@@ -426,30 +424,21 @@ class CredentialStoreTool implements Tool {
|
|
|
426
424
|
}
|
|
427
425
|
|
|
428
426
|
const allMetadata = listCredentialMetadata();
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// re-read/re-derive the store). On keychain we trust metadata since the
|
|
432
|
-
// OS keychain has no batch list API.
|
|
433
|
-
// In downgraded mode (keychain failed, switched to encrypted), skip
|
|
434
|
-
// batch verification because listSecureKeys() only returns keys from
|
|
435
|
-
// the encrypted store — keychain-only credentials would be hidden.
|
|
436
|
-
const downgraded = isDowngradedFromKeychain();
|
|
437
|
-
const verifySecrets = getBackendType() === "encrypted" && !downgraded;
|
|
427
|
+
// Verify secrets still exist by reading all key names once (instead of
|
|
428
|
+
// per-entry getSecureKey calls that each re-read/re-derive the store).
|
|
438
429
|
let secureKeySet: Set<string> | undefined;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
};
|
|
452
|
-
}
|
|
430
|
+
try {
|
|
431
|
+
secureKeySet = new Set(listSecureKeys());
|
|
432
|
+
} catch (err) {
|
|
433
|
+
log.error(
|
|
434
|
+
{ err },
|
|
435
|
+
"Failed to read secure store while listing credentials",
|
|
436
|
+
);
|
|
437
|
+
return {
|
|
438
|
+
content:
|
|
439
|
+
"Error: failed to read secure storage; cannot list credentials",
|
|
440
|
+
isError: true,
|
|
441
|
+
};
|
|
453
442
|
}
|
|
454
443
|
const entries = allMetadata
|
|
455
444
|
.filter((m) => {
|
|
@@ -505,8 +494,14 @@ class CredentialStoreTool implements Tool {
|
|
|
505
494
|
}
|
|
506
495
|
|
|
507
496
|
const key = `credential:${service}:${field}`;
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
497
|
+
const result = deleteSecureKey(key);
|
|
498
|
+
if (result === "error") {
|
|
499
|
+
return {
|
|
500
|
+
content: `Error: failed to delete credential ${service}/${field} from secure storage`,
|
|
501
|
+
isError: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (result === "not-found") {
|
|
510
505
|
return {
|
|
511
506
|
content: `Error: credential ${service}/${field} not found`,
|
|
512
507
|
isError: true,
|
|
@@ -32,6 +32,7 @@ import { listCredentialMetadata } from "../../credentials/metadata-store.js";
|
|
|
32
32
|
import type { CredentialInjectionTemplate } from "../../credentials/policy-types.js";
|
|
33
33
|
import {
|
|
34
34
|
resolveById,
|
|
35
|
+
resolveByServiceField,
|
|
35
36
|
type ResolvedCredential,
|
|
36
37
|
} from "../../credentials/resolve.js";
|
|
37
38
|
|
|
@@ -92,6 +93,35 @@ const sessions = new Map<ProxySessionId, ManagedSession>();
|
|
|
92
93
|
*/
|
|
93
94
|
const acquireLocks = new Map<string, Promise<ProxySession>>();
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Build the final header value for a matched credential injection template.
|
|
98
|
+
* Handles optional composition with a second credential and value transforms.
|
|
99
|
+
* Returns null if any referenced credential cannot be resolved.
|
|
100
|
+
*/
|
|
101
|
+
function buildInjectedValue(
|
|
102
|
+
tpl: CredentialInjectionTemplate,
|
|
103
|
+
primaryValue: string,
|
|
104
|
+
): string | null {
|
|
105
|
+
let value = primaryValue;
|
|
106
|
+
|
|
107
|
+
if (tpl.composeWith) {
|
|
108
|
+
const composed = resolveByServiceField(
|
|
109
|
+
tpl.composeWith.service,
|
|
110
|
+
tpl.composeWith.field,
|
|
111
|
+
);
|
|
112
|
+
if (!composed) return null;
|
|
113
|
+
const composedValue = getSecureKey(composed.storageKey);
|
|
114
|
+
if (!composedValue) return null;
|
|
115
|
+
value = `${value}${tpl.composeWith.separator}${composedValue}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tpl.valueTransform === "base64") {
|
|
119
|
+
value = Buffer.from(value).toString("base64");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (tpl.valuePrefix ?? "") + value;
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
/**
|
|
96
126
|
* Resolve injection templates for a credential.
|
|
97
127
|
*
|
|
@@ -295,8 +325,15 @@ export async function startSession(
|
|
|
295
325
|
const value = getSecureKey(resolved.storageKey);
|
|
296
326
|
if (!value) return req.headers;
|
|
297
327
|
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
const headerValue = buildInjectedValue(tpl, value);
|
|
329
|
+
if (!headerValue) {
|
|
330
|
+
log.warn(
|
|
331
|
+
{ host: req.hostname, credentialId: credId },
|
|
332
|
+
"MITM rewrite: blocking request — composeWith credential missing",
|
|
333
|
+
);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
req.headers[tpl.headerName.toLowerCase()] = headerValue;
|
|
300
337
|
return req.headers;
|
|
301
338
|
}
|
|
302
339
|
|
|
@@ -377,7 +414,14 @@ export async function startSession(
|
|
|
377
414
|
if (!value) return {};
|
|
378
415
|
|
|
379
416
|
if (template.injectionType === "header" && template.headerName) {
|
|
380
|
-
const headerValue = (template
|
|
417
|
+
const headerValue = buildInjectedValue(template, value);
|
|
418
|
+
if (!headerValue) {
|
|
419
|
+
log.warn(
|
|
420
|
+
{ hostname, credentialId },
|
|
421
|
+
"Policy: blocking matched request — composeWith credential missing",
|
|
422
|
+
);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
381
425
|
return { [template.headerName.toLowerCase()]: headerValue };
|
|
382
426
|
}
|
|
383
427
|
// Query param injection is handled via URL rewriting in the MITM path
|
package/src/tools/types.ts
CHANGED
|
@@ -167,6 +167,8 @@ export interface ToolContext {
|
|
|
167
167
|
requesterChatId?: string;
|
|
168
168
|
/** Slack channel ID for channel-scoped permission enforcement. When set, tools are checked against the channel's permission profile. */
|
|
169
169
|
channelPermissionChannelId?: string;
|
|
170
|
+
/** The tool_use block ID from the LLM response, used to correlate confirmation prompts with specific tool invocations. */
|
|
171
|
+
toolUseId?: string;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
export interface DiffInfo {
|
|
@@ -61,8 +61,7 @@ export const uiShowTool: Tool = {
|
|
|
61
61
|
"- Bulk actions: include `selectedRows` array with full row data for context\n\n" +
|
|
62
62
|
"Presenting choices: When the user needs to make a choice or provide structured input, prefer interactive surfaces over plain text. " +
|
|
63
63
|
"Use list (2-8 options, single select), form (structured input with typed fields), confirmation (destructive/important actions), or table (data review with selectable rows).\n\n" +
|
|
64
|
-
"Tool chaining: After gathering data via tools (web search, browser, APIs), synthesize results into a visual output
|
|
65
|
-
"Exception: get_weather automatically renders its own surface with live API data — do NOT call ui_show, ui_update, app_create, or web_search after get_weather. Just respond with a brief summary.\n\n" +
|
|
64
|
+
"Tool chaining: After gathering data via tools (web search, browser, APIs), synthesize results into a visual output.\n\n" +
|
|
66
65
|
'Task progress for multi-step workflows: Create a card with template "task_progress" and templateData containing steps. ' +
|
|
67
66
|
"As each step completes, call ui_update to patch data.templateData (not top-level fields). " +
|
|
68
67
|
'Set templateData.status to "completed" or "failed" when done.',
|
|
@@ -34,6 +34,8 @@ export interface WatchSession {
|
|
|
34
34
|
recordingId?: string;
|
|
35
35
|
/** Path where the learn recording was successfully saved (undefined if save failed) */
|
|
36
36
|
savedRecordingPath?: string;
|
|
37
|
+
/** Reason the learn-mode bootstrap failed (CDP launch vs recorder attach) */
|
|
38
|
+
bootstrapFailureReason?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Module-level map of watch sessions keyed by watchId. */
|