Repository navigation
Add credential fill, vaults, TOTP and 1Password tokens, with LLM tools - #131
Merged
Merged
Conversation
AHarmlessPyro
marked this pull request as ready for review
October 14, 2026 00:46
NikhilShahi
approved these changes
Oct 14, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 80e2b69. Configure here.
…l tool
Website logins are stored as typed secrets and filled into browser
sessions without the caller or a model seeing the values.
Secrets:
- Secret gains type ("secret" | "credential") and metadata
({ browser: { allowedOrigins } }). create accepts type and metadata, and
list filters by type.
- Credential values are objects of non-empty strings with keys matching
^[a-zA-Z0-9_.-]{1,64}$, at most 32 KiB as UTF-8 JSON. They are checked
locally (invalid_secret_value) before the key is fetched or anything is
created, and sealed as JSON to the encryption context the API returns.
- create and addVersion require an object for credentials and a string or
bytes for plain secrets; addVersion checks against the resolved type.
- update sends metadata with the API's merge semantics (null removes a
member).
- New createCredential({ name, value, allowedOrigins, description? }) and
setAllowedOrigins(secret, origins | null).
Sessions:
- CreateSessionParams.credentials and SessionDetail.credentials.
- sessions.credentials.fill(sessionId, { credential, fields, pageId?,
frame? }) posts to /session/:id/credentials/fill with a request timeout
of at least 30 s (the server's own is about 18 s). Requests the API
would reject fail locally with invalid_request. Errors keep the
server's code and retryable flag, and expose the body's details (such
as details.pages for CREDENTIAL_PAGE_AMBIGUOUS) as error.details.
- credential_filled and credential_fill_refused event log types.
Tools (@hyperbrowser/sdk/tools):
- FILL_CREDENTIAL_SCHEMA (OpenAI strict compatible) with OpenAI,
Anthropic and Gemini definitions, and CredentialFillTool whose runnable
takes the session ID from the application, maps the model's snake_case
input to the API, and returns the result or the error as JSON text.
- A gemini module with its own FunctionDeclaration type and a converter
from the SDK's JSON Schemas to Gemini's schema form (nullable instead of
type arrays, no additionalProperties).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover creating credentials, attaching them at session create, the fill-then-submit flow with Playwright and Puppeteer, outcomes, choosing a page with pageId, error codes, and tool loops for OpenAI, Anthropic and Gemini using CredentialFillTool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sessions.credentials.fill no longer replaces HyperbrowserError.details: as elsewhere in the SDK it is the whole error body, so the candidate tabs for CREDENTIAL_PAGE_AMBIGUOUS are at error.details.details.pages. CredentialFillTool's error JSON still gives the model the server's own details object (null when the body has none). - Every tool class has a static geminiToolDefinition, built from its OpenAI definition with toGeminiFunctionDeclaration and exported as <NAME>_TOOL_GEMINI. All existing schemas convert without changes to the converter. - README: the details path, Gemini definitions on all tools, and claude-sonnet-5-5 in the Anthropic sample. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
The API no longer has credential secrets with JSON-object values: any
secret with allowed origins can be filled, and a vault groups a login's
secrets under entry names.
- sessions.create takes credentials as secret names, { secret } or
{ vault }; the session lists the attached secrets.
- sessions.credentials.fill takes credential (one secret) or vault, and
fields as { selector } or, with a vault, { selector, field } naming the
entry. pageUrl picks a tab by URL prefix (not with pageId), and
frameUrl replaces frame.url. All of it is checked locally first:
exactly one of credential and vault, field iff vault, unique
selectors, 1-16 fields, and the API's length limits.
- Results report per input the selector, outcome, secretId, version, the
vault field and, for TOTP inputs, expiresAt; version is top-level only
for single-secret fills. not_populated is gone.
- fill_credential lets the model pass credential or vault, a nullable
field per input, and page_url next to page_id and frame_url; every
property stays required for OpenAI strict mode. The description covers
vaults, TOTP entries and the retry with page_id or page_url.
- Environment variable names for secrets are limited to 256 characters,
as the API stores them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- secrets.create takes allowedOrigins (sent as metadata.browser) and a type of "secret", "totp" or "op_token". createCredential, CredentialValue and object values are gone. - Values are checked per type before anything is created: every type gets the common rules, and a TOTP value must be a base32 seed or an otpauth://totp/ URI with a valid secret, algorithm, digits and period, read the way vm-service reads it. Seeds under 10 bytes are accepted with a process warning. op_token values are not checked for ops_. - An op_token with allowedOrigins is refused locally. - addVersion applies the rules of the resolved secret's type; types the SDK doesn't know get the common rules only. - Secret.type is a string so later types are read, and list passes any type filter through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- list({ page, limit, search }), get, create, update and delete, taking
a vault's ID or exact name like secrets do.
- create takes a source ({ type: "stored" } by default, or
{ type: "1password", tokenSecret, vault }) and entries: { name,
version? } for stored vaults, { item, section?, field } for 1Password.
update takes description, the 1Password source settings and entries,
where null removes one.
- Entry names, secret names, entry kinds for the source, 1Password names
(no '/' or '?', control or format characters, '.' or '..'; a field may
end with a query), the 512-byte op:// reference and the 32-entry limit
are checked locally.
- A vault without source, as APIs before 1Password vaults send it, reads
as { type: "stored" }; unknown source types pass through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- sandboxes.create (image launches) and process exec/start take vault, a vault name, next to secrets; a vault alone starts the process through the control API. A create with a vault whose result is unknown is reported like one with secrets, never retried. - Bindings (process.secrets and the new SandboxDetail.startupSecrets) make name, secretId and version optional and add secretType (TOTP), reference and tokenSecret (1Password), source and overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- The credentials section becomes secrets, vaults and credential fill: a login as secrets with allowed origins grouped in a vault, a TOTP entry, fill by vault, and the tab rules with pageUrl. - Vaults and 1Password: an op_token secret and a 1Password vault. - An error table covering fill, TOTP, 1Password and vault codes. - Sandboxes: vault on exec and create, TOTP variables and the binding fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- Base32 seeds whose length after removing spaces, hyphens and trailing '=' is 1, 3 or 6 past a multiple of 8 are refused: vm-service would silently drop those characters and type wrong codes. - otpauth URIs with control characters, HOTP URIs (with their own message) and other URI schemes are refused. - Short seeds are accepted without a process warning; the README and JSDoc note them instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API reports an entry of the wrong kind for a vault's source as VAULT_ENTRY_INVALID; VAULT_SOURCE_INVALID is for a source change on a stored vault and for a 1Password vault used in a browser session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
- The TOTP check mirrors Go: one leading BOM is stripped, then Go's IsSpace set (including U+0085, but not U+FEFF) is trimmed. otpauth URIs are split as url.Parse does: a '%' that doesn't start a two-digit hex escape is refused outside the query, and query parameters are read as url.ParseQuery reads them, skipping pairs with bad escapes. Seeds must be ASCII base32 before uppercasing, so characters like "ß" or "ff" no longer become valid. The SDK stays stricter than vm-service on seeds with impossible lengths, newlines, or "ı". - fill_credential treats "" like null for credential, vault, field, page_id, page_url and frame_url, as Gemini sends "" for unused strings. - A sandbox handle keeps startupSecrets when it refreshes its details, such as on token renewal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API (#1552) no longer links vaults to 1Password; sandbox create and
process start take onePassword next to vault and secrets instead.
- Vaults are stored only again: no source in params or responses (a stray
one still parses), entries are { name, version? }, and update needs a
description or entries. The 1Password entry and source types are gone.
- sandboxes.create (image launches), processes.start and processes.exec
take onePassword: { token, vault, fields }. It is checked locally before
any request: the token name, each part's length and pattern, the 512-byte
op:// reference, 1-32 fields with env names, unknown members, and a
variable also set by secrets or env. A vault clash is left to the API.
- Bindings are a stored-secret shape with source vault or secrets, or a
1Password shape { env, reference, tokenSecret, source: onePassword }.
- README: a 1Password section for onePassword, stored-only vaults, and the
error table without VAULT_ENTRY_INVALID and VAULT_SOURCE_INVALID.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Sessions attach an op_token with credentials: [{ onePassword: name }], and
sessions.credentials.fill takes onePassword: { token, vault, allowedOrigins }
with a ref ({ item, section?, field }) per input. Each field result carries
the op:// reference read; the result has no top-level version.
Fill requests are checked locally as the API checks them: one source, ref on
every field with onePassword and on none without, no field with onePassword,
the 1Password part rules and 512-byte references (shared with sandbox
onePassword), and unknown members. Allowed origins, for fills and secrets,
are now checked by the API's rules (https except localhost, no path, *. only
on a registrable domain), and session credentials by name and shape.
fill_credential stays without onePassword: a 1Password fill names its own
allowed origins, and a model shouldn't choose them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
#1573's vm-service drops CR and LF from a seed with its spaces and hyphens before the length check (its base32 decoder skips them), so a seed pasted across lines is valid. The clients refused it; they now drop them too. A raw line break inside an otpauth URI is still refused, as Go's url.Parse does. Seeds are also upper-cased one character at a time, as Go does, so a dotless "ı" reads as "I" like on the platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API trims a vault description before limiting it to 1024 characters, so a longer one with surrounding whitespace was refused here but accepted there. The README also now says that an iframe fill doesn't narrow tabs by origin, and that a malformed 1Password token fails with SECRET_ONEPASSWORD_AUTH_FAILED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API has sent tab_opened events since browserctrl #559. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
AHarmlessPyro
force-pushed
the
credentials
branch
from
October 15, 2026 01:32
1c8c2ce to
9d92768
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AHarmlessPyro
force-pushed
the
credentials
branch
from
October 15, 2026 18:11
5acb48d to
6899a11
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Adds browser credential fill, vaults and typed secrets to the SDK, plus
fill_credentialtool definitions for OpenAI, Anthropic and Gemini. It matches browserctrl master at 08c5d5232: #1572 (fill on plain secrets), #1551 (vaults), #1552 (op_token, per-request 1Password fields and 1Password browser fill) and #1573 (totp).The
credentialsecret type from #1537 is gone. A login is plain secrets that have allowed origins, usually grouped in a vault (USERNAME,PASSWORD, and optionallyOTPfrom atotpsecret). Values are encrypted locally and typed into pages by the platform, so neither the caller's code nor an LLM ever sees them.API
Secrets
client.secrets.create({ name, value?, description?, type?: "secret" | "totp" | "op_token", allowedOrigins? })totp: a base32 seed or anotpauth://totp/URI. The parsing mirrors vm-service'sparseTOTPSeed: spaces, hyphens and line breaks are ignored,=padding is allowed only at the end, and lengths 1, 3 or 6 past a multiple of 8 are refused, as vm-service also does since #1573. In a URI,algorithm,digitsandperiodare range-checked, and HOTP is refused.op_token: a 1Password service account token, named byonePassword.token. It takes no allowed origins.encryptionContextthe API returns.addVersion(secret, value)checks the value against the secret's stored type.setAllowedOrigins(secret, origins | null),update(secret, { description?, metadata? })andlist({ type }).Secret.typeis a plain string, so types added later still parse.createCredentialand object values are removed.Vaults (new
client.vaults, by ID or exact name)list,get,delete.create({ name, description?, entries? })andupdate(vault, { description?, entries?: { ENV: { name, version? } | null } }), wherenullremoves an entry. Vaults hold stored secrets only, as on master.Sandboxes
vaultonsandboxes.create(image launches) and on processexec/start.secretswins on a clash.onePassword: { token, vault, fields: { ENV: { item, section?, field } } }on the same calls reads fields of a 1Password vault when the process starts.startupSecrets, processsecrets) are either stored ({ env, name, secretId, version, secretType?, source: vault | secrets, overrides? }) or 1Password ({ env, reference, tokenSecret, source: { type: "onePassword" } }).startupSecretsacross refreshes.Sessions
credentials?: Array<string | { secret } | { vault } | { onePassword }>on create.{ onePassword: "op-token" }attaches an op_token so fills can read 1Password fields. The response lists the attached secrets.client.sessions.credentials.fill(sessionId, { credential? | vault?, fields: [{ selector, field? }], pageId? | pageUrl?, frameUrl? }){ version?, pageId, origin, fields: [{ selector, outcome, secretId, version, field?, expiresAt? }] }.codeandretryable.fill(sessionId, { onePassword: { token, vault, allowedOrigins }, fields: [{ selector, ref: { item, section?, field } }] }). Each field result gainsreference, and there's no top-levelversion. One source,refon every field, origins, part rules and 512-byte references are checked locally.Tools:
CredentialFillTool/fill_credential(OpenAI strict, Anthropic, Gemini)credentialorvault,fields[{selector, field}], and optionallypage_id,page_urlandframe_url. The application binds the session.filldirectly.nulland""count as absent.README: the secrets, vaults and credential fill section is rewritten. It covers
onePassword, error codes and the tool loops.Validation split: the SDK checks request shapes, secret values (UTF-8, size, no NUL) and TOTP seeds locally, since the API can't see an encrypted value. Policy rules (allowed-origin syntax, 1Password names and the 512-byte reference, field and entry limits, source clashes) are left to the API, which returns its status, code,
retryableflag and details unchanged. Seedocs/credentials-cleanup.md.Version
The package is bumped to 1.3.1 (published: 1.2.2).
Validation
mainand the validation cleanup,yarn testpasses 837 tests. Lint, typecheck and build are clean.frameUrldoesn't narrow tabs by allowed origin;SECRET_ONEPASSWORD_AUTH_FAILEDalso covers a malformed token.[{ vault }];expiresAt);pageId, and origin refusal;SECRET_IN_USE;parseTOTPSeed(including its line-break fix, 456995310) on 143 edge cases, alongside python-sdk, hx and the dashboard: no differences.onePasswordon sandbox create (the startup command),execandprocesses.start; the values matched by hash;Needs browserctrl #1552 and #1573 (both merged) to be deployed for
op_token,onePasswordandtotp.🤖 Generated with Claude Code
https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v