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

agent-device's gateway side runs as an optional module

Abierto
#598 2 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@V3RON ya está trabajando en esto.

Desde el 15/10/2026.

  • #606 de @V3RON — abierto

Evaluación

Este issue todavía no se ha evaluado.

Descripción

task:ready

Part of #591.

Scope

Moves agent-device's gateway side onto the module host. After this PR:

  • A gateway loads a module's gateway side through the registry when
    capabilities.<name>.enabled is true and the module has loadGateway.
  • agent-device builds its fleet port inside the module, from a gateway
    context bound to its name.
  • src/daemon/main.ts imports nothing from any module.

Behaviour is unchanged with one exception. On daemon stop, a gateway now
gives agent-device's stop step at most the
Hook deadline;
today that step has no limit.

In short

Nothing changes for a user. A gateway with agent-device on still serves
/v1/agent-device, and sends each agent to a worker whose agent-device is
available. The code that turns the gateway's parts into agent-device's port
moves from the start-up file into agent-device.

flowchart LR
  G["gateway parts:<br/>worker views, lease index, relay"] --> X["GatewayContext *new*<br/>bound to agent-device"]
  X --> P["agent-device fleet port *moved*"]
  P --> F["/v1/agent-device front"]

Words: gateway context is what the host hands a module on a gateway.
It holds the worker views, the lease index, and a relay that can only reach
that module's own target.

If this goes wrong: agents on a gateway get "no worker can serve
agent-device" while a worker can, or a relay goes to the wrong target.

Technical spec

Facts
# Fact Source
F1 The gateway builds agent-device's front parts from agentDeviceFleetPort(...) before DaemonServer, when enabled is set. src/daemon/main.ts:827-838
F2 agentDeviceFleetPort and its helpers listedLease, describeDevice and leaseFacts live in main.ts. They read FleetViews, grantedDevices(), the lease index and GatewayRelay, and relay with target: AGENT_DEVICE. src/daemon/main.ts:1010-1150, :1091
F3 A worker serves agent-device when it is connected, not drained (unless asked), its agent-device capability is available, its catalog is read, and its lease.maxTtlMs is known. src/daemon/main.ts:1030-1043
F4 agent-device's FleetPort interface, with FleetWorker, PlacedLease and FleetLeaseFacts. src/agent-device/front/relay-host-link.ts:16-61
F5 FleetViews has views(), routingViews(), view(id) and onViewsChanged. src/gateway/fleet-ports.ts:86-91
F6 FleetLeaseIndex has requesterPrefix, all(), resolve(id) and onRemoved(listener). src/gateway/lease-index.ts:98-116, :359-361
F7 GatewayRelay.relay(workerId, call) takes { request, target, tenant, leaseId? }. src/gateway/relay.ts:30
F8 The gateway's stopAuxiliary disposes the edge and cancels waiting allocates through fleetCoordinator.cancelPending, with no deadline. src/daemon/main.ts:915-919
F9 The gateway mounts agent-device's front with leaseTtlCapMs and maxRunMs. src/daemon/main.ts:949-962
F10 GatewayDaemonOptions has no module option. src/daemon/main.ts:695-712
F11 The gateway fleet suite passes agentDeviceUpstream to workers and drives the gateway's /v1/agent-device. It pins the drained and catalog-not-read cases of F3. src/agent-device/gateway-front.test.ts:270, :747, :799
F12 main.test.ts checks that gateway mode builds no supervisor. src/daemon/main.test.ts:2545
F13 dispose.test.ts imports disposeAgentDeviceEdge from the index; that function and AgentDeviceFrontParts are declared in index.ts itself. src/agent-device/dispose.test.ts:3; src/agent-device/index.ts:24-28, :93-97
F14 The gateway's routing stage has-capabilities reads each view's capability list and keeps a worker only when every required name is available; it writes nothing. src/gateway/routing/stages/has-capabilities.ts:3-19
F15 WorkerView and the granted device are z.infer of contract schemas; FleetLeaseEntry is a plain interface; FleetLeaseIndex is a class with private fields and more public members than the port needs. src/gateway/worker-registry.ts:15-25, :51; src/gateway/lease-index.ts:25-32, :60-116
F16 When the gateway's dispose runs, the fleet coordinator rejects every waiter still queued; an existing test expects a waiting allocate to end cancelled on stop. src/gateway/fleet-coordinator.ts:311-321, :634-637; src/agent-device/gateway-front.test.ts:1233
F17 Outside the component, only main.ts imports src/agent-device/index.js; tests import testing.js. git grep -ln "agent-device/index.js" origin/main -- src e2e
Modules touched
flowchart LR
  main["daemon/main.ts *"] --> host["modules host *"]
  host --> types["modules types *"]
  reg["module-registry"] --> def["agent-device/definition *"]
  def -. loadGateway .-> g["agent-device/gateway *new*"]
  g --> port["fleet port *moved*"]
  main --> gw["gateway: views, lease index, relay"]
