@reclaimprotocol/agent 0.1.4 → 0.1.6

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 (126) hide show
  1. package/README.md +5 -5
  2. package/lib/authoring/backend.d.ts +71 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/find.d.ts +3 -0
  5. package/lib/authoring/find.js +54 -0
  6. package/lib/authoring/index.d.ts +4 -0
  7. package/lib/authoring/index.js +6 -0
  8. package/lib/authoring/propose.d.ts +3 -0
  9. package/lib/authoring/propose.js +109 -0
  10. package/lib/authoring/replay.d.ts +10 -0
  11. package/lib/authoring/replay.js +34 -0
  12. package/lib/authoring/tools.d.ts +16 -0
  13. package/lib/authoring/tools.js +193 -0
  14. package/lib/authoring/util.d.ts +27 -0
  15. package/lib/authoring/util.js +21 -0
  16. package/lib/cdp/capture-engine.js +1 -1
  17. package/lib/consts.d.ts +6 -1
  18. package/lib/consts.js +9 -2
  19. package/lib/mcp/api/operations.gen.js +46 -18
  20. package/lib/mcp/server.d.ts +2 -25
  21. package/lib/mcp/server.js +4 -17
  22. package/lib/mcp/start.js +4 -2
  23. package/lib/mcp/tools/agent/analyze.js +3 -3
  24. package/lib/mcp/tools/agent/build.d.ts +4 -4
  25. package/lib/mcp/tools/agent/build.js +13 -15
  26. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  27. package/lib/mcp/tools/agent/capture.js +18 -85
  28. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  29. package/lib/mcp/tools/agent/eval.js +11 -121
  30. package/lib/mcp/tools/agent/find.d.ts +10 -3
  31. package/lib/mcp/tools/agent/find.js +25 -31
  32. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  33. package/lib/mcp/tools/agent/proof.js +81 -132
  34. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  35. package/lib/mcp/tools/agent/propose.js +87 -64
  36. package/lib/mcp/tools/agent/publish.js +2 -2
  37. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  38. package/lib/mcp/tools/agent/replay.js +29 -36
  39. package/lib/mcp/tools/agent/session.d.ts +1 -3
  40. package/lib/mcp/tools/agent/session.js +5 -5
  41. package/lib/mcp/tools/agent/shared.d.ts +8 -0
  42. package/lib/mcp/tools/agent/shared.js +125 -0
  43. package/lib/old/client.d.ts +35 -0
  44. package/lib/old/client.js +18 -0
  45. package/lib/old/login-cdp.d.ts +24 -0
  46. package/lib/old/login-cdp.js +28 -3
  47. package/lib/old/sdk-client.d.ts +39 -0
  48. package/lib/old/sdk-client.js +85 -0
  49. package/lib/old/to-register.d.ts +69 -0
  50. package/lib/old/to-register.js +52 -2
  51. package/lib/old/tools/build.d.ts +6 -1
  52. package/lib/old/tools/build.js +11 -1
  53. package/lib/old/tools/get-provider-info.d.ts +16 -0
  54. package/lib/old/tools/get-provider-info.js +73 -0
  55. package/lib/old/tools/get-version-config.d.ts +17 -0
  56. package/lib/old/tools/get-version-config.js +47 -0
  57. package/lib/old/tools/publish.js +189 -2
  58. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  59. package/lib/old/tools/update-provider-metadata.js +61 -0
  60. package/lib/proof/attestor.d.ts +6 -3
  61. package/lib/proof/attestor.js +10 -1
  62. package/lib/proof/debug-response-dump.d.ts +4 -0
  63. package/lib/proof/debug-response-dump.js +55 -0
  64. package/lib/proof/patch-domhandler.d.ts +1 -0
  65. package/lib/proof/patch-domhandler.js +59 -0
  66. package/lib/proof/proof-core.d.ts +1 -1
  67. package/lib/proof/proof-core.js +1 -1
  68. package/lib/proof/zk-files.js +7 -6
  69. package/lib/provider/dom-xpath.d.ts +17 -0
  70. package/lib/provider/dom-xpath.js +187 -0
  71. package/lib/provider/draft.d.ts +30 -2
  72. package/lib/provider/draft.js +117 -28
  73. package/lib/provider/index.d.ts +4 -0
  74. package/lib/provider/index.js +7 -0
  75. package/lib/provider/injection-doc.js +6 -0
  76. package/lib/provider/matcher.d.ts +17 -20
  77. package/lib/provider/matcher.js +394 -280
  78. package/lib/provider/schema.d.ts +7 -5
  79. package/lib/tools/registry.d.ts +50 -0
  80. package/lib/tools/registry.js +49 -0
  81. package/package.json +9 -1
  82. package/skill/SKILL.builder.md +55 -32
  83. package/skill/SKILL.old.md +20 -12
  84. package/skill/builder-mode.md +1 -1
  85. package/skill/injection-api.md +117 -0
  86. package/src/authoring/backend.ts +98 -0
  87. package/src/authoring/index.ts +20 -0
  88. package/src/authoring/tools.ts +273 -0
  89. package/src/authoring/util.ts +42 -0
  90. package/src/cdp/capture-engine.ts +1 -1
  91. package/src/consts.ts +15 -2
  92. package/src/mcp/api/operations.gen.ts +46 -18
  93. package/src/mcp/server.ts +9 -39
  94. package/src/mcp/start.ts +4 -1
  95. package/src/mcp/tools/agent/analyze.ts +3 -3
  96. package/src/mcp/tools/agent/build.ts +13 -21
  97. package/src/mcp/tools/agent/capture.ts +31 -115
  98. package/src/mcp/tools/agent/eval.ts +19 -155
  99. package/src/mcp/tools/agent/find.ts +33 -36
  100. package/src/mcp/tools/agent/proof.ts +87 -160
  101. package/src/mcp/tools/agent/propose.ts +93 -85
  102. package/src/mcp/tools/agent/publish.ts +2 -2
  103. package/src/mcp/tools/agent/replay.ts +32 -42
  104. package/src/mcp/tools/agent/session.ts +4 -5
  105. package/src/mcp/tools/agent/shared.ts +152 -0
  106. package/src/old/client.ts +56 -0
  107. package/src/old/login-cdp.ts +28 -3
  108. package/src/old/sdk-client.ts +105 -0
  109. package/src/old/to-register.ts +98 -6
  110. package/src/old/tools/build.ts +12 -0
  111. package/src/old/tools/get-provider-info.ts +91 -0
  112. package/src/old/tools/get-version-config.ts +56 -0
  113. package/src/old/tools/publish.ts +244 -1
  114. package/src/old/tools/update-provider-metadata.ts +72 -0
  115. package/src/proof/attestor.ts +16 -4
  116. package/src/proof/debug-response-dump.ts +70 -0
  117. package/src/proof/patch-domhandler.ts +68 -0
  118. package/src/proof/proof-core.ts +1 -1
  119. package/src/proof/zk-files.ts +7 -6
  120. package/src/provider/dom-xpath.ts +225 -0
  121. package/src/provider/draft.ts +168 -35
  122. package/src/provider/index.ts +8 -0
  123. package/src/provider/injection-doc.ts +6 -0
  124. package/src/provider/matcher.ts +477 -357
  125. package/src/provider/schema.ts +7 -5
  126. package/src/tools/registry.ts +92 -0
