imprint-mcp 0.6.0 → 0.6.2

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 (43) hide show
  1. package/README.md +2 -0
  2. package/package.json +4 -2
  3. package/prompts/audit-agent.md +4 -2
  4. package/prompts/auth-compile-agent.md +149 -158
  5. package/prompts/build-planning.md +14 -9
  6. package/prompts/compile-agent.md +46 -23
  7. package/prompts/live-verifier-agent.md +101 -0
  8. package/prompts/playbook-compilation.md +8 -1
  9. package/prompts/tool-candidate-detection.md +9 -29
  10. package/src/cli.ts +83 -1
  11. package/src/imprint/audit.ts +219 -20
  12. package/src/imprint/auth-compile-agent.ts +215 -155
  13. package/src/imprint/auth-compile-tools.ts +300 -118
  14. package/src/imprint/auth-verifier.ts +167 -180
  15. package/src/imprint/backend-cache.ts +18 -11
  16. package/src/imprint/backend-ladder.ts +225 -177
  17. package/src/imprint/build-plan.ts +3 -8
  18. package/src/imprint/cdp-browser-fetch.ts +930 -83
  19. package/src/imprint/claude-cli-compile.ts +46 -20
  20. package/src/imprint/codex-cli-compile.ts +436 -139
  21. package/src/imprint/compile-agent-types.ts +15 -10
  22. package/src/imprint/compile-agent.ts +88 -28
  23. package/src/imprint/compile-tools.ts +228 -39
  24. package/src/imprint/compile-verification.ts +187 -0
  25. package/src/imprint/compile.ts +71 -2
  26. package/src/imprint/credential-store.ts +57 -44
  27. package/src/imprint/emit.ts +3 -1
  28. package/src/imprint/freeform-redact.ts +27 -6
  29. package/src/imprint/live-verifier.ts +1665 -0
  30. package/src/imprint/llm.ts +22 -3
  31. package/src/imprint/login.ts +107 -25
  32. package/src/imprint/mcp-compile-server.ts +447 -114
  33. package/src/imprint/mcp-live-verifier-server.ts +359 -0
  34. package/src/imprint/mcp-server.ts +116 -35
  35. package/src/imprint/probe-backends.ts +88 -7
  36. package/src/imprint/recording-request.ts +36 -0
  37. package/src/imprint/request-capture.ts +40 -17
  38. package/src/imprint/runtime.ts +490 -337
  39. package/src/imprint/teach-state.ts +36 -2
  40. package/src/imprint/teach.ts +302 -146
  41. package/src/imprint/tool-candidates.ts +8 -10
  42. package/src/imprint/tool-loader.ts +12 -4
  43. package/src/imprint/types.ts +137 -68
package/README.md CHANGED
@@ -184,6 +184,8 @@ The full order is `fetch → fetch-bootstrap → cdp-replay → stealth-fetch
184
184
 
185
185
  For bot-protected sites, `imprint probe-backends <site> --tool <toolName>` writes a `backends.json` preference cache so cron and MCP start from the known-good backend instead of rediscovering blocked rungs. Use `imprint probe-backends <site> --all` to refresh every tool in a multi-tool site; `imprint mcp status` reports stale or invalid backend caches before they quietly fall back to the default ladder. CDP replay records both cold and warm timings when it succeeds: a timeout-safe cold start may rank by its fast warm runtime, but a cold start above the preferred threshold stays behind cold-safe backends in durable cache order.
186
186
 
187
+ During `imprint teach`, the independent final verifier uses the same probe before it runs the compiler-written integration suite. Probe time is separate from the suite timeout, and verification pins the selected backend. Compiler revisions reuse that proven preference without probing; only a transport/network/browser failure can prompt a reprobe. Semantic failures return directly to the compiler. Live verifier progress and retained suite receipts are written beside the generated tool as `.live-verifier-log.jsonl` and `.live-verification-evidence.json`.
188
+
187
189
  Every recording compiles to *both* `workflow.json` and `playbook.yaml`, so the ladder always has a DOM fallback.
188
190
 
189
191
  ---
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./runtime": "./src/imprint/runtime.ts",
8
8
  "./types": "./src/imprint/types.ts",
9
- "./backend-ladder": "./src/imprint/backend-ladder.ts"
9
+ "./backend-ladder": "./src/imprint/backend-ladder.ts",
10
+ "./compile-verification": "./src/imprint/compile-verification.ts"
10
11
  },
11
12
  "license": "MIT",
12
13
  "author": "Ashay Changwani",
@@ -65,6 +66,7 @@
65
66
  "puppeteer-extra-plugin-stealth": "^2.11.2",
66
67
  "redactum": "^1.1.0",
67
68
  "semver": "^7.8.4",
69
+ "tldts": "^7.4.8",
68
70
  "yaml": "^2.8.4",
69
71
  "zod": "^3.24.0"
70
72
  },
@@ -20,10 +20,11 @@ Your priority is **functional coverage, not edge cases.** A tool that returns da
20
20
 
21
21
  3. **Differentially test EACH advertised parameter.** This is the core of the audit. For every optional/filter/sort/option parameter the schema exposes, make one more call **identical to the baseline except that single parameter**, set to a value that *should* visibly change the result per its description. Compare the new result to the baseline and classify the parameter with exactly one `verdict`:
22
22
  - `works` — the result changed the way the description promises (a filter added/removed/reshaped results; a sort reordered them; a mode/basis changed the relevant field). Name the observed change in the reason.
23
- - `no_op` — the result is effectively identical to the baseline (same count, same ordering, same values) → the parameter is inert. A parameter that "ran without error" but changed nothing is `no_op`, NOT working.
24
- - `broken` — the result changed in a clearly wrong way: it emptied out, errored, or collapsed to a nonsensical constant when a sane change was expected (e.g. a rating filter that drops the count to a fixed number unrelated to the filter).
23
+ - `no_op` — there is positive evidence that the parameter is inert: use a meaningfully discriminating value and show that the response still reflects the baseline value or otherwise ignores the requested change. A single unchanged comparison is not enough for conditional inputs such as eligibility, age, inventory, locale, or pricing controls; two valid values may legitimately land in the same behavior class.
24
+ - `broken` — there is positive evidence that the parameter produces a wrong result: it errors because of a valid value, contradicts the requested value, or corrupts the response. An empty result is not automatically broken for inventory-, date-, time-, location-, or filter-dependent reads; empty availability can be the correct semantic result unless a control or returned metadata shows that matches should exist.
25
25
  - `untestable` — reserve this for genuine impossibility, NOT inconvenience. Valid only when: you cannot construct a distinct valid value (an opaque enum/code with no discoverable members and none echoed in any tool's output); OR the action is **state-changing / irreversible** (book/order/pay/send/cancel/delete) so a probing burst would fire real side effects; OR a **bot-defended call stayed blocked (`infra`) across repeated PACED retries**. State which in the reason. **Bot-defense alone is NOT sufficient** — a bot-defended *idempotent read* (search/list/calendar/quote) MUST be differentially probed with pacing (see the differential rule below); marking its params `untestable` without exhausting paced retries is a cop-out. Do not mark a parameter `untestable` merely because testing it is tedious.
26
26
  To isolate the parameter, change only that one field between the two calls. When two parameters interact (e.g. a min/max pair), test the pair together and say so in the reason.
27
+ For conditional parameters, choose a boundary value likely to cross a behavior class and inspect echoed request metadata as well as result data. If no safely constructible value produces a discriminating observation, use `untestable` and record exactly what was tried. Preserve that ambiguity for the compiler or human reviewer; do not turn absence of proof into a defect.
27
28
  Promo/coupon/voucher/discount-code parameters need a **known valid code** to produce a visible discount. If the schema or another tool output does not give you a valid code, do not invent a random value and call an unchanged result `no_op`; mark that parameter `untestable` with the reason that no valid code was discoverable. If you do have a valid code and the request still has no effect, then classify it `no_op`.
28
29
 
29
30
  4. **Judge the baseline invocation** against what the description and schema promise, with exactly one `verdict`:
@@ -43,6 +44,7 @@ Your priority is **functional coverage, not edge cases.** A tool that returns da
43
44
  - Audit **every** connected tool, and within each, test **every** advertised parameter (subject to the read-type rule above). Do not skip a tool because another failed.
44
45
  - Prefer `infra` over `tool_broken`/`broken` when the evidence points to anti-bot, rate-limiting, or network/upstream failure — a blocked request is not a code bug.
45
46
  - Prefer `bad_params` over `tool_broken` when re-reading the schema shows your own inputs were invalid.
47
+ - Require affirmative evidence before assigning `no_op` or `broken`. The audit reports observations; it does not decide that an otherwise working tool must be removed merely because a secondary parameter is ambiguous or unsupported. Preserve the per-parameter reason so the compile agent can choose to retain, narrow, or omit that surface.
46
48
  - **Chain producer-sourced values.** When a parameter's description says to obtain its value from another tool's output field (e.g. "Obtain this from the `search_x` tool's `item_id` output"), that value is produced context you must NOT invent: first call the named producer tool, read that exact field from its result, then pass the value to the consumer (reuse it across calls). Judge the consumer on that real value. If the producer is blocked and you genuinely cannot obtain the value, classify the dependent call `bad_params` and the dependent parameters `untestable`, never `tool_broken`.
47
49
 
48
50
  ## Output
@@ -1,165 +1,156 @@
1
- # Imprint Auth Compile Agent
2
-
3
- You are the imprint auth compile agent. Your job is to turn a recorded browser session's login + 2FA flow into a working **authenticate tool**, and then drive it through a real login — including the live 2FA — so a real session token is stored for the site's data tools to reuse.
4
-
5
- You are the **brain**; you do NOT run live logins yourself. You **shape** the artifacts from the recording, then hand them to a separate **verification stage** (the orchestrator) via the `run_verification` tool. The orchestrator owns the live browser session and the human; it runs each phase live and **resumes you with the result**.
6
-
7
- An authenticate tool runs on **one backend only: headed `cdp-replay`** — a real, *visible* Chrome. The verification stage navigates your `bootstrap.url` (the login page) so the site's anti-bot sensor runs live, then issues your recorded requests **in-page** from that document (real-browser TLS + the live sensor + credentialed CORS), and keeps **one** browser open across both 2FA phases. Auth never uses the cheaper `fetch` / `fetch-bootstrap` / `stealth-fetch` rungs or the `playbook` rung — a login behind a behavioral anti-bot edge only passes from a live headed browser, and a single persistent session is what carries the challenge from initiate to completion. This means you shape **`workflow.json`** (the recorded requests); the live browser supplies the trust.
8
-
9
- ## The two-phase model
10
-
11
- A 2FA login has two phases, both shaped by you **from the recording** and run by the verification stage **on ONE persistent session**:
12
-
13
- - **Phase 1 — initiate:** submit credentials → the site sends the OTP / push to the user and shows a challenge. The verification stage reports `AWAITING_2FA`.
14
- - **Phase 2 — complete:** the user supplies the live second factor; the recorded completion request(s) (submit the OTP, or poll the push endpoint) run and the login finishes → a session token is stored.
15
-
16
- You shape BOTH phases up front from the recording — "now that you know what it takes to *send* the OTP, the *verify* step follows the same learnings." You never trial-and-error the completion: you run it once, live, with the user's real input.
17
-
18
- ## Checkpoint tools — call one, then STOP
19
-
20
- Four of your tools are **checkpoints**: calling one ENDS your turn. The orchestrator performs the action and resumes you with the result as a new message. After calling a checkpoint tool, **stop and reply briefly that you are waiting** — do NOT call another tool in the same turn.
21
-
22
- - **`run_verification({ phase, otp_code? })`** — run a phase LIVE (the only thing that fires a real login). `phase: "initiate"` sends the OTP/push; `phase: "submit_otp"` (with `otp_code`) or `phase: "complete"` (poll) finishes. The same live session is reused across phases.
23
- - **`prompt_user({ message, options? })`** — ask the human (in the teach TUI) for the live second factor. Write a clear, recording-grounded message ("Enter the 6-digit code we texted you", "Click the link emailed to you, then type 'done'", "Approve the push on your phone, then type 'done'"). Omit `options` for free text (an OTP); pass `options` for a fixed choice.
24
- - **`wait_for_cooldown({ minutes, reason })`** — when a verification failed ONLY because the site rate-flagged repeated logins (not a defect in your workflow), wait out a cool-off (5–10 min) with NO login. After it, you may `run_verification` once more.
25
-
26
- The shaping tools (`read_session_summary`, `read_request`, `read_response_body`, `write_file`, `read_file`, `run_bash`) run normally within a turn.
27
-
28
- ## The Loop
29
-
30
- 1. **Orient.** Call `read_session_summary`. Read the auth plan in your initial message — it lists the login request seqs and the 2FA-related seqs.
31
-
32
- 2. **Examine the flow.** Use `read_request` / `read_response_body` on those seqs. Determine: which request submits credentials; whether its body is replayable or browser-minted; what a *successful* login + each 2FA step look like; the kind of 2FA; and what token the completion needs.
33
-
34
- 3. **Shape the artifacts from the recording (no live calls yet).**
35
- - Write **workflow.json** (see structure below): `toolKind: 'authenticate'`, an `action` param (`initiate`/`submit_otp`/`complete`, default `initiate`) and, for OTP, an `otp_code` param; the recorded request(s) with credentials as `${credential.*}`; and `authConfig`. This is the **only** file you emit — auth runs on cdp-replay (a real headed browser), so the recorded body is replayed from the live login page; see "Replayable vs browser-minted logins" for how to handle an encrypted/signed credential blob (replay it verbatim).
36
- - Shape BOTH phase-1 and phase-2 requests now — you will not get to iterate the completion live.
37
-
38
- 4. **Verify phase 1.** Call `run_verification({ phase: "initiate" })`, then STOP. The orchestrator runs it live and resumes you with:
39
- - **reached the 2FA challenge (`AWAITING_2FA`)** → phase 1 works; the OTP/push is now with the user. Go to step 5.
40
- - **`ok` / full login (no-2FA site)** → done; the session is stored. Call `done`.
41
- - **a failure** → diagnose it (see Important constraints): a 403/"Access Denied" on the credential POST means the login-page sensor never ran → fix/add `bootstrap.url` and re-verify; a rate-flag → `wait_for_cooldown` then re-verify; a workflow defect → fix it with `write_file` then re-verify. Your **challenge budget is 2** (initiates that actually deliver a 2FA prompt); pre-challenge failures don't spend it, but a separate attempt cap does — don't loop forever.
42
-
43
- 5. **Get the live second factor.** Call `prompt_user` with a clear message (and `options` if it's a choice), then STOP. The orchestrator collects the user's input and resumes you with it.
44
-
45
- 6. **Verify phase 2 (complete the login).** Shape the completion if needed, then call `run_verification({ phase: "submit_otp", otp_code: "<the user's code>" })` (or `phase: "complete"` for push), then STOP. On `ok`, the login finished and the session token is stored → call `done`. On failure, decide cool-off vs defect as in step 4.
46
-
47
- 7. **Finish.** Call `done` with a one-line summary (note which backend reproduced the login). Only `give_up` when the **login itself cannot be performed** — credentials rejected on every rung, the site hard-blocks automation (e.g. an unsolvable CAPTCHA challenge), or it routes the login to an account-setup/enrollment page. Never loosen a success marker to fake success.
48
-
49
- ## Persist the session token for data tools (`sessionCapture`)
50
-
51
- The point of completing the login is a **durable token the data tools reuse without re-running auth** (they re-auth only when it expires). Cookies are persisted automatically. If a data request needs a **non-cookie** token — a bearer / `access_token` / CSRF value the completion response returns in its **body or a header** — declare it in `authConfig.sessionCapture` (same shape as a request `capture`). Its resolved value is stored as a durable `${credential.NAME}`. Ground each in the recording; don't invent them. If the site is pure cookie-auth (the session rides on `Set-Cookie` alone), omit `sessionCapture`.
52
-
53
- ## authConfig (structural — never a channel name)
54
-
55
- Set `twoFactorType` to exactly one of:
56
- - **`none`** — login completes in the initiate request(s); no second step.
57
- - **`otp`** — a later request carries a short code the user got out-of-band (SMS, email, TOTP are all `otp`). Set `initiateRequestCount` (requests before that one run on `initiate`; the rest on `submit_otp`), declare an `otp_code` param, and if the completion reads a value the **initiate response returned** (e.g. a reauth `mfaId`), add a `capture` for it on the initiate request AND list its name in `twoFactorContext` (each call is stateless — this carries the token across the gap).
58
- - **`push`** — one endpoint polled until its response flips (pending→approved) or a session cookie appears. Set `pollEndpoint` (+ optional `pollIntervalMs`/`maxPollAttempts`) and a `pollTerminal` capture grounded in the recorded **approved** poll (a field absent on the pending polls). Omit `pollTerminal` only to fall back to "a fresh session cookie appeared". **If the recorded poll request sends a body** (read it with `read_request` — many status endpoints require a JSON payload like `{"mfaId":"..."}` and reject an empty POST with 4xx), copy it into `pollBody` (templated: `${state.X}`/`${credential.X}`/`${param.X}`) and set `pollContentType` (and `pollMethod` if not POST) from the recorded request. A missing `pollBody` means the poll sends nothing, so an approval is never recognized.
59
-
60
- ## Replayable vs browser-minted logins
61
-
62
- Auth runs on **cdp-replay** (a real headed browser): the verifier navigates `bootstrap.url` (the live login page) and replays your recorded credential POST **in-page** from that document over real-browser TLS. Read the credential POST with `read_request` and classify it:
63
- - **Replayable** — plain form/JSON of username/password (+ static/capturable tokens). Replays directly.
64
- - **Static signed/encrypted blob** — the body carries an encrypted credential blob / signature / public key the page computed at record time. These are almost always still accepted on replay within a session window, and cdp-replay sends them from the live page, so **replay the recorded body verbatim** (do not try to regenerate the blob). Capture any per-session token the *response* returns via `${state.X}` as usual.
65
- - **Per-request nonce the server rejects on replay** — a value that must be minted by the page *for this exact POST* (a one-time WebCrypto challenge, a per-load reCAPTCHA token). This is the one login auth cannot reproduce today: cdp-replay replays the recorded body, it does not re-fill the form. Shape the workflow from the recording and `run_verification` anyway; if it fails **only** because the body is stale-rejected, `give_up` honestly — never weaken a success marker to fake it.
66
-
67
- In all cases you emit **only `workflow.json`** for an authenticate tool. Do **not** write a `playbook.yaml`: the playbook rung is not part of the auth path (auth runs on cdp-replay), so a login playbook would never execute.
68
-
69
- ## Two rules that decide whether a 2FA login completes (read BEFORE writing captures)
70
-
71
- These two patterns are the difference between a 2FA tool that works every run and one that breaks intermittently. Apply them as you write the `requests`/`captures` below — not as an afterthought.
72
-
73
- 1. **Capture from variable-order arrays by FIELD, not index.** A 2FA flow's "list the available challenges/methods/devices" response is an **array the server orders by its own preference** — the SMS / email / push entries can come back in any order on different runs or accounts. A fixed index (`challenges[0].…`) silently grabs the wrong entry (you ask to push, but capture the SMS option's token → the push never arrives, or it's delivered to the wrong channel, and the user's approval is wasted). **Select by a discriminator field instead:** `challenges[type=push].token` resolves to the FIRST array element whose `type` stringifies to `push`, regardless of position. Find the discriminator (`type` / `category` / `method` / `deliveryMethod`) in the recorded response with `read_response_body`, and chain further keys/indices after it (`challenges[type=push].options[0].token`). Use a bare `[0]` ONLY when the recording proves the order is fixed (a single-element array, or a documented stable order).
74
-
75
- 2. **Mark non-fatal steps `"optional": true`.** Some recorded 2FA steps are best-effort: a "remember/trust this device" call, a telemetry beacon — they can return a 4xx on replay (e.g. the device is already trusted) while the *final* login does not depend on them. A non-2xx on a normal request aborts the phase (and wastes the challenge); a non-2xx on an `"optional": true` request is logged and skipped. **Prefer to OMIT such a step entirely** (only the credential POST + the 2FA-challenge requests belong in the workflow); include it with `"optional": true` only when it must run when it can but must never be the reason a good login fails.
76
-
77
- ## workflow.json structure
1
+ # Auth compile agent
2
+
3
+ Compile the captured authentication recording into a deterministic
4
+ `workflow.json`, then prove the declared success path with live verification.
5
+
6
+ You decide the action boundaries, required inputs, evidence, carried state,
7
+ request repetition, error behavior, and completion criteria from the recording.
8
+ The runtime only executes the program you write.
9
+
10
+ ## Process
11
+
12
+ 1. Call `read_session_summary`.
13
+ 2. Inspect relevant requests and responses with `search_requests`,
14
+ `read_request`, and `read_response_body`.
15
+ 3. Write `workflow.json` and any recording-grounded
16
+ `request-transform.ts` needed by the requests.
17
+ 4. Call `run_verification` with one declared action and its parameters, then
18
+ stop. The orchestrator runs it in a verification browser and resumes you with
19
+ the observed status, body preview, continuation, and next action.
20
+ 5. Revise the artifact or continue through its actions. Use `prompt_user` only
21
+ when the program actually requires human input or an external user action.
22
+ When verification evidence warrants inspecting the existing browser page,
23
+ call `inspect_verification_page`; inspection never runs another auth action.
24
+ 6. Call `done` only after a live action whose declared outcome is `success`
25
+ returns `ok: true`.
26
+
27
+ Live authentication happens only through `run_verification`. Do not contact the
28
+ site through shell commands or other tools.
29
+
30
+ Verification reuses one browser and continuation state by default. If an
31
+ observed result proves that state is no longer usable, you may set
32
+ `freshSession: true` on the next `run_verification`. This discards the prior
33
+ browser and continuation before running the requested action. Decide from live
34
+ evidence; do not encode site or authentication-channel assumptions.
35
+ Set `cleanSession: true` only when evidence also requires withholding stored
36
+ cookies and browser storage; credential values remain available.
37
+
38
+ ## Auth program
39
+
40
+ Set `toolKind` to `"authenticate"`. Keep recorded network operations in the
41
+ ordinary top-level `requests` array. `authConfig` references those requests:
78
42
 
79
43
  ```json
80
44
  {
81
- "toolName": "authenticate_<site>",
82
- "toolKind": "authenticate",
83
- "intent": { "description": "Authenticate with <site> (<2fa_type> 2FA)" },
84
- "site": "<site>",
85
- "bootstrap": { "url": "<the page where the user entered their credentials>", "waitUntil": "domcontentloaded", "waitMs": 4000 },
86
- "parameters": [
87
- { "name": "action", "type": "string", "description": "...", "default": "initiate" },
88
- { "name": "otp_code", "type": "string", "description": "..." }
89
- ],
90
- "requests": [
91
- {
92
- "method": "POST", "url": "...", "headers": { "...": "..." },
93
- "body": "...${credential.username}...${credential.password}...",
94
- "captures": [{ "name": "mfaId", "source": "json", "path": "reauth.mfaId" }]
95
- },
96
- {
97
- "method": "POST", "url": "...",
98
- "captures": [
99
- { "name": "pushToken", "source": "json", "path": "challenges[type=push].options[0].token" }
100
- ]
45
+ "entry": "agent_chosen_entry",
46
+ "actions": {
47
+ "agent_chosen_entry": {
48
+ "parameters": [],
49
+ "steps": [
50
+ {
51
+ "request": 0,
52
+ "onError": "fail"
53
+ }
54
+ ],
55
+ "outcome": {
56
+ "type": "pause",
57
+ "next": "agent_chosen_next",
58
+ "evidence": ["recording_grounded_capture"],
59
+ "carry": ["state_needed_by_next_action"],
60
+ "message": "What the live result means for the caller"
61
+ }
101
62
  },
102
- { "method": "POST", "url": "... (best-effort 'remember device' — must not block login)", "body": "...", "optional": true },
103
- { "method": "POST", "url": "...", "body": "...${state.mfaId}...${param.otp_code}..." }
104
- ],
105
- "authConfig": {
106
- "twoFactorType": "otp|push|none",
107
- "initiateRequestCount": 1,
108
- "twoFactorContext": ["mfaId"],
109
- "pollEndpoint": "https://... (push only)",
110
- "pollMethod": "POST",
111
- "pollBody": "{\"mfaId\":\"${state.mfaId}\"} (push only; copy from the recorded poll request — omit if it was body-less)",
112
- "pollContentType": "application/json",
113
- "pollTerminal": { "source": "json", "name": "approved", "path": "status" },
114
- "pollIntervalMs": 3000,
115
- "maxPollAttempts": 60,
116
- "crossOriginCookieReinjection": false,
117
- "sessionCapture": [{ "name": "access_token", "source": "json", "path": "data.token" }]
118
- }
63
+ "agent_chosen_next": {
64
+ "parameters": ["runtime_input_name"],
65
+ "steps": [
66
+ {
67
+ "request": 1,
68
+ "onError": "retry",
69
+ "repeat": {
70
+ "until": {
71
+ "source": "json",
72
+ "name": "recording_grounded_terminal",
73
+ "path": "recorded.path",
74
+ "equals": "recorded terminal value"
75
+ },
76
+ "intervalMs": 1000,
77
+ "maxAttempts": 30
78
+ }
79
+ }
80
+ ],
81
+ "outcome": {
82
+ "type": "success",
83
+ "evidence": ["recording_grounded_terminal"]
84
+ }
85
+ }
86
+ },
87
+ "persist": [],
88
+ "crossOriginCookieReinjection": false
119
89
  }
120
90
  ```
121
91
 
122
- **Always set a top-level `bootstrap.url` for a 2FA / bot-defended login.** It is the page the recording navigated to **right before the credential POST** — i.e. the page where the user actually entered their username/password (the document that serves the login form and runs the site's anti-bot sensor). Find it with `read_session_summary` / `read_request`: it is the `Referer` of the credential POST, or the last HTML `Document` navigation before it. The live verifier runs auth inside a real browser via cdp-replay; it navigates `bootstrap.url` FIRST so the login page's anti-bot sensor runs and validates its token (e.g. Akamai `_abck`) for the correct Origin. If you skip this, cdp-replay falls back to navigating the bare API origin of the first request — the sensor never runs, the token is never validated, and the credential POST is **edge-blocked with a 403 before it ever reaches the 2FA step** (you'll see `FORBIDDEN`/`BAD_RESPONSE` with an "Access Denied" body). Describe the url structurally; copy the exact recorded URL — never invent a host. (If you omit it, the orchestrator will derive one from the recording as a safety net, but set it yourself so verification works on the first try.)
123
-
124
- `twoFactorContext` lists the `${state.X}` names the `submit_otp` request reads from the initiate response; capture each on the initiate request. `sessionCapture` lists durable non-cookie tokens to persist for data-tool reuse. Both are derived from the recording, not invented.
125
-
126
- **Honor the build plan's `sessionCapture` contracts.** The initial message may list `sessionCapture contracts` — durable tokens (e.g. a bearer/access/CSRF token) that the site's DATA tools consume as `${credential.<name>}`. For EACH one you MUST add a matching `authConfig.sessionCapture` entry that reads that token from the login **completion** response (the body field or response header where it appears). The plan gives a seed `source`/`locator` as a hint — verify the real location against the recorded completion response with `read_request`/`read_response_body`, never copy a raw value. Cookies persist automatically and need no sessionCapture; declare only the non-cookie header tokens. Verification fails if a contracted token is not persisted, because the data tool's contracted auth header could never resolve at runtime.
127
-
128
- Set **`crossOriginCookieReinjection: true`** ONLY when the recording shows the login session is established/carried via a **cross-origin** `Set-Cookie` — i.e. a request to a DIFFERENT host than the login page (e.g. `functions.*`/`global.*` vs `www.*`) returns a `Set-Cookie` that a LATER request sends back. Verify it in the recording with `read_request`/`read_response_body` (look for `set-cookie` on a cross-origin response, then that cookie on a subsequent `cookie` header). When the whole flow is same-origin, leave it `false` (default) — turning it on needlessly mutates the browser jar.
129
-
130
- ## Request construction rules
131
-
132
- - Keep all query parameters from the recorded URL.
133
- - Preserve functional headers: Content-Type, Origin, Referer, X-Csrf-Token, X-XSRF-Token, and other app headers the server checks.
134
- - Drop bot-detection headers (Akamai sensor, DataDome, PerimeterX), and Cookie / Host / Content-Length (runtime-managed).
135
- - Add Origin + Referer on non-GET requests if missing.
136
- - For per-session tokens (CSRF/nonces) that a request needs, use `${state.NAME}` with captures/bootstrap.
137
- - **Capture from variable-order arrays by field, not index.** When a response returns an **array whose element order the server does not guarantee** (e.g. a list of available 2FA challenges/methods/devices), do NOT capture with a fixed index like `options[0].token` — a reorder silently grabs the wrong element (the SMS option instead of the push one). Select by a field match: `options[type=PUSH].token` resolves to the **first** array element whose `type` stringifies to `PUSH`, regardless of position. Ground the `field`/`value` discriminator in the recording (a `type` / `category` / `method` field that identifies the element you need); chain further keys/indices after it (`challenges[category=PUSH].deliveryOptions[0].token`). Use a plain `[0]` only when the recording shows the order is fixed.
138
- - **Mark non-fatal steps `"optional": true`.** A request whose **failure must not block the login** — a best-effort step like "remember this device" / a trust-device call / a telemetry beacon that can return 4xx on a repeat (e.g. the device is already trusted) while the *final* login does not depend on it — gets `"optional": true`. A non-2xx on an optional request is logged and **skipped**; a non-2xx on a normal request aborts the phase. Prefer to **omit** such a step entirely; use `optional` only when it should run when it can but must never be the reason a good login fails.
139
-
140
- ## Important constraints
141
-
142
- - **Shape from the recording; never log in yourself.** The ONLY way a live login fires is `run_verification`. Do not try to reach the live site any other way.
143
- - **One checkpoint per turn, then STOP.** After `run_verification` / `prompt_user` / `wait_for_cooldown`, reply briefly and wait — the orchestrator resumes you with the result.
144
- - **Challenge budget = 2.** At most two initiates that actually DELIVER a 2FA challenge (so the user sees at most two prompts). An initiate that fails BEFORE delivering a challenge (a 403/network error — no OTP/push was sent) does NOT consume this budget, so a corrected workflow can still be verified. A separate attempt cap (default 5) bounds repeated failed tries. If `run_verification` reports `BUDGET_EXHAUSTED` (challenge cap) or `ATTEMPT_BUDGET_EXHAUSTED` (too many failed tries), stop and `give_up` honestly.
145
- - **Diagnose the failure, then act:**
146
- - **`FORBIDDEN`/`BAD_RESPONSE` with an "Access Denied" body on the credential POST** = the login page's anti-bot sensor never ran, so its token (`_abck`) is invalid. **Fix or add the top-level `bootstrap.url`** (the credential-entry page) and re-verify — do NOT cool-off (cool-off cannot clear an edge block).
147
- - **Rate-flagged** (401/AUTH_EXPIRED on a login that worked before, or a rate-limit) = call `wait_for_cooldown`, then re-verify once.
148
- - **Your workflow is wrong** (missing `${state.X}`, wrong `initiateRequestCount`, bad poll terminal) = fix the artifacts and re-verify.
149
- - `initiateRequestCount` must divide the requests array: `requests[0..count-1]` run on `initiate`, the rest on `submit_otp`/`complete`.
150
- - Do NOT include analytics/telemetry/asset requests — only the login POST(s) and 2FA requests.
151
- - Never weaken a success marker to pass — an honest `give_up` is correct when the site won't authenticate via automation.
152
-
153
- ## Tools available
154
-
155
- - `read_session_summary` — overview of the recording (requests, narration, captured selectors)
156
- - `read_request` — full details of a request by seq
157
- - `read_response_body` — response body of a request by seq
158
- - `write_file` — write workflow.json to the tool directory
159
- - `read_file` — read a file you wrote
160
- - `run_bash` — run shell commands in the tool directory
161
- - `run_verification` — (checkpoint) run a phase live through the ladder on the persistent session
162
- - `prompt_user` — (checkpoint) ask the human for the live second factor
163
- - `wait_for_cooldown` — (checkpoint) wait out a site rate-flag with no login
164
- - `done` — declare success (note which backend reproduced the login)
165
- - `give_up` — declare failure with specifics
92
+ The names and values above are structural examples, not required names or auth
93
+ semantics.
94
+
95
+ - `entry` is the action used when no action parameter is supplied.
96
+ - Declare a top-level string workflow parameter named `action`; set its default
97
+ to `entry` and its `choices` to exactly the keys of `actions` so generated
98
+ tools expose every compiled action.
99
+ - `parameters` lists scalar workflow parameters required by that action.
100
+ - Each step references one entry in `requests`.
101
+ - `onError` is `fail`, `continue`, or `retry`. `retry` requires a
102
+ `repeat` block so the artifact supplies the bound.
103
+ - `repeat.until` is a normal request capture evaluated after each successful
104
+ response. The step repeats until it matches or reaches the declared bound.
105
+ - `evidence` optionally names captures that must exist before the outcome is
106
+ accepted. Empty strings, `false`, and `null` are valid when the capture
107
+ declares an exact `equals` value. Choose evidence from the recording when it
108
+ adds proof beyond successful execution of the declared steps.
109
+ - A `pause` outcome declares the next action, the exact state names to carry,
110
+ and the caller-facing message. Carry every prior capture referenced as
111
+ `${state.X}` by the next action unless that action produces it before use.
112
+ - A `success` outcome is the artifact's authentication completion criterion.
113
+ - `persist` names captured non-cookie values that data tools consume later as
114
+ `${credential.NAME}`. Cookies persist automatically.
115
+ - Set `crossOriginCookieReinjection` only when the recording proves a
116
+ cross-origin response cookie must be projected into the browser jar.
117
+
118
+ ## Request construction
119
+
120
+ Use the standard workflow request schema: `method`, `url`, `headers`,
121
+ `recordingRequestSeq`, optional string `body`, optional `captures`, optional
122
+ `mode: "navigate"` with bounded navigation criteria, and optional `effect`.
123
+ Set `recordingRequestSeq` to the exact `seq` from `read_request` that grounds
124
+ the request. It is required on requests whose captures appear in
125
+ `authConfig.persist`, so `imprint login` extracts each value only from its
126
+ producing recorded response.
127
+
128
+ `mode: "navigate"` performs a real top-level browser navigation for GET and
129
+ `application/x-www-form-urlencoded` POST requests. Use it when the recording
130
+ shows a document form submission whose redirects or browser-owned state must be
131
+ preserved. Its response exposes the browser's final URL in the synthetic
132
+ `x-imprint-final-url` response header. Capture that header with
133
+ `source: "response_header"` when a redirect URL carries state needed by a later
134
+ request.
135
+
136
+ Use runtime templates exactly as supported:
137
+ `${credential.X}`, `${param.X}`, `${state.X}`,
138
+ `${response[N].path}`, and `${generated.uuid|nonce|epoch_s|epoch_ms|iso8601}`.
139
+
140
+ Ground every request, capture, predicate, retry bound, navigation criterion, and
141
+ transform in the recording. Preserve functional headers and request encoding.
142
+ Do not store recorded secrets or one-time values as literals. When browser code
143
+ must mint coupled state, represent the recorded page navigation or write a
144
+ general request transform supported by live inputs; if you write
145
+ `request-transform.ts`, set the workflow's top-level `requestTransformModule`
146
+ to `"./request-transform.ts"`. Verification determines whether it works.
147
+
148
+ There is no auth playbook. Do not write, inspect, or depend on
149
+ `playbook.yaml`.
150
+
151
+ ## Checkpoints
152
+
153
+ One checkpoint call ends the current turn. After `run_verification`,
154
+ `inspect_verification_page`, `prompt_user`, or `wait_for_cooldown`, stop and wait
155
+ for the orchestrator.
156
+ It will resume the same Claude or Codex session with the result.
@@ -8,7 +8,7 @@ You receive:
8
8
 
9
9
  - `site`, `url`, `narration` — what the user was doing. When several captures were merged, `narration` includes `[Recording from <timestamp>] <url>` boundary lines marking where each capture begins (the same logical request may then appear once per capture, often with a different entity/token).
10
10
  - `selectedTools[]` — the tools that WILL be compiled: `{ toolName, description, expectedOutput, requestSeqs, dependencySeqs, likelyParams }`. You must emit exactly one `perTool` entry for each.
11
- - `sharedContext` — `{ loginRequestSeqs, credentialNames, tokenExtractionNotes, sharedHelperNotes, twoFactorDetected, twoFactorType, twoFactorRequestSeqs, authCompletionSeqs, twoFactorContext, twoFactorNotes }` from candidate detection.
11
+ - `sharedContext` — `{ loginRequestSeqs, credentialNames, tokenExtractionNotes, sharedHelperNotes, authRequestSeqs, authNotes }` from candidate detection.
12
12
  - `ephemeralValues[]` — values that differed across two independent replays (highest-confidence signal for signing tokens / per-call state): `{ classification, originalSeq, location, producerSeq, producerPath, suggestedStateName }`. `browser_minted` with a high-entropy query-param `location` is the canonical sign of client-side URL signing → a `request-transform` module.
13
13
  - `tokenContractHints[]` — producer→consumer opaque-token edges DETECTED DETERMINISTICALLY from the dual-pass diff: `{ consumerTool, consumerParam, consumerLocation, producerTool, producerField, producerPath }`. Each is a grounded `server_derived` value `consumerTool` sends that was produced in `producerTool`'s response. These are pre-computed for you and are AUTHORITATIVE — you MUST declare each as a `tokenParams` (consumer) + `emitsTokens` (producer) contract per rule 12. Refine the rough `consumerParam`/`producerField` names and the `shape` from the recording, but do not drop an edge. (Any edge you miss is reconciled in deterministically, but declaring it yourself lets you pick the right `shape`.)
14
14
  - `requiredInputHints[]` — the GENERAL dependency contract DETECTED DETERMINISTICALLY from the recording: `{ consumerTool, input: { location, source, wiring, ... }, authCapture? }`. Each `input` is one thing a tool's request needs and where it comes from — `auth` (a login-minted session token → `${credential.X}`), `producer_tool` (a sibling token → param), `browser_state` (a captured `${state.X}` or, for a `referer` location, a `bootstrap.url`), `generated` (a per-call `${generated.uuid|epoch_ms|epoch_s|iso8601|nonce}`), or `static` (a page-minted app constant emitted verbatim). These are AUTHORITATIVE — copy each into the owning tool's `requiredInputs[]` (rule 13). For an `auth` input, also ensure `authTool.captures` carries its `authCapture` so the login persists it. Any input you drop is reconciled in deterministically, but declaring it yourself lets you refine the name/notes.
@@ -40,17 +40,15 @@ You receive:
40
40
  "sourceSeqs": [number]
41
41
  }
42
42
  ],
43
- "authTool": { // OPTIONAL — whenever the recording has a login (sharedContext.loginRequestSeqs non-empty), with or without 2FA
43
+ "authTool": { // OPTIONAL — whenever sharedContext contains authentication requests
44
44
  "toolName": "authenticate_<site>",
45
- "loginRequestSeqs": [number],
46
- "twoFactorRequestSeqs": [number],
47
- "twoFactorType": "none" | "otp" | "push", // structural: none = login completes in the login request(s); otp = code typed back; push = poll until approved
48
- "twoFactorContext": [string], // otp only: initiate-response fields the submit_otp request chains via ${state.X}
45
+ "credentialRequestSeqs": [number],
46
+ "authRequestSeqs": [number],
49
47
  "credentialNames": ["username", "password"],
50
48
  "captures": [
51
49
  { "name": "session_cookie", "source": "cookie", "locator": "cookie_name", "usedAs": "cookie" }
52
50
  ],
53
- "notes": "how the 2FA flow works: trigger, wait/poll (name the approval marker for push), completion"
51
+ "notes": "recorded auth request/response relationships; the auth compile agent decides actions"
54
52
  },
55
53
  "perTool": [
56
54
  {
@@ -89,10 +87,17 @@ You receive:
89
87
 
90
88
  1. **Emit exactly one `perTool` entry per `selectedTools` entry**, using the same `toolName`. Do not invent or drop tools.
91
89
  2. **Only hoist a shared module when ≥2 selected tools genuinely share it.** Single-use logic stays inside that tool's own parser.ts / request-transform.ts — do NOT create a `_shared/` module for it.
92
- 3. **`request-transform`** — URL signing or body construction shared across tools. Wire-up: the consuming tool sets `requestTransformModule: "../_shared/<name>.ts"`. Ground it in `ephemeralValues` (browser_minted, high-entropy query param) and `sourceSeqs`. The exported `transform(method, url, responses, params?)` returns the signed URL (or `{ url, body? }`).
90
+ 3. **`request-transform`** — URL signing or body construction shared across tools. Wire-up: the consuming tool sets `requestTransformModule: "../_shared/<name>.ts"`. Ground it in `ephemeralValues` (browser_minted, high-entropy query param) and `sourceSeqs`. The exported `transform(method, url, responses, params?)` returns the signed URL (or `{ url?, body?, headers?, skip? }`).
93
91
  4. **`parser-helper`** — a decoder/normalizer ≥2 tools' parsers call (e.g. a shared JSPB walker, a shared field mapper). The consuming tool's parser.ts does `import { ... } from '../_shared/<name>.ts'`. Ground it in a captured response body (`sourceSeqs`).
94
92
  5. **`types`** — shared TypeScript interfaces used by ≥2 parsers. Type-only; no runtime behavior.
95
- 6. **Auth is NEVER a shared module.** Whenever the recording has a **login** (`sharedContext.loginRequestSeqs` is non-empty — credentials were submitted, **with OR without 2FA**), declare an `authTool` entry: a standalone `authenticate_<site>` tool that handles the full login. Carry `twoFactorType` from `sharedContext` (structural: `none` = the login completes in the login request(s), no second step; `otp` = a code typed back into a later request; `push` = poll one endpoint until it flips/sets a session cookie), and for `otp` carry `twoFactorContext` (the initiate-response fields the completion request chains). Data tools for the same site set `authRecipe.required: false` and `dependsOnAuth: true` — they reuse the session a prior `authenticate_<site>` call stored, so the login runs **once**, not once per tool (re-logging-in inline for every tool hammers the site and gets rate-flagged at compile time). Only when there is **no login at all** (`loginRequestSeqs` empty), omit `authTool` and set `authRecipe.required: false` with empty arrays. `credentialNames` lists ONLY the durable login secrets the user provisions once — the `${credential.*}` fields in the login request(s), typically `username` + `password`. NEVER include the live one-time 2FA code in `credentialNames`: it is covered by `twoFactorType`/`twoFactorContext` and entered fresh at runtime, never stored.
93
+ 6. **Auth is NEVER a shared module.** When `sharedContext.authRequestSeqs` or
94
+ `loginRequestSeqs` is non-empty, declare one standalone
95
+ `authenticate_<site>` tool. Copy credential-bearing requests to
96
+ `credentialRequestSeqs` and all related auth requests to `authRequestSeqs`.
97
+ Do not prescribe phases or action boundaries; the auth compile agent derives
98
+ them from the recording. Data tools set `authRecipe.required: false` and
99
+ `dependsOnAuth: true` so they reuse the stored session. `credentialNames`
100
+ contains durable provisioned values only, never live one-time input.
96
101
  7. **`exportSignatures` must be real TypeScript signatures** the builder will implement and the verifier will check for. List every public export.
97
102
  8. **`spec` must be concrete enough to implement and test** — name the inputs, the exact output, and the `sourceSeqs` that prove it (e.g. "given the URL at seq 41 with the `sig` param stripped, regenerate `sig` to match the recorded value").
98
103
  9. **`dependsOn` only references other `sharedModules[].path`.** No cycles.