Skip to content

fix(providers): refuse lease allocation when the daemon holds other provider credentials - #3207

Merged
thymikee merged 3 commits into
mainfrom
claude/provider-credential-fingerprint
Oct 5, 2026
Merged

thymikee merged 3 commits into
mainfrom
claude/provider-credential-fingerprint

Conversation

@thymikee

@thymikee thymikee commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Summary

A local daemon keeps its startup env while connect checks the shell's, so a stale daemon could act on old credentials, such as creating a billed Limrun instance for an attach-mode shell. Lease allocation now refuses that before anything is created:

Error (INVALID_ARGS): The running daemon holds different limrun credentials than this shell.
hint: Stop it with agent-device daemon stop --state-dir '<dir>', then rerun the command …
  • On a local lease_allocate, the CLI sends a versioned hash of the values each provider's own reader uses (Limrun, BrowserStack), never the values themselves. It goes over the socket and local HTTP. The daemon compares it with the hash of its startup env.
  • Boundaries: the check runs only on new allocations; leases already held are not re-checked. No hash is sent from a shell without credentials, or to remote, proxy or cloud daemons. A daemon with an HTTP auth hook treats every caller as remote and skips the check. AWS Device Farm is excluded.
  • The CLI loads the fingerprint module only for lease_allocate.

30 files, 814 gross lines; help and docs updated. An empty BrowserStack variable now counts as missing.

Validation

At 3fccd8615c, rebased on main after #3208 and #3209: pnpm check:affected --run passes (5,044 related tests, eager-closure budgets, daemon-wire-compat). Remaining minor: changing only LIMRUN_REGION, or the other platform's variables, also refuses (fails closed, as documented). No live run with real keys.

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.07 MB 5.07 MB +2.8 kB
Package (unpacked) 5.07 MB 5.07 MB +2.8 kB
Package (download) 1.52 MB 1.52 MB +1.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.8 ms 27.0 ms +0.2 ms
CLI --help 83.5 ms 85.6 ms +2.1 ms

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-05 14:11 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 27 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/provider-credential-fingerprint.ts Outdated
Comment thread src/daemon/server/http-server.ts
Comment thread website/docs/docs/browserstack.md Outdated
Comment thread src/provider-credential-fingerprint.ts Outdated
Comment thread src/provider-credential-fingerprint.ts Outdated
Comment thread website/docs/docs/limrun.md Outdated
Comment thread src/commands/schema/cli-help.ts Outdated
Comment thread src/daemon/handlers/lease.ts Outdated
Comment thread test/wire-compat/ledger.json Outdated
@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

Reviewed 27dd43a. The refusal logic itself looks right, but the Coverage check fails on this diff, so it needs a change before merge.

The eager-closure test in scripts/__tests__/eager-closure-budgets.test.ts fails on two edges this PR adds. providers.ts now imports AppError, which grows the providers subpath from 1 to 3 modules. src/cli.ts now reaches provider-limrun-credentials.ts through daemon-client.ts and the fingerprint module, which takes it from 295 to 297. Please make the gate pass without raising its budgets. The rule: providers.ts stays a leaf that only holds BROWSERSTACK_CREDENTIAL_VARIABLES, and the CLI loads the fingerprint module only for lease_allocate. Moving requireBrowserStackCredentials next to its consumers in provider-definitions.ts and loading the fingerprint lazily in sendToDaemon would do it. The Coverage run should then pass.

Not blocking: no test pins that the daemon digests its own startup env at daemon-runtime.ts:450, so reading {} there would still type-check. One startDaemonRuntime-level assertion would cover it. You can take or leave this.

Could the BrowserStack reader and its trimming rule live in one function that both the consumer and the fingerprint call? That would close the trim thread by construction and keep providers.ts a leaf. I also looked for a smaller owner than the per-request field. Putting the digest in daemon.json and checking it on the stale-daemon path would decide before any RPC. It writes a credential digest to disk and needs a restart policy, so I do not think it is smaller.

On the open threads, two P1s from cubic-dev-ai still apply: trim vs raw key mismatch and auth-hook strip on a local daemon. One P2 still applies: constructor provider lookup throws. Four P3 threads still apply. You can resolve two P2s: remote daemon wording does not apply because the help text says these providers only use local profiles, and fingerprint as authentication does not apply because it is only a staleness check and a caller that sends none already proceeds with the daemon's credentials.

I did not run the eager-closure test locally. I took the failure from the CI log and the diff's import edges. There was no live run with real Limrun or BrowserStack keys. The refusal fires before any provider call, and the socket and HTTP sendToDaemon test covers the client-to-daemon route, so I did not require one. The end-to-end connect limrun plus open refusal under a stale daemon is still unobserved. I also did not check whether a Limrun runtime registers in a daemon started without credentials, which decides whether the "no credentials" refusal or the provider-not-available refusal fires first.

No conflicts. Before merge, the eager-closure gate must pass, and the two held P1 threads need an answer. For the second, either cover the local auth-hook HTTP case or document it.

@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

Addressed in a0ac012. pnpm check:affected --run passes locally, including eager-closure-budgets.

  • Eager closure: providers.ts is a leaf again. It holds the BrowserStack variable names and a non-throwing readBrowserStackCredentials, with no imports. requireBrowserStackCredentials moved into provider-definitions.ts and is exported from the package index, which the CLI callers already load. sendToDaemon imports the fingerprint module only for a local lease_allocate, so src/cli.ts is back at its merge-base count.
  • Trim P1: closed by construction, as you suggested. The fingerprint hashes the values each provider's reader returns: BrowserStack's single reader, and Limrun's trimmed values through readLimrunCredentialValues.
  • Auth-hook P1: documented. An auth-hook daemon already treats every HTTP caller as remote (it strips developerDir and refuses path installs) and runs on its operator's credentials, so it skips the comparison. The strip comment, the help rule and the ledger rationale say so.
  • constructor P2: the provider lookup is now a Map.
  • P3s: fixed the docs wording, "rerun the command", the refusal text for a daemon without credentials (it now records none), and the ledger rationale. I replied to and resolved the two P2s you marked as not applicable.
  • Daemon env wiring: a new test in daemon-runtime-interactor-composition.test.ts boots a daemon with BrowserStack credentials and sends lease allocations over HTTP. A mismatch is refused and a match is not. It fails if the daemon reads {}.
  • Reachability: a Limrun daemon without credentials doesn't register the runtime, so the provider-not-available refusal fires first. The "no credentials" lease test therefore uses BrowserStack, which always registers.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 17 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/daemon/server/daemon-runtime-interactor-composition.test.ts
@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

The earlier findings at 27dd43a are fixed at a0ac012. The Coverage job that failed then now passes, because the change removes the providers.ts AppError edge and the static fingerprint import in daemon-client.ts. All 21 checks pass at this head, and I found no code problems in the delta. There are no conflicts.

Not blocking: verifyBrowserStack in src/cli/connection/connect-provider-adapters.ts:140 now calls requireBrowserStackCredentials. A profile missing a BrowserStack credential now fails with INVALID_ARGS and no reconnect hint, where it used to fail with COMMAND_FAILED and the hint to reconnect. An empty value is now refused too. You can note this in the PR body or rethrow the old error shape, or leave it as is.

