You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The CronControl worker optionally exposes a local HTTP endpoint that acts as a relay/proxy for SDK calls (heartbeats, results, chat, artifacts). Tasks running on the same host as the worker can send telemetry to `localhost` instead of directly to the control plane, gaining resilience, lower latency, and buffering. The relay is opt-in and only benefits tasks that execute on the worker host.
5
+
The CronControl worker optionally exposes a local HTTP endpoint that acts as a relay for all SDK write calls (heartbeats, results, chat, artifacts). Tasks running as subprocesses on the worker host send telemetry to `localhost` instead of directly to the control plane, gaining resilience, lower latency, and buffering.
6
6
7
7
## Why This Epic Exists
8
8
9
-
Today, SDKs call the CronControl control plane directly for heartbeats, results, artifacts, and chat. This works but has three weaknesses:
9
+
When a worker runs a local subprocess (EPIC-13 `exec` method), the subprocess uses the CronControl SDK to report heartbeats, set results, post chat messages, and upload artifacts. These calls go directly to the control plane over the network. If the control plane is temporarily unavailable, the SDK call blocks or fails — impacting the running task.
10
10
11
-
1.**Fragility**: If the control plane is temporarily unavailable (deploy, network blip), the SDK call blocks or fails inside the running task, potentially affecting its execution.
12
-
2.**Latency**: Every heartbeat is a remote HTTP call (50-200ms). For tasks reporting progress every second, this adds up.
13
-
3.**Network isolation**: In private networks, tasks may not have direct route to the control plane. The worker already has connectivity — tasks should leverage it.
11
+
The relay buffers, batches, and retries these calls locally. The task always gets a fast `202 Accepted` from localhost and continues working.
14
12
15
-
## Scope: Which Execution Methods Benefit
13
+
## Scope
16
14
17
-
The relay only works when the task process runs **on the same host** as the worker binary.
15
+
The relay benefits tasks that run **on the same host** as the worker:
18
16
19
-
| Execution Method | Relay Works? | Why |
20
-
|-----------------|-------------|-----|
21
-
| HTTP (sync) | No | HTTP tasks call external URLs, no subprocess spawned |
22
-
| HTTP (async_tracked) | No | Same — the "task" is a remote HTTP server |
23
-
| SSH (foreground) | No | Task runs on remote SSH host, not on worker host |
24
-
| SSH (detached) | No | Same — remote host |
25
-
| SSM | No | Task runs on EC2 instance, not on worker host |
26
-
| K8s | Partial | Only if worker and task pod share network (sidecar pattern) |
27
-
| Container (Docker) | Yes | If worker runs on Swarm manager, containers share host network |
28
-
| Future: subprocess/exec | Yes | Task is a child process of the worker |
29
-
30
-
**Key insight**: The relay is most useful for a future subprocess/exec execution method where the worker spawns tasks as local child processes. For SSH/SSM, tasks must continue calling the control plane directly.
17
+
| Execution Method | Relay Works? | Notes |
18
+
|-----------------|-------------|-------|
19
+
|`exec` (EPIC-13) |**Yes**| Primary use case — subprocess on worker host |
- Worker starts a local HTTP server on `--relay-port` (default: 0 = disabled)
60
-
- Worker injects `CRONCONTROL_RELAY_URL=http://localhost:9091` into task environment
61
-
- SDKs check `CRONCONTROL_RELAY_URL` first, fall back to `CRONCONTROL_URL`
58
+
The control plane verifies the worker credential, checks the run/job belongs to the worker's workspace, and executes the operation internally.
62
59
63
-
### Authentication Model
60
+
**Heartbeats** are the exception — they are unauthenticated on both relay and control plane, so they are forwarded directly to `POST /api/v1/heartbeat` without the relay endpoint.
64
61
65
-
The control plane includes a **task-scoped API key**in the task payload dispatched to the worker. This key has the same permissions as the workspace API key but is short-lived (expires when the task finishes).
**Heartbeats**: unauthenticated (both on relay and control plane). No key needed.
82
-
**Results, chat, artifacts**: relay injects the task-scoped API key from the stored payload.
83
-
84
-
This requires:
85
-
- New field `TaskAPIKey string` in `worker.Task` struct
86
-
- Control plane generates a short-lived key on dispatch, includes it in the task payload
87
-
- Worker stores it per active task and adds `X-API-Key` header when forwarding
88
-
89
-
### Environment Variable Naming
69
+
When enabled:
70
+
- Worker starts local HTTP server on `--relay-port` (default: 0 = disabled)
71
+
- Worker injects `CRONCONTROL_RELAY_URL=http://localhost:9091` into subprocess env
72
+
- SDKs detect `CRONCONTROL_RELAY_URL` and route write operations through it
90
73
91
-
Current state (inconsistent):
92
-
- Worker injects: `CRONCONTROL_API_URL`
93
-
- SDKs read: `CRONCONTROL_URL`
94
-
- Relay adds: `CRONCONTROL_RELAY_URL`
74
+
### Environment Variables (Breaking Change)
95
75
96
-
**Resolution**: Standardize on `CRONCONTROL_URL` everywhere. The worker should inject `CRONCONTROL_URL`(not `CRONCONTROL_API_URL`). SDKs already read it. The relay URL is a separate variable:
--relay-max-buffer 1000 # Max buffered heartbeats (default 1000)
164
-
--relay-retry-count 3 # Retries for forwarded calls (default 3)
165
-
```
166
-
167
-
No control plane configuration needed beyond generating the task-scoped API key on dispatch.
134
+
## Control Plane: Relay Endpoint
168
135
169
-
## Data Model
170
-
171
-
### Task struct change
136
+
New endpoint on the control plane:
172
137
173
138
```go
174
-
typeTaskstruct {
175
-
// ... existing fields
176
-
TaskAPIKeystring`json:"task_api_key,omitempty"`// short-lived workspace key for relay forwarding
139
+
// POST /api/v1/workers/{id}/relay
140
+
// Auth: worker credential
141
+
func(s *Service) WorkerRelay(whttp.ResponseWriter, r *http.Request) {
142
+
worker:=authenticateWorkerRequest(r)
143
+
144
+
varreqstruct {
145
+
Method string`json:"method"`
146
+
Path string`json:"path"`
147
+
Body json.RawMessage`json:"body"`
148
+
}
149
+
150
+
// Validate path is in allowlist
151
+
// Execute the operation internally with workspace context
152
+
// Return result to relay
177
153
}
178
154
```
179
155
180
-
### Task-scoped API key
181
-
182
-
Generated on dispatch, stored in `api_keys` table with:
183
-
-`name`: `task_{run_id}` or `task_{job_id}`
184
-
-`role`: `operator`
185
-
-`expires_at`: task timeout or 24h max
186
-
- Deleted on task completion
187
-
188
-
No schema migration needed — uses existing `api_keys` table.
189
-
190
-
## Implementation Notes
156
+
Allowlisted paths:
157
+
-`PATCH /api/v1/runs/*/result`
158
+
-`POST /api/v1/orchestras/*/chat`
159
+
-`POST /api/v1/runs/*/artifacts`
191
160
192
-
### SSH/SSM Detached Env Gap
161
+
##Configuration
193
162
194
-
The SSH detached start path (`ssh.go` line 140) does not pass `params.Environment` to `runRemoteCommand`. This is a pre-existing gap. Even without the relay, `CRONCONTROL_URL` and custom env vars are not available to detached SSH tasks. This should be fixed independently of EPIC-12.
--relay-retry-count 3 # Retries for forwarded calls
168
+
```
195
169
196
-
### K8s Sidecar Pattern
170
+
##Data Model
197
171
198
-
For K8s, the relay can work if the worker runs as a sidecar container in the same pod as the task. In this case, `localhost` correctly resolves within the pod network. This is an advanced deployment pattern documented separately.
172
+
No database changes. No new tables. The relay endpoint uses existing worker authentication.
199
173
200
174
## Security
201
175
202
-
- Relay listens on `localhost` only — not exposed to external network
203
-
- No authentication on relay endpoints (same-host trust model)
204
-
- Relay authenticates to control plane using task-scoped API key (not worker credential)
205
-
- Task-scoped API key is short-lived and auto-deleted on task completion
206
-
- Artifacts forwarded with task-scoped key (workspace-scoped permissions)
207
-
208
-
## Out of Scope
209
-
210
-
- Persistent buffering (disk-based) — memory only
211
-
- Relay for SSH/SSM tasks (they run on remote hosts)
212
-
- Multi-worker relay aggregation
213
-
- WebSocket/SSE relay for real-time chat streaming
0 commit comments