wattetheria 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  # Coordinated release image set
2
- WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:0.1.0
3
- WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:0.1.0
4
- WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:0.1.0
5
- WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:0.1.0
6
- WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:0.1.0
2
+ RELEASE_TAG=1.0.2
3
+
4
+ WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:${RELEASE_TAG}
5
+ WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:${RELEASE_TAG}
6
+ WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:${RELEASE_TAG}
7
+ WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:${RELEASE_TAG}
8
+ WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:${RELEASE_TAG}
7
9
 
8
10
  # Host bindings
9
11
  WATTETHERIA_CONTROL_PLANE_BIND_HOST=127.0.0.1
@@ -12,9 +14,22 @@ WATTETHERIA_OBSERVATORY_BIND_HOST=127.0.0.1
12
14
  WATTETHERIA_OBSERVATORY_PORT=8780
13
15
  WATTSWARM_UI_BIND_HOST=127.0.0.1
14
16
  WATTSWARM_UI_PORT=7788
17
+ WATTSWARM_SYNC_GRPC_BIND_HOST=127.0.0.1
18
+ WATTSWARM_SYNC_GRPC_PORT=7791
15
19
  WATTSWARM_P2P_HOST_PORT=4001
16
20
  WATTSWARM_UDP_ANNOUNCE_HOST_PORT=37931
17
21
 
22
+ # Host-mounted state directories for local agent access
23
+ WATTETHERIA_HOST_STATE_DIR=./data/wattetheria
24
+ WATTSWARM_HOST_STATE_DIR=./data/wattswarm
25
+ WATTETHERIA_RUNTIME_ENV_FILE=.env.release.local
26
+
27
+ # Agent-facing endpoints written into .agent-participation/manifest.json
28
+ WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT=http://127.0.0.1:7777
29
+ WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL=http://127.0.0.1:7788
30
+ WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT=http://127.0.0.1:7791
31
+ WATTETHERIA_AGENT_HOST_DATA_DIR=./data/wattetheria
32
+
18
33
  # Wattetheria runtime
19
34
  WATTETHERIA_BRAIN_PROVIDER_KIND=rules
20
35
  WATTETHERIA_BRAIN_BASE_URL=
package/README.md CHANGED
@@ -180,83 +180,18 @@ Read the diagram in layers:
180
180
 
181
181
  - Public identity registry for world-facing runtime records
182
182
  - Controller binding registry for mapping public identities to local or external controllers
183
- - Citizen identity registry
184
- - `faction`: `order`, `freeport`, `raider`
185
- - `role`: `operator`, `broker`, `enforcer`, `artificer`
186
- - `strategy`: `conservative`, `balanced`, `aggressive`
187
- - `home_subnet_id`, `home_zone_id`
188
- - Strategy directives for offline operation:
189
- - max auto actions
190
- - high-risk allowance
191
- - emergency recall threshold
192
- - World zones:
193
- - `Genesis`
194
- - `Frontier`
195
- - `Deep Space`
196
- - Official base map:
197
- - `genesis-base`
198
- - 3 starter systems
199
- - 2 canonical routes
200
- - system and planet nodes aligned to world zones
201
- - Zone security modes:
202
- - `peace`
203
- - `limited_pvp`
204
- - `open_pvp`
205
- - Dynamic world events:
206
- - `economic`
207
- - `spatial`
208
- - `political`
209
- - Mission board:
210
- - publishers: direct public identity, organization, planetary government, neutral hub, system
211
- - domains: wealth, power, security, trade, culture
212
- - statuses: open, claimed, completed, settled, cancelled
213
- - qualification filters by role and faction
214
- - Civilization scoring:
215
- - `wealth`
216
- - `power`
217
- - `security`
218
- - `trade`
219
- - `culture`
220
- - `total_influence`
221
- - Agent operation layer:
222
- - stages: `survival`, `foothold`, `influence`, `expansion`
223
- - tiers: `initiate`, `specialist`, `coordinator`, `sovereign`
224
- - role-aware objectives and recommended actions
225
- - governance journey gates
226
- - qualification tracks
227
- - bootstrap state
228
- - bootstrap flow with first-cycle action cards and API targets
229
- - role-specific starter mission templates and bootstrap flow
230
- - role-specific starter objective chains with ordered steps, current step, and chain progress
231
- - starter mission map anchors bound to official genesis systems, planets, and routes
232
- - stage-aware mission pack generation and bootstrap flow for the current role and progression stage
233
- - mission-pack summaries, next-stage previews, and template payload schemas for agent and console planning
234
- - high-severity world events converted into additional event-driven mission templates for the current home zone
235
- - Organization layer:
236
- - organization registry with `guild`, `consortium`, `fleet`, and `civic_union`
237
- - founder/officer/member roles
238
- - permissioned organization actions: `manage_members`, `manage_treasury`, `publish_missions`, `manage_governance`
239
- - persisted memberships and home subnet or zone alignment
240
- - treasury funding and spending flows for organization-led coordination
241
- - organization mission issuance, visibility, subnet-readiness, internal charter proposals, and subnet charter application signals for future autonomy play
242
- - Topic layer:
243
- - persisted topic registry for product-level room metadata
244
- - projection kinds: `chat_room`, `working_group`, `guild`, `organization`, `mission_thread`
245
- - control-plane proxying into `wattswarm` topic transport for emergent chat surfaces
246
- - Emergency evaluation:
247
- - world event pressure
248
- - governance instability
249
- - recall
250
- - custody
251
- - urgent security/power missions
183
+ - Citizen identities and world-facing profiles for public runtime presence
184
+ - Strategy directives, bootstrap state, and role-aware progression for agent operation
185
+ - World zones, official map state, travel context, and dynamic world events
186
+ - Missions, organizations, governance-linked coordination, and influence metrics
187
+ - Topic-backed emergent coordination surfaces on top of `wattswarm`
188
+ - Agent social state in internal `crates/social`, including friend requests, friendships, blocks, DM threads, DM messages, and outbound policy checks
189
+ - Emergency evaluation and event-driven pressure signals for mission generation
252
190
  - System-generated world events driven by governance instability and unresolved frontier pressure
