Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Input socket for a lease

Cerrado
#1,017 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@V3RON ya está trabajando en esto.

Desde el 26/10/2026.

  • #1034 de @V3RON — fusionado

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
20/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
node.js, typescript
Área
api, backend

Línea de trabajo

Start at the upgrade listener in src/ports/uplink-websocket.ts and the attach in src/daemon/main.ts; the new router authenticates through src/auth/index.ts. Read src/modules/types.ts and host.ts for the two-step lease socket, and copy the module shape from src/stream/worker.ts/gateway.ts. The device port is src/core/driver-catalog.ts plus the optional member in src/core/driver.ts; src/daemon/stream-gateway.e2e.test.ts is the e2e pattern. Done: an owned lease upgrades, gets 0x83 then 0x82 with inputUnavailable, closes with the listed codes, and docs are updated.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

task:ready

Part of #1013.

Scope

After this PR, a client with an agent token opens WS /v1/leases/{id}/input on a gateway or a standalone worker. It gets 0x83, then 0x82, and its serve-sim messages reach the device input of the worker that holds the lease. No device takes input yet: no driver has an input member, so every device input is unavailable until #1018 and #1019 add the Android and iOS ones. The socket ends when the lease ends, when either end goes quiet, and when the daemon stops.

This task adds the new optional module input, the upgrade routing in ports, the two-step lease socket seam, the device input port and its optional driver member. It is the feature's widest task (five Behaviours rows, about 20 modules). It does not split further: the socket, its gateway relay and its lifecycle are one path a test drives end to end, and a split would leave a seam with no caller.

In short

A lease holder's app opens one WebSocket for its lease and gets 0x83 (input admitted), then a screen config 0x82. While no driver takes input, that is 0x82 {"width":1,"height":1,"orientation":"portrait","inputUnavailable":true}. Through a gateway, the socket rides #1016's two-way relay to the lease's worker. It closes with 4001 and the lease-end reason (explicit, killed, expired, device-lost) when the lease ends.

New terms: device input, what turns messages into taps on one device, one per lease, shared by up to 8 sockets; unavailable and failed, as the feature's Words used define them (Shared values, "Device input port").

flowchart LR
  App[App] -- WS upgrade --> P[ports upgrade router NEW]
  P --> H[http: lease resolve for the caller]
  H -- open: refusal or accept --> GI[input module, gateway side NEW]
  GI -- relayTwoWay #1016 --> WI[input module, worker side NEW]
  H -- open: refusal or accept --> WI
  WI -- WorkerContext.inputs NEW --> C[DriverCatalog.openInput NEW]
  C --> D[driver input member: none yet → unavailable]

Closes a client sees, word for word (each reason cut to 123 bytes of UTF-8):

Code Reason When
4001 explicit, killed, expired or device-lost the lease ended and the worker said why
4001 lease ended through a gateway, the lease went and no 0xF1 came within 2 s
1011 Worker <workerId> stopped answering heartbeat silence or uplink lost
1001 Simlock is stopping daemon stop
1013 Input is unavailable for this lease a 9th socket, a failed device input, or a reconnect within 10 s of a failed open

Refusals before the upgrade, through a gateway:

$ websocat -H "Authorization: Bearer $AGENT_TOKEN" wss://gw:7480/v1/leases/L-42/input
websocat: WebSocketError: Received unexpected status code (503 Service Unavailable)
# input off on the worker: {"error":{"code":"INPUT_UNAVAILABLE","message":"Worker w1 does not take input for lease L-42; turn on capabilities.input.enabled there"}}
# no head from the worker in 10 s: {"error":{"code":"WORKER_UNREACHABLE","message":"Worker w1 did not answer the input socket within 10 s"}}
Request Today After
upgrade /v1/leases/L-42/input, owner, input on 404, socket destroyed 101, then 0x83, 0x82
upgrade, another agent's lease 404 404 UNKNOWN_LEASE
upgrade, no token 404 401 UNAUTHENTICATED
upgrade, input off on the answering daemon 404 404
plain GET /v1/leases/L-42/input, no upgrade 404 426
upgrade /v1/uplink with a worker token, on a gateway 101 101 (unchanged)
upgrade /v1/leases/L-42/stream 404 404 (only input takes sockets)