sequenceDiagram
  participant A as agent CLI
  participant G as gateway front (agent-device)
  participant C as GatewayContext
  participant W as worker
  A->>G: POST /v1/agent-device/...
  G->>C: serving()
  alt a worker serves agent-device
    C->>W: relay(workerId, request), target agent-device
    W-->>A: response
  else none
    G-->>A: no worker connected to this gateway can serve agent-device
  end
Surface and extension points

src/modules/index.ts gains GatewayModule and GatewayContext<C> from
ADR 0030 §3 and §4. Under #595's modules-imports-little rule:

  • WorkerView and FleetGrantedDevice are inferred from the contract's
    schemas, which src/modules may import (F15). No copy is needed.
  • FleetLeases (all, resolve, onRemoved) and FleetLease (the
    entry's fields) are declared by shape. A type test checks each against
    the Pick of its original's members (F15), both ways.
export interface GatewayContext<C> {
  readonly name: string;
  readonly config: C;
  readonly daemonConfig: Config;
  readonly logger: Logger;
  readonly clock: Clock;
  readonly instanceId: string;
  readonly startedAt: number;
  readonly requesterPrefix: string;
  workers(): readonly WorkerView[];
  worker(id: string): WorkerView | undefined;
  serving(): readonly WorkerView[];
  grantedDevices(): readonly FleetGrantedDevice[];
  readonly leases: FleetLeases;     // all(), resolve(id), onRemoved(listener)
  relay(workerId: string, request: Request, tenant: string, leaseId?: string): Promise<Response>;
  cancelPending(requesterId: string): Promise<unknown>;
}
  • serving() lists connected workers whose capability named name is
    available (ADR 0030 §4), nothing more. agent-device's own fleet port
    keeps the rest of F3's rule (not drained unless asked, catalog read,
    lease.maxTtlMs known) by filtering the views itself.
  • relay calls GatewayRelay.relay with target: name (F7).
  • main.ts builds one context per module from the gateway's parts.

createModuleHost({ role: "gateway", … }), called in
startGatewayDaemon before DaemonServer (where F1 builds today):

  • A module with no loadGateway is never loaded on a gateway, and mounts
    and runs nothing.
  • For each module turned on that has loadGateway, it awaits
    loadGateway() and calls createGateway(ctx).
  • A loadGateway() or createGateway that throws stops the gateway's
    start with an error naming the module. Creating starts nothing.
  • It plugs in mount under /v1/<name>.
  • It runs hooks.beforeDrain from the gateway's stopAuxiliary, before
    the HTTP front stops, under the
    Hook deadline.

agent-device's definition gains loadGateway: () => import("./gateway.js").
src/agent-device/gateway.ts's createGateway(ctx):

  • builds today's fleet port from the context (F2, F4);
  • returns its front's handler as mount (F9);
  • has hooks.beforeDrain run F8's steps, through ctx.cancelPending.

agentDeviceModuleWith keeps the real loadGateway, so a gateway test
loads the real gateway side.