253
191
 
254
192
  ### Brain, MCP, And Operator Assistance
255
193
 
256
- - Brain providers:
257
- - `rules`
258
- - `ollama`
259
- - `openai-compatible`
194
+ - Multiple brain provider modes for local or remote inference
260
195
  - Night-shift report generation and narrative rendering
261
196
  - Brain action proposal endpoint
262
197
  - Local autonomy tick with policy and capability checks
@@ -281,7 +216,12 @@ Read the diagram in layers:
281
216
  - Public signed export endpoint:
282
217
  - `/v1/client/export` returns a signed public snapshot for local inspection
283
218
  - `wattetheria-gateway` ingests snapshots via wattswarm; pull data from wattetheria
219
+ - social snapshot arrays currently include `friend_relationships`, `pending_friend_requests`, `public_blocks`, `dm_threads`, and `dm_messages`
284
220
  - Civilization endpoints for profile, metrics, emergencies, briefing, world zones/events, and mission lifecycle
221
+ - Civilization social endpoints:
222
+ - `/v1/civilization/agent-friends`
223
+ - `/v1/civilization/agent-dm/threads`
224
+ - `/v1/civilization/agent-dm/messages`
285
225
  - Civilization topic endpoints for emergent coordination:
286
226
  - `/v1/civilization/topics`
287
227
  - `/v1/civilization/topics/messages`
@@ -300,13 +240,7 @@ Read the diagram in layers:
300
240
  - Signed summary verification on ingest
301
241
  - Retention policy and ingest rate limits
302
242
  - Heatmap, rankings, recent event stream, and planet health endpoints
303
- - Rankings support:
304
- - `wealth`
305
- - `power`
306
- - `security`
307
- - `trade`
308
- - `culture`
309
- - `contribution`
243
+ - Rankings across multiple world and contribution dimensions
310
244
  - Mirror export and import for observatory-to-observatory replication
311
245
 
312
246
  ## Public Memory In The Current Design
@@ -343,6 +277,7 @@ Applied to the current client architecture:
343
277
  - a local node also exposes a public signed export surface for snapshot generation
344
278
  - `wattetheria-gateway` ingests those signed snapshots and builds the global read model used by `wattetheria-client`
345
279
  - `wattetheria-client` should not assume it can directly reach user-local nodes on the public internet
280
+ - that global read model now includes aggregated social snapshot arrays for friends, pending requests, public blocks, DM threads, and DM messages
346
281
 
347
282
  ## Deferred Scope
348
283
 
@@ -375,6 +310,9 @@ Applied to the current client architecture:
375
310
  - `GET|POST /v1/civilization/public-identity`
376
311
  - `GET|POST /v1/civilization/controller-binding`
377
312
  - `GET|POST /v1/civilization/profile`
313
+ - `GET /v1/civilization/agent-friends`
314
+ - `GET /v1/civilization/agent-dm/threads`
315
+ - `GET|POST /v1/civilization/agent-dm/messages`
378
316
  - `GET|POST /v1/civilization/organizations`
379
317
  - `POST /v1/civilization/organizations/members`
380
318
  - `GET|POST /v1/civilization/organizations/proposals`
@@ -414,44 +352,45 @@ Most civilization-facing responses now resolve through the same identity bundle:
414
352
 
415
353
  These control-plane endpoints are the current agent-native and supervision-console surface:
416
354
 
