Skip to content

feat(remote): name the held lease on upload, artifact download and diagnostics requests - #3594

Merged
thymikee merged 3 commits into
mainfrom
feat/lease-id-on-upload-artifact-diagnostics
Oct 14, 2026
Merged

thymikee merged 3 commits into
mainfrom
feat/lease-id-on-upload-artifact-diagnostics

Conversation

@V3RON

@V3RON V3RON commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A gateway in front of many daemons routes each request to the daemon that holds the request's lease. RPC calls already name the lease in their body, but the plain HTTP calls do not, so such a gateway cannot route them:

  • uploads: POST /upload, POST /upload/preflight, PUT /upload/direct/<ticket>, POST /upload/finalize
  • artifact downloads: GET /artifacts/<id>
  • request diagnostics: GET /sessions/<s>/requests/<r>/diagnostics

When the client talks to a remote daemon and holds a lease, it now sends x-agent-device-lease-id: <leaseId> on these requests. This is the same lease id it puts in its RPC calls. With no lease held, it sends no header. A direct upload ticket on another origin, such as a signed storage URL, gets only the headers the daemon handed out.

Nothing reads the header yet. The daemon ignores it, and the proxy forwards only its allowlist of headers, so it drops this one. The client never sends GET /artifacts (the list), so that route is unchanged.

The wire-compat ledger gets new digests and additive acks for the changed client declarations. The stale #2946 acks on the same upload functions are dropped.

Validation

  • pnpm check:affected --run: all runnable checks passed (format, lint, typecheck, layering, fallow, build, vitest-related 366 files / 2465 tests, daemon-wire-compat).
  • New daemon-client-lease-header.test.ts runs sendToDaemon against a loopback remote daemon (install upload, screenshot download, failing command diagnostics). With a lease, every one of these requests carries the header. Without a lease, none does. The lease test fails when the call-site wiring is removed.
  • New unit tests in upload-client, artifact-download, remote-request-diagnostics and daemon-http cover the header on each request kind, its absence without a lease, and that a foreign-origin ticket gets no header.

View guided diff Turn on auto-fix

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.21 MB 5.21 MB +436 B
Package (unpacked) 5.21 MB 5.21 MB +436 B
Package (download) 1.57 MB 1.57 MB +101 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 23.4 ms 23.9 ms +0.5 ms
CLI --help 63.2 ms 63.5 ms +0.2 ms

@thymikee

Copy link
Copy Markdown
Member

The lease header change in 0bd157d looks right to me, but Coverage fails on the test-file-size ratchet that this PR caused: src/__tests__/upload-client.test.ts grew from 988 to 1134 lines. Before merge, that file must stay at or below 988 lines. Collapse the four new lease tests into one table-driven test over {direct, legacy, foreign-origin ticket} x {lease, none} on the existing startServer helper, or move them into a lease-focused test that mirrors upload-client.ts. Smoke Tests is still queued, and its device routes do not exercise this remote HTTP header code. I did not run the new tests, and I did not inspect the wire-compat ledger digests beyond the diffstat. The direct-upload PUT and finalize lease path is covered only by the upload-client unit tests, not by the sendToDaemon loopback test.

Not blocking, and you can take or leave these: the scope headers (auth, tenant, lease) are now built by hand at six sites in upload-client.ts, artifact-download.ts and remote-request-diagnostics.ts, and the upload sites already drift because they send no tenant header while download and diagnostics do. Both new test files also hard-code x-agent-device-lease-id because DAEMON_HTTP_LEASE_HEADER is not exported like the tenant header.

Could RemoteDiagnosticsEndpoint become a shared {baseUrl, token, tenantId, leaseId} endpoint, built once per request and passed to upload, download and diagnostics, with one contracts helper that builds all scoped headers from it? That would remove the five threaded leaseId option fields and make a missing header impossible. The upload option types are internal to src/remote, so nothing has to change first.

No conflicts.