Other changes:

  • GatewayDaemonOptions gains optionalModules, passed from
    StartDaemonOptions (F10).
  • src/agent-device/index.ts becomes one line,
    export { agentDeviceModule } from "./definition.js", and
    src/module-registry imports it from there. Its only importer was
    main.ts (F17), so the component keeps an entry and nothing unused.
  • main.ts loses its dynamic import from #597, agentDeviceFleetPort,
    FleetPortParts, AGENT_DEVICE and F2's helpers.
  • Everything else index.ts holds after #597 moves to
    src/agent-device/front/edge-parts.ts: AgentDeviceFrontParts,
    disposeAgentDeviceEdge, createGatewayAgentDeviceFrontParts,
    cancelWaitingAllocates, createAgentDeviceFront, and the FleetPort
    and FleetWorker types. worker.ts, gateway.ts and dispose.test.ts
    import them from there (F13).
  • #597's dependency-cruiser rule tightens: main.ts imports nothing from
    src/agent-device, of any type.
Contract and event changes

None. Searched the contract for agent-device's gateway shapes: the relay
and the routes are unchanged.

Other code on the same state
  • The fleet coordinator places leases and cancels pending requests.
    agent-device reaches it only through ctx.cancelPending, as F8 does
    today.
  • The gateway's routing (has-capabilities) reads the same capability
    lists serving() reads (F14). Neither writes them, so the order between
    them does not matter.
  • The fleet coordinator's dispose (F16): when agent-device's
    beforeDrain runs past the deadline, dispose rejects the allocates
    still waiting, which then end DAEMON_STOPPING instead of cancelled.
    A cancelPending that settles after that gets not-found, and the
    host has already stopped waiting for it. In the normal case the step
    finishes well inside 30 s, and F16's test still holds.
  • The gateway's stop order (F8): beforeDrain runs where agent-device's
    steps run today, before the HTTP front stops.
Rules in play
  • architecture.md rules 18, 20 and 21: one context shape for every
    module, and main.ts imports none.
  • architecture.md rule 10: each by-shape fleet type has its type test.
  • safety.md: the relay sends only to the module's own target, so a
    module cannot reach another's.
Tests

Seam: the gateway fleet harness in src/agent-device/gateway-front.test.ts,
with optionalModules: [agentDeviceModuleWith({ upstream: fake })] on the
workers and on the gateway. A fake gateway module covers the host's own
rules. Every existing test in that suite and in
src/daemon/lease-requires.e2e.test.ts still passes, with only its setup
changed.

  • a gateway with agent-device off mounts no /v1/agent-device, and never calls agent-device's loadGateway (a spying definition)
  • a gateway with agent-device on relays an agent's call to a worker whose agent-device is available (as today)
  • a gateway answers that no worker can serve agent-device when every worker reports it off (as today)
  • a drained worker keeps serving the agent-device leases it holds, and a worker whose catalog is unread serves none (as today)
  • a worker whose view has no lease.maxTtlMs is not in serving()
  • a fake gateway module's relay sends with its own name as the target, and no other
  • a fake gateway module's serving() lists only connected workers whose capability named after it is available, drained and catalog-unread ones included
  • a module turned on with no loadGateway is never loaded on a gateway, and mounts nothing
  • a loadGateway() that throws, and a createGateway that throws, each stop the gateway's start with an error naming the module
  • the gateway's daemon stop cancels a waiting agent-device allocate through the fleet coordinator before the HTTP front stops (as today)
  • a fake gateway module whose beforeDrain never settles is logged as ran out of time at 30 s on a fake clock, and the HTTP front still stops and daemon.stop() resolves
  • gateway mode builds no supervisor: no ps, no agent-device process, no signal (as today, F12)
  • FleetLeases and FleetLease and the Pick of their originals are assignable both ways
  • the conformance suite covers each registered gateway side: createGateway starts nothing, and its beforeDrain settles within the hook deadline
  • dependency-cruiser fails any import of src/agent-device from main.ts

Done when

  • Every Tests line passes, and every existing test in
    gateway-front.test.ts and lease-requires.e2e.test.ts passes with only
    its setup changed.
  • git grep -n "agent-device\|agentDevice" -- src/daemon/main.ts prints
    nothing.
  • On a Mac with an iOS runtime:
    scripts/slow-e2e.sh e2e/slow-agent-device-gateway.test.ts passes.

Out of scope

  • The doctor finding's shape (#596).
  • Streaming (#576).

Depends on

  • #597

Written by an agent.

Lenguaje dominante
TypeScript
Estrellas
19
Forks
1
Merge medio
7 h 57 min
PR fusionados (30 d)
213

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.