@ours.network/install 1.2.1-nightly.40 → 1.2.1-nightly.42

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.
package/README.md CHANGED
@@ -314,6 +314,24 @@ build. `server status` reports the pending phase; `server stop` remains availabl
314
314
  Other mutations refuse until activation completes. There is no automatic rollback
315
315
  of application state after publication.
316
316
 
317
+ For Docker updates interrupted by the Notifications provenance omission in older
318
+ installers, use an installer containing the recovery helper and resume with
319
+ `ours-install server update --mode docker --state-dir /path/to/installation --compatible`.
320
+ Retain any original `--sources` input; do not substitute a newer candidate.
321
+ This uses the compatibility attestation saved in the pending update. Passing the
322
+ flag again cannot authorize a transition that did not originally record it.
323
+ The helper requires the four core markers to match the retained candidate and
324
+ Notifications to match its retained previous build, including every recorded
325
+ context, lockfile and dependency-tree byte. It preserves a Notifications-only
326
+ backup with the original provenance, repairs that marker atomically with the
327
+ complete state tree, and resumes preparation and the original running subset.
328
+ It also recognizes a completed state exchange whose journal still says `prepared`;
329
+ untouched source state continues through normal migration. Missing retained
330
+ evidence, arbitrary mixed generations, unsafe files and unclean service exits
331
+ remain errors. Keep the installation, candidate and backups intact on refusal.
332
+ An `all update` requires a running daemon for client verification, so complete
333
+ this server recovery first, then repeat `all update` to refresh clients.
334
+
317
335
 
318
336
  New builds retain `build-context.json` alongside their original lockfile and dependency
319
337
  tree. The context binds the original record bytes to verified vendor tar names,
@@ -637,7 +655,7 @@ Existing installation records keep their ports and identities. A listener acquir
637
655
  by another process after preflight still causes startup to fail safely; no foreign
638
656
  process is stopped and no retained installation is silently moved.
639
657
 
