@appliqation/agent-core 0.1.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/appq/index.d.ts +3 -0
  4. package/dist/appq/index.d.ts.map +1 -0
  5. package/dist/appq/index.js +3 -0
  6. package/dist/appq/index.js.map +1 -0
  7. package/dist/appq/mcpClient.d.ts +52 -0
  8. package/dist/appq/mcpClient.d.ts.map +1 -0
  9. package/dist/appq/mcpClient.js +79 -0
  10. package/dist/appq/mcpClient.js.map +1 -0
  11. package/dist/appq/scenarioResolvers.d.ts +60 -0
  12. package/dist/appq/scenarioResolvers.d.ts.map +1 -0
  13. package/dist/appq/scenarioResolvers.js +114 -0
  14. package/dist/appq/scenarioResolvers.js.map +1 -0
  15. package/dist/audit/index.d.ts +2 -0
  16. package/dist/audit/index.d.ts.map +1 -0
  17. package/dist/audit/index.js +2 -0
  18. package/dist/audit/index.js.map +1 -0
  19. package/dist/audit/sink.d.ts +69 -0
  20. package/dist/audit/sink.d.ts.map +1 -0
  21. package/dist/audit/sink.js +93 -0
  22. package/dist/audit/sink.js.map +1 -0
  23. package/dist/config/helpers.d.ts +3 -0
  24. package/dist/config/helpers.d.ts.map +1 -0
  25. package/dist/config/helpers.js +16 -0
  26. package/dist/config/helpers.js.map +1 -0
  27. package/dist/config/index.d.ts +2 -0
  28. package/dist/config/index.d.ts.map +1 -0
  29. package/dist/config/index.js +2 -0
  30. package/dist/config/index.js.map +1 -0
  31. package/dist/engine/budget.d.ts +18 -0
  32. package/dist/engine/budget.d.ts.map +1 -0
  33. package/dist/engine/budget.js +31 -0
  34. package/dist/engine/budget.js.map +1 -0
  35. package/dist/engine/index.d.ts +4 -0
  36. package/dist/engine/index.d.ts.map +1 -0
  37. package/dist/engine/index.js +4 -0
  38. package/dist/engine/index.js.map +1 -0
  39. package/dist/engine/loop.d.ts +20 -0
  40. package/dist/engine/loop.d.ts.map +1 -0
  41. package/dist/engine/loop.js +60 -0
  42. package/dist/engine/loop.js.map +1 -0
  43. package/dist/engine/workflowRunner.d.ts +27 -0
  44. package/dist/engine/workflowRunner.d.ts.map +1 -0
  45. package/dist/engine/workflowRunner.js +31 -0
  46. package/dist/engine/workflowRunner.js.map +1 -0
  47. package/dist/evidence/capture.d.ts +53 -0
  48. package/dist/evidence/capture.d.ts.map +1 -0
  49. package/dist/evidence/capture.js +80 -0
  50. package/dist/evidence/capture.js.map +1 -0
  51. package/dist/evidence/index.d.ts +2 -0
  52. package/dist/evidence/index.d.ts.map +1 -0
  53. package/dist/evidence/index.js +2 -0
  54. package/dist/evidence/index.js.map +1 -0
  55. package/dist/index.d.ts +9 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +9 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/providers/anthropic.d.ts +4 -0
  60. package/dist/providers/anthropic.d.ts.map +1 -0
  61. package/dist/providers/anthropic.js +113 -0
  62. package/dist/providers/anthropic.js.map +1 -0
  63. package/dist/providers/index.d.ts +3 -0
  64. package/dist/providers/index.d.ts.map +1 -0
  65. package/dist/providers/index.js +3 -0
  66. package/dist/providers/index.js.map +1 -0
  67. package/dist/providers/openai.d.ts +4 -0
  68. package/dist/providers/openai.d.ts.map +1 -0
  69. package/dist/providers/openai.js +87 -0
  70. package/dist/providers/openai.js.map +1 -0
  71. package/dist/tools/apiTools.d.ts +21 -0
  72. package/dist/tools/apiTools.d.ts.map +1 -0
  73. package/dist/tools/apiTools.js +82 -0
  74. package/dist/tools/apiTools.js.map +1 -0
  75. package/dist/tools/authState.d.ts +21 -0
  76. package/dist/tools/authState.d.ts.map +1 -0
  77. package/dist/tools/authState.js +29 -0
  78. package/dist/tools/authState.js.map +1 -0
  79. package/dist/tools/browserTools.d.ts +35 -0
  80. package/dist/tools/browserTools.d.ts.map +1 -0
  81. package/dist/tools/browserTools.js +313 -0
  82. package/dist/tools/browserTools.js.map +1 -0
  83. package/dist/tools/destructiveActionGate.d.ts +10 -0
  84. package/dist/tools/destructiveActionGate.d.ts.map +1 -0
  85. package/dist/tools/destructiveActionGate.js +40 -0
  86. package/dist/tools/destructiveActionGate.js.map +1 -0
  87. package/dist/tools/gatedDispatcher.d.ts +15 -0
  88. package/dist/tools/gatedDispatcher.d.ts.map +1 -0
  89. package/dist/tools/gatedDispatcher.js +39 -0
  90. package/dist/tools/gatedDispatcher.js.map +1 -0
  91. package/dist/tools/index.d.ts +8 -0
  92. package/dist/tools/index.d.ts.map +1 -0
  93. package/dist/tools/index.js +8 -0
  94. package/dist/tools/index.js.map +1 -0
  95. package/dist/tools/projectContext.d.ts +4 -0
  96. package/dist/tools/projectContext.d.ts.map +1 -0
  97. package/dist/tools/projectContext.js +37 -0
  98. package/dist/tools/projectContext.js.map +1 -0
  99. package/dist/tools/roleInference.d.ts +65 -0
  100. package/dist/tools/roleInference.d.ts.map +1 -0
  101. package/dist/tools/roleInference.js +128 -0
  102. package/dist/tools/roleInference.js.map +1 -0
  103. package/dist/types.d.ts +78 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/package.json +74 -0
