feat(local-surface): fleet-node bridge for running proactive personas on a laptop
Summary
Implements the ../workforce half of "Local Surface" — running any proactive
persona on a user's own laptop, triggered like a real Cloudflare deployment —
per AgentWorkforce/cloud#2623 (companion PR, cloud-side provisioning +
webhook consumer) and AgentWorkforce/relaycast#269 (fleet-trigger wiring for
the inbound-webhook path).
packages/local-surface(new package):defineWorkforcePersonaNode({personaPath, channel, connection})
wraps@agent-relay/fleet'sdefineNodewith arun-eventaction and an
onMessage({channel})trigger. On first invocation it launches the persona
via the existingdeploy({mode: 'dev', detach: true})orchestrator
(packages/deploy), keeping the child alive across messages. Each
relaycast message is unpacked frommessage.metadata(theNormalizedWebhook
cloud posts, flattened by relaycast'striggerWebhook()) into a
RawGatewayEnvelope, mirroring cloud's real gateway construction
(buildPayload/buildEnvelopein
deployment-trigger-delivery.ts/integration-watch-dispatcher.ts:
type:${provider}.${eventType}``,resource: payload) rather than an ad
hoc shape — confirmed against cloud+relaycast source together with
cloud-builder/lead in the coordination channel.packages/deploy:ModeLaunchHandlegains an optionalwrite(line)
(dev mode only). The bridge callsdeploy()from a long-lived fleet-node
process with no real piped parent stdin, so it needs direct access to the
child's stdin thatdev.tsalready holds in closure — this is the smallest
change that avoids reimplementing the spawn/bundlingdev.tsowns.packages/cli:agentworkforce local-surface <persona-path>—
resolves/redeems a fleet-node enrollment (@agent-relay/cloud's
enrollFleetNode/resolveActiveFleetNodeEnrollment, the same persisted
storerelay cloud enroll/relay node upalready read/write — bumped
@agent-relay/cloudto^10.1.0for this package only, since these exports
don't exist in the previously-pinned^8.7.0), resolves the persona's
bound relaycast channel via--channel(cached in
~/.agentworkforce/local-surface/state.json), writes a generated
node-config file, and shells out torelay node up --config <file>.
Why --channel instead of calling cloud's opt-in API directly
POST /api/v1/fleet/local-surface (cloud PR #2623) is session-cookie-gated
(requireSessionAuth, org-owner-only) — same constraint as
POST /api/v1/fleet/enrollment-tokens. A CLI's workspace bearer token
resolves server-side to source: "token", never "session", so it can never
satisfy that gate. This mirrors the existing enrollment-token flow exactly:
the CLI can redeem something minted via a real browser session but can't
mint/opt-in itself. --channel carries the value from wherever Cloud's
dashboard surfaces that opt-in action, cached locally after the first run.
Known gap (explicitly out of scope per the plan)
Local secrets/credentials are not mirrored — a --mode dev persona has no
Resource.<Name>.value equivalent. local-surface warns when a persona
declares integrations; the safe target today is cron/timer-only or
webhook-shape-only personas.
Test plan
-
packages/local-surface: 7/7 unit tests (envelope mapping incl.
provider/eventType-missing drop-and-ack, lazy single-launch + per-message
stdin writes, missing-token rejection) -
packages/cli: 10/10 newlocal-surface-commandtests (arg parsing,
enrollment reuse/redeem, channel cache round-trip, missing-enrollment
and missing-channel error messages,--jsonmode, integration-gap
warning) -
packages/deploy: 220/220 (no regressions from theModeLaunchHandle.write
addition) - Full repo
pnpm -r buildandpnpm -r test --no-bail: all 17 relevant
packages green; the only failures are 34 pre-existinglocal-personas.test.js
failures inpackages/cli, verified present on a cleanorigin/main
checkout before this branch existed (environment-dependent, unrelated to
this change) - Root
pnpm run typecheck(aggregate tsconfig): clean - Real end-to-end (
relay node upagainst a live relaycast + a real
provider webhook) — deferred to the companion cloud/relaycast PRs
landing; not exercisable from this repo alone
🤖 Generated with Claude Code