@weaccelerateinc/agent-sdk 0.0.0-stage → 0.11.0

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.
Files changed (52) hide show
  1. package/README.md +151 -2
  2. package/approved-extension-releases/0.0.0.1.json +41 -0
  3. package/approved-extension-releases/0.0.0.2.json +34 -0
  4. package/approved-extension.d.ts +24 -0
  5. package/approved-extension.js +90 -0
  6. package/browser-connector.d.ts +109 -0
  7. package/browser-connector.js +218 -0
  8. package/browser-operations.d.ts +56 -0
  9. package/browser-operations.js +144 -0
  10. package/browser-use-host.d.ts +46 -0
  11. package/browser-use-host.js +149 -0
  12. package/checkout-diagnostics.d.ts +34 -0
  13. package/checkout-diagnostics.js +81 -0
  14. package/cli.js +105 -0
  15. package/customer.env.example +6 -0
  16. package/host-adapter.d.ts +90 -0
  17. package/host-adapter.js +357 -0
  18. package/index.d.ts +253 -0
  19. package/index.js +504 -0
  20. package/linq-messaging.d.ts +45 -0
  21. package/linq-messaging.js +123 -0
  22. package/managed-mcp.d.ts +156 -0
  23. package/managed-mcp.js +427 -0
  24. package/package.json +114 -4
  25. package/partner-starter/.env.example +13 -0
  26. package/partner-starter/README.md +61 -0
  27. package/partner-starter/gitignore.template +5 -0
  28. package/partner-starter/package.json +21 -0
  29. package/partner-starter/runtime.mjs +33 -0
  30. package/partner-starter/server.mjs +13 -0
  31. package/partner-starter/smoke.mjs +61 -0
  32. package/partner-starter/synthetic.mjs +256 -0
  33. package/partner-starter/tests/starter.test.mjs +119 -0
  34. package/partner-starter/trusted-context.example.mjs +23 -0
  35. package/partner.d.ts +27 -0
  36. package/partner.js +53 -0
  37. package/postgres-host.d.ts +14 -0
  38. package/postgres-host.js +102 -0
  39. package/private-chat.d.ts +104 -0
  40. package/private-chat.js +316 -0
  41. package/private-ingress.d.ts +46 -0
  42. package/private-ingress.js +132 -0
  43. package/starter/.env.example +19 -0
  44. package/starter/README.md +82 -0
  45. package/starter/gitignore.template +5 -0
  46. package/starter/package.json +21 -0
  47. package/starter/runtime.mjs +66 -0
  48. package/starter/server.mjs +13 -0
  49. package/starter/smoke.mjs +55 -0
  50. package/starter/synthetic.mjs +163 -0
  51. package/starter/tests/starter.test.mjs +224 -0
  52. package/starter/trusted-context.example.mjs +49 -0
@@ -0,0 +1,218 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { normalizeBrowserOperation } from './browser-operations.js';
3
+ import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
4
+
5
+ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/u;
6
+ const ID = /^mcs_[A-Za-z0-9_-]{43}$/u;
7
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
8
+ const CONNECTOR = /^[a-z][a-z0-9_-]{0,63}$/u;
9
+ const OPAQUE = /^[A-Za-z0-9_.:-]{1,200}$/u;
10
+ const CONTEXT = ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'];
11
+ const DOC = ['owner', 'browser', 'document_id', 'checkout_url', 'expires_at'];
12
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
13
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
14
+ const check = (ok, code = 'configuration_invalid') => { if (!ok) throw new AccelerateError(code); };
15
+ function shape(value, required, optional = [], code) {
16
+ check(object(value) && required.every(key => Object.hasOwn(value, key)) &&
17
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key)), code);
18
+ }
19
+ function freeze(value) {
20
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
21
+ Object.values(value).forEach(freeze); Object.freeze(value);
22
+ }
23
+ return value;
24
+ }
25
+ function owner(value) {
26
+ shape(value, CONTEXT, [], 'permission_denied');
27
+ check(CONTEXT.slice(0, 3).every(key => typeof value[key] === 'string' && OWNER.test(value[key])) &&
28
+ typeof value.phone_number === 'string' && /^\+[1-9]\d{6,14}$/u.test(value.phone_number), 'permission_denied');
29
+ return freeze(Object.fromEntries(CONTEXT.map(key => [key, value[key]])));
30
+ }
31
+ const sameOwner = (a, b) => CONTEXT.every(key => a[key] === b[key]);
32
+ function url(value) {
33
+ try { normalizeBrowserOperation('navigate', { url: value }); }
34
+ catch { throw new AccelerateError('invalid_input'); }
35
+ return value;
36
+ }
37
+ function options(value = {}) {
38
+ shape(value, [], ['signal']); check(value.signal === undefined || value.signal instanceof AbortSignal);
39
+ check(!value.signal?.aborted, 'request_cancelled');
40
+ return Object.freeze(value.signal ? { signal: value.signal } : {});
41
+ }
42
+ function browser(value, provider, connectorId) {
43
+ shape(value, ['provider', 'browser_id', 'custody_mode'], provider === 'connector' ? ['connector_id'] : [], 'permission_denied');
44
+ check(value.provider === provider && value.custody_mode === 'customer_backend_trusted' &&
45
+ typeof value.browser_id === 'string' && (provider === 'browser_use' ? UUID : OPAQUE).test(value.browser_id) &&
46
+ (provider !== 'connector' || value.connector_id === connectorId), 'permission_denied');
47
+ return { ...value };
48
+ }
49
+ function record(value, context, config, { fenced = false, readiness = false, checkoutUrl } = {}) {
50
+ shape(value, DOC, [...(fenced ? ['fence_id'] : []), ...(readiness ? ['recording_disabled', 'extension', 'automation'] : [])], 'permission_denied');
51
+ const identity = owner(value.owner);
52
+ check(sameOwner(identity, context) && typeof value.document_id === 'string' && OPAQUE.test(value.document_id), 'permission_denied');
53
+ const binding = browser(value.browser, config.provider, config.connectorId);
54
+ const checkout = url(value.checkout_url);
55
+ check(!checkoutUrl || checkout === checkoutUrl, 'permission_denied');
56
+ check(typeof value.expires_at === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value.expires_at) &&
57
+ Number.isFinite(Date.parse(value.expires_at)), 'integration_unavailable');
58
+ const result = { owner: identity, browser: binding, document_id: value.document_id,
59
+ checkout_url: checkout, expires_at: value.expires_at };
60
+ if (fenced) {
61
+ check(typeof value.fence_id === 'string' && OPAQUE.test(value.fence_id), 'permission_denied');
62
+ result.fence_id = value.fence_id;
63
+ }
64
+ return freeze(result);
65
+ }
66
+ function sameDocument(actual, expected) {
67
+ check(actual.document_id === expected.document_id && actual.checkout_url === expected.checkout_url &&
68
+ ['provider', 'browser_id', 'connector_id', 'custody_mode'].every(key => actual.browser[key] === expected.browser[key]) &&
69
+ actual.expires_at === expected.expires_at, 'permission_denied');
70
+ }
71
+ function makeConnector(configuration, provider) {
72
+ shape(configuration, ['extensionRelease', 'host'], ['connectorId', 'minimumRemainingMs', 'now']);
73
+ const { connectorId, extensionRelease, host, minimumRemainingMs = 600000, now = Date.now } = configuration;
74
+ check(provider === 'browser_use' ? connectorId === undefined : typeof connectorId === 'string' && CONNECTOR.test(connectorId));
75
+ check(host && ['resolveOwnedBrowser', 'inspectBrowser', 'drainAndFence', 'bindCheckout', 'resolveCheckoutBrowser', 'confirmCleanup']
76
+ .every(key => typeof host[key] === 'function') && Number.isSafeInteger(minimumRemainingMs) &&
77
+ minimumRemainingMs >= 600000 && minimumRemainingMs <= 3600000 && typeof now === 'function');
78
+ let release;
79
+ try { release = validateApprovedExtensionRelease(extensionRelease); }
80
+ catch { throw new AccelerateError('configuration_invalid'); }
81
+ check(release.status === 'approved');
82
+ const config = { provider, connectorId };
83
+ const attachments = new WeakMap();
84
+ const hostCall = async (method, ...args) => {
85
+ try { return await host[method](...args); }
86
+ catch (error) {
87
+ // A provider transport error can contain connection URLs or credentials.
88
+ // Preserve only the SDK's fixed, allowlisted error projection.
89
+ throw error instanceof AccelerateError ? new AccelerateError(error.code, error.status, error.supportReference)
90
+ : new AccelerateError('integration_unavailable');
91
+ }
92
+ };
93
+ const checkpoint = opts => check(!opts.signal?.aborted, 'request_cancelled');
94
+ const inspect = async (context, expected, opts, fenceId, minimum = minimumRemainingMs) => {
95
+ checkpoint(opts);
96
+ const value = await hostCall('inspectBrowser', context, expected, opts);
97
+ checkpoint(opts);
98
+ const actual = record(value, context, config, { readiness: true });
99
+ sameDocument(actual, expected);
100
+ const current = now();
101
+ check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >= minimum &&
102
+ value.recording_disabled === true, 'integration_unavailable');
103
+ check(object(value.extension) && value.extension.artifact_sha256 === release.artifact.sha256, 'integration_unavailable');
104
+ try { verifyApprovedExtensionRuntime(value.extension, release, { executorContract: 'accelerate_manual_checkout_v1', provider }); }
105
+ catch { throw new AccelerateError('integration_unavailable'); }
106
+ shape(value.automation, ['ordinary_operations_drained', 'agent_access_fenced', 'fence_id'], [], 'integration_unavailable');
107
+ check(typeof value.automation.ordinary_operations_drained === 'boolean' && typeof value.automation.agent_access_fenced === 'boolean' &&
108
+ (value.automation.fence_id === null || typeof value.automation.fence_id === 'string' && OPAQUE.test(value.automation.fence_id)), 'integration_unavailable');
109
+ if (fenceId) check(value.automation.ordinary_operations_drained === true && value.automation.agent_access_fenced === true &&
110
+ value.automation.fence_id === fenceId, 'operation_conflict');
111
+ };
112
+ const resolve = async (context, checkoutId, opts) => {
113
+ check(typeof checkoutId === 'string' && ID.test(checkoutId), 'invalid_input');
114
+ const resolved = await hostCall('resolveCheckoutBrowser', context, freeze({ checkout_id: checkoutId }), opts);
115
+ checkpoint(opts);
116
+ return record(resolved, context, config, { fenced: true });
117
+ };
118
+ const stamp = value => {
119
+ check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_input');
120
+ return Date.parse(value);
121
+ };
122
+ const preparations = new WeakMap();
123
+ const canClick = typeof host.performFinalClick === 'function';
124
+ // Slow work (ownership lookup, provider read, runtime inspection) happens here, BEFORE the short arm exists,
125
+ // so none of it consumes the click window. The fence is never lifted. One preparation serves one click.
126
+ const prepareClick = canClick ? async (contextValue, input, requestOptions) => {
127
+ const context = owner(contextValue), opts = options(requestOptions);
128
+ shape(input, ['checkout_id'], [], 'invalid_input');
129
+ const expected = await resolve(context, input.checkout_id, opts);
130
+ // Fenced, drained runtime check; the ten-minute attachment headroom does not apply after the review window.
131
+ await inspect(context, expected, opts, expected.fence_id, 1);
132
+ const preparation = Object.freeze({});
133
+ preparations.set(preparation, { context, checkout_id: input.checkout_id, record: expected, at: now(), used: false });
134
+ return preparation;
135
+ } : undefined;
136
+ const finalClick = canClick ? async (contextValue, input, requestOptions) => {
137
+ const context = owner(contextValue), opts = options(requestOptions);
138
+ shape(input, ['checkout_id', 'preparation', 'checkout_url', 'submit_selector', 'armed_expires_at'], [], 'invalid_input');
139
+ const saved = preparations.get(input.preparation);
140
+ check(saved && !saved.used && sameOwner(saved.context, context) && saved.checkout_id === input.checkout_id, 'permission_denied');
141
+ saved.used = true;
142
+ check(typeof input.submit_selector === 'string' && input.submit_selector.length > 0 && input.submit_selector.length <= 200 &&
143
+ !/[\u0000-\u001f\u007f]/u.test(input.submit_selector), 'invalid_input');
144
+ const armedUntil = stamp(input.armed_expires_at), started = now();
145
+ // The service arm lives at most 15 seconds (never accepted longer); refuse an expired or implausibly long window.
146
+ check(Number.isFinite(started) && armedUntil > started && armedUntil - started <= 15000 && started - saved.at <= 60000, 'submit_unavailable');
147
+ // Cheap checks only inside the arm window: exact URL, browser outlives the arm, then the customer's physical click.
148
+ check(url(input.checkout_url) === saved.record.checkout_url && Date.parse(saved.record.expires_at) > armedUntil, 'permission_denied');
149
+ checkpoint(opts);
150
+ const result = await hostCall('performFinalClick', context, freeze({ checkout_id: input.checkout_id,
151
+ submit_selector: input.submit_selector, armed_expires_at: input.armed_expires_at, ...saved.record }), opts);
152
+ shape(result, ['dispatched'], [], 'integration_unavailable');
153
+ check(typeof result.dispatched === 'boolean', 'integration_unavailable');
154
+ return Object.freeze({ dispatched: result.dispatched });
155
+ } : undefined;
156
+ // Every method belongs to the server integration. A connector never creates a
157
+ // replacement browser, releases its durable fence or retries a payment call.
158
+ return Object.freeze({ contract: 'accelerate_existing_browser_v1', provider,
159
+ ...(connectorId ? { connector_id: connectorId } : {}),
160
+ async prepareAttachment(contextValue, input, requestOptions) {
161
+ const context = owner(contextValue), opts = options(requestOptions);
162
+ shape(input, ['attempt_key', 'checkout_url'], [], 'invalid_input');
163
+ check(typeof input.attempt_key === 'string' && input.attempt_key.length > 0 && input.attempt_key.length <= 200 &&
164
+ !/[\u0000-\u001f\u007f]/u.test(input.attempt_key), 'invalid_input');
165
+ const request = freeze({ attempt_key: input.attempt_key, checkout_url: url(input.checkout_url) });
166
+ const resolved = await hostCall('resolveOwnedBrowser', context, request, opts);
167
+ checkpoint(opts);
168
+ const expected = record(resolved, context, config, { checkoutUrl: request.checkout_url });
169
+ // Capabilities are checked BEFORE taking the fence or collecting OTP/CVV.
170
+ await inspect(context, expected, opts);
171
+ const ack = await hostCall('drainAndFence', context, expected, freeze({ attempt_key: request.attempt_key }), opts);
172
+ checkpoint(opts);
173
+ shape(ack, ['fence_id', 'ordinary_operations_drained', 'agent_access_fenced'], [], 'integration_unavailable');
174
+ check(typeof ack.fence_id === 'string' && OPAQUE.test(ack.fence_id) && ack.ordinary_operations_drained === true &&
175
+ ack.agent_access_fenced === true, 'operation_conflict');
176
+ await inspect(context, expected, opts, ack.fence_id);
177
+ const browser_resource = typeof host.clientBrowserResource === 'function'
178
+ ? await hostCall('clientBrowserResource', context, expected, opts) : undefined;
179
+ checkpoint(opts);
180
+ const attachment = Object.freeze({ browser: expected.browser, ...(browser_resource ? { browser_resource } : {}) });
181
+ attachments.set(attachment, { context, attempt_key: request.attempt_key, record: freeze({ ...expected, fence_id: ack.fence_id }) });
182
+ return attachment;
183
+ },
184
+ async commitAttachment(contextValue, input, requestOptions) {
185
+ const context = owner(contextValue), opts = options(requestOptions);
186
+ shape(input, ['checkout_id', 'attachment'], [], 'invalid_input');
187
+ const saved = attachments.get(input.attachment);
188
+ check(saved && sameOwner(saved.context, context) && typeof input.checkout_id === 'string' && ID.test(input.checkout_id), 'permission_denied');
189
+ check(!saved.checkout_id || saved.checkout_id === input.checkout_id, 'operation_conflict');
190
+ // Bind before awaiting transport so concurrent/lost-ACK recovery cannot
191
+ // assign this attachment to a different checkout.
192
+ saved.checkout_id = input.checkout_id;
193
+ await hostCall('bindCheckout', context, freeze({ ...saved.record, checkout_id: input.checkout_id, attempt_key: saved.attempt_key }), opts);
194
+ checkpoint(opts);
195
+ // Keep the capability for an idempotent same-checkout ACK recovery only.
196
+ },
197
+ async assertPaymentReady(contextValue, input, requestOptions) {
198
+ const context = owner(contextValue), opts = options(requestOptions);
199
+ shape(input, ['checkout_id'], ['checkout_url'], 'invalid_input');
200
+ const expected = await resolve(context, input.checkout_id, opts);
201
+ if (input.checkout_url !== undefined) check(url(input.checkout_url) === expected.checkout_url, 'permission_denied');
202
+ await inspect(context, expected, opts, expected.fence_id);
203
+ },
204
+ ...(finalClick ? { prepareFinalClick: prepareClick, performFinalClick: finalClick } : {}),
205
+ async confirmCleanup(contextValue, input, requestOptions) {
206
+ const context = owner(contextValue), opts = options(requestOptions);
207
+ shape(input, ['checkout_id'], [], 'invalid_input');
208
+ const expected = await resolve(context, input.checkout_id, opts);
209
+ const ack = await hostCall('confirmCleanup', context, expected, freeze({ checkout_id: input.checkout_id }), opts);
210
+ checkpoint(opts);
211
+ shape(ack, ['provider_browser_stopped', 'credentials_cleared', 'fence_retained'], [], 'integration_unavailable');
212
+ check(Object.values(ack).every(value => value === true), 'integration_unavailable');
213
+ },
214
+ });
215
+ }
216
+
217
+ export function createExistingBrowserConnector(options) { return makeConnector(options ?? {}, 'connector'); }
218
+ export function createBrowserUseConnector(options) { return makeConnector(options ?? {}, 'browser_use'); }
@@ -0,0 +1,56 @@
1
+ export type BrowserStep =
2
+ | { type: 'click'; ref: string; button?: 'left'; num_clicks?: 1; modifiers?: [] }
3
+ | { type: 'hover' | 'scroll_to'; ref: string }
4
+ | { type: 'fill'; ref: string; value: string }
5
+ | { type: 'type'; text: string; repeat?: number }
6
+ | { type: 'key'; text: 'Enter' | 'Tab' | 'Escape' | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' |
7
+ 'Backspace' | 'Delete' | 'Space'; repeat?: number }
8
+ | { type: 'wait'; ms?: number };
9
+ export type BrowserCondition =
10
+ | { type: 'text'; text: string; exists?: boolean }
11
+ | ({ type: 'url' | 'title'; exists?: boolean } &
12
+ ({ equals: string; contains?: string } | { equals?: string; contains: string }));
13
+ export type BrowserOperation =
14
+ | { operation: 'browser_snapshot'; arguments: { filter?: 'all' | 'interactive'; ref?: 'root'; depth?: number } }
15
+ | { operation: 'browser_text'; arguments: Record<string, never> }
16
+ | { operation: 'browser_find'; arguments: { query: string } }
17
+ | { operation: 'browser_wait_for'; arguments: { expect: BrowserCondition; timeout_ms?: number; poll_ms?: number } }
18
+ | { operation: 'browser_act'; arguments: { steps: BrowserStep[]; timeout_ms?: number; successor?: { filter?: 'all' | 'interactive'; depth?: number } } }
19
+ | { operation: 'navigate'; arguments: { url: string } };
20
+ export interface BrowserResult { content: Array<{ type: 'text'; text: string }> }
21
+
22
+ /** Dependency-free JSON Schema vocabulary used by the published browser grammar. */
23
+ export interface BrowserOperationJsonSchema {
24
+ [keyword: string]: unknown;
25
+ $schema?: 'https://json-schema.org/draft/2020-12/schema';
26
+ type?: 'object' | 'array' | 'string' | 'integer' | 'boolean';
27
+ description?: string;
28
+ properties?: Record<string, BrowserOperationJsonSchema>;
29
+ required?: string[];
30
+ additionalProperties?: boolean;
31
+ oneOf?: BrowserOperationJsonSchema[];
32
+ items?: BrowserOperationJsonSchema;
33
+ const?: string | number;
34
+ enum?: string[];
35
+ pattern?: string;
36
+ format?: string;
37
+ minimum?: number;
38
+ maximum?: number;
39
+ minLength?: number;
40
+ maxLength?: number;
41
+ minItems?: number;
42
+ maxItems?: number;
43
+ }
44
+
45
+ /** Frozen model-visible grammar. normalizeBrowserOperation remains required for semantic safety. */
46
+ export const browserOperationJsonSchema: Readonly<BrowserOperationJsonSchema & {
47
+ type: 'object'; oneOf: BrowserOperationJsonSchema[];
48
+ }>;
49
+
50
+ /** Strictly parses unknown JSON input through the same runtime semantic normalizer. */
51
+ export function parseBrowserOperation(value: unknown): BrowserOperation;
52
+
53
+ /** Throws a fixed error on unsupported or credential-bearing preparation input. */
54
+ export function normalizeBrowserOperation<T extends BrowserOperation['operation']>(
55
+ operation: T, args: Extract<BrowserOperation, { operation: T }>['arguments']
56
+ ): Extract<BrowserOperation, { operation: T }>['arguments'];
@@ -0,0 +1,144 @@
1
+ // One wire grammar shared by the customer SDK and Accelerate's upstream adapter.
2
+ // It contains no transport, tenant credentials or provider capabilities.
3
+ const sensitive = /(?:\d[ -]?){13,19}|\b(?:0[1-9]|1[0-2])\s*\/\s*(?:20)?\d{2}\b|\b(?:cvv|cvc|csc|security\s*code)\b["'\s:=]{0,12}\d{3,4}\b|\b(?:Bearer\s|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|aw[12]\.|agw_[A-Za-z0-9_-]{43}|atk_(?:test|live)_|tok_(?:sandbox|live)_|vgs_)|\b(?:cdp_ws_url|browser_live_view_url|devtoolsFrontendUrl|webSocketDebuggerUrl|kernel_session_id|playwright_endpoint)\b|(?:wss?|chrome-extension|devtools):\/\/|https?:\/\/[^\s/]*(?:onkernel\.com|verygoodproxy\.com|verygoodvault\.com)(?:[/:\s]|$)/i;
4
+ const limits = { timeout: [1, 8000], poll: [10, 1000], depth: [1, 15], repeat: [1, 3], wait: [1, 1000],
5
+ steps: [1, 8], ref: 60, text: 2000, query: 1000, url: 2048 };
6
+ const filters = ['all', 'interactive'];
7
+ const keys = ['Enter', 'Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Space'];
8
+ const refPattern = /^e\d+_\d+$/u;
9
+ const integerSchema = ([minimum, maximum]) => ({ type: 'integer', minimum, maximum });
10
+ const textSchema = (maxLength, minLength = 0) => ({ type: 'string', minLength, maxLength, pattern: '^[^\\u0000-\\u001f\\u007f]*$' });
11
+ const objectSchema = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
12
+ const snapshotSchema = (successor = false) => objectSchema({ filter: { type: 'string', enum: filters },
13
+ ...(!successor ? { ref: { type: 'string', const: 'root' } } : {}), depth: integerSchema(limits.depth) });
14
+ const conditionSchema = { oneOf: [
15
+ objectSchema({ type: { const: 'text' }, text: textSchema(limits.query, 1), exists: { type: 'boolean' } }, ['type', 'text']),
16
+ objectSchema({ type: { type: 'string', enum: ['url', 'title'] }, equals: textSchema(limits.url, 1),
17
+ contains: textSchema(limits.query, 1), exists: { type: 'boolean' } }, ['type', 'equals']),
18
+ objectSchema({ type: { type: 'string', enum: ['url', 'title'] }, contains: textSchema(limits.query, 1),
19
+ exists: { type: 'boolean' } }, ['type', 'contains']),
20
+ ] };
21
+ const refSchema = { type: 'string', pattern: refPattern.source, maxLength: limits.ref,
22
+ description: 'Copy a bare ref from the latest result. Every browser result replaces earlier refs.' };
23
+ const stepSchema = { oneOf: [
24
+ objectSchema({ type: { const: 'click' }, ref: refSchema, button: { const: 'left' }, num_clicks: { const: 1 },
25
+ modifiers: { type: 'array', maxItems: 0 } }, ['type', 'ref']),
26
+ objectSchema({ type: { type: 'string', enum: ['hover', 'scroll_to'] }, ref: refSchema }, ['type', 'ref']),
27
+ objectSchema({ type: { const: 'fill' }, ref: refSchema, value: { ...textSchema(limits.text),
28
+ description: 'Non-sensitive shopping/contact text only. Never enter card data or credentials.' } }, ['type', 'ref', 'value']),
29
+ objectSchema({ type: { const: 'type' }, text: textSchema(limits.text), repeat: integerSchema(limits.repeat) }, ['type', 'text']),
30
+ objectSchema({ type: { const: 'key' }, text: { type: 'string', enum: keys }, repeat: integerSchema(limits.repeat) }, ['type', 'text']),
31
+ objectSchema({ type: { const: 'wait' }, ms: integerSchema(limits.wait) }, ['type']),
32
+ ] };
33
+ const operationSchema = (operation, args) => objectSchema({ operation: { type: 'string', const: operation }, arguments: args }, ['operation', 'arguments']);
34
+ const freezeSchema = value => {
35
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
36
+ for (const child of Object.values(value)) freezeSchema(child);
37
+ Object.freeze(value);
38
+ }
39
+ return value;
40
+ };
41
+
42
+ /** Model-visible structural grammar. Always run normalizeBrowserOperation too;
43
+ * credential detection, URL safety and private checkout permissions remain runtime checks. */
44
+ export const browserOperationJsonSchema = freezeSchema({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object',
45
+ description: 'Restricted checkout preparation only. No scripts, screenshots, card entry or final submission.',
46
+ oneOf: [
47
+ operationSchema('browser_snapshot', snapshotSchema()),
48
+ operationSchema('browser_text', objectSchema({})),
49
+ operationSchema('browser_find', objectSchema({ query: textSchema(limits.query, 1) }, ['query'])),
50
+ operationSchema('browser_wait_for', objectSchema({ expect: conditionSchema,
51
+ timeout_ms: integerSchema(limits.timeout), poll_ms: integerSchema(limits.poll) }, ['expect'])),
52
+ operationSchema('browser_act', objectSchema({ steps: { type: 'array', items: stepSchema,
53
+ minItems: limits.steps[0], maxItems: limits.steps[1] }, timeout_ms: integerSchema(limits.timeout), successor: snapshotSchema(true) }, ['steps'])),
54
+ operationSchema('navigate', objectSchema({ url: { ...textSchema(limits.url, 1), format: 'uri',
55
+ description: 'Actual intended public HTTPS merchant URL; never use navigation to retry uncertain payment work.' } }, ['url'])),
56
+ ] });
57
+ const check = value => { if (!value) throw new Error('invalid_browser_operation'); };
58
+ function shape(value, required, optional = []) {
59
+ check(value !== null && typeof value === 'object' && !Array.isArray(value) &&
60
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
61
+ required.every(field => Object.hasOwn(value, field)) &&
62
+ Object.keys(value).every(field => required.includes(field) || optional.includes(field)));
63
+ }
64
+ function text(value, max, nonempty = false) {
65
+ check(typeof value === 'string' && value.length <= max && (!nonempty || value.length > 0) &&
66
+ !/[\u0000-\u001f\u007f]/u.test(value) && !sensitive.test(value));
67
+ }
68
+ const optionalInteger = (value, min, max) => check(value === undefined || Number.isInteger(value) && value >= min && value <= max);
69
+ function snapshot(args, successor = false) {
70
+ shape(args, [], successor ? ['filter', 'depth'] : ['filter', 'ref', 'depth']);
71
+ check(args.filter === undefined || filters.includes(args.filter));
72
+ check(args.ref === undefined || args.ref === 'root'); optionalInteger(args.depth, ...limits.depth);
73
+ }
74
+ function checkoutUrl(value) {
75
+ text(value, limits.url, true); check(!/\s/u.test(value));
76
+ const url = new URL(value);
77
+ // A checkout fragment selects merchant payment state; retain the exact URL.
78
+ check(url.protocol === 'https:' && !url.username && !url.password && !url.port &&
79
+ value.match(/^https:\/\/([^/?#]+)/)?.[1] === url.hostname &&
80
+ /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(url.hostname) &&
81
+ !/(?:^|\.)(?:localhost|local|internal|lan|corp|home|home\.arpa)$/u.test(url.hostname));
82
+ }
83
+
84
+ /** Validate the restricted preparation operation and return new JSON-only arguments.
85
+ * Harmless Browser Loop click defaults are stripped before reaching the gateway.
86
+ * Unsupported fields fail locally, preserving the active browser's usability.
87
+ */
88
+ export function normalizeBrowserOperation(operation, args) {
89
+ try {
90
+ if (operation === 'navigate') { shape(args, ['url']); checkoutUrl(args.url); }
91
+ else if (operation === 'browser_text') shape(args, []);
92
+ else if (operation === 'browser_find') { shape(args, ['query']); text(args.query, limits.query, true); }
93
+ else if (operation === 'browser_snapshot') snapshot(args);
94
+ else if (operation === 'browser_wait_for') {
95
+ shape(args, ['expect'], ['timeout_ms', 'poll_ms']);
96
+ if (args.expect?.type === 'text') {
97
+ shape(args.expect, ['type', 'text'], ['exists']);
98
+ text(args.expect.text, limits.query, true);
99
+ } else {
100
+ shape(args.expect, ['type'], ['equals', 'contains', 'exists']);
101
+ check(['url', 'title'].includes(args.expect.type));
102
+ check(args.expect.equals !== undefined || args.expect.contains !== undefined);
103
+ for (const name of ['equals', 'contains']) if (args.expect[name] !== undefined) text(args.expect[name], name === 'equals' ? limits.url : limits.query, true);
104
+ }
105
+ check(args.expect.exists === undefined || typeof args.expect.exists === 'boolean');
106
+ optionalInteger(args.timeout_ms, ...limits.timeout); optionalInteger(args.poll_ms, ...limits.poll);
107
+ } else if (operation === 'browser_act') {
108
+ shape(args, ['steps'], ['timeout_ms', 'successor']);
109
+ check(Array.isArray(args.steps) && args.steps.length >= limits.steps[0] && args.steps.length <= limits.steps[1]);
110
+ optionalInteger(args.timeout_ms, ...limits.timeout);
111
+ if (args.successor !== undefined) snapshot(args.successor, true);
112
+ const steps = args.steps.map(step => {
113
+ if (step?.type === 'click') {
114
+ shape(step, ['type', 'ref'], ['button', 'num_clicks', 'modifiers']);
115
+ check(step.button === undefined || step.button === 'left');
116
+ check(step.num_clicks === undefined || step.num_clicks === 1);
117
+ check(step.modifiers === undefined || Array.isArray(step.modifiers) && step.modifiers.length === 0);
118
+ } else if (['hover', 'scroll_to', 'fill'].includes(step?.type)) {
119
+ shape(step, step.type === 'fill' ? ['type', 'ref', 'value'] : ['type', 'ref']);
120
+ } else if (['type', 'key'].includes(step?.type)) {
121
+ shape(step, ['type', 'text'], ['repeat']); text(step.text, limits.text);
122
+ optionalInteger(step.repeat, ...limits.repeat);
123
+ if (step.type === 'key') check(keys.includes(step.text));
124
+ return step;
125
+ } else {
126
+ shape(step, ['type'], ['ms']); check(step.type === 'wait'); optionalInteger(step.ms, ...limits.wait); return step;
127
+ }
128
+ check(typeof step.ref === 'string' && step.ref.length <= limits.ref && refPattern.test(step.ref));
129
+ if (step.type === 'fill') text(step.value, limits.text);
130
+ return step.type === 'click' ? { type: 'click', ref: step.ref } : step;
131
+ });
132
+ args = { ...args, steps };
133
+ } else check(false);
134
+ return JSON.parse(JSON.stringify(args));
135
+ } catch { throw new Error('invalid_browser_operation'); }
136
+ }
137
+
138
+ /** Parse an unknown framework input without requiring an unsafe TypeScript assertion. */
139
+ export function parseBrowserOperation(value) {
140
+ try {
141
+ shape(value, ['operation', 'arguments']);
142
+ return { operation: value.operation, arguments: normalizeBrowserOperation(value.operation, value.arguments) };
143
+ } catch { throw new Error('invalid_browser_operation'); }
144
+ }
@@ -0,0 +1,46 @@
1
+ import type { ManagedCheckoutContext } from './managed-mcp.js';
2
+ import type { RequestOptions } from './index.js';
3
+ import type { ApprovedExtensionRelease, ApprovedExtensionRuntime } from './approved-extension.js';
4
+ import type { BrowserConnectorHost, OwnedBrowserDocument, FencedBrowserDocument, BrowserReadiness } from './browser-connector.js';
5
+
6
+ /** Existing platform automation backend observes only the bound document and
7
+ * fixed extension health, never card values. It retains private CDP credentials. */
8
+ export interface BrowserUseDocumentObservation {
9
+ document_id: string;
10
+ checkout_url: string;
11
+ extension: Omit<ApprovedExtensionRuntime, 'artifact_sha256'>;
12
+ automation: BrowserReadiness['automation'];
13
+ }
14
+ export interface BrowserUsePlatformHost {
15
+ /** Resolve an EXISTING browser from authenticated durable ownership. SDK reads actual provider expiry. */
16
+ resolveOwnedBrowser(context: Readonly<ManagedCheckoutContext>, request: Readonly<{ attempt_key: string; checkout_url: string }>,
17
+ options: RequestOptions): Promise<Omit<OwnedBrowserDocument, 'expires_at'> & { expires_at?: string }>;
18
+ drainAndFence: BrowserConnectorHost['drainAndFence'];
19
+ bindCheckout: BrowserConnectorHost['bindCheckout'];
20
+ resolveCheckoutBrowser: BrowserConnectorHost['resolveCheckoutBrowser'];
21
+ /** Use existing server browser control. Wake only the pinned MV3 registration
22
+ * if suspended. Do not create/navigate a page or accept model-authored code. */
23
+ inspectDocument(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>,
24
+ runtime: Readonly<{ runtime_id: string }>, options: RequestOptions): Promise<BrowserUseDocumentObservation>;
25
+ /** Optional; required for purchase. One trusted pointer click on the unique bound selector in the exact fenced document,
26
+ * through your existing server browser control. No retry, no general DOM reads, never lift the fence. */
27
+ performFinalClick?: BrowserConnectorHost['performFinalClick'];
28
+ /** Independently verify extension/page cleanup and retain every payment uncertainty fence. */
29
+ confirmLocalCleanup(context: Readonly<ManagedCheckoutContext>, browser: Readonly<FencedBrowserDocument>,
30
+ request: Readonly<{ checkout_id: import('./index.js').CheckoutId }>, options: RequestOptions): Promise<{
31
+ credentials_cleared: true; fence_retained: true;
32
+ }>;
33
+ }
34
+ /** Packaged Browser Use Cloud v4 read-only provider adapter. API key stays in
35
+ * the authenticated backend. No create, extension upload, navigation or stop is
36
+ * performed; Accelerate's executor/reaper owns payment retirement. */
37
+ export function createBrowserUseHost(options: {
38
+ apiKey: string;
39
+ merchantId: string;
40
+ extensionId: string;
41
+ extensionRelease: ApprovedExtensionRelease;
42
+ host: BrowserUsePlatformHost;
43
+ fetchImpl?: typeof fetch;
44
+ /** Test clock only. */
45
+ now?: () => number;
46
+ }): Readonly<BrowserConnectorHost>;
@@ -0,0 +1,149 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
3
+ import { normalizeBrowserOperation } from './browser-operations.js';
4
+
5
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
6
+ const RUNTIME = /^[a-p]{32}$/u;
7
+ const BASE = 'https://api.browser-use.com/api/v4';
8
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
9
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
10
+ const check = (ok, code = 'integration_unavailable') => { if (!ok) throw new AccelerateError(code); };
11
+ function exact(value, required, optional = []) {
12
+ check(object(value) && required.every(key => Object.hasOwn(value, key)) &&
13
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key)));
14
+ }
15
+ function bounded(work, signal) {
16
+ return new Promise((resolve, reject) => {
17
+ const abort = () => { signal.removeEventListener('abort', abort); reject(new AccelerateError('integration_unavailable')); };
18
+ signal.addEventListener('abort', abort, { once: true });
19
+ if (signal.aborted) { abort(); return; }
20
+ Promise.resolve(work).then(value => { signal.removeEventListener('abort', abort); resolve(value); }, abort);
21
+ });
22
+ }
23
+ async function json(response, signal) {
24
+ const reader = response.body.getReader(), chunks = []; let length = 0;
25
+ const cancel = () => { void reader.cancel().catch(() => {}); };
26
+ signal.addEventListener('abort', cancel, { once: true });
27
+ try {
28
+ for (;;) {
29
+ check(!signal.aborted);
30
+ const { value, done } = await bounded(reader.read(), signal);
31
+ check(!signal.aborted);
32
+ if (done) break;
33
+ length += value.byteLength; check(length <= 262144); chunks.push(value);
34
+ }
35
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
36
+ } finally {
37
+ signal.removeEventListener('abort', cancel); cancel();
38
+ try { reader.releaseLock(); } catch {}
39
+ }
40
+ }
41
+ const freeze = value => {
42
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(freeze); Object.freeze(value); }
43
+ return value;
44
+ };
45
+
46
+ export function createBrowserUseHost(configuration = {}) {
47
+ exact(configuration, ['apiKey', 'merchantId', 'extensionId', 'extensionRelease', 'host'], ['fetchImpl', 'now']);
48
+ const { apiKey, merchantId, extensionId, host, fetchImpl = fetch, now = Date.now } = configuration;
49
+ check(typeof apiKey === 'string' && apiKey.length >= 16 && apiKey.length <= 500 && !/[\u0000-\u0020\u007f]/u.test(apiKey) &&
50
+ UUID.test(merchantId) && UUID.test(extensionId) && typeof fetchImpl === 'function' && typeof now === 'function' && host &&
51
+ ['resolveOwnedBrowser', 'drainAndFence', 'bindCheckout', 'resolveCheckoutBrowser', 'inspectDocument', 'confirmLocalCleanup']
52
+ .every(key => typeof host[key] === 'function'), 'configuration_invalid');
53
+ let release;
54
+ try { release = validateApprovedExtensionRelease(configuration.extensionRelease); }
55
+ catch { throw new AccelerateError('configuration_invalid'); }
56
+ check(release.qualifications.some(value => value.provider === 'browser_use'), 'configuration_invalid');
57
+ const callHost = async (method, ...args) => {
58
+ try { return await host[method](...args); }
59
+ catch { throw new AccelerateError('integration_unavailable'); }
60
+ };
61
+ const owned = (context, document) => {
62
+ exact(document, ['owner', 'browser', 'document_id', 'checkout_url'], ['expires_at', 'fence_id']);
63
+ exact(document.owner, ['customer_id', 'workspace_id', 'conversation_id', 'phone_number']);
64
+ check(['customer_id', 'workspace_id', 'conversation_id', 'phone_number'].every(key => document.owner[key] === context[key]), 'permission_denied');
65
+ exact(document.browser, ['provider', 'browser_id', 'custody_mode']);
66
+ check(document.browser.provider === 'browser_use' && document.browser.custody_mode === 'customer_backend_trusted' && UUID.test(document.browser.browser_id) &&
67
+ typeof document.document_id === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/u.test(document.document_id), 'permission_denied');
68
+ try { normalizeBrowserOperation('navigate', { url: document.checkout_url }); }
69
+ catch { throw new AccelerateError('permission_denied'); }
70
+ return { owner: { ...document.owner }, browser: { ...document.browser }, document_id: document.document_id, checkout_url: document.checkout_url };
71
+ };
72
+ const resource = async (context, document, requiredStatus, options = {}) => {
73
+ const binding = owned(context, document).browser;
74
+ const timeout = AbortSignal.timeout(10000), signal = AbortSignal.any([timeout, ...(options.signal ? [options.signal] : [])]);
75
+ const endpoint = `${BASE}/browsers/${binding.browser_id}`;
76
+ let response;
77
+ try {
78
+ check(!signal.aborted);
79
+ const pending = Promise.resolve(fetchImpl(endpoint, { method: 'GET', headers: {
80
+ 'X-Browser-Use-API-Key': apiKey, Accept: 'application/json',
81
+ }, cache: 'no-store', redirect: 'error', signal }));
82
+ void pending.then(value => { if (signal.aborted) void value.body?.cancel().catch(() => {}); }, () => {});
83
+ response = await bounded(pending, signal);
84
+ check(response.ok && !response.redirected && (!response.url || response.url === endpoint) &&
85
+ /^application\/json(?:\s*;|$)/iu.test(response.headers.get('content-type') ?? '') && response.body);
86
+ const length = response.headers.get('content-length');
87
+ check(length === null || /^\d+$/u.test(length) && Number(length) <= 262144);
88
+ const value = await json(response, signal);
89
+ const metadata = value?.metadata;
90
+ check(object(value) && value.id === binding.browser_id && value.status === requiredStatus &&
91
+ value.recordingAvailable === false && (value.recordingUrl == null || value.recordingUrl === '') &&
92
+ object(metadata) && metadata.accelerate_merchant_id === merchantId &&
93
+ metadata.accelerate_owner_user_id === context.customer_id && metadata.accelerate_workspace_id === context.workspace_id &&
94
+ metadata.accelerate_custody_mode === 'customer_backend_trusted' && metadata.accelerate_agent_extension_id === extensionId &&
95
+ metadata.accelerate_extension_sha256 === release.artifact.sha256 && metadata.accelerate_extension_version === release.extension_version &&
96
+ typeof value.timeoutAt === 'string' && Number.isFinite(Date.parse(value.timeoutAt)) &&
97
+ (requiredStatus === 'stopped' || Date.parse(value.timeoutAt) > now()) &&
98
+ Array.isArray(value.extensions) && value.extensions.length === 1 && value.extensions[0]?.id === extensionId &&
99
+ value.extensions[0]?.version === release.extension_version && RUNTIME.test(value.extensions[0]?.runtimeId ?? ''));
100
+ // Deliberately project away provider CDP/viewer URLs and unknown response fields.
101
+ const read = freeze({ expires_at: value.timeoutAt, runtime_id: value.extensions[0].runtimeId });
102
+ if (!options.withConnection) return read;
103
+ check(typeof value.cdpUrl === 'string' && value.cdpUrl.length <= 2048 && typeof value.startedAt === 'string');
104
+ return freeze({ ...read, connection: freeze({ id: value.id, status: value.status, cdpUrl: value.cdpUrl, startedAt: value.startedAt,
105
+ timeoutAt: value.timeoutAt, recordingAvailable: false, metadata, extensions: value.extensions }) });
106
+ } catch {
107
+ void response?.body?.cancel().catch(() => {});
108
+ throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'integration_unavailable');
109
+ }
110
+ };
111
+ return Object.freeze({
112
+ async resolveOwnedBrowser(context, request, options) {
113
+ const document = await callHost('resolveOwnedBrowser', context, request, options);
114
+ const observed = await resource(context, document, 'active', options);
115
+ return { ...owned(context, document), expires_at: observed.expires_at };
116
+ },
117
+ /** This browser's connection for Accelerate, read here so the provider key stays with the host. */
118
+ async clientBrowserResource(context, document, options) {
119
+ return (await resource(context, document, 'active', { ...options, withConnection: true })).connection;
120
+ },
121
+ async inspectBrowser(context, document, options) {
122
+ const observed = await resource(context, document, 'active', options);
123
+ const current = await callHost('inspectDocument', context, document, freeze({ runtime_id: observed.runtime_id }), options);
124
+ exact(current, ['document_id', 'checkout_url', 'extension', 'automation']);
125
+ exact(current.extension, ['extension_version', 'success', 'initialized', 'wasmInitialized', 'autosaveDisabled',
126
+ 'boundedRetentionSupported', 'maximumRetentionMs'], ['bridge_protocol']);
127
+ const extension = { ...current.extension, artifact_sha256: release.artifact.sha256 };
128
+ try { verifyApprovedExtensionRuntime(extension, release, { provider: 'browser_use' }); }
129
+ catch { throw new AccelerateError('integration_unavailable'); }
130
+ return { ...owned(context, document), document_id: current.document_id, checkout_url: current.checkout_url, expires_at: observed.expires_at,
131
+ recording_disabled: true, extension,
132
+ automation: current.automation };
133
+ },
134
+ // Optional: only a platform host that can click may enable purchases. Readiness was read by prepareFinalClick before the arm.
135
+ ...(typeof host.performFinalClick === 'function' ? { performFinalClick: (...args) => callHost('performFinalClick', ...args) } : {}),
136
+ drainAndFence: (...args) => callHost('drainAndFence', ...args),
137
+ bindCheckout: (...args) => callHost('bindCheckout', ...args),
138
+ resolveCheckoutBrowser: (...args) => callHost('resolveCheckoutBrowser', ...args),
139
+ async confirmCleanup(context, document, request, options) {
140
+ // A service closure or stop response is insufficient: GET the exact
141
+ // original provider resource before accepting the local cleanup proof.
142
+ await resource(context, document, 'stopped', options);
143
+ const receipt = await callHost('confirmLocalCleanup', context, document, request, options);
144
+ exact(receipt, ['credentials_cleared', 'fence_retained']);
145
+ check(receipt.credentials_cleared === true && receipt.fence_retained === true);
146
+ return { provider_browser_stopped: true, credentials_cleared: true, fence_retained: true };
147
+ },
148
+ });
149
+ }