Skip to content

One lease is read directly, and gateway leases name the worker's lease id #913

Closed

Description

@V3RON

Part of #905.

Scope

GET /v1/leases/{id} and GET /v1/leases/{id}/events find one lease directly instead of building the whole lease list, and on a gateway every lease in GET /v1/leases names the worker's own lease id, so a relayed event can be matched to it (#916).

In short

Reading one lease no longer walks every lease of the fleet. On a gateway, each lease record gains workerLeaseId, e.g. { "id": "w1.lse_42", "workerId": "w1", "workerLeaseId": "lse_42", … }. Nothing else a user sees changes: the GET /v1/leases/{id} reply keeps its shape (no workerLeaseId), status.get keeps its shape, and an unknown or not-owned lease still answers 404 UNKNOWN_LEASE.

flowchart LR
  H[GET /v1/leases/id] --> F["findOwnedLease"] --> G["lease.get (new)"]
  L[GET /v1/leases] --> P["lease.list + workerLeaseId (new)"]
Loading

If it goes wrong: an agent reads a lease that is not theirs, or a lease answers 404 while it is live.

Technical spec

Facts

# Fact Source
F1 findOwnedLease dispatches lease.list and .finds the id; not found → unknownLease (404). There is no lease.get. src/http/app.ts:783-801
F2 GET /v1/leases/{id} uses findOwnedLease then findDevice; /events uses findOwnedLease too. src/http/app.ts:371-376; src/http/AGENTS.md Known gaps
F3 Worker lease.list: registry snapshot leases, own only unless admin. src/daemon/dispatcher.ts:627-632
F4 Gateway lease.list: admin → every view's leases through leaseIndex.project; else each index entry owned by the caller, its raw lease found in the view by entry.workerLeaseId. src/gateway/dispatcher.ts:517-537
F5 project replaces id with the gateway id when the index knows (workerId, lease.id), else keeps the worker's id; it adds workerId in both cases. The raw worker id is not kept. src/gateway/lease-index.ts:330-350
F6 lease.list output is { leases: statusLeaseSchema[] }; statusLeaseSchema = leaseRecordSchema + optional workerId. src/contract/operations.ts:530-535; src/contract/schemas.ts:1383-1386
F7 defineOperation declares name, role, effect, input, output, authorize. src/contract/operations.ts:60-90
F8 A contract shape change bumps the protocol in the same PR, checked by protocol-bump-check.mjs. docs/internal/agent-rules/toolchain.md:196-203
F10 project has three callers: gateway lease.list (owned, dispatcher.ts:534), the admin fleet list #fleetLeases used by lease.list and list.get kind=leases (dispatcher.ts:590), and status.get's aggregate (aggregate.ts:89). grep -rn "\.project(" src
F11 Today an admin's GET /v1/leases/{id} of a worker's own local lease (one the index does not know) answers 200: the admin list holds it under its raw id. src/gateway/dispatcher.ts:585-592; src/gateway/lease-index.ts:332-350
F12 Every operation but daemon.stop has a case in DaemonServer's socket switch. src/daemon/server.ts:1000-1218
F9 remote-agents.mdx lists GET /v1/leases ("Your leases.") and GET /v1/leases/<id>. website/src/content/docs/docs/using-devices/remote-agents.mdx:166-168

Modules touched

src/contract/operations.ts, src/contract/schemas.ts (workerLeaseId on statusLeaseSchema), src/contract/protocol.ts (bump), src/daemon/dispatcher.ts and src/gateway/dispatcher.ts (lease.get; gateway lease.list adds workerLeaseId), src/daemon/server.ts (socket case for lease.get), src/http/app.ts (findOwnedLease dispatches lease.get), src/http/AGENTS.md, src/gateway/AGENTS.md, src/contract/AGENTS.md, remote-agents.mdx (field in the lease list). project itself does not change: the gateway's two fleet lease builders (F10, dispatcher.ts:534, 590) add workerLeaseId: raw.id after projecting, so lease.list and list.get kind=leases carry it and status.get does not. Searches: findOwnedLease (3 hits), lease.list in src/ (dispatchers, http, simlock-client), \.project( (3 hits, F10).