* origin/main: (24 commits)
  feat(ios): interpret React Native accessibility value semantics (#3589)
  fix(android): reinstall and reselect the test IME helper after it disappears (#3606)
  feat(snapshot): carry provider keyboard observations through acquisition (#3593)
  chore(web): bump managed agent-browser backend to 0.39.0 (#3591)
  fix(fold): report a dispatched pose as unverified when devicectl has no hinge-angle stream (#3587)
  feat(android): expose system-bar membership on snapshot nodes (#3585)
  fix(android): align ADB and Appium field values (#3584)
  test(closure): read committed JSON modules on the merge-base side (#3602)
  fix(ci): preserve manual Android suite timing (#3554)
  refactor(daemon): make the layer folders the declaration (PR 10) (#3520)
  refactor(move): extract the daemon server into @agent-device/daemon (11b) (#3546)
  refactor(daemon): package-local runtime fakes for the remaining daemon ports [11a-4] (#3544)
  refactor(daemon): cut daemon test edges into root data fixtures and fakes [11a-3] (#3517)
  refactor(11a-2): package-local fakes at the daemon ports; root-own command-surface composition tests (#3519)
  test(replay): verify Settings queries and bound scrolling (#3560)
  feat(snapshot): measure Android Appium viewport (#3586)
  fix(ios): verify a fill whose unidentified field moved while it kept focus (#3596)
  refactor(coverage): one fallow config with per-function CRAP ceilings (#3588)
  test: bound CLI promotion cases and align provider budget (#3590)
  fix(metro): report what the dev-server stop proved instead of assuming success (#3575)
  ...

# Conflicts:
#	src/remote/daemon-artifacts.ts
…eader constant

Collapse the four upload lease tests into one table over the direct,
legacy and foreign-origin flows, each run with and without a lease, so
upload-client.test.ts stays under its merge-base length. Export
DAEMON_HTTP_LEASE_HEADER so tests read the header name from contracts.
@thymikee

Copy link
Copy Markdown
Member

New head is 373ee4d. I merged main into the branch. The only conflict was an import in src/remote/daemon-artifacts.ts: I kept main's @agent-device/daemon/daemon-request import and the lease-scope import from this PR.

  • Fixed, test-file-size ratchet: the four upload lease tests are now one table-driven test on the existing startServer helper. It covers the direct, legacy and foreign-origin ticket flows, and runs each one with a lease and without a lease. To get src/__tests__/upload-client.test.ts back under 988 lines (it is now 985), I also made startServer use the test-kit loopback helpers, replaced the per-event progress checks with one array comparison in two tests, and built the redirect route list instead of writing out each line. None of these changes what the tests check. When I removed the same-origin check in uploadDirectArtifact, the foreign-origin case failed, so the test catches that regression.
  • Fixed, hard-coded header name: DAEMON_HTTP_LEASE_HEADER is now exported from contracts, and the upload, download, diagnostics and sendToDaemon lease tests use it. The contracts test still pins the literal wire name. check:production-exports passes.
  • Declined, shared {baseUrl, token, tenantId, leaseId} endpoint with one scoped-header helper: the upload routes pass the tenant header to the auth hook and the tenant-trust gate. Sending it on uploads would change how uploads are authorized, and that is a separate change from naming the lease. I can do the shared endpoint in a follow-up that makes that tenant change on purpose.
  • Not changed: the direct-upload PUT and finalize lease path is still covered only by the upload-client tests, not by the sendToDaemon loopback test. The table test covers both requests with and without a lease.

Checks run: pnpm typecheck, pnpm lint, oxfmt check on the changed files, vitest for upload-client, daemon-client-lease-header, artifact-download, remote-request-diagnostics, daemon-http and the test-file-size ratchet (40 tests passed), pnpm check:daemon-wire-compat, pnpm check:production-exports and pnpm check:fallow. All passed.

@thymikee

Copy link
Copy Markdown
Member

The PR is ready at 373ee4d. The lease header now reaches upload, artifact download and diagnostics requests, and the end-to-end lease-header test is in place, so what the earlier review at 0bd157d was waiting on is now covered. Not blocking, and fine to take or leave: artifact download reads the lease from params.requestScope?.leaseId (https://github.com/callstack/agent-device/blob/373ee4d/src/remote/artifact-download.ts#L93), while upload and diagnostics use leaseScopeFromRequest(req).leaseId, so resolving it once in the caller would keep a single rule (the result is the same today because buildRequestScopeMeta copies the flag into meta); and the lease header is now composed by hand at six sites (https://github.com/callstack/agent-device/blob/373ee4d/src/remote/upload-client.ts#L215), so one shared header builder in the daemon-http contract could replace them. All 16 CI lanes are still queued and none has failed, so I can't attribute any failure yet. I ran no tests, gates or wire-compat check, and I did not confirm that the lease-header test fails when the call-site wiring is removed. No gateway in this repo reads the header, so routing behavior can't be tested here. Before merge, the queued lanes need to pass, especially Repo Guards (the test-file-size ratchet is at 985 of 988 lines) and the daemon-wire-compat ledger check. There are no conflicts.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 14, 2026
@thymikee
thymikee marked this pull request as ready for review October 14, 2026 13:19

@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.

2 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/remote/upload-client.ts">

<violation number="1" location="src/remote/upload-client.ts:357">
P2: A daemon-origin ticket can redirect to a different origin, and `streamFileToHttpRequest` reuses these headers for every redirect, sending the lease ID to that foreign host. Strip the lease header whenever a redirect crosses origins.</violation>
</file>

<file name="test/wire-compat/ledger.json">

<violation number="1" location="test/wire-compat/ledger.json:413">
P2: The new lease header’s shared contract is not covered by the wire ledger: changes to its name or to `buildDaemonHttpLeaseHeaders`’ omission behavior will not change a tracked digest. Add both declarations to the surface and ledger, with compatible-change acknowledgements.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Turn on auto-fix | Re-trigger cubic

// too; a ticket elsewhere (a signed storage URL) gets only the headers the daemon handed out.
headers:
url.origin === new URL(options.normalizedBase).origin
? { ...ticket.headers, ...buildDaemonHttpLeaseHeaders(options.leaseId) }

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.

P2: A daemon-origin ticket can redirect to a different origin, and streamFileToHttpRequest reuses these headers for every redirect, sending the lease ID to that foreign host. Strip the lease header whenever a redirect crosses origins.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/remote/upload-client.ts, line 357:

<comment>A daemon-origin ticket can redirect to a different origin, and `streamFileToHttpRequest` reuses these headers for every redirect, sending the lease ID to that foreign host. Strip the lease header whenever a redirect crosses origins.</comment>

<file context>
@@ -317,15 +336,26 @@ function isStringRecord(value: unknown): value is Record<string, string> {
+    // too; a ticket elsewhere (a signed storage URL) gets only the headers the daemon handed out.
+    headers:
+      url.origin === new URL(options.normalizedBase).origin
+        ? { ...ticket.headers, ...buildDaemonHttpLeaseHeaders(options.leaseId) }
+        : ticket.headers,
     payloadPath: artifact.payloadPath,
</file context>

"rationale": "The health-probe budget includes requester loading and response completion. The caller AbortSignal joins the probe timer only in-process; caller cancellation remains distinct from probe expiry. No signal, timedOut flag, request route, auth header or accepted health payload field changes on the wire, so released daemons and proxies parse and respond as before (ADR 0006)."
},
{
"declaration": "src/remote/remote-request-diagnostics.ts#RemoteDiagnosticsEndpoint",

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.

P2: The new lease header’s shared contract is not covered by the wire ledger: changes to its name or to buildDaemonHttpLeaseHeaders’ omission behavior will not change a tracked digest. Add both declarations to the surface and ledger, with compatible-change acknowledgements.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At test/wire-compat/ledger.json, line 413:

<comment>The new lease header’s shared contract is not covered by the wire ledger: changes to its name or to `buildDaemonHttpLeaseHeaders`’ omission behavior will not change a tracked digest. Add both declarations to the surface and ledger, with compatible-change acknowledgements.</comment>

<file context>
@@ -433,6 +408,51 @@
       "rationale": "The health-probe budget includes requester loading and response completion. The caller AbortSignal joins the probe timer only in-process; caller cancellation remains distinct from probe expiry. No signal, timedOut flag, request route, auth header or accepted health payload field changes on the wire, so released daemons and proxies parse and respond as before (ADR 0006)."
+    },
+    {
+      "declaration": "src/remote/remote-request-diagnostics.ts#RemoteDiagnosticsEndpoint",
+      "digest": "sha256:c1d5fe2fe90c425502cf8b7c87d6606374d6af65b4e9faf942a416f683930bdc",
+      "rationale": "Clients name a held agent-device lease in an optional x-agent-device-lease-id request header on upload, artifact download and request diagnostics HTTP calls, so a gateway can route them. Released daemons ignore unknown request headers and the proxy forwards only its allowlist, so routes, bodies and responses parse as before; with no lease held the request is unchanged (ADR 0006)."
</file context>

@thymikee
thymikee merged commit bd9ff42 into main Oct 14, 2026
19 checks passed
@thymikee
thymikee deleted the feat/lease-id-on-upload-artifact-diagnostics branch October 14, 2026 13:25
thymikee added a commit that referenced this pull request Oct 14, 2026
* origin/main:
  feat(leasing): proxy CLI names each new lease, daemon keeps a caller-chosen lease id (#3479)
  feat(remote): name the held lease on upload, artifact download and diagnostics requests (#3594)
  fix(android): record the launched intent owner from the device (#3600)
  fix(selectors): type rect-requiring targets as addressable (#3608)
  feat(snapshot): interpret iOS node facts once at producer seams (#3592)
  refactor(gates): own the client-only replay rule as daemon-process reachability (R82) (#3604)
thymikee added a commit that referenced this pull request Oct 14, 2026
* origin/main:
  feat(remote): name the held lease on upload, artifact download and diagnostics requests (#3594)
  fix(android): record the launched intent owner from the device (#3600)
  fix(selectors): type rect-requiring targets as addressable (#3608)
  feat(snapshot): interpret iOS node facts once at producer seams (#3592)
  refactor(gates): own the client-only replay rule as daemon-process reachability (R82) (#3604)
thymikee added a commit that referenced this pull request Oct 14, 2026
* origin/main:
  feat(remote): name the held lease on upload, artifact download and diagnostics requests (#3594)
  fix(android): record the launched intent owner from the device (#3600)
  fix(selectors): type rect-requiring targets as addressable (#3608)
  feat(snapshot): interpret iOS node facts once at producer seams (#3592)
  refactor(gates): own the client-only replay rule as daemon-process reachability (R82) (#3604)
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.

2 participants