@ours.network/fleet 1.2.0-nightly.4 → 1.2.0-nightly.5
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 +16 -0
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/client-profile.d.ts +1 -0
- package/dist/client-profile.js +18 -3
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +28 -9
- package/dist/rooms-tasks/cowork-adapter.js +9 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1694,3 +1694,19 @@ provisioning or automatic reverse-proxy configuration. Existing local HTTP and
|
|
|
1694
1694
|
SSH-tunnel profiles continue to work.
|
|
1695
1695
|
|
|
1696
1696
|
See [supervisor-owned ours and migration](docs/supervisor-ours.md) for the runtime contract and review build instructions.
|
|
1697
|
+
|
|
1698
|
+
### One-server gateway profile
|
|
1699
|
+
|
|
1700
|
+
A private client profile may include `serverUrl`, for example
|
|
1701
|
+
`https://ours.example/base`, together with `endpoint` set to
|
|
1702
|
+
`https://ours.example/base/daemon`, the existing `expectedInstanceId`, and the
|
|
1703
|
+
absolute private `credentialPath`. Fleet preserves that daemon prefix for doctor
|
|
1704
|
+
and monitoring and derives `/cowork/management/rpc` from `serverUrl` for room
|
|
1705
|
+
management. Requests use the issued server credential, reject redirects, have
|
|
1706
|
+
bounded bodies/deadlines, and never replay mutations. This capability is declared
|
|
1707
|
+
as `cowork.http-management-v1` in the built artifact.
|
|
1708
|
+
|
|
1709
|
+
Explicit Cowork socket, config or state-directory overrides continue to select
|
|
1710
|
+
local Unix management. Profiles without `serverUrl` retain legacy behavior. The
|
|
1711
|
+
HTTP API grants operator room authority; use the installer's supported private
|
|
1712
|
+
or authenticated external gateway entry, and keep its backend ports private.
|
package/dist/build-info.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.5",
|
|
3
|
+
"buildId": "4d7255bd137c",
|
|
4
|
+
"commit": "273d3bec2505bd1a4244c6b00b836238faaac99e",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-22T18:42:44.399Z",
|
|
7
7
|
"capabilities": [
|
|
8
|
+
"cowork.http-management-v1",
|
|
8
9
|
"monitor.interrupt.after_tool"
|
|
9
10
|
]
|
|
10
11
|
}
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
/** `monitor.interrupt: after_tool` — cancel at the next tool boundary (#67). */
|
|
16
16
|
export declare const CAP_MONITOR_INTERRUPT_AFTER_TOOL = "monitor.interrupt.after_tool";
|
|
17
|
-
export declare const
|
|
17
|
+
export declare const CAP_HTTP_COWORK = "cowork.http-management-v1";
|
|
18
|
+
export declare const CAPABILITIES: readonly ["cowork.http-management-v1", "monitor.interrupt.after_tool"];
|
|
18
19
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
19
20
|
/** Does this build declare `token`? */
|
|
20
21
|
export declare const hasCapability: (token: string) => boolean;
|
package/dist/capabilities.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
/** `monitor.interrupt: after_tool` — cancel at the next tool boundary (#67). */
|
|
16
16
|
export const CAP_MONITOR_INTERRUPT_AFTER_TOOL = 'monitor.interrupt.after_tool';
|
|
17
|
+
export const CAP_HTTP_COWORK = 'cowork.http-management-v1';
|
|
17
18
|
export const CAPABILITIES = [
|
|
19
|
+
CAP_HTTP_COWORK,
|
|
18
20
|
CAP_MONITOR_INTERRUPT_AFTER_TOOL,
|
|
19
21
|
];
|
|
20
22
|
/** Does this build declare `token`? */
|
package/dist/client-profile.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface ExplicitClientProfile {
|
|
|
3
3
|
readonly expectedInstanceId: string;
|
|
4
4
|
readonly credentialPath: string;
|
|
5
5
|
readonly configPath: string;
|
|
6
|
+
readonly serverUrl?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare class ClientProfileError extends Error {
|
|
8
9
|
constructor(message: string);
|
package/dist/client-profile.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeDaemonEndpoint } from '@ours.network/sdk/client';
|
|
1
2
|
import { lstatSync, readFileSync, statSync } from 'node:fs';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { isAbsolute, join } from 'node:path';
|
|
@@ -97,17 +98,31 @@ export function readClientProfile(env) {
|
|
|
97
98
|
catch {
|
|
98
99
|
throw invalid(configPath, 'has an invalid endpoint');
|
|
99
100
|
}
|
|
100
|
-
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password
|
|
101
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password
|
|
101
102
|
|| url.search || url.hash)
|
|
102
|
-
throw invalid(configPath, 'endpoint must be an HTTP or HTTPS
|
|
103
|
+
throw invalid(configPath, 'endpoint must be an HTTP or HTTPS base URL');
|
|
103
104
|
const expectedInstanceId = row.expectedInstanceId.trim();
|
|
104
105
|
if (!LOWERCASE_UUID.test(expectedInstanceId))
|
|
105
106
|
throw invalid(configPath, 'expectedInstanceId must be a lowercase UUID');
|
|
106
107
|
const credentialPath = row.credentialPath.trim();
|
|
107
108
|
if (!isAbsolute(credentialPath))
|
|
108
109
|
throw invalid(configPath, 'credentialPath must be absolute');
|
|
110
|
+
let serverUrl;
|
|
111
|
+
if (Object.hasOwn(row, 'serverUrl')) {
|
|
112
|
+
if (typeof row.serverUrl !== 'string' || /[\s\\?#]/.test(row.serverUrl))
|
|
113
|
+
throw invalid(configPath, 'serverUrl must be a safe HTTP or HTTPS base URL');
|
|
114
|
+
try {
|
|
115
|
+
serverUrl = normalizeDaemonEndpoint(row.serverUrl);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw invalid(configPath, 'serverUrl must be a safe HTTP or HTTPS base URL');
|
|
119
|
+
}
|
|
120
|
+
if (normalizeDaemonEndpoint(endpoint) !== serverUrl + '/daemon')
|
|
121
|
+
throw invalid(configPath, 'endpoint must select serverUrl/daemon');
|
|
122
|
+
}
|
|
109
123
|
return Object.freeze({
|
|
110
|
-
|
|
124
|
+
...(serverUrl === undefined ? {} : { serverUrl }),
|
|
125
|
+
endpoint: normalizeDaemonEndpoint(endpoint), expectedInstanceId, credentialPath, configPath,
|
|
111
126
|
});
|
|
112
127
|
}
|
|
113
128
|
export function clientProfileKey(profile) {
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Exec } from './exec.js';
|
|
|
3
3
|
import type { YamlMode } from './config-yaml.js';
|
|
4
4
|
import { type FetchLike } from './monitor.js';
|
|
5
5
|
import type { PrereqReport } from './harness/types.js';
|
|
6
|
-
type DoctorDaemonClient = Pick<OursClient, 'version'>;
|
|
6
|
+
type DoctorDaemonClient = Pick<OursClient, 'version' | 'close'>;
|
|
7
7
|
type AttachDoctorDaemon = (options: AttachOursClientOptions) => Promise<DoctorDaemonClient>;
|
|
8
8
|
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
9
9
|
export declare function doctor(opts?: {
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { userInfo, homedir } from 'node:os';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
4
5
|
import { realExec } from './exec.js';
|
|
@@ -272,21 +273,39 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
272
273
|
}
|
|
273
274
|
catch { /* state dir may not exist yet */ }
|
|
274
275
|
}
|
|
276
|
+
let managedSelection = false;
|
|
275
277
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
// Mark explicit selection before validation so a damaged managed profile does
|
|
279
|
+
// not produce advice to start an unrelated local daemon.
|
|
280
|
+
managedSelection = process.env.OURS_CONFIG !== undefined
|
|
281
|
+
|| existsSync(join(process.env.HOME || homedir(), '.ours-client', 'profile.json'));
|
|
282
|
+
const selected = readClientProfile(process.env);
|
|
283
|
+
managedSelection = selected !== undefined;
|
|
284
|
+
const client = await attachDaemon(selected ? {
|
|
285
|
+
endpoint: selected.endpoint, expectedInstanceId: selected.expectedInstanceId,
|
|
286
|
+
credentialPath: selected.credentialPath, env: {}, sessionMode: 'external',
|
|
278
287
|
leaseToken: `ours-fleet-doctor-${process.pid}`,
|
|
279
|
-
|
|
288
|
+
} : {
|
|
289
|
+
env: process.env, leaseToken: `ours-fleet-doctor-${process.pid}`, clientPid: process.pid,
|
|
280
290
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
let daemonVersion;
|
|
292
|
+
try {
|
|
293
|
+
const info = await client.version();
|
|
294
|
+
if (info.name !== 'ours' || typeof info.version !== 'string')
|
|
295
|
+
throw new Error('the selected endpoint did not return a valid ours daemon identity');
|
|
296
|
+
daemonVersion = info.version;
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
await client.close();
|
|
300
|
+
}
|
|
301
|
+
checks.push({ name: 'ours daemon', ok: true, detail: `running (${daemonVersion})` });
|
|
285
302
|
}
|
|
286
303
|
catch (error) {
|
|
287
304
|
checks.push({
|
|
288
305
|
name: 'ours daemon', ok: false,
|
|
289
|
-
detail:
|
|
306
|
+
detail: (managedSelection
|
|
307
|
+
? 'selected client profile failed — check its endpoint, instance and issued credential '
|
|
308
|
+
: 'not reachable through the SDK — start it with: ours-daemon start ')
|
|
290
309
|
+ `[${error?.message ?? String(error)}]`,
|
|
291
310
|
});
|
|
292
311
|
}
|
|
@@ -8,6 +8,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { createConnection } from 'node:net';
|
|
11
|
+
import { readClientProfile } from '../client-profile.js';
|
|
12
|
+
import { coworkHttpCall } from './cowork-http.js';
|
|
11
13
|
const RPC_VERSION = 1;
|
|
12
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
15
|
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
@@ -275,10 +277,15 @@ function rpcCall(socketPath, method, params, timeoutMs, connect) {
|
|
|
275
277
|
});
|
|
276
278
|
}
|
|
277
279
|
export function createCoworkAdapter(options = {}) {
|
|
278
|
-
const
|
|
280
|
+
const env = options.env ?? process.env;
|
|
281
|
+
const explicitLocal = options.socketPath !== undefined || options.configPath !== undefined
|
|
282
|
+
|| env.OURS_COWORK_CONFIG !== undefined || !!env.OURS_COWORK_STATE_DIR;
|
|
283
|
+
const profile = explicitLocal ? undefined : readClientProfile({ ...env, HOME: options.home ?? env.HOME });
|
|
284
|
+
const remote = profile?.serverUrl ? profile : undefined;
|
|
285
|
+
const socketPath = remote ? undefined : resolveCoworkSocketPath(options);
|
|
279
286
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
280
287
|
const connect = options.connect ?? ((path) => createConnection(path));
|
|
281
|
-
const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
288
|
+
const call = (method, params) => remote ? coworkHttpCall(remote, method, params, timeoutMs) : rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
282
289
|
return {
|
|
283
290
|
async available() {
|
|
284
291
|
try {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExplicitClientProfile } from '../client-profile.js';
|
|
2
|
+
/** Same RPC protocol as the local socket; never replay a possibly delivered mutation. */
|
|
3
|
+
export declare function coworkHttpCall(profile: ExplicitClientProfile, method: string, params: Record<string, unknown>, timeoutMs: number): Promise<unknown>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
3
|
+
import { readPrivateFile } from '@ours.network/sdk/connector';
|
|
4
|
+
import { CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
5
|
+
/** Same RPC protocol as the local socket; never replay a possibly delivered mutation. */
|
|
6
|
+
export async function coworkHttpCall(profile, method, params, timeoutMs) {
|
|
7
|
+
const id = randomUUID();
|
|
8
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
9
|
+
const body = JSON.stringify({ version: 1, id, method, params });
|
|
10
|
+
if (Buffer.byteLength(body) > 1024 * 1024)
|
|
11
|
+
throw new CoworkProtocolError(method, 'request exceeded 1 MiB');
|
|
12
|
+
try {
|
|
13
|
+
const client = await attachOursClient({ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
|
|
14
|
+
credentialPath: profile.credentialPath, sessionMode: 'external', leaseToken: `fleet-cowork-check-${id}`, env: {}, requestSignal: signal });
|
|
15
|
+
await client.close();
|
|
16
|
+
const token = readPrivateFile(profile.credentialPath, 4096).toString('utf8').trim();
|
|
17
|
+
const response = await fetch(profile.serverUrl + '/cowork/management/rpc', {
|
|
18
|
+
method: 'POST', redirect: 'error', signal,
|
|
19
|
+
headers: { 'content-type': 'application/json', 'x-ours-api-token': token },
|
|
20
|
+
body,
|
|
21
|
+
});
|
|
22
|
+
if (response.status === 401 || response.status === 403) {
|
|
23
|
+
await response.body?.cancel();
|
|
24
|
+
throw new CoworkProtocolError(method, 'server credential was rejected', 'unauthorized');
|
|
25
|
+
}
|
|
26
|
+
const reader = response.body?.getReader();
|
|
27
|
+
if (!reader)
|
|
28
|
+
throw new CoworkProtocolError(method, 'empty HTTP response');
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
size += value.length;
|
|
37
|
+
if (size > 4 * 1024 * 1024)
|
|
38
|
+
throw new CoworkProtocolError(method, 'response exceeded 4 MiB');
|
|
39
|
+
chunks.push(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await reader.cancel();
|
|
44
|
+
}
|
|
45
|
+
let rpc;
|
|
46
|
+
try {
|
|
47
|
+
rpc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new CoworkProtocolError(method, 'server returned malformed JSON');
|
|
51
|
+
}
|
|
52
|
+
if (!rpc || Array.isArray(rpc) || rpc.version !== 1 || rpc.id !== id
|
|
53
|
+
|| Object.hasOwn(rpc, 'result') === Object.hasOwn(rpc, 'error'))
|
|
54
|
+
throw new CoworkProtocolError(method, 'server returned an invalid RPC response');
|
|
55
|
+
if (Object.hasOwn(rpc, 'error')) {
|
|
56
|
+
const error = rpc.error;
|
|
57
|
+
if (!error || typeof error.code !== 'string' || typeof error.message !== 'string')
|
|
58
|
+
throw new CoworkProtocolError(method, 'server returned an invalid RPC error');
|
|
59
|
+
throw new CoworkProtocolError(method, error.message, error.code);
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok)
|
|
62
|
+
throw new CoworkProtocolError(method, `management answered HTTP ${response.status}`);
|
|
63
|
+
return rpc.result;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof CoworkProtocolError)
|
|
67
|
+
throw error;
|
|
68
|
+
throw new CoworkUnavailableError('Cowork HTTP management failed; check the selected server and credential', { cause: error });
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.5",
|
|
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",
|