Repository navigation
Expand file tree
/
Copy pathnode.yml
More file actions
159 lines (140 loc) · 9.27 KB
/
Copy pathnode.yml
File metadata and controls
159 lines (140 loc) · 9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# THE NODE.
#
# One Ashlar node, deployable and restart-durable. This is the file to copy to a machine you want
# in the fleet; everything under deploy/compose/ is a lab, a demo or a dev stack.
#
# docker compose -f deploy/node.yml up -d
# docker compose -f deploy/node.yml logs -f
#
# Update the pin with scripts/node-update.sh rather than by hand.
services:
node:
# PINNED BY DIGEST, and to a `sha-<12>` tag rather than `:latest`.
#
# `:latest` is republished on every master push and its previous manifest becomes untagged and
# eligible for GHCR garbage collection — so a node pinned to the :latest INDEX digest 404s
# after a few merges, and one pinned to the moving tag silently changes under you. Observed
# 2026-08-28: :latest moved twice inside a single working session.
#
# The `sha-<12>` tags only became multi-arch on master builds as of 0983d74. Anything older is
# amd64-only and fails on an arm64 node with "no matching manifest for linux/arm64".
#
# 0.2.0, verified to carry both linux/amd64 and linux/arm64 (index sha256:56827bbc..., pushed by
# release.yml run 36863529554 as nexo-cli:sha-5bd7b89327ec, the v0.2.0 release commit).
image: ghcr.io/ianfrelinger/nexo-cli@sha256:56827bbcf74d8bc1a9589915afa6dd822925de411cf27e5a8d662fd8562230c5
# No `build:` key, deliberately. A node runs a published, signed artifact that CI produced —
# not whatever happens to be in the working tree of the machine it is standing on.
# THE GATE STORE. No environment variable reaches it: GateStore's constructor takes a
# stateRoot and every call site passes a projectDir, so the store lands at
# <working_dir>/.ashlar/gates. Left at the image default (/app) it lives in the container
# layer and `docker rm` destroys every durable trust decision, the entire held queue and the
# whole admission history — while a criterion that only checks keys and mesh would still pass.
#
# /data/state/project must EXIST IN THE IMAGE owned by the app user. Docker creates a missing
# working_dir as root, and the container runs as uid 1654, so without that the gate store is
# silently unwritable. Measured: touch -> "Permission denied".
working_dir: /data/state/project
command:
- background-agent
- daemon
- --config
- /app/config/agents.json
# Observation is the highest-rate unbounded appender in the daemon; it stays off on
# unattended nodes until Phase 5's rotating bounded writer lands (CLOSING-PLAN Phase 1
# step 3 / Phase 5 step 5 — that step removes this flag).
- --disable-observation
# Publish the mesh serve port so LAN peers can pull from this node (pairs with
# ASHLAR_MESH_SERVE_PORT below; keep both commented to expose nothing — the default).
# ports:
# - "7420:7420"
volumes:
- ashlar-state:/data/state
# The mesh store is how packages reach this node. Point it at a folder your sync tool owns —
# Syncthing, a mounted share, a USB stick — and uncomment. MeshStore.Resolve APPENDS
# '/published' to ASHLAR_MESH_DIR, so the directory to hand the sync tool is
# /data/state/mesh/published. One level off yields an empty peer store with no error, which
# reads exactly like a peer that is switched off.
# - /srv/ashlar-share:/data/state/mesh
restart: unless-stopped
# Log rotation. Nothing in deploy/, .docker/ or scripts/install/ configured this before, and
# the default json-file driver grows without bound — on a node left alone for weeks that is
# the disk filling for no reason anyone will connect to a container they last touched in
# August.
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
# A clean OOM kill beats swap thrash. On a small board the alternative is thrashing that takes
# sshd down before the container, leaving a physical power pull as the only remedy — into
# LiteDB and ext4 on a card that was just thrashed. Raise these on a bigger machine.
mem_limit: 1g
cpus: 1.5
# host.docker.internal resolves natively on Docker Desktop; this makes it work on a plain
# Linux host too, which is what a small always-on box actually is.
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# The node capability runtime probes a model backend on startup and defaults to
# 127.0.0.1:11434 — inside a container that is the container itself, so it can never reach a
# model server on the host or the LAN. Unset this only if you genuinely run one as a sidecar.
ASHLAR_OLLAMA_BASE_URL: ${ASHLAR_OLLAMA_BASE_URL:-http://host.docker.internal:11434}
# A5 cross-machine sharing (consumer auto-pull). OFF by default. Uncomment to auto-pull
# TRUSTED signed .ashpkg a peer published into the shared mesh folder, on a timer. You MUST
# trust the peer's signer first (`ashlar keys trust <fingerprint>` — read it off the origin's
# `ashlar keys show`), or every package is refused (fail-closed). With the mesh mount above,
# the peer's published folder arrives as /data/state/mesh/published. Imported code is re-gated
# by THIS node's policy: a `proposing` policy HOLDS it for review (the safe default); only a
# `self-extending` policy auto-applies it.
# ASHLAR_MESH_PULL_DIR: /data/state/mesh/published
# ASHLAR_MESH_PULL_INTERVAL_SECONDS: "300"
# ASHLAR_MESH_PULL_PROJECT: /data/state/project
# The LAN party (federated, no hub, no shared folder). Every node is a symmetric peer:
# SERVE offers this node's published signed .ashpkg read-only on a port (uncomment the
# matching `ports:` entry below so the LAN can reach it); PEERS pulls directly from other
# nodes' serve endpoints, through the SAME trust gate as any import — an untrusted signer
# is refused before anything parks, and a `proposing` policy HOLDS what it admits for
# review. A peer being offline is not an error; it just isn't at the party.
# ASHLAR_MESH_SERVE_PORT: "7420"
# ASHLAR_NODE_NAME: study-node
# Private-fleet TLS / mTLS (defence in depth on top of the package seal — access control +
# encryption on the wire). Give the serve endpoint a PEM cert+key to make it HTTPS; require a
# client cert the fleet CA signed to make it mutual TLS (only fleet members can list/download).
# Peers then pull over https:// presenting their own cert (ASHLAR_MESH_CLIENT_CERT/_KEY) and
# pinning the fleet CA (ASHLAR_MESH_CA). Mount the PEMs read-only (e.g. docker secrets). mTLS
# pairs with configured/tailnet peers (a known roster); LAN multicast stays the plaintext path.
# ASHLAR_MESH_SERVE_TLS_CERT: /run/secrets/mesh-cert.pem
# ASHLAR_MESH_SERVE_TLS_KEY: /run/secrets/mesh-key.pem
# ASHLAR_MESH_SERVE_REQUIRE_CLIENT_CERT: "1"
# ASHLAR_MESH_SERVE_CA: /run/secrets/fleet-ca.pem
# ASHLAR_MESH_CLIENT_CERT: /run/secrets/mesh-cert.pem # this node's cert, for pulling from https peers
# ASHLAR_MESH_CLIENT_KEY: /run/secrets/mesh-key.pem
# ASHLAR_MESH_CA: /run/secrets/fleet-ca.pem # pin the fleet CA when validating https peers
# ASHLAR_MESH_PEERS: "http://192.168.1.20:7420,http://192.168.1.21:7420"
# PEERS accepts ANY routable address — a LAN IP, a Tailscale/VPN address, a hostname. The
# transport doesn't care and neither does the trust model (the seal is the trust). Peer
# sources are a strategy seam: configured (here), multicast discovery (below), and future
# sources (tailnet enumeration, rendezvous) all feed the same trust-gated pull.
# Zero-config discovery (F3): announce + hear peers via multicast (239.7.42.1:7421), shown by
# `ashlar mesh lan`. Presence, not trust — a stranger's beacon only gets it pulled FROM, and
# its untrusted packages are refused before anything parks. Reaches the physical LAN from
# native/host-network nodes; Docker Desktop's bridge does NOT forward multicast to the LAN
# (containers on one bridge still hear each other) — use ASHLAR_MESH_PEERS there.
# ASHLAR_MESH_DISCOVERY: "1"
# Internet-wide P2P over a Tailscale/WireGuard tailnet — NO LAN required, works across
# networks and NAT. Reads `tailscale status --json` for online peers and pulls from
# http://<tailnet-ip>:<port> through the same trust gate. The node (or its host) must be on
# the tailnet; peers serve on the same port (ASHLAR_TAILNET_PEER_PORT, default the serve port).
# ASHLAR_MESH_TAILNET: "1"
# ASHLAR_TAILNET_PEER_PORT: "7420"
# ASHLAR_TAILNET_CMD: tailscale # path/wrapper for the tailscale CLI
# NOTE: `restart: unless-stopped` does NOT act on HEALTHCHECK. Docker restarts a container on
# EXIT, never on unhealthy. A node that parks or wedges while still running stays up and
# unhealthy forever, and `docker ps` is where you find out — which is why the park-don't-exit
# rule and the heartbeat exist rather than an exit-and-be-restarted design.
volumes:
# Pinned literal name: the host wrapper's fallback (`docker run -v ashlar-state:...`) and
# NodeUnitConventionTests both address this volume by name; without `name:` compose would
# prefix the project name and every consumer would have to guess it.
ashlar-state:
name: ashlar-state