Skip to content

Commit 54dba16

Browse files
authored
feat: add isolated FakeCo staging surface (#44)
* feat: add isolated FakeCo staging * docs: freeze FakeCo OpenClaw contract * fix: remove unreachable client wrapper
1 parent 03feef8 commit 54dba16

31 files changed

Lines changed: 1816 additions & 32 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ playwright-report/
2323
cloudflare-seed/uploads/*
2424
!cloudflare-seed/uploads/.gitkeep
2525
supabase/.temp/
26+
deploy/fakeco/.env
27+
deploy/fakeco/seed-manifest.json

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Added an isolated FakeCo small-VM deployment path with idempotent synthetic chat seed data, OpenClaw and ClawRouter SecretRef configuration, correlated health/readiness and metadata-only telemetry, a quoted-reply end-to-end canary, tests, and teardown guidance.
6+
57
## 0.1.0 - 2026-07-06
68

79
- Published automated macOS, Windows, and Linux desktop installers with verified SHA-256 manifests alongside every tagged GitHub Release.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ short `read_when` hint at the top — open the one that matches your change.
4747
- **Start here:** [docs/README.md](docs/README.md) — landing page + index.
4848
- [Architecture](docs/architecture/overview.md) — process layout, durable vs
4949
realtime split.
50+
- [FakeCo staging](docs/fakeco.md) — isolated Docker/VM deployment, deterministic synthetic data, and OpenClaw round-trip canary.
5051
- [API overview](docs/api/overview.md) — REST/WebSocket surface and where to
5152
find each endpoint.
5253
- [Data model](docs/data-model.md) — tables, IDs, invariants.

apps/api/cmd/clickclack/client.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ type clientConfig struct {
3838
}
3939

4040
type apiClient struct {
41-
opts clientOptions
42-
defaults clientDefaults
43-
http *http.Client
41+
opts clientOptions
42+
defaults clientDefaults
43+
http *http.Client
44+
correlationID string
4445
}
4546

4647
type clientDefaults struct {
@@ -71,6 +72,8 @@ func client(args []string) error {
7172
return c.whoami(rest[1:])
7273
case "status":
7374
return c.status(rest[1:])
75+
case "canary":
76+
return c.canary(rest[1:])
7477
case "workspaces":
7578
return c.workspaces(rest[1:])
7679
case "channels":
@@ -161,27 +164,39 @@ func (c apiClient) withOptions(opts clientOptions, useStoredToken bool) apiClien
161164
}
162165

163166
func (c apiClient) currentUser() (store.User, error) {
167+
return c.currentUserContext(context.Background())
168+
}
169+
170+
func (c apiClient) currentUserContext(ctx context.Context) (store.User, error) {
164171
var result struct {
165172
User store.User `json:"user"`
166173
}
167-
if err := c.get("/api/me", &result); err != nil {
174+
if err := c.doJSON(ctx, http.MethodGet, "/api/me", nil, &result); err != nil {
168175
return store.User{}, err
169176
}
170177
return result.User, nil
171178
}
172179

173180
func (c apiClient) listWorkspaces() ([]store.Workspace, error) {
181+
return c.listWorkspacesContext(context.Background())
182+
}
183+
184+
func (c apiClient) listWorkspacesContext(ctx context.Context) ([]store.Workspace, error) {
174185
var result struct {
175186
Workspaces []store.Workspace `json:"workspaces"`
176187
}
177-
if err := c.get("/api/workspaces", &result); err != nil {
188+
if err := c.doJSON(ctx, http.MethodGet, "/api/workspaces", nil, &result); err != nil {
178189
return nil, err
179190
}
180191
return result.Workspaces, nil
181192
}
182193

183194
func (c apiClient) resolveWorkspace() (store.Workspace, error) {
184-
items, err := c.listWorkspaces()
195+
return c.resolveWorkspaceContext(context.Background())
196+
}
197+
198+
func (c apiClient) resolveWorkspaceContext(ctx context.Context) (store.Workspace, error) {
199+
items, err := c.listWorkspacesContext(ctx)
185200
if err != nil {
186201
return store.Workspace{}, err
187202
}
@@ -201,24 +216,32 @@ func (c apiClient) resolveWorkspace() (store.Workspace, error) {
201216
}
202217

203218
func (c apiClient) listChannels(workspaceID string) ([]store.Channel, error) {
219+
return c.listChannelsContext(context.Background(), workspaceID)
220+
}
221+
222+
func (c apiClient) listChannelsContext(ctx context.Context, workspaceID string) ([]store.Channel, error) {
204223
var result struct {
205224
Channels []store.Channel `json:"channels"`
206225
}
207-
if err := c.get("/api/workspaces/"+url.PathEscape(workspaceID)+"/channels", &result); err != nil {
226+
if err := c.doJSON(ctx, http.MethodGet, "/api/workspaces/"+url.PathEscape(workspaceID)+"/channels", nil, &result); err != nil {
208227
return nil, err
209228
}
210229
return result.Channels, nil
211230
}
212231

213232
func (c apiClient) resolveChannel() (store.Channel, error) {
233+
return c.resolveChannelContext(context.Background())
234+
}
235+
236+
func (c apiClient) resolveChannelContext(ctx context.Context) (store.Channel, error) {
214237
needle := strings.TrimSpace(c.opts.Channel)
215238
if strings.HasPrefix(needle, "chn_") {
216-
workspaces, err := c.channelSearchWorkspaces()
239+
workspaces, err := c.channelSearchWorkspacesContext(ctx)
217240
if err != nil {
218241
return store.Channel{}, err
219242
}
220243
for _, workspace := range workspaces {
221-
items, err := c.listChannels(workspace.ID)
244+
items, err := c.listChannelsContext(ctx, workspace.ID)
222245
if err != nil {
223246
return store.Channel{}, err
224247
}
@@ -230,11 +253,11 @@ func (c apiClient) resolveChannel() (store.Channel, error) {
230253
}
231254
return store.Channel{}, fmt.Errorf("channel %q not found", needle)
232255
}
233-
workspace, err := c.resolveWorkspace()
256+
workspace, err := c.resolveWorkspaceContext(ctx)
234257
if err != nil {
235258
return store.Channel{}, err
236259
}
237-
items, err := c.listChannels(workspace.ID)
260+
items, err := c.listChannelsContext(ctx, workspace.ID)
238261
if err != nil {
239262
return store.Channel{}, err
240263
}
@@ -257,15 +280,15 @@ func (c apiClient) resolveChannel() (store.Channel, error) {
257280
return store.Channel{}, fmt.Errorf("channel %q not found", needle)
258281
}
259282

260-
func (c apiClient) channelSearchWorkspaces() ([]store.Workspace, error) {
283+
func (c apiClient) channelSearchWorkspacesContext(ctx context.Context) ([]store.Workspace, error) {
261284
if strings.TrimSpace(c.opts.Workspace) != "" {
262-
workspace, err := c.resolveWorkspace()
285+
workspace, err := c.resolveWorkspaceContext(ctx)
263286
if err != nil {
264287
return nil, err
265288
}
266289
return []store.Workspace{workspace}, nil
267290
}
268-
return c.listWorkspaces()
291+
return c.listWorkspacesContext(ctx)
269292
}
270293

271294
func (c apiClient) get(path string, out any) error {
@@ -290,6 +313,9 @@ func (c apiClient) doJSON(ctx context.Context, method, path string, body any, ou
290313
if c.opts.UserID != "" {
291314
req.Header.Set("X-ClickClack-User", c.opts.UserID)
292315
}
316+
if c.correlationID != "" {
317+
req.Header.Set("X-Correlation-ID", c.correlationID)
318+
}
293319
resp, err := c.http.Do(req)
294320
if err != nil {
295321
return err

0 commit comments

Comments
 (0)