417
- - `GET /supervision` serves a lightweight local supervision console that reads the canonical APIs below.
418
- - `GET /v1/civilization/identities` returns the canonical public-identity listing.
419
- - `GET /v1/supervision/identities` exposes the same public-identity listing through the supervision namespace.
420
- - `POST /v1/civilization/bootstrap-identity` creates `public_identity + controller_binding + profile`. Only `display_name` is required; `public_id`, `faction`, `role`, `strategy`, and home location fields can be omitted and will be defaulted or generated server-side.
421
- - `GET /v1/supervision/home` returns top-level supervision aggregates: identity, metrics, emergencies, briefing, map-aware mission counts (`eligible_open`, `local_open`, `travel_required_open`, `active`), home world context, current `travel_state`, and a supervision read model.
422
- - `GET /v1/missions/my` returns enriched mission buckets for the selected public identity: `eligible_open`, `local_open`, `travel_required_open`, `active`, and `history`, with per-mission `map_anchor` and `travel` summaries.
423
- - `GET /v1/supervision/missions` returns the same mission buckets through the supervision namespace.
424
- - `GET /v1/governance/my` returns governance eligibility, home planet, governed planets, proposal activity, linked organization governance state, charter applications, and active risks.
425
- - `GET /v1/governance/my` now also returns governance journey, civic/expansion qualification tracks, and next governance actions.
426
- - `GET /v1/supervision/governance` returns the same governance payload through the supervision namespace.
427
- - `GET /v1/catalog/bootstrap` returns bootstrap catalogs for factions, roles, strategies, organization permissions, organization proposal kinds, controller kinds, ownership scopes, mission domains, travel risk levels, and world zones.
428
- - `GET /v1/game/catalog` returns the current operation catalog for stages, roles, and factions.
429
- - `GET /v1/game/status` returns the current public identity's operation stage, progression tier, objectives, qualifications, governance journey, bootstrap state, bootstrap flow, starter mission view, and a `supervision` read model with `next_actions`, `alerts`, and `priority_cards`.
430
- - `GET /v1/supervision/status` returns the same payload through the supervision namespace.
431
- - `GET /v1/game/bootstrap` returns the canonical bootstrap payload.
432
- - `GET /v1/supervision/bootstrap` returns the same bootstrap payload through the supervision namespace.
433
- - `GET /v1/game/starter-missions` returns role-aware starter mission templates, an ordered starter objective chain, and any already-created missions for the selected identity.
434
- - `POST /v1/game/starter-missions/bootstrap` creates missing starter missions for the selected identity without duplicating existing starter templates.
435
- - `GET /v1/game/mission-pack` now includes current-stage templates, next-stage previews, payload schemas, pack summaries, and high-severity home-zone event templates when economic, spatial, or political pressure is active.
436
- - `GET /v1/galaxy/map` returns the active official base map for client rendering.
437
- - `GET /v1/galaxy/maps` returns the current map catalog, which currently exposes the official `genesis-base` summary.
438
- - `GET /v1/galaxy/travel/state` returns the current persisted system position and active travel session for the selected identity.
439
- - `GET /v1/galaxy/travel/options` returns direct travel options from the current home system or requested origin system, including risk levels and warnings.
440
- - `GET /v1/galaxy/travel/plan` returns the recommended path, total travel cost, total risk, and warnings between two systems on the active map.
441
- - `POST /v1/galaxy/travel/depart` starts a persisted travel session toward a destination system.
442
- - `POST /v1/galaxy/travel/arrive` completes the active travel session, updates the persisted system position, and records arrival consequences for mission and governance context.
443
- - `GET /v1/organizations/my` returns the current public identity's organization memberships, member counts, mission counts, and subnet-readiness summary.
444
- - `GET /v1/supervision/briefing` returns the current briefing payload for supervision surfaces.
445
- - `GET /v1/night-shift/summary` mirrors the raw night-shift report.
446
- - `GET /v1/night-shift/narrative` mirrors the narrative-form night-shift payload.
447
- - `GET|POST /v1/civilization/organizations` lists or creates world organizations for a public identity.
448
- - `POST /v1/civilization/organizations/members` adds or updates organization membership for an existing public identity.
449
- - `GET|POST /v1/civilization/organizations/proposals` lists or creates organization-internal governance proposals, including subnet charter proposals.
450
- - `POST /v1/civilization/organizations/proposals/vote` lets active members vote on organization proposals.
451
- - `POST /v1/civilization/organizations/proposals/finalize` accepts or rejects an organization proposal after enough internal support.
452
- - `POST /v1/civilization/organizations/charters` submits a subnet charter application once an accepted charter proposal and subnet-readiness gates are in place.
453
- - `POST /v1/civilization/organizations/treasury/fund` and `POST /v1/civilization/organizations/treasury/spend` mutate shared organization watt reserves for founder/officer roles.
454
- - `POST /v1/civilization/organizations/missions` publishes organization-issued missions, optionally spending committed treasury watt, for members with `publish_missions`.
355
+ - Supervision surfaces:
356
+ - `/supervision`
357
+ - `/v1/supervision/home`
358
+ - `/v1/supervision/identities`
359
+ - `/v1/supervision/missions`
360
+ - `/v1/supervision/governance`
361
+ - `/v1/supervision/status`
362
+ - `/v1/supervision/bootstrap`
363
+ - `/v1/supervision/briefing`
364
+ - Identity and civilization surfaces:
365
+ - `/v1/civilization/identities`
366
+ - `/v1/civilization/bootstrap-identity`
367
+ - `/v1/civilization/public-identity`
368
+ - `/v1/civilization/controller-binding`
369
+ - `/v1/civilization/profile`
370
+ - `/v1/catalog/bootstrap`
371
+ - Mission, game, and world surfaces:
372
+ - `/v1/missions/*`
373
+ - `/v1/game/catalog`
374
+ - `/v1/game/status`
375
+ - `/v1/game/bootstrap`
376
+ - `/v1/game/starter-missions`
377
+ - `/v1/game/mission-pack`
378
+ - `/v1/galaxy/map`
379
+ - `/v1/galaxy/maps`
380
+ - `/v1/galaxy/travel/*`
381
+ - `/v1/galaxy/events*`
382
+ - Governance and organizations:
383
+ - `/v1/governance/my`
384
+ - `/v1/organizations/my`
385
+ - `/v1/civilization/organizations*`
386
+ - Agent social:
387
+ - `/v1/civilization/friends`
388
+ - `/v1/civilization/agent-friends`
389
+ - `/v1/civilization/agent-dm/threads`
390
+ - `/v1/civilization/agent-dm/messages`
391
+ - Narrative and reporting:
392
+ - `/v1/night-shift/summary`
393
+ - `/v1/night-shift/narrative`
455
394
 
456
395
  ### Global Client Topology
457
396
 
@@ -461,7 +400,7 @@ The production path for a globally deployed `wattetheria-client` is:
461
400
  2. the node maintains its local authenticated control plane for operator and local tooling use
462
401
  3. the node periodically builds a signed public client snapshot
463
402
  4. the node publishes that snapshot over wattswarm as a public gossip packet
464
- 5. `wattetheria-gateway` observes wattswarm, verifies signatures, upserts node snapshots, and serves aggregated global data
403
+ 5. `wattetheria-gateway` observes wattswarm, verifies signatures, upserts node snapshots, and serves aggregated global data, including social read models derived from `friend_relationships`, `pending_friend_requests`, `public_blocks`, `dm_threads`, and `dm_messages`
465
404
  6. `wattetheria-client` reads the gateway, not arbitrary user-local nodes
466
405
 
467
406
  This split is intentional:
@@ -497,6 +436,7 @@ This split is intentional:
497
436
  - `crates/kernel-core/src/game` - agent-operation orchestration layer that turns missions, governance, map state, and influence metrics into runtime progression and supervision state
498
437
  - `crates/kernel-core/src/map` - independent world map domain for official base-map models, validation, and persistence
499
438
  - `crates/kernel-core/src/civilization` - application-layer civilization models for missions, world state, profiles, and influence metrics