If this goes wrong, a user sees T3 Code's device panel say "input socket refused" or never mark input as connected.

Technical spec

Facts

Commit e1f2f43e6.

# Fact Source
F1 The gateway's only upgrade listener answers 404 and destroys the socket for any path but /v1/uplink. src/ports/uplink-websocket.ts:214-227
F2 It is attached to the gateway's HTTP server through onServerCreated: (server) => uplinks.attach(server); a worker's HttpGateway gets no such hook, so a worker upgrades nothing. src/daemon/main.ts:1158-1165, src/daemon/main.ts:807-811, src/http/server.ts:41-49
F3 Only uplink-websocket.ts imports ws (8.18.3), with new WebSocketServer({ noServer: true }); the ports barrel does not re-export it, so the CLI and MCP server never load ws. src/ports/AGENTS.md:53, package.json:82, src/ports/uplink-websocket.ts:181, src/ports/index.ts:53-58
F4 The uplink authenticates the bearer token before handleUpgrade, answering 401 or 403 and destroying the socket on refusal. src/ports/uplink-websocket.ts:249-264
F5 The uplink's worker side pings every 15 s and terminates when no pong came since the last ping. src/ports/uplink-websocket.ts:39-41, src/ports/uplink-websocket.ts:325-340
F6 HTTP auth resolves a bearer token from request headers through Auth.authenticateRequest(headers). src/auth/index.ts:46-47
F7 A module mount is (Request, identity) → Promise<Response>, mounted under /v1/<module name>; it cannot take an upgrade. src/modules/types.ts:85-90, src/modules/host.ts:377-381
F8 A worker module returns capability, relayTarget, mount, findings, hooks; the host collects relay targets by module name. src/modules/types.ts:103-109, src/modules/host.ts:307-310
F9 ModuleScreens hands a module the drivers' capture, routed by platform, without the drivers; the worker builds it from DriverCatalog. src/modules/types.ts:120-129, src/daemon/main.ts:519
F10 DriverCatalog.openScreen routes to the driver's optional screenCapture member and throws ScreenNotSupportedError when it is absent. src/core/driver-catalog.ts:53, src/core/driver-catalog.ts:96-107, src/core/driver.ts:372-374
F11 FakeDriver takes screenCapture as an option, and the stream e2e test fakes the screen through it. src/core/fake-driver.ts:131, src/core/fake-driver.ts:239, src/daemon/stream-gateway.e2e.test.ts:115-121
F12 beforeDeviceReclaim is how a worker module hears a lease end, with ModuleEndedLease.reason explicit, killed, expired or device-lost. src/modules/types.ts:44-50, src/modules/types.ts:96-101, src/modules/host.ts:312-318
F13 A worker emits lease.released or lease.expired when it commits the end, before lease-end participants run. src/leasing/lease-lifecycle.ts:178-196
F14 The gateway removes a lease from its index when that relayed event arrives, and FleetLeases.onRemoved hands listeners the entry with no reason. src/gateway/owner-routed-facts.ts:84-98, src/modules/types.ts:164-177
F15 Both daemons run moduleHost.beforeDrain() before stopping the HTTP gateway, which destroys open sockets without a close frame. src/daemon/main.ts:741-744, src/daemon/main.ts:1102-1107, src/http/server.ts:206-221
F16 The stream's gateway side maps RELAY_TARGET_MISSING to 503 with a "turn on capabilities.stream.enabled there" message. src/stream/gateway-link.ts:68-72
F17 A module's error codes are declared in its definition.ts statuses, not in the contract (STREAM_UNAVAILABLE). src/stream/definition.ts:58-65
F18 Optional modules are listed in one registry array; a module's capability name is its config section capabilities.<name>.enabled. src/module-registry/index.ts:1-7, src/core/config.ts:256-258
F19 The stream's worker side checks the device against registry.snapshot when a stream opens. src/stream/worker-link.ts:124-131
F20 The relay's head deadline is 330 s and a channel's accept wait 30 s; neither is a bound this task can rely on for a quick refusal. src/gateway/relay.ts:31-36, src/link/channel.ts:49-50
F21 ws throws RangeError for a close reason over 123 bytes. node_modules/ws/lib/sender.js:190-196
F22 Expo serve-sim refuses a socket past 8 with close 1013, sends 0x83 on attach, then the 0x82 config. expo/expo-device-hub v1357114:packages/serve-sim/packages/serve-sim/src/device-session.ts:136, :1384-1392, :1449-1453
F23 Expo serve-sim answers 0x10 with 0x90 and JSON {requestId, ok, error?}. expo/expo-device-hub v1357114:packages/serve-sim/packages/serve-sim/src/device-session.ts:1781-1806
F24 Expo serve-sim's 0x82 carries inputUnavailable and keyboardUnavailable. expo/expo-device-hub v1357114:packages/serve-sim/packages/serve-sim/src/device-session.ts:2808-2826
F25 T3 Code accepts a 0x82 only with width and height above 0 and one of four orientations: portrait, portrait_upside_down, landscape_left, landscape_right. pingdotgg/t3code v1316d48:packages/client-runtime/src/device/stream.ts:31-40
F26 T3 Code opens /helper/ws?device=<id>, sends nothing first, marks input admitted on 0x83 and blocked by inputUnavailable in 0x82. pingdotgg/t3code v1316d48:packages/client-runtime/src/device/stream.ts:1075-1102
F27 T3 Code sends input only after 0x83 and while 0x82 does not say unavailable; earlier input is dropped. pingdotgg/t3code v1316d48:packages/client-runtime/src/device/stream.ts:1275-1282
F28 T3 Code treats close 1008 and 4401 as unauthorized and retries any other close after 1 s. pingdotgg/t3code v1316d48:packages/client-runtime/src/device/stream.ts:1133-1151
F29 The stream module's worker and gateway files show the module shape this one copies. src/stream/worker.ts:4-21, src/stream/gateway.ts:4-14
F30 pnpm openapi <file> writes the OpenAPI document to the named file and exits 2 without one. scripts/openapi.mjs:1-11
F31 capabilities.stream.enabled is documented on the configuration page; access control lists what an agent token may do. website/src/content/docs/docs/reference/configuration.mdx:314, website/astro.config.mjs:102, website/astro.config.mjs:131
Modules touched

Searches: upgrade, WebSocketServer, onServerCreated, ModuleMount, relayTarget, ModuleScreens, screenCapture, class DriverCatalog, beforeDeviceReclaim, optionalModules, capabilities.stream over src/, website/, ui/src.

  • src/input/ (new): definition.ts, index.ts, worker.ts, gateway.ts, the session (messages, sharing, heartbeat, lease end) and its manual AGENTS.md.
  • src/core/input.ts (new), src/core/index.ts: message tags and JSON shapes from the feature's Shared values "Input messages", and the DeviceInput port types.
  • src/core/driver.ts: an optional input member beside screenCapture (F10).
  • src/core/driver-catalog.ts: openInput, routed to that member; a driver without one answers {unavailable}.
  • src/core/fake-driver.ts, src/core/testing.ts: FakeDriver takes an input option, as it takes screenCapture (F11).
  • src/ports/: a new upgrade router file beside uplink-websocket.ts, and AGENTS.md. The router is not re-exported from src/ports/index.ts (F3).
  • src/modules/types.ts, src/modules/host.ts: the two-step lease socket handler on WorkerModule and GatewayModule, WorkerContext.inputs.
  • src/http/: the lease socket resolution next to #1015's lease route.
  • src/daemon/main.ts: the router replaces the uplinks.attach(server) call at 1163 on the gateway and is added on the worker's listener; inputs from DriverCatalog.
  • src/module-registry/index.ts: add inputModule.
  • website/src/content/docs/docs/using-devices/watching-the-screen.mdx, .../reference/configuration.mdx, .../gateway/access-control.mdx.
