@reclaimprotocol/agent 0.1.33 → 0.1.34

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 (88) hide show
  1. package/lib/authoring/backend.d.ts +13 -0
  2. package/lib/authoring/index.d.ts +1 -1
  3. package/lib/authoring/tools.js +78 -42
  4. package/lib/config/store.d.ts +10 -0
  5. package/lib/mcp/api/operations.gen.d.ts +238 -4
  6. package/lib/mcp/api/operations.gen.js +6433 -328
  7. package/lib/mcp/api/tools.js +21 -8
  8. package/lib/mcp/instructions.js +11 -11
  9. package/lib/mcp/server.d.ts +1 -1
  10. package/lib/mcp/server.js +3 -0
  11. package/lib/mcp/success-response.js +5 -1
  12. package/lib/mcp/tools/agent/analyze.js +5 -4
  13. package/lib/mcp/tools/agent/attach.js +11 -7
  14. package/lib/mcp/tools/agent/build.js +3 -1
  15. package/lib/mcp/tools/agent/capture.d.ts +2 -2
  16. package/lib/mcp/tools/agent/capture.js +15 -11
  17. package/lib/mcp/tools/agent/eval.js +4 -3
  18. package/lib/mcp/tools/agent/find.js +9 -10
  19. package/lib/mcp/tools/agent/how-it-works.js +1 -0
  20. package/lib/mcp/tools/agent/live-view.js +10 -7
  21. package/lib/mcp/tools/agent/pod-proof.js +7 -11
  22. package/lib/mcp/tools/agent/propose.js +4 -3
  23. package/lib/mcp/tools/agent/publish.d.ts +2 -1
  24. package/lib/mcp/tools/agent/publish.js +12 -6
  25. package/lib/mcp/tools/agent/replay.js +10 -7
  26. package/lib/mcp/tools/agent/session.js +16 -8
  27. package/lib/mcp/tools/agent/shared.js +3 -2
  28. package/lib/mcp/tools/agent/start-integration.d.ts +15 -0
  29. package/lib/mcp/tools/agent/start-integration.js +61 -0
  30. package/lib/mcp/tools/authenticate/authenticate.js +9 -11
  31. package/lib/mcp/tools/authenticate/build.js +2 -0
  32. package/lib/mcp/tools/authenticate/builder-access.js +1 -0
  33. package/lib/mcp/tools/authenticate/eth-key.d.ts +8 -0
  34. package/lib/mcp/tools/authenticate/eth-key.js +27 -15
  35. package/lib/mcp/tools/authenticate/import.js +18 -31
  36. package/lib/mcp/tools/authenticate/issue-org-token.d.ts +8 -0
  37. package/lib/mcp/tools/authenticate/issue-org-token.js +60 -0
  38. package/lib/mcp/tools/authenticate/issue.js +9 -9
  39. package/lib/mcp/tools/authenticate/org-key-store.d.ts +10 -0
  40. package/lib/mcp/tools/authenticate/org-key-store.js +23 -0
  41. package/lib/mcp/tools/authenticate/resolve-owner-key.js +10 -12
  42. package/lib/mcp/tools/mode/mode.js +2 -0
  43. package/lib/mcp/tools/verification/result.js +10 -7
  44. package/lib/tools/registry.d.ts +12 -0
  45. package/lib/tools/registry.js +1 -0
  46. package/package.json +2 -2
  47. package/skill/SKILL.builder.md +77 -74
  48. package/skill/SKILL.common.md +2 -1
  49. package/skill/builder-mode.md +135 -138
  50. package/skill/start-integration.md +49 -0
  51. package/skill/user-script.md +64 -0
  52. package/skill/verification-client-integration.md +28 -22
  53. package/src/authoring/backend.ts +15 -0
  54. package/src/authoring/index.ts +2 -0
  55. package/src/authoring/tools.ts +86 -42
  56. package/src/config/store.ts +11 -0
  57. package/src/mcp/api/operations.gen.ts +6477 -364
  58. package/src/mcp/api/tools.ts +21 -8
  59. package/src/mcp/instructions.ts +11 -11
  60. package/src/mcp/server.ts +4 -0
  61. package/src/mcp/success-response.ts +5 -1
  62. package/src/mcp/tools/agent/analyze.ts +5 -4
  63. package/src/mcp/tools/agent/attach.ts +11 -7
  64. package/src/mcp/tools/agent/build.ts +5 -1
  65. package/src/mcp/tools/agent/capture.ts +20 -12
  66. package/src/mcp/tools/agent/eval.ts +4 -3
  67. package/src/mcp/tools/agent/find.ts +9 -10
  68. package/src/mcp/tools/agent/how-it-works.ts +1 -0
  69. package/src/mcp/tools/agent/live-view.ts +10 -7
  70. package/src/mcp/tools/agent/pod-proof.ts +7 -11
  71. package/src/mcp/tools/agent/propose.ts +4 -3
  72. package/src/mcp/tools/agent/publish.ts +12 -4
  73. package/src/mcp/tools/agent/replay.ts +10 -7
  74. package/src/mcp/tools/agent/session.ts +16 -8
  75. package/src/mcp/tools/agent/shared.ts +7 -2
  76. package/src/mcp/tools/agent/start-integration.ts +83 -0
  77. package/src/mcp/tools/authenticate/authenticate.ts +9 -11
  78. package/src/mcp/tools/authenticate/build.ts +2 -0
  79. package/src/mcp/tools/authenticate/builder-access.ts +1 -0
  80. package/src/mcp/tools/authenticate/eth-key.ts +48 -18
  81. package/src/mcp/tools/authenticate/import.ts +21 -36
  82. package/src/mcp/tools/authenticate/issue-org-token.ts +84 -0
  83. package/src/mcp/tools/authenticate/issue.ts +9 -9
  84. package/src/mcp/tools/authenticate/org-key-store.ts +25 -0
  85. package/src/mcp/tools/authenticate/resolve-owner-key.ts +10 -12
  86. package/src/mcp/tools/mode/mode.ts +2 -0
  87. package/src/mcp/tools/verification/result.ts +10 -7
  88. package/src/tools/registry.ts +14 -0
