@ours.network/fleet 2.1.1 → 2.1.2
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 +41 -2
- package/dist/build-info.json +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1 -0
- package/dist/doctor.js +17 -4
- package/dist/runtime-security.d.ts +1 -0
- package/dist/runtime-security.js +6 -0
- package/dist/security-versions.d.ts +20 -0
- package/dist/security-versions.js +70 -0
- package/dist/setup-tunnel-preflight.js +10 -3
- package/dist/web/runtime.js +1 -1
- package/dist/web/workspace-tunnel.d.ts +2 -1
- package/dist/web/workspace-tunnel.js +29 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,7 +100,8 @@ The maintained Codex and Claude ACP adapters remain bundled optional dependencie
|
|
|
100
100
|
and are resolved internally; users do not install adapter commands or add them to
|
|
101
101
|
`PATH`. Explicit `session_options.codex_app_server.command` and
|
|
102
102
|
`session_options.acp.command` overrides remain available. ours-fleet and its
|
|
103
|
-
maintained adapters require Node 22 or
|
|
103
|
+
maintained adapters require a patched Node.js 22.23.4+, 24.22.1+ or 26.12.1+
|
|
104
|
+
release (see [Security version requirements](#security-version-requirements)).
|
|
104
105
|
|
|
105
106
|
Each OS user manages their own fleet — to host roles under a sandboxed account,
|
|
106
107
|
become that account and repeat.
|
|
@@ -1737,7 +1738,7 @@ These package checks do not replace native Fleet/harness or platform acceptance.
|
|
|
1737
1738
|
|
|
1738
1739
|
### Build with selected SDK and CLI archives
|
|
1739
1740
|
|
|
1740
|
-
Run in the build container with Node 22+, npm and tar available:
|
|
1741
|
+
Run in the build container with a patched Node.js (22.23.4+, 24.22.1+ or 26.12.1+), npm and tar available:
|
|
1741
1742
|
|
|
1742
1743
|
```sh
|
|
1743
1744
|
node scripts/build-selected.mjs --sdk /artifacts/ours.network-sdk-3.7.2.tgz --cli /artifacts/ours.network-cli-2.7.2.tgz --out-dir /artifacts/consumer
|
|
@@ -1971,3 +1972,41 @@ existing role/default configuration when applying this change. Meaningful agent
|
|
|
1971
1972
|
commentary, attention requests and final answers continue to be delivered.
|
|
1972
1973
|
The app associates activity with the channel's bound CID, never a display name.
|
|
1973
1974
|
It shows unavailable status when the directory observation disconnects.
|
|
1975
|
+
|
|
1976
|
+
## Security version requirements
|
|
1977
|
+
|
|
1978
|
+
Ours refuses to run on Node.js or cloudflared releases with known security
|
|
1979
|
+
defects that affect it. The checks run before setup changes anything and
|
|
1980
|
+
again before every tunnel start, and the refusal names the minimum version,
|
|
1981
|
+
the version found and where to update.
|
|
1982
|
+
|
|
1983
|
+
**Node.js.** Only reviewed stable release lines are accepted, each with its
|
|
1984
|
+
own patched floor: 22 (22.23.4+), 24 (24.22.1+) and 26 (26.12.1+). Earlier
|
|
1985
|
+
patch levels of these lines, end-of-life lines (20, 21, 23, 25), prerelease
|
|
1986
|
+
or nightly builds and release lines that have not been reviewed yet (such as
|
|
1987
|
+
27) are refused. A new line is admitted by a reviewed change that adds its
|
|
1988
|
+
floor, never by a single global "newer than" comparison. The
|
|
1989
|
+
[October 2026 Node.js security releases](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases)
|
|
1990
|
+
fix CVE-2026-111497: the bundled Brotli decoder can write out of bounds when
|
|
1991
|
+
decompressing attacker-controlled data, which can cause memory corruption,
|
|
1992
|
+
crashes, information disclosure and potentially remote code execution.
|
|
1993
|
+
Update from [nodejs.org](https://nodejs.org/en/download) or your package
|
|
1994
|
+
manager. The `engines` field in `package.json` records the same floors but
|
|
1995
|
+
is advisory; the runtime checks enforce them.
|
|
1996
|
+
|
|
1997
|
+
**cloudflared.** A tunnel requires cloudflared **2026.10.2 or later**, as
|
|
1998
|
+
reported by `cloudflared --version` for the executable found on the PATH of
|
|
1999
|
+
the process that starts the tunnel. Older, prerelease, unrecognized, failing,
|
|
2000
|
+
missing or timed-out version checks are refused.
|
|
2001
|
+
[Cloudflare's eMudhra incident explanation](https://blog.cloudflare.com/emudhra-certificate-compromise/)
|
|
2002
|
+
describes why: earlier releases trust compromised eMudhra intermediate
|
|
2003
|
+
certificates, which allows server impersonation. Update with your package
|
|
2004
|
+
manager or
|
|
2005
|
+
[Cloudflare's update instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/update-cloudflared/),
|
|
2006
|
+
then check `cloudflared --version` again.
|
|
2007
|
+
|
|
2008
|
+
These checks do not install or upgrade prerequisites, disable TLS
|
|
2009
|
+
verification, rotate credentials or restart services. A process that is
|
|
2010
|
+
already running keeps the binaries it started with: update Node.js and
|
|
2011
|
+
cloudflared, then restart the affected services (and rebuild or recreate
|
|
2012
|
+
containers) yourself.
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.1.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "2.1.2",
|
|
3
|
+
"buildId": "5bf10a772912",
|
|
4
|
+
"commit": "dd8adad553ca71b0edae5cc1040eacaaed51e50a",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-28T20:22:31.826Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import './runtime-security.js';
|
|
2
3
|
import { beginWorkspaceReplacement, abandonWorkspaceReplacement, discardRetiredWorkspaceReplacement, clearWorkspaceReplacement, confirmWorkspaceReplacement, finishWorkspaceReplacement, readWorkspaceBinding, readWorkspaceReplacement, requiresWorkspaceReplacement, refreshWorkspaceReplacement } from './workspace-replacement.js';
|
|
3
4
|
import { completeTunnelSetup } from './tunnel-setup-completion.js';
|
|
4
5
|
import { readTunnelSetupFile, readTunnelSetupStdin, readTunnelSetupArgument, redeemTunnelSetup, inspectTunnelSetup, verifyTunnelSetupTarget } from './tunnel-setup-input.js';
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { cloudflaredUnavailableError, cloudflaredVersionError, CLOUDFLARED_VERSION_TIMEOUT_MS, isSecureCloudflaredVersion, isSecureNodeVersion, nodeVersionError, parseCloudflaredVersion } from './security-versions.js';
|
|
1
2
|
import { describeRegistration, refreshServiceRegistrations, registrationDrift } from './supervisor/registration-refresh.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { userInfo } from 'node:os';
|
|
3
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
6
|
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
@@ -8,7 +10,7 @@ import { getAdapter, productionAdapters } from './harness/registry.js';
|
|
|
8
10
|
import { analyzeFleetPermissions, formatNative } from './permissions.js';
|
|
9
11
|
import { managedCliReport } from './managed-cli.js';
|
|
10
12
|
import { resolveBundledAcpAgent } from './harness/acp-agent.js';
|
|
11
|
-
import { agentDir, deriveXdgRuntimeDir } from './paths.js';
|
|
13
|
+
import { agentDir, deriveXdgRuntimeDir, stateRoot } from './paths.js';
|
|
12
14
|
import { resolveIsolation, harnessRuntimeDir } from './isolation/policy.js';
|
|
13
15
|
import { hermesConfiguredProvider } from './harness/hermes-startup.js';
|
|
14
16
|
import { makeBubblewrapBackend } from './isolation/bubblewrap.js';
|
|
@@ -116,11 +118,22 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
116
118
|
detail: `could not inspect: ${error?.message ?? String(error)}` });
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
|
-
const
|
|
121
|
+
const secureNode = isSecureNodeVersion(process.versions.node);
|
|
120
122
|
checks.push({
|
|
121
|
-
name: 'node', ok:
|
|
122
|
-
detail:
|
|
123
|
+
name: 'node', ok: secureNode,
|
|
124
|
+
detail: secureNode ? `v${process.versions.node}` : nodeVersionError(process.versions.node),
|
|
123
125
|
});
|
|
126
|
+
// A configured workspace tunnel starts the cloudflared found on PATH.
|
|
127
|
+
if (existsSync(join(stateRoot(), 'workspace', 'tunnel.json'))) {
|
|
128
|
+
const connector = await exec('cloudflared', ['--version'], { timeout: CLOUDFLARED_VERSION_TIMEOUT_MS });
|
|
129
|
+
const secure = connector.code === 0 && isSecureCloudflaredVersion(connector.stdout);
|
|
130
|
+
checks.push({
|
|
131
|
+
name: 'cloudflared', ok: secure,
|
|
132
|
+
detail: secure ? `version ${parseCloudflaredVersion(connector.stdout).join('.')}`
|
|
133
|
+
: connector.code !== 0 ? cloudflaredUnavailableError(connector.code === 127 ? 'not found' : 'version check failed')
|
|
134
|
+
: cloudflaredVersionError(connector.stdout),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
124
137
|
// The configuration is a checked prerequisite in its own right. A config the
|
|
125
138
|
// `config` command rejects must fail here too, with the same cause — while the
|
|
126
139
|
// host checks below still run, because they are what the operator needs next.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Imported first by the CLI entry point: refuse an unpatched Node.js before any other module runs.
|
|
2
|
+
import { isSecureNodeVersion, nodeVersionError } from './security-versions.js';
|
|
3
|
+
if (!isSecureNodeVersion(process.versions.node)) {
|
|
4
|
+
process.stderr.write(`${nodeVersionError(process.versions.node)}\n`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const NODE_PATCHED_FLOORS: Readonly<Record<number, readonly [number, number, number]>>;
|
|
2
|
+
export declare const NODE_SECURITY_ADVISORY = "https://nodejs.org/en/blog/vulnerability/october-2026-security-releases";
|
|
3
|
+
export declare const NODE_DOWNLOAD_URL = "https://nodejs.org/en/download";
|
|
4
|
+
export declare const NODE_SECURITY_REQUIREMENT: string;
|
|
5
|
+
export declare const CLOUDFLARED_MIN_VERSION: readonly number[];
|
|
6
|
+
export declare const CLOUDFLARED_SECURITY_ADVISORY = "https://blog.cloudflare.com/emudhra-certificate-compromise/";
|
|
7
|
+
export declare const CLOUDFLARED_UPDATE_URL = "https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/update-cloudflared/";
|
|
8
|
+
export declare const CLOUDFLARED_VERSION_TIMEOUT_MS = 10000;
|
|
9
|
+
/** Returns [major, minor, patch] for a stable Node.js version string, otherwise null. */
|
|
10
|
+
export declare function parseNodeVersion(version: string): number[] | null;
|
|
11
|
+
export declare function isSecureNodeVersion(version: string): boolean;
|
|
12
|
+
export declare function nodeVersionError(version: string): string;
|
|
13
|
+
export declare function assertSecureNodeVersion(version?: string): void;
|
|
14
|
+
/** Returns [year, month, patch] for exact stable `cloudflared --version` output, otherwise null. */
|
|
15
|
+
export declare function parseCloudflaredVersion(output: string): number[] | null;
|
|
16
|
+
/** `reason` must be a fixed description such as "not found" or "timed out", never command output. */
|
|
17
|
+
export declare function cloudflaredUnavailableError(reason: string): string;
|
|
18
|
+
export declare function cloudflaredVersionError(output: string): string;
|
|
19
|
+
export declare function isSecureCloudflaredVersion(output: string): boolean;
|
|
20
|
+
export declare function assertSecureCloudflaredVersion(output: string): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Security version floors for the runtimes Ours executes. Keep this file
|
|
2
|
+
// identical in ours-network, ours-fleet and ours-app.
|
|
3
|
+
//
|
|
4
|
+
// Node.js policy: only reviewed stable release lines are admitted, each with
|
|
5
|
+
// its own patched floor. A new line (for example a future 27 or 28 release) is
|
|
6
|
+
// added by a reviewed change once its security releases have been checked;
|
|
7
|
+
// until then it is refused, as are prerelease builds, end-of-life lines and
|
|
8
|
+
// earlier patch levels of a listed line. A single global ">=" comparison would
|
|
9
|
+
// admit vulnerable intermediate lines, so every line keeps an explicit floor.
|
|
10
|
+
export const NODE_PATCHED_FLOORS = Object.freeze({ 22: [22, 23, 4], 24: [24, 22, 1], 26: [26, 12, 1] });
|
|
11
|
+
export const NODE_SECURITY_ADVISORY = 'https://nodejs.org/en/blog/vulnerability/october-2026-security-releases';
|
|
12
|
+
export const NODE_DOWNLOAD_URL = 'https://nodejs.org/en/download';
|
|
13
|
+
export const NODE_SECURITY_REQUIREMENT = Object.values(NODE_PATCHED_FLOORS).map(floor => `${floor.join('.')}+`).join(', ');
|
|
14
|
+
// cloudflared releases before 2026.10.2 trust compromised eMudhra intermediates.
|
|
15
|
+
export const CLOUDFLARED_MIN_VERSION = Object.freeze([2026, 10, 2]);
|
|
16
|
+
export const CLOUDFLARED_SECURITY_ADVISORY = 'https://blog.cloudflare.com/emudhra-certificate-compromise/';
|
|
17
|
+
export const CLOUDFLARED_UPDATE_URL = 'https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/update-cloudflared/';
|
|
18
|
+
export const CLOUDFLARED_VERSION_TIMEOUT_MS = 10_000;
|
|
19
|
+
const atLeast = (version, floor) => {
|
|
20
|
+
for (let i = 0; i < floor.length; i++)
|
|
21
|
+
if (version[i] !== floor[i])
|
|
22
|
+
return version[i] > floor[i];
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
/** Returns [major, minor, patch] for a stable Node.js version string, otherwise null. */
|
|
26
|
+
export function parseNodeVersion(version) {
|
|
27
|
+
const match = /^v?(\d{1,9})\.(\d{1,9})\.(\d{1,9})$/.exec(String(version));
|
|
28
|
+
return match ? match.slice(1).map(Number) : null;
|
|
29
|
+
}
|
|
30
|
+
export function isSecureNodeVersion(version) {
|
|
31
|
+
const parsed = parseNodeVersion(version);
|
|
32
|
+
const floor = parsed && NODE_PATCHED_FLOORS[parsed[0]];
|
|
33
|
+
return Boolean(floor && atLeast(parsed, floor));
|
|
34
|
+
}
|
|
35
|
+
export function nodeVersionError(version) {
|
|
36
|
+
const shown = /^v?[0-9A-Za-z.+-]{1,64}$/.test(String(version)) ? String(version).replace(/^v/, '') : 'unrecognized';
|
|
37
|
+
return `Node.js ${shown} is not permitted. Ours requires a patched supported Node.js release: ${NODE_SECURITY_REQUIREMENT} `
|
|
38
|
+
+ 'within those release lines; prerelease, end-of-life and unreviewed release lines are refused. '
|
|
39
|
+
+ `Earlier patch levels are affected by CVE-2026-111497 (Brotli decompression out-of-bounds write) and the other October 2026 fixes: ${NODE_SECURITY_ADVISORY} `
|
|
40
|
+
+ `Update Node.js from ${NODE_DOWNLOAD_URL} or your package manager, then retry.`;
|
|
41
|
+
}
|
|
42
|
+
export function assertSecureNodeVersion(version = process.versions.node) {
|
|
43
|
+
if (!isSecureNodeVersion(version))
|
|
44
|
+
throw new Error(nodeVersionError(version));
|
|
45
|
+
}
|
|
46
|
+
/** Returns [year, month, patch] for exact stable `cloudflared --version` output, otherwise null. */
|
|
47
|
+
export function parseCloudflaredVersion(output) {
|
|
48
|
+
const match = /^cloudflared version (\d{4})\.(\d{1,2})\.(\d{1,6})(?: \(built [^\r\n()]*\))?\r?\n?$/.exec(String(output));
|
|
49
|
+
const parsed = match?.slice(1).map(Number);
|
|
50
|
+
return parsed && parsed[1] >= 1 && parsed[1] <= 12 ? parsed : null;
|
|
51
|
+
}
|
|
52
|
+
const cloudflaredGuidance = () => `cloudflared releases before ${CLOUDFLARED_MIN_VERSION.join('.')} trust compromised eMudhra intermediate certificates, which allows server impersonation: ${CLOUDFLARED_SECURITY_ADVISORY} `
|
|
53
|
+
+ `Update the cloudflared executable on this host's PATH (${CLOUDFLARED_UPDATE_URL}, or your package manager), check it with "cloudflared --version", then retry.`;
|
|
54
|
+
/** `reason` must be a fixed description such as "not found" or "timed out", never command output. */
|
|
55
|
+
export function cloudflaredUnavailableError(reason) {
|
|
56
|
+
return `cloudflared could not be verified (${reason}). A tunnel requires cloudflared ${CLOUDFLARED_MIN_VERSION.join('.')} or later. ${cloudflaredGuidance()}`;
|
|
57
|
+
}
|
|
58
|
+
export function cloudflaredVersionError(output) {
|
|
59
|
+
const parsed = parseCloudflaredVersion(output);
|
|
60
|
+
const found = parsed ? `version ${parsed.join('.')}` : 'with an unrecognized version';
|
|
61
|
+
return `cloudflared ${found} is not permitted; a tunnel requires cloudflared ${CLOUDFLARED_MIN_VERSION.join('.')} or later. ${cloudflaredGuidance()}`;
|
|
62
|
+
}
|
|
63
|
+
export function isSecureCloudflaredVersion(output) {
|
|
64
|
+
const parsed = parseCloudflaredVersion(output);
|
|
65
|
+
return Boolean(parsed && atLeast(parsed, CLOUDFLARED_MIN_VERSION));
|
|
66
|
+
}
|
|
67
|
+
export function assertSecureCloudflaredVersion(output) {
|
|
68
|
+
if (!isSecureCloudflaredVersion(output))
|
|
69
|
+
throw new Error(cloudflaredVersionError(output));
|
|
70
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cloudflaredUnavailableError, cloudflaredVersionError, CLOUDFLARED_VERSION_TIMEOUT_MS, isSecureCloudflaredVersion, isSecureNodeVersion, nodeVersionError } from './security-versions.js';
|
|
1
2
|
import { existsSync, lstatSync, readFileSync, rmSync } from 'node:fs';
|
|
2
3
|
import { userInfo } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -10,9 +11,8 @@ const UNCHANGED = 'Nothing was changed and this host was not bound.';
|
|
|
10
11
|
* proof is sent: a failure afterwards cannot be retried with a fresh command.
|
|
11
12
|
*/
|
|
12
13
|
export async function setupTunnelPreflight({ exec = realExec, platform = process.platform, node = process.versions.node } = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw Error(`Tunnel setup requires Node.js 22.13 or newer; this is ${node}. Update Node.js, then run a fresh tunnel setup command. ${UNCHANGED}`);
|
|
14
|
+
if (!isSecureNodeVersion(node))
|
|
15
|
+
throw Error(`${nodeVersionError(node)} ${UNCHANGED}`);
|
|
16
16
|
if (platform === 'linux') {
|
|
17
17
|
const manager = await exec('systemctl', ['--user', 'show-environment'], { timeout: 10_000 });
|
|
18
18
|
if (manager.code !== 0)
|
|
@@ -20,6 +20,13 @@ export async function setupTunnelPreflight({ exec = realExec, platform = process
|
|
|
20
20
|
+ `Log in directly as ${userInfo().username} (not through su or sudo), or run: sudo loginctl enable-linger ${userInfo().username} && export XDG_RUNTIME_DIR=/run/user/${process.getuid?.()} `
|
|
21
21
|
+ `Then run a fresh tunnel setup command. ${UNCHANGED}`);
|
|
22
22
|
}
|
|
23
|
+
// Refuse an unsafe connector before anything is redeemed; the service checks its own
|
|
24
|
+
// PATH-selected cloudflared again before every tunnel start.
|
|
25
|
+
const version = await exec('cloudflared', ['--version'], { timeout: CLOUDFLARED_VERSION_TIMEOUT_MS });
|
|
26
|
+
if (version.code !== 0)
|
|
27
|
+
throw Error(`${cloudflaredUnavailableError(version.code === 127 ? 'not found' : 'version check failed')} ${UNCHANGED}`);
|
|
28
|
+
if (!isSecureCloudflaredVersion(version.stdout))
|
|
29
|
+
throw Error(`${cloudflaredVersionError(version.stdout)} ${UNCHANGED}`);
|
|
23
30
|
const help = await exec('cloudflared', ['tunnel', 'run', '--help'], { timeout: 10_000 });
|
|
24
31
|
if (help.code === 127)
|
|
25
32
|
throw Error(`cloudflared is not installed or not on PATH. Install it, then run a fresh tunnel setup command. ${UNCHANGED}`);
|
package/dist/web/runtime.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Exec } from '../exec.js';
|
|
2
|
+
export declare function startWorkspaceTunnel(origin: string, exec?: Exec): Promise<() => Promise<void>>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { realExec } from '../exec.js';
|
|
5
|
+
import { assertSecureCloudflaredVersion, cloudflaredUnavailableError, CLOUDFLARED_VERSION_TIMEOUT_MS } from '../security-versions.js';
|
|
4
6
|
import { stateRoot } from '../paths.js';
|
|
5
|
-
export function startWorkspaceTunnel(origin) {
|
|
7
|
+
export async function startWorkspaceTunnel(origin, exec = realExec) {
|
|
6
8
|
const dir = join(stateRoot(), 'workspace'), file = join(dir, 'tunnel.json');
|
|
7
9
|
if (!existsSync(file))
|
|
8
10
|
return async () => { };
|
|
@@ -15,7 +17,31 @@ export function startWorkspaceTunnel(origin) {
|
|
|
15
17
|
const stat = lstatSync(tokenFile);
|
|
16
18
|
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())
|
|
17
19
|
throw Error('Connector credential must be an owned private file');
|
|
18
|
-
|
|
20
|
+
// The connector is the executable named cloudflared on this process's PATH, the
|
|
21
|
+
// same lookup spawn uses. Its version is verified before the first start and
|
|
22
|
+
// before every reconnect, so a downgrade is refused and an upgrade is picked up.
|
|
23
|
+
const verify = async () => {
|
|
24
|
+
const result = await exec('cloudflared', ['--version'], { timeout: CLOUDFLARED_VERSION_TIMEOUT_MS });
|
|
25
|
+
if (result.code !== 0)
|
|
26
|
+
throw Error(cloudflaredUnavailableError(result.code === 127 ? 'not found' : 'version check failed'));
|
|
27
|
+
assertSecureCloudflaredVersion(result.stdout);
|
|
28
|
+
};
|
|
29
|
+
await verify();
|
|
30
|
+
let stopped = false, child, timer, backoff = 1000, reported = '';
|
|
31
|
+
const schedule = () => { if (stopped)
|
|
32
|
+
return; timer = setTimeout(reconnect, backoff); backoff = Math.min(30000, backoff * 2); };
|
|
33
|
+
// A refused or unverifiable connector is never started; retry after the backoff.
|
|
34
|
+
const reconnect = () => {
|
|
35
|
+
timer = undefined;
|
|
36
|
+
void verify().then(() => { reported = ''; launch(); }, (error) => {
|
|
37
|
+
if (stopped)
|
|
38
|
+
return;
|
|
39
|
+
if (error.message !== reported)
|
|
40
|
+
console.error(`Workspace tunnel not restarted: ${error.message}`);
|
|
41
|
+
reported = error.message;
|
|
42
|
+
schedule();
|
|
43
|
+
});
|
|
44
|
+
};
|
|
19
45
|
const launch = () => {
|
|
20
46
|
if (stopped)
|
|
21
47
|
return;
|
|
@@ -23,7 +49,7 @@ export function startWorkspaceTunnel(origin) {
|
|
|
23
49
|
let scheduled = false;
|
|
24
50
|
// Keep an error listener for the child's whole lifetime, including shutdown.
|
|
25
51
|
const retry = () => { if (scheduled || stopped)
|
|
26
|
-
return; scheduled = true;
|
|
52
|
+
return; scheduled = true; schedule(); };
|
|
27
53
|
child.on('error', retry);
|
|
28
54
|
child.once('exit', retry);
|
|
29
55
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"docs/task-progress-review.md"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=22.
|
|
33
|
+
"node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1 <27"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|