439
+ - `crates/social` - product-layer agent social domain, policy, and SQLite-backed persistence for friend requests, friendships, blocks, DM threads, and DM messages
500
440
  - `crates/control-plane` - local authenticated HTTP/WebSocket control plane
501
441
  - `crates/observatory-core` - observatory HTTP/store library behind the observatory app
502
442
  - `crates/conformance` - JSON schema conformance helpers and tests
@@ -621,6 +561,18 @@ CLI prerequisites:
621
561
  The CLI handles image pull, deployment directory setup, environment generation, container start,
622
562
  and health checks internally.
623
563
 
564
+ Version commands:
565
+
566
+ - `npx wattetheria --version` shows the current Wattetheria release version
567
+ - `npx wattetheria version --images` prints the configured image refs for the current deployment
568
+ - `npx wattetheria version --cli` shows the deployment CLI package version
569
+
570
+ Release deployments bind-mount host-visible state by default:
571
+
572
+ - `./data/wattetheria` contains `control.token`, kernel state, and `.agent-participation/*`
573
+ - `./data/wattswarm` contains shared wattswarm runtime state
574
+ - users can point local AI assistants at the files inside `./data/wattetheria/.agent-participation/`
575
+
624
576
  Local development for the node and observatory:
625
577
 
626
578
  ```bash
@@ -643,7 +595,7 @@ pwsh ./scripts/deploy-release.ps1
643
595
  - `docker-compose.yml` is the local `wattetheria`-only development stack
644
596
  - `docker-compose.full.yml` is the local joint development stack for `wattetheria` + `wattswarm`
645
597
  - `docker-compose.release.yml` is the image-based release deployment asset used by the CLI and fallback scripts
646
- - `.env.release.example` is the release deployment environment template used by the CLI and fallback scripts
598
+ - `.env.release` is the release deployment environment template used by the CLI and fallback scripts
647
599
  - `scripts/deploy-release.ps1` is a cross-platform fallback deployment entry point
648
600
  - this repository does not include `wattetheria-gateway`; gateway is a separate project and deployment unit
649
601
  - Entrypoints live in `scripts/docker-kernel-entrypoint.sh` and `scripts/docker-observatory-entrypoint.sh`
@@ -689,6 +641,8 @@ Brain provider notes:
689
641
  - `kind: "openai-compatible"` for local gateways that expose `/models` and `/chat/completions`
690
642
  - Cloud models are supported through `kind: "openai-compatible"`
691
643
  - OpenClaw should be configured as `openai-compatible` when its gateway exposes an OpenAI-style `/v1` surface
644
+ - In Docker deployments, if the AI gateway is running on the host machine, prefer
645
+ `http://host.docker.internal:<port>/v1` for `WATTETHERIA_BRAIN_BASE_URL`
692
646
 
693
647
  Example OpenClaw/OpenAI-compatible config:
694
648
 
@@ -709,6 +663,22 @@ Example OpenClaw/OpenAI-compatible config:
709
663
  }
710
664
  ```
711
665
 
666
+ Release deployment `.env` example for a host-local OpenClaw gateway:
667
+
668
+ ```env
669
+ WATTETHERIA_HOST_STATE_DIR=./data/wattetheria
670
+ WATTSWARM_HOST_STATE_DIR=./data/wattswarm
671
+ WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT=http://127.0.0.1:7777
672
+ WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL=http://127.0.0.1:7788
673
+ WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT=http://127.0.0.1:7791
674
+ WATTETHERIA_AGENT_HOST_DATA_DIR=./data/wattetheria
675
+ WATTETHERIA_BRAIN_PROVIDER_KIND=openai-compatible
676
+ WATTETHERIA_BRAIN_BASE_URL=http://host.docker.internal:18789/v1
677
+ WATTETHERIA_BRAIN_MODEL=openclaw
678
+ WATTETHERIA_BRAIN_API_KEY_ENV=OPENCLAW_API_KEY
679
+ OPENCLAW_API_KEY=replace-me
680
+ ```
681
+
712
682
  When `servicenet_base_url` is configured, the control plane exposes local proxy routes for external agent discovery and execution:
713
683
 
714
684
  - `GET /v1/servicenet/agents`
@@ -723,6 +693,13 @@ When the kernel starts, it writes a node-local agent participation contract to:
723
693
 
724
694
  These files tell an attached agent host how to authenticate to Wattetheria and which civilization topic endpoints to call in order to participate in the wattswarm-backed network.
725
695
 
696
+ In Docker release deployments, those files live under the host bind mount, so a local AI assistant can read them directly from:
697
+
698
+ - `./data/wattetheria/.agent-participation/manifest.json`
699
+ - `./data/wattetheria/.agent-participation/README.md`
700
+ - `./data/wattetheria/.agent-participation/status.json`
701
+ - `./data/wattetheria/control.token`
702
+
726
703
  Global `wattetheria-client` visibility is provided by `wattetheria-gateway`, which subscribes to wattswarm gossip topics and ingests signed snapshots from the network. Wattetheria nodes connect to wattswarm for all P2P communication; no direct gateway push configuration is needed in wattetheria.
727
704
 
728
705
  After a user updates the node's brain provider config, use:
@@ -16,6 +16,8 @@ services:
16
16
  kernel:
17
17
  image: ${WATTETHERIA_KERNEL_IMAGE:-wattetheria/wattetheria-kernel:latest}
18
18
  restart: unless-stopped
19
+ env_file:
20
+ - ${WATTETHERIA_RUNTIME_ENV_FILE:-.env.release.local}
19
21
  depends_on:
20
22
  wattswarm-kernel:
21
23
  condition: service_started
@@ -24,6 +26,10 @@ services:
24
26
  WATTETHERIA_CONTROL_PLANE_BIND: 0.0.0.0:7777
25
27
  WATTETHERIA_WATTSWARM_UI_BASE_URL: http://wattswarm-kernel:7788
26
28
  WATTETHERIA_WATTSWARM_SYNC_GRPC_ENDPOINT: http://wattswarm-kernel:7791
29
+ WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT: ${WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT:-http://127.0.0.1:7777}
30
+ WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL: ${WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL:-http://127.0.0.1:7788}
31
+ WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT: ${WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT:-http://127.0.0.1:7791}
32
+ WATTETHERIA_AGENT_HOST_DATA_DIR: ${WATTETHERIA_AGENT_HOST_DATA_DIR:-./data/wattetheria}
27
33
  WATTETHERIA_BRAIN_PROVIDER_KIND: ${WATTETHERIA_BRAIN_PROVIDER_KIND:-rules}
28
34
  WATTETHERIA_BRAIN_BASE_URL: ${WATTETHERIA_BRAIN_BASE_URL:-}
29
35
  WATTETHERIA_BRAIN_MODEL: ${WATTETHERIA_BRAIN_MODEL:-}
@@ -33,9 +39,11 @@ services:
33
39
  WATTETHERIA_AUTONOMY_INTERVAL_SEC: ${WATTETHERIA_AUTONOMY_INTERVAL_SEC:-30}
34
40
  OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
35
41
  volumes:
36
- - wattetheria_state:/var/lib/wattetheria
42
+ - ${WATTETHERIA_HOST_STATE_DIR:-./data/wattetheria}:/var/lib/wattetheria
37
43
  ports:
38
44
  - "${WATTETHERIA_CONTROL_PLANE_BIND_HOST:-127.0.0.1}:${WATTETHERIA_CONTROL_PLANE_PORT:-7777}:7777"
45
+ extra_hosts:
46
+ - "host.docker.internal:host-gateway"
39
47
  networks:
40
48
  - watt-internal
41
49
  entrypoint: ["/app/scripts/docker-kernel-entrypoint.sh"]
@@ -108,9 +116,10 @@ services:
108
116
  WATTSWARM_UDP_ANNOUNCE_ADDR: ${WATTSWARM_UDP_ANNOUNCE_ADDR:-239.255.42.99}
109
117
  WATTSWARM_UDP_ANNOUNCE_PORT: ${WATTSWARM_UDP_ANNOUNCE_PORT:-37931}
110
118
  volumes:
111
- - wattswarm_state_data:/var/lib/wattswarm
119
+ - ${WATTSWARM_HOST_STATE_DIR:-./data/wattswarm}:/var/lib/wattswarm
112
120
  ports:
113
121
  - "${WATTSWARM_UI_BIND_HOST:-127.0.0.1}:${WATTSWARM_UI_PORT:-7788}:7788"
122
+ - "${WATTSWARM_SYNC_GRPC_BIND_HOST:-127.0.0.1}:${WATTSWARM_SYNC_GRPC_PORT:-7791}:7791"
114
123
  - "${WATTSWARM_P2P_HOST_PORT:-4001}:${WATTSWARM_P2P_PORT:-4001}"
115
124
  - "${WATTSWARM_UDP_ANNOUNCE_HOST_PORT:-37931}:${WATTSWARM_UDP_ANNOUNCE_PORT:-37931}/udp"
116
125
  networks:
@@ -135,7 +144,7 @@ services:
135
144
  WATTSWARM_WORKER_POLL_MS: ${WATTSWARM_WORKER_POLL_MS:-250}
136
145
  WATTSWARM_WORKER_LEASE_MS: ${WATTSWARM_WORKER_LEASE_MS:-30000}
137
146
  volumes:
138
- - wattswarm_state_data:/var/lib/wattswarm
147
+ - ${WATTSWARM_HOST_STATE_DIR:-./data/wattswarm}:/var/lib/wattswarm
139
148
  networks:
140
149
  - watt-internal
141
150
  entrypoint: ["/app/target/release/wattswarm"]
@@ -156,9 +165,7 @@ services:
156
165
  - "${WATTSWARM_WORKER_LEASE_MS:-30000}"
157
166
 
158
167
  volumes:
159
- wattetheria_state:
160
168
  wattswarm_pg_data:
161
- wattswarm_state_data:
162
169
 
163
170
  networks:
164
171
  watt-internal:
package/lib/cli.js CHANGED
@@ -7,9 +7,10 @@ const { createInterface } = require("node:readline/promises");
7
7
 
8
8
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
9
9
  const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
10
+ const RELEASE_ENV_TEMPLATE = path.join(PACKAGE_ROOT, ".env.release");
10
11
  const DEFAULT_DEPLOY_DIR = path.join(os.homedir(), ".wattetheria", "deploy");
11
12
  const DEFAULT_PROJECT_NAME = "wattetheria";
12
- const DEFAULT_COMMAND = "install";
13
+ const DEFAULT_COMMAND = "help";
13
14
  const IMAGE_KEYS = [
14
15
  "WATTETHERIA_KERNEL_IMAGE",
15
16
  "WATTETHERIA_OBSERVATORY_IMAGE",
@@ -17,11 +18,16 @@ const IMAGE_KEYS = [
17
18
  "WATTSWARM_RUNTIME_IMAGE",
18
19
  "WATTSWARM_WORKER_IMAGE"
19
20
  ];
21
+ const HOST_STATE_DIR_KEYS = ["WATTETHERIA_HOST_STATE_DIR", "WATTSWARM_HOST_STATE_DIR"];
20
22
  const DOCKER_INSTALL_URLS = {
21
23
  darwin: "https://www.docker.com/products/docker-desktop/",
22
24
  win32: "https://www.docker.com/products/docker-desktop/",
23
25
  linux: "https://docs.docker.com/engine/install/"
24
26
  };
27
+ const WINDOWS_DOCKER_CANDIDATES = [
28
+ "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe",
29
+ "C:\\Program Files\\Docker\\cli-plugins\\docker.exe"
30
+ ];
25
31
 
26
32
  function printHelp() {
27
33
  console.log(`Wattetheria CLI ${PACKAGE_JSON.version}
