@ours.network/install 1.1.0 → 1.2.0-nightly.1

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.
Files changed (49) hide show
  1. package/README.md +196 -6
  2. package/assets/Dockerfile +41 -0
  3. package/assets/docker-compose.yaml +222 -0
  4. package/assets/scripts/README.md +20 -0
  5. package/assets/scripts/build/README.md +39 -0
  6. package/assets/scripts/build/build-common.mjs +37 -0
  7. package/assets/scripts/build/build-cowork.mjs +2 -0
  8. package/assets/scripts/build/build-fleet.mjs +2 -0
  9. package/assets/scripts/build/build-mcp.mjs +9 -0
  10. package/assets/scripts/build/build-messenger.mjs +2 -0
  11. package/assets/scripts/build/build-sdk.mjs +9 -0
  12. package/assets/scripts/build/build-telegram.mjs +2 -0
  13. package/assets/scripts/build/build.mjs +49 -0
  14. package/assets/scripts/build/record-build.mjs +31 -0
  15. package/assets/scripts/maintenance/README.md +35 -0
  16. package/assets/scripts/maintenance/build-context.mjs +162 -0
  17. package/assets/scripts/maintenance/docker-layout-conversion.mjs +238 -0
  18. package/assets/scripts/maintenance/provenance-compare.mjs +160 -0
  19. package/assets/scripts/maintenance/state-archive.mjs +238 -0
  20. package/assets/scripts/maintenance/state-native.mjs +56 -0
  21. package/assets/scripts/maintenance/state-operation.mjs +249 -0
  22. package/assets/scripts/runtime/README.md +24 -0
  23. package/assets/scripts/runtime/check-client.mjs +21 -0
  24. package/assets/scripts/runtime/check-start.mjs +15 -0
  25. package/assets/scripts/runtime/client-setup.mjs +197 -0
  26. package/assets/scripts/runtime/entrypoint.sh +13 -0
  27. package/assets/scripts/runtime/health-cowork.sh +11 -0
  28. package/assets/scripts/runtime/health-messenger.mjs +6 -0
  29. package/assets/scripts/runtime/health-telegram.sh +8 -0
  30. package/assets/scripts/runtime/healthcheck.mjs +17 -0
  31. package/assets/scripts/runtime/runtime-common.mjs +47 -0
  32. package/assets/scripts/runtime/start-cowork.sh +6 -0
  33. package/assets/scripts/runtime/start-messenger.sh +6 -0
  34. package/assets/scripts/runtime/start-telegram.sh +6 -0
  35. package/assets/sources.json +21 -0
  36. package/install.sh +2 -1
  37. package/lib/build-transition.mjs +56 -0
  38. package/lib/docker-conversion-runtime.mjs +96 -0
  39. package/lib/docker-layout-installation.mjs +62 -0
  40. package/lib/effects.mjs +945 -11
  41. package/lib/extras.mjs +23 -68
  42. package/lib/layout-conversion.mjs +297 -0
  43. package/lib/orchestrate-uninstall.mjs +30 -1
  44. package/lib/orchestrate.mjs +265 -18
  45. package/lib/plan.mjs +194 -1
  46. package/lib/target.mjs +100 -0
  47. package/lib/usage.mjs +27 -2
  48. package/package.json +9 -2
  49. package/uninstall.sh +2 -0
package/README.md CHANGED
@@ -42,10 +42,9 @@ connector from another daemon still require explicit confirmation.
42
42
  installations.
43
43
  - Configures and starts cowork as a durable shim over the shared daemon.
44
44
  - Configures and starts Telegram as a durable shim over the same daemon.
45
- - Runs Fleet's host initialization and, when `~/fleet.yaml` is absent, writes a
46
- conservative stopped starter with `FleetCoordinator`, a `fleet-health`
47
- watchdog, and a ten-minute `coordinator_health` loop. An existing
48
- `~/fleet.yaml` is never overwritten.
45
+ - Runs Fleet's native initialization through prepared settings or its interactive wizard. Fleet owns and
46
+ publishes its v2 configuration, including subscriptions, models, roles,
47
+ templates, and permissions. The installer does not start Fleet roles.
49
48
 
50
49
  The operator CLI owns daemon configuration, lifecycle, and boot persistence.
51
50
  The MCP package is only the stdio adapter spawned by agent harnesses; the
@@ -66,7 +65,7 @@ managed service and old state, then initialize the new major. The default answer
66
65
  Only Fleet is intentionally not started. Review and activate it when ready:
67
66
 
68
67
  ```sh
69
- # After reviewing ~/fleet.yaml:
68
+ # After completing Fleet's wizard and reviewing ~/fleet.yaml:
70
69
  ours-fleet doctor
71
70
  ours-fleet config
72
71
  ours-fleet up
@@ -97,6 +96,16 @@ cannot store an environment value; for those harnesses the installer prints the
97
96
  exact `export OURS_CONFIG=...` line that must be added to the shell profile
98
97
  before starting the harness. There is no per-application daemon.
99
98
 
99
+ `OURS_CONFIG` may also name a prepared private host profile containing the
100
+ complete `endpoint`, `expectedInstanceId`, and absolute `credentialPath` tuple.
101
+ The profile and credential must already be regular current-user files with
102
+ private permissions. In this mode the installer verifies `/selection` before
103
+ sending the credential to `/version`, installs only MCP, detected Claude/Codex
104
+ plugins, and Fleet client support, and never creates, starts, stops, or services
105
+ a host daemon. Telegram, cowork, messenger, daemon voice, and daemon state stay
106
+ Compose-owned. If `OURS_CONFIG` is unset, the same profile is discovered at
107
+ `~/.ours/config.json`; a legacy config there keeps the existing local behavior.
108
+
100
109
  ## Uninstall
101
110
 
102
111
  ```sh