640
- Workspace setup pins Fleet `1.2.0-nightly.52` with its recorded registry integrity. This release includes first-time Fleet setup through the host API for the account App, direct App access to the workspace services with the device credential, v2 positional one-use grants and automatic QR/connection-code output from `ours-fleet setup-tunnel`, optional private file/stdin inputs for legacy v1 payloads, a separate `ours-fleet link-device` command for additional devices, host prerequisite checks before the one-time workspace proof (`setup-tunnel --check`), `setup-tunnel --resume` to finish a setup that stopped after the host saved its private pending setup record (not when the proof's answer was lost, or after the setup window expired with an unconfirmed non-default port), and persists the available Fleet loopback port used by the workspace tunnel. It also accepts a setup-generated configuration left behind after the host enrollment state was removed, runs the host Codex when it is at least as new as the packaged one, offers only the Codex models the signed-in account can use, and removes the temporary setup contact after confirmation. Fleet serves no pages and opens no browser: every screen is in the App. Fleet also serves the workspace notification routes, produces notifications through the server gateway with the imported producer credential, accepts the App's device credential on the notification presence socket so an open App receives no push, and defaults agent wakes to `monitor.interrupt: after_tool` (Hermes: no interrupt; role snapshots saved before this default keep no interrupt). Existing installation identities and configured ports remain retained; a retained-port collision fails safely.
658
+ Workspace setup pins Fleet `1.2.0-nightly.54` with its recorded registry integrity. This release includes first-time Fleet setup through the host API for the account App, direct App access to the workspace services with the device credential, v2 positional one-use grants and automatic QR/connection-code output from `ours-fleet setup-tunnel`, optional private file/stdin inputs for legacy v1 payloads, a separate `ours-fleet link-device` command for additional devices, host prerequisite checks before the one-time workspace proof (`setup-tunnel --check`), `setup-tunnel --resume` to finish a setup that stopped after the host saved its private pending setup record (not when the proof's answer was lost, or after the setup window expired with an unconfirmed non-default port), and persists the available Fleet loopback port used by the workspace tunnel. It also accepts a setup-generated configuration left behind after the host enrollment state was removed, runs the host Codex when it is at least as new as the packaged one, offers only the Codex models the signed-in account can use, and removes the temporary setup contact after confirmation. Fleet serves no pages and opens no browser: every screen is in the App. Fleet also serves the workspace notification routes, produces notifications through the server gateway with the imported producer credential, accepts the App's device credential on the notification presence socket so an open App receives no push, and defaults agent wakes to `monitor.interrupt: after_tool` (Hermes: no interrupt; role snapshots saved before this default keep no interrupt). Existing installation identities and configured ports remain retained; a retained-port collision fails safely.
641
659
 
642
660
  ### App tunnel setup prerelease sequencing
643
661
 
@@ -654,7 +672,7 @@ connection code automatically; the App checks readiness and opens the code field
654
672
  Private `--file`/`--stdin` transports remain optional and are the only transports
655
673
  for legacy v1 payloads with long-lived credentials.
656
674
 
657
- The nightly installer release manifest selects published Fleet `1.2.0-nightly.52`
675
+ The nightly installer release manifest selects published Fleet `1.2.0-nightly.54`
658
676
  and its actual registry integrity through the normal release manifest/source-policy
659
677
  generators. This release includes v2 argument and private-input support. The former
660
678
  nightly.41 selection predates this capability. Merge and publish the repinned
@@ -8,13 +8,13 @@
8
8
  "name": "ours-release-set-verification",
9
9
  "version": "0.0.0",
10
10
  "dependencies": {
11
- "@ours.network/claude-code": "1.2.0-nightly.12",
11
+ "@ours.network/claude-code": "1.2.0-nightly.13",
12
12
  "@ours.network/cli": "2.8.1-nightly.15",
13
- "@ours.network/codex": "1.2.0-nightly.12",
13
+ "@ours.network/codex": "1.2.0-nightly.13",
14
14
  "@ours.network/cowork": "1.3.3-nightly.20261005.f549477",
15
15
  "@ours.network/daemon": "3.8.1-nightly.9",
16
- "@ours.network/fleet": "1.2.0-nightly.52",
17
- "@ours.network/mcp": "1.2.0-nightly.12",
16
+ "@ours.network/fleet": "1.2.0-nightly.54",
17
+ "@ours.network/mcp": "1.2.0-nightly.13",
18
18
  "@ours.network/messenger-server": "1.0.31-nightly.17",
19
19
  "@ours.network/notifications": "0.3.1-nightly.2",
20
20
  "@ours.network/sdk": "3.8.1-nightly.17",
@@ -908,13 +908,13 @@
908
908
  }
909
909
  },
910
910
  "node_modules/@ours.network/claude-code": {
911
- "version": "1.2.0-nightly.12",
912
- "resolved": "https://registry.npmjs.org/@ours.network/claude-code/-/claude-code-1.2.0-nightly.12.tgz",
913
- "integrity": "sha512-122cnztQqVkXvXy4y4zJQAVVD5gBTqdj5gkf2dTVnR/g0Nzw5uBxdch0xkhyluqmjN74MHuklv2YRr9Gs8vbbQ==",
911
+ "version": "1.2.0-nightly.13",
912
+ "resolved": "https://registry.npmjs.org/@ours.network/claude-code/-/claude-code-1.2.0-nightly.13.tgz",
913
+ "integrity": "sha512-KnVGMyZAL9i8vqFTvI3V6OEpbOqk4YH106jfeB0JlTCRkk0Oy5H1rx764GucIxmYCiasAF1NKExHe2tDLlmiXQ==",
914
914
  "license": "FSL-1.1-Apache-2.0",
915
915
  "dependencies": {
916
916
  "@modelcontextprotocol/sdk": "^1.29.0",
917
- "@ours.network/mcp": "1.2.0-nightly.12",
917
+ "@ours.network/mcp": "1.2.0-nightly.13",
918
918
  "@ours.network/sdk": "3.8.1-nightly.17",
919
919
  "ws": "^8.21.0",
920
920
  "zod": "^3.25.76"
@@ -939,13 +939,13 @@
939
939
  }
940
940
  },
941
941
  "node_modules/@ours.network/codex": {
942
- "version": "1.2.0-nightly.12",
943
- "resolved": "https://registry.npmjs.org/@ours.network/codex/-/codex-1.2.0-nightly.12.tgz",
944
- "integrity": "sha512-O3emBbe4KXfD9nDsgjN3Z8J11aSMz4KAdiZo4EPZIqEypOXCaRnbZqpuJpd14FZpFHSHGEfel6KgP0mZYFYtRA==",
942
+ "version": "1.2.0-nightly.13",
943
+ "resolved": "https://registry.npmjs.org/@ours.network/codex/-/codex-1.2.0-nightly.13.tgz",
944
+ "integrity": "sha512-m5tcTu6QoMhrcrKgglTyG+nxe1nwRLc2t7nPJcjVRqf/5m8e9CRGqXssrioOAS/wD7qGKl2jPVwJr/0ZmMltVQ==",
945
945
  "license": "FSL-1.1-Apache-2.0",
946
946
  "dependencies": {
947
947
  "@modelcontextprotocol/sdk": "^1.29.0",
948
- "@ours.network/mcp": "1.2.0-nightly.12",
948
+ "@ours.network/mcp": "1.2.0-nightly.13",
949
949
  "@ours.network/sdk": "3.8.1-nightly.17",
950
950
  "ws": "^8.21.0",
951
951
  "zod": "^3.25.76"
@@ -1002,15 +1002,15 @@
1002
1002
  }
1003
1003
  },
1004
1004
  "node_modules/@ours.network/fleet": {
1005
- "version": "1.2.0-nightly.52",
1006
- "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.52.tgz",
1007
- "integrity": "sha512-nIwGknps0FBDhdtivXAFalRbZ67XHAdNcs6AiTBE0ULYeLcohdfqoFqyAS0iZQPUeEUhxkPTKfVuESKeQKP/ow==",
1005
+ "version": "1.2.0-nightly.54",
1006
+ "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.54.tgz",
1007
+ "integrity": "sha512-Sh3t8iRGBoYzbyslS+IKuqQ573Zn+M0GVhR0cGepy/hlyNMAwXD2TBM+j7d8QiA1+ALugkMhKnanivE97JBmhQ==",
1008
1008
  "license": "FSL-1.1-Apache-2.0",
1009
1009
  "dependencies": {
1010
1010
  "@agentclientprotocol/sdk": "^1.3.0",
1011
1011
  "@fastify/websocket": "^11.2.0",
1012
1012
  "@ours.network/cli": "2.8.1-nightly.15",
1013
- "@ours.network/mcp": "1.2.0-nightly.12",
1013
+ "@ours.network/mcp": "1.2.0-nightly.13",
1014
1014
  "@ours.network/sdk": "3.8.1-nightly.17",
1015
1015
  "commander": "^12.1.0",
1016
1016
  "fastify": "^5.4.0",
@@ -1031,12 +1031,12 @@
1031
1031
  }
1032
1032
  },
1033
1033
  "node_modules/@ours.network/mcp": {
1034
- "version": "1.2.0-nightly.12",
1035
- "resolved": "https://registry.npmjs.org/@ours.network/mcp/-/mcp-1.2.0-nightly.12.tgz",
1036
- "integrity": "sha512-ZvwCaIgMaMcUrLhWNaWWOErIHwtvQyOZHfophf+rrjnlG0eBpBwmwQPuABS7jPuQvi69pBIT4jUWN3CArgxzRQ==",
1034
+ "version": "1.2.0-nightly.13",
1035
+ "resolved": "https://registry.npmjs.org/@ours.network/mcp/-/mcp-1.2.0-nightly.13.tgz",
1036
+ "integrity": "sha512-/SxWOeiQfOu0i+jV3su2khYQ4eUCH+exe6CjSUZ9u5FmTUfXmg7cBnPOzFT2PKbNulsSLf9vnU+QBGBwSa/Iag==",
1037
1037
  "license": "FSL-1.1-Apache-2.0",
1038
1038
  "dependencies": {
1039
- "@modelcontextprotocol/sdk": "^1.0.4",
1039
+ "@modelcontextprotocol/sdk": "^1.29.0",
1040
1040
  "@ours.network/sdk": "3.8.1-nightly.17",
1041
1041
  "zod": "^3.23.8"
1042
1042
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "channel": "nightly",
4
- "installerVersion": "1.2.1-nightly.40",
4
+ "installerVersion": "1.2.1-nightly.42",
5
5
  "packages": {
6
6
  "@ours.network/sdk": {
7
7
  "version": "3.8.1-nightly.17",
@@ -28,20 +28,20 @@
28
28
  "integrity": "sha512-OjnrJ8ZuOfKYT4KAmiRLU7czfiuacs6rShwOvvqxqBaUKcs1UwjsYwHARNb6erMxG4am3xxcE0ItahL+d2vt4A=="
29
29
  },
30
30
  "@ours.network/fleet": {
31
- "version": "1.2.0-nightly.52",
32
- "integrity": "sha512-nIwGknps0FBDhdtivXAFalRbZ67XHAdNcs6AiTBE0ULYeLcohdfqoFqyAS0iZQPUeEUhxkPTKfVuESKeQKP/ow=="
31
+ "version": "1.2.0-nightly.54",
32
+ "integrity": "sha512-Sh3t8iRGBoYzbyslS+IKuqQ573Zn+M0GVhR0cGepy/hlyNMAwXD2TBM+j7d8QiA1+ALugkMhKnanivE97JBmhQ=="
33
33
  },
34
34
  "@ours.network/mcp": {
35
- "version": "1.2.0-nightly.12",
36
- "integrity": "sha512-ZvwCaIgMaMcUrLhWNaWWOErIHwtvQyOZHfophf+rrjnlG0eBpBwmwQPuABS7jPuQvi69pBIT4jUWN3CArgxzRQ=="
35
+ "version": "1.2.0-nightly.13",
36
+ "integrity": "sha512-/SxWOeiQfOu0i+jV3su2khYQ4eUCH+exe6CjSUZ9u5FmTUfXmg7cBnPOzFT2PKbNulsSLf9vnU+QBGBwSa/Iag=="
37
37
  },
38
38
  "@ours.network/codex": {
39
- "version": "1.2.0-nightly.12",
40
- "integrity": "sha512-O3emBbe4KXfD9nDsgjN3Z8J11aSMz4KAdiZo4EPZIqEypOXCaRnbZqpuJpd14FZpFHSHGEfel6KgP0mZYFYtRA=="
39
+ "version": "1.2.0-nightly.13",
40
+ "integrity": "sha512-m5tcTu6QoMhrcrKgglTyG+nxe1nwRLc2t7nPJcjVRqf/5m8e9CRGqXssrioOAS/wD7qGKl2jPVwJr/0ZmMltVQ=="
41
41
  },
42
42
  "@ours.network/claude-code": {
43
- "version": "1.2.0-nightly.12",
44
- "integrity": "sha512-122cnztQqVkXvXy4y4zJQAVVD5gBTqdj5gkf2dTVnR/g0Nzw5uBxdch0xkhyluqmjN74MHuklv2YRr9Gs8vbbQ=="
43
+ "version": "1.2.0-nightly.13",
44
+ "integrity": "sha512-KnVGMyZAL9i8vqFTvI3V6OEpbOqk4YH106jfeB0JlTCRkk0Oy5H1rx764GucIxmYCiasAF1NKExHe2tDLlmiXQ=="
45
45
  },
46
46
  "@ours.network/daemon": {
47
47
  "version": "3.8.1-nightly.9",
@@ -0,0 +1,36 @@
1
+ /** Non-secret retained build evidence arrives on stdin, never in argv/env. */
2
+ import * as fs from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { runStateOperation } from './state-operation.mjs';
6
+
7
+ // The shipped state-operation service is read_only without a /tmp tmpfs, and
8
+ // retained Compose definitions cannot gain one. /dev/shm stays a writable
9
+ // in-memory mount there, so evidence never enters the stored-state volume.
10
+ function scratch() {
11
+ for (const base of [tmpdir(), '/dev/shm']) {
12
+ try { return fs.mkdtempSync(join(base, 'ours-notifications-recovery-')); }
13
+ catch (error) { if (!['EROFS', 'EACCES', 'ENOENT'].includes(error.code)) throw error; }
14
+ }
15
+ throw new Error('No writable private scratch directory for recovery evidence');
16
+ }
17
+ const root = scratch();
18
+ fs.chmodSync(root, 0o700);
19
+ try {
20
+ const evidence = JSON.parse(fs.readFileSync(0, 'utf8'));
21
+ if (!evidence || Object.keys(evidence).sort().join(',') !== 'phase,previous,target'
22
+ || !['prepared', 'state-updated', 'runtime-activated'].includes(evidence.phase)) throw new Error('Invalid recovery evidence');
23
+ for (const name of ['previous', 'target']) {
24
+ const records = evidence[name], names = Object.keys(records).sort();
25
+ if (!['dependency-tree.json,package-lock.json', 'build-context.json,dependency-tree.json,package-lock.json'].includes(names.join(','))) throw new Error('Invalid recovery record set');
26
+ const directory = join(root, name); fs.mkdirSync(directory, { mode: 0o700 });
27
+ for (const file of names) {
28
+ if (typeof records[file] !== 'string') throw new Error('Invalid recovery record bytes');
29
+ fs.writeFileSync(join(directory, file), Buffer.from(records[file], 'base64'), { flag: 'wx', mode: 0o600 });
30
+ }
31
+ }
32
+ const result = await runStateOperation(['recover-notifications', 'server', '--compatible'], {
33
+ ...process.env, OURS_PREVIOUS_BUILD_ROOT: join(root, 'previous'), OURS_EXPECTED_BUILD_ROOT: join(root, 'target'), OURS_RECOVERY_PHASE: evidence.phase,
34
+ });
35
+ console.log(JSON.stringify(result));
36
+ } finally { fs.rmSync(root, { recursive: true }); }
@@ -37,11 +37,12 @@ export async function runStateOperation(argv, env = process.env, checkpoints = {
37
37
  const operation = argv[0];
38
38
  const paired = domain === 'daemon' && ['backup', 'restore', 'reset'].includes(operation);
39
39
  const commonTree = domain === 'server' || paired;
40
- if (compatible && !['restore', 'update'].includes(operation)) fail('compatibility attestation applies only to update or restore');
41
- if (domain === 'server' && !['backup', 'restore', 'update', 'rebuild'].includes(operation)) fail('full-server scope supports only backup, restore, update and rebuild');
40
+ const recovery = operation === 'recover-notifications' && domain === 'server' && compatible;
41
+ if (compatible && !['restore', 'update'].includes(operation) && !recovery) fail('compatibility attestation applies only to update or restore');
42
+ if (domain === 'server' && !['backup', 'restore', 'update', 'rebuild'].includes(operation) && !recovery) fail('full-server scope supports only backup, restore, update and rebuild');
42
43
  if (operation === 'rebuild' && domain !== 'server') fail('rebuild selects the complete server');
43
44
  const adopt = operation === 'init' && argv[2] === '--adopt-existing';
44
- const valid = (['init', 'update', 'rebuild'].includes(operation) && argv.length === 2) || (adopt && argv.length === 3) || (['backup', 'restore'].includes(operation) && argv.length === 3) || (operation === 'reset' && argv.length === 3 && argv[2] === '--confirm');
45
+ const valid = ((['init', 'update', 'rebuild'].includes(operation) || recovery) && argv.length === 2) || (adopt && argv.length === 3) || (['backup', 'restore'].includes(operation) && argv.length === 3) || (operation === 'reset' && argv.length === 3 && argv[2] === '--confirm');
45
46
  if (!valid) fail('usage: init DOMAIN | backup DOMAIN LABEL | restore DOMAIN LABEL | reset DOMAIN --confirm | update DOMAIN');
46
47
  const uid = process.getuid(), gid = process.getgid();
47
48
  if (!uid || !gid) fail('state maintenance requires a non-root UID and GID');
@@ -184,6 +185,52 @@ export async function runStateOperation(argv, env = process.env, checkpoints = {
184
185
  try { if (!sameInode(fs.fstatSync(fd), fs.fstatSync(check))) fail('domain state changed while acquiring its lock'); }
185
186
  finally { fs.closeSync(check); }
186
187
  let records;
188
+ if (recovery) {
189
+ // Only the installer supplies the retained transition's previous and
190
+ // candidate records. Never infer compatibility from a mixed live tree.
191
+ const previous = readRecords(selected('OURS_PREVIOUS_BUILD_ROOT'), false);
192
+ const expected = readRecords(selected('OURS_EXPECTED_BUILD_ROOT'), false);
193
+ if (!sameRecords(target, expected)) fail('recovery runtime differs from the retained candidate');
194
+ const core = APPLICATIONS.map(name => readRecords(join(live, name, PROVENANCE)));
195
+ const oldCore = core.every(records => sameRecords(records, previous));
196
+ const newCore = core.every(records => sameRecords(records, target));
197
+ if (!newCore && !(env.OURS_RECOVERY_PHASE === 'prepared' && oldCore)) fail('recovery core provenance differs from the retained candidate');
198
+ for (const name of ['mcp', 'credentials']) privateStat(join(live, name), true, 0o700);
199
+ const notifications = join(live, 'notifications'), marker = join(notifications, PROVENANCE);
200
+ if (!exists(notifications)) return { stateUpdated: newCore };
201
+ privateStat(notifications, true, 0o700);
202
+ if (!exists(marker)) { scanSource(notifications, { uid, gid }); return { stateUpdated: newCore }; }
203
+ privateStat(marker, true, 0o700);
204
+ if (!fs.readdirSync(marker).length) { scanSource(notifications, { uid, gid }); return { stateUpdated: newCore }; }
205
+ const retained = readRecords(marker);
206
+ if (newCore && sameRecords(retained, target)) return { stateUpdated: true }; // Already repaired, including retries after exchange.
207
+ if (!sameRecords(retained, previous)) fail('recovery notification provenance differs from the retained previous build');
208
+ if (!newCore) return { stateUpdated: false }; // Prepared journal with untouched state: use the regular state operation.
209
+ prepare(); layout();
210
+ const archive = labelPath(`pre-notifications-recovery-${new Date().toISOString().replace(/[-:.]/g, '')}-${randomBytes(4).toString('hex')}`);
211
+ checkpoints.beforeBackup?.();
212
+ await createArchive(notifications, archive, { domain: 'notifications', uid, gid, provenance: previous });
213
+ checkpoints.afterBackup?.();
214
+ const staging = join(maintenance, `recovery-${randomBytes(12).toString('hex')}`);
215
+ try {
216
+ const entries = scanSource(live, { uid, gid });
217
+ copyTree(live, staging);
218
+ // Ordinary maintenance emits private copies; recovery retains every
219
+ // accepted payload mode and timestamp outside the replaced marker.
220
+ for (const entry of entries.reverse()) {
221
+ const path = entry.name === 'state' ? staging : join(staging, entry.name.slice(6));
222
+ fs.chmodSync(path, Number(entry.st.mode) & 0o777);
223
+ setMtimeNs(path, entry.st.mtimeNs);
224
+ }
225
+ const replacement = join(staging, 'notifications', PROVENANCE);
226
+ fs.rmSync(replacement, { recursive: true }); mkdir(replacement);
227
+ for (const name of recordNames(target)) writeRecord(join(replacement, name), target[name]);
228
+ setMtimeNs(dirname(replacement), fs.lstatSync(notifications, { bigint: true }).mtimeNs);
229
+ validateTree(staging, target);
230
+ replace(staging);
231
+ } finally { if (exists(staging)) fs.rmSync(staging, { recursive: true }); }
232
+ return { stateUpdated: true };
233
+ }
187
234
  if (domain === 'server') { records = readRecords(join(live, 'daemon', PROVENANCE)); validateTree(live, records); }
188
235
  else if (paired) { records = readRecords(join(live, 'daemon', PROVENANCE)); privateStat(join(live, 'mcp'), true, 0o700); }
189
236
  else if (operation !== 'init') records = readRecords(join(live, PROVENANCE));
@@ -2,7 +2,7 @@
2
2
  "release": {
3
3
  "schema": 1,
4
4
  "channel": "nightly",
5
- "installerVersion": "1.2.1-nightly.40",
5
+ "installerVersion": "1.2.1-nightly.42",
6
6
  "packages": {
7
7
  "@ours.network/sdk": {
8
8
  "version": "3.8.1-nightly.17",
@@ -29,20 +29,20 @@
29
29
  "integrity": "sha512-OjnrJ8ZuOfKYT4KAmiRLU7czfiuacs6rShwOvvqxqBaUKcs1UwjsYwHARNb6erMxG4am3xxcE0ItahL+d2vt4A=="
30
30
  },
31
31
  "@ours.network/fleet": {
32
- "version": "1.2.0-nightly.52",
33
- "integrity": "sha512-nIwGknps0FBDhdtivXAFalRbZ67XHAdNcs6AiTBE0ULYeLcohdfqoFqyAS0iZQPUeEUhxkPTKfVuESKeQKP/ow=="
32
+ "version": "1.2.0-nightly.54",
33
+ "integrity": "sha512-Sh3t8iRGBoYzbyslS+IKuqQ573Zn+M0GVhR0cGepy/hlyNMAwXD2TBM+j7d8QiA1+ALugkMhKnanivE97JBmhQ=="
34
34
  },
35
35
  "@ours.network/mcp": {
36
- "version": "1.2.0-nightly.12",
37
- "integrity": "sha512-ZvwCaIgMaMcUrLhWNaWWOErIHwtvQyOZHfophf+rrjnlG0eBpBwmwQPuABS7jPuQvi69pBIT4jUWN3CArgxzRQ=="
36
+ "version": "1.2.0-nightly.13",
37
+ "integrity": "sha512-/SxWOeiQfOu0i+jV3su2khYQ4eUCH+exe6CjSUZ9u5FmTUfXmg7cBnPOzFT2PKbNulsSLf9vnU+QBGBwSa/Iag=="
38
38
  },
39
39
  "@ours.network/codex": {
40
- "version": "1.2.0-nightly.12",
41
- "integrity": "sha512-O3emBbe4KXfD9nDsgjN3Z8J11aSMz4KAdiZo4EPZIqEypOXCaRnbZqpuJpd14FZpFHSHGEfel6KgP0mZYFYtRA=="
40
+ "version": "1.2.0-nightly.13",
41
+ "integrity": "sha512-m5tcTu6QoMhrcrKgglTyG+nxe1nwRLc2t7nPJcjVRqf/5m8e9CRGqXssrioOAS/wD7qGKl2jPVwJr/0ZmMltVQ=="
42
42
  },
43
43
  "@ours.network/claude-code": {
44
- "version": "1.2.0-nightly.12",
45
- "integrity": "sha512-122cnztQqVkXvXy4y4zJQAVVD5gBTqdj5gkf2dTVnR/g0Nzw5uBxdch0xkhyluqmjN74MHuklv2YRr9Gs8vbbQ=="
44
+ "version": "1.2.0-nightly.13",
45
+ "integrity": "sha512-KnVGMyZAL9i8vqFTvI3V6OEpbOqk4YH106jfeB0JlTCRkk0Oy5H1rx764GucIxmYCiasAF1NKExHe2tDLlmiXQ=="
46
46
  },
47
47
  "@ours.network/daemon": {
48
48
  "version": "3.8.1-nightly.9",
@@ -87,19 +87,19 @@
87
87
  },
88
88
  "@ours.network/fleet": {
89
89
  "type": "npm",
90
- "version": "1.2.0-nightly.52"
90
+ "version": "1.2.0-nightly.54"
91
91
  },
92
92
  "@ours.network/mcp": {
93
93
  "type": "npm",
94
- "version": "1.2.0-nightly.12"
94
+ "version": "1.2.0-nightly.13"
95
95
  },
96
96
  "@ours.network/codex": {
97
97
  "type": "npm",
98
- "version": "1.2.0-nightly.12"
98
+ "version": "1.2.0-nightly.13"
99
99
  },
100
100
  "@ours.network/claude-code": {
101
101
  "type": "npm",
102
- "version": "1.2.0-nightly.12"
102
+ "version": "1.2.0-nightly.13"
103
103
  },
104
104
  "@ours.network/daemon": {
105
105
  "type": "npm",
@@ -19,6 +19,7 @@ export async function serverBuildTransition(record, args, effects) {
19
19
  effects.writeJson(path, JSON.stringify(record, null, 2) + '\n');
20
20
  };
21
21
  let transition = record.buildTransition;
22
+ const resuming = !!transition;
22
23
  if (!transition) {
23
24
  const candidate = await stage('Prepare the updated runtime', () => effects.prepareServerBuild(record, args));
24
25
  try {
@@ -41,11 +42,23 @@ export async function serverBuildTransition(record, args, effects) {
41
42
  // A failed readiness check can leave some new services running. Every retry
42
43
  // excludes those writers again and retains the original requested running set.
43
44
  await stage('Stop services before updating stored state', () => effects.retireServerBuildRuntime(record));
45
+ const recover = resuming && record.mode === 'docker' && transition.operation === 'update' && transition.compatible;
46
+ if (recover && transition.phase === 'prepared'
47
+ && await stage('Verify and recover retained Notifications provenance', () => effects.recoverServerBuildNotifications(record, candidate, 'prepared'))) {
48
+ transition = { ...transition, phase: 'state-updated' };
49
+ save(transition);
50
+ }
44
51
  if (transition.phase === 'prepared') {
45
52
  await stage('Update stored state while retaining identities and credentials', () => effects.updateServerBuildState(record, candidate, transition.compatible, transition.operation));
46
53
  transition = { ...transition, phase: 'state-updated' };
47
54
  save(transition);
48
55
  }
56
+ // Old installers omitted Notifications from the already completed state
57
+ // operation. The shipped helper validates both retained generations before
58
+ // repairing that one domain; the candidate and all lifecycle guards remain.
59
+ if (recover) {
60
+ await stage('Verify and recover retained Notifications provenance', () => effects.recoverServerBuildNotifications(record, candidate, transition.phase));
61
+ }
49
62
  if (transition.phase === 'state-updated') {
50
63
  await stage('Activate the prepared runtime', () => effects.publishServerBuild(record, candidate));
51
64
  transition = { ...transition, phase: 'runtime-activated' };
package/lib/effects.mjs CHANGED
@@ -408,7 +408,7 @@ export function realEffects({ write, ttyFd, env = process.env, home = homedir(),
408
408
  // invocation only and never to the installer's own process: a state
409
409
  // directory selected by one run must not leak into anything the operator
410
410
  // starts afterwards.
411
- run: async (cmd, args, { env: extraEnv = null, stream = false, cwd, sensitive = false, allowCodes = [], timeout } = {}) => {
411
+ run: async (cmd, args, { env: extraEnv = null, stream = false, cwd, sensitive = false, allowCodes = [], timeout, input } = {}) => {
412
412
  // Always built from this layer's OWN env rather than left to spawnSync's
413
413
  // implicit inheritance, so what a child receives is a property of the
414
414
  // effects object a caller constructed and not of whatever ambient shell
@@ -421,7 +421,8 @@ export function realEffects({ write, ttyFd, env = process.env, home = homedir(),
421
421
  cwd,
422
422
  timeout,
423
423
  encoding: 'utf8',
424
- stdio: [...(stream ? ['ignore', 'inherit', 'inherit'] : ['ignore', 'pipe', 'pipe']), ...(installationLockFd === null ? [] : [installationLockFd])],
424
+ input,
425
+ stdio: [...(stream ? [input === undefined ? 'ignore' : 'pipe', 'inherit', 'inherit'] : [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe']), ...(installationLockFd === null ? [] : [installationLockFd])],
425
426
  env: childEnv,
426
427
  });
427
428
  if (r.error) {
@@ -1002,6 +1003,38 @@ export function networkEffects(effects) {
1002
1003
  });
1003
1004
  }
1004
1005
  },
1006
+ async recoverServerBuildNotifications(record, candidate, phase) {
1007
+ const transition = record.buildTransition;
1008
+ if (record.mode !== 'docker' || transition?.operation !== 'update' || !transition.compatible
1009
+ || !['prepared', 'state-updated', 'runtime-activated'].includes(phase)
1010
+ || transition.phase !== phase
1011
+ || transition.candidate.root !== candidate.root) throw new Error('Notifications recovery requires the retained compatible Docker update');
1012
+ const previous = join(candidate.root, 'previous-build');
1013
+ privateDirectory(previous);
1014
+ let active = phase === 'runtime-activated' ? record.workDir : candidate.workDir;
1015
+ // Publication can move both directories before its journal save. Resume
1016
+ // from the protected active copy only when the previous runtime was kept.
1017
+ if (phase === 'state-updated' && !existsSync(candidate.workDir)) {
1018
+ privateDirectory(join(candidate.root, 'previous-runtime'));
1019
+ active = record.workDir;
1020
+ }
1021
+ privateDirectory(active);
1022
+ const encode = directory => Object.fromEntries(Object.entries(readBuildRecords(directory, { privateFiles: true })).map(([name, bytes]) => [name, bytes.toString('base64')]));
1023
+ const input = JSON.stringify({ phase, previous: encode(previous), target: encode(active) });
1024
+ // Execute shipped maintenance against the retained image and its selected
1025
+ // volume/user contract. No image retag, build replacement or host bind mount.
1026
+ const source = readFileSync(join(INSTALLER_ASSETS, 'scripts/maintenance/state-operation.mjs'), 'utf8')
1027
+ .replaceAll("from './", "from 'file:///opt/ours/docker/");
1028
+ const module = `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
1029
+ const helper = readFileSync(join(INSTALLER_ASSETS, 'scripts/maintenance/recover-notifications.mjs'), 'utf8')
1030
+ .replace("'./state-operation.mjs'", JSON.stringify(module));
1031
+ const result = await compose({ ...record, workDir: active }, ['run', '--rm', '--no-deps', '-T', '--entrypoint', 'node', 'state-operation', '--input-type=module', '-e', helper], {
1032
+ input, env: { OURS_MAINTENANCE_IMAGE: `${candidate.project}:maintenance`, OURS_STATE_DOMAIN: 'server', OURS_LIVE_ROOT: '/storage/state' },
1033
+ });
1034
+ const recovered = JSON.parse(result.stdout);
1035
+ if (!recovered || Object.keys(recovered).join(',') !== 'stateUpdated' || typeof recovered.stateUpdated !== 'boolean') throw new Error('Invalid Notifications recovery result');
1036
+ return recovered.stateUpdated;
1037
+ },
1005
1038
  async publishServerBuild(record, candidate) {
1006
1039
  const previous = join(candidate.root, 'previous-runtime');
1007
1040
  if (record.mode === 'docker') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.2.1-nightly.40",
3
+ "version": "1.2.1-nightly.42",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
6
6
  "type": "module",