The tasks client and the SDK client dial the Tasks server and are never
started, so they bind no socket. Responses and server-pushed deliveries
arrive on the connection the client opened; with discovery off nothing
dials back.
TestZAPClientsListenNowhere asks the OS which sockets the process
listens on: the server adds one, both clients add none.
Co-authored-by: Hanzo Dev <[email protected]>
The engine took a PORT and the SDK an Options.HostPort, so neither end
could use a unix socket for traffic that never leaves the host. The
field name was not the only obstacle: luxfi/zap bound net.Listen("tcp",
":port") and reached a peer with net.DialTimeout("tcp", addr), so a path
was refused at the dialer with "missing port in address" and could not
be bound at all. That is fixed upstream (luxfi/zap Address + Network);
this pins the commit until it is tagged.
EmbedConfig.Address, Embedded.Address, ServeGated(addr) and
client.Options.Address each take ONE address, because a port is a detail
of one transport rather than a thing every listener has. A path binds or
dials a unix socket; anything else is host:port.
TestDialRequiresHostPort became TestDialRequiresAddress: Dial needs an
address, and asserts a socket path is refused for being absent rather
than for being portless. TestE2E_UnixSocket runs a real workflow over a
socket end to end, and TestE2E_UnixSocketRebind proves the socket is
unlinked on stop so a restart binds cleanly.
Co-authored-by: Hanzo Dev <[email protected]>
Enables consolidating the standalone tasksd onto an in-process embedded engine
(cloud) by exposing it cluster-wide under mandatory identity gating, and by
fixing two gaps that made identity-gated ZAP durable execution impossible.
Embedded.ServeGated(ctx, port, *auth.Validator): starts a SECOND ZAP listener
that exposes the SAME engine to out-of-process callers with RequireIdentity --
every request must carry an auth_token that validates against IAM, org-scoped to
the token owner. The loopback listener from Embed is unchanged (in-process,
trusted). Server-push is routed to the listener holding the worker's
subscription, so gated and loopback workers both receive delivery. A nil
validator is refused (never expose durable execution ungated).
SDK: Options.Token TokenSource attaches auth_token to every JSON-body RPC
(client lifecycle + worker Subscribe) at the transport; binary respond frames
stay task_token-gated. This is the missing client half -- the frontend already
read req["auth_token"], but no SDK ever set it, so a RequireIdentity frontend
was unreachable (every caller 401'd). StaticToken helper for fixed bearers;
client_credentials callers return a fresh token per call so long-lived workers
never present an expired identity.
Org-scoped completion: the respond handlers ran on rootEn (unscoped) while
org-scoped runs live in an org-prefixed store partition, so an org-scoped
workflow was accepted (RUNNING) but never advanced. Thread orgID through the
dispatcher's pending tasks and apply completion via en.WithOrg(orgID). orgID==""
(loopback/unscoped) makes WithOrg a no-op -- the in-process path is unchanged.
RegisterNamespace over ZAP: accept the SDK's flat wire ({"name",...}); the nested
{"namespaceInfo":{...}} unmarshal never matched what the client sends, so an
org-scoped caller could not register the namespace ExecuteWorkflow requires.
Test: TestE2E_ServeGated_DurableWorkflow_RequiresIdentity drives a real durable
workflow (schedules an activity, returns its result) to COMPLETED over the gated
listener with a JWKS-signed token, and asserts an unauthenticated caller is
refused. Full suite green.
Co-authored-by: Hanzo Dev <[email protected]>
Replace the coroutine-push activity path with deterministic, history-backed
replay. One way: the old push-result mechanism is removed.
Server (pkg/tasks):
- durable.go: seq-keyed activity records (wfact/ns/wf/run/seq) — the
exactly-once anchor. applyScheduleActivity is idempotent per (run,seq);
completeWorkflowActivity records the terminal outcome and schedules a new
workflow task so the worker replays and advances. Per-run keyedMutex
serializes advancing a run. Server-side retry+backoff per RetryPolicy
(temporal.RetryPolicy, finite default bound). Recover() rebuilds in-flight
dispatch from the store at boot (called from Embed).
- workflow tasks now carry the run's full event-sourced history.
- dispatch.go: ScheduleActivity/CompleteActivityTask(push) -> DispatchActivity
(seq-bound token) + ResolveActivityToken. workflowPeer/pendingActivity and
OpcodeDeliverActivityResult retired.
- respondWorkflowHandler applies ScheduleActivity{seq} commands under the run
lock; respondActivity* resolve the token and advance the run.
Worker (pkg/sdk/worker):
- workerEnv is now a replay decider: ExecuteActivity resolves from history by
seq (recorded result -> no re-dispatch; else emit ScheduleActivity{seq} and
end the episode via a blockedFuture). Stateless between episodes.
- removed OnActivityResult/pendingActivities push machinery.
Client (pkg/sdk/client): drop OnActivityResult + ScheduleActivity from
WorkerTransport (dead in the replay model).
Tests: e2e (exactly-once, real workers) green; new worker replay determinism
tests (unresolved->schedule+block, resolved->recorded result, 2-activity
determinism). Full 'go test -race ./pkg/tasks/... ./pkg/sdk/...' green.
- newZAPTransport now adds a random suffix to client NodeID so
multiple worker pods don't collide on the server's peer table.
- dispatcher.go gains DISPATCH_TRACE=1 env-gated stderr logs for
Subscribe and EnqueueWorkflowTask, helping triage delivery
routing without recompiling.
ui/src/lib/events.ts — useRealtime() React hook subscribes to
/v1/tasks/events via EventSource and invalidates SWR caches when
workflow / schedule / batch / namespace transitions affect the
current view. No polling. No proxy. Native SSE only.
Wired into:
Workflows page — live workflow.{started,canceled,terminated,signaled}
Schedules page — live schedule.{created,paused,resumed,deleted}
Batches page — live batch.started
Namespaces page — live namespace.registered
pkg/sdk/client/events.go — Go callers get the same stream natively
via client.SubscribeEvents(ctx, httpAddr) → <-chan Event. No SDK
spin-up, no JSON-RPC overhead — long-lived TCP read with inline SSE
parsing. End-to-end verified with a /tmp demo: two POST workflow
starts hit the channel in real time as workflow.started events.
README.md — drop the stale gRPC compat reference. The wire is ZAP
plus an HTTP/JSON shim plus SSE + MCP for browsers and agents. No
gRPC, no go.temporal.io, anywhere.
Bundle 320.7 KB (+0.6 from 320.1). Binary 7.6 MB unchanged.
4/4 integration tests still pass.
ZAP binary protocol duplex RPC standardizes on :9999 across every
service. Lux validator staking/RPC ports (9630/9631 mainnet, 9650/9651
devnet) are unchanged — those are luxd network protocol ports, not ZAP.
One ZAP port everywhere (was a mix of 9651/9652/9653/9633/9900).
Three new opcodes (0x0074-0x0076) wired through the frontend ZAP
handler, along with a thin Unpause alias over PauseSchedule(false).
Update replaces the schedule definition wholesale (partial updates
not on the v1 wire — same shape as upstream UpdateSchedule).
Trigger fires immediately, ignoring the schedule's spec; overlap_policy
maps to schedulepb.ScheduleOverlapPolicy via triggerOverlapEnum so all
six policy enums round-trip.
Describe returns the live schedule body + ScheduleInfo (action_count,
missed_catchup, overlap_skipped, buffer counters, running workflows).
Frontend handlers reuse CreateSchedule's spec/action transform path
so update / create stay in lockstep on encoding.
Tests assert opcodes, request body shape, response decoding, alias
semantics, and ID validation rejects.
The v1 broker issued raw "zapact:<id>" tokens, so any caller who
learned the activityTaskID string could complete/fail an activity
belonging to another workflow. v2 mints an opaque HMAC-SHA256-signed
token bound to {namespace, taskQueue, workflowID, runID,
activityTaskID, exp}; Respond* handlers verify the signature,
expiry, and scope-match before delivering.
Signing key loads from TASKS_ACTIVITY_TOKEN_KEY (base64); unset /
invalid falls back to an ephemeral random key with a WARN log so
tokens no longer validate across restarts (acceptable for dev,
not prod).
The schedule-activity response now carries the signed TaskToken
alongside ActivityTaskID so the worker that executes the activity
presents the signed token on respond.
v1 tokens are still accepted as legacy (prefix "zapact:") so any
in-flight tokens at the upgrade boundary drain; new registrations
always mint v2.
Tests cover: forged token (different signing key) rejected, expired
token rejected, cross-workflow token mismatch rejected, malformed
token rejected, happy-path round-trip, env-var key load, claims
inspection.
Adds every symbol hanzoai/base/plugins/tasks and hanzoai/commerce
call today that was missing from pkg/sdk/*: SignalWithStartWorkflow,
GetWorkflow, QueryWorkflow, CheckHealth, WorkflowRunGetOptions +
WorkflowRun.GetWithOptions, ExecuteChildWorkflow + ChildWorkflowFuture,
LocalActivityOptions + WithLocalActivityOptions + ExecuteLocalActivity
(Phase-1 remote-path), worker.InterruptCh(), worker.Options rate-limit
fields wired via golang.org/x/time/rate.
New opcodes 0x0066 signalWithStartWorkflow / 0x0067 queryWorkflow
land here; frontend-side handlers were wired in PR-A via the
translate.go dispatch table.
Closes red-review §2 gap table. Dry-run base migration compiles.
Replaces StubEnv use in dispatch.go with workerEnv, a real
CoroutineEnv implementation. Activity dispatch now goes over the
wire via ScheduleActivity (opcode 0x006B) + long-poll
WaitActivityResult (0x006C), not a pre-populated in-memory map.
NewTimer uses time.NewTimer in a per-timer goroutine; Select uses
a fan-in wake channel with a 20ms watcher poll (no per-iteration
allocation). Root cancel scope propagates to timers, activity polls,
and child scopes on return.
Workflow return errors are no longer swallowed — non-nil fn returns
get encoded via temporal.Encode into a single FailureCommand entry
in the commands envelope so the frontend can fail the execution
per its retry policy.
Phase-1 note documented in struct comment: every dispatch re-runs
the workflow from the top with the same input — activities must be
idempotent. Phase 2 replaces this with event-sourced replay
without changing the public surface.
Adds WorkerTransport.ScheduleActivity / WaitActivityResult /
StartChildWorkflow with v1 JSON-in-envelope bodies. Schema
schema/tasks.zap gains the 0x006B/0x006C struct definitions.
Implements pkg/sdk/client — the Temporal-replacement surface base,
commerce, and ta call when migrating off go.temporal.io/sdk/client.
Transport is luxfi/zap; zero go.temporal.io/* and zero
google.golang.org/grpc imports.
Opcode allocation (append-only, never reuse):
0x0050-0x005F pkg/tasks legacy (submit / schedule), reserved
0x0060-0x006F workflow lifecycle (start, signal, cancel,
terminate, describe, list)
0x0070-0x007F schedule ops (create, list, delete, pause)
0x0080-0x008F namespace ops (register, describe, list)
0x0090-0x009F health + meta
0x00A0-0x00AF worker poll / respond (moved up from 0x0070
to avoid collision with schedule ops)
Wire (v1): request and response bodies are a JSON document wrapped
in a ZAP envelope with three fields:
field 0 (Bytes) — body
field 8 (Uint32) — status (0/200 = ok)
field 12 (Bytes) — error detail on non-zero status
Opcode is stamped on the ZAP frame flags (high byte). Native ZAP
serde replaces JSON in a follow-up without changing opcodes.
Files:
- client.go — Client interface (Close/ExecuteWorkflow/SignalWorkflow/
CancelWorkflow/Terminate/Describe/List + schedule +
namespace + health), Dial, Transport abstraction,
luxfi/zap default transport, envelope codec.
- options.go — Options, StartWorkflowOptions (RetryPolicy via
pkg/sdk/temporal, no upstream import).
- workflow.go — ExecuteWorkflow, Signal/Cancel/Terminate/Describe/List,
WorkflowRun with reflect-based type-name resolution.
- namespace.go — Register/Describe/List + wire duration-ms codec.
- schedule.go — Create/List/Delete/Pause + wire duration-ms codec.
- transport.go — WorkerTransport + PollWorkflowTaskRequest etc. +
the worker-opcode constants (now 0x00A0+).
- client_test.go — Dial/Close test, ExecuteWorkflow round-trip with a
fake Transport capturing the request and replaying
a canned response (same pattern as pkg/tasks).
Worker bridge helpers (worker_bridge.go, worker_transport.go) were
added concurrently on feat/zap-internal; they import and wrap the
Transport this commit defines.
WorkflowRun.Get gap: the v1 schema does not yet declare a
GetWorkflowHistory / GetWorkflowResult RPC, so Get polls
DescribeWorkflow and returns ErrNotImplementedLocally when the
caller asks for a result from a completed workflow. This is marked
in the doc comment and will be filled in when pkg/sdk/worker lands
the history-fetch opcode.
Verification:
go vet ./pkg/sdk/client/... — clean
go test ./pkg/sdk/client/... — 7/7 pass
go list -deps ./pkg/sdk/client/... — no go.temporal.io/*, no
google.golang.org/grpc