flowchart LR
  daemon[daemon/main.ts *] --> ports[ports upgrade router NEW]
  daemon --> host[modules/host.ts *]
  ports --> http[http lease resolution *]
  http --> input[input module NEW]
  host --> input
  input --> core[core/input.ts NEW]
  input -- GatewayContext.relayTwoWay --> gw[gateway relay #1016]
  input -- WorkerContext.inputs --> cat[core/driver-catalog.ts *]
  cat --> drv[core/driver.ts input member *]
sequenceDiagram
  participant App
  participant GW as Gateway (ports, http, input)
  participant W as Worker (input)
  App->>GW: GET /v1/leases/L-42/input, Upgrade: websocket
  GW->>GW: authenticate, resolve L-42 for the caller
  alt not theirs or unknown
    GW-->>App: 404 UNKNOWN_LEASE, socket destroyed
  else
    GW->>W: relayTwoWay, target input, lease L-42 (10 s signal)
    alt input off on the worker
      W-->>GW: RELAY_TARGET_MISSING
      GW-->>App: 503 INPUT_UNAVAILABLE, socket destroyed
    else no head in 10 s
      GW-->>App: 503 WORKER_UNREACHABLE, socket destroyed
    else head
      GW-->>App: 101 Switching Protocols
      W-->>App: 0x83, 0x82
      App->>W: 0x03 touch
      W-->>GW: 0xF0 every 5 s (dropped)
      Note over GW,W: 15 s with no bytes from W
      GW-->>App: close 1011
    end
  end
Surface and extension points
  • src/input/index.ts exports inputModule, an OptionalModuleDefinition (F18, F29). Its worker factory returns a WorkerModule with capability, leaseRoute (426 for a plain GET), a lease socket handler, relayTwoWay (the worker end of #1016's two-way relay, see the feature's Two-way relay row), and hooks.beforeDeviceReclaim and hooks.beforeDrain. Its gateway factory returns leaseRoute, a lease socket handler and hooks.beforeDrain.
  • Lease socket seam, written to the feature's Shared values "Lease socket": WorkerModule.socket and GatewayModule.socket, each open(request, identity, lease) → Promise<Response | accept>. A Response is sent as the refusal and the socket destroyed; an accept(socket) runs after the 101. The socket type is a small port interface (send, close(code, reason), onMessage, onClose), so no module imports ws (F3). The gateway's open calls relayTwoWay and answers its refusal, so a 503 always comes before the upgrade.
  • ports gains one upgrade router per HTTP server: /v1/uplink goes to the uplink's handler exactly as attach does today; /v1/leases/{id}/<segment> goes to http's lease resolution; anything else gets 404 and is destroyed (F1). attach stays for uplink-websocket.test.ts; main.ts calls the router instead, so only one upgrade listener exists per server.
  • Device input port, written to the feature's Shared values "Device input port": Driver.input?: DeviceInputFactory (optional, like screenCapture), DriverCatalog.openInput(platform, device, signal), and WorkerContext.inputs: ModuleInputs modeled on ModuleScreens (F9). A driver without the member answers {unavailable: "<platform> devices take no input"}. #1018 and #1019 are the two implementations, so the port is added now (architecture rule 19).
Contract and event changes

none. Searched src/contract/ for relay, capability and error shapes: #1016 makes the relay change and bumps the protocol; INPUT_UNAVAILABLE is a module error code declared in src/input/definition.ts (F17); the capability input uses the existing {name, state, reason?} shape. No event is added; opening and refusing sockets is logged only.

New wire fields, all from the client: each WebSocket message is one tag byte and UTF-8 JSON (feature Shared values "Input messages"); a message over 64 KiB, bad JSON, an unknown tag, a position outside 0..1, or 0x07 is dropped and the socket stays open (F22's server drops the same way). Config capabilities.input.enabled: boolean, default false.

Other code on the same state

Searches: on("upgrade" (1 hit, F1), handleUpgrade (1), screenCapture (driver, catalog, fake driver, stream e2e), beforeDeviceReclaim (1: stream), beforeDrain (module host, 2 daemons), leases.onRemoved (1: agent-device relay-host-link).

  • The uplink's upgrade handling (F1): now behind the router; an uplink upgrade behaves byte for byte as today. src/ports/uplink-websocket.test.ts already proves it and stays green unchanged.
  • The stream module's beforeDeviceReclaim (F12): both modules hear the same lease end; neither waits for the other, and each closes only its own sockets.
  • DriverCatalog.openScreen (F10): openInput sits beside it and reads a different driver member; neither changes the other.
  • #1015's lease route (feature Shared values "Lease route"): this task reuses its lease resolution for upgrades, and adds leaseRoute on input for the 426.
  • The lease ending while a socket opens: the worker session registers the socket for its lease first, then re-checks registry.snapshot.leases (as the stream checks the registry at open, F19); a lease already gone closes the socket 4001 with the reason it recorded, or lease ended. The gateway does the same against FleetLeases.resolve after the relay's head.
  • The gateway's onRemoved and the worker's 0xF1 (F13, F14): the relayed lease event usually reaches the gateway before the worker's 0xF1. On onRemoved, the gateway waits up to 2 s for that socket's 0xF1; the 0xF1 reason wins; with none, it closes 4001 lease ended.
  • HttpGateway.stop (F15): the input module closes its sockets with 1001 in beforeDrain, which runs first, so no socket is destroyed without a close frame.
Failure modes
Call Outcome What happens Limit, counted from
Upgrade auth (F4, F6) no or unknown token 401, socket destroyed none, synchronous
Lease resolution not theirs, unknown 404 UNKNOWN_LEASE, socket destroyed none
relayTwoWay open head 101, then the worker's 0x83 and 0x82 10 s, from the open
relayTwoWay open RELAY_TARGET_MISSING 503 INPUT_UNAVAILABLE before the upgrade (F16) same
relayTwoWay open WORKER_UNREACHABLE, or no head in 10 s 503 WORKER_UNREACHABLE before the upgrade; the 10 s signal aborts the open (F20) same
Worker heartbeat silent close 1011, relay ended 15 s from the last byte from the worker
Uplink closed every socket on that worker closes 1011 at once
Client ping no pong socket terminated, its share of the device input released 15 s from the ping, pings every 15 s
inputs.openInput {unavailable} (every platform in this task) 0x83, then 0x82 {"width":1,"height":1,"orientation":"portrait","inputUnavailable":true}; messages dropped none
inputs.openInput rejects close 1013; the failure is remembered for 10 s, and sockets in that window get close 1013 without a new open the open's own bound, set by #1018/#1019
Device input onFailed every socket of the lease closes 1013; the device input closes at once
Socket count 9th socket close 1013 before 0x83 (F22) none
Lease end (F12) any reason 0xF1 {code:4001, reason} on the channel, then close 4001 with the reason the participant's own signal
Gateway onRemoved (F14) lease gone, socket still open wait for that socket's 0xF1; then close 4001 with its reason, or lease ended 2 s, from onRemoved
Daemon stop (F15) beforeDrain close 1001 Simlock is stopping on every socket before the HTTP gateway stops
Any close reason over 123 bytes cut at a character boundary to 123 bytes (F21) none

State: the device input per lease, its socket set and the 10 s failed-open memory, in memory only. A worker restart drops them; through a gateway the uplink closes, so its sockets close 1011; standalone, the TCP connection closes. Two sockets opening at once for one lease share one device input: the first open runs, the second waits for it.

Rules in play
  • Architecture rule 11, "Every wait that crosses a process boundary is bounded": the relay open (10 s), the worker silence (15 s), the client pong (15 s) and the gateway's wait for 0xF1 (2 s) are each counted from a fixed point.
  • Architecture rule 10, "A rule is enforced in exactly one place": lease ownership is checked only by http's lease resolution; the module re-checks only that the lease still exists.
  • Architecture rule 3, "Adding a driver must require zero core changes": the input member and openInput land in src/core/ here, so #1018 and #1019 add driver code only.
  • Architecture rule 15, "Dependencies point from the optional to the required": input imports modules, core and contract; nothing required imports input.
  • Architecture rule 18, "A module hands out a capability, not its parts": index.ts exports only inputModule.
  • Architecture rule 19, "An extension point is added when a second implementation is named": the driver input member has #1018 and #1019 as its implementations.
  • src/ports/AGENTS.md:53, "Only uplink-websocket.ts imports ws": the router lives beside it in ports and owns the only other ws import; the rule's wording changes to name both files.
  • Documentation rule 9, "A change to the public surface updates the website in the same change": the route, capabilities.input.enabled and INPUT_UNAVAILABLE go on the three pages under Modules touched.
  • ADR 0049 §1–§6: lease routes, the module lease route, expo serve-sim messages, the shared device input, the two-way relay's heartbeat and lease end.
Tests

Seam: the gateway and standalone worker HTTP listeners with a device input faked through FakeDriver({input}), in a new src/daemon/input-lease.e2e.test.ts modeled on src/daemon/stream-gateway.e2e.test.ts; the session's message handling in src/input/ modeled on src/stream/worker-link.test.ts.

  • A caller opens WS /v1/leases/{id}/input and gets 0x83 then 0x82, or a refused upgrade: own lease; another's 404; unknown 404; operator; input off on the answering daemon 404; off on the lease's worker 503 INPUT_UNAVAILABLE; worker gives no head in 10 s 503 WORKER_UNREACHABLE; plain GET 426; no token 401; a proxy-signed (ambient) identity without Sec-Fetch-Site: same-origin 403 FORBIDDEN, as ADR 0033 §5 (decided in delivery, pending the maintainer); a 9th socket closed 1013; an unavailable device input gets "inputUnavailable":true; a failed open closes 1013 and a reconnect within 10 s is refused 1013 without a new open.
  • A client following T3 Code's order drives a faked device through a gateway: waits for 0x83; touch begin/move/end, key, button arrive in order; a second socket shares the same device input; the device input closes when the last socket closes; a size change reaches every socket as 0x82.
  • A client sending a bad message keeps its socket and nothing reaches the device: bad JSON; unknown tag; x outside 0..1; 0x07 dropped; 0x10 answered 0x90 {ok:false}.
  • An input socket closes with 4001 and the reason when the lease ends, and a reconnect gets 404: explicit, expired, killed, device-lost; through a gateway the worker's reason wins; a lease that ends while the socket opens.
  • An input socket closes when either end goes quiet or away: client misses its pong (worker and gateway); worker heartbeat stops (1011); uplink drops (1011); daemon stops (1001); the gateway sees the lease go (4001).

The uplink's upgrade, unchanged, stays proven by src/ports/uplink-websocket.test.ts.

Done when

  • pnpm test passes, including src/daemon/input-lease.e2e.test.ts and src/ports/uplink-websocket.test.ts unchanged.
  • With capabilities.input.enabled true on a standalone worker, an agent token's upgrade to its own lease's /v1/leases/{id}/input gets 0x83 then 0x82 {"width":1,"height":1,"orientation":"portrait","inputUnavailable":true}; another token gets 404.
  • pnpm openapi /tmp/openapi.json writes a document listing GET /v1/leases/{id}/input with 101, 401, 404, 426 and 503 (INPUT_UNAVAILABLE, WORKER_UNREACHABLE).
  • The three website pages under Modules touched describe the input socket, capabilities.input.enabled and INPUT_UNAVAILABLE.

Out of scope

  • Any device actually taking input: Android in #1018, iOS simulators and physical iPhones in #1019.
  • Rotation: 0x07 is dropped on every platform.
  • The two-way relay itself and its protocol bump (#1016).
  • The lease stream route and the lease route seam for plain HTTP (#1015).
  • Input from the console; a preview {url, token}.

Depends on

  • #1015
  • #1016

Written by an agent.

Lenguaje dominante
TypeScript
Estrellas
22
Forks
2
Merge medio
9 h 19 min
PR fusionados (30 d)
430

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de callstackincubator/simlock

Todos los issues de callstackincubator/simlock

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.