@@ -4,6 +4,7 @@ import assert from 'node:assert';
4
4
  import { orgSecret } from "../../consts.js";
5
5
  import { remapErrorAsResponse } from "../remap_errors.js";
6
6
  import { successResponse } from "../success-response.js";
7
+ import { savedOrgKey } from "../tools/authenticate/org-key-store.js";
7
8
  import { MCP_TOOLS } from "./operations.gen.js";
8
9
  /**
9
10
  * The generated `MCP_TOOLS` are keyed by OpenAPI `operationId` (PascalCase,
@@ -33,6 +34,11 @@ export function registerTools(server, client, orgClient = (orgToken) => new Recl
33
34
  const shape = {
34
35
  description: tool.description,
35
36
  inputSchema: tool.inputSchema,
37
+ // Generated entries gain `annotations` when the spec marks them; read
38
+ // it defensively so an older generated file still registers.
39
+ ...('annotations' in tool && tool.annotations
40
+ ? { annotations: tool.annotations }
41
+ : {}),
36
42
  };
37
43
  if (!isOperationId(opId)) {
38
44
  throw new Error(`Generated MCP operation is not in the API client: ${opId}`);
@@ -51,7 +57,7 @@ export function registerTools(server, client, orgClient = (orgToken) => new Recl
51
57
  throw new Error(`Invalid generated MCP arguments for ${opId}`);
52
58
  }
53
59
  const caller = orgTokenOnly
54
- ? orgClient(orgTokenFor(toSnakeCase(opId), orgToken))
60
+ ? orgClient(orgTokenFor(toSnakeCase(opId), orgToken, args))
55
61
  : client;
56
62
  const result = await caller.call(opId, args);
57
63
  return successResponse(result.data);
@@ -85,12 +91,19 @@ function isRecord(value) {
85
91
  return typeof value === 'object' && value !== null && !Array.isArray(value);
86
92
  }
87
93
  /** The org API key a call authenticates with: its `orgToken`, else the
88
- * server's RECLAIM_ORG_SECRET. The session token (the developer's sign-in)
89
- * can't stand in, since these routes accept only an org key. */
90
- function orgTokenFor(tool, orgToken) {
91
- const token = (typeof orgToken === 'string' && orgToken) || orgSecret();
92
- assert(token, unauthorized(`${tool} authenticates with the organization's API key. Pass `
93
- + 'orgToken: one from issue_org_token, or the app\'s '
94
- + 'RECLAIM_ORG_SECRET. Or set RECLAIM_ORG_SECRET for the MCP server.'));
94
+ * server's RECLAIM_ORG_SECRET, else the key `issue_org_token` saved (for the
95
+ * call's `orgId` when it has one, otherwise the most recent). The session
96
+ * token (the developer's sign-in) can't stand in, since these routes accept
97
+ * only an org key. */
98
+ function orgTokenFor(tool, orgToken, args) {
99
+ const orgId = isRecord(args) && isRecord(args.params)
100
+ ? args.params.orgId
101
+ : undefined;
102
+ const token = (typeof orgToken === 'string' && orgToken)
103
+ || orgSecret()
104
+ || savedOrgKey(typeof orgId === 'string' ? orgId : undefined);
105
+ assert(token, unauthorized(`${tool} authenticates with the organization's API key. Run `
106
+ + 'issue_org_token to save one, or pass orgToken, or set '
107
+ + 'RECLAIM_ORG_SECRET for the MCP server.'));
95
108
  return token;
96
109
  }
@@ -45,7 +45,7 @@ const BUILDER_TOPIC_FILES = {
45
45
  * the right one without fetching several. Keyed across both modes; only the
46
46
  * topics a mode actually has are listed back to it. */
47
47
  const TOPIC_SUMMARIES = {
48
- 'overview': 'Terminology, your stance, and the whole loop at a glance.',
48
+ 'overview': 'Terminology, your stance, and the authoring checklist.',
49
49
  'contributions': 'Contributing a public provider as a volunteer: the '
50
50
  + 'candidate-and-review workflow and the extra rules it adds.',
51
51
  'auth': 'Signing in to the backend, and which token authorizes what.',
@@ -54,23 +54,23 @@ const TOPIC_SUMMARIES = {
54
54
  'browser': 'Choosing a browser source (local Chrome, the Reclaim runtime in '
55
55
  + 'Docker, a remote Builder browser, or a custom CDP endpoint), '
56
56
  + 'geolocation, live view, and launch troubleshooting.',
57
- 'capture': 'Tabs, login detection, driving the page, and picking an '
58
- + 'authenticated request to prove.',
57
+ 'capture': 'Tabs, sign-in detection, capturing through the navigation '
58
+ + 'script, and picking an authenticated request to prove.',
59
59
  'draft': 'The OPRF/privacy decision, propose_provider, and the canonical '
60
60
  + 'drafted shape.',
61
61
  'params': 'Extracted vs consumer-supplied vs secret params, the URL '
62
62
  + 'character budget, and runtime resolution.',
63
63
  'prove': 'replay_request, the mandatory auth-bound check, run_proof, and '
64
64
  + 'diagnosing failures.',
65
- 'publish': 'Creating/reusing a provider, versions, multi-request providers, '
66
- + 'and going live.',
67
- 'discover': 'Finding providers, resolving them, and creating a verification '
68
- + 'session.',
69
- 'integration': 'Set up a Builder token, callback, frontend and backend '
70
- + 'over the HTTP session API; direct `api=2` API and result '
65
+ 'publish': 'Creating a provider, provider versions, multi-request '
66
+ + 'providers, and going live.',
67
+ 'discover': 'Reusing existing providers: finding, resolving, and '
68
+ + 'creating a verification session.',
69
+ 'integration': 'After start_integration: org key, callback, frontend and '
70
+ + 'backend over the HTTP session API; `api=2` and result '
71
71
  + 'verification.',
72
- 'user-script': 'Authoring jsUserScripts / customInjection to drive '
73
- + 'login-gated pages.',
72
+ 'user-script': 'The navigation script (jsUserScripts): sign-in redirect, '
73
+ + 'multi-page walk, test_user_script with keep.',
74
74
  'interception': 'injectionType / interceptorType, live interception, the '
75
75
  + 'document-replay flag, and what to do when a site won\'t load or '
76
76
  + 'won\'t let the user sign in.',
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type RegisteredTool } from '../tools/registry.ts';
3
- export { defineTool, type RegisteredTool, type ToolDefinition, } from '../tools/registry.ts';
3
+ export { defineTool, type RegisteredTool, type ToolAnnotations, type ToolDefinition, } from '../tools/registry.ts';
4
4
  /**
5
5
  * Registers a list of RegisteredTool entries on a high-level McpServer.
6
6
  * Converts JSON Schema definitions to Zod and provides consistent
package/lib/mcp/server.js CHANGED
@@ -15,6 +15,9 @@ export function registerToolList(server, tools) {
15
15
  server.registerTool(tool.definition.name, {
16
16
  description: tool.definition.description,
17
17
  inputSchema: toZod(tool.definition.inputSchema),
18
+ ...(tool.definition.annotations
19
+ ? { annotations: tool.definition.annotations }
20
+ : {}),
18
21
  }, async (args) => {
19
22
  try {
20
23
  return successResponse(await tool.handler(args));
@@ -6,7 +6,11 @@ export function successResponse(value) {
6
6
  structuredContent: structured(value),
7
7
  content: [{
8
8
  type: 'text',
9
- text: JSON.stringify(value, null, 2) ?? 'null',
9
+ // A string result is text written for the agent (a plan, a guide):
10
+ // JSON-quoting it would escape every newline.
11
+ text: typeof value === 'string'
12
+ ? value
13
+ : JSON.stringify(value, null, 2) ?? 'null',
10
14
  }],
11
15
  };
12
16
  }
@@ -3,10 +3,10 @@ import { defineTool } from "../../server.js";
3
3
  export function analyzeTool(captures) {
4
4
  return defineTool({
5
5
  name: 'analyze_request_constraints',
6
- description: 'Flag provider-authoring concerns for one captured request — signed '
7
- + 'request, CSRF dependency, bot challenge, short-lived token, '
8
- + 'GraphQL. Call it on each top candidate from '
9
- + 'find_requests_containing before drafting, and relay the returned '
6
+ description: 'Returns provider-authoring concerns for one captured request: '
7
+ + 'signed request, CSRF dependency, bot challenge, short-lived '
8
+ + 'token, or GraphQL. Call it on each top candidate from '
9
+ + '`find_requests_containing` before drafting, and relay the '
10
10
  + '`concerns` to the developer verbatim.',
11
11
  inputSchema: {
12
12
  type: 'object',
@@ -16,6 +16,7 @@ export function analyzeTool(captures) {
16
16
  },
17
17
  required: ['captureId', 'requestId'],
18
18
  },
19
+ annotations: { readOnlyHint: true, openWorldHint: false },
19
20
  }, async ({ captureId, requestId }) => {
20
21
  const entry = captures.sessions.get(captureId);
21
22
  if (!entry) {
@@ -104,13 +104,16 @@ dependencies = {}) {
104
104
  };
105
105
  return defineTool({
106
106
  name: 'attach_browser',
107
- description: 'Connects provider-authoring tools to a browser. Call without '
108
- + '`mode` to receive the current options, then ask the developer to '
109
- + 'choose; `builder` consumes quota and only `container` and `builder` '
110
- + 'support a shareable live view. Call again once with the chosen '
111
- + 'mode. Relay returned live-view URLs verbatim. For selection, '
112
- + 'geolocation, and lifecycle guidance, call '
113
- + 'how_it_works({ topic: "browser" }).',
107
+ description: 'Connects the authoring tools to a browser that loads the '
108
+ + 'developer\'s sign-in page and target site. `builder` and '
109
+ + '`container` run the browser remotely or in Docker, away from the '
110
+ + 'developer\'s own profile; `dedicated`, `attach`, and `custom` use a '
111
+ + 'local or supplied Chrome. Call without `mode` to receive the '
112
+ + 'options, ask the developer to choose, then call again once with '
113
+ + 'the chosen mode. `builder` consumes quota. Only `container` and '
114
+ + '`builder` support a shareable live view. Relay returned live-view '
115
+ + 'URLs verbatim. See how_it_works({ topic: "browser" }) for '
116
+ + 'selection, geolocation, and lifecycle.',
114
117
  inputSchema: {
115
118
  type: 'object',
116
119
  properties: {
@@ -153,6 +156,7 @@ dependencies = {}) {
153
156
  },
154
157
  },
155
158
  },
159
+ annotations: { openWorldHint: true, destructiveHint: false },
156
160
  }, async (parsed) => {
157
161
  const persisted = state.config.read()?.browser_agent;
158
162
  // Deliberately NOT falling back to the persisted mode: which browser
@@ -21,6 +21,7 @@ import { podProofTools } from "./pod-proof.js";
21
21
  import { createVersionFromCaptureTool } from "./publish.js";
22
22
  import { resetAll, sessionTools } from "./session.js";
23
23
  import { sharedAuthoringTools } from "./shared.js";
24
+ import { startIntegrationTool } from "./start-integration.js";
24
25
  /** Exported so a mode (for example, old-devtools) that supplies its own
25
26
  * publish tool can build the attach/capture state up front and share the SAME
26
27
  * instance with `buildAgentTools` — letting that publish tool see the live
@@ -71,7 +72,8 @@ export function buildAgentTools(client, builderClient = client, externalState) {
71
72
  tools.push(t);
72
73
  }
73
74
  if (client) {
74
- tools.push(createVersionFromCaptureTool(client, state.attach));
75
+ tools.push(startIntegrationTool());
76
+ tools.push(createVersionFromCaptureTool(client, state.attach, state.captures));
75
77
  }
76
78
  return tools;
77
79
  }
@@ -1,4 +1,4 @@
1
- import { type DraftStore } from '../../../authoring/index.ts';
1
+ import { type DraftStore, type KeptScriptRef } from '../../../authoring/index.ts';
2
2
  import { type CaptureSession } from '../../../cdp/capture.ts';
3
3
  import { type RegisteredTool } from '../../server.ts';
4
4
  import type { AttachState } from './attach.ts';
@@ -13,7 +13,7 @@ export interface CaptureEntry {
13
13
  tabClient: Awaited<ReturnType<AttachState['handles']['attachToTab']>>;
14
14
  drafts: DraftStore;
15
15
  }
16
- export interface CaptureStore {
16
+ export interface CaptureStore extends KeptScriptRef {
17
17
  sessions: Map<string, CaptureEntry>;
18
18
  activeTab?: string;
19
19
  }
@@ -1,4 +1,4 @@
1
- import { makeDraftStore } from "../../../authoring/index.js";
1
+ import { makeDraftStore, } from "../../../authoring/index.js";
2
2
  import { startCapture } from "../../../cdp/capture.js";
3
3
  import { defineTool } from "../../server.js";
4
4
  export async function resetCaptures(captures) {
@@ -7,6 +7,7 @@ export async function resetCaptures(captures) {
7
7
  }
8
8
  captures.sessions.clear();
9
9
  delete captures.activeTab;
10
+ delete captures.keptScript;
10
11
  }
11
12
  export function captureTools(attachRef, captures) {
12
13
  const requireAttached = () => {
@@ -17,14 +18,15 @@ export function captureTools(attachRef, captures) {
17
18
  };
18
19
  const startTool = defineTool({
19
20
  name: 'start_capture',
20
- description: 'Begin recording network requests on the specified tab. '
21
- + 'Returns a captureId — the session handle the authoring tools '
22
- + '(navigate, propose_provider, run_proof, …) all take.',
21
+ description: 'Starts recording network requests on a tab. Returns a captureId, '
22
+ + 'the session handle that the authoring tools (`navigate`, '
23
+ + '`propose_provider`, `run_proof`) take.',
23
24
  inputSchema: {
24
25
  type: 'object',
25
26
  properties: { tabId: { type: 'string' } },
26
27
  required: ['tabId'],
27
28
  },
29
+ annotations: { openWorldHint: true },
28
30
  }, async ({ tabId }) => {
29
31
  const attached = requireAttached();
30
32
  const tabClient = await attached.handles.attachToTab(tabId);
@@ -37,13 +39,14 @@ export function captureTools(attachRef, captures) {
37
39
  });
38
40
  const stopTool = defineTool({
39
41
  name: 'stop_capture',
40
- description: 'Stop recording on the given capture. '
41
- + 'Captured data remains queryable until reset_session.',
42
+ description: 'Stops recording on a capture. Captured requests stay queryable '
43
+ + 'until `reset_session`.',
42
44
  inputSchema: {
43
45
  type: 'object',
44
46
  properties: { captureId: { type: 'string' } },
45
47
  required: ['captureId'],
46
48
  },
49
+ annotations: { openWorldHint: false, idempotentHint: true },
47
50
  }, async ({ captureId }) => {
48
51
  const entry = captures.sessions.get(captureId);
49
52
  if (!entry) {
@@ -54,11 +57,11 @@ export function captureTools(attachRef, captures) {
54
57
  });
55
58
  const getTool = defineTool({
56
59
  name: 'get_request',
57
- description: 'Return the full headers and bodies of one captured request. Use it '
58
- + 'to inspect a candidate that find_requests_containing ranked. '
59
- + 'Unlike every other tool here, the headers are RAW — cookie and '
60
- + 'authorization values are visible. Read them; never repeat them to '
61
- + 'the developer or write them to a file.',
60
+ description: 'Returns the full headers and bodies of one captured request, to '
61
+ + 'inspect a candidate that `find_requests_containing` ranked. Unlike '
62
+ + 'the other tools, headers are raw: cookie and authorization values '
63
+ + 'are visible. Read them for diagnosis; do not repeat them to the '
64
+ + 'developer or write them to a file.',
62
65
  inputSchema: {
63
66
  type: 'object',
64
67
  properties: {
@@ -67,6 +70,7 @@ export function captureTools(attachRef, captures) {
67
70
  },
68
71
  required: ['captureId', 'requestId'],
69
72
  },
73
+ annotations: { readOnlyHint: true, openWorldHint: false },
70
74
  }, async ({ captureId, requestId }) => {
71
75
  const entry = captures.sessions.get(captureId);
72
76
  if (!entry) {
@@ -5,10 +5,11 @@ import { defineTool } from "../../server.js";
5
5
  export function listTabsTool(attachRef) {
6
6
  return defineTool({
7
7
  name: 'list_tabs',
8
- description: 'List the open tabs in the attached browser, with their tabId, title '
9
- + 'and URL. Call it after attach_browser to pick the tabId to pass to '
10
- + 'start_capture.',
8
+ description: 'Returns the open tabs of the attached browser, each with its tabId, '
9
+ + 'title, and URL. Call it after `attach_browser` to pick the tabId '
10
+ + 'for `start_capture`.',
11
11
  inputSchema: { type: 'object', properties: {} },
12
+ annotations: { readOnlyHint: true, openWorldHint: true },
12
13
  }, async () => {
13
14
  if (!attachRef.current) {
14
15
  throw new Error('Not attached. Call attach_browser first.');
@@ -11,17 +11,16 @@ import { defineTool } from "../../../tools/registry.js";
11
11
  export function findTool(resolveBackend) {
12
12
  return defineTool({
13
13
  name: 'find_requests_containing',
14
- description: 'Search captured responses for any encoding of `value` (raw, '
15
- + 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
16
- + 'the matched variant, score, and a short snippet — used to locate '
17
- + 'the request that returns a target. Also finds SPLIT matches: when '
18
- + '`value` is multi-word (for example, a full name), a response '
19
- + 'that carries every word SEPARATELY (a JSON profile API with '
20
- + 'first and last name in distinct fields) is returned with '
21
- + '`splitMatch:true` — prove those '
22
- + 'per-field (a redaction per part) rather than as one string. '
23
- + 'Server-side; bodies stay here.',
14
+ description: 'Searches captured responses for any encoding of `value` (raw, '
15
+ + 'URL-, JSON-, or HTML-escaped). Returns ranked candidates with the '
16
+ + 'matched variant, score, and a short snippet, to locate the '
17
+ + 'request that returns a data point. Also finds split matches: when '
18
+ + '`value` has several words (for example, a full name) and a '
19
+ + 'response carries each word in a separate field, the candidate has '
20
+ + '`splitMatch: true`; prove those per field (one redaction per '
21
+ + 'part). Response bodies stay server-side.',
24
22
  inputSchema: objectSchema({ value: { type: 'string' }, limit: { type: 'number' } }, ['value']),
23
+ annotations: { readOnlyHint: true, openWorldHint: false },
25
24
  }, async (args) => {
26
25
  const backend = resolveBackend(args);
27
26
  const value = String(args.value);
@@ -16,6 +16,7 @@ export function howItWorksTool(oldMode) {
16
16
  + 'backend. Call without a topic for the overview and index. Before an '
17
17
  + 'unfamiliar or sensitive step, request that topic. Prefer one topic; '
18
18
  + '`all` returns the much larger complete guide.',
19
+ annotations: { readOnlyHint: true, openWorldHint: false },
19
20
  inputSchema: {
20
21
  type: 'object',
21
22
  properties: {
@@ -21,13 +21,15 @@ export function liveViewTools(attachRef) {
21
21
  return [
22
22
  defineTool({
23
23
  name: 'share_browser_view',
24
- description: 'Shares an attached `container` browser with someone who is not at '
25
- + 'this machine. Step 1: call with no arguments to get the local '
26
- + 'viewer URL and the `cloudflared` command to run yourself (this '
27
- + 'server does not run it). Step 2: after cloudflared prints its '
28
- + '`https://*.trycloudflare.com` URL, call again with it as '
29
- + '`publicUrl` to get the link to relay. Treat the link as a '
30
- + 'credential. For `builder`, use the `liveViewUrl` from '
24
+ description: 'Returns a link that lets someone who is not at this machine watch '
25
+ + 'and drive the attached `container` browser, for example to sign '
26
+ + 'in. This server opens no tunnel itself. Step 1: call with no '
27
+ + 'arguments to get the local viewer URL and the `cloudflared` '
28
+ + 'command for the developer to run. Step 2: after cloudflared '
29
+ + 'prints its `https://*.trycloudflare.com` URL, call again with it '
30
+ + 'as `publicUrl` to get the link to relay. The link grants access '
31
+ + 'to that browser session only, so share it only with the person '
32
+ + 'who needs it. For `builder`, use the `liveViewUrl` from '
31
33
  + '`attach_browser`.',
32
34
  inputSchema: {
33
35
  type: 'object',
@@ -39,6 +41,7 @@ export function liveViewTools(attachRef) {
39
41
  },
40
42
  },
41
43
  },
44
+ annotations: { openWorldHint: true, idempotentHint: true },
42
45
  }, async (parsed) => {
43
46
  const attached = attachRef.current;
44
47
  assert(attached, badRequest('No browser attached. Call attach_browser first.'));
@@ -22,17 +22,12 @@ import { shapeRichProofResult } from "./proof.js";
22
22
  export function podProofTools(attachRef, captures) {
23
23
  const tool = defineTool({
24
24
  name: 'run_proof_via_pod',
25
- description: 'Prove the drafted recipe through the Popcorn pod\'s own TEE '
26
- + '(/reclaim/prove) instead of the local attestor SDK — this is '
27
- + 'the remote browser session\'s own api for creating a proof '
28
- + 'from that same session. Use this instead of run_proof '
29
- + 'whenever this capture\'s '
30
- + 'attach mode is "builder": the pod is the same enclave that '
31
- + 'held the browser\'s TLS session (and its country-routed egress, '
32
- + 'if any), so '
33
- + 'the proof reaches the target from the browser\'s own exit IP. '
34
- + 'No ownerAddress needed — the pod signs with the attestor\'s '
35
- + 'own TEE key, not a local credential.',
25
+ description: 'Proves the drafted recipe through the remote browser pod\'s own '
26
+ + 'TEE (/reclaim/prove) instead of the local attestor SDK. Use it '
27
+ + 'instead of `run_proof` when the attach mode is "builder": the '
28
+ + 'proof then reaches the target from the browser\'s own exit IP and '
29
+ + 'country. Needs no `ownerAddress`; the pod signs with its own TEE '
30
+ + 'key.',
36
31
  inputSchema: {
37
32
  type: 'object',
38
33
  properties: {
@@ -41,6 +36,7 @@ export function podProofTools(attachRef, captures) {
41
36
  },
42
37
  required: ['captureId', 'draftId'],
43
38
  },
39
+ annotations: { openWorldHint: true, destructiveHint: false },
44
40
  }, async ({ captureId, draftId }) => {
45
41
  const apiUrl = attachRef.current?.apiUrl;
46
42
  assert(apiUrl, badRequest('No pod api available for this attach — run_proof_via_pod needs '
@@ -14,9 +14,9 @@ export function proposeTool(resolveBackend) {
14
14
  return defineTool({
15
15
  name: 'propose_provider',
16
16
  description: 'Drafts a provider from one authenticated captured request and '
17
- + 'keeps captured secrets server-side. Use `targets` for every value '
18
- + 'available in the same response; the result contains a `draftId` '
19
- + 'and secret-free recipe. Set `shortenNames: false` to preserve an '
17
+ + 'keeps captured secrets server-side. Use `targets` for the data '
18
+ + 'points the claim needs from that response; returns a `draftId` '
19
+ + 'and a secret-free recipe. Set `shortenNames: false` to preserve an '
20
20
  + 'explicit parameter name, including a hidden `REQ_` or `URL_` '
21
21
  + 'prefix. For matching and naming guidance, call '
22
22
  + 'how_it_works({ topic: "draft" }) and topic `params`.',
@@ -54,6 +54,7 @@ export function proposeTool(resolveBackend) {
54
54
  target: { type: 'string' },
55
55
  hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
56
56
  }, ['requestId', 'name']),
57
+ annotations: { openWorldHint: false, destructiveHint: false },
57
58
  }, async (args) => {
58
59
  const backend = resolveBackend(args);
59
60
  const requestId = String(args.requestId);
@@ -1,6 +1,7 @@
1
1
  import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import type { KeptScriptRef } from '../../../authoring/index.ts';
2
3
  import { type RegisteredTool } from '../../server.ts';
3
4
  import type { AttachState } from './attach.ts';
4
5
  export declare function createVersionFromCaptureTool(client: ReclaimClient, attachRef?: {
5
6
  current?: AttachState;
6
- }): RegisteredTool;
7
+ }, kept?: KeptScriptRef): RegisteredTool;
@@ -165,11 +165,13 @@ const BROWSER_REQUEST_ACTION_SCHEMA = {
165
165
  },
166
166
  required: ['id', 'origin', 'path', 'method'],
167
167
  };
168
- export function createVersionFromCaptureTool(client, attachRef) {
168
+ export function createVersionFromCaptureTool(client, attachRef, kept) {
169
169
  return defineTool({
170
170
  name: 'create_provider_version_from_capture',
171
171
  description: 'Creates a new immutable provider version after `run_proof` succeeds. '
172
- + 'Pass exactly one `provider` or `providers` value returned by '
172
+ + 'Publishes the navigation script kept by `test_user_script` as '
173
+ + '`jsUserScripts` unless `jsUserScripts` is passed. Pass exactly '
174
+ + 'one `provider` or `providers` value returned by '
173
175
  + '`propose_provider`; do not rebuild the recipe. The default is a '
174
176
  + 'patch bump, or use `bump` or an explicitly requested higher '
175
177
  + '`version`. The new version remains a draft until published or '
@@ -230,7 +232,9 @@ export function createVersionFromCaptureTool(client, attachRef) {
230
232
  },
231
233
  jsUserScripts: {
232
234
  type: 'string',
233
- description: USER_SCRIPT_HINT +
235
+ description: 'Defaults to the script kept by `test_user_script` with ' +
236
+ '`keep: true`; pass it only to override. ' +
237
+ USER_SCRIPT_HINT +
234
238
  ' Stored as webSettings.jsUserScripts; the Builder ' +
235
239
  'verification client runs it before every page load ' +
236
240
  '(including before navigating to initialUrl), matching ' +
@@ -288,7 +292,9 @@ export function createVersionFromCaptureTool(client, attachRef) {
288
292
  },
289
293
  required: ['providerId', 'initialUrl'],
290
294
  },
295
+ annotations: { openWorldHint: true, destructiveHint: false },
291
296
  }, async (args) => {
297
+ const jsUserScripts = args.jsUserScripts ?? kept?.keptScript?.script;
292
298
  // Exactly one of `provider` / `providers` — normalize to an array.
293
299
  const hasMany = Array.isArray(args.providers) && args.providers.length > 0;
294
300
  const hasOne = !!args.provider;
@@ -306,7 +312,7 @@ export function createVersionFromCaptureTool(client, attachRef) {
306
312
  notes: args.notes,
307
313
  version: ver,
308
314
  requiredContext: args.requiredContext,
309
- jsUserScripts: args.jsUserScripts,
315
+ jsUserScripts,
310
316
  allowedJsRequests: args.allowedJsRequests,
311
317
  browserRequestActions: args.browserRequestActions,
312
318
  interceptorOptions: args.interceptorOptions,
@@ -332,11 +338,11 @@ export function createVersionFromCaptureTool(client, attachRef) {
332
338
  const attached = attachRef?.current;
333
339
  notes.push(...postPublishBrowserNotes(attached, {
334
340
  initialUrl: args.initialUrl,
335
- hasUserScript: !!args.jsUserScripts?.trim(),
341
+ hasUserScript: !!jsUserScripts?.trim(),
336
342
  hasInterceptor: !!args.interceptorOptions,
337
343
  }));
338
344
  // Let the developer know how and when the script runs.
339
- if (args.jsUserScripts?.trim()) {
345
+ if (jsUserScripts?.trim()) {
340
346
  notes.push('jsUserScripts will run in the Builder verification client before ' +
341
347
  'every page load (before navigating to initialUrl), matching ' +
342
348
  'old-devtool customInjection. window.Reclaim is available for ' +
@@ -10,17 +10,20 @@ import { defineTool } from "../../../tools/registry.js";
10
10
  export function replayTool(resolveBackend) {
11
11
  return defineTool({
12
12
  name: 'replay_request',
13
- description: 'Validate the drafted recipe before proving — confirm its selectors '
14
- + 'extract the target value. Returns diagnostics; run before '
15
- + 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
16
- + 're-issues the request with NO secrets/cookies (an anonymous '
17
- + 'visitor). If the values still extract (valid:true), the endpoint '
18
- + 'is PUBLIC and the proof would not bind to the user — reject it '
19
- + 'and find an auth-bound request instead.',
13
+ description: 'Re-sends the drafted request from inside the attached authoring '
14
+ + 'browser (not from this machine) and checks that its selectors '
15
+ + 'extract the target data points. Captured secrets stay server-side '
16
+ + 'and are not returned. Returns diagnostics. Run it before '
17
+ + '`run_proof`. With `withoutSecrets: true`, sends the request with '
18
+ + 'no cookies or auth headers, like an anonymous visitor. If the '
19
+ + 'values still extract (`valid: true`), the endpoint is public and '
20
+ + 'the proof would not bind to the user: choose an auth-bound '
21
+ + 'request instead.',
20
22
  inputSchema: objectSchema({
21
23
  draftId: { type: 'string' },
22
24
  withoutSecrets: { type: 'boolean' },
23
25
  }, ['draftId']),
26
+ annotations: { readOnlyHint: true, openWorldHint: true },
24
27
  }, async (args) => {
25
28
  const backend = resolveBackend(args);
26
29
  const draft = backend.drafts.get(String(args.draftId));
@@ -5,10 +5,13 @@ export function sessionTools(attachRef, captures) {
5
5
  return [
6
6
  defineTool({
7
7
  name: 'get_session',
8
- description: 'Report the current attach state, the live captureIds, and the '
9
- + 'persisted browser config. Call it to check whether a browser is '
10
- + 'still attached before reattaching, or to recover a captureId.',
8
+ description: 'Returns the current attach state, the live captureIds, the '
9
+ + 'persisted browser config, and the navigation script kept by '
10
+ + '`test_user_script`. Call it to check whether a browser is still '
11
+ + 'attached before reattaching, or to recover a captureId or the '
12
+ + 'kept script.',
11
13
  inputSchema: { type: 'object', properties: {} },
14
+ annotations: { readOnlyHint: true, openWorldHint: false },
12
15
  }, async () => ({
13
16
  config: attachRef.config.read()?.browser_agent,
14
17
  attached: Boolean(attachRef.current),
@@ -19,22 +22,27 @@ export function sessionTools(attachRef, captures) {
19
22
  orgId: attachRef.current?.orgId,
20
23
  captures: [...captures.sessions.keys()],
21
24
  activeTab: captures.activeTab,
25
+ keptUserScript: captures.keptScript?.script,
22
26
  })),
23
27
  defineTool({
24
28
  name: 'dispose_browser',
25
- description: 'Stop captures, disconnect the current browser, and immediately '
26
- + 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
27
- + 'are disconnected but never deleted by Reclaim.',
29
+ description: 'Stops captures, disconnects the current browser, and deletes a '
30
+ + 'Builder-hosted browser session at once. Clears the kept '
31
+ + 'navigation script. Reclaim disconnects a custom CDP browser but '
32
+ + 'never deletes it.',
28
33
  inputSchema: { type: 'object', properties: {} },
34
+ annotations: { destructiveHint: true, openWorldHint: true },
29
35
  }, async () => {
30
36
  await resetAll(attachRef, captures);
31
37
  return { disposed: true };
32
38
  }),
33
39
  defineTool({
34
40
  name: 'reset_session',
35
- description: 'Stop all captures and clear capture state — drops every '
36
- + 'session\'s drafts and their in-memory secrets.',
41
+ description: 'Stops all captures and clears capture state, dropping every '
42
+ + 'draft and its in-memory secrets. Also disconnects the browser '
43
+ + 'and clears the kept navigation script.',
37
44
  inputSchema: { type: 'object', properties: {} },
45
+ annotations: { destructiveHint: true, openWorldHint: true },
38
46
  }, async () => {
39
47
  await resetAll(attachRef, captures);
40
48
  return { reset: true };
@@ -32,12 +32,13 @@ function withLocalOprfMode(provider) {
32
32
  /** The local backend for one capture: drive its tab over CDP, read its
33
33
  * captured requests, prove via the in-process attestor, replay by re-fetching
34
34
  * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
35
- function localBackend(entry) {
35
+ function localBackend(entry, kept) {
36
36
  const { tabClient, session, drafts } = entry;
37
37
  const sendCdp = (method, params) => {
38
38
  return tabClient.send(method, params ?? {});
39
39
  };
40
40
  return {
41
+ kept,
41
42
  capture: session,
42
43
  drafts,
43
44
  sendCdp,
@@ -131,7 +132,7 @@ export function sharedAuthoringTools(captures) {
131
132
  throw new Error(`no capture ${captureId} — call start_capture first `
132
133
  + '(it has been reset or never started).');
133
134
  }
134
- return localBackend(entry);
135
+ return localBackend(entry, captures);
135
136
  };
136
137
  return authoringTools(resolveBackend).map((tool) => {
137
138
  if (tool.definition.name === 'run_proof') {