@ours.network/install 1.2.0-nightly.2 → 1.2.1-nightly.10

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 (39) hide show
  1. package/README.md +151 -131
  2. package/assets/Dockerfile +6 -3
  3. package/assets/docker-compose.yaml +21 -1
  4. package/assets/release-lock.json +7161 -0
  5. package/assets/release.json +57 -0
  6. package/assets/scripts/build/build-common.mjs +1 -1
  7. package/assets/scripts/build/build-sdk.mjs +6 -1
  8. package/assets/scripts/build/record-build.mjs +2 -0
  9. package/assets/scripts/maintenance/build-context.mjs +1 -1
  10. package/assets/scripts/maintenance/daemon-owner.mjs +15 -0
  11. package/assets/scripts/maintenance/docker-layout-conversion.mjs +3 -1
  12. package/assets/scripts/maintenance/release-graph.mjs +111 -0
  13. package/assets/scripts/maintenance/state-operation.mjs +4 -2
  14. package/assets/scripts/runtime/client-setup.mjs +3 -3
  15. package/assets/scripts/runtime/entrypoint.sh +1 -1
  16. package/assets/scripts/runtime/legacy-import.mjs +103 -0
  17. package/assets/scripts/runtime/runtime-common.mjs +2 -0
  18. package/assets/sources.json +96 -16
  19. package/install.mjs +2 -2
  20. package/install.sh +1 -1
  21. package/lib/build-transition.mjs +13 -7
  22. package/lib/client-cli.mjs +117 -0
  23. package/lib/docker-conversion-runtime.mjs +1 -0
  24. package/lib/docker-runtime-repair.mjs +125 -0
  25. package/lib/effects.mjs +163 -85
  26. package/lib/fleet-settings.mjs +43 -0
  27. package/lib/legacy-migration.mjs +202 -0
  28. package/lib/legacy-state.mjs +205 -0
  29. package/lib/managed-cli.mjs +164 -0
  30. package/lib/orchestrate.mjs +162 -58
  31. package/lib/plan.mjs +13 -7
  32. package/lib/prompt.mjs +113 -119
  33. package/lib/server-onboarding.mjs +114 -0
  34. package/lib/setup-options.mjs +253 -0
  35. package/lib/setup.mjs +154 -0
  36. package/lib/target.mjs +4 -4
  37. package/lib/uninstall.mjs +2 -2
  38. package/lib/usage.mjs +49 -44
  39. package/package.json +5 -4
@@ -0,0 +1,57 @@
1
+ {
2
+ "schema": 1,
3
+ "channel": "nightly",
4
+ "installerVersion": "1.2.1-nightly.10",
5
+ "packages": {
6
+ "@ours.network/sdk": {
7
+ "version": "3.8.1-nightly.9",
8
+ "integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
9
+ },
10
+ "@ours.network/cli": {
11
+ "version": "2.8.1-nightly.7",
12
+ "integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
13
+ },
14
+ "@ours.network/tg-connector": {
15
+ "version": "1.0.1-nightly.3",
16
+ "integrity": "sha512-UDHEgM5C3mnnyfV3KTM2XFDGZ7vYs7oBE/3r8bKDtxJdlQZ4bxxI44LKrPI3gO2sdGROsDdM8/tvzwbJ8MiBYw=="
17
+ },
18
+ "@ours.network/cowork": {
19
+ "version": "1.3.2-nightly.20260921.0d6bb50",
20
+ "integrity": "sha512-gyUgY7Fw6RQVhONqexLNcS8m0WuBUQiHYIrWqd2FeAyyJOONjYGfbRtgJ67Xin1YTYf8Cxlomuf6j8cEc4ZwZg=="
21
+ },
22
+ "@ours.network/messenger-server": {
23
+ "version": "1.0.31-nightly.3",
24
+ "integrity": "sha512-j86POD2U2yh7jiIqbIixDu8TpEpaVf+dHLs3adtTRfTs6V7ucl//digdkyc1uaZrUliRduxXKBOrb/1Nu648xQ=="
25
+ },
26
+ "@ours.network/fleet": {
27
+ "version": "1.2.0-nightly.3",
28
+ "integrity": "sha512-FzizLkh1fQ9RkzmID1v42QlZs9E0QHWJ44IWIWzYd0DRaetcXg/5FUVTNc82U5yWZyveBZTJq6mvg9H73NSlkw=="
29
+ },
30
+ "@ours.network/mcp": {
31
+ "version": "1.2.0-nightly.5",
32
+ "integrity": "sha512-CsRsHlJwU5WkUeF8mMLysq9GsYLBA2wnl8v+qZhaJZhSJWoNb5TqfXd2v5/Su7VUpOaRwvubv/9Jf86eQgqhGw=="
33
+ },
34
+ "@ours.network/codex": {
35
+ "version": "1.2.0-nightly.5",
36
+ "integrity": "sha512-6E3tE0valPE0FSOsONh3BrrBo9Gxt1IgWm5Hwj0xm00mGQ+qsjGRrzCB8snrOh9OdnGuvPOF20aunmp4g4KoMg=="
37
+ },
38
+ "@ours.network/claude-code": {
39
+ "version": "1.2.0-nightly.5",
40
+ "integrity": "sha512-PxPegFvXx5bkvTprB8dcPjIYvE5bvjQoAtw31onOAZa0YdX2K7yYpesYcZ0sOnFdSQkI4+aaVyTb1Lj9EiMABQ=="
41
+ },
42
+ "@ours.network/daemon": {
43
+ "version": "3.8.1-nightly.1",
44
+ "integrity": "sha512-1+RA0UH/wJUSmKnmZuHS3v6UyH5PsrZWoqwa5rFKl6n2Cd8sWDs++DrwwV0IOFdgFcDOtb/k8mes4sI2J7J8mg=="
45
+ }
46
+ },
47
+ "hostCli": {
48
+ "@ours.network/sdk": {
49
+ "version": "3.8.1-nightly.9",
50
+ "integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
51
+ },
52
+ "@ours.network/cli": {
53
+ "version": "2.8.1-nightly.7",
54
+ "integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
55
+ }
56
+ }
57
+ }
@@ -7,7 +7,7 @@ export const ROOT = resolve(process.env.OURS_BUILD_ROOT || '/opt/ours');
7
7
  export const SOURCE_ROOT = resolve(process.env.OURS_SOURCE_ROOT || '/src');
8
8
  export const OUT = join(ROOT, 'docker/vendor');
9
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));
10
+ export const SELECTED = new Set((process.env.OURS_BUILD_PACKAGES || 'sdk,cli,daemon,tg-connector,cowork,messenger-server').split(',').map(name => '@ours.network/' + name));
11
11
  export const inheritedLock = process.env.OURS_INSTALLER_LOCK_FD === '3' ? [3] : [];
12
12
  export const run = (args, cwd) => execFileSync(args[0], args.slice(1), { cwd, stdio: ['inherit', 'inherit', 'inherit', ...inheritedLock] });
13
13
  export const capture = (args, cwd, env = process.env) => execFileSync(args[0], args.slice(1), { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit', ...inheritedLock] });
@@ -2,8 +2,13 @@ import { join } from 'node:path';
2
2
  import { SOURCE_ROOT, SELECTED, pack, run } from './build-common.mjs';
3
3
  const source = join(SOURCE_ROOT, 'sdk');
4
4
  run(['npm', 'ci', '--no-audit', '--no-fund'], source);
5
- run(['bash', 'scripts/compile-mufl.sh'], source);
5
+ if (SELECTED.has('@ours.network/daemon')) run(['bash', 'scripts/compile-mufl.sh'], source);
6
6
  run(['npm', 'run', 'build'], source);
7
7
  if (SELECTED.has('@ours.network/cli')) run(['npm', 'run', 'build:cli'], source);
8
8
  pack(source, '@ours.network/sdk', 'sdk');
9
9
  pack(join(source, 'packages/cli'), '@ours.network/cli', 'sdk');
10
+
11
+ if (SELECTED.has('@ours.network/daemon')) {
12
+ run(['npm', 'run', 'build:daemon'], source);
13
+ pack(join(source, 'packages/daemon'), '@ours.network/daemon', 'sdk');
14
+ }
@@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { createBuildContext, CONTEXT } from '../maintenance/build-context.mjs';
7
+ import { verifyRuntimeRelease } from '../maintenance/release-graph.mjs';
7
8
  export function finalizeBuild(root) {
8
9
  if (fs.realpathSync(root) !== root) throw new Error('Build root must be canonical');
9
10
  try { fs.lstatSync(join(root, CONTEXT)); throw new Error('Existing context cannot be regenerated'); }
@@ -20,6 +21,7 @@ export function finalizeBuild(root) {
20
21
  if (typeof spec !== 'string' || !/^file:docker\/vendor\/ours\.network-[a-z-]+\.tgz$/.test(spec)) throw new Error('Unexpected installer vendor reference');
21
22
  protect(join(root, spec.slice(5)));
22
23
  }
24
+ verifyRuntimeRelease(root);
23
25
  const tree = execFileSync('npm', ['ls', '--omit=dev', '--all', '--json'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
24
26
  JSON.parse(tree);
25
27
  const path = join(root, 'dependency-tree.json');
@@ -8,7 +8,7 @@ import { semanticRecordEqual } from './provenance-compare.mjs';
8
8
  export const BASE_RECORDS = ['package-lock.json', 'dependency-tree.json'];
9
9
  export const CONTEXT = 'build-context.json';
10
10
  const admitted = new WeakMap();
11
- const packages = new Set(['sdk', 'cli', 'mcp', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'codex', 'claude-code', 'install'].map(n => '@ours.network/' + n));
11
+ const packages = new Set(['sdk', 'cli', 'daemon', 'mcp', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'codex', 'claude-code', 'install'].map(n => '@ours.network/' + n));
12
12
  const digest = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
13
13
  const fail = text => { throw new Error(`Build context: ${text}`); };
14
14
  const keys = (v, expected) => v && !Array.isArray(v) && typeof v === 'object' && Object.keys(v).sort().join('\0') === [...expected].sort().join('\0');
@@ -0,0 +1,15 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // Native callers provide an exact selected executable. Compose mounts the
5
+ // selected runtime and supplies its bin directory, including retained releases.
6
+ export function daemonOwner(env = process.env) {
7
+ if (env.OURS_CLI_PATH) return env.OURS_CLI_PATH;
8
+ const bin = env.OURS_DAEMON_BIN_DIR;
9
+ if (!bin) throw new Error('daemon maintenance requires its selected executable or bin directory');
10
+ const daemon = join(bin, 'ours-daemon');
11
+ if (existsSync(daemon)) return daemon;
12
+ const legacy = join(bin, 'ours');
13
+ if (existsSync(legacy)) return legacy;
14
+ throw new Error('selected runtime contains no daemon administrator');
15
+ }
@@ -6,6 +6,8 @@ import { pathToFileURL } from 'node:url';
6
6
  import { copyPrivateTree, scanSource, createArchive, validateArchive } from './state-archive.mjs';
7
7
  import { publishNoReplace } from './state-native.mjs';
8
8
 
9
+ import { daemonOwner } from './daemon-owner.mjs';
10
+
9
11
  const COMPONENTS = ['daemon', 'telegram', 'cowork', 'messenger'];
10
12
  import { recordNames, readBuildRecords, validateBuildRecordSet } from './build-context.mjs';
11
13
  const DAEMON_STATE = '/var/lib/ours';
@@ -207,7 +209,7 @@ export async function runDockerLayoutCommand(argv, env = process.env) {
207
209
  const build = env.OURS_BUILD_ROOT || '/opt/ours';
208
210
  const options = {
209
211
  uid: process.getuid(), gid: process.getgid(), instanceId: env.OURS_DAEMON_ID,
210
- cli: env.OURS_CLI_PATH || '/opt/ours/node_modules/.bin/ours',
212
+ cli: daemonOwner({ OURS_DAEMON_BIN_DIR: '/opt/ours/node_modules/.bin', ...env }),
211
213
  configPath: env.OURS_DAEMON_CONFIG || '/var/lib/ours/config.json',
212
214
  provenance: readBuildRecords(build),
213
215
  };
@@ -0,0 +1,111 @@
1
+ /** Enforce the selected ours artifacts; this is not third-party lock replay. */
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { join, resolve, relative, isAbsolute } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+
6
+ const names = ['sdk', 'cli', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'mcp', 'codex', 'claude-code'].map(name => '@ours.network/' + name);
7
+ const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
8
+ const nightly = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-nightly\.(0|[1-9]\d*)(?:\.[0-9a-f]{7,40})?$/;
9
+ const fail = message => { throw new Error(`Release graph refused: ${message}`); };
10
+ function file(path) {
11
+ if (!lstatSync(path).isFile() || realpathSync(path) !== path) fail(`non-regular or linked file: ${path}`);
12
+ return readFileSync(path);
13
+ }
14
+ const json = path => JSON.parse(file(path));
15
+
16
+ export function releaseBinding(policy) {
17
+ // Only an absent binding denotes explicit development/retained legacy inputs.
18
+ if (!Object.hasOwn(policy ?? {}, 'release')) return null;
19
+ const release = policy.release;
20
+ if (release?.schema !== 1 || !['stable', 'nightly'].includes(release.channel)) fail('invalid release binding');
21
+ const pattern = release.channel === 'nightly' ? nightly : stable;
22
+ if (typeof release.installerVersion !== 'string' || !pattern.test(release.installerVersion)) fail('invalid installer version/channel');
23
+ if (release.scope !== undefined && release.scope !== 'host-cli') fail('invalid release scope');
24
+ const required = release.scope === 'host-cli' ? ['@ours.network/cli', '@ours.network/sdk'] : [...names, ...(release.packages?.['@ours.network/daemon'] ? ['@ours.network/daemon'] : [])];
25
+ if (!release.packages || JSON.stringify(Object.keys(release.packages).sort()) !== JSON.stringify([...required].sort())) fail('release must select exactly the required ours packages');
26
+ if (release.scope === 'host-cli' && Object.hasOwn(release, 'hostCli')) fail('nested host CLI selection');
27
+ for (const [name, entry] of Object.entries(release.packages)) {
28
+ if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid release artifact: ${name}`);
29
+ }
30
+ if (Object.hasOwn(release, 'hostCli')) {
31
+ if (!release.hostCli || Object.keys(release.hostCli).sort().join(',') !== '@ours.network/cli,@ours.network/sdk') fail('host CLI must select exactly CLI and SDK');
32
+ for (const [name, entry] of Object.entries(release.hostCli)) {
33
+ if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid host CLI artifact: ${name}`);
34
+ }
35
+ }
36
+ if (!policy.packages || !Object.keys(policy.packages).length) fail('missing release source selection');
37
+ for (const [name, entry] of Object.entries(policy.packages)) {
38
+ if (entry?.type !== 'npm' || entry.version !== release.packages[name]?.version || Object.keys(entry).sort().join(',') !== 'type,version') fail(`source selection differs from release: ${name}`);
39
+ }
40
+ return release;
41
+ }
42
+
43
+ export function verifyReleaseGraph(root, policy, { requiredPackages = Object.keys(policy?.packages ?? {}) } = {}) {
44
+ const release = releaseBinding(policy);
45
+ if (!release) return { verified: false, reason: 'development or retained legacy source selection' };
46
+ root = resolve(root);
47
+ if (realpathSync(root) !== root) fail('build root is not canonical');
48
+ const manifest = json(join(root, 'package.json'));
49
+ if (manifest.name?.startsWith('@ours.network/') && manifest.version !== release.packages[manifest.name]?.version) fail(`root package differs from release: ${manifest.name}`);
50
+ const locks = ['package-lock.json', 'node_modules/.package-lock.json'].map(path => {
51
+ const lock = json(join(root, path));
52
+ if (lock.lockfileVersion !== 3 || !lock.packages || Array.isArray(lock.packages)) fail(`invalid lock: ${path}`);
53
+ const entries = new Map();
54
+ for (const [location, entry] of Object.entries(lock.packages)) {
55
+ const match = location.match(/(?:^|\/)node_modules\/(@ours\.network\/[^/]+)$/);
56
+ if (!match) continue;
57
+ const name = match[1], expected = release.packages[name];
58
+ if (!expected || entry?.link || entry.version !== expected.version || entry.integrity !== expected.integrity) fail(`version/integrity mismatch: ${path}:${location}`);
59
+ const installed = resolve(root, location), rel = relative(root, installed);
60
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) fail(`escaping lock location: ${location}`);
61
+ if (typeof entry.resolved !== 'string') fail(`missing artifact location: ${location}`);
62
+ if (entry.resolved.startsWith('file:')) {
63
+ const archive = resolve(root, entry.resolved.slice(5));
64
+ const archiveRel = relative(root, archive);
65
+ if (archiveRel.startsWith('..') || isAbsolute(archiveRel) || !archive.endsWith('.tgz')) fail(`unbound local artifact: ${location}`);
66
+ const integrity = 'sha512-' + createHash('sha512').update(file(archive)).digest('base64');
67
+ if (integrity !== expected.integrity) fail(`vendor bytes differ from release: ${location}`);
68
+ } else if (!entry.resolved.startsWith(`https://registry.npmjs.org/${name}/-/`)) fail(`non-official artifact: ${location}`);
69
+ entries.set(location, { name, entry, installed });
70
+ }
71
+ return entries;
72
+ });
73
+ const [declared, installed] = locks;
74
+ const direct = Object.entries(manifest.dependencies ?? {}).filter(([name]) => name.startsWith('@ours.network/'));
75
+ if (!manifest.name?.startsWith('@ours.network/') && JSON.stringify(direct.map(([name]) => name).sort()) !== JSON.stringify([...requiredPackages].sort())) fail('direct package set differs from release source selection');
76
+ for (const [name, spec] of direct) {
77
+ const expected = release.packages[name], locked = declared.get(`node_modules/${name}`)?.entry;
78
+ if (!expected || (spec !== expected.version && !(typeof spec === 'string' && spec.startsWith('file:') && spec === locked?.resolved))) fail(`direct package spec differs from release: ${name}`);
79
+ }
80
+ for (const name of Object.keys(manifest.dependencies ?? {}).filter(name => name.startsWith('@ours.network/'))) {
81
+ if (!declared.has(`node_modules/${name}`) || !installed.has(`node_modules/${name}`)) fail(`missing direct release package: ${name}`);
82
+ }
83
+ for (const [location, value] of declared) {
84
+ // npm's root lock retains dev-only entries omitted from a production install.
85
+ if (!installed.has(location) && !value.entry.dev && !value.entry.optional) fail(`installed release entry missing: ${location}`);
86
+ }
87
+ for (const [location, value] of installed) {
88
+ const expected = declared.get(location);
89
+ if (!expected || expected.entry.version !== value.entry.version || expected.entry.integrity !== value.entry.integrity || expected.entry.resolved !== value.entry.resolved) fail(`installed/root lock mismatch: ${location}`);
90
+ if (!lstatSync(value.installed).isDirectory() || realpathSync(value.installed) !== value.installed) fail(`linked installed package: ${location}`);
91
+ const actual = json(join(value.installed, 'package.json'));
92
+ if (actual.name !== value.name || actual.version !== value.entry.version) fail(`installed package differs from release: ${location}`);
93
+ }
94
+ return { verified: true, packages: installed.size };
95
+ }
96
+
97
+ export function verifyRuntimeRelease(root) {
98
+ const path = join(resolve(root), 'sources.json');
99
+ // Historic installations predate sources/release bindings; their existing
100
+ // provenance and conversion checks remain authoritative until explicit update.
101
+ if (!existsSync(path)) return { verified: false, reason: 'legacy runtime without sources' };
102
+ return verifyReleaseGraph(root, json(path));
103
+ }
104
+
105
+ /** The client command has its own exact dependency selection, never a graph bypass. */
106
+ export function hostCliPolicy(policy) {
107
+ const release = releaseBinding(policy);
108
+ if (!release?.hostCli) return null;
109
+ const { hostCli, ...base } = release;
110
+ return { release: { ...base, scope: 'host-cli', packages: hostCli }, packages: Object.fromEntries(Object.entries(hostCli).map(([name, p]) => [name, { type: 'npm', version: p.version }])) };
111
+ }
@@ -9,6 +9,8 @@ import { createArchive, extractArchive, validateArchive, scanSource, copyPrivate
9
9
  import { exchange, tryLock, setMtimeNs } from './state-native.mjs';
10
10
  import { recordNames, readBuildRecords, equalBuildRecords, initializeBuildMarker } from './build-context.mjs';
11
11
 
12
+ import { daemonOwner } from './daemon-owner.mjs';
13
+
12
14
  const PROVENANCE = '.ours-provenance';
13
15
  const APPLICATIONS = ['daemon', 'telegram', 'cowork', 'messenger'];
14
16
  const exists = path => { try { fs.lstatSync(path); return true; } catch (e) { if (e.code === 'ENOENT') return false; throw e; } };
@@ -145,9 +147,9 @@ export async function runStateOperation(argv, env = process.env, checkpoints = {
145
147
  }
146
148
  function retainAuthority(staging) {
147
149
  if (!['daemon', 'server'].includes(domain)) return;
148
- if (!env.OURS_CLI_PATH || !env.OURS_DAEMON_CONFIG) fail('daemon maintenance requires its selected executable and configuration');
150
+ if (!env.OURS_DAEMON_CONFIG) fail('daemon maintenance requires its selected executable and configuration');
149
151
  const daemon = commonTree ? join(staging, 'daemon') : staging;
150
- owner([env.OURS_CLI_PATH, 'config', 'access-retain', '--config', env.OURS_DAEMON_CONFIG, '--target-state-dir', daemon, '--json']);
152
+ owner([daemonOwner(env), 'config', 'access-retain', '--config', env.OURS_DAEMON_CONFIG, '--target-state-dir', daemon, '--json']);
151
153
  if (!commonTree) return;
152
154
  const credentials = join(live, 'credentials'), destination = join(staging, 'credentials');
153
155
  for (const name of ['telegram', 'cowork', 'messenger']) if (!privateStat(join(credentials, name, 'daemon-token'), false, 0o600).size) fail('current managed credential is empty');
@@ -2,7 +2,7 @@ import { readBuildRecords, initializeBuildMarker } from '../maintenance/build-co
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { randomBytes } from 'node:crypto';
4
4
  import {
5
- closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync,
5
+ existsSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync,
6
6
  realpathSync, renameSync, unlinkSync, writeFileSync, readdirSync, chmodSync, chownSync,
7
7
  } from 'node:fs';
8
8
 
@@ -67,7 +67,7 @@ function validateCommon(idName = 'OURS_DAEMON_ID') {
67
67
 
68
68
  function composeConfig(domain) {
69
69
  if (domain === 'daemon') {
70
- const value = { stateDir: '/var/lib/ours', port: 3050, apiVisibility: 'owner', networkMcp: { profile: { endpoint: 'http://127.0.0.1:3050', expectedInstanceId: process.env.OURS_DAEMON_ID, credentialPath: '/var/lib/ours/daemon-token' }, applicationConfigPath: '/var/lib/ours-mcp/config.json' } };
70
+ const value = { stateDir: '/var/lib/ours', port: 3050, apiVisibility: 'owner' };
71
71
  if (process.env.OURS_BROKER_URL) value.brokerUrl = process.env.OURS_BROKER_URL;
72
72
  return value;
73
73
  }
@@ -155,7 +155,7 @@ async function telegramInput() {
155
155
  }
156
156
 
157
157
  function cliJson(args) {
158
- const result = spawnSync(process.execPath, ['/opt/ours/node_modules/@ours.network/cli/dist/cli.js', ...args], {
158
+ const result = spawnSync(process.execPath, [existsSync('/opt/ours/node_modules/@ours.network/daemon/dist/cli.js') ? '/opt/ours/node_modules/@ours.network/daemon/dist/cli.js' : '/opt/ours/node_modules/@ours.network/cli/dist/cli.js', ...args], {
159
159
  encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024,
160
160
  });
161
161
  if (result.status !== 0) fail('Official OURS CLI operation failed');
@@ -10,4 +10,4 @@ else
10
10
  exit "$status"
11
11
  fi
12
12
  node /opt/ours/docker/check-start.mjs
13
- exec node /opt/ours/node_modules/@ours.network/cli/dist/cli.js daemon serve
13
+ exec node /opt/ours/node_modules/@ours.network/daemon/dist/cli.js serve
@@ -0,0 +1,103 @@
1
+ /** Import a stopped, read-only host migration stage into the managed Docker volume. */
2
+ import * as fs from 'node:fs';
3
+ import { dirname, join, resolve, isAbsolute } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { randomUUID } from 'node:crypto';
6
+ // Repository layout and the maintenance image intentionally place this binding differently.
7
+ const nativeModule = new URL('../maintenance/state-native.mjs', import.meta.url);
8
+ const { publishNoReplace } = await import(fs.existsSync(nativeModule) ? nativeModule.href : new URL('./state-native.mjs', import.meta.url).href);
9
+
10
+ const MARKER = '.ours-legacy-import.json';
11
+ const COMPONENTS = ['mcp', 'daemon'];
12
+ const fail = message => { throw Error(`Legacy volume import: ${message}`); };
13
+ function canonical(path) {
14
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path) fail('paths must be normalized absolute paths');
15
+ if (fs.realpathSync(path) !== path) fail('symlink paths are not supported');
16
+ }
17
+ function privateEntry(path, uid, gid, directory) {
18
+ const stat = fs.lstatSync(path);
19
+ if (!(directory ? stat.isDirectory() : stat.isFile()) || stat.isSymbolicLink()
20
+ || stat.uid !== uid || stat.gid !== gid
21
+ || (directory ? (stat.mode & 0o7777) !== 0o700 : ((stat.mode & 0o7077) !== 0 || !(stat.mode & 0o400)))
22
+ || (!directory && stat.nlink !== 1)) fail('unsafe ownership, permissions, or linked state entry');
23
+ return stat;
24
+ }
25
+ function scan(root, uid, gid) {
26
+ const entries = [];
27
+ function visit(path, relative) {
28
+ const stat = fs.lstatSync(path), directory = stat.isDirectory();
29
+ privateEntry(path, uid, gid, directory); entries.push({ relative, directory, mode: stat.mode & 0o700 });
30
+ if (directory) for (const name of fs.readdirSync(path).sort()) visit(join(path, name), relative ? join(relative, name) : name);
31
+ }
32
+ canonical(root); visit(root, ''); return entries;
33
+ }
34
+ function readReceipt(root, uid, gid, targetRoot) {
35
+ privateEntry(join(root, MARKER), uid, gid, false);
36
+ const value = JSON.parse(fs.readFileSync(join(root, MARKER), 'utf8'));
37
+ const keys = ['sourceStateDir', 'sourceConfigPath', 'targetRoot', 'rootName'];
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)
39
+ || Object.keys(value).sort().join('\0') !== [...keys].sort().join('\0')
40
+ || keys.some(key => typeof value[key] !== 'string' || !value[key])
41
+ || value.targetRoot !== targetRoot
42
+ || ['sourceStateDir', 'sourceConfigPath', 'targetRoot'].some(key => !isAbsolute(value[key]) || resolve(value[key]) !== value[key])) fail('invalid or mismatched migration receipt');
43
+ return Object.fromEntries(keys.map(key => [key, value[key]]));
44
+ }
45
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
46
+ function existingDestination(path, expected, uid, gid, targetRoot) {
47
+ if (!fs.existsSync(path)) return false;
48
+ canonical(path); privateEntry(path, uid, gid, true);
49
+ if (!fs.readdirSync(path).length) return false;
50
+ if (!fs.existsSync(join(path, MARKER)) || !same(readReceipt(path, uid, gid, targetRoot), expected)) fail('nonmatching existing destination');
51
+ return true;
52
+ }
53
+ function makeDirectory(path, uid, gid) {
54
+ if (!fs.existsSync(path)) { fs.mkdirSync(path, { mode: 0o700 }); fs.chownSync(path, uid, gid); }
55
+ canonical(path); privateEntry(path, uid, gid, true);
56
+ }
57
+
58
+ /** Caller holds installation exclusion and has stopped all destination writers. */
59
+ export function importLegacyVolume({ sourceRoot = '/legacy-import', storageRoot = '/storage', targetRoot, uid, gid }) {
60
+ if (![uid, gid].every(value => Number.isSafeInteger(value) && value >= 0)) fail('numeric target UID/GID required');
61
+ if (typeof targetRoot !== 'string' || !isAbsolute(targetRoot) || resolve(targetRoot) !== targetRoot) fail('expected target root required');
62
+ canonical(sourceRoot); canonical(storageRoot);
63
+ const sourceOwner = fs.lstatSync(sourceRoot);
64
+ privateEntry(sourceRoot, sourceOwner.uid, sourceOwner.gid, true);
65
+ const sources = COMPONENTS.map(component => {
66
+ const path = join(sourceRoot, component);
67
+ const entries = scan(path, sourceOwner.uid, sourceOwner.gid);
68
+ return { component, path, entries, receipt: readReceipt(path, sourceOwner.uid, sourceOwner.gid, targetRoot) };
69
+ });
70
+ if (!same(sources[0].receipt, sources[1].receipt)) fail('source daemon and MCP receipts differ');
71
+ const rootStat = fs.lstatSync(storageRoot);
72
+ const freshRoot = rootStat.isDirectory() && rootStat.uid === 0 && rootStat.gid === 0 && fs.readdirSync(storageRoot).length === 0;
73
+ if (!freshRoot) privateEntry(storageRoot, uid, gid, true);
74
+ const state = join(storageRoot, 'state');
75
+ if (fs.existsSync(state)) { canonical(state); privateEntry(state, uid, gid, true); }
76
+ // Validate every existing destination before changing even an empty volume.
77
+ for (const source of sources) source.reused = existingDestination(join(state, source.component), source.receipt, uid, gid, targetRoot);
78
+ if (freshRoot) { fs.chmodSync(storageRoot, 0o700); fs.chownSync(storageRoot, uid, gid); }
79
+ makeDirectory(state, uid, gid);
80
+ for (const source of sources) {
81
+ if (source.reused) continue;
82
+ const destination = join(state, source.component), staging = join(state, `.legacy-import-${randomUUID()}`);
83
+ try {
84
+ fs.cpSync(source.path, staging, { recursive: true, errorOnExist: true, force: false });
85
+ for (const entry of [...source.entries].reverse()) {
86
+ const path = entry.relative ? join(staging, entry.relative) : staging;
87
+ fs.chmodSync(path, entry.mode); fs.chownSync(path, uid, gid);
88
+ }
89
+ scan(staging, uid, gid);
90
+ if (!same(readReceipt(staging, uid, gid, targetRoot), source.receipt)) fail('copied receipt changed');
91
+ if (fs.existsSync(destination)) fs.rmdirSync(destination); // empty skeleton only
92
+ publishNoReplace(staging, destination);
93
+ } finally { fs.rmSync(staging, { recursive: true, force: true }); }
94
+ }
95
+ return { imported: sources.filter(source => !source.reused).map(source => source.component), retained: sources.filter(source => source.reused).map(source => source.component) };
96
+ }
97
+
98
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
99
+ try {
100
+ importLegacyVolume({ targetRoot: process.env.OURS_LEGACY_TARGET_ROOT, uid: Number(process.env.OURS_UID), gid: Number(process.env.OURS_GID) });
101
+ console.log('Legacy daemon and MCP state imported into the selected volume.');
102
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
103
+ }
@@ -1,4 +1,5 @@
1
1
  import { readBuildRecords, initializeBuildMarker } from '../maintenance/build-context.mjs';
2
+ import { verifyRuntimeRelease } from '../maintenance/release-graph.mjs';
2
3
  import { accessSync, constants, lstatSync, readFileSync } from 'node:fs';
3
4
 
4
5
  export function privatePath(path, directory = false, writable = false) {
@@ -43,5 +44,6 @@ export function checkCredential(path) {
43
44
  // Called with the startup state-directory lock held. These are build records,
44
45
  // not a storage schema or a declaration that arbitrary upgrades are compatible.
45
46
  export function recordBuild(state) {
47
+ verifyRuntimeRelease('/opt/ours');
46
48
  initializeBuildMarker(`${state}/.ours-provenance`, readBuildRecords('/opt/ours'));
47
49
  }
@@ -1,21 +1,101 @@
1
1
  {
2
- "sources": {
3
- "sdk": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-sdk.git", "commit": "9cb51add34888e6ae2cc3b554c97362bb1ca2d33" },
4
- "telegram": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-tg-connector.git", "commit": "81798b836b15ac4ba9b32ac180f94fb258a54a41" },
5
- "cowork": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-cowork.git", "commit": "1e17f7666c1682a858101c98a8f371aac7b737e3" },
6
- "messenger": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-messenger-server.git", "commit": "fc852e49eb3b43c077f59cf4919536be6f6d03b1" },
7
- "fleet": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-fleet.git", "commit": "b8444a7aac2eb6148a3ab534e90308bfc76447d7" },
8
- "mcp": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-mcp.git", "commit": "3d970d0cb7baf52351d5cd44dd59d4a2f705207b" }
2
+ "release": {
3
+ "schema": 1,
4
+ "channel": "nightly",
5
+ "installerVersion": "1.2.1-nightly.10",
6
+ "packages": {
7
+ "@ours.network/sdk": {
8
+ "version": "3.8.1-nightly.9",
9
+ "integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
10
+ },
11
+ "@ours.network/cli": {
12
+ "version": "2.8.1-nightly.7",
13
+ "integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
14
+ },
15
+ "@ours.network/tg-connector": {
16
+ "version": "1.0.1-nightly.3",
17
+ "integrity": "sha512-UDHEgM5C3mnnyfV3KTM2XFDGZ7vYs7oBE/3r8bKDtxJdlQZ4bxxI44LKrPI3gO2sdGROsDdM8/tvzwbJ8MiBYw=="
18
+ },
19
+ "@ours.network/cowork": {
20
+ "version": "1.3.2-nightly.20260921.0d6bb50",
21
+ "integrity": "sha512-gyUgY7Fw6RQVhONqexLNcS8m0WuBUQiHYIrWqd2FeAyyJOONjYGfbRtgJ67Xin1YTYf8Cxlomuf6j8cEc4ZwZg=="
22
+ },
23
+ "@ours.network/messenger-server": {
24
+ "version": "1.0.31-nightly.3",
25
+ "integrity": "sha512-j86POD2U2yh7jiIqbIixDu8TpEpaVf+dHLs3adtTRfTs6V7ucl//digdkyc1uaZrUliRduxXKBOrb/1Nu648xQ=="
26
+ },
27
+ "@ours.network/fleet": {
28
+ "version": "1.2.0-nightly.3",
29
+ "integrity": "sha512-FzizLkh1fQ9RkzmID1v42QlZs9E0QHWJ44IWIWzYd0DRaetcXg/5FUVTNc82U5yWZyveBZTJq6mvg9H73NSlkw=="
30
+ },
31
+ "@ours.network/mcp": {
32
+ "version": "1.2.0-nightly.5",
33
+ "integrity": "sha512-CsRsHlJwU5WkUeF8mMLysq9GsYLBA2wnl8v+qZhaJZhSJWoNb5TqfXd2v5/Su7VUpOaRwvubv/9Jf86eQgqhGw=="
34
+ },
35
+ "@ours.network/codex": {
36
+ "version": "1.2.0-nightly.5",
37
+ "integrity": "sha512-6E3tE0valPE0FSOsONh3BrrBo9Gxt1IgWm5Hwj0xm00mGQ+qsjGRrzCB8snrOh9OdnGuvPOF20aunmp4g4KoMg=="
38
+ },
39
+ "@ours.network/claude-code": {
40
+ "version": "1.2.0-nightly.5",
41
+ "integrity": "sha512-PxPegFvXx5bkvTprB8dcPjIYvE5bvjQoAtw31onOAZa0YdX2K7yYpesYcZ0sOnFdSQkI4+aaVyTb1Lj9EiMABQ=="
42
+ },
43
+ "@ours.network/daemon": {
44
+ "version": "3.8.1-nightly.1",
45
+ "integrity": "sha512-1+RA0UH/wJUSmKnmZuHS3v6UyH5PsrZWoqwa5rFKl6n2Cd8sWDs++DrwwV0IOFdgFcDOtb/k8mes4sI2J7J8mg=="
46
+ }
47
+ },
48
+ "hostCli": {
49
+ "@ours.network/sdk": {
50
+ "version": "3.8.1-nightly.9",
51
+ "integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
52
+ },
53
+ "@ours.network/cli": {
54
+ "version": "2.8.1-nightly.7",
55
+ "integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
56
+ }
57
+ }
9
58
  },
10
59
  "packages": {
11
- "@ours.network/sdk": { "source": "sdk" },
12
- "@ours.network/cli": { "source": "sdk" },
13
- "@ours.network/tg-connector": { "source": "telegram" },
14
- "@ours.network/cowork": { "source": "cowork" },
15
- "@ours.network/messenger-server": { "source": "messenger" },
16
- "@ours.network/fleet": { "source": "fleet" },
17
- "@ours.network/mcp": { "source": "mcp" },
18
- "@ours.network/codex": { "source": "mcp" },
19
- "@ours.network/claude-code": { "source": "mcp" }
60
+ "@ours.network/sdk": {
61
+ "type": "npm",
62
+ "version": "3.8.1-nightly.9"
63
+ },
64
+ "@ours.network/cli": {
65
+ "type": "npm",
66
+ "version": "2.8.1-nightly.7"
67
+ },
68
+ "@ours.network/tg-connector": {
69
+ "type": "npm",
70
+ "version": "1.0.1-nightly.3"
71
+ },
72
+ "@ours.network/cowork": {
73
+ "type": "npm",
74
+ "version": "1.3.2-nightly.20260921.0d6bb50"
75
+ },
76
+ "@ours.network/messenger-server": {
77
+ "type": "npm",
78
+ "version": "1.0.31-nightly.3"
79
+ },
80
+ "@ours.network/fleet": {
81
+ "type": "npm",
82
+ "version": "1.2.0-nightly.3"
83
+ },
84
+ "@ours.network/mcp": {
85
+ "type": "npm",
86
+ "version": "1.2.0-nightly.5"
87
+ },
88
+ "@ours.network/codex": {
89
+ "type": "npm",
90
+ "version": "1.2.0-nightly.5"
91
+ },
92
+ "@ours.network/claude-code": {
93
+ "type": "npm",
94
+ "version": "1.2.0-nightly.5"
95
+ },
96
+ "@ours.network/daemon": {
97
+ "type": "npm",
98
+ "version": "3.8.1-nightly.1"
99
+ }
20
100
  }
21
101
  }
package/install.mjs CHANGED
@@ -3,14 +3,14 @@
3
3
  import { readFileSync } from 'node:fs';
4
4
 
5
5
  import { realEffects } from './lib/effects.mjs';
6
- import { runInstall } from './lib/orchestrate.mjs';
6
+ import { runSetup } from './lib/setup.mjs';
7
7
  import { closeSync, makeWriter, openTty } from './lib/ui.mjs';
8
8
 
9
9
  const version = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
10
10
  const ttyFd = openTty();
11
11
 
12
12
  try {
13
- const code = await runInstall(process.argv.slice(2), realEffects({
13
+ const code = await runSetup(process.argv.slice(2), realEffects({
14
14
  write: makeWriter(ttyFd),
15
15
  ttyFd,
16
16
  env: process.env,
package/install.sh CHANGED
@@ -9,7 +9,7 @@
9
9
  # pipes a script into your shell). It just gets Node.js/npm sorted, then does the same
10
10
  # `npm i -g @ours.network/install` and runs `ours-install`. Meant to be run as:
11
11
  #
12
- # curl -fsSL https://raw.githubusercontent.com/adapt-toolkit/ours-mcp/main/packages/installer/install.sh | bash
12
+ # curl -fsSL https://raw.githubusercontent.com/adapt-toolkit/ours-network/main/install.sh | bash
13
13
  #
14
14
  # This file is a THIN bootstrap: it checks that Node.js + npm are present (and prints friendly,
15
15
  # per-OS guidance if not), then runs the real experience — the Node installer (install.mjs) that