@@ -108,6 +117,10 @@ The uninstaller delegates service and daemon removal to the `ours` CLI. Identity
108
117
  state is retained by default. Purging requires the existing destructive gates and
109
118
  targets only the explicit state directory.
110
119
 
120
+ When `OURS_CONFIG` selects a host profile, uninstall removes only selected
121
+ client attachments. The operator-owned profile and shared credential are kept
122
+ even with `--purge`, and no Compose daemon, service, or state is touched.
123
+
111
124
  ## Release channel
112
125
 
113
126
  `OURS_CHANNEL=nightly` (or `OURS_INSTALL_CHANNEL`) selects the packages' nightly
@@ -120,6 +133,183 @@ both channels.
120
133
  - `OURS_ASSUME_YES=1`: accept safe defaults without prompting.
121
134
  - `OURS_INSTALL_DRY_RUN=1`: preview without mutation.
122
135
  - `OURS_NPM`: npm executable.
123
- - `OURS_CONFIG`: explicit daemon configuration file.
136
+ - `OURS_CONFIG`: explicit legacy daemon config or prepared private host profile.
124
137
  - `OURS_STATE_DIR`: explicit daemon state directory.
125
138
  - `OURS_CHANNEL`: `latest` or `nightly`.
139
+
140
+ ## Selected network installations (2.0)
141
+
142
+ The installed package carries its compatible `assets/sources.json` policy.
143
+ `--sources` remains available as an explicit development override. Feature
144
+ revisions in the testing delivery remain unpublished; local build evidence does
145
+ not prove remote fetchability or compatibility of a replacement source set.
146
+
147
+ ```sh
148
+ ours-install server install --mode docker --state-dir /private/ours-install
149
+ ours-install server install --mode packages --state-dir /private/ours-install
150
+ ours-install server status --state-dir /private/ours-install
151
+ ours-install server stop --state-dir /private/ours-install
152
+ ours-install server start --state-dir /private/ours-install
153
+ ours-install server restart --state-dir /private/ours-install
154
+ ```
155
+
156
+ Choose one mode for a private installation root. The private `installation.json`
157
+ records mode, instance selection, configuration/source paths and service selection;
158
+ it never records master bytes or an issued-token registry. The selected policy
159
+ is resolved to an exact, role-filtered `sources.json` before acquisition.
160
+ Repeated installation retains that concrete selection,
161
+ settings and state and repairs credential delivery without rotating the master.
162
+ Conflicting modes or source mappings are refused. Docker Compose is an internal
163
+ diagnostic artifact under the root's `runtime` directory; all installer calls use
164
+ an explicit project directory, Compose file, project name and selected environment.
165
+ Assets resolve from the installed package, independently of the working directory.
166
+
167
+ Package mode requires Node.js 22+, npm and a working systemd user manager on
168
+ Linux/WSL or a launchd GUI user domain on macOS. Source builds also require Git,
169
+ Python 3, a C/C++ toolchain, make, and the selected repositories' own prerequisites.
170
+ Docker mode requires a working Docker engine and Compose 2.35+; its build image
171
+ contains the source-build prerequisites. No bind or Docker socket mounts are used.
172
+ Server runtime installation selects SDK/CLI, main MCP, Telegram, Cowork and
173
+ Messenger. Main MCP is injected into the daemon, not started as a second daemon.
174
+ The daemon starts first; consumers are checked through their owning readiness
175
+ interfaces. Messenger uses its existing identity prerequisite and never creates an
176
+ identity. Its failure is reported after attempting unrelated consumers. Package
177
+ mode uses an installer-owned per-installation systemd/launchd Messenger service
178
+ for `ours-messenger-server serve`; other services use their owning CLIs.
179
+ Native daemon stop invokes the owning CLI's guarded `uninstall-service --yes`
180
+ before its process/endpoint stop and verifies the selected job is unloaded or
181
+ inactive. This removes its boot-service definition so the manager cannot respawn
182
+ a writer during authority operations. A later selected start uses the owning
183
+ `install-service --yes` to recreate it. Replacement/restart restore only previously
184
+ running services; an excluded stopped daemon keeps its service removed.
185
+
186
+ ```sh
187
+ ours-install server access-issue --state-dir /private/ours-install --output /private/client/credential
188
+ ours-install server access-replace --state-dir /private/ours-install --confirm
189
+ ```
190
+
191
+ Fresh Docker preparation leaves daemon state containing only its selected config.
192
+ Only after owning `access-init` succeeds are daemon provenance and MCP profile
193
+ files materialized, so fresh setup never needs an inferred migration.
194
+ Initial setup calls owning CLI `access-init`, then `access-issue --replace` for
195
+ managed credential destinations. Explicit legacy migration adds `--migrate` to
196
+ `server install`; migration is never inferred. Global replacement stops consumers
197
+ and the daemon, calls `access-replace` once, repairs managed credential delivery,
198
+ and restarts only previously running services. If delivery/startup fails after
199
+ replacement, the new master remains authoritative and setup is reported incomplete.
200
+ Repeat ordinary `server install` to repair delivery without another rotation.
201
+ Separately configured clients need newly issued credentials after replacement.
202
+ Docker issuance copies only a newly issued file from an administrative container
203
+ into private host staging, publishes to an absent protected destination and removes
204
+ the temporary administrative container. It never transfers the master.
205
+
206
+ For client-only installation, supply a private complete network profile with
207
+ client-owned installer settings:
208
+
209
+ ```json
210
+ {
211
+ "endpoint": "http://server:3050",
212
+ "expectedInstanceId": "12345678-1234-1234-1234-123456789abc",
213
+ "credentialPath": "/private/client/credential",
214
+ "installer": {
215
+ "integrations": ["codex", "claude-code", "fleet"],
216
+ "fleetSettingsPath": "fleet-settings.json"
217
+ }
218
+ }
219
+ ```
220
+
221
+ ```sh
222
+ ours-install client install --config /private/client/profile.json
223
+ ```
224
+
225
+ Optional `sourcesPath` and `fleetSettingsPath` values are absolute or resolved
226
+ relative to the profile file. Select any nonempty subset of integrations; Fleet is optional.
227
+ When Fleet settings are supplied, the installer passes them to Fleet's strict
228
+ noninteractive `init --settings`; otherwise it opens Fleet's existing wizard.
229
+ Fleet validates and publishes its own configuration. Client setup verifies the selected
230
+ daemon and packaged OURS MCP before registering native integrations. It acquires
231
+ only selected integration packages and their actual client dependencies. It does
232
+ not install main MCP, administer daemon state or invoke Docker. Exact npm client
233
+ installation needs no Python; Git source builds use the supplied source recipes
234
+ and their build prerequisites. Normal native clients need neither Docker nor
235
+ server maintenance tools.
236
+
237
+ Installer imports the profile, issued token, source manifest and optional Fleet
238
+ settings into `~/.ours-client/` (directory 0700, files 0600). The saved profile is
239
+ `profile.json`, referencing the imported `credential`. Original input files are
240
+ left untouched and may be moved or removed after setup. Ordinary new Codex,
241
+ Claude and Fleet invocations discover the saved server without an environment
242
+ export. Explicit `OURS_CONFIG` still overrides new invocations; installer reports
243
+ an existing override and does not edit shell configuration or running sessions.
244
+
245
+ Repeat with `ours-install client install` to reuse imported inputs and installed
246
+ package records. Existing settings and Fleet-generated files are preserved. A
247
+ prepared profile for the same endpoint/instance may supply a valid replacement
248
+ issued token; another server is refused without changing the default. Invalid
249
+ server access is rejected before import. An acquisition or component failure
250
+ reports incomplete setup and keeps the saved configuration for retry. Selected
251
+ native integrations whose executable is unavailable are incomplete, not installed.
252
+ Source repositories or registry access remain prerequisites when packages still
253
+ need to be acquired; they are not runtime configuration dependencies.
254
+
255
+ Interactive no-argument installation asks for package/Docker server mode, a
256
+ prepared client profile, or `client`. Client setup offers the saved server when
257
+ present. On first setup it asks for the server HTTP endpoint, issued-token file,
258
+ source manifest and integration choices, obtains the instance UUID from
259
+ `/selection`, displays it, then checks authenticated daemon and MCP access before
260
+ saving anything. No UUID must be entered manually. The 2.0 HTTP flow assumes the
261
+ approved trusted same-host deployment; selection metadata is not cryptographic
262
+ server authentication. Fleet collects missing setup answers through its own wizard.
263
+ This network flow never
264
+ creates a Human identity. Legacy explicitly selected local flows are retained.
265
+
266
+ For a managed server, maintenance runs on the server machine through the installer:
267
+
268
+ ```sh
269
+ ours-install server backup server snapshot --state-dir /path/to/installation
270
+ ours-install server restore server snapshot --state-dir /path/to/installation
271
+ ours-install server rebuild --state-dir /path/to/installation
272
+ ours-install server update --state-dir /path/to/installation --compatible
273
+ ```
274
+
275
+ Backup and restore support the full server or an individual `daemon`, `telegram`,
276
+ `cowork`, or `messenger` domain. Reset requires an individual domain and `--confirm`.
277
+ The installer stops affected writers and preserves a validated pre-operation
278
+ backup before restore, reset, or update. Restore retains current access authority.
279
+
280
+ Rebuild uses the saved exact source selection. Update resolves the packaged policy,
281
+ or an explicitly supplied development override, into a retained replacement.
282
+ Preparation does not replace the active runtime. Rebuild requires the same selected sources and equivalent verified dependency records.
283
+ Different JSON ordering is harmless; different package bytes or dependency edges refuse
284
+ rebuild. Fresh builds record the verified vendor archive bindings so staging paths
285
+ can differ without ignoring package integrity.
286
+ Changed sources require reviewed storage compatibility through explicit update with `--compatible`; this flag
287
+ attests to external evidence and does not establish compatibility by itself.
288
+ Services stop before state/runtime publication, and only previously running services
289
+ resume. Credentials and identities are not regenerated.
290
+
291
+ If activation fails, repeat the same update/rebuild command and retain its input
292
+ manifest. The installer resumes its saved candidate rather than fetching another
293
+ build. `server status` reports the pending phase; `server stop` remains available.
294
+ Other mutations refuse until activation completes. There is no automatic rollback
295
+ of application state after publication.
296
+
297
+
298
+ New builds retain `build-context.json` alongside their original lockfile and dependency
299
+ tree. The context binds the original record bytes to verified vendor tar names,
300
+ versions and integrity. Only proven top-level vendor staging paths are normalized;
301
+ lockfile integrity and other dependency references remain unchanged. An invalid or
302
+ unknown context is an error, including with `--compatible`.
303
+
304
+ Older records without a context retain their conservative comparison. If a rebuild
305
+ is refused because historical staging evidence is unavailable, use a reviewed
306
+ `server update --compatible` to establish a newly verified context; this operation
307
+ preserves the original record set in the pre-update backup. The flag is an explicit
308
+ storage-compatibility attestation, not proof of compatibility or a way to reconstruct
309
+ missing historic evidence. Startup never adds a context to existing legacy markers.
310
+
311
+ Backups with a context use archive format2; legacy backups remain format1. Updated
312
+ maintenance reads both formats. Format2 is not readable by older maintenance tools,
313
+ so this change does not promise executable downgrade support. Restore validates the
314
+ original archive records unchanged, then writes markers for the active target runtime.
315
+ Mixed/incomplete context and component record sets are refused before activation.
@@ -0,0 +1,41 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM node:24 AS toolchain
3
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
4
+ FROM toolchain AS build
5
+ WORKDIR /opt/ours
6
+ COPY sources.json /opt/ours/sources.json
7
+ COPY scripts/build/*.mjs /build-scripts/
8
+ COPY scripts/maintenance/build-context.mjs scripts/maintenance/provenance-compare.mjs /maintenance/
9
+ RUN --mount=type=secret,id=github_token --mount=type=cache,id=ours-dist-2-npm,target=/root/.npm,sharing=locked node /build-scripts/build.mjs
10
+ # Testing delivery: npm resolves transitive versions afresh; repeat builds may differ.
11
+ # build.mjs creates package.json once from the sources.json selections.
12
+ # Keep npm's generated lock and resolved tree in the image for this build's provenance.
13
+ RUN --mount=type=cache,id=ours-dist-2-npm,target=/root/.npm,sharing=locked npm install --omit=dev --no-audit --no-fund
14
+ RUN node /build-scripts/record-build.mjs
15
+ # Build provenance is non-secret, root-owned image content; arbitrary runtime UIDs
16
+ # must be able to read it. Protected state/host copies retain private ownership.
17
+ RUN chmod 755 /opt/ours /opt/ours/docker /opt/ours/docker/vendor && chmod 644 package.json package-lock.json dependency-tree.json build-context.json docker/vendor/*.tgz
18
+ LABEL network.ours.build-context="1"
19
+ COPY scripts/maintenance/build-context.mjs scripts/maintenance/provenance-compare.mjs /opt/ours/maintenance/
20
+ COPY scripts/runtime/runtime-common.mjs /opt/ours/docker/runtime-common.mjs
21
+ COPY scripts/runtime/check-client.mjs /opt/ours/docker/check-client.mjs
22
+ COPY scripts/runtime/check-start.mjs /opt/ours/docker/check-start.mjs
23
+ COPY scripts/runtime/client-setup.mjs /opt/ours/docker/client-setup.mjs
24
+ COPY scripts/runtime/entrypoint.sh /opt/ours/docker/entrypoint.sh
25
+ COPY scripts/runtime/healthcheck.mjs /opt/ours/docker/healthcheck.mjs
26
+ COPY scripts/runtime/start-telegram.sh scripts/runtime/health-telegram.sh scripts/runtime/start-cowork.sh scripts/runtime/health-cowork.sh scripts/runtime/start-messenger.sh scripts/runtime/health-messenger.mjs /opt/ours/docker/
27
+ FROM node:24 AS maintenance
28
+ LABEL network.ours.build-context="1"
29
+ WORKDIR /opt/ours
30
+ COPY --from=build /opt/ours /opt/ours
31
+ COPY scripts/maintenance/*.mjs /opt/ours/docker/
32
+ COPY --chmod=644 scripts/maintenance/package.json /opt/ours/docker/package.json
33
+ RUN --mount=type=cache,id=ours-dist-2-npm,target=/root/.npm,sharing=locked npm install --prefix /opt/ours/docker --omit=dev --ignore-scripts --no-audit --no-fund
34
+ USER 1000:1000
35
+ ENTRYPOINT ["node", "/opt/ours/docker/state-operation.mjs"]
36
+ FROM node:24 AS runtime
37
+ LABEL network.ours.build-context="1"
38
+ WORKDIR /opt/ours
39
+ COPY --from=build /opt/ours /opt/ours
40
+ USER 1000:1000
41
+ ENTRYPOINT ["/bin/sh", "/opt/ours/docker/entrypoint.sh"]
@@ -0,0 +1,222 @@
1
+ # OURS 2.0 source delivery. Build input: Dockerfile, sources.json and scripts/ in this delivery directory.
2
+ # Pinned feature commits must be available in their upstream repositories before building.
3
+ # Docker Compose 2.35+ with BuildKit; current upstream MUFL compiler: linux/amd64.
4
+ # Set OURS_DAEMON_ID to one retained lowercase UUID before the first start.
5
+ # Private SDK source access: export OURS_BUILD_GITHUB_TOKEN with read access to
6
+ # adapt-toolkit/ours-sdk. Passed only as a BuildKit secret, never an image ARG.
7
+ # Supported setup/start entrypoint: ours-install server install --mode docker ...
8
+ # The installer calls prepare, then owning HMAC access-init/access-issue, then
9
+ # starts daemon and consumers in dependency order. Compose is diagnostic only.
10
+ # Protected optional Telegram JSON can use the prepare service telegram-input.
11
+ # No bot settings are required to start the connector without a configured bot.
12
+ # Fleet and the Codex/Claude integrations stay on the host; main MCP executes in Docker.
13
+ # Host clients use the same pinned feature revisions documented in README.md.
14
+
15
+ x-build: &source-build
16
+ context: .
17
+ target: runtime
18
+ secrets: [github_token]
19
+ dockerfile: Dockerfile
20
+
21
+ x-runtime: &runtime
22
+ build: *source-build
23
+ platform: linux/amd64
24
+ image: "${OURS_IMAGE:-ours-compose-2-runtime:local}"
25
+ user: "${OURS_UID:-1000}:${OURS_GID:-1000}"
26
+ init: true
27
+ read_only: true
28
+ cap_drop: [ALL]
29
+ security_opt: [no-new-privileges:true]
30
+ tmpfs: ["/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777"]
31
+ networks: [ours]
32
+ stop_grace_period: 30s
33
+
34
+ x-owner-environment: &owner-environment
35
+ HOME: /tmp
36
+ OURS_CONFIG: /var/lib/ours/config.json
37
+ OURS_STATE_DIR: /var/lib/ours
38
+ OURS_PORT: "3050"
39
+ OURS_LISTEN_HOST: 0.0.0.0
40
+ OURS_API_VISIBILITY: owner
41
+ OURS_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
42
+ OURS_ACCESS_MIGRATE: "${OURS_ACCESS_MIGRATE:-0}"
43
+ OURS_MESSENGER_IDENTITY: "${OURS_MESSENGER_IDENTITY:-}"
44
+
45
+ x-owner-volumes: &owner-volumes
46
+ - {type: volume, source: server-storage, target: /var/lib/ours, volume: {nocopy: true, subpath: state/daemon}}
47
+ - {type: volume, source: server-storage, target: /var/lib/ours-mcp, volume: {nocopy: true, subpath: state/mcp}}
48
+ - {type: volume, source: server-storage, target: /credentials/telegram, volume: {nocopy: true, subpath: state/credentials/telegram}}
49
+ - {type: volume, source: server-storage, target: /credentials/cowork, volume: {nocopy: true, subpath: state/credentials/cowork}}
50
+ - {type: volume, source: server-storage, target: /credentials/messenger, volume: {nocopy: true, subpath: state/credentials/messenger}}
51
+ - type: volume
52
+ source: owner-locks
53
+ target: "/tmp/ours-${OURS_UID:-1000}"
54
+ volume: {nocopy: true}
55
+
56
+ x-client-order: &client-order
57
+ daemon: {condition: service_healthy}
58
+
59
+ services:
60
+ prepare:
61
+ profiles: [administration]
62
+ build: *source-build
63
+ platform: linux/amd64
64
+ image: "${OURS_IMAGE:-ours-compose-2-runtime:local}"
65
+ user: "0:0"
66
+ read_only: true
67
+ network_mode: none
68
+ cap_drop: [ALL]
69
+ cap_add: [CHOWN, DAC_OVERRIDE, SETUID, SETGID]
70
+ security_opt: [no-new-privileges:true]
71
+ entrypoint: [node, /opt/ours/docker/client-setup.mjs]
72
+ command: [prepare]
73
+ environment:
74
+ OURS_UID: "${OURS_UID:-1000}"
75
+ OURS_GID: "${OURS_GID:-1000}"
76
+ OURS_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
77
+ OURS_COWORK_REST_PORT: "${OURS_COWORK_PORT:-3052}"
78
+ OURS_MESSENGER_IDENTITY: "${OURS_MESSENGER_IDENTITY:-}"
79
+ OURS_BROKER_URL: "${OURS_BROKER_URL:-}"
80
+ volumes:
81
+ - {type: volume, source: server-storage, target: /storage, volume: {nocopy: true}}
82
+ - {type: volume, source: owner-locks, target: /owner-locks, volume: {nocopy: true}}
83
+
84
+ daemon:
85
+ <<: *runtime
86
+ environment: *owner-environment
87
+ volumes:
88
+ - {type: volume, source: server-storage, target: /var/lib/ours, volume: {nocopy: true, subpath: state/daemon}}
89
+ - {type: volume, source: server-storage, target: /var/lib/ours-mcp, volume: {nocopy: true, subpath: state/mcp}}
90
+ - {type: volume, source: owner-locks, target: "/tmp/ours-${OURS_UID:-1000}", volume: {nocopy: true}}
91
+ ports:
92
+ - {target: 3050, published: "${OURS_HOST_PORT:-3050}", host_ip: 127.0.0.1}
93
+ healthcheck:
94
+ test: [CMD, node, /opt/ours/docker/healthcheck.mjs]
95
+ interval: 5s
96
+ timeout: 4s
97
+ start_period: 30s
98
+ retries: 12
99
+
100
+ access:
101
+ <<: *runtime
102
+ environment: *owner-environment
103
+ volumes: *owner-volumes
104
+ entrypoint: [node, /opt/ours/docker/client-setup.mjs]
105
+ command: [access-init]
106
+ profiles: [administration]
107
+
108
+ telegram:
109
+ <<: *runtime
110
+ entrypoint: [/bin/sh, -e, /opt/ours/docker/start-telegram.sh]
111
+ environment:
112
+ HOME: /tmp
113
+ OURS_TG_CONFIG: /var/lib/ours-telegram/config.json
114
+ OURS_TG_STATE_DIR: /var/lib/ours-telegram
115
+ OURS_TG_CONTROL_PORT: "3051"
116
+ OURS_TG_DAEMON_URL: http://daemon:3050
117
+ OURS_TG_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
118
+ OURS_TG_DAEMON_CREDENTIAL_PATH: /credentials/telegram/daemon-token
119
+ volumes:
120
+ - {type: volume, source: server-storage, target: /var/lib/ours-telegram, volume: {nocopy: true, subpath: state/telegram}}
121
+ - {type: volume, source: server-storage, target: /credentials/telegram, read_only: true, volume: {nocopy: true, subpath: state/credentials/telegram}}
122
+ depends_on: *client-order
123
+ healthcheck:
124
+ test: [CMD, /bin/sh, /opt/ours/docker/health-telegram.sh]
125
+ interval: 5s
126
+ timeout: 4s
127
+ start_period: 30s
128
+ retries: 12
129
+
130
+ cowork:
131
+ <<: *runtime
132
+ entrypoint: [/bin/sh, -e, /opt/ours/docker/start-cowork.sh]
133
+ environment:
134
+ HOME: /tmp
135
+ OURS_COWORK_CONFIG: /var/lib/ours-cowork/config.json
136
+ OURS_COWORK_STATE_DIR: /var/lib/ours-cowork
137
+ OURS_COWORK_REST_PORT: "${OURS_COWORK_PORT:-3052}"
138
+ OURS_DAEMON_URL: http://daemon:3050
139
+ OURS_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
140
+ OURS_DAEMON_CREDENTIAL_PATH: /credentials/cowork/daemon-token
141
+ volumes:
142
+ - {type: volume, source: server-storage, target: /var/lib/ours-cowork, volume: {nocopy: true, subpath: state/cowork}}
143
+ - {type: volume, source: server-storage, target: /credentials/cowork, read_only: true, volume: {nocopy: true, subpath: state/credentials/cowork}}
144
+ ports:
145
+ - {target: "${OURS_COWORK_PORT:-3052}", published: "${OURS_COWORK_PORT:-3052}", host_ip: 127.0.0.1}
146
+ depends_on: *client-order
147
+ healthcheck:
148
+ test: [CMD, /bin/sh, /opt/ours/docker/health-cowork.sh]
149
+ interval: 5s
150
+ timeout: 15s
151
+ start_period: 30s
152
+ retries: 12
153
+
154
+ messenger:
155
+ <<: *runtime
156
+ entrypoint: [/bin/sh, -e, /opt/ours/docker/start-messenger.sh]
157
+ environment:
158
+ HOME: /tmp
159
+ OURS_DAEMON_URL: http://daemon:3050
160
+ OURS_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
161
+ OURS_DAEMON_CREDENTIAL_PATH: /credentials/messenger/daemon-token
162
+ OURS_MESSENGER_IDENTITY: "${OURS_MESSENGER_IDENTITY:-}"
163
+ OURS_MESSENGER_HOST: 0.0.0.0
164
+ OURS_MESSENGER_PORT: "8420"
165
+ OURS_MESSENGER_PUBLIC_ORIGIN: "http://127.0.0.1:${OURS_MESSENGER_PORT:-8420}"
166
+ OURS_MESSENGER_STATE_DIR: /var/lib/ours-messenger
167
+ volumes:
168
+ - {type: volume, source: server-storage, target: /var/lib/ours-messenger, volume: {nocopy: true, subpath: state/messenger}}
169
+ - {type: volume, source: server-storage, target: /credentials/messenger, read_only: true, volume: {nocopy: true, subpath: state/credentials/messenger}}
170
+ ports:
171
+ - {target: 8420, published: "${OURS_MESSENGER_PORT:-8420}", host_ip: 127.0.0.1}
172
+ depends_on: *client-order
173
+ healthcheck:
174
+ test: [CMD, node, /opt/ours/docker/health-messenger.mjs]
175
+ interval: 5s
176
+ timeout: 4s
177
+ start_period: 30s
178
+ retries: 12
179
+
180
+ state-operation:
181
+ profiles: [maintenance]
182
+ build:
183
+ <<: *source-build
184
+ target: maintenance
185
+ platform: linux/amd64
186
+ image: "${OURS_MAINTENANCE_IMAGE:-ours-compose-2-maintenance:local}"
187
+ user: "${OURS_UID:-1000}:${OURS_GID:-1000}"
188
+ network_mode: none
189
+ read_only: true
190
+ cap_drop: [ALL]
191
+ security_opt: [no-new-privileges:true]
192
+ command: [--help]
193
+ environment:
194
+ OURS_STATE_DOMAIN: "${OURS_STATE_DOMAIN:-daemon}"
195
+ OURS_STATE_ROOT: /storage
196
+ OURS_LIVE_ROOT: "${OURS_LIVE_ROOT:-/storage/state}"
197
+ OURS_BUILD_ROOT: /opt/ours
198
+ OURS_CLI_PATH: /opt/ours/node_modules/.bin/ours
199
+ OURS_DAEMON_CONFIG: /var/lib/ours/config.json
200
+ OURS_COWORK_CLI_PATH: /opt/ours/node_modules/.bin/ours-cowork
201
+ OURS_COWORK_CONFIG: /var/lib/ours-cowork/config.json
202
+ OURS_COWORK_STATE_DIR: /var/lib/ours-cowork
203
+ OURS_DAEMON_ID: "${OURS_DAEMON_ID:?Set a stable daemon UUID}"
204
+ OURS_COWORK_REST_PORT: "${OURS_COWORK_PORT:-3052}"
205
+ OURS_BROKER_URL: "${OURS_BROKER_URL:-}"
206
+ volumes:
207
+ - {type: volume, source: server-storage, target: /storage, volume: {nocopy: true}}
208
+ - {type: volume, source: server-storage, target: /var/lib/ours, volume: {nocopy: true, subpath: state/daemon}}
209
+ - {type: volume, source: server-storage, target: /var/lib/ours-mcp, volume: {nocopy: true, subpath: state/mcp}}
210
+ - {type: volume, source: server-storage, target: /var/lib/ours-cowork, volume: {nocopy: true, subpath: state/cowork}}
211
+
212
+ volumes:
213
+ server-storage:
214
+ owner-locks:
215
+
216
+ networks:
217
+ ours:
218
+ driver: bridge
219
+
220
+ secrets:
221
+ github_token:
222
+ environment: OURS_BUILD_GITHUB_TOKEN
@@ -0,0 +1,20 @@
1
+ # Installer assets
2
+
3
+ | Directory | Purpose | Caller |
4
+ |---|---|---|
5
+ | [build](build/README.md) | Fetch selected sources, build and package npm packages | Dockerfile or installer |
6
+ | [runtime](runtime/README.md) | Prepare selected storage and settings, start services and check readiness | Installer and Compose |
7
+ | [maintenance](maintenance/README.md) | Back up and restore stopped state using owning package interfaces | Installer |
8
+
9
+ These are internal assets of `ours-install`; users do not invoke the scripts
10
+ manually. Installer materializes Docker assets into the selected installation's
11
+ runtime directory. Package mode uses the same build coordinator and the installed
12
+ maintenance implementation.
13
+
14
+ Build scripts run under Node.js. Source builds may additionally need native build
15
+ tools, including Python for third-party dependencies. Archive and state maintenance
16
+ do not require Python.
17
+
18
+ The Dockerfile copies build scripts to `/build-scripts/` and runtime/maintenance
19
+ scripts to `/opt/ours/docker/`. Runtime services receive only their selected state
20
+ subdirectories; maintenance operates on the installation's shared state tree.
@@ -0,0 +1,39 @@
1
+ # Build
2
+
3
+ Inputs: [sources.json](../../sources.json), access to selected Git/npm sources and,
4
+ when needed, the BuildKit secret `github_token`. Docker builds run these scripts
5
+ inside the image build; package installations run them in installer-selected
6
+ staging on the host. Outputs: npm archives and one package.json under
7
+ `OURS_BUILD_ROOT` (default `/opt/ours`) for the final installation.
8
+
9
+ | File | Responsibility |
10
+ |---|---|
11
+ | `build.mjs` | Fetch sources, run required recipes in order and generate the final manifest |
12
+ | `build-sdk.mjs` | Build SDK and CLI from one repository, including MUFL |
13
+ | `build-mcp.mjs` | Build selected packages from the MCP monorepo |
14
+ | `build-telegram.mjs`, `build-cowork.mjs`, `build-messenger.mjs`, `build-fleet.mjs` | Build the corresponding repository with the selected SDK/CLI |
15
+ | `record-build.mjs` | Finalize a fresh npm installation: record the resolved tree, protect owned inputs and verify/publish vendor context |
16
+ | `build-common.mjs` | Shared archive naming and dispatch to consumer-owned build recipes |
17
+
18
+ The Dockerfile invokes `node /build-scripts/build.mjs`; no manual script calls are
19
+ needed. Each Git repository is built once. Selected published npm packages are
20
+ retrieved without rebuilding them. Archives are created inside the image; prebuilt
21
+ dependencies are not included in the delivery directory.
22
+
23
+ Telegram, Cowork, Messenger and Fleet own `scripts/build-selected.mjs`. The
24
+ assembler passes the actual selected SDK and CLI archives with `--sdk PATH --cli
25
+ PATH --out-dir PATH`, reads the JSON `filename`, verifies the packed package name,
26
+ and collects it using the existing archive naming convention. The owning recipe
27
+ handles disposable dependency preparation, build and portable packing. Consumer
28
+ source manifests and locks are not rewritten by the assembler. Missing owner
29
+ recipes or selected SDK/CLI archives fail explicitly; there is no fallback build.
30
+ SDK and MCP retain their existing repository build owners.
31
+
32
+ After the final npm install, the owning installer/Dockerfile runs `record-build.mjs`.
33
+ It requires a fresh output with no retained dependency tree/context, verifies actual
34
+ archive bytes and lock/tree bindings, and writes context privately. An existing
35
+ context is never regenerated as historical evidence. Image export makes non-secret
36
+ provenance root-owned and readable for configured runtime UIDs; host/state copies
37
+ remain private. Docker images carrying context are labelled
38
+ `network.ours.build-context=1`; a missing/copy-failed context on such an image aborts
39
+ preparation instead of treating it as legacy.
@@ -0,0 +1,37 @@
1
+ /** Shared npm operations for the six fixed repository recipes. */
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs';
3
+ import { join, basename, resolve } from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ export const ROOT = resolve(process.env.OURS_BUILD_ROOT || '/opt/ours');
7
+ export const SOURCE_ROOT = resolve(process.env.OURS_SOURCE_ROOT || '/src');
8
+ export const OUT = join(ROOT, 'docker/vendor');
9
+ export const CONFIG = JSON.parse(readFileSync(join(ROOT, 'sources.json'), 'utf8'));
10
+ export const SELECTED = new Set((process.env.OURS_BUILD_PACKAGES || 'sdk,cli,mcp,tg-connector,cowork,messenger-server').split(',').map(name => '@ours.network/' + name));
11
+ export const inheritedLock = process.env.OURS_INSTALLER_LOCK_FD === '3' ? [3] : [];
12
+ export const run = (args, cwd) => execFileSync(args[0], args.slice(1), { cwd, stdio: ['inherit', 'inherit', 'inherit', ...inheritedLock] });
13
+ export const capture = (args, cwd, env = process.env) => execFileSync(args[0], args.slice(1), { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit', ...inheritedLock] });
14
+ export const archive = packageName => join(OUT, `ours.network-${packageName.replace(/^@ours\.network\//, '')}.tgz`);
15
+ // npm already owns package archive interpretation; no second tar parser here.
16
+ export const manifest = path => JSON.parse(capture(['npm', 'pack', '--dry-run', '--ignore-scripts', '--json', path], ROOT))[0];
17
+ export function pack(cwd, packageName, source) {
18
+ if (!SELECTED.has(packageName) || CONFIG.packages[packageName]?.source !== source) return;
19
+ const result = JSON.parse(capture(['npm', 'pack', '--ignore-scripts', '--json', '--pack-destination', OUT], cwd))[0];
20
+ renameSync(join(OUT, result.filename), archive(packageName));
21
+ }
22
+ export function buildConsumer(source, packageName) {
23
+ if (!SELECTED.has(packageName) || CONFIG.packages[packageName]?.source !== source) return;
24
+ const directory = join(SOURCE_ROOT, source);
25
+ const recipe = join(directory, 'scripts/build-selected.mjs');
26
+ const sdk = archive('@ours.network/sdk'), cli = archive('@ours.network/cli');
27
+ for (const input of [recipe, sdk, cli]) if (!existsSync(input)) throw Error(`${packageName}: required build input missing: ${input}`);
28
+ mkdirSync(OUT, { recursive: true });
29
+ const output = mkdtempSync(join(OUT, source + '-'));
30
+ try {
31
+ const result = JSON.parse(capture([process.execPath, recipe, '--sdk', sdk, '--cli', cli, '--out-dir', output], directory));
32
+ if (typeof result.filename !== 'string' || basename(result.filename) !== result.filename) throw Error(`${packageName}: recipe must return an archive filename`);
33
+ const packed = join(output, result.filename);
34
+ if (manifest(packed).name !== packageName) throw Error(`Packed output does not match selection: ${packageName}`);
35
+ renameSync(packed, archive(packageName));
36
+ } finally { rmSync(output, { recursive: true, force: true }); }
37
+ }
@@ -0,0 +1,2 @@
1
+ import { buildConsumer } from './build-common.mjs';
2
+ buildConsumer('cowork', '@ours.network/cowork');
@@ -0,0 +1,2 @@
1
+ import { buildConsumer } from './build-common.mjs';
2
+ buildConsumer('fleet', '@ours.network/fleet');
@@ -0,0 +1,9 @@
1
+ import { join } from 'node:path';
2
+ import { SOURCE_ROOT, SELECTED, pack, run } from './build-common.mjs';
3
+ const source = join(SOURCE_ROOT, 'mcp');
4
+ run(['npm', 'ci', '--ignore-scripts', '--no-audit', '--no-fund'], source);
5
+ for (const [directory, name] of [['core', 'mcp'], ['codex', 'codex'], ['claude-code', 'claude-code'], ['installer', 'install']]) {
6
+ if (!SELECTED.has('@ours.network/' + name)) continue;
7
+ if (name !== 'install') run(['npm', 'run', 'build', '--workspace', '@ours.network/' + name], source);
8
+ pack(join(source, 'packages', directory), '@ours.network/' + name, 'mcp');
9
+ }
@@ -0,0 +1,2 @@
1
+ import { buildConsumer } from './build-common.mjs';
2
+ buildConsumer('messenger', '@ours.network/messenger-server');
@@ -0,0 +1,9 @@
1
+ import { join } from 'node:path';
2
+ import { SOURCE_ROOT, SELECTED, pack, run } from './build-common.mjs';
3
+ const source = join(SOURCE_ROOT, 'sdk');
4
+ run(['npm', 'ci', '--no-audit', '--no-fund'], source);
5
+ run(['bash', 'scripts/compile-mufl.sh'], source);
6
+ run(['npm', 'run', 'build'], source);
7
+ if (SELECTED.has('@ours.network/cli')) run(['npm', 'run', 'build:cli'], source);
8
+ pack(source, '@ours.network/sdk', 'sdk');
9
+ pack(join(source, 'packages/cli'), '@ours.network/cli', 'sdk');
@@ -0,0 +1,2 @@
1
+ import { buildConsumer } from './build-common.mjs';
2
+ buildConsumer('telegram', '@ours.network/tg-connector');