sequenceDiagram
  participant C as Client
  participant H as HTTP app
  participant D as Dispatcher (worker or gateway)
  C->>H: GET /v1/leases/w1.lse_42
  H->>D: lease.get {leaseId}
  alt found and owned (or admin)
    D-->>H: {lease}
    H-->>C: 200 (shape as today)
  else unknown or not owned
    D-->>H: UNKNOWN_LEASE
    H-->>C: 404 UNKNOWN_LEASE
  end
Loading

Contract and event changes

  • New op lease.get, role agent, effect read, input { leaseId: string } (unbounded, as lease.renew's leaseId, src/contract/operations.ts:376; an unknown string answers 404), output { lease: statusLeaseSchema }, no authorize: the handler answers UNKNOWN_LEASE for a lease the caller does not own, as lease.list hides it today (F1).
  • statusLeaseSchema gains optional workerLeaseId: string, set only by a gateway's lease.list, lease.get and list.get kind=leases; status.get never sets it.
  • Gateway lease.get finds the record lease.list would hold for this caller with that id: an index entry → its view's lease (owner checked unless admin); otherwise, for an admin only, a view lease with that raw id that the index does not rename (F11). A raw id the index renames (lse_42 known as w1.lse_42) answers 404, as today.
  • The socket serves lease.get too (F12).
  • This PR bumps the protocol by one. No event changes.

Other code on the same state

The gateway lease index: #ownedFleetLeases (F4) and findByWorkerLease read it; lease.get reads, never writes it. A lease released between lookup and reply answers as at that moment, as lease.list does. Searches: leaseIndex. in src/gateway/ (12 hits).

Failure modes

Call Outcome What happens Limit, counted from
lease.get found 200 none
lease.get unknown / not owned 404 UNKNOWN_LEASE none
lease.get (gateway) index entry, view missing or no longer listing the lease 404 UNKNOWN_LEASE, as #ownedFleetLeases skips it none
lease.get (gateway) worker's own local lease, admin 200 (F11) none

No state is written.

Rules in play

  • ADR 0044 §3 (lease.get, workerLeaseId): complies.
  • ADR 0005 §16 "a gateway lease id is <workerId>.<workerLeaseId>": workerLeaseId names that part, also for requester-chosen ids (ADR 0020).
  • Toolchain "every contract-shape change bumps it": bumps.

Tests

Seams: the handlers through src/daemon/dispatcher.test.ts (as its lease.list test, :1159) and src/gateway/dispatcher.test.ts; the HTTP mapping through src/http/app.test.ts with its fake dispatcher.

  • A client reads one lease (lease.get on each dispatcher, and GET /v1/leases/{id} in the app): own → found, reply in today's shape; another agent's → UNKNOWN_LEASE / 404; admin any → found; unknown id → 404; gateway by gateway id → found with workerLeaseId; gateway admin, a worker's own local lease → found; gateway, a raw id the index renames → 404; gateway, an index entry whose worker view is missing or no longer lists the lease → 404 (decided in delivery, accepted by the maintainer 2026-10-23); /events of an unknown id → 404; over the daemon socket → found (as the socket tests of src/daemon/server.test.ts).
  • An admin lists leases on a gateway: gateway-minted id → workerLeaseId is the worker's id; requester-chosen id → workerLeaseId equals it; a lease the index does not know → workerLeaseId equals id; list.get kind=leases → same; status.get → no workerLeaseId; a worker's own list → no workerLeaseId.

Done when

  • pnpm vitest run src/http/app.test.ts src/gateway/dispatcher.test.ts src/daemon/dispatcher.test.ts src/gateway/aggregate.test.ts passes.
  • The protocol bump check passes.

Out of scope

403 instead of 404 for a not-owned lease. findDevice's device scan.

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  2. changed the title [-]Unchanged JSON answers under /v1 are 304 with an ETag[/-] [+]One lease is read directly, and gateway leases name the worker's lease id[/+] on Oct 23, 2026
  3. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  4. self-assigned this
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions