@ours.network/fleet 1.2.0-nightly.14 → 1.2.0-nightly.16

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/dist/monitor.d.ts CHANGED
@@ -88,33 +88,6 @@ export interface MonitorDeps {
88
88
  * turns they trigger keep dying.
89
89
  */
90
90
  export type StatusCause = 'connectivity' | 'delivery' | 'modal' | 'offline' | 'turns-failing' | 'safe-boundary' | 'auth';
91
- /** Best-effort daemon config (issue #17): the fields the MCP client reads. */
92
- interface DaemonConfig {
93
- apiToken?: string;
94
- port?: number;
95
- stateDir?: string;
96
- }
97
- /**
98
- * Read the daemon config the way the MCP client does — best-effort. Any missing,
99
- * malformed, or unreadable config yields `{}` so token resolution falls through
100
- * (issue #17). Only the well-typed fields we consume are surfaced.
101
- */
102
- export declare function readDaemonConfig(env: NodeJS.ProcessEnv): DaemonConfig;
103
- /**
104
- * Resolve the daemon API token exactly like the MCP client (issue #17), a 3-step
105
- * chain: `OURS_API_TOKEN` (trimmed) → config `apiToken` (trimmed) → the 0600 owner
106
- * token at `<stateDir>/daemon-token`. Never generates a token; a failed read of
107
- * any source (missing/unreadable) silently falls through to the next.
108
- */
109
- export declare function resolveApiToken(env: NodeJS.ProcessEnv, file?: DaemonConfig): string | undefined;
110
- export interface DaemonEndpoint {
111
- origin: string;
112
- port: number;
113
- configPath: string;
114
- stateDir: string;
115
- url(name: string): string;
116
- headers: Record<string, string>;
117
- }
118
91
  export type IdentityPresence = {
119
92
  state: 'present';
120
93
  temporary: boolean;
@@ -125,16 +98,12 @@ export type IdentityPresence = {
125
98
  state: 'unknown';
126
99
  detail: string;
127
100
  };
128
- /** Resolve the daemon endpoint + auth header from env → config → defaults. */
129
- export declare function resolveEndpoint(env: NodeJS.ProcessEnv, includeToken?: boolean): DaemonEndpoint;
130
101
  /**
131
102
  * Ask the daemon's authoritative identity index. The notifications endpoint is
132
103
  * intentionally unsuitable for lifecycle: it serves an empty 200 page for a
133
104
  * valid but missing identity, which made a closed temp identity look healthy.
134
105
  */
135
106
  export declare function probeIdentityPresence(name: string, fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: IdentityProbeDeps): Promise<IdentityPresence>;
136
- /** Actionable, secret-free description of every token source for this profile. */
137
- export declare function authResolutionHint(ep: DaemonEndpoint): string;
138
107
  /** Keep only the events whose type the role asked to wake on. */
139
108
  export declare function filterEvents(events: NotifyEvent[], wakeSources: string[]): NotifyEvent[];
140
109
  /**
@@ -196,8 +165,7 @@ export declare class Monitor {
196
165
  private readonly identity;
197
166
  private readonly cfg;
198
167
  private readonly deps;
199
- private readonly ep?;
200
- private readonly profile?;
168
+ private readonly profile;
201
169
  private readonly profileKey;
202
170
  private readonly monitorLeaseToken;
203
171
  private profileClientPromise?;
@@ -263,4 +231,3 @@ export declare class Monitor {
263
231
  private writeStatus;
264
232
  }
265
233
  export declare function createMonitor(o: MonitorOpts): Monitor;
266
- export {};
package/dist/monitor.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
5
4
  import { attachOursClient } from '@ours.network/sdk/client';
6
5
  import { clientProfileKey, readClientProfile, } from './client-profile.js';
@@ -42,124 +41,28 @@ const TURN_OBSERVE_INTERVAL_MS = 1_500;
42
41
  const DEFAULT_TURN_FAIL_THRESHOLD = 3; // fallback when the resolved config omits it
43
42
  class AuthError extends Error {
44
43
  }
45
- /** Path to the daemon config the MCP client uses: OURS_CONFIG ?? real ~/.ours/config.json. */
46
- const daemonConfigPath = (env) => env.OURS_CONFIG ?? join(homedir(), '.ours', 'config.json');
47
- /** Preserve the daemon's legacy env integer semantics: parseInt, invalid → absent. */
48
- function envInt(env, name) {
49
- const raw = env[name];
50
- if (raw === undefined)
51
- return undefined;
52
- const n = parseInt(raw, 10);
53
- return Number.isNaN(n) ? undefined : n;
54
- }
55
- /**
56
- * Read the daemon config the way the MCP client does — best-effort. Any missing,
57
- * malformed, or unreadable config yields `{}` so token resolution falls through
58
- * (issue #17). Only the well-typed fields we consume are surfaced.
59
- */
60
- export function readDaemonConfig(env) {
61
- try {
62
- const p = JSON.parse(readFileSync(daemonConfigPath(env), 'utf8'));
63
- const o = {};
64
- if (typeof p.apiToken === 'string' && p.apiToken.trim())
65
- o.apiToken = p.apiToken.trim();
66
- if (typeof p.port === 'number' && Number.isFinite(p.port))
67
- o.port = p.port;
68
- if (typeof p.stateDir === 'string')
69
- o.stateDir = p.stateDir;
70
- return o;
71
- }
72
- catch {
73
- return {};
74
- }
75
- }
76
- /**
77
- * Resolve the daemon API token exactly like the MCP client (issue #17), a 3-step
78
- * chain: `OURS_API_TOKEN` (trimmed) → config `apiToken` (trimmed) → the 0600 owner
79
- * token at `<stateDir>/daemon-token`. Never generates a token; a failed read of
80
- * any source (missing/unreadable) silently falls through to the next.
81
- */
82
- export function resolveApiToken(env, file = readDaemonConfig(env)) {
83
- const e = env.OURS_API_TOKEN?.trim();
84
- if (e)
85
- return e;
86
- if (file.apiToken)
87
- return file.apiToken;
88
- const sd = env.OURS_STATE_DIR ?? file.stateDir ?? join(homedir(), '.ours');
89
- try {
90
- const t = readFileSync(join(sd, 'daemon-token'), 'utf8').trim();
91
- if (t)
92
- return t;
93
- }
94
- catch { /* missing/unreadable (e.g. cross-user 0600) → fall through */ }
95
- return undefined;
96
- }
97
- /** Resolve the daemon endpoint + auth header from env → config → defaults. */
98
- export function resolveEndpoint(env, includeToken = true) {
99
- const file = readDaemonConfig(env);
100
- const port = envInt(env, 'OURS_PORT') ?? file.port ?? DEFAULT_PORT;
101
- const configPath = daemonConfigPath(env);
102
- const stateDir = env.OURS_STATE_DIR ?? file.stateDir ?? join(homedir(), '.ours');
103
- const token = includeToken ? resolveApiToken(env, file) : undefined;
104
- const origin = `http://127.0.0.1:${port}`;
105
- return {
106
- origin,
107
- port,
108
- configPath,
109
- stateDir,
110
- url: (name) => `${origin}/identities/${encodeURIComponent(name)}/notifications`,
111
- headers: token ? { 'x-ours-api-token': token } : {},
112
- };
113
- }
114
44
  /**
115
45
  * Ask the daemon's authoritative identity index. The notifications endpoint is
116
46
  * intentionally unsuitable for lifecycle: it serves an empty 200 page for a
117
47
  * valid but missing identity, which made a closed temp identity look healthy.
118
48
  */
119
49
  export async function probeIdentityPresence(name, fetch, env, deps = {}) {
120
- const profile = readClientProfile(env);
121
- if (profile) {
122
- let client;
123
- try {
124
- client = await (deps.attachClient?.({
125
- endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
126
- credentialPath: profile.credentialPath,
127
- sessionMode: 'external', leaseToken: randomUUID(), env: {},
128
- }) ?? attachOursClient({
129
- endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
130
- credentialPath: profile.credentialPath,
131
- sessionMode: 'external', leaseToken: randomUUID(), env: {},
132
- }));
133
- return classifyIdentityPresence(name, await client.identities());
134
- }
135
- catch {
136
- return { state: 'unknown', detail: 'selected identity index is unavailable' };
137
- }
138
- finally {
139
- await client?.close().catch(() => undefined);
140
- }
141
- }
142
- const ep = resolveEndpoint(env);
143
- let response;
50
+ let client;
144
51
  try {
145
- response = await fetch(`${ep.origin}/identities`, { headers: ep.headers });
146
- }
147
- catch (error) {
148
- return { state: 'unknown', detail: `identity index unreachable (${msg(error)})` };
149
- }
150
- if (!response.ok)
151
- return {
152
- state: 'unknown',
153
- detail: response.status === 401
154
- ? `daemon rejected the API token (401) — ${authResolutionHint(ep)}`
155
- : `identity index returned HTTP ${response.status}`,
52
+ const profile = readClientProfile(env);
53
+ const options = {
54
+ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
55
+ credentialPath: profile.credentialPath,
56
+ sessionMode: 'external', leaseToken: randomUUID(), env: {},
156
57
  };
157
- try {
158
- const body = await response.json();
159
- return classifyIdentityPresence(name, body.identities);
58
+ client = await (deps.attachClient?.(options) ?? attachOursClient(options));
59
+ return classifyIdentityPresence(name, await client.identities());
160
60
  }
161
- catch (error) {
162
- return { state: 'unknown', detail: `identity index response is unreadable (${msg(error)})` };
61
+ catch {
62
+ return { state: 'unknown', detail: 'gateway identity index unavailable; check the shared client profile and issued credential' };
63
+ }
64
+ finally {
65
+ await client?.close().catch(() => undefined);
163
66
  }
164
67
  }
165
68
  function classifyIdentityPresence(name, identities) {
@@ -186,12 +89,6 @@ function classifyIdentityPresence(name, identities) {
186
89
  stale: found.stale === true,
187
90
  };
188
91
  }
189
- /** Actionable, secret-free description of every token source for this profile. */
190
- export function authResolutionHint(ep) {
191
- const tokenPath = join(ep.stateDir, 'daemon-token');
192
- return `set OURS_API_TOKEN, set apiToken in ${JSON.stringify(ep.configPath)}, or ensure ` +
193
- `${JSON.stringify(tokenPath)} is readable by the fleet supervisor`;
194
- }
195
92
  /** Keep only the events whose type the role asked to wake on. */
196
93
  export function filterEvents(events, wakeSources) {
197
94
  const set = new Set(wakeSources);
@@ -414,7 +311,6 @@ export class Monitor {
414
311
  identity;
415
312
  cfg;
416
313
  deps;
417
- ep;
418
314
  profile;
419
315
  profileKey;
420
316
  monitorLeaseToken = `ours-fleet-monitor-${process.pid}-${randomUUID()}`;
@@ -441,8 +337,7 @@ export class Monitor {
441
337
  this.cfg = o.cfg;
442
338
  this.deps = o.deps;
443
339
  this.profile = readClientProfile(o.deps.env);
444
- this.ep = this.profile ? undefined : resolveEndpoint(o.deps.env);
445
- this.profileKey = this.profile ? clientProfileKey(this.profile) : this.ep.origin;
340
+ this.profileKey = clientProfileKey(this.profile);
446
341
  this.statusPath = join(o.agentDir, '.monitor-status');
447
342
  this.cursorPath = join(o.agentDir, '.notify-cursor');
448
343
  this.statePath = join(o.agentDir, '.monitor-state.json');
@@ -470,14 +365,8 @@ export class Monitor {
470
365
  }
471
366
  catch (e) {
472
367
  if (e instanceof AuthError) {
473
- if (this.profile) {
474
- this.cursor = null;
475
- this.degrade('auth', e.message);
476
- }
477
- else {
478
- this.fatal = true;
479
- this.degrade('auth', e.message, 'failed');
480
- }
368
+ this.cursor = null;
369
+ this.degrade('auth', e.message);
481
370
  }
482
371
  else {
483
372
  this.cursor = null;
@@ -510,11 +399,6 @@ export class Monitor {
510
399
  catch (e) {
511
400
  if (this.stopped)
512
401
  return;
513
- if (e instanceof AuthError && !this.profile) {
514
- this.fatal = true;
515
- this.degrade('auth', e.message, 'failed');
516
- return;
517
- }
518
402
  backoff = Math.min(backoff + BACKOFF_STEP_MS, BACKOFF_MAX_MS);
519
403
  if (e instanceof AuthError)
520
404
  this.degrade('auth', e.message);
@@ -630,23 +514,19 @@ export class Monitor {
630
514
  this.currentAbort = ctrl;
631
515
  let timedOut = false;
632
516
  const timer = this.deps.timers.set(() => { timedOut = true; ctrl.abort(); }, timeoutMs);
633
- let resp;
634
517
  try {
635
- if (this.profile) {
636
- const client = await this.profileClient();
637
- const page = await client.readNotificationPage(this.identity, {
638
- since: since === 'tip' ? 'tip' : Number.parseInt(since, 10),
639
- signal: ctrl.signal,
640
- requestTimeoutMs: timeoutMs,
641
- });
642
- return { cursor: page.cursor, events: page.events };
643
- }
644
- resp = await this.deps.fetch(`${this.ep.url(this.identity)}?since=${since}`, { headers: this.ep.headers, signal: ctrl.signal });
518
+ const client = await this.profileClient();
519
+ const page = await client.readNotificationPage(this.identity, {
520
+ since: since === 'tip' ? 'tip' : Number.parseInt(since, 10),
521
+ signal: ctrl.signal,
522
+ requestTimeoutMs: timeoutMs,
523
+ });
524
+ return { cursor: page.cursor, events: page.events };
645
525
  }
646
526
  catch (error) {
647
527
  if (timedOut && timeoutKind === 'stall')
648
528
  throw new Error(`notification stream stalled for ${Math.round(timeoutMs / 1000)}s`);
649
- if (this.profile && /(?:HTTP\s*401|unauthori[sz]ed|API token)/i.test(msg(error)))
529
+ if (/(?:HTTP\s*401|unauthori[sz]ed|API token)/i.test(msg(error)))
650
530
  throw new AuthError(`daemon rejected the API token (401) — verify the credential file `
651
531
  + `${JSON.stringify(this.profile.credentialPath)} selected by `
652
532
  + `${JSON.stringify(this.profile.configPath)}`);
@@ -656,15 +536,8 @@ export class Monitor {
656
536
  this.deps.timers.clear(timer);
657
537
  this.currentAbort = null;
658
538
  }
659
- if (resp.status === 401)
660
- throw new AuthError(`daemon rejected the API token (401) — ${authResolutionHint(this.ep)}`);
661
- if (!resp.ok)
662
- throw new Error(`daemon returned HTTP ${resp.status}`);
663
- return resp.json();
664
539
  }
665
540
  profileClient() {
666
- if (!this.profile)
667
- throw new Error('explicit client profile is not selected');
668
541
  if (!this.profileClientPromise) {
669
542
  const { endpoint, expectedInstanceId, credentialPath } = this.profile;
670
543
  const options = {
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { extname, join } from 'node:path';
4
- import { readDaemonConfig, resolveEndpoint } from '../monitor.js';
3
+ import { extname } from 'node:path';
5
4
  import { readClientProfile } from '../client-profile.js';
6
5
  import { OursError, attachOursClient, } from '@ours.network/sdk/client';
7
6
  /** Any failure of a daemon operation. Never carries a message body or a token. */
@@ -131,24 +130,10 @@ export class OursSdkClient {
131
130
  return;
132
131
  const environment = { ...process.env, ...this.env };
133
132
  const profile = readClientProfile(environment);
134
- // Reuse existing connection configuration. Identity selection checks the
135
- // daemon capability before reading the client's protected token delivery file.
136
- const endpoint = !profile && environment.OURS_DAEMON_ID
137
- ? resolveEndpoint(environment, false) : undefined;
138
- const token = endpoint ? environment.OURS_API_TOKEN?.trim() || readDaemonConfig(environment).apiToken : undefined;
139
- const options = profile ? {
133
+ const options = {
140
134
  endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
141
135
  credentialPath: profile.credentialPath,
142
136
  sessionMode: 'external', leaseToken: this.leaseToken, env: {},
143
- } : endpoint ? {
144
- endpoint: endpoint.origin, expectedInstanceId: environment.OURS_DAEMON_ID,
145
- sessionMode: 'external', leaseToken: this.leaseToken,
146
- ...(token ? { token } : { credentialPath: join(endpoint.stateDir, 'daemon-token') }),
147
- } : {
148
- env: environment,
149
- leaseToken: this.leaseToken,
150
- clientPid: process.pid,
151
- fetch: notificationDeadlineFetch(this.deps.fetch ?? globalThis.fetch, this.deps.notificationRequestDeadlineMs ?? NOTIFICATION_REQUEST_DEADLINE_MS),
152
137
  };
153
138
  this.client = await (this.deps.attachClient?.(options) ?? attachOursClient(options));
154
139
  this.terminalReleasePending = true;
@@ -77,15 +77,11 @@ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {})
77
77
  async function withIdentityClient(work) {
78
78
  const profile = readClientProfile(process.env);
79
79
  const leaseToken = `ours-fleet-room-close-${process.pid}-${randomUUID()}`;
80
- const client = await attachOursClient(profile ? {
80
+ const client = await attachOursClient({
81
81
  endpoint: profile.endpoint,
82
82
  expectedInstanceId: profile.expectedInstanceId,
83
83
  credentialPath: profile.credentialPath,
84
84
  sessionMode: 'external', env: {}, leaseToken,
85
- } : {
86
- env: process.env,
87
- leaseToken,
88
- clientPid: process.pid,
89
85
  });
90
86
  try {
91
87
  return await work(client);
@@ -54,7 +54,9 @@ export function validateRoomsConfig(raw, vars, path) {
54
54
  if (!isPlainObject(raw.cowork))
55
55
  throw new RoomsTasksConfigError(path, 'rooms.cowork: must be a mapping');
56
56
  rejectUnknown(raw.cowork, RCK, path, 'rooms.cowork');
57
- cowork = { config: raw.cowork.config };
57
+ if (raw.cowork.config !== undefined)
58
+ throw new RoomsTasksConfigError(path, 'rooms.cowork.config is no longer supported; remove it and configure ~/.ours-client/profile.json for all services');
59
+ cowork = {};
58
60
  }
59
61
  if (!isPlainObject(raw.owner))
60
62
  throw new RoomsTasksConfigError(path, 'rooms.owner: required and must be a mapping');
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Typed client for the ours-cowork v1 private management-socket protocol.
2
+ * Typed client for the ours-cowork v1 gateway management protocol.
3
3
  *
4
4
  * Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
5
- * by Cowork's Unix socket and projects the response into orchestration data.
5
+ * through the shared HTTP gateway and projects the response into orchestration data.
6
6
  */
7
7
  import type { Socket } from 'node:net';
8
8
  import type { RoomHistoryEvidence } from './types.js';
@@ -108,6 +108,4 @@ export interface CoworkAdapterOptions {
108
108
  home?: string;
109
109
  connect?: (path: string) => Socket;
110
110
  }
111
- /** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
112
- export declare function resolveCoworkSocketPath(options?: CoworkAdapterOptions): string;
113
111
  export declare function createCoworkAdapter(options?: CoworkAdapterOptions): CoworkAdapter;
@@ -1,20 +1,14 @@
1
1
  /**
2
- * Typed client for the ours-cowork v1 private management-socket protocol.
2
+ * Typed client for the ours-cowork v1 gateway management protocol.
3
3
  *
4
4
  * Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
5
- * by Cowork's Unix socket and projects the response into orchestration data.
5
+ * through the shared HTTP gateway and projects the response into orchestration data.
6
6
  */
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import { homedir } from 'node:os';
9
- import { join, resolve } from 'node:path';
10
- import { createConnection } from 'node:net';
11
7
  import { readClientProfile } from '../client-profile.js';
12
8
  import { coworkHttpCall } from './cowork-http.js';
13
- const RPC_VERSION = 1;
14
9
  const DEFAULT_TIMEOUT_MS = 30_000;
15
- const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
16
10
  export class CoworkUnavailableError extends Error {
17
- constructor(message = 'Cowork management socket is not reachable', options) {
11
+ constructor(message = 'Cowork gateway management is not reachable', options) {
18
12
  super(message, options);
19
13
  this.name = 'CoworkUnavailableError';
20
14
  }
@@ -171,121 +165,13 @@ function projectHistoryRecord(value) {
171
165
  }
172
166
  return undefined;
173
167
  }
174
- /** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
175
- export function resolveCoworkSocketPath(options = {}) {
176
- if (options.socketPath)
177
- return resolve(options.socketPath);
178
- const env = options.env ?? process.env;
179
- const home = options.home ?? homedir();
180
- const configuredPath = options.configPath ?? env.OURS_COWORK_CONFIG;
181
- const configPath = resolve(configuredPath ?? join(home, '.ours-cowork', 'config.json'));
182
- let stateDir = resolve(home, '.ours-cowork');
183
- if (existsSync(configPath)) {
184
- let parsed;
185
- try {
186
- parsed = JSON.parse(readFileSync(configPath, 'utf8'));
187
- }
188
- catch (error) {
189
- throw new CoworkProtocolError('config', `malformed config at ${configPath}`, 'invalid_config', { cause: error });
190
- }
191
- const config = object(parsed, 'config', 'config');
192
- if (config.version !== 1 || typeof config.stateDir !== 'string' || !config.stateDir)
193
- throw new CoworkProtocolError('config', `invalid Cowork v1 config at ${configPath}`, 'invalid_config');
194
- stateDir = resolve(config.stateDir);
195
- }
196
- else if (configuredPath !== undefined) {
197
- throw new CoworkProtocolError('config', `configured Cowork config does not exist: ${configPath}`, 'invalid_config');
198
- }
199
- if (env.OURS_COWORK_STATE_DIR)
200
- stateDir = resolve(env.OURS_COWORK_STATE_DIR);
201
- return join(stateDir, 'management.sock');
202
- }
203
- function rpcCall(socketPath, method, params, timeoutMs, connect) {
204
- const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
205
- const request = `${JSON.stringify({ version: RPC_VERSION, id, method, params })}\n`;
206
- return new Promise((resolveCall, rejectCall) => {
207
- const socket = connect(socketPath);
208
- let bytes = '';
209
- let size = 0;
210
- let settled = false;
211
- let connected = false;
212
- const finishError = (error) => {
213
- if (settled)
214
- return;
215
- settled = true;
216
- clearTimeout(timer);
217
- socket.destroy();
218
- rejectCall(error);
219
- };
220
- const timer = setTimeout(() => finishError(new CoworkUnavailableError(connected ? 'Cowork did not answer the management socket' : 'Cowork management socket is not reachable')), timeoutMs);
221
- socket.setEncoding('utf8');
222
- socket.once('connect', () => {
223
- connected = true;
224
- socket.write(request);
225
- });
226
- socket.on('data', (chunk) => {
227
- if (settled)
228
- return;
229
- const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
230
- size += Buffer.byteLength(text);
231
- if (size > MAX_RESPONSE_BYTES) {
232
- finishError(new CoworkProtocolError(method, 'response exceeded 4 MiB'));
233
- return;
234
- }
235
- bytes += text;
236
- const newline = bytes.indexOf('\n');
237
- if (newline < 0)
238
- return;
239
- if (bytes.slice(newline + 1).trim() !== '') {
240
- finishError(new CoworkProtocolError(method, 'daemon returned more than one response'));
241
- return;
242
- }
243
- let response;
244
- try {
245
- response = object(JSON.parse(bytes.slice(0, newline)), method, 'RPC response');
246
- }
247
- catch (error) {
248
- finishError(error instanceof CoworkProtocolError
249
- ? error : new CoworkProtocolError(method, 'daemon returned malformed JSON', 'protocol_error', { cause: error }));
250
- return;
251
- }
252
- if (response.version !== RPC_VERSION || response.id !== id
253
- || (Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result'))) {
254
- finishError(new CoworkProtocolError(method, 'daemon returned an invalid RPC response'));
255
- return;
256
- }
257
- settled = true;
258
- clearTimeout(timer);
259
- socket.destroy();
260
- if (Object.hasOwn(response, 'error')) {
261
- const error = object(response.error, method, 'RPC error');
262
- rejectCall(new CoworkProtocolError(method, typeof error.message === 'string' ? error.message : 'unknown RPC error', typeof error.code === 'string' ? error.code : 'rpc_error'));
263
- }
264
- else {
265
- resolveCall(response.result);
266
- }
267
- });
268
- socket.once('end', () => {
269
- if (!settled)
270
- finishError(new CoworkProtocolError(method, 'daemon closed without a complete response'));
271
- });
272
- socket.once('error', (error) => {
273
- if (!settled)
274
- finishError(new CoworkUnavailableError(error.code === 'EACCES' || error.code === 'EPERM'
275
- ? 'Cowork management socket access denied' : 'Cowork management socket is not reachable', { cause: error }));
276
- });
277
- });
278
- }
279
168
  export function createCoworkAdapter(options = {}) {
280
169
  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);
170
+ if (options.socketPath !== undefined || options.configPath !== undefined)
171
+ throw new CoworkProtocolError('config', 'Local Cowork selectors are unsupported; configure the shared gateway client profile', 'invalid_config');
172
+ const profile = readClientProfile({ ...env, HOME: options.home ?? env.HOME });
286
173
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
287
- const connect = options.connect ?? ((path) => createConnection(path));
288
- const call = (method, params) => remote ? coworkHttpCall(remote, method, params, timeoutMs) : rpcCall(socketPath, method, params, timeoutMs, connect);
174
+ const call = (method, params) => coworkHttpCall(profile, method, params, timeoutMs);
289
175
  return {
290
176
  async available() {
291
177
  try {
@@ -10,7 +10,6 @@ const LIFECYCLE_RETRY_MS = 250;
10
10
  export function fleetWorkerEnv(source = process.env) {
11
11
  const inheritedKeys = [
12
12
  'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
13
- 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
14
13
  ];
15
14
  const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
16
15
  const lifecycleStateDir = source[FLEET_WORKER_LIFECYCLE_STATE_DIR_ENV]
package/dist/runner.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
2
2
  import { type ResolvedRole } from './config.js';
3
- import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
3
+ import { probeIdentityPresence, type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
4
4
  import { type DaemonGenerationProbe } from './daemon-recovery.js';
5
5
  import { type Exec } from './exec.js';
6
6
  import { RoleControlServer } from './session/control.js';
@@ -20,6 +20,7 @@ export interface RunnerDeps {
20
20
  log(line: string): void;
21
21
  /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */
22
22
  fetch: FetchLike;
23
+ identityProbeDeps?: Parameters<typeof probeIdentityPresence>[3];
23
24
  probeGeneration(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
24
25
  /** Construct the supervisor mail monitor (injectable so tests stub it out). */
25
26
  createMonitor(opts: MonitorOpts): MonitorHandle;
package/dist/runner.js CHANGED
@@ -936,7 +936,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
936
936
  }
937
937
  if (temp && now >= nextIdentityPollAt) {
938
938
  nextIdentityPollAt = now + TEMP_IDENTITY_POLL_MS;
939
- const presence = await probeIdentityPresence(role.identity, deps.fetch, resolvedMonitorDeps.env);
939
+ const presence = await probeIdentityPresence(role.identity, deps.fetch, resolvedMonitorDeps.env, deps.identityProbeDeps);
940
940
  if (presence.state === 'present') {
941
941
  identityObserved = true;
942
942
  identityAbsentSince = undefined;
@@ -1,3 +1,4 @@
1
+ import { clientConfigPath } from '../client-profile.js';
1
2
  import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { home, logsRoot } from '../paths.js';
@@ -22,7 +23,7 @@ const xml = (value) => value
22
23
  .replaceAll("'", '&apos;');
23
24
  function plist(name, binPath) {
24
25
  const log = join(logsRoot(), `${name}.log`);
25
- const config = process.env.OURS_CONFIG;
26
+ const config = clientConfigPath(process.env);
26
27
  return `<?xml version="1.0" encoding="UTF-8"?>
27
28
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
29
  <plist version="1.0">
@@ -1,3 +1,4 @@
1
+ import { clientConfigPath } from '../client-profile.js';
1
2
  import { mkdirSync, writeFileSync } from 'node:fs';
2
3
  import { delimiter, dirname, join } from 'node:path';
3
4
  import { userInfo } from 'node:os';
@@ -69,30 +70,10 @@ export function makeSystemdBackend(exec = realExec) {
69
70
  dirname(process.execPath),
70
71
  ...(process.env.PATH ?? '').split(delimiter),
71
72
  ].filter(Boolean))].join(delimiter);
72
- // Same reason as PATH, for the other thing a lingering unit cannot inherit:
73
- // WHICH ours daemon this fleet was set up against.
74
- //
75
- // ours-fleet resolves its daemon from OURS_CONFIG / OURS_PORT / OURS_STATE_DIR
76
- // and otherwise falls back to ~/.ours and port 3050 (src/monitor.ts). A host
77
- // set up against a non-default daemon — the installer's multi-daemon profiles
78
- // do exactly this — passes that selection to `ours-fleet init` in the
79
- // environment, and it died there: nothing persisted it, so every runner
80
- // systemd started at boot resolved the default daemon again, and on a host
81
- // where only the non-default daemon exists that is a daemon that is not there.
82
- //
83
- // ONLY OURS_CONFIG is baked, deliberately. It names which daemon, and leaves
84
- // that daemon's own config file authoritative for port and state directory, so
85
- // a later edit to it still wins. Baking OURS_PORT/OURS_STATE_DIR would freeze
86
- // those into a unit file that outranks the config for ever after — the failure
87
- // mode @ours.network/cli avoids for the same reason (packages/cli/src/
88
- // service.ts: "The port is deliberately NOT baked").
89
- //
90
- // Absent from init's environment ⇒ no line, and the unit is byte-for-byte what
91
- // it has always been. This teaches fleet nothing about installer profiles; it
92
- // persists the selection fleet was initialised with.
73
+ // Persist one profile path, never a URL or service port. Profile updates
74
+ // are picked up by every fresh process, including lingering user services.
93
75
  const unitEnv = ['PATH=' + servicePath];
94
- if (process.env.OURS_CONFIG)
95
- unitEnv.push('OURS_CONFIG=' + process.env.OURS_CONFIG);
76
+ unitEnv.push('OURS_CONFIG=' + clientConfigPath(process.env));
96
77
  const environmentLines = unitEnv
97
78
  .map(value => `Environment="${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`)
98
79
  .join('\n');