On the open threads: the Cubic P2 about the test spreading process.env still applies (#3207 (comment)). Add AGENT_DEVICE_HTTP_AUTH_HOOK: '' to the env in daemon-runtime-interactor-composition.test.ts so a runner with the hook set does not strip the fingerprint and fail the test. These threads do not apply and you can resolve them. They are fixed at this head: #3207 (comment) (shared credential reader), #3207 (comment) (map lookup), #3207 (comment) (docs wording), #3207 (comment) (help text), #3207 (comment) (missing-credentials message) and #3207 (comment) (ledger rationale). These are benign by design: #3207 (comment) (stripping is now documented contract), #3207 (comment) (these providers use local profiles only) and #3207 (comment) (the fingerprint is a staleness check, not authentication).

I did not run the eager-closure budget test locally. That result rests on green CI and on the import edges I read in the diff. I also did not run connect plus open against a stale daemon with real Limrun or BrowserStack keys. The refusal fires before any provider call, and the startDaemonRuntime HTTP test covers the daemon composition, so I am not asking for that run.

Once the Cubic P2 is answered or applied, this is ready for a human review.

@thymikee
thymikee force-pushed the claude/provider-credential-fingerprint branch from a0ac012 to 9b3c7ca Compare October 5, 2026 05:57
@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Rebased on main and addressed at 9b3c7ca. pnpm check:affected --run passes (4,984 related tests).

  • Rebase: squashed the branch into one commit and rebased onto main. The only conflict was in limrun.md, beside the fix(limrun): keep the owner's reverse mappings on attached Android instances #3204 port-reverse paragraph; both paragraphs are kept.
  • Cubic P2 (auth hook in the test env): fixed in the shared hermetic setup instead of one test. hermetic-env-setup.ts now clears AGENT_DEVICE_HTTP_AUTH_HOOK and _EXPORT along with the daemon URL and token. With the hook exported, the startup test fails without the scrub and passes with it.
  • verifyBrowserStack: it keeps the shared reader, but restores the old COMMAND_FAILED "profile missed …" error and reconnect hint. An empty value now counts as missing, as noted in the PR body.
  • I resolved the threads you listed. No review threads remain open.

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

The earlier findings from a0ac012 are now fixed at 9b3c7ca, and I found no new problems. The ambient hook is cleared for all tests in the shared test setup, the credential fingerprint reads through the same reader every consumer uses, and the connect error path and docs now match the behavior.

CI is green: 21/21 checks pass at 9b3c7ca. The changes since the last review touch only the test setup, one connect error path and docs, and none of that overlaps a check route. I know of no conflicts. Nothing else needs to change before human review.

On the open Cubic threads, no P1 or P2 thread still applies. These are fixed at this head and can be resolved: #3207 (comment) (ambient hook in tests), #3207 (comment) (fingerprint credential reader), #3207 (comment) (reader map lookup), #3207 (comment) (limrun docs wording), #3207 (comment) (help wording), #3207 (comment) (missing-credentials message) and #3207 (comment) (ledger rationale). These are intended behavior and can be resolved too. #3207 (comment): a hook-configured daemon serves remote callers on its operator's credentials, so stripping the fingerprint is intended and documented. #3207 (comment): lease allocation from a shell goes through local daemons, and the docs say remote daemons are not checked. #3207 (comment): the digest is a staleness check for local callers and grants no authentication.

I read the code but did not run two things. I did not run the startup composition test with AGENT_DEVICE_HTTP_AUTH_HOOK exported, so the claim that it fails without the scrub rests on reading restrictRemoteHttpRequest. I also did not run a live connect plus lease_allocate against a stale daemon with real Limrun or BrowserStack keys. The refusal fires before any provider call, so that run is not needed.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 5, 2026
@thymikee
thymikee added this pull request to stack #3223 October 5, 2026 08:59
…rovider credentials

A local daemon keeps the environment it started with, while connect checks
the shell's. A stale daemon could act on old credentials, for example
creating a billed Limrun instance for an attach-mode shell. On a local
lease_allocate the CLI now sends a versioned hash of the values each
provider's own reader uses (Limrun, BrowserStack), over the socket and local
HTTP; the daemon compares it with the hash of its startup environment and
refuses with INVALID_ARGS, reason provider-credentials-changed, and a hint
naming its state dir. No hash is sent from a shell without credentials or to
remote, proxy, or cloud daemons; a daemon with an HTTP auth hook treats every
caller as remote. AWS Device Farm is excluded.
…HTTP auth hook in tests

verifyBrowserStack reads credentials through the shared reader again but keeps
its COMMAND_FAILED 'profile missed' error and reconnect hint. The hermetic test
setup now also clears AGENT_DEVICE_HTTP_AUTH_HOOK and _EXPORT, so a host with a
hook configured cannot turn a local daemon test into a remote one.
DaemonRequestMeta and toLeaseDaemonRequest now carry both the retainOnClose
fields from #3208 and providerCredentialFingerprint; their acks keep both
rationales and the digest of the merged declarations.
@thymikee
thymikee force-pushed the claude/provider-credential-fingerprint branch from 9b3c7ca to 3fccd86 Compare October 5, 2026 11:18
@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

The earlier review at 9b3c7ca found no code problems, and the rebased head 3fccd86 still has none. The new changes only touch the lease handler and test/wire-compat/ledger.json, and they keep the rule that lease allocation is refused when the daemon holds other provider credentials.

All 21 checks pass at 3fccd86. That includes the wire-compat gate, which runs the route this change touched. I did not recompute the sha256 digests or rerun the lease handler tests locally after the rebase, so I relied on CI for both.

Nothing else is needed from you. The PR is ready for human review.

@thymikee
thymikee merged commit d0deeb8 into main Oct 5, 2026
21 checks passed
@thymikee
thymikee deleted the claude/provider-credential-fingerprint branch October 5, 2026 14:11
amankansal-lt added a commit to LambdaTest/agent-device that referenced this pull request Oct 5, 2026
Brings in callstack#3207 (refuse lease allocation when the daemon holds other
provider credentials) and callstack#3222.

Conflicts:
- provider-definitions.ts: keep main's requireBrowserStackCredentials and
  drop its local request helpers, which this branch moved to
  webdriver-utils.ts; requireEnv is no longer used here but stays exported
  from provider-webdriver/plugin for TestMu.
- cli-help.ts: keep the TestMu AI provider and credential text and add
  main's daemon credential comparison line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
amankansal-lt added a commit to LambdaTest/agent-device that referenced this pull request Oct 6, 2026
…dentials

A provider plugin can declare agentDevicePlugin.credentialVariables in its
manifest. providerCredentialFingerprint falls back to that declaration for
providers without a built-in reader, so the client (shell env) and the
daemon (startup env) both fingerprint plugin credentials without loading
plugin code. TestMu declares LT_USERNAME and LT_ACCESS_KEY, which brings
it under the provider-credentials-changed refusal from callstack#3207.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant