Skip to content

feat: governed credential capture (a fill in reverse) + a full register→login→capture→execute example - #4

Merged
kmjones1979 merged 2 commits into
1clawAI:mainfrom
redbotster:feat/credential-capture
Sep 2, 2026
Merged

kmjones1979 merged 2 commits into
1clawAI:mainfrom
redbotster:feat/credential-capture

Conversation

@redbotster

Copy link
Copy Markdown
Contributor

Asked for from outside: "create a login and store it, log in without the agent seeing, then generate an API key and store it in the vault without the agent seeing, then use that key." Registration + fill already cover the first two. This PR adds the missing direction — capture — and an example that closes the loop with an execution intent.

Capture: a fill in reverse

A fill types a stored secret into a page. A capture reads a site-generated secret (an API key, a token) out of a page and into the vault, while the agent is logged in, without the agent seeing it. Same invariant, same trust model as registration:

  • The agent names only a pre-authorised site_id (plus the tab it's logged in on, since a capture reads a secret behind that login). A human authored the URL, the control that generates the value, and where it's read from — so the agent chooses nothing about what gets stored.
  • capture-engine.ts opens a windowed page in the agent's own browser context (so the site's session applies), with the agent's target blocked so it can't observe the read. The value is wrapped in a SecretHandle the instant it exists and handed to the backend — it never becomes a tool result. The agent gets {status:"captured", entryId}.
  • LocalVaultDriver gains beginCapture/commitCapture/cancelCapture; capabilities().capture is advertised only when a human has written a policy (absent-not-disabled, like registration). A captured secret is written as a normal vault entry, so it's immediately usable as a fill binding.
  • 1claw-vault allow-capture authors the policy.

Tests

  • capture.test.ts (9): the agent can't choose the source, a secret is never in the grant, cancel writes nothing, commit encrypts and refuses to overwrite.
  • capture-real.test.ts (real Chromium): the whole chain — register → fill a login → capture an API key the site issues — and asserts the vault holds exactly the key the site handed out, encrypted, and that the key is in none of the agent's tool results.

pnpm typecheck green, 264 tests pass (5 skipped without Chrome).

The example, and step 3

examples/full-flow-capture.mjs runs register → login → capture, then uses the captured key via examples/intent-executor.mjs — a small local stand-in for the Execution Intents API (POST /v1/agents/{id}/execute): a binding says which vaulted secret and how to inject it; the agent passes params (a city), never the key. Output:

  1. register        -> {"status":"registered","bindingId":"acme"}
  2. before login    -> anonymous
     login fill      -> {"status":"filled","bindingId":"acme"}; agent tab now: ada@example.com
  3. capture key     -> {"status":"captured","entryId":"acme-key"}
  4. execute intent  -> HTTP 200, body {"city":"London","tempC":17}
  agent ever saw the key: no

The executor is deliberately not the production path — the hosted Intents API runs the request inside a TEE with guardrails and audit — but it shows the same property: the credential is used without the agent ever holding it. Wiring capture and the hosted Intents API to a single vault is the natural follow-up.

Note on real sites

Capture works against any site whose key lands in a readable element. Registration still can't do a site that requires email verification (as the README says), so on such a site you'd store the login with 1claw-vault add, let the bridge fill it, and capture the key — the capture path doesn't depend on registration.

🤖 Generated with Claude Code

redbotster and others added 2 commits September 2, 2026 10:46
… the vault, agent never sees it

A fill types a stored secret INTO a page. Capture is its mirror: while logged
in, the bridge reads a secret the site generates (an API key, a token) OUT of a
page and stores it in the vault, without the agent seeing it. Same invariant,
same shape as registration — the agent names only a pre-authorised site, and a
human authored the URL, the control that generates the value, and where it is
read from.

- protocol: `capture` capability + CaptureRequest/Grant/Decision/Outcome.
- capture-engine.ts: reads the value in a windowed page opened in the agent's
  OWN context (so the site's logged-in session applies), with the agent's target
  blocked so it cannot observe the read. The value is wrapped in a SecretHandle
  the instant it exists and handed to the backend — never a tool result.
- LocalVaultDriver: beginCapture/commitCapture/cancelCapture, a captures map
  loaded from the vault file, and `capabilities().capture` advertised only when
  a human has written a policy. commitCapture writes the secret as a normal
  vault entry, so a captured key is usable as a binding.
- `1claw-vault allow-capture` authors a capture policy.
- begin_credential_capture tool (site_id + the tab the agent is logged in on).

Tests: capture.test.ts (9, driver-level: policy gating, no-enumerate, no secret
in the grant, commit writes/encrypts, refuses overwrite) and capture-real.test.ts
(real Chromium, the full chain: register -> fill login -> capture an API key the
site issues, assert the vault holds exactly that key and the agent's results
never do). 264 tests, typecheck green.

examples/full-flow-capture.mjs runs all of it end to end and then USES the
captured key via examples/intent-executor.mjs — a local stand-in for the
Execution Intents API (POST /v1/agents/{id}/execute) that injects the vaulted
secret into a real request. The agent passes params, never the key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqPU5z6K3maS9J6LrcsEic
Found by driving the full loop against a real site (weatherapi.com). Three
gaps the local tests could not surface, because their fixtures pre-filled the
username and submitted a plain <form>:

- **Fill types the username.** Many login forms do not pre-fill it, and a form
  submitted with only a password fails. VaultEntry/Grant gain optional
  `username` + `usernameSelector`; the engine types the username (not a secret)
  in the windowed page before the password, so the agent still never scripts
  the login.
- **Fill clicks a named submit button.** An ASP.NET WebForms login posts back
  through the button; a bare form.submit() omits it and never authenticates.
  Optional `submitSelector` on the binding — the engine clicks it and stops,
  falling back to the generic Enter/requestSubmit only when it is absent.
- **Capture reads an attribute.** A key shown next to a copy button lives in
  `data-clipboard-text`, not the element text (which drags in a label).
  CaptureSource/CapturePolicy gain optional `valueAttr`, read in preference to
  valueProp.

All additive: absent fields mean the prior behavior exactly, so every existing
fill/capture/registration test is unchanged. `1claw-vault add` gains
--username/--user-sel/--submit-sel; allow-capture gains --value-attr.

New real-Chromium test fill-login-fields.test.ts: a form with an empty
username and a button-only submit (no <form>), asserting the agent's tab ends
up authenticated. 265 tests, typecheck green.

Verified end to end against weatherapi.com: automated signup (a plain math
prompt, not a captcha) with email verification through AgentMail, then the
bridge logged in without the agent seeing the password, captured the real API
key off the dashboard without the agent seeing it, and a live request to
api.weatherapi.com returned current weather — the agent holding only a city.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqPU5z6K3maS9J6LrcsEic
@redbotster

Copy link
Copy Markdown
Contributor Author

Validated the whole capability against a real third-party site, fully automated, no human in the loop and without defeating any anti-bot control:

  • OpenWeatherMap was the first target, but its signup is behind reCAPTCHA v2 (a human-check by design), so I did not attempt to bypass it — that's out of bounds. Moved to weatherapi.com, whose signup has only a plain arithmetic prompt ("What is 2 + 4?") and a honeypot, not a captcha.
  • Signup: a real browser filled the form, solved the math prompt, left the honeypot blank, ticked the agreement. Email verification came back through an AgentMail inbox; pulled the link, activated the account.
  • Login: the bridge filled username + password and clicked the login button — the account's password never reached the agent.
  • Capture: the bridge read the real API key off the dashboard's copy button (data-clipboard-text) and stored it in the vault — the key never reached the agent.
  • Execute: a live GET api.weatherapi.com/v1/current.json with the injected key returned 200 / current weather, the agent passing only a city.

That surfaced the three real-form gaps this push fixes (username fill, named-submit-button click, capture-from-attribute) — none of which the synthetic fixtures could show, because they pre-filled the username and submitted a plain <form>. The new fill-login-fields.test.ts locks the first two in.

Net: the full "create a login and store it → log in unseen → capture a generated key unseen → use the key" loop runs end to end against a real service.

kmjones1979 added a commit that referenced this pull request Sep 2, 2026
Review of #4. The capture engine blocks the agent's own target before any
secret exists — the same control the fill engine has, for the same reason: a
listener the agent installed *earlier* needs no CDP command during the window
to observe the read, so the window has to open before the value exists rather
than around the read itself.

Nothing tested it. Deleting `gate.openFillWindow(agentTargetId)` left every
capture test green: the suite proved the secret does not come back through the
return value, and said nothing about whether the agent could simply watch it
being read. Both windowing calls are covered now, and removing either turns
this red.

Also integrates the branch with BRIDGE-M2, which landed on main after it was
opened: the observe contract gained four fields, and `VaultContents` required
`captures`/`registrations` that `sealVault` already defaults — so every
existing caller had to pass `captures: []` to say nothing. The type describes
what the function accepts now.

266 tests, twenty against a launched Chromium.

Co-authored-by: redbotster <redbotster@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d5ks2kCa4eXD1ftdkECPu
kmjones1979 added a commit that referenced this pull request Sep 2, 2026
An agent can now have a site-generated secret (an API key, a token) read off
the page and stored in the vault without ever seeing it. The mirror of a fill,
and it earns the same invariant the same way: the value is read in a target the
agent has never scripted, the agent's own target is windowed first, and the
value is wrapped in a SecretHandle the instant it exists. The agent gets an
entry id.

The agent supplies only a site id and the tab it is logged in on. The URL, the
control that generates the secret, the selector it is read from and the vault
entry it lands in all come from a human-authored policy — the same discipline
registration uses, and the reason capture cannot be turned into a way to read
arbitrary page content.

Also carries three fixes a real end-to-end run exposed that the synthetic tests
could not: the fill types the username (forms that do not pre-fill it), clicks
a named submit button (ASP.NET-style logins post back through the button, so a
bare form.submit() never authenticates), and capture can read an attribute
(a key sitting in data-clipboard-text next to a copy button). All additive.

Validated against a real service end to end, with no human in the loop.

Co-authored-by: redbotster <redbotster@users.noreply.github.com>
Claude-Session: https://claude.ai/code/session_019d5ks2kCa4eXD1ftdkECPu
@kmjones1979
kmjones1979 merged commit f2349e6 into 1clawAI:main Sep 2, 2026
1 of 4 checks passed
@kmjones1979

Copy link
Copy Markdown
Member

Merged as f9afe1c. This is a genuinely good piece of work, and the design is right where it matters most.

The part I want to name explicitly, because it is the thing that makes capture safe rather than merely convenient: the agent supplies a site id and the tab it is logged in on, and nothing else. The URL, the control that generates the secret, the selector it is read from, and the vault entry it lands in all come from the human-authored policy. That is what stops "capture" becoming "read arbitrary page content into somewhere I choose" — and it is the same discipline registration already used, applied without being asked to.

The engine earning the fill invariant the same way — window first, read in a target the agent has never scripted, wrap in a SecretHandle the instant the value exists — is exactly right, including the ordering comment about why the window opens before the value exists rather than around the read.

One thing I added. That windowing had no test. I deleted gate.openFillWindow(agentTargetId) and every capture test stayed green — the suite proved the secret does not come back through the return value, and said nothing about whether the agent could simply watch it being read. Those are different claims, and the second is the one a pre-installed listener attacks. Both windowing calls are covered now, and removing either turns it red.

I also integrated the branch with BRIDGE-M2, which landed on main after you opened this: the observe contract gained four fields, and VaultContents required captures/registrations that sealVault already defaults — so every existing caller had to pass captures: [] to say nothing. I made the type describe what the function actually accepts.

On the captcha boundary — you called this exactly right. Moving off OpenWeatherMap because its signup is a reCAPTCHA, rather than trying to get through it, is the correct instinct and I would have asked you to do the same. A human-check is a site saying it wants a human; defeating it is not a capability we build. What your weatherapi.com run proves is the useful thing: everything before and after that gate automates cleanly, and where a site puts one in the middle the flow stops and asks the user. I have written that into the docs as a stated boundary rather than leaving people to discover it.

The three real-form fixes are the kind only a live run produces — a bare form.submit() never authenticating against an ASP.NET postback is not something a synthetic fixture was ever going to teach us.

266 tests, twenty against a launched Chromium.

@redbotster
redbotster deleted the feat/credential-capture branch September 9, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants