@ours.network/install 1.2.1-nightly.24 → 1.2.1-nightly.26
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 +1 -1
- package/assets/Dockerfile +1 -0
- package/assets/release-lock.json +7 -7
- package/assets/release.json +3 -3
- package/assets/scripts/runtime/client-setup.mjs +20 -2
- package/assets/scripts/runtime/diagnostics.mjs +72 -0
- package/assets/sources.json +4 -4
- package/install.mjs +2 -1
- package/lib/client-cli.mjs +1 -1
- package/lib/diagnostics.mjs +1 -0
- package/lib/docker-runtime-repair.mjs +14 -6
- package/lib/effects.mjs +15 -6
- package/lib/orchestrate.mjs +2 -6
- package/lib/server-onboarding.mjs +3 -2
- package/lib/setup-options.mjs +10 -2
- package/lib/setup.mjs +2 -1
- package/lib/usage.mjs +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -591,4 +591,4 @@ Existing installation records keep their ports and identities. A listener acquir
|
|
|
591
591
|
by another process after preflight still causes startup to fail safely; no foreign
|
|
592
592
|
process is stopped and no retained installation is silently moved.
|
|
593
593
|
|
|
594
|
-
Workspace setup pins Fleet `1.2.0-nightly.
|
|
594
|
+
Workspace setup pins Fleet `1.2.0-nightly.33` with its recorded registry integrity. This release includes the packaged `/fleet` browser entry, separate `ours-fleet setup-tunnel` and `ours-fleet link-device` commands, 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. Existing installation identities and configured ports remain retained; a retained-port collision fails safely.
|
package/assets/Dockerfile
CHANGED
|
@@ -23,6 +23,7 @@ COPY --chmod=644 scripts/runtime/runtime-common.mjs /opt/ours/docker/runtime-com
|
|
|
23
23
|
COPY --chmod=644 scripts/runtime/check-client.mjs /opt/ours/docker/check-client.mjs
|
|
24
24
|
COPY --chmod=644 scripts/runtime/check-start.mjs /opt/ours/docker/check-start.mjs
|
|
25
25
|
COPY --chmod=644 scripts/runtime/client-setup.mjs /opt/ours/docker/client-setup.mjs
|
|
26
|
+
COPY --chmod=644 scripts/runtime/diagnostics.mjs /opt/ours/docker/diagnostics.mjs
|
|
26
27
|
COPY --chmod=644 scripts/runtime/legacy-import.mjs /opt/ours/docker/legacy-import.mjs
|
|
27
28
|
COPY --chmod=644 scripts/runtime/entrypoint.sh /opt/ours/docker/entrypoint.sh
|
|
28
29
|
COPY --chmod=644 scripts/runtime/healthcheck.mjs /opt/ours/docker/healthcheck.mjs
|
package/assets/release-lock.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@ours.network/codex": "1.2.0-nightly.10",
|
|
14
14
|
"@ours.network/cowork": "1.3.3-nightly.20261001.6d49d3a",
|
|
15
15
|
"@ours.network/daemon": "3.8.1-nightly.7",
|
|
16
|
-
"@ours.network/fleet": "1.2.0-nightly.
|
|
16
|
+
"@ours.network/fleet": "1.2.0-nightly.33",
|
|
17
17
|
"@ours.network/mcp": "1.2.0-nightly.10",
|
|
18
18
|
"@ours.network/messenger-server": "1.0.31-nightly.13",
|
|
19
19
|
"@ours.network/sdk": "3.8.1-nightly.15",
|
|
@@ -440,9 +440,9 @@
|
|
|
440
440
|
}
|
|
441
441
|
},
|
|
442
442
|
"node_modules/@agentclientprotocol/sdk": {
|
|
443
|
-
"version": "1.6.
|
|
444
|
-
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.6.
|
|
445
|
-
"integrity": "sha512-
|
|
443
|
+
"version": "1.6.1",
|
|
444
|
+
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.6.1.tgz",
|
|
445
|
+
"integrity": "sha512-ziLAZzqrAyW3bkK24c2DSTzLw48fUSKtgG3MTe2pqZgmsvepxXZFlUh9y8UZ2eYOnQ3qBgdcl879YrdWiTu6HQ==",
|
|
446
446
|
"license": "Apache-2.0",
|
|
447
447
|
"peerDependencies": {
|
|
448
448
|
"zod": "^3.25.0 || ^4.0.0"
|
|
@@ -1085,9 +1085,9 @@
|
|
|
1085
1085
|
}
|
|
1086
1086
|
},
|
|
1087
1087
|
"node_modules/@ours.network/fleet": {
|
|
1088
|
-
"version": "1.2.0-nightly.
|
|
1089
|
-
"resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.
|
|
1090
|
-
"integrity": "sha512-
|
|
1088
|
+
"version": "1.2.0-nightly.33",
|
|
1089
|
+
"resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.33.tgz",
|
|
1090
|
+
"integrity": "sha512-2z1Q01LeLRnq1HOcBeYJ67WnH5hI/tC/YdTkA4/K2LFdFtbff0j7M2l4SZDpKQaL8zgLjL8NREMSlOufdULJaA==",
|
|
1091
1091
|
"license": "FSL-1.1-Apache-2.0",
|
|
1092
1092
|
"dependencies": {
|
|
1093
1093
|
"@agentclientprotocol/sdk": "^1.3.0",
|
package/assets/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"channel": "nightly",
|
|
4
|
-
"installerVersion": "1.2.1-nightly.
|
|
4
|
+
"installerVersion": "1.2.1-nightly.26",
|
|
5
5
|
"packages": {
|
|
6
6
|
"@ours.network/sdk": {
|
|
7
7
|
"version": "3.8.1-nightly.15",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"integrity": "sha512-hDosXHgQ+vyzPdSxqTz6vCaOCn0LMTmOL4TJiWplkJhzyw2c586RckXJbqNJsTa0mmodvLxW0ewZyQ/xJVIeXA=="
|
|
25
25
|
},
|
|
26
26
|
"@ours.network/fleet": {
|
|
27
|
-
"version": "1.2.0-nightly.
|
|
28
|
-
"integrity": "sha512-
|
|
27
|
+
"version": "1.2.0-nightly.33",
|
|
28
|
+
"integrity": "sha512-2z1Q01LeLRnq1HOcBeYJ67WnH5hI/tC/YdTkA4/K2LFdFtbff0j7M2l4SZDpKQaL8zgLjL8NREMSlOufdULJaA=="
|
|
29
29
|
},
|
|
30
30
|
"@ours.network/mcp": {
|
|
31
31
|
"version": "1.2.0-nightly.10",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readBuildRecords, initializeBuildMarker } from '../maintenance/build-context.mjs';
|
|
2
|
+
import { redactDiagnostic } from './diagnostics.mjs';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { randomBytes } from 'node:crypto';
|
|
4
5
|
import {
|
|
@@ -12,6 +13,7 @@ const DELIVERY_FILES = [
|
|
|
12
13
|
'/credentials/messenger/daemon-token',
|
|
13
14
|
];
|
|
14
15
|
const fail = (message) => { throw new Error(message); };
|
|
16
|
+
let diagnosticStage = 'validation';
|
|
15
17
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
16
18
|
|
|
17
19
|
function privatePath(path, directory, exact = directory ? 0o700 : 0o600) {
|
|
@@ -128,10 +130,13 @@ function prepare() {
|
|
|
128
130
|
|
|
129
131
|
function finishDaemonSetup() {
|
|
130
132
|
const state = '/var/lib/ours';
|
|
133
|
+
diagnosticStage = 'daemon-provenance';
|
|
131
134
|
initializeBuildMarker(`${state}/.ours-provenance`, readBuildRecords('/opt/ours'));
|
|
135
|
+
diagnosticStage = 'mcp-directory';
|
|
132
136
|
privatePath('/var/lib/ours-mcp', true);
|
|
133
137
|
const mcpProfile = { endpoint: 'http://127.0.0.1:3050', expectedInstanceId: process.env.OURS_DAEMON_ID, credentialPath: `${state}/daemon-token` };
|
|
134
138
|
const mcpPath = '/var/lib/ours-mcp/profile.json';
|
|
139
|
+
diagnosticStage = 'mcp-profile';
|
|
135
140
|
if (!existingPrivate(mcpPath, false)) atomicJson(mcpPath, mcpProfile);
|
|
136
141
|
else if (JSON.stringify(jsonObject(mcpPath)) !== JSON.stringify(mcpProfile)) fail('Existing MCP profile differs from the selected daemon');
|
|
137
142
|
}
|
|
@@ -155,10 +160,19 @@ async function telegramInput() {
|
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
function cliJson(args) {
|
|
163
|
+
diagnosticStage = 'official-cli';
|
|
158
164
|
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
165
|
encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024,
|
|
160
166
|
});
|
|
161
|
-
if (result.status !== 0)
|
|
167
|
+
if (result.error || result.status !== 0) {
|
|
168
|
+
// --json writes failures on stderr. Successful credential stdout is never
|
|
169
|
+
// forwarded, even if a later operation exits unsuccessfully.
|
|
170
|
+
let reason = 'CLI supplied no structured error reason';
|
|
171
|
+
for (const line of String(result.stderr ?? '').split('\n').reverse()) {
|
|
172
|
+
try { const value = JSON.parse(line); if (typeof value?.error?.message === 'string') { reason = value.error.message; break; } } catch {}
|
|
173
|
+
}
|
|
174
|
+
fail(`Official OURS CLI operation failed (${args[1]}; exit=${result.status ?? 'none'}; signal=${result.signal ?? 'none'}; system=${result.error?.code ?? 'none'}): ${reason}`);
|
|
175
|
+
}
|
|
162
176
|
try { return JSON.parse(result.stdout); }
|
|
163
177
|
catch { fail('Official OURS CLI returned malformed JSON'); }
|
|
164
178
|
}
|
|
@@ -192,6 +206,10 @@ try {
|
|
|
192
206
|
else if (['access-init', 'access-issue', 'access-replace'].includes(operation)) access(operation, process.argv[3]);
|
|
193
207
|
else fail('usage: client-setup.mjs prepare | telegram-input | access-init | access-issue [OUTPUT] | access-replace');
|
|
194
208
|
} catch (error) {
|
|
195
|
-
|
|
209
|
+
// Structured stderr retains the failing stage across sensitive Compose
|
|
210
|
+
// capture. The installer redacts secrets before exposing this envelope.
|
|
211
|
+
const message = redactDiagnostic(error.message);
|
|
212
|
+
console.error(JSON.stringify({ oursInstallerError: { stage: diagnosticStage, message } }));
|
|
213
|
+
console.error(`OURS client setup refused: ${message}`);
|
|
196
214
|
process.exit(1);
|
|
197
215
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Retain actionable failure details without emitting credential output. */
|
|
2
|
+
export function redactDiagnostic(value, env = process.env) {
|
|
3
|
+
let text = String(value ?? '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
4
|
+
for (const [key, secret] of Object.entries(env)) {
|
|
5
|
+
if (/(?:token|password|secret|api.?key|private.?key|invitation|enrollment|authorization|credential|access.?token|refresh.?token)/i.test(key) && typeof secret === 'string' && secret)
|
|
6
|
+
text = text.split(secret).join('[redacted]');
|
|
7
|
+
}
|
|
8
|
+
return text
|
|
9
|
+
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g, '[redacted private key]')
|
|
10
|
+
.replace(/(Bearer\s+)[^\s"',;]+/gi, '$1[redacted]')
|
|
11
|
+
.replace(/((?:["']?(?:api[-_]?token|token|password|secret|api[-_]?key|private[-_]?key|invitation|enrollment|authorization|credential|access.?token|refresh.?token)["']?)\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;}]+)/gi, '$1[redacted]')
|
|
12
|
+
.replace(/(--(?:setup-workspace|password|token|secret|api-key|private-key|invitation|enrollment)(?:=|\s+))\S+/gi, '$1[redacted]')
|
|
13
|
+
.replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/g, '$1[redacted]@')
|
|
14
|
+
.replace(/\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '[redacted credential]')
|
|
15
|
+
.replace(/[A-Za-z0-9_+/=-]{43,}/g, '[redacted opaque value]');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const bounded = text => text.length <= 65536 ? text : text.slice(0, 65536) + '\n[Diagnostic output exceeds 64 KiB; remainder omitted.]';
|
|
19
|
+
function credentialFailure(stderr) {
|
|
20
|
+
// Credential stdout can be a successful issuance followed by failure: never
|
|
21
|
+
// include it. Only a structured error envelope or shipped helper refusal is
|
|
22
|
+
// accepted from stderr; arbitrary lines/warnings remain private.
|
|
23
|
+
const lines = String(stderr ?? '').split('\n').reverse();
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(line);
|
|
27
|
+
if (typeof value?.oursInstallerError?.message === 'string') {
|
|
28
|
+
const detail = value.oursInstallerError;
|
|
29
|
+
const stage = /^[a-z][a-z-]{0,40}$/.test(detail.stage ?? '') ? detail.stage : 'credential-operation';
|
|
30
|
+
return `Stage: ${stage}\nReason: ${detail.message}`;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value?.error?.message === 'string') return `Reason: ${value.error.message}`;
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
const prefix = 'OURS client setup refused: ';
|
|
36
|
+
const legacy = lines.find(line => line.startsWith(prefix));
|
|
37
|
+
if (legacy) return `Reason: ${legacy.slice(prefix.length)}`;
|
|
38
|
+
return 'The credential operation provided no structured reason. Its raw output was withheld because it can contain credentials.';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function commandFailure(command, args, result, { sensitive = false, cwd, env = process.env } = {}) {
|
|
42
|
+
const visibleArgs = [...args];
|
|
43
|
+
if (sensitive) {
|
|
44
|
+
for (let i = 0; i < visibleArgs.length - 1; i++) if (['-e', '--eval'].includes(visibleArgs[i])) visibleArgs[i + 1] = '[captured installer helper]';
|
|
45
|
+
}
|
|
46
|
+
const details = [`Command: ${command} ${visibleArgs.join(' ')}${result.status != null ? ` exited ${result.status}` : ''}`];
|
|
47
|
+
if (cwd) details.push(`Working directory: ${cwd}`);
|
|
48
|
+
if (result.status !== null && result.status !== undefined) details.push(`Exit code: ${result.status}`);
|
|
49
|
+
if (result.signal) details.push(`Signal: ${result.signal}`);
|
|
50
|
+
if (result.error?.code) details.push(`System error: ${result.error.code}`);
|
|
51
|
+
if (result.error?.code === 'ETIMEDOUT') details.push('Reason: command timed out.');
|
|
52
|
+
if (sensitive) details.push(credentialFailure(result.stderr));
|
|
53
|
+
else {
|
|
54
|
+
const stderr = String(result.stderr ?? '').trim(), stdout = String(result.stdout ?? '').trim();
|
|
55
|
+
if (stderr) details.push(`Standard error:\n${stderr}`);
|
|
56
|
+
if (stdout) details.push(`Standard output:\n${stdout}`);
|
|
57
|
+
if (!stderr && !stdout) details.push('No captured command output. Streaming commands print their output above.');
|
|
58
|
+
}
|
|
59
|
+
details.push('Keep installation data intact. Include this error and the preceding installation stage when requesting help.');
|
|
60
|
+
return new Error(bounded(redactDiagnostic(details.join('\n'), env)), result.error ? { cause: result.error } : undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function installerFailure(error, env = process.env) {
|
|
64
|
+
const details = []; const seen = new Set();
|
|
65
|
+
for (let cause = error; cause && !seen.has(cause) && seen.size < 8; cause = cause.cause) {
|
|
66
|
+
seen.add(cause);
|
|
67
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
68
|
+
if (!details.includes(message)) details.push(message);
|
|
69
|
+
if (typeof cause.code === 'string' && /^[A-Z_0-9]+$/.test(cause.code)) details.push(`System error: ${cause.code}`);
|
|
70
|
+
}
|
|
71
|
+
return bounded(redactDiagnostic(details.join('\nCaused by: '), env));
|
|
72
|
+
}
|
package/assets/sources.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"release": {
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"channel": "nightly",
|
|
5
|
-
"installerVersion": "1.2.1-nightly.
|
|
5
|
+
"installerVersion": "1.2.1-nightly.26",
|
|
6
6
|
"packages": {
|
|
7
7
|
"@ours.network/sdk": {
|
|
8
8
|
"version": "3.8.1-nightly.15",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"integrity": "sha512-hDosXHgQ+vyzPdSxqTz6vCaOCn0LMTmOL4TJiWplkJhzyw2c586RckXJbqNJsTa0mmodvLxW0ewZyQ/xJVIeXA=="
|
|
26
26
|
},
|
|
27
27
|
"@ours.network/fleet": {
|
|
28
|
-
"version": "1.2.0-nightly.
|
|
29
|
-
"integrity": "sha512-
|
|
28
|
+
"version": "1.2.0-nightly.33",
|
|
29
|
+
"integrity": "sha512-2z1Q01LeLRnq1HOcBeYJ67WnH5hI/tC/YdTkA4/K2LFdFtbff0j7M2l4SZDpKQaL8zgLjL8NREMSlOufdULJaA=="
|
|
30
30
|
},
|
|
31
31
|
"@ours.network/mcp": {
|
|
32
32
|
"version": "1.2.0-nightly.10",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"@ours.network/fleet": {
|
|
81
81
|
"type": "npm",
|
|
82
|
-
"version": "1.2.0-nightly.
|
|
82
|
+
"version": "1.2.0-nightly.33"
|
|
83
83
|
},
|
|
84
84
|
"@ours.network/mcp": {
|
|
85
85
|
"type": "npm",
|
package/install.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
import { realEffects } from './lib/effects.mjs';
|
|
6
|
+
import { installerFailure } from './lib/diagnostics.mjs';
|
|
6
7
|
import { runWorkspaceSetup } from './lib/workspace-setup.mjs';
|
|
7
8
|
import { runSetup } from './lib/setup.mjs';
|
|
8
9
|
import { closeSync, makeWriter, openTty } from './lib/ui.mjs';
|
|
@@ -22,7 +23,7 @@ try {
|
|
|
22
23
|
}));
|
|
23
24
|
process.exitCode = code;
|
|
24
25
|
} catch (error) {
|
|
25
|
-
process.stderr.write(`ours-install: ${
|
|
26
|
+
process.stderr.write(`ours-install: ${installerFailure(error)}\n`);
|
|
26
27
|
process.exitCode = 1;
|
|
27
28
|
} finally {
|
|
28
29
|
if (ttyFd != null) {
|
package/lib/client-cli.mjs
CHANGED
|
@@ -88,7 +88,7 @@ export async function publishClientCli(effects, packagePath, { policy = {}, isol
|
|
|
88
88
|
if (sdk.name !== '@ours.network/sdk' || sdk.version !== policy.release.packages['@ours.network/sdk'].version) throw new Error('Private CLI SDK differs from selection');
|
|
89
89
|
mkdirSync(dirname(entry), { recursive: true });
|
|
90
90
|
const binDir = lstatSync(dirname(entry));
|
|
91
|
-
if (!binDir.isDirectory() || binDir.uid !== process.getuid() || (binDir.mode & 0o022)) throw new Error(
|
|
91
|
+
if (!binDir.isDirectory() || binDir.uid !== process.getuid() || (binDir.mode & 0o022)) throw new Error(`CLI bin directory ${dirname(entry)} must be owned by you and not writable by group or others; run chmod go-w on it, or point npm's global prefix at a directory you own`);
|
|
92
92
|
const current = stat(entry);
|
|
93
93
|
if (before ? !current || current.dev !== before.dev || current.ino !== before.ino : current) throw new Error('CLI entry changed during publication');
|
|
94
94
|
const staged = entry + '.' + randomUUID();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { redactDiagnostic, commandFailure, installerFailure } from '../assets/scripts/runtime/diagnostics.mjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runContainer } from './container-engine.mjs';
|
|
1
|
+
import { engineName, runContainer } from './container-engine.mjs';
|
|
2
2
|
/** Qualify cached images before reuse; repair only the known root-owned 0600 policy. */
|
|
3
3
|
import { existsSync, readdirSync, lstatSync, readFileSync, writeFileSync, mkdtempSync, rmSync, realpathSync, openSync, closeSync, fstatSync, fchmodSync, constants } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -112,16 +112,24 @@ export async function qualifyDockerRuntime(record, effects) {
|
|
|
112
112
|
}
|
|
113
113
|
};
|
|
114
114
|
const policy = readFileSync(record.sourcesPath), expectedHash = digest(policy);
|
|
115
|
-
const check = (report, mode) => {
|
|
116
|
-
|
|
115
|
+
const check = (report, mode, stage) => {
|
|
116
|
+
const expected = { mode, uid: 0, gid: 0, regular: true, links: 1, policyHash: expectedHash };
|
|
117
|
+
const mismatched = Object.keys(expected).filter(key => report?.[key] !== expected[key]);
|
|
118
|
+
if (mismatched.length) {
|
|
119
|
+
// Only fixed field names, a bounded numeric mode and a comparison boolean
|
|
120
|
+
// enter diagnostics. Never disclose policy content, hashes or probe output.
|
|
121
|
+
const observedMode = Number.isInteger(report?.mode) && report.mode >= 0 && report.mode <= 0o7777
|
|
122
|
+
? report.mode.toString(8).padStart(4, '0') : 'invalid';
|
|
123
|
+
fail(`image policy verification failed (stage=${stage}; engine=${engineName(record)}; mismatched=${mismatched.join(',')}; mode=${observedMode}; expectedMode=${mode.toString(8).padStart(4, '0')}; policyMatch=${report?.policyHash === expectedHash})`);
|
|
124
|
+
}
|
|
117
125
|
};
|
|
118
126
|
const original = await inspect(tag);
|
|
119
127
|
const ordinary = await probe(original.Id, `${uid}:${gid}`);
|
|
120
|
-
if (ordinary.code === 0) { check(ordinary.report, 0o644); return; }
|
|
128
|
+
if (ordinary.code === 0) { check(ordinary.report, 0o644, 'ordinary'); return; }
|
|
121
129
|
if (ordinary.code !== 74) fail('cached image is unusable; automatic repair only supports the known source-policy permission defect');
|
|
122
130
|
const privileged = await probe(original.Id, '0:0');
|
|
123
131
|
if (privileged.code !== 0) fail('original release graph could not be verified');
|
|
124
|
-
check(privileged.report, 0o600);
|
|
132
|
+
check(privileged.report, 0o600, 'privileged');
|
|
125
133
|
effects.out?.('Repairing cached Docker image permissions; keeping the selected packages and stored data.');
|
|
126
134
|
const directory = mkdtempSync(join(record.root, '.image-permissions-'));
|
|
127
135
|
const name = `ours-permission-repair-${randomUUID()}`, baseTag = `${name}:base`, candidateTag = `${name}:candidate`;
|
|
@@ -138,7 +146,7 @@ export async function qualifyDockerRuntime(record, effects) {
|
|
|
138
146
|
|| candidate.RootFS.Layers.length !== original.RootFS.Layers.length + 1) fail('repair changed image execution settings or ancestry');
|
|
139
147
|
const verified = await probe(candidate.Id, `${uid}:${gid}`);
|
|
140
148
|
if (verified.code !== 0) fail('repaired image did not pass runtime verification');
|
|
141
|
-
check(verified.report, 0o644);
|
|
149
|
+
check(verified.report, 0o644, 'repaired');
|
|
142
150
|
if (!isDeepStrictEqual(verified.report.records, privileged.report.records)) fail('repair changed build provenance');
|
|
143
151
|
if ((await inspect(tag)).Id !== original.Id) fail('selected image changed during repair; retry setup');
|
|
144
152
|
await runContainer(effects, record, ['image', 'tag', candidate.Id, tag]);
|
package/lib/effects.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { engineName, bindPodman, runContainer, nativeBuildCommands } from './con
|
|
|
15
15
|
// unit file or the service manager directly.
|
|
16
16
|
|
|
17
17
|
import { publishClientCli } from './client-cli.mjs';
|
|
18
|
+
import { commandFailure } from './diagnostics.mjs';
|
|
18
19
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
19
20
|
import { chmodSync, closeSync, constants, cpSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
20
21
|
import { homedir, userInfo, platform as osPlatform, release as osRelease, arch as osArch } from 'node:os';
|
|
@@ -422,13 +423,12 @@ export function realEffects({ write, ttyFd, env = process.env, home = homedir(),
|
|
|
422
423
|
env: childEnv,
|
|
423
424
|
});
|
|
424
425
|
if (r.error) {
|
|
425
|
-
const error =
|
|
426
|
+
const error = commandFailure(executable, args, r, { sensitive, cwd, env: childEnv });
|
|
426
427
|
error.code = r.error.code;
|
|
427
428
|
throw error;
|
|
428
429
|
}
|
|
429
430
|
if (r.status !== 0 && !allowCodes.includes(r.status)) {
|
|
430
|
-
|
|
431
|
-
throw new Error(`${executable} ${args.join(' ')} exited ${r.status}${detail ? `: ${detail}` : ''}`);
|
|
431
|
+
throw commandFailure(executable, args, r, { sensitive, cwd, env: childEnv });
|
|
432
432
|
}
|
|
433
433
|
return { ok: true, code: r.status, stdout: r.stdout ?? '' };
|
|
434
434
|
},
|
|
@@ -442,7 +442,8 @@ export function realEffects({ write, ttyFd, env = process.env, home = homedir(),
|
|
|
442
442
|
delete childEnv.OURS_INSTALLER_LOCK_FD;
|
|
443
443
|
if (installationLockFd !== null) childEnv.OURS_INSTALLER_LOCK_FD = '3';
|
|
444
444
|
const r = spawnSync(cmd, args, { stdio: ['inherit', 'inherit', 'inherit', ...(installationLockFd === null ? [] : [installationLockFd])], env: childEnv });
|
|
445
|
-
|
|
445
|
+
if (r.error || r.status !== 0) throw commandFailure(cmd, args, r, { env: childEnv });
|
|
446
|
+
return { ok: true, code: r.status };
|
|
446
447
|
},
|
|
447
448
|
installedVersion: (pkg) => installedVersionOf(pkg, npmBin),
|
|
448
449
|
packageDependencies: (spec) => packageDependenciesOf(spec, npmBin),
|
|
@@ -1040,7 +1041,15 @@ export function networkEffects(effects) {
|
|
|
1040
1041
|
},
|
|
1041
1042
|
async serverAccess(record, operation, { output, migrate = false } = {}) {
|
|
1042
1043
|
if (record.mode === 'docker') {
|
|
1043
|
-
|
|
1044
|
+
// Retained images contain the old helper that discarded CLI errors.
|
|
1045
|
+
// Execute the shipped installer helper against the SAME selected image
|
|
1046
|
+
// and Compose volume/user/environment contract, without retag/rebuild.
|
|
1047
|
+
const diagnosticsUrl = `data:text/javascript;base64,${readFileSync(join(INSTALLER_ASSETS, 'scripts/runtime/diagnostics.mjs')).toString('base64')}`;
|
|
1048
|
+
const helper = `process.argv.splice(1, 0, '/opt/ours/docker/client-setup.mjs');\n` + readFileSync(join(INSTALLER_ASSETS, 'scripts/runtime/client-setup.mjs'), 'utf8')
|
|
1049
|
+
.replace("'../maintenance/build-context.mjs'", "'/opt/ours/maintenance/build-context.mjs'")
|
|
1050
|
+
.replace("'./diagnostics.mjs'", JSON.stringify(diagnosticsUrl));
|
|
1051
|
+
const accessArgs = (...args) => ['--entrypoint', 'node', 'access', '--input-type=module', '-e', helper, '--', ...args];
|
|
1052
|
+
if (!output) return compose(record, ['run', '--rm', '--no-deps', '-T', ...accessArgs(operation)], { sensitive: true, env: { OURS_ACCESS_MIGRATE: migrate ? '1' : '0' } });
|
|
1044
1053
|
privateDirectory(dirname(output));
|
|
1045
1054
|
if (existsSync(output)) throw new Error('Credential output already exists');
|
|
1046
1055
|
const name = `${record.project}-issue-${randomUUID()}`;
|
|
@@ -1048,7 +1057,7 @@ export function networkEffects(effects) {
|
|
|
1048
1057
|
const staging = join(dirname(output), `.ours-issued-${randomUUID()}`);
|
|
1049
1058
|
ensurePrivateDirectory(staging);
|
|
1050
1059
|
try {
|
|
1051
|
-
await compose(record, ['run', '--name', name, '--no-deps', '-T', 'access
|
|
1060
|
+
await compose(record, ['run', '--name', name, '--no-deps', '-T', ...accessArgs('access-issue', issuedPath)], { sensitive: true });
|
|
1052
1061
|
const file = join(staging, 'credential');
|
|
1053
1062
|
await container(record, ['cp', `${name}:${issuedPath}`, file], { sensitive: true });
|
|
1054
1063
|
assertPrivateRegularFile(file, 'issued credential');
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
validateChannelVersion,
|
|
46
46
|
} from './marketplace.mjs';
|
|
47
47
|
import { USAGE } from './usage.mjs';
|
|
48
|
+
import { installerFailure } from './diagnostics.mjs';
|
|
48
49
|
import { ok, info, warn, heading, banner, box, c, progress } from './ui.mjs';
|
|
49
50
|
|
|
50
51
|
export const EXIT_OK = 0;
|
|
@@ -89,12 +90,7 @@ async function perform(effects, dryRun, label, thunk) {
|
|
|
89
90
|
const reason = (error) => (error instanceof Error ? error.message : String(error));
|
|
90
91
|
|
|
91
92
|
function clientDiagnostic(error) {
|
|
92
|
-
return
|
|
93
|
-
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
94
|
-
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
|
|
95
|
-
.replace(/((?:api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|token|password|secret|authorization|credential)\s*["']?\s*[:=]\s*["']?)[^\s"',;]+/gi, '$1[redacted]')
|
|
96
|
-
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[redacted]@')
|
|
97
|
-
.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 700);
|
|
93
|
+
return installerFailure(error);
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
function clientRetry(effects, imported, integrations) {
|
|
@@ -136,7 +136,8 @@ else { await request('identity/profile', selection.human); process.stdout.write(
|
|
|
136
136
|
`;
|
|
137
137
|
const docker = record.mode === 'docker';
|
|
138
138
|
const selection = { base: `http://127.0.0.1:${docker ? 8420 : record.messengerPort}`,
|
|
139
|
-
origin
|
|
139
|
+
// Behind the gateway Messenger accepts mutations only from the gateway origin.
|
|
140
|
+
origin: record.gateway ? gatewayAddress(record).origin : `http://127.0.0.1:${record.messengerPort}`,
|
|
140
141
|
credentialPath: docker ? '/credentials/messenger/daemon-token' : installationPaths(record).credentials.messenger,
|
|
141
142
|
human: { name: human.name, surname: human.surname } };
|
|
142
143
|
const args = ['--input-type=module', '-e', script, JSON.stringify(selection)];
|
|
@@ -169,7 +170,7 @@ else { await request('identity/profile', selection.human); process.stdout.write(
|
|
|
169
170
|
if (!current) {
|
|
170
171
|
effects.out?.('Issuing a separate local client credential with the retained server authority.');
|
|
171
172
|
try { await effects.serverAccess(record, 'access-issue', { output: credential }); }
|
|
172
|
-
catch { throw new Error('Client credential issuance failed; existing profiles were retained'); }
|
|
173
|
+
catch (cause) { throw new Error('Client credential issuance failed; existing profiles were retained', { cause }); }
|
|
173
174
|
}
|
|
174
175
|
const retainedCredential = current?.credentialPath ?? credential;
|
|
175
176
|
const stat = privatePath(retainedCredential);
|
package/lib/setup-options.mjs
CHANGED
|
@@ -219,9 +219,17 @@ export async function collectSetupOptions(effects, presets = {}) {
|
|
|
219
219
|
}
|
|
220
220
|
if (options.scope !== 'client') {
|
|
221
221
|
const recommendation = recommendedMode({ ...effects.platform, arch: effects.platform?.arch ?? process.arch });
|
|
222
|
-
|
|
222
|
+
// Native runs the server only: local clients need the container gateway. A retained
|
|
223
|
+
// native installation keeps its mode, so a full setup of it cannot finish.
|
|
224
|
+
// (An unfinished migration is resumed in its recorded mode and is validated with the plan.)
|
|
225
|
+
if (options.scope === 'all' && existing?.mode === 'packages' && !options.migrateFrom)
|
|
226
|
+
throw new Error('This installation runs natively, which supports the server only. Choose the server-only setup to maintain it (ours-install server); local clients need an installation that runs in Docker or Podman. Nothing was changed.');
|
|
227
|
+
const nativeOffered = options.scope === 'server' || existing?.mode === 'packages';
|
|
228
|
+
effects.out(nativeOffered
|
|
229
|
+
? `${recommendation.reason} Native runs directly on this computer; Docker runs in containers and needs Docker installed and running. An existing installation must keep its current mode.`
|
|
230
|
+
: 'The server runs in containers. Docker and Podman both work; the one you choose must be installed and running. An existing installation must keep its current mode.');
|
|
223
231
|
const runtime = options.mode ?? await effects.select('How should the server run?', [
|
|
224
|
-
{ value: 'packages', label: 'Native packages' }, { value: 'docker', label: 'Docker' }, { value: 'podman', label: 'Podman' },
|
|
232
|
+
...(nativeOffered ? [{ value: 'packages', label: 'Native packages' }] : []), { value: 'docker', label: 'Docker' }, { value: 'podman', label: 'Podman' },
|
|
225
233
|
], existing?.mode === 'docker' && existing?.containerEngine === 'podman' ? 'podman' : existing?.mode ?? recommendation.mode);
|
|
226
234
|
options.mode = runtime === 'podman' ? 'docker' : runtime;
|
|
227
235
|
if (runtime === 'podman') options.containerEngine = 'podman';
|
package/lib/setup.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { isCancel } from './prompt.mjs';
|
|
|
11
11
|
import { USAGE } from './usage.mjs';
|
|
12
12
|
import { validateIdentityName } from './server-onboarding.mjs';
|
|
13
13
|
import { validateFleetSettings } from './fleet-settings.mjs';
|
|
14
|
+
import { installerFailure } from './diagnostics.mjs';
|
|
14
15
|
|
|
15
16
|
const maintenance = new Set(['status', 'start', 'stop', 'restart', 'rebuild', 'gateway-enable', 'access-issue', 'access-replace', 'backup', 'restore', 'reset']);
|
|
16
17
|
|
|
@@ -165,7 +166,7 @@ export async function runSetup(argv, effects) {
|
|
|
165
166
|
return await executeSetupPlan(plan, effects);
|
|
166
167
|
} catch (error) {
|
|
167
168
|
if (isCancel(error)) { effects.out(warn('Installation cancelled.')); return 130; }
|
|
168
|
-
effects.out(warn(`ours-install: ${error.
|
|
169
|
+
effects.out(warn(`ours-install: ${installerFailure(error, effects.env)}`));
|
|
169
170
|
return 2;
|
|
170
171
|
}
|
|
171
172
|
}
|
package/lib/usage.mjs
CHANGED
|
@@ -4,7 +4,8 @@ export const USAGE = `ours-install — interactive setup with optional CLI prese
|
|
|
4
4
|
ours-install
|
|
5
5
|
Opens the console form. Choose all (server + clients), server, or client;
|
|
6
6
|
runtime, installation directory, identity, integrations and Fleet settings.
|
|
7
|
-
|
|
7
|
+
Full setup runs the server in Docker or Podman, which provide the gateway local clients need.
|
|
8
|
+
Native mode is offered for server-only setup. Windows uses WSL.
|
|
8
9
|
|
|
9
10
|
Install now, configure Fleet agents in the App after linking:
|
|
10
11
|
ours-install --username alex --name Alex --surname Taylor --disable-fleet-agents-setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "1.2.1-nightly.
|
|
3
|
+
"version": "1.2.1-nightly.26",
|
|
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",
|