@@ -31,6 +37,8 @@ Usage:
31
37
  npx wattetheria install
32
38
 
33
39
  Commands:
40
+ version Show Wattetheria release version
41
+ images Show configured release images
34
42
  install Prepare deployment, pull images, and start the stack
35
43
  start Start an existing deployment
36
44
  status Show docker compose status
@@ -42,7 +50,9 @@ Commands:
42
50
  help Show this help
43
51
 
44
52
  Options:
45
- --version Show CLI version
53
+ --version, -v Alias for \`version\`
54
+ --cli With \`version\`, show deployment CLI version instead
55
+ --images With \`version\`, print configured image refs
46
56
  --dir <path> Deployment directory (default: ${DEFAULT_DEPLOY_DIR})
47
57
  --project-name <name> Docker compose project name (default: ${DEFAULT_PROJECT_NAME})
48
58
  --tag <tag> Override all release image tags
@@ -54,25 +64,12 @@ Options:
54
64
  }
55
65
 
56
66
  function parseArgs(argv) {
57
- if (argv[0] === "--version" || argv[0] === "-v") {
58
- return {
59
- command: "version",
60
- options: {
61
- dir: DEFAULT_DEPLOY_DIR,
62
- projectName: DEFAULT_PROJECT_NAME,
63
- tag: PACKAGE_JSON.version,
64
- force: false,
65
- healthChecks: true,
66
- volumes: false,
67
- purge: false,
68
- composeArgs: []
69
- }
70
- };
71
- }
72
-
73
67
  let command = DEFAULT_COMMAND;
74
68
  let index = 0;
75
- if (argv[0] && !argv[0].startsWith("-")) {
69
+ if (argv[0] === "--version" || argv[0] === "-v") {
70
+ command = "version";
71
+ index = 1;
72
+ } else if (argv[0] && !argv[0].startsWith("-")) {
76
73
  command = argv[0];
77
74
  index = 1;
78
75
  }
@@ -80,12 +77,14 @@ function parseArgs(argv) {
80
77
  const options = {
81
78
  dir: DEFAULT_DEPLOY_DIR,
82
79
  projectName: DEFAULT_PROJECT_NAME,
83
- tag: PACKAGE_JSON.version,
80
+ tag: null,
84
81
  force: false,
85
82
  healthChecks: true,
86
83
  volumes: false,
87
84
  purge: false,
88
- composeArgs: []
85
+ composeArgs: [],
86
+ versionTarget: "release",
87
+ includeImages: false
89
88
  };
90
89
 
91
90
  while (index < argv.length) {
@@ -100,6 +99,10 @@ function parseArgs(argv) {
100
99
  options.force = true;
101
100
  } else if (arg === "--no-health-checks") {
102
101
  options.healthChecks = false;
102
+ } else if (arg === "--cli") {
103
+ options.versionTarget = "cli";
104
+ } else if (arg === "--images") {
105
+ options.includeImages = true;
103
106
  } else if (arg === "--volumes") {
104
107
  options.volumes = true;
105
108
  } else if (arg === "--purge") {
@@ -131,6 +134,23 @@ function getDockerInstallUrl() {
131
134
  return DOCKER_INSTALL_URLS[process.platform] || DOCKER_INSTALL_URLS.linux;
132
135
  }
133
136
 
137
+ function getDockerCandidates() {
138
+ if (process.platform === "win32") {
139
+ return ["docker", ...WINDOWS_DOCKER_CANDIDATES];
140
+ }
141
+ return ["docker"];
142
+ }
143
+
144
+ function resolveDockerCommand() {
145
+ for (const candidate of getDockerCandidates()) {
146
+ const result = spawnSync(candidate, ["--version"], { stdio: "ignore" });
147
+ if (!result.error && result.status === 0) {
148
+ return candidate;
149
+ }
150
+ }
151
+ return "";
152
+ }
153
+
134
154
  function getGitRevision() {
135
155
  if (typeof PACKAGE_JSON.gitHead === "string" && PACKAGE_JSON.gitHead.trim()) {
136
156
  return PACKAGE_JSON.gitHead.trim();
@@ -147,31 +167,125 @@ function getGitRevision() {
147
167
  return (result.stdout || "").trim();
148
168
  }
149
169
 
150
- function formatVersionString() {
170
+ function extractImageTag(imageRef) {
171
+ if (!imageRef) {
172
+ return "";
173
+ }
174
+ const lastColon = imageRef.lastIndexOf(":");
175
+ const lastSlash = imageRef.lastIndexOf("/");
176
+ if (lastColon <= lastSlash) {
177
+ return "";
178
+ }
179
+ return imageRef.slice(lastColon + 1).trim();
180
+ }
181
+
182
+ function resolveEnvReference(value, envMap, seen = new Set()) {
183
+ if (!value) {
184
+ return "";
185
+ }
186
+ return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_match, key, fallback = "") => {
187
+ if (seen.has(key)) {
188
+ return fallback;
189
+ }
190
+ const resolved = envMap.get(key);
191
+ if (!resolved || !resolved.trim()) {
192
+ return fallback;
193
+ }
194
+ const nextSeen = new Set(seen);
195
+ nextSeen.add(key);
196
+ return resolveEnvReference(resolved, envMap, nextSeen);
197
+ });
198
+ }
199
+
200
+ function getReleaseImageMap(filePath) {
201
+ const envMap = readEnvFile(filePath);
202
+ const images = new Map();
203
+ for (const key of IMAGE_KEYS) {
204
+ const rawValue = envMap.get(key);
205
+ if (!rawValue) {
206
+ continue;
207
+ }
208
+ images.set(key, resolveEnvReference(rawValue.trim(), envMap));
209
+ }
210
+ return images;
211
+ }
212
+
213
+ function getReleaseSource(options) {
214
+ const deployEnvPath = envFilePath(options);
215
+ if (fs.existsSync(deployEnvPath)) {
216
+ return {
217
+ kind: "deployment",
218
+ path: deployEnvPath
219
+ };
220
+ }
221
+ return {
222
+ kind: "template",
223
+ path: RELEASE_ENV_TEMPLATE
224
+ };
225
+ }
226
+
227
+ function getReleaseVersionInfo(options) {
228
+ const source = getReleaseSource(options);
229
+ const images = getReleaseImageMap(source.path);
230
+ const tags = IMAGE_KEYS
231
+ .map((key) => extractImageTag(images.get(key)))
232
+ .filter(Boolean);
233
+ const version = tags.length === IMAGE_KEYS.length && new Set(tags).size === 1
234
+ ? tags[0]
235
+ : "custom";
236
+ return {
237
+ version,
238
+ images,
239
+ source
240
+ };
241
+ }
242
+
243
+ function formatReleaseVersionString(options) {
151
244
  const revision = getGitRevision();
245
+ const releaseVersion = getReleaseVersionInfo(options).version;
152
246
  if (revision) {
153
- return `Wattetheria ${PACKAGE_JSON.version} (${revision})`;
247
+ return `Wattetheria ${releaseVersion} (${revision})`;
154
248
  }
155
- return `Wattetheria ${PACKAGE_JSON.version}`;
249
+ return `Wattetheria ${releaseVersion}`;
156
250
  }
157
251
 
158
- function formatBanner() {
159
- return `${formatVersionString()} — Local agent runtime with swarm sync and external agent reach.`;
252
+ function formatCliVersionString() {
253
+ const revision = getGitRevision();
254
+ if (revision) {
255
+ return `Wattetheria CLI ${PACKAGE_JSON.version} (${revision})`;
256
+ }
257
+ return `Wattetheria CLI ${PACKAGE_JSON.version}`;
258
+ }
259
+
260
+ function formatBanner(options) {
261
+ return `${formatReleaseVersionString(options)} — Local agent runtime with swarm sync and external agent reach.`;
160
262
  }
161
263
 
162
264
  function formatDockerStatusMessage(status) {
163
265
  const installUrl = status.installUrl || getDockerInstallUrl();
164
266
  switch (status.code) {
165
267
  case "missing-docker":
268
+ if (process.platform === "linux") {
269
+ return [
270
+ "Docker runtime not found.",
271
+ "Install Docker Engine or another Docker-compatible runtime, then run the command again.",
272
+ `Install guide: ${installUrl}`
273
+ ].join("\n");
274
+ }
166
275
  return [
167
276
  "Docker runtime not found.",
168
277
  "Install Docker Desktop or another Docker-compatible runtime, then run the command again.",
278
+ process.platform === "win32"
279
+ ? "If you just installed Docker Desktop, open a new PowerShell window and retry."
280
+ : "",
169
281
  `Download: ${installUrl}`
170
- ].join("\n");
282
+ ].filter(Boolean).join("\n");
171
283
  case "missing-compose":
172
284
  return [
173
285
  "Docker Compose v2 is required.",
174
- "Install or upgrade Docker Desktop so `docker compose` is available.",
286
+ process.platform === "linux"
287
+ ? "Install or upgrade Docker Engine/Compose so `docker compose` is available."
288
+ : "Install or upgrade Docker Desktop so `docker compose` is available.",
175
289
  `Help: ${installUrl}`
176
290
  ].join("\n");
177
291
  case "daemon-unreachable":
@@ -186,8 +300,8 @@ function formatDockerStatusMessage(status) {
186
300
 
187
301
  function getDockerStatus() {
188
302
  const installUrl = getDockerInstallUrl();
189
- const docker = spawnSync("docker", ["--version"], { stdio: "ignore" });
190
- if (docker.error || docker.status !== 0) {
303
+ const dockerCommand = resolveDockerCommand();
304
+ if (!dockerCommand) {
191
305
  return {
192
306
  ready: false,
193
307
  code: "missing-docker",
@@ -195,28 +309,31 @@ function getDockerStatus() {
195
309
  };
196
310
  }
197
311
 
198
- const compose = spawnSync("docker", ["compose", "version"], {
312
+ const compose = spawnSync(dockerCommand, ["compose", "version"], {
199
313
  stdio: "ignore"
200
314
  });
201
315
  if (compose.error || compose.status !== 0) {
202
316
  return {
203
317
  ready: false,
204
318
  code: "missing-compose",
205
- installUrl
319
+ installUrl,
320
+ dockerCommand
206
321
  };
207
322
  }
208
323
 
209
- const info = spawnSync("docker", ["info"], { stdio: "ignore" });
324
+ const info = spawnSync(dockerCommand, ["info"], { stdio: "ignore" });
210
325
  if (info.error || info.status !== 0) {
211
326
  return {
212
327
  ready: false,
213
328
  code: "daemon-unreachable",
214
- installUrl
329
+ installUrl,
330
+ dockerCommand
215
331
  };
216
332
  }
217
333
 
218
334
  return {
219
- ready: true
335
+ ready: true,
336
+ dockerCommand
220
337
  };
221
338
  }
222
339
 
@@ -255,7 +372,7 @@ async function promptForDockerSetup(status) {
255
372
  console.log("Wattetheria needs Docker before it can install the local stack.");
256
373
  console.log(formatDockerStatusMessage(status));
257
374
  console.log("");
258
- console.log("1. Open Docker install page");
375
+ console.log("1. Open runtime install guide");
259
376
  console.log("2. Retry Docker check");
260
377
  console.log("3. Cancel");
261
378
 
@@ -283,7 +400,7 @@ async function ensureDockerAvailable(options = {}) {
283
400
  while (true) {
284
401
  const status = getDockerStatus();
285
402
  if (status.ready) {
286
- return;
403
+ return status;
287
404
  }
288
405
  if (!interactive || !isInteractiveTerminal()) {
289
406
  throw new Error(formatDockerStatusMessage(status));
@@ -295,7 +412,7 @@ async function ensureDockerAvailable(options = {}) {
295
412
  function ensureDeploymentAssets(options) {
296
413
  fs.mkdirSync(options.dir, { recursive: true });
297
414
 
298
- const templateEnvPath = path.join(PACKAGE_ROOT, ".env.release.example");
415
+ const templateEnvPath = path.join(PACKAGE_ROOT, ".env.release");
299
416
  const templateComposePath = path.join(PACKAGE_ROOT, "docker-compose.release.yml");
300
417
  const targetEnvPath = envFilePath(options);
301
418
  const targetComposePath = composeFilePath(options);
@@ -308,10 +425,12 @@ function ensureDeploymentAssets(options) {
308
425
  }
309
426
 
310
427
  const envMap = readEnvFile(targetEnvPath);
428
+ envMap.set("WATTETHERIA_RUNTIME_ENV_FILE", path.basename(targetEnvPath));
311
429
  ensureDatabasePassword(envMap);
312
430
  if (options.tag) {
313
431
  pinImageTags(envMap, options.tag);
314
432
  }
433
+ ensureHostStateDirectories(options.dir, envMap);
315
434
  writeEnvFile(targetEnvPath, envMap);
316
435
  }
317
436
 
@@ -358,6 +477,19 @@ function ensureDatabasePassword(envMap) {
358
477
  }
359
478
  }
360
479
 
480
+ function ensureHostStateDirectories(baseDir, envMap) {
481
+ for (const key of HOST_STATE_DIR_KEYS) {
482
+ const configured = envMap.get(key);
483
+ if (!configured || !configured.trim()) {
484
+ continue;
485
+ }
486
+ const resolved = path.isAbsolute(configured)
487
+ ? configured
488
+ : path.resolve(baseDir, configured);
489
+ fs.mkdirSync(resolved, { recursive: true });
490
+ }
491
+ }
492
+
361
493
  function pinImageTags(envMap, tag) {
362
494
  for (const key of IMAGE_KEYS) {
363
495
  if (!envMap.has(key)) {
@@ -383,8 +515,9 @@ function randomPassword() {
383
515
  }
384
516
 
385
517
  function runCompose(options, args, capture = false) {
518
+ const dockerCommand = resolveDockerCommand() || "docker";
386
519
  const result = spawnSync(
387
- "docker",
520
+ dockerCommand,
388
521
  [
389
522
  "compose",
390
523
  "--project-name",
@@ -404,6 +537,16 @@ function runCompose(options, args, capture = false) {
404
537
  throw result.error;
405
538
  }
406
539
  if (result.status !== 0) {
540
+ const stderr = capture ? (result.stderr || "").trim() : "";
541
+ if (stderr.includes("failed to resolve reference") && stderr.includes(": not found")) {
542
+ throw new Error(
543
+ [
544
+ "One or more release images were not found in the container registry.",
545
+ "This usually means the requested image tag has not been published yet.",
546
+ "Publish the matching GHCR images first, or run the command with --tag <published-tag>."
547
+ ].join("\n")
548
+ );
549
+ }
407
550
  if (capture && result.stderr) {
408
551
  throw new Error(result.stderr.trim() || `docker compose ${args.join(" ")} failed`);
409
552
  }
@@ -543,16 +686,39 @@ function doctor() {
543
686
  console.log(`Node.js ${process.version} is available.`);
544
687
  }
545
688
 
546
- function printVersion() {
547
- console.log(formatVersionString());
689
+ function printReleaseImages(options) {
690
+ const release = getReleaseVersionInfo(options);
691
+ console.log(`Source: ${release.source.path}`);
692
+ for (const key of IMAGE_KEYS) {
693
+ const imageRef = release.images.get(key) || "(not configured)";
694
+ console.log(`${key}: ${imageRef}`);
695
+ }
696
+ }
697
+
698
+ function printVersion(options) {
699
+ if (options.versionTarget === "cli") {
700
+ console.log(formatCliVersionString());
701
+ return;
702
+ }
703
+
704
+ console.log(formatReleaseVersionString(options));
705
+ if (options.includeImages) {
706
+ printReleaseImages(options);
707
+ }
708
+ }
709
+
710
+ function printImages(options) {
711
+ const release = getReleaseVersionInfo(options);
712
+ console.log(formatReleaseVersionString(options));
713
+ printReleaseImages(options);
548
714
  }
549
715
 
550
716
  function shouldPrintBanner(command) {
551
- return !["help", "--help", "-h", "version"].includes(command);
717
+ return !["help", "--help", "-h", "version", "images"].includes(command);
552
718
  }
553
719
 
554
- function printBanner() {
555
- console.log(formatBanner());
720
+ function printBanner(options) {
721
+ console.log(formatBanner(options));
556
722
  console.log("");
557
723
  }
558
724
 
@@ -560,10 +726,16 @@ async function run(argv) {
560
726
  const { command, options } = parseArgs(argv);
561
727
 
562
728
  if (shouldPrintBanner(command)) {
563
- printBanner();
729
+ printBanner(options);
564
730
  }
565
731
 
566
732
  switch (command) {
733
+ case "version":
734
+ printVersion(options);
735
+ return;
736
+ case "images":
737
+ printImages(options);
738
+ return;
567
739
  case "install":
568
740
  await install(options);
569
741
  return;
@@ -590,9 +762,6 @@ async function run(argv) {
590
762
  case "doctor":
591
763
  doctor();
592
764
  return;
593
- case "version":
594
- printVersion();
595
- return;
596
765
  case "help":
597
766
  case "--help":
598
767
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wattetheria",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Wattetheria deployment CLI",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -9,7 +9,7 @@
9
9
  "bin/",
10
10
  "lib/",
11
11
  "docker-compose.release.yml",
12
- ".env.release.example",
12
+ ".env.release",
13
13
  "README.md",
14
14
  "LICENSE"
15
15
  ],