@@ -1,120 +1,128 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { draftProvider } from '../../../provider/draft.ts'
1
+ /**
2
+ * `propose_provider` — draft a provider (recipe + captured secrets) from ONE
3
+ * captured request, one match + redaction per target value. Authored once as a
4
+ * plain {@link RegisteredTool}; hosts pass their own backend resolver. The
5
+ * draft (recipe + real secret header values) is held server-side under an
6
+ * opaque `draftId`; the result carries only the secret-free provider.
7
+ */
8
+ import { objectSchema, type ResolveBackend } from '../../../authoring/util.ts'
9
+ import { draftProvider, type DraftTarget } from '../../../provider/draft.ts'
3
10
  import { OPRF_MAX_BYTES } from '../../../provider/matcher.ts'
4
11
  import type { OprfMode } from '../../../provider/schema.ts'
5
12
  import { verificationWarnings } from '../../../provider/verification-checks.ts'
6
- import { defineTool, type RegisteredTool } from '../../server.ts'
7
- import type { CaptureStore } from './capture.ts'
13
+ import { defineTool, type RegisteredTool } from '../../../tools/registry.ts'
8
14
 
9
- export interface SecretStore {
10
- put: (values: Record<string, string>) => string
11
- resolve: (ref: string) => Record<string, string> | undefined
12
- clear: () => void
13
- }
14
-
15
- export function makeSecretStore(): SecretStore {
16
- const map = new Map<string, Record<string, string>>()
17
- return {
18
- put: (values) => {
19
- const id = `sref_${randomUUID()}`
20
- map.set(id, values)
21
- return id
22
- },
23
- resolve: (ref) => map.get(ref),
24
- clear: () => map.clear(),
25
- }
26
- }
27
-
28
- export function proposeTool(
29
- captures: CaptureStore,
30
- secrets: SecretStore,
31
- ): RegisteredTool {
32
- return defineTool<{
33
- captureId: string
34
- requestId: string
35
- target: string
36
- name: string
37
- hash?: OprfMode
38
- }>(
15
+ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
16
+ return defineTool<Record<string, unknown>>(
39
17
  {
40
18
  name: 'propose_provider',
41
19
  description:
42
- 'Draft a Reclaim provider JSON from a captured request + ' +
43
- 'a target value. ' +
44
- 'Secret header values are stored server-side and replaced with an ' +
45
- 'opaque secretParamsRef ' +
46
- 'in the response — raw secret values never appear in tool output. ' +
47
- "`hash` is opt-in: when set, the captured value is OPRF'd so " +
48
- 'the attestor never sees ' +
49
- 'cleartext. Skill should ask the dev first. `oprf-raw` ' +
50
- 'is the only variant that runs ' +
51
- 'without external deps; `oprf` needs gnark (Linux/macOS), ' +
52
- '`oprf-mpc` needs a TEE attestor. ' +
53
- 'The response may include `verificationWarnings` — advisory ' +
54
- 'author-only decisions (e.g. a user-specific URL part needing ' +
55
- 'the {{context.<name>}}-vs-bare call). Surface each to the ' +
56
- 'dev verbatim and resolve before publishing.',
57
- inputSchema: {
58
- type: 'object',
59
- properties: {
60
- captureId: { type: 'string' },
20
+ 'Draft a provider from ONE captured request. Pass `targets` (an '
21
+ + 'array): one entry per value to extract, each with its `value` '
22
+ + '(verbatim) and a distinct `name` (its param). The provider gets '
23
+ + 'one match + redaction per target. Returns a draftId + recipe '
24
+ + '(never secrets). Prefer a SINGLE request holding ALL values. '
25
+ + 'Single-value shorthand: `target` + `name`. Per-target `hash` '
26
+ + 'OPRF-hashes that value.',
27
+ inputSchema: objectSchema(
28
+ {
61
29
  requestId: { type: 'string' },
62
- target: { type: 'string' },
63
30
  name: { type: 'string' },
31
+ targets: {
32
+ type: 'array',
33
+ items: {
34
+ type: 'object',
35
+ properties: {
36
+ value: { type: 'string' },
37
+ name: { type: 'string' },
38
+ hash: {
39
+ type: 'string',
40
+ enum: ['oprf-raw', 'oprf', 'oprf-mpc'],
41
+ },
42
+ },
43
+ required: ['value', 'name'],
44
+ },
45
+ },
46
+ target: { type: 'string' },
64
47
  hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
65
48
  },
66
- required: ['captureId', 'requestId', 'target', 'name'],
67
- },
49
+ ['requestId', 'name'],
50
+ ),
68
51
  },
69
- async({ captureId, requestId, target, name, hash }) => {
70
- if(hash !== undefined && target.length > OPRF_MAX_BYTES) {
52
+ async(args) => {
53
+ const backend = resolveBackend(args)
54
+ const requestId = String(args.requestId)
55
+ const providerName = String(args.name)
56
+ const defaultHash = args.hash as OprfMode | undefined
57
+
58
+ // Explicit multi-value `targets`, else the single `target` shorthand.
59
+ const raw = Array.isArray(args.targets) ? args.targets : []
60
+ const targets: DraftTarget[] = raw.length
61
+ ? raw.map((t) => {
62
+ const o = t as Record<string, unknown>
63
+ return {
64
+ value: String(o.value),
65
+ name: String(o.name),
66
+ ...(typeof o.hash === 'string'
67
+ ? { hash: o.hash as OprfMode }
68
+ : {}),
69
+ }
70
+ })
71
+ : typeof args.target === 'string'
72
+ ? [{
73
+ value: args.target,
74
+ name: providerName,
75
+ ...(defaultHash ? { hash: defaultHash } : {}),
76
+ }]
77
+ : []
78
+ if(targets.length === 0) {
71
79
  throw new Error(
72
- `OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; ` +
73
- `got ${target.length}-byte target ` +
74
- `"${target}". Try a tighter target ` +
75
- '(e.g. just the relevant substring) or call ' +
76
- 'propose_provider without `hash`.',
80
+ 'propose_provider needs a `target` value or a non-empty '
81
+ + '`targets` array',
77
82
  )
78
83
  }
79
84
 
80
- const session = captures.sessions.get(captureId)
81
- if(!session) {
82
- throw new Error(`no capture ${captureId}`)
85
+ for(const t of targets) {
86
+ if((t.hash ?? defaultHash) !== undefined
87
+ && t.value.length > OPRF_MAX_BYTES) {
88
+ throw new Error(
89
+ `OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got `
90
+ + `${t.value.length}-byte "${t.value}". Use a tighter `
91
+ + 'target or drop `hash`.',
92
+ )
93
+ }
83
94
  }
84
95
 
85
- const r = session.requests.get(requestId)
86
- if(!r) {
87
- throw new Error(`no request ${requestId}`)
96
+ const captured = backend.capture.requests.get(requestId)
97
+ if(!captured) {
98
+ throw new Error(`no captured request ${requestId}`)
88
99
  }
89
100
 
90
- const body = session.bodies.get(requestId)
91
- if(!body.available) {
92
- throw new Error(`request body unavailable: ${body.reason}`)
101
+ if(!captured.responseBody) {
102
+ throw new Error(`request body unavailable for ${requestId}`)
93
103
  }
94
104
 
95
- const reqWithBody = { ...r, responseBody: body.body }
96
- const { provider, secretRefs } = draftProvider(
97
- reqWithBody,
98
- target,
99
- name,
100
- hash,
105
+ const { provider, secretRefs, extractions } = draftProvider(
106
+ captured, targets, providerName, defaultHash,
101
107
  )
102
-
103
- // Stash real secret values for later replay/proof.
104
- const realSecrets: Record<string, string> = {}
108
+ // Materialize the real secret header values for later replay/prove;
109
+ // they stay in the server-side draft, never in this result.
110
+ const secrets: Record<string, string> = {}
105
111
  for(const ref of secretRefs) {
106
- const real = r.requestHeaders[ref.name]
112
+ const real = captured.requestHeaders[ref.name]
107
113
  if(real !== undefined) {
108
- realSecrets[ref.name] = real
114
+ secrets[ref.name] = real
109
115
  }
110
116
  }
111
117
 
112
- const secretParamsRef = secrets.put(realSecrets)
118
+ const draftId = backend.drafts.put({
119
+ provider, secretRefs, secrets, requestId, extractions,
120
+ })
113
121
  const warnings = verificationWarnings(provider)
114
122
  return {
123
+ draftId,
115
124
  provider,
116
125
  secretRefs,
117
- secretParamsRef,
118
126
  ...(warnings.length ? { verificationWarnings: warnings } : {}),
119
127
  }
120
128
  },
@@ -129,7 +129,7 @@ export function createVersionFromCaptureTool(
129
129
  {
130
130
  name: 'create_provider_version_from_capture',
131
131
  description:
132
- 'Persist drafted ReclaimProvider(s) as a verification version. ' +
132
+ 'Persist drafted ReclaimProvider(s) as a provider version. ' +
133
133
  'Call this after run_proof succeeds, passing the `provider` ' +
134
134
  'object(s) from propose_provider VERBATIM. This tool builds the ' +
135
135
  'body (the requests[] array, webSettings, etc.) for you — do NOT ' +
@@ -160,7 +160,7 @@ export function createVersionFromCaptureTool(
160
160
  properties: {
161
161
  providerId: {
162
162
  type: 'string',
163
- description: 'UUID of the verification to publish the version on.',
163
+ description: 'UUID of the provider to publish the version on.',
164
164
  },
165
165
  provider: PROVIDER_SCHEMA,
166
166
  providers: {
@@ -1,53 +1,43 @@
1
- import { replayProvider } from '../../../proof/replay.ts'
2
- import type { ReclaimProvider } from '../../../provider/schema.ts'
3
- import { defineTool, type RegisteredTool } from '../../server.ts'
4
- import type { SecretStore } from './propose.ts'
1
+ /**
2
+ * `replay_request` — validate a drafted recipe before proving: confirm its
3
+ * selectors extract the target(s), and (with `withoutSecrets`) that the
4
+ * endpoint is auth-bound. Authored once as a plain {@link RegisteredTool}; the
5
+ * backend does the actual replay (MCP re-runs locally, cloud re-issues in the
6
+ * TEE pod).
7
+ */
8
+ import { objectSchema, type ResolveBackend } from '../../../authoring/util.ts'
9
+ import { defineTool, type RegisteredTool } from '../../../tools/registry.ts'
5
10
 
6
- export function replayTool(secrets: SecretStore): RegisteredTool {
7
- return defineTool<{ provider: unknown, secretParamsRef?: string }>(
11
+ export function replayTool(resolveBackend: ResolveBackend): RegisteredTool {
12
+ return defineTool<Record<string, unknown>>(
8
13
  {
9
14
  name: 'replay_request',
10
15
  description:
11
- 'Replay a drafted provider against the live endpoint ' +
12
- 'using Node fetch (no attestor). ' +
13
- 'Pre-flight gate before run_proof — ' +
14
- 'diagnoses CSRF / token-rotation / matcher-too-tight ' +
15
- 'issues with richer hints than the attestor would produce.',
16
- inputSchema: {
17
- type: 'object',
18
- properties: {
19
- provider: { type: 'object', properties: {} },
20
- secretParamsRef: { type: 'string' },
16
+ 'Validate the drafted recipe before proving — confirm its selectors '
17
+ + 'extract the target value. Returns diagnostics; run before '
18
+ + 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
19
+ + 're-issues the request with NO secrets/cookies (an anonymous '
20
+ + 'visitor). If the values still extract (valid:true), the endpoint '
21
+ + 'is PUBLIC and the proof would not bind to the user — reject it '
22
+ + 'and find an auth-bound request instead.',
23
+ inputSchema: objectSchema(
24
+ {
25
+ draftId: { type: 'string' },
26
+ withoutSecrets: { type: 'boolean' },
21
27
  },
22
- required: ['provider'],
23
- },
28
+ ['draftId'],
29
+ ),
24
30
  },
25
- async({ provider, secretParamsRef }) => {
26
- let resolved: Record<string, string> = {}
27
- if(secretParamsRef !== undefined) {
28
- const r = secrets.resolve(secretParamsRef)
29
- if(!r) {
30
- throw new Error(
31
- `unknown secretParamsRef "${secretParamsRef}" — ` +
32
- 'the MCP server has restarted or reset_session was called. ' +
33
- 'Re-run propose_provider to mint a fresh ref.',
34
- )
35
- }
36
-
37
- resolved = r
38
- }
39
-
40
- const result = await replayProvider(provider as ReclaimProvider, resolved)
41
- const out: Record<string, unknown> = {
42
- status: result.status,
43
- matchesOriginal: result.matchesOriginal,
44
- bodySnippet: result.body.slice(0, 256),
45
- }
46
- if(result.hints !== undefined) {
47
- out.hints = result.hints
31
+ async(args) => {
32
+ const backend = resolveBackend(args)
33
+ const draft = backend.drafts.get(String(args.draftId))
34
+ if(!draft) {
35
+ throw new Error(`no draft ${String(args.draftId)}`)
48
36
  }
49
37
 
50
- return out
38
+ return backend.replay(draft, {
39
+ withoutSecrets: Boolean(args.withoutSecrets),
40
+ })
51
41
  },
52
42
  )
53
43
  }
@@ -6,7 +6,6 @@ import type { CaptureStore } from './capture.ts'
6
6
  export function sessionTools(
7
7
  attachRef: { current?: AttachState, config: ConfigStore },
8
8
  captures: CaptureStore,
9
- secrets: { clear: () => void },
10
9
  ): RegisteredTool[] {
11
10
  return [
12
11
  defineTool<Record<string, never>>(
@@ -28,17 +27,17 @@ export function sessionTools(
28
27
  {
29
28
  name: 'reset_session',
30
29
  description:
31
- 'Stop all captures, drop in-memory secrets, and clear capture state.',
30
+ 'Stop all captures and clear capture state — drops every '
31
+ + 'session\'s drafts and their in-memory secrets.',
32
32
  inputSchema: { type: 'object', properties: {} },
33
33
  },
34
34
  async() => {
35
- for(const s of captures.sessions.values()) {
36
- await s.stop().catch(() => {})
35
+ for(const entry of captures.sessions.values()) {
36
+ await entry.session.stop().catch(() => {})
37
37
  }
38
38
 
39
39
  captures.sessions.clear()
40
40
  delete captures.activeTab
41
- secrets.clear()
42
41
  return { reset: true }
43
42
  },
44
43
  ),
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Build the shared authoring tools for the MCP, bound to a per-capture LOCAL
3
+ * backend: a Chrome tab over CDP + the in-process attestor SDK. The tool logic
4
+ * (navigate/inspect/propose/replay/prove) is authored once in `../../../
5
+ * authoring` with the shared `defineTool` primitive; this only supplies the
6
+ * MCP host — resolve the backend from `captureId` (the session handle from
7
+ * `start_capture`, since one capture bundles the tab, its requests, and its
8
+ * drafts) and the rich, credential-aware `run_proof` schema.
9
+ */
10
+ import {
11
+ type AuthoringBackend,
12
+ authoringTools,
13
+ extendTool,
14
+ } from '../../../authoring/index.ts'
15
+ import { runAttestorProof } from '../../../proof/attestor.ts'
16
+ import { replayProvider } from '../../../proof/replay.ts'
17
+ import type { RegisteredTool } from '../../server.ts'
18
+ import type { CaptureEntry, CaptureStore } from './capture.ts'
19
+ import { shapeRichProofResult } from './proof.ts'
20
+
21
+ /** The local backend for one capture: drive its tab over CDP, read its
22
+ * captured requests, prove via the in-process attestor, replay by re-fetch. */
23
+ function localBackend(entry: CaptureEntry): AuthoringBackend {
24
+ const { tabClient, session, drafts } = entry
25
+ // The per-tab CDP client is strongly typed to `keyof Commands`; the shared
26
+ // tools drive it with dynamic method/event names, so view it as string-keyed.
27
+ const client = tabClient as unknown as {
28
+ send: (method: string, params: object) => Promise<unknown>
29
+ once: (e: string, cb: () => void) => void
30
+ removeListener: (e: string, cb: () => void) => void
31
+ }
32
+
33
+ return {
34
+ capture: session,
35
+ drafts,
36
+ sendCdp: (method, params) => client.send(method, params ?? {}),
37
+ waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
38
+ const onEvent = () => {
39
+ cleanup()
40
+ resolve('fired')
41
+ }
42
+
43
+ const timer = setTimeout(() => {
44
+ cleanup()
45
+ resolve('timeout')
46
+ }, timeoutMs)
47
+ const cleanup = () => {
48
+ clearTimeout(timer)
49
+ client.removeListener(method, onEvent)
50
+ }
51
+
52
+ client.once(method, onEvent)
53
+ }),
54
+ replay: async(draft, opts) => {
55
+ // Auth-bound check: re-fetch with the secrets dropped. replayProvider
56
+ // already does a live Node fetch, so empty secrets == anonymous.
57
+ const secrets = opts?.withoutSecrets ? {} : draft.secrets
58
+ const result = await replayProvider(draft.provider, secrets)
59
+ return {
60
+ status: result.status,
61
+ matchesOriginal: result.matchesOriginal,
62
+ bodySnippet: result.body.slice(0, 256),
63
+ ...(result.hints !== undefined ? { hints: result.hints } : {}),
64
+ }
65
+ },
66
+ prove: async(draft, opts) => {
67
+ const result = await runAttestorProof(
68
+ draft.provider,
69
+ draft.secrets,
70
+ {
71
+ ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
72
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
73
+ },
74
+ )
75
+ return shapeRichProofResult(result, {
76
+ provider: draft.provider,
77
+ hasSecrets: Object.keys(draft.secrets).length > 0,
78
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
79
+ })
80
+ },
81
+ }
82
+ }
83
+
84
+ /** The MCP's rich, credential-aware `run_proof` schema, merged onto the shared
85
+ * tool by the host below (the cloud's TEE signs in-enclave, so it needs none
86
+ * of this). */
87
+ const RUN_PROOF_EXTRAS = {
88
+ description:
89
+ 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
90
+ + 'attestor SDK. Use only after replay_request looks right — attestor '
91
+ + 'calls are expensive. Writes the full claim to '
92
+ + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
93
+ + 'extractedParameters + claimPath + a prettified `claim` (request '
94
+ + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
95
+ + 'writes a per-call diagnostic dump (input provider, sentParams, full '
96
+ + 'attestor log stream, result) and returns it as `runLogPath`. '
97
+ + '`attestorUrl` overrides the default only when the dev supplies a '
98
+ + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
99
+ + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
100
+ + 'first. errorKind="missing-owner-key" means that address has no known '
101
+ + 'private key; re-run resolve_owner_key or issue/import a credential.',
102
+ properties: {
103
+ ownerAddress: {
104
+ type: 'string',
105
+ pattern: '^0x[0-9a-fA-F]{40}$',
106
+ description:
107
+ 'ETH address of the credential to sign the claim with. Get it '
108
+ + 'from resolve_owner_key first. The private key is resolved '
109
+ + 'locally (cache / env / .env) — never passed here.',
110
+ },
111
+ attestorUrl: { type: 'string' },
112
+ },
113
+ required: ['ownerAddress'],
114
+ }
115
+
116
+ /** The shared authoring tools mapped to the MCP's shape: resolve each call's
117
+ * local backend from `args.captureId`, and widen the input schema — every tool
118
+ * takes `captureId`; `run_proof` also gets the credential-aware contract. The
119
+ * tools are authored plain in `../../../authoring`; this only wraps them with
120
+ * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
121
+ export function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[] {
122
+ const resolveBackend = (args: Record<string, unknown>): AuthoringBackend => {
123
+ const captureId = String(args.captureId)
124
+ const entry = captures.sessions.get(captureId)
125
+ if(!entry) {
126
+ throw new Error(
127
+ `no capture ${captureId} — call start_capture first `
128
+ + '(it has been reset or never started).',
129
+ )
130
+ }
131
+
132
+ return localBackend(entry)
133
+ }
134
+
135
+ return authoringTools(resolveBackend).map((tool) => {
136
+ if(tool.definition.name === 'run_proof') {
137
+ return extendTool(tool, {
138
+ description: RUN_PROOF_EXTRAS.description,
139
+ properties: {
140
+ captureId: { type: 'string' },
141
+ ...RUN_PROOF_EXTRAS.properties,
142
+ },
143
+ required: ['captureId', ...RUN_PROOF_EXTRAS.required],
144
+ })
145
+ }
146
+
147
+ return extendTool(tool, {
148
+ properties: { captureId: { type: 'string' } },
149
+ required: ['captureId'],
150
+ })
151
+ })
152
+ }
package/src/old/client.ts CHANGED
@@ -34,6 +34,15 @@ export interface RegisterProviderBody {
34
34
  useProxy?: boolean
35
35
  providerType?: 'PRIVATE' | 'PUBLIC'
36
36
  requestData: unknown[]
37
+ /** Page-injection script (old-devtools name for the builder's
38
+ * `webSettings.jsUserScripts`). Top-level on register; nested under
39
+ * `providerConfig` on the config (add-version) body instead. */
40
+ customInjection?: string
41
+ /** Accepted at register too (unlike most `providerConfig` extras, which
42
+ * the register controller only reads on config/add-version). */
43
+ userAgent?: { ios?: string, android?: string }
44
+ /** Accepted at register too. */
45
+ pageTitle?: string
37
46
  }
38
47
 
39
48
  export interface UpdateProviderConfigBody {
@@ -42,6 +51,19 @@ export interface UpdateProviderConfigBody {
42
51
  versionInfo: string
43
52
  }
44
53
 
54
+ /** Body for `POST /api/providers/:providerId` (`updateProviderMetadata`) —
55
+ * a genuine PARTIAL update on the PROVIDER document itself (name/
56
+ * description/providerType/tags/isActive), distinct from both `register`
57
+ * (creates the provider) and `config` (adds an immutable new version). Only
58
+ * the fields present get changed; everything else is left untouched. */
59
+ export interface UpdateProviderMetadataBody {
60
+ name?: string
61
+ description?: string
62
+ providerType?: 'PRIVATE' | 'PUBLIC'
63
+ tags?: string[]
64
+ isActive?: boolean
65
+ }
66
+
45
67
  export interface MyProvidersQuery {
46
68
  pageKey?: number
47
69
  pageSize?: number
@@ -121,6 +143,40 @@ export class ReclaimOldClient {
121
143
  )
122
144
  }
123
145
 
146
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
147
+ * one version — the latest when `version` is omitted. Readable for ANY
148
+ * non-archived provider (not owner-restricted), same as the dashboard's
149
+ * edit-form prefill fetch. */
150
+ getProviderConfig(
151
+ providerId: string,
152
+ version?: string,
153
+ ): Promise<unknown> {
154
+ return this.#request(
155
+ 'GET',
156
+ `/providers/${encodeURIComponent(providerId)}/config`,
157
+ {
158
+ auth: true,
159
+ query: version ? { versionNumber: version } : undefined,
160
+ },
161
+ )
162
+ }
163
+
164
+ /** Partial update on the PROVIDER document itself — name/description/
165
+ * providerType/tags/isActive, after creation. The backend 403s if you
166
+ * don't own the provider (and aren't admin); other providerConfig-style
167
+ * fields sent here are simply ignored (this endpoint only reads the
168
+ * ones above from the body). */
169
+ updateProviderMetadata(
170
+ providerId: string,
171
+ body: UpdateProviderMetadataBody,
172
+ ): Promise<unknown> {
173
+ return this.#request(
174
+ 'POST',
175
+ `/providers/${encodeURIComponent(providerId)}`,
176
+ { body, auth: true },
177
+ )
178
+ }
179
+
124
180
  /** Analytics logs for a verification session, from the logs service (a
125
181
  * DIFFERENT host than the devtools API — hence its own fetch, not
126
182
  * `#request`). Attaches the caller identity when one is set; queried
@@ -18,16 +18,39 @@ import { DEFAULT_LOGIN_URL, oldLoginUrl } from '../consts.ts'
18
18
  * `firebaseLocalStorage`) in case a build switches persistence. Returns the
19
19
  * token string, or null when the user has not finished logging in yet.
20
20
  *
21
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
22
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
23
+ * session is often still sitting in storage the instant the page loads —
24
+ * before the developer has clicked anything. Returning that immediately
25
+ * makes the poll "succeed" on tick one, closing the tab before the developer
26
+ * can sign in, and the captured token then fails backend verification. So
27
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
28
+ * available) and stale ones are skipped — the loop keeps polling until a
29
+ * genuinely fresh token shows up from an interactive sign-in.
30
+ *
21
31
  * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
22
32
  */
23
- const EXTRACT_TOKEN_JS = `(async () => {
33
+ export const EXTRACT_TOKEN_JS = `(async () => {
34
+ const isFresh = (token) => {
35
+ try {
36
+ const payload = JSON.parse(
37
+ atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
38
+ )
39
+ // No exp claim to check — don't block a token we can't evaluate.
40
+ if (typeof payload.exp !== 'number') return true
41
+ // 60s buffer so a token that's about to expire isn't treated as fresh.
42
+ return payload.exp * 1000 > Date.now() + 60000
43
+ } catch (e) {
44
+ return true
45
+ }
46
+ }
24
47
  try {
25
48
  for (let i = 0; i < localStorage.length; i++) {
26
49
  const k = localStorage.key(i)
27
50
  if (k && k.indexOf('firebase:authUser:') === 0) {
28
51
  const v = JSON.parse(localStorage.getItem(k) || 'null')
29
52
  const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
30
- if (t) return t
53
+ if (t && isFresh(t)) return t
31
54
  }
32
55
  }
33
56
  } catch (e) {}
@@ -49,7 +72,9 @@ const EXTRACT_TOKEN_JS = `(async () => {
49
72
  all.onsuccess = () => {
50
73
  for (const row of (all.result || [])) {
51
74
  const m = row && row.value && row.value.stsTokenManager
52
- if (m && m.accessToken) { resolve(m.accessToken); return }
75
+ if (m && m.accessToken && isFresh(m.accessToken)) {
76
+ resolve(m.accessToken); return
77
+ }
53
78
  }
54
79
  resolve(null)
55
80
  }