@@ -0,0 +1,87 @@
1
+ import OpenAI from 'openai';
2
+ export const DEFAULT_OPENAI_MODEL = 'gpt-5';
3
+ function toResponsesInput(messages) {
4
+ const out = [];
5
+ for (const m of messages) {
6
+ if (m.role === 'user') {
7
+ out.push({ role: 'user', content: m.content });
8
+ }
9
+ else if (m.role === 'assistant') {
10
+ if (m.content)
11
+ out.push({ role: 'assistant', content: m.content });
12
+ for (const call of m.toolCalls ?? []) {
13
+ out.push({
14
+ type: 'function_call',
15
+ call_id: call.id,
16
+ name: call.name,
17
+ arguments: JSON.stringify(call.arguments),
18
+ });
19
+ }
20
+ }
21
+ else if (m.role === 'tool') {
22
+ out.push({ type: 'function_call_output', call_id: m.toolCallId ?? '', output: m.content });
23
+ // The Responses API's function_call_output is text-only — there's no
24
+ // way to attach an image to a tool result the way Anthropic's
25
+ // tool_result blocks allow. The documented workaround is a synthetic
26
+ // user message with input_image content immediately after the output
27
+ // — without this, an "image" is just a base64 blob sitting unseen in
28
+ // a JSON string, never actually shown to the model.
29
+ if (m.images?.length) {
30
+ out.push({
31
+ role: 'user',
32
+ content: m.images.map((img) => ({
33
+ type: 'input_image',
34
+ detail: 'auto',
35
+ image_url: `data:${img.mimeType};base64,${img.data}`,
36
+ })),
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ export function createOpenAiAdapter(apiKey, model = DEFAULT_OPENAI_MODEL, maxOutputTokens = 4096) {
44
+ const client = new OpenAI({ apiKey });
45
+ return {
46
+ async complete({ system, messages, tools, signal }) {
47
+ const response = await client.responses.create({
48
+ model,
49
+ max_output_tokens: maxOutputTokens,
50
+ instructions: system,
51
+ input: toResponsesInput(messages),
52
+ tools: tools.map((t) => ({
53
+ type: 'function',
54
+ name: t.name,
55
+ description: t.description,
56
+ parameters: t.inputSchema,
57
+ strict: false,
58
+ })),
59
+ }, { signal });
60
+ let text = '';
61
+ const toolCalls = [];
62
+ for (const item of response.output) {
63
+ if (item.type === 'message') {
64
+ for (const part of item.content) {
65
+ if (part.type === 'output_text')
66
+ text += part.text;
67
+ }
68
+ }
69
+ else if (item.type === 'function_call') {
70
+ toolCalls.push({
71
+ id: item.call_id,
72
+ name: item.name,
73
+ arguments: JSON.parse(item.arguments || '{}'),
74
+ });
75
+ }
76
+ }
77
+ return {
78
+ text,
79
+ toolCalls,
80
+ usage: response.usage
81
+ ? { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }
82
+ : undefined,
83
+ };
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { APIRequestContext } from 'playwright';
2
+ import type { LlmToolDef, ToolResult } from '../types.js';
3
+ export declare const API_TOOL_DEFS: LlmToolDef[];
4
+ export interface ApiRequestRecord {
5
+ method: string;
6
+ url: string;
7
+ status: number | null;
8
+ ok: boolean;
9
+ dryRunSuppressed: boolean;
10
+ timestamp: number;
11
+ }
12
+ /** Wraps a live Playwright APIRequestContext as an http_request tool dispatcher. */
13
+ export declare class ApiRequestTools {
14
+ private readonly context;
15
+ private readonly dryRun;
16
+ private readonly requestHistory;
17
+ constructor(context: APIRequestContext, dryRun?: boolean);
18
+ getRequestHistory(): ApiRequestRecord[];
19
+ dispatch(name: string, args: Record<string, unknown>): Promise<ToolResult>;
20
+ }
21
+ //# sourceMappingURL=apiTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTools.d.ts","sourceRoot":"","sources":["../../src/tools/apiTools.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI1D,eAAO,MAAM,aAAa,EAAE,UAAU,EAkBrC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,oFAAoF;AACpF,qBAAa,eAAe;IAIxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0B;gBAGtC,OAAO,EAAE,iBAAiB,EAC1B,MAAM,GAAE,OAAe;IAG1C,iBAAiB,IAAI,gBAAgB,EAAE;IAIjC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;CAuCjF"}
@@ -0,0 +1,82 @@
1
+ // The http_request tool palette — the API-testing counterpart to
2
+ // browserTools.ts's browser_* tools. Structurally parallel on purpose: one
3
+ // tool, backed by a real Playwright APIRequestContext (already a dependency
4
+ // via the `playwright` package — no new one added for this), returning the
5
+ // real status/headers/body as the tool result. Same "never fabricate, only
6
+ // report what actually happened" discipline as every other tool in this
7
+ // family.
8
+ //
9
+ // Write-verb gating (POST/PUT/PATCH/DELETE) is dry-run-aware by
10
+ // construction, not bolted on after — this is the one place that knows a
11
+ // request is about to have a real side effect, the same reasoning behind
12
+ // browserTools.ts's destructive-action gate on browser_click.
13
+ const WRITE_VERBS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
14
+ export const API_TOOL_DEFS = [
15
+ {
16
+ name: 'http_request',
17
+ description: 'Send a real HTTP request and return the real response (status, headers, body, duration). Use this for ' +
18
+ 'every step of an API test case — no browser is offered for these. Write-verb requests ' +
19
+ '(POST/PUT/PATCH/DELETE) are suppressed and logged instead of sent when running in dry-run mode.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ method: { type: 'string', description: 'HTTP method, e.g. GET, POST, PUT, PATCH, DELETE.' },
24
+ url: { type: 'string', description: 'Absolute URL, or a path relative to the configured base URL.' },
25
+ headers: { type: 'object', description: 'Optional extra request headers.' },
26
+ body: { description: 'Optional request body — a JSON-serializable value, sent as the JSON request body.' },
27
+ },
28
+ required: ['method', 'url'],
29
+ },
30
+ },
31
+ ];
32
+ /** Wraps a live Playwright APIRequestContext as an http_request tool dispatcher. */
33
+ export class ApiRequestTools {
34
+ context;
35
+ dryRun;
36
+ requestHistory = [];
37
+ constructor(context, dryRun = false) {
38
+ this.context = context;
39
+ this.dryRun = dryRun;
40
+ }
41
+ getRequestHistory() {
42
+ return [...this.requestHistory];
43
+ }
44
+ async dispatch(name, args) {
45
+ if (name !== 'http_request')
46
+ return { ok: false, text: `Unknown API tool "${name}"` };
47
+ const method = String(args.method ?? 'GET').toUpperCase();
48
+ const url = String(args.url ?? '');
49
+ const headers = args.headers ?? undefined;
50
+ const body = args.body;
51
+ if (this.dryRun && WRITE_VERBS.has(method)) {
52
+ this.requestHistory.push({ method, url, status: null, ok: true, dryRunSuppressed: true, timestamp: Date.now() });
53
+ console.error(`[dry-run] would send ${method} ${url} with body: ${JSON.stringify(body)}`);
54
+ return {
55
+ ok: true,
56
+ text: `[dry-run] ${method} ${url} suppressed — no request was sent. Args were logged for review.`,
57
+ };
58
+ }
59
+ const startedAt = Date.now();
60
+ try {
61
+ const response = await this.context.fetch(url, {
62
+ method,
63
+ headers,
64
+ data: body !== undefined ? JSON.stringify(body) : undefined,
65
+ });
66
+ const durationMs = Date.now() - startedAt;
67
+ const status = response.status();
68
+ const responseHeaders = response.headers();
69
+ const responseBodyText = await response.text().catch(() => '');
70
+ const ok = response.ok();
71
+ this.requestHistory.push({ method, url, status, ok, dryRunSuppressed: false, timestamp: Date.now() });
72
+ const result = { method, url, status, headers: responseHeaders, body: responseBodyText, duration_ms: durationMs };
73
+ const text = JSON.stringify(result);
74
+ return { ok: true, text: text.length > 20_000 ? `${text.slice(0, 20_000)}... (truncated)` : text };
75
+ }
76
+ catch (err) {
77
+ this.requestHistory.push({ method, url, status: null, ok: false, dryRunSuppressed: false, timestamp: Date.now() });
78
+ return { ok: false, text: `Request failed: ${err.message}` };
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=apiTools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTools.js","sourceRoot":"","sources":["../../src/tools/apiTools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { BrowserContextOptions } from 'playwright';
2
+ export declare function resolveStorageState(projectId: number, role: string): NonNullable<BrowserContextOptions['storageState']>;
3
+ /**
4
+ * BYO-credential resolution for API testing — same posture as
5
+ * resolveStorageState()'s UI auth: read from the consuming agent's own
6
+ * .env, never performed or handled beyond that single read, never exposed
7
+ * to the model as a value (the caller injects the returned header directly
8
+ * into an APIRequestContext's extraHTTPHeaders, once, at construction).
9
+ *
10
+ * APPQ_PROJECT_<id>_<ROLE>_API_KEY holds the credential value.
11
+ * APPQ_PROJECT_<id>_<ROLE>_API_HEADER_NAME optionally overrides the header
12
+ * name (defaults to "Authorization", with the value sent as "Bearer
13
+ * <key>" — set API_HEADER_NAME to send the raw key as-is under a different
14
+ * header, e.g. "X-Api-Key").
15
+ */
16
+ export interface ApiAuthHeader {
17
+ name: string;
18
+ value: string;
19
+ }
20
+ export declare function resolveApiAuth(projectId: number, role: string): ApiAuthHeader | undefined;
21
+ //# sourceMappingURL=authState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authState.d.ts","sourceRoot":"","sources":["../../src/tools/authState.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC,qBAAqB,CAAC,cAAc,CAAC,CAAC,CAUvH;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAQzF"}
@@ -0,0 +1,29 @@
1
+ // Reads the Playwright storageState a project/role's authenticated session
2
+ // lives at, computed by @appliqation/automation-sdk's setupAuth() — the same
3
+ // pure path function the customer's own Playwright config, the appq-auth-setup
4
+ // CLI, and Appliqation's hosted executor all already agree on. A consuming
5
+ // agent never performs login itself and never handles credentials: only the
6
+ // resulting session (cookies/localStorage) is ever read, directly from disk,
7
+ // never through an LLM tool call.
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { setupAuth } from '@appliqation/automation-sdk/utils';
10
+ export function resolveStorageState(projectId, role) {
11
+ const path = setupAuth({ project_id: projectId, role });
12
+ if (!existsSync(path)) {
13
+ throw new Error(`No authenticated session found for project ${projectId}, role "${role}" (expected at ${path}). ` +
14
+ `Run \`npx appq-auth-setup --project-id ${projectId} --role ${role}\` first ` +
15
+ `(needs APPQ_PROJECT_${projectId}_${role.toUpperCase()}_USERNAME/_PASSWORD and APPLIQATION_SUT_BASE_URL set).`);
16
+ }
17
+ return JSON.parse(readFileSync(path, 'utf-8'));
18
+ }
19
+ export function resolveApiAuth(projectId, role) {
20
+ const upperRole = role.toUpperCase();
21
+ const apiKey = process.env[`APPQ_PROJECT_${projectId}_${upperRole}_API_KEY`];
22
+ if (!apiKey)
23
+ return undefined;
24
+ const headerName = process.env[`APPQ_PROJECT_${projectId}_${upperRole}_API_HEADER_NAME`];
25
+ if (headerName)
26
+ return { name: headerName, value: apiKey };
27
+ return { name: 'Authorization', value: `Bearer ${apiKey}` };
28
+ }
29
+ //# sourceMappingURL=authState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authState.js","sourceRoot":"","sources":["../../src/tools/authState.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,4EAA4E;AAC5E,6EAA6E;AAC7E,kCAAkC;AAElC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAG9D,MAAM,UAAU,mBAAmB,CAAC,SAAiB,EAAE,IAAY;IACjE,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,8CAA8C,SAAS,WAAW,IAAI,kBAAkB,IAAI,KAAK;YAC/F,0CAA0C,SAAS,WAAW,IAAI,WAAW;YAC7E,uBAAuB,SAAS,IAAI,IAAI,CAAC,WAAW,EAAE,wDAAwD,CACjH,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AACjD,CAAC;AAoBD,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,IAAY;IAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,SAAS,IAAI,SAAS,UAAU,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,SAAS,IAAI,SAAS,kBAAkB,CAAC,CAAC;IACzF,IAAI,UAAU;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC3D,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,MAAM,EAAE,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { Page } from 'playwright';
2
+ import type { LlmToolDef, ToolResult } from '../types.js';
3
+ import { EvidenceCapture } from '../evidence/capture.js';
4
+ import { type ClickTarget } from './destructiveActionGate.js';
5
+ export type ClickGate = (target: ClickTarget) => ToolResult | null;
6
+ /** Stages a captured screenshot somewhere (an appq upload, a local file, ...) and returns a ref for it. */
7
+ export type ScreenshotSink = (png: Buffer, label: string) => Promise<{
8
+ ok: true;
9
+ ref: string;
10
+ } | {
11
+ ok: false;
12
+ note: string;
13
+ }>;
14
+ export interface BrowserToolsHooks {
15
+ onBeforeClick?: ClickGate;
16
+ screenshotSink?: ScreenshotSink;
17
+ }
18
+ export declare const BROWSER_TOOL_DEFS: LlmToolDef[];
19
+ /** Wraps a live Playwright Page as a browser_* tool dispatcher, tracking evidence as it goes. */
20
+ export declare class PlaywrightBrowserTools {
21
+ private readonly ringBufferCap?;
22
+ private readonly hooks;
23
+ private knownRefs;
24
+ private pages;
25
+ private selected;
26
+ private evidenceByPage;
27
+ constructor(page: Page, ringBufferCap?: number | undefined, hooks?: BrowserToolsHooks);
28
+ private currentPage;
29
+ /** The currently selected tab's evidence tracker — same call shape as the old single-page `.evidence` field. */
30
+ get evidence(): EvidenceCapture;
31
+ private locatorFor;
32
+ dispatch(name: string, args: Record<string, unknown>): Promise<ToolResult>;
33
+ private dispatchTabs;
34
+ }
35
+ //# sourceMappingURL=browserTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserTools.d.ts","sourceRoot":"","sources":["../../src/tools/browserTools.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE7E,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,WAAW,KAAK,UAAU,GAAG,IAAI,CAAC;AAEnE,2GAA2G;AAC3G,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,eAAO,MAAM,iBAAiB,EAAE,UAAU,EAmHzC,CAAC;AAEF,iGAAiG;AACjG,qBAAa,sBAAsB;IAqB/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAhBxB,OAAO,CAAC,SAAS,CAAqB;IAStC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,cAAc,CAA6B;gBAGjD,IAAI,EAAE,IAAI,EACO,aAAa,CAAC,EAAE,MAAM,YAAA,EACtB,KAAK,GAAE,iBAAsB;IAMhD,OAAO,CAAC,WAAW;IAInB,gHAAgH;IAChH,IAAI,QAAQ,IAAI,eAAe,CAE9B;IAED,OAAO,CAAC,UAAU;IAKZ,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;YAqGlE,YAAY;CA2C3B"}
@@ -0,0 +1,313 @@
1
+ // Playwright-backed implementations of the browser_* tool palette. Tool names
2
+ // mirror Playwright MCP's own naming convention so any workflow prose written
3
+ // against that vocabulary transfers unchanged to this engine.
4
+ //
5
+ // Two behaviors are injectable rather than hardcoded, so this class has zero
6
+ // appq-specific coupling: the destructive-action check before a click
7
+ // (defaults to the shared classifyClick()), and where a screenshot gets
8
+ // staged for attachment (no default — a consuming agent either wires its own
9
+ // appq upload, a local file sink, or neither; browser_take_screenshot still
10
+ // works either way, just without a ref to hand back when there's no sink).
11
+ import { EvidenceCapture } from '../evidence/capture.js';
12
+ import { classifyClick } from './destructiveActionGate.js';
13
+ export const BROWSER_TOOL_DEFS = [
14
+ {
15
+ name: 'browser_navigate',
16
+ description: 'Navigate the page to a URL.',
17
+ inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
18
+ },
19
+ {
20
+ name: 'browser_navigate_back',
21
+ description: 'Go back to the previous page.',
22
+ inputSchema: { type: 'object', properties: {} },
23
+ },
24
+ {
25
+ name: 'browser_snapshot',
26
+ description: 'Take an accessibility-tree snapshot of the current page. Returns a text tree with element refs ' +
27
+ '(e1, e2, ...) to use with browser_click/browser_type/etc.',
28
+ inputSchema: { type: 'object', properties: {} },
29
+ },
30
+ {
31
+ name: 'browser_click',
32
+ description: 'Click an element by its ref from the last browser_snapshot. `label` must describe the control\'s ' +
33
+ 'visible text/purpose (e.g. "Delete account", "Pay now") — it is checked against a destructive-action ' +
34
+ 'gate before the click happens.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: { ref: { type: 'string' }, label: { type: 'string' } },
38
+ required: ['ref', 'label'],
39
+ },
40
+ },
41
+ {
42
+ name: 'browser_type',
43
+ description: 'Type text into an element by its ref from the last browser_snapshot.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: { ref: { type: 'string' }, text: { type: 'string' }, submit: { type: 'boolean' } },
47
+ required: ['ref', 'text'],
48
+ },
49
+ },
50
+ {
51
+ name: 'browser_select_option',
52
+ description: 'Select an option in a <select> element by its ref.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: { ref: { type: 'string' }, value: { type: 'string' } },
56
+ required: ['ref', 'value'],
57
+ },
58
+ },
59
+ {
60
+ name: 'browser_press_key',
61
+ description: 'Press a keyboard key (e.g. "Enter", "Escape").',
62
+ inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] },
63
+ },
64
+ {
65
+ name: 'browser_take_screenshot',
66
+ description: 'Take a screenshot of the current viewport and stage it for attachment. Returns a screenshot_upload_id ' +
67
+ 'when a screenshot sink is configured — pass that straight through wherever the calling workflow expects ' +
68
+ 'it. You never need to handle the image bytes yourself.',
69
+ inputSchema: { type: 'object', properties: {} },
70
+ },
71
+ {
72
+ name: 'browser_console_messages',
73
+ description: 'Return console messages logged since the last check.',
74
+ inputSchema: { type: 'object', properties: {} },
75
+ },
76
+ {
77
+ name: 'browser_network_requests',
78
+ description: 'Return network requests observed since the last check.',
79
+ inputSchema: { type: 'object', properties: {} },
80
+ },
81
+ {
82
+ name: 'browser_wait_for',
83
+ description: 'Wait for a duration in milliseconds, or for text to appear on the page.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: { millis: { type: 'number' }, text: { type: 'string' } },
87
+ },
88
+ },
89
+ {
90
+ name: 'browser_resize',
91
+ description: 'Resize the browser viewport of the currently selected tab.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: { width: { type: 'number' }, height: { type: 'number' } },
95
+ required: ['width', 'height'],
96
+ },
97
+ },
98
+ {
99
+ name: 'browser_tabs',
100
+ description: 'List, open, close, or switch between browser tabs. `list` returns every open tab with its index and URL, ' +
101
+ 'marking which one is selected. `new` opens a tab (optionally navigating it to `url`) and selects it. ' +
102
+ '`close` closes a tab by `index` (defaults to the selected tab) — at least one tab must remain open. ' +
103
+ '`select` by `index` makes that tab the target of every other browser_* tool. Switching tabs invalidates ' +
104
+ 'refs from a prior browser_snapshot — call browser_snapshot again after selecting a different tab.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ action: { type: 'string', enum: ['list', 'new', 'close', 'select'] },
109
+ index: { type: 'number' },
110
+ url: { type: 'string' },
111
+ },
112
+ required: ['action'],
113
+ },
114
+ },
115
+ {
116
+ name: 'browser_evaluate',
117
+ description: 'Run JavaScript in the page context of the currently selected tab and return the result. Pass either a ' +
118
+ 'plain expression (e.g. "document.title") or a function (e.g. "() => document.title"). Unrestricted — use ' +
119
+ 'for read-only inspection (Service Worker state, Performance API, localStorage, etc.), not to work around ' +
120
+ 'the destructive-action gate on browser_click.',
121
+ inputSchema: { type: 'object', properties: { function: { type: 'string' } }, required: ['function'] },
122
+ },
123
+ ];
124
+ /** Wraps a live Playwright Page as a browser_* tool dispatcher, tracking evidence as it goes. */
125
+ export class PlaywrightBrowserTools {
126
+ ringBufferCap;
127
+ hooks;
128
+ // Refs come from page.ariaSnapshot({mode:'ai'}), which embeds [ref=eN]
129
+ // markers directly in its output — no manual tree-walking needed. A ref
130
+ // resolves back to a live element via the 'aria-ref=' selector engine.
131
+ // Refs are only valid against the page they were snapshotted from, so a
132
+ // tab switch clears them — the model must call browser_snapshot again.
133
+ knownRefs = new Set();
134
+ // Multi-tab support: every existing browser_* op targets whichever page is
135
+ // currently selected, via currentPage()/currentEvidence(). Consumers that
136
+ // never call browser_tabs see no behavior change — pages always has
137
+ // exactly one entry, selected always 0. Evidence is tracked per page (not
138
+ // just the original one) since console/network listeners are bound at
139
+ // Page construction — a new tab's console output would otherwise be
140
+ // silently invisible to browser_console_messages after switching to it.
141
+ pages;
142
+ selected = 0;
143
+ evidenceByPage;
144
+ constructor(page, ringBufferCap, hooks = {}) {
145
+ this.ringBufferCap = ringBufferCap;
146
+ this.hooks = hooks;
147
+ this.pages = [page];
148
+ this.evidenceByPage = new Map([[page, new EvidenceCapture(page, ringBufferCap)]]);
149
+ }
150
+ currentPage() {
151
+ return this.pages[this.selected];
152
+ }
153
+ /** The currently selected tab's evidence tracker — same call shape as the old single-page `.evidence` field. */
154
+ get evidence() {
155
+ return this.evidenceByPage.get(this.currentPage());
156
+ }
157
+ locatorFor(ref) {
158
+ if (!this.knownRefs.has(ref))
159
+ throw new Error(`Unknown ref "${ref}" — call browser_snapshot first`);
160
+ return this.currentPage().locator(`aria-ref=${ref}`);
161
+ }
162
+ async dispatch(name, args) {
163
+ switch (name) {
164
+ case 'browser_navigate': {
165
+ await this.currentPage().goto(String(args.url), { waitUntil: 'domcontentloaded' });
166
+ return { ok: true, text: `Navigated to ${args.url}` };
167
+ }
168
+ case 'browser_navigate_back': {
169
+ await this.currentPage().goBack({ waitUntil: 'domcontentloaded' });
170
+ return { ok: true, text: 'Navigated back' };
171
+ }
172
+ case 'browser_snapshot': {
173
+ const text = await this.currentPage().ariaSnapshot({ mode: 'ai' });
174
+ this.knownRefs = new Set([...text.matchAll(/\[ref=([^\]]+)\]/g)].map((m) => m[1]));
175
+ return { ok: true, text: text || '(empty page)' };
176
+ }
177
+ case 'browser_click': {
178
+ const ref = String(args.ref);
179
+ // label is required (not derived from the ref) so the destructive-action
180
+ // gate has something to check before the click ever dispatches.
181
+ const label = String(args.label ?? '');
182
+ const gate = this.hooks.onBeforeClick ?? classifyClick;
183
+ const blocked = gate({ label, tag: 'button' });
184
+ if (blocked)
185
+ return blocked;
186
+ await this.locatorFor(ref).click();
187
+ return { ok: true, text: `Clicked "${label || ref}"` };
188
+ }
189
+ case 'browser_type': {
190
+ const ref = String(args.ref);
191
+ await this.locatorFor(ref).fill(String(args.text));
192
+ if (args.submit)
193
+ await this.locatorFor(ref).press('Enter');
194
+ return { ok: true, text: `Typed into ${ref}` };
195
+ }
196
+ case 'browser_select_option': {
197
+ const ref = String(args.ref);
198
+ await this.locatorFor(ref).selectOption(String(args.value));
199
+ return { ok: true, text: `Selected "${args.value}" in ${ref}` };
200
+ }
201
+ case 'browser_press_key': {
202
+ await this.currentPage().keyboard.press(String(args.key));
203
+ return { ok: true, text: `Pressed ${args.key}` };
204
+ }
205
+ case 'browser_take_screenshot': {
206
+ const png = await this.currentPage().screenshot({ type: 'png' });
207
+ if (!this.hooks.screenshotSink) {
208
+ return { ok: true, text: `Captured screenshot (${png.length} bytes). No screenshot sink configured for this agent.`, data: png };
209
+ }
210
+ try {
211
+ const staged = await this.hooks.screenshotSink(png, 'autotest-step');
212
+ if (staged.ok) {
213
+ return { ok: true, text: `Captured screenshot (${png.length} bytes). screenshot_upload_id: ${staged.ref}`, data: png };
214
+ }
215
+ // Non-fatal — the step's other evidence (accessibility snapshot,
216
+ // console/network deltas) still stands on its own; one failed
217
+ // staging attempt shouldn't block the whole step.
218
+ return {
219
+ ok: true,
220
+ text: `Captured screenshot (${png.length} bytes), but staging it failed: ${staged.note}. ` +
221
+ 'No screenshot_upload_id available for this step — submit evidence without one.',
222
+ data: png,
223
+ };
224
+ }
225
+ catch (err) {
226
+ return {
227
+ ok: true,
228
+ text: `Captured screenshot (${png.length} bytes), but staging it failed: ${err.message}. ` +
229
+ 'No screenshot_upload_id available for this step — submit evidence without one.',
230
+ data: png,
231
+ };
232
+ }
233
+ }
234
+ case 'browser_console_messages': {
235
+ return { ok: true, text: JSON.stringify(this.evidence.getConsoleDeltas()) };
236
+ }
237
+ case 'browser_network_requests': {
238
+ return { ok: true, text: JSON.stringify(this.evidence.getNetworkDeltas()) };
239
+ }
240
+ case 'browser_wait_for': {
241
+ if (args.text) {
242
+ await this.currentPage().getByText(String(args.text)).waitFor({ timeout: 15000 });
243
+ return { ok: true, text: `Text "${args.text}" appeared` };
244
+ }
245
+ await this.currentPage().waitForTimeout(Number(args.millis ?? 1000));
246
+ return { ok: true, text: 'Waited' };
247
+ }
248
+ case 'browser_resize': {
249
+ const width = Number(args.width);
250
+ const height = Number(args.height);
251
+ await this.currentPage().setViewportSize({ width, height });
252
+ return { ok: true, text: `Resized viewport to ${width}x${height}` };
253
+ }
254
+ case 'browser_tabs': {
255
+ return this.dispatchTabs(args);
256
+ }
257
+ case 'browser_evaluate': {
258
+ const result = await this.currentPage().evaluate(String(args.function));
259
+ return { ok: true, text: result === undefined ? 'undefined' : JSON.stringify(result) };
260
+ }
261
+ default:
262
+ return { ok: false, text: `Unknown browser tool "${name}"` };
263
+ }
264
+ }
265
+ async dispatchTabs(args) {
266
+ const action = String(args.action ?? '');
267
+ switch (action) {
268
+ case 'list': {
269
+ const lines = this.pages.map((p, i) => `${i}${i === this.selected ? ' (selected)' : ''}: ${p.url()}`);
270
+ return { ok: true, text: lines.join('\n') };
271
+ }
272
+ case 'new': {
273
+ const newPage = await this.currentPage().context().newPage();
274
+ if (args.url)
275
+ await newPage.goto(String(args.url), { waitUntil: 'domcontentloaded' });
276
+ this.pages.push(newPage);
277
+ this.evidenceByPage.set(newPage, new EvidenceCapture(newPage, this.ringBufferCap));
278
+ this.selected = this.pages.length - 1;
279
+ this.knownRefs = new Set();
280
+ return { ok: true, text: `Opened new tab at index ${this.selected}${args.url ? ` (${args.url})` : ''}` };
281
+ }
282
+ case 'close': {
283
+ const index = args.index !== undefined ? Number(args.index) : this.selected;
284
+ if (!Number.isInteger(index) || index < 0 || index >= this.pages.length) {
285
+ return { ok: false, text: `No tab at index ${args.index}` };
286
+ }
287
+ if (this.pages.length === 1)
288
+ return { ok: false, text: 'Cannot close the only remaining tab' };
289
+ const [closed] = this.pages.splice(index, 1);
290
+ this.evidenceByPage.delete(closed);
291
+ await closed.close();
292
+ if (this.selected >= this.pages.length)
293
+ this.selected = this.pages.length - 1;
294
+ else if (this.selected > index)
295
+ this.selected -= 1;
296
+ this.knownRefs = new Set();
297
+ return { ok: true, text: `Closed tab ${index}. Now on tab ${this.selected} (${this.currentPage().url()}).` };
298
+ }
299
+ case 'select': {
300
+ const index = Number(args.index);
301
+ if (!Number.isInteger(index) || index < 0 || index >= this.pages.length) {
302
+ return { ok: false, text: `No tab at index ${args.index}` };
303
+ }
304
+ this.selected = index;
305
+ this.knownRefs = new Set();
306
+ return { ok: true, text: `Selected tab ${index} (${this.currentPage().url()})` };
307
+ }
308
+ default:
309
+ return { ok: false, text: `Unknown browser_tabs action "${action}"` };
310
+ }
311
+ }
312
+ }
313
+ //# sourceMappingURL=browserTools.js.map