@ours.network/fleet 1.2.0-nightly.66 → 1.2.0-nightly.68
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 +23 -0
- package/dist/atomic-file.d.ts +17 -0
- package/dist/atomic-file.js +93 -18
- package/dist/build-info.json +6 -4
- package/dist/capabilities.d.ts +4 -1
- package/dist/capabilities.js +5 -0
- package/dist/cli.js +30 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +13 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +20 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.js +42 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +10 -1
- package/dist/rooms-tasks/cowork-adapter.js +8 -0
- package/dist/rooms-tasks/cowork-http.js +29 -7
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +76 -12
- package/dist/session/acp.d.ts +14 -0
- package/dist/session/acp.js +22 -6
- package/dist/session/codex-app-server.d.ts +6 -0
- package/dist/session/codex-app-server.js +17 -4
- package/dist/session/types.d.ts +6 -0
- package/dist/startup-readiness.d.ts +19 -0
- package/dist/startup-readiness.js +63 -0
- package/dist/supervisor/registration-refresh.d.ts +45 -0
- package/dist/supervisor/registration-refresh.js +266 -0
- package/dist/workspace-enrollment.d.ts +3 -2
- package/dist/workspace-enrollment.js +19 -5
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { replaceFileAtomically } from '../atomic-file.js';
|
|
4
|
+
import { realExec } from '../exec.js';
|
|
5
|
+
import { home } from '../paths.js';
|
|
6
|
+
import { WATCHDOG_LAUNCHD_LABEL, WATCHDOG_SYSTEMD_UNIT } from '../watchdog/service.js';
|
|
7
|
+
import { WEB_LAUNCHD_LABEL, WEB_SYSTEMD_UNIT } from '../web/service.js';
|
|
8
|
+
import { labelFor } from './launchd.js';
|
|
9
|
+
import { UNIT_TEMPLATE } from './systemd.js';
|
|
10
|
+
/**
|
|
11
|
+
* Service definitions written by `init`, `web install` and the watchdog
|
|
12
|
+
* installer pin the realpath of the CLI that wrote them: an immutable,
|
|
13
|
+
* per-release package tree. An update installs a new tree and moves the
|
|
14
|
+
* `ours-fleet` command, but a definition is only rewritten when that command
|
|
15
|
+
* runs again, so after the next reboot the OS starts the previous release's
|
|
16
|
+
* supervisor while agents, web and CLI run the new one.
|
|
17
|
+
*
|
|
18
|
+
* Refresh re-points an existing Fleet-generated definition at the invoking
|
|
19
|
+
* release and changes nothing else: environment, profile, ports and drop-ins
|
|
20
|
+
* are kept byte for byte, and no unit is created, enabled, started, stopped
|
|
21
|
+
* or restarted. A running process keeps its old code until its operator
|
|
22
|
+
* restarts it, which the report states rather than doing.
|
|
23
|
+
*/
|
|
24
|
+
const PACKAGE_SCRIPT = ['node_modules', '@ours.network', 'fleet', 'dist', 'cli.js'].join(sep);
|
|
25
|
+
/** The release tree that owns a Fleet CLI script, if it is a packaged one. */
|
|
26
|
+
export function releaseRoot(script) {
|
|
27
|
+
const suffix = sep + PACKAGE_SCRIPT;
|
|
28
|
+
return script.endsWith(suffix) ? script.slice(0, -suffix.length) : undefined;
|
|
29
|
+
}
|
|
30
|
+
function definitions(platform, homeDir) {
|
|
31
|
+
if (platform === 'linux') {
|
|
32
|
+
const dir = join(homeDir, '.config', 'systemd', 'user');
|
|
33
|
+
return [
|
|
34
|
+
{ service: 'supervisor', unit: UNIT_TEMPLATE, path: join(dir, UNIT_TEMPLATE), entry: '_run-fleet',
|
|
35
|
+
marker: 'ours-fleet process manager' },
|
|
36
|
+
{ service: 'web', unit: WEB_SYSTEMD_UNIT, path: join(dir, WEB_SYSTEMD_UNIT), entry: 'web',
|
|
37
|
+
marker: 'ours-fleet localhost web console' },
|
|
38
|
+
{ service: 'watchdogs', unit: WATCHDOG_SYSTEMD_UNIT, path: join(dir, WATCHDOG_SYSTEMD_UNIT), entry: '_run-watchdogs',
|
|
39
|
+
marker: 'ours-fleet watchdog scheduler' },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'darwin') {
|
|
43
|
+
const dir = join(homeDir, 'Library', 'LaunchAgents');
|
|
44
|
+
return [
|
|
45
|
+
{ service: 'supervisor', path: join(dir, `${labelFor('fleet')}.plist`), entry: '_run-fleet', marker: labelFor('fleet') },
|
|
46
|
+
{ service: 'web', path: join(dir, `${WEB_LAUNCHD_LABEL}.plist`), entry: 'web', marker: WEB_LAUNCHD_LABEL },
|
|
47
|
+
{ service: 'watchdogs', path: join(dir, `${WATCHDOG_LAUNCHD_LABEL}.plist`), entry: '_run-watchdogs',
|
|
48
|
+
marker: WATCHDOG_LAUNCHD_LABEL },
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const NODE_RUNTIME = /^node(js)?[0-9.]*$/;
|
|
54
|
+
const systemdQuote = (value) => `"${value.replace(/[%\\"]/g, char => char === '%' ? '%%' : `\\${char}`)}"`;
|
|
55
|
+
const xml = (value) => value.replace(/[&<>"']/g, char => ({
|
|
56
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
57
|
+
}[char]));
|
|
58
|
+
const unxml = (value) => value.replaceAll('"', '"').replaceAll(''', "'")
|
|
59
|
+
.replaceAll('<', '<').replaceAll('>', '>').replaceAll('&', '&');
|
|
60
|
+
function parseGenerated(text, platform, definition) {
|
|
61
|
+
if (platform === 'linux') {
|
|
62
|
+
const lines = text.split('\n');
|
|
63
|
+
const only = (pattern) => {
|
|
64
|
+
const found = lines.flatMap((line, index) => pattern.test(line) ? [index] : []);
|
|
65
|
+
return found.length === 1 ? found[0] : undefined;
|
|
66
|
+
};
|
|
67
|
+
if (lines[only(/^Description=/) ?? -1] !== `Description=${definition.marker}`)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (lines[only(/^Type=/) ?? -1] !== 'Type=simple')
|
|
70
|
+
return undefined;
|
|
71
|
+
const exec = only(/^\s*ExecStart\s*=/);
|
|
72
|
+
if (exec === undefined || !lines[exec].startsWith('ExecStart="'))
|
|
73
|
+
return undefined;
|
|
74
|
+
const args = [...lines[exec].slice('ExecStart='.length).matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)]
|
|
75
|
+
.map(match => match[1] !== undefined ? match[1].replace(/\\(.)/g, '$1').replaceAll('%%', '%') : match[2]);
|
|
76
|
+
const [runtime, script, entry] = args;
|
|
77
|
+
if (!runtime?.startsWith('/') || !NODE_RUNTIME.test(runtime.split('/').at(-1)) || entry !== definition.entry || !script)
|
|
78
|
+
return undefined;
|
|
79
|
+
const quoted = `${systemdQuote(runtime)} ${systemdQuote(script)} `;
|
|
80
|
+
if (!lines[exec].startsWith(`ExecStart=${quoted}`))
|
|
81
|
+
return undefined;
|
|
82
|
+
const pathLine = only(/^Environment="PATH=/);
|
|
83
|
+
return {
|
|
84
|
+
script,
|
|
85
|
+
rewrite(next, path) {
|
|
86
|
+
const out = [...lines];
|
|
87
|
+
out[exec] = `ExecStart=${systemdQuote(runtime)} ${systemdQuote(next)} ${lines[exec].slice(`ExecStart=${quoted}`.length)}`;
|
|
88
|
+
if (pathLine !== undefined) {
|
|
89
|
+
const value = /^Environment="PATH=((?:[^"\\]|\\.)*)"$/.exec(lines[pathLine])?.[1];
|
|
90
|
+
if (value !== undefined && !/[\\%]/.test(value))
|
|
91
|
+
out[pathLine] = `Environment="PATH=${value.split(':').map(path).join(':')}"`;
|
|
92
|
+
}
|
|
93
|
+
return out.join('\n');
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!new RegExp(`<key>Label</key>\\s*<string>${definition.marker.replaceAll('.', '\\.')}</string>`).test(text))
|
|
98
|
+
return undefined;
|
|
99
|
+
const program = /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>\s*<string>([^<]*)<\/string>\s*<string>([^<]*)<\/string>/.exec(text);
|
|
100
|
+
if (!program)
|
|
101
|
+
return undefined;
|
|
102
|
+
const [, runtime, script, entry] = program.map(unxml);
|
|
103
|
+
if (!runtime.startsWith('/') || !NODE_RUNTIME.test(runtime.split('/').at(-1)) || entry !== definition.entry)
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
script,
|
|
107
|
+
rewrite(next, path) {
|
|
108
|
+
const head = program[0];
|
|
109
|
+
const moved = head.replace(`<string>${program[2]}</string>`, `<string>${xml(next)}</string>`);
|
|
110
|
+
let out = text.replace(head, moved);
|
|
111
|
+
out = out.replace(/(<key>PATH<\/key>\s*<string>)([^<]*)(<\/string>)/, (_all, open, value, close) => `${open}${xml(unxml(value).split(':').map(path).join(':'))}${close}`);
|
|
112
|
+
return out;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** A systemd drop-in that sets ExecStart owns the command line; Fleet must not fight it. */
|
|
117
|
+
function overriddenByDropIn(path) {
|
|
118
|
+
const dir = `${path}.d`;
|
|
119
|
+
try {
|
|
120
|
+
return readdirSync(dir).some(name => name.endsWith('.conf')
|
|
121
|
+
&& /^\s*ExecStart\s*=/m.test(readFileSync(join(dir, name), 'utf8')));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const procArgv = (pid) => {
|
|
128
|
+
try {
|
|
129
|
+
return readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
async function runningState(definition, options, platform) {
|
|
136
|
+
if (platform !== 'linux' || !definition.unit)
|
|
137
|
+
return 'unknown';
|
|
138
|
+
const exec = options.exec ?? realExec;
|
|
139
|
+
const shown = await exec('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', definition.unit]);
|
|
140
|
+
if (shown.code !== 0)
|
|
141
|
+
return 'unknown';
|
|
142
|
+
const pid = Number(shown.stdout.trim());
|
|
143
|
+
if (!Number.isSafeInteger(pid) || pid < 0)
|
|
144
|
+
return 'unknown';
|
|
145
|
+
if (pid === 0)
|
|
146
|
+
return 'stopped';
|
|
147
|
+
const argv = (options.processArgv ?? procArgv)(pid);
|
|
148
|
+
const script = argv?.[1];
|
|
149
|
+
if (!script)
|
|
150
|
+
return 'unknown';
|
|
151
|
+
// A stable launcher resolves to whichever release it pointed at when started.
|
|
152
|
+
if (!releaseRoot(script))
|
|
153
|
+
return 'unknown';
|
|
154
|
+
return script === options.binPath ? 'current' : 'stale';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether systemd still runs an older load of this unit than the file on disk.
|
|
158
|
+
* A rewrite whose daemon-reload failed looks current on the next pass; this is
|
|
159
|
+
* what keeps that pending reload visible instead of losing it.
|
|
160
|
+
*/
|
|
161
|
+
async function reloadPending(definition, options, platform) {
|
|
162
|
+
if (platform !== 'linux' || !definition.unit)
|
|
163
|
+
return false;
|
|
164
|
+
const shown = await (options.exec ?? realExec)('systemctl', ['--user', 'show', '-p', 'NeedDaemonReload', '--value', definition.unit]);
|
|
165
|
+
return shown.code === 0 && shown.stdout.trim() === 'yes';
|
|
166
|
+
}
|
|
167
|
+
/** Report, and with `write` refresh, each installed Fleet service definition. */
|
|
168
|
+
export async function refreshServiceRegistrations(options) {
|
|
169
|
+
const platform = options.platform ?? process.platform;
|
|
170
|
+
const homeDir = options.homeDir ?? home();
|
|
171
|
+
const exists = options.exists ?? existsSync;
|
|
172
|
+
const target = releaseRoot(options.binPath);
|
|
173
|
+
const reports = [];
|
|
174
|
+
let rewrote = false;
|
|
175
|
+
for (const definition of definitions(platform, homeDir)) {
|
|
176
|
+
const base = { service: definition.service, path: definition.path };
|
|
177
|
+
if (!exists(definition.path)) {
|
|
178
|
+
reports.push({ ...base, state: 'absent', running: 'stopped' });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const running = await runningState(definition, options, platform);
|
|
182
|
+
const text = readFileSync(definition.path, 'utf8');
|
|
183
|
+
const generated = parseGenerated(text, platform, definition);
|
|
184
|
+
if (!generated) {
|
|
185
|
+
reports.push({ ...base, state: 'unrecognized', running,
|
|
186
|
+
detail: 'not the definition Fleet generates for this service; left untouched' });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const from = releaseRoot(generated.script);
|
|
190
|
+
if (!from || generated.script === options.binPath) {
|
|
191
|
+
if (await reloadPending(definition, options, platform)) {
|
|
192
|
+
// Normally an earlier refresh whose daemon-reload failed: the file is
|
|
193
|
+
// right, the loaded unit is not. Reload now, or keep reporting drift.
|
|
194
|
+
if (options.write) {
|
|
195
|
+
rewrote = true;
|
|
196
|
+
reports.push({ ...base, state: 'refreshed', running, ...(from ? { from } : {}),
|
|
197
|
+
detail: 'definition on disk was not loaded yet; reloaded' });
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
reports.push({ ...base, state: 'reload-failed', running, ...(from ? { from } : {}),
|
|
201
|
+
detail: 'systemd has not loaded the definition on disk; run systemctl --user daemon-reload' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
reports.push({ ...base, state: 'current', running, ...(from ? { from } : {}) });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!target) {
|
|
208
|
+
reports.push({ ...base, state: 'stale', running, from,
|
|
209
|
+
detail: 'the invoking CLI is not a packaged release; nothing to point the definition at' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (platform === 'linux' && overriddenByDropIn(definition.path)) {
|
|
213
|
+
reports.push({ ...base, state: 'unrecognized', running, from,
|
|
214
|
+
detail: `a drop-in in ${definition.path}.d sets ExecStart; left untouched` });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!options.write) {
|
|
218
|
+
reports.push({ ...base, state: 'stale', running, from, to: target });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Only bundled tool directories on PATH follow the release; every other
|
|
222
|
+
// setting, including a profile or home stored under a release tree, stays.
|
|
223
|
+
const kept = [];
|
|
224
|
+
const next = generated.rewrite(options.binPath, entry => {
|
|
225
|
+
if (!entry.startsWith(from + '/'))
|
|
226
|
+
return entry;
|
|
227
|
+
const moved = target + entry.slice(from.length);
|
|
228
|
+
if (exists(moved))
|
|
229
|
+
return moved;
|
|
230
|
+
kept.push(entry);
|
|
231
|
+
return entry;
|
|
232
|
+
});
|
|
233
|
+
if (parseGenerated(next, platform, definition)?.script !== options.binPath) {
|
|
234
|
+
reports.push({ ...base, state: 'unrecognized', running, from,
|
|
235
|
+
detail: 'the rewritten definition would not launch the invoking release; left untouched' });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
replaceFileAtomically(definition.path, next, statSync(definition.path).mode & 0o777);
|
|
239
|
+
rewrote = true;
|
|
240
|
+
reports.push({ ...base, state: 'refreshed', running, from, to: target,
|
|
241
|
+
...(kept.length ? { detail: `kept PATH entries absent from the new release: ${kept.join(', ')}` } : {}) });
|
|
242
|
+
}
|
|
243
|
+
if (rewrote && platform === 'linux') {
|
|
244
|
+
const reload = await (options.exec ?? realExec)('systemctl', ['--user', 'daemon-reload']);
|
|
245
|
+
// The file is rewritten, but systemd keeps the loaded unit until a reload,
|
|
246
|
+
// so a failed reload is not a refresh.
|
|
247
|
+
if (reload.code !== 0)
|
|
248
|
+
for (const report of reports)
|
|
249
|
+
if (report.state === 'refreshed') {
|
|
250
|
+
report.state = 'reload-failed';
|
|
251
|
+
report.detail = [report.detail, `systemctl --user daemon-reload failed: ${reload.stderr.trim() || `exit ${reload.code}`}; `
|
|
252
|
+
+ 'run it, then restart the service when convenient'].filter(Boolean).join('; ');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return reports;
|
|
256
|
+
}
|
|
257
|
+
/** One operator line per definition, including the restart a refresh cannot do. */
|
|
258
|
+
export function describeRegistration(report) {
|
|
259
|
+
const what = `${report.service.padEnd(10)} ${report.state}`;
|
|
260
|
+
const pins = report.from ? ` ${report.from}${report.to ? ` -> ${report.to}` : ''}` : '';
|
|
261
|
+
const restart = report.running === 'stale'
|
|
262
|
+
? `; the running process still uses ${report.state === 'refreshed' ? 'the previous' : 'another'} release until restarted`
|
|
263
|
+
: '';
|
|
264
|
+
return `${what}${pins}${restart}${report.detail ? ` (${report.detail})` : ''}`;
|
|
265
|
+
}
|
|
266
|
+
export const registrationDrift = (reports) => reports.filter(report => report.state === 'stale' || report.state === 'reload-failed' || report.running === 'stale');
|
|
@@ -4,8 +4,9 @@ export interface WorkspacePayload {
|
|
|
4
4
|
appOrigin: string;
|
|
5
5
|
hostname: string;
|
|
6
6
|
rootName: string;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
fullName?: string | null;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
surname?: string | null;
|
|
9
10
|
connectorToken?: string;
|
|
10
11
|
invitation: string;
|
|
11
12
|
serverCid: string;
|
|
@@ -25,14 +25,24 @@ export function decodeWorkspacePayload(input, options = {}) {
|
|
|
25
25
|
catch {
|
|
26
26
|
throw Error('Invalid workspace payload');
|
|
27
27
|
}
|
|
28
|
-
if (p.version !== 1 || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(p.appOrigin) || !/^[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(p.hostname) || !/^[a-z0-9-]{2,30}@[a-z0-9-]{2,30}$/.test(p.rootName) || !/^[a-f0-9]{64}$/i.test(p.serverCid) || !p.challenge || !['nonce', 'accountId', 'workspaceId'].every(k => /^[\w-]{43}$/.test(String(p.challenge[k]))) || (!options.allowExpired && p.challenge.expiresAt <= Date.now()) || !Number.isSafeInteger(p.challenge.expiresAt) || p.challenge.expiresAt > Date.now() + 16 * 60000)
|
|
28
|
+
if (!p || typeof p !== 'object' || Array.isArray(p) || p.version !== 1 || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(p.appOrigin) || !/^[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(p.hostname) || !/^[a-z0-9-]{2,30}@[a-z0-9-]{2,30}$/.test(p.rootName) || !/^[a-f0-9]{64}$/i.test(p.serverCid) || !p.challenge || !['nonce', 'accountId', 'workspaceId'].every(k => /^[\w-]{43}$/.test(String(p.challenge[k]))) || (!options.allowExpired && p.challenge.expiresAt <= Date.now()) || !Number.isSafeInteger(p.challenge.expiresAt) || p.challenge.expiresAt > Date.now() + 16 * 60000)
|
|
29
29
|
throw Error('Workspace payload is invalid or expired');
|
|
30
|
-
for (const k of ['connectorToken', 'invitation'
|
|
30
|
+
for (const k of ['connectorToken', 'invitation']) {
|
|
31
31
|
if (k === 'connectorToken' && p[k] === undefined)
|
|
32
32
|
continue;
|
|
33
33
|
if (typeof p[k] !== 'string' || !p[k] || p[k].length > 8192 || /[\x00-\x1f\x7f]/.test(p[k]))
|
|
34
34
|
throw Error('Invalid workspace payload');
|
|
35
35
|
}
|
|
36
|
+
for (const k of ['fullName', 'name', 'surname']) {
|
|
37
|
+
const value = p[k];
|
|
38
|
+
if (value == null || value === '') {
|
|
39
|
+
p[k] = null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== 'string' || value.length > 8192 || /[\x00-\x1f\x7f]/.test(value))
|
|
43
|
+
throw Error('Invalid workspace profile');
|
|
44
|
+
p[k] = value.trim() || null;
|
|
45
|
+
}
|
|
36
46
|
return p;
|
|
37
47
|
}
|
|
38
48
|
/** Credential-free renewal is allowed only on the host already holding this private connector. */
|
|
@@ -120,11 +130,11 @@ const CID = /^[a-f0-9]{64}$/i, same = (a, b) => a.toLowerCase() === b.toLowerCas
|
|
|
120
130
|
async function messengerIdentity(request, attach) {
|
|
121
131
|
const bound = await request('workspace/enrollment-identity');
|
|
122
132
|
if (bound.rootCid === undefined)
|
|
123
|
-
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true };
|
|
133
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, optionalWorkspaceProfileV1: bound.optionalWorkspaceProfileV1 === true };
|
|
124
134
|
if (typeof bound.cid !== 'string' || !CID.test(bound.cid) || typeof bound.rootCid !== 'string' || !CID.test(bound.rootCid) || same(bound.cid, bound.rootCid))
|
|
125
135
|
throw Error('Bound Messenger identity unavailable');
|
|
126
136
|
const root = await humanRoot(attach, bound.rootCid, bound.cid);
|
|
127
|
-
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, root };
|
|
137
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, optionalWorkspaceProfileV1: bound.optionalWorkspaceProfileV1 === true, root };
|
|
128
138
|
}
|
|
129
139
|
/** This host's only Human root, as its daemon lists it; optionally with a permanent identity that must be listed under it. */
|
|
130
140
|
async function humanRoot(attach, rootCid, under) {
|
|
@@ -280,6 +290,8 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
280
290
|
&& (options.replacingWorkspace !== undefined || JSON.parse(readFileSync(previousFile, 'utf8')).serverCid?.toUpperCase() === payload.serverCid.toUpperCase());
|
|
281
291
|
if (options.preserveProfile && identity.preserveProfile !== true)
|
|
282
292
|
throw Error('Existing-host setup requires Messenger profile-preservation support; update the host Messenger before retrying');
|
|
293
|
+
if (!options.preserveProfile && (payload.fullName?.trim() || !payload.name?.trim() || !payload.surname?.trim()) && !identity.optionalWorkspaceProfileV1)
|
|
294
|
+
throw Error('Optional workspace profile requires updated Messenger; update the host Messenger before retrying (the host is not enrolled)');
|
|
283
295
|
if (loaded.rooms && !lost && !moved && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
284
296
|
throw Error('Retained room owner CID conflicts with the Human root');
|
|
285
297
|
const inviteFile = loaded.rooms?.owner.public_invite_file || join(dir, 'owner.invite');
|
|
@@ -291,7 +303,7 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
291
303
|
const devices = new WorkspaceDeviceStore();
|
|
292
304
|
const hostWorkspaceId = devices.workspaceId;
|
|
293
305
|
devices.close();
|
|
294
|
-
const { name, surname, ...proofPayload } = payload;
|
|
306
|
+
const { fullName, name, surname, ...proofPayload } = payload;
|
|
295
307
|
let result, ownerCid;
|
|
296
308
|
// A replacement can stop after its one-time proof but before local files commit. Read the
|
|
297
309
|
// account's existing signed receipt first; a verified proof must not be sent a second time.
|
|
@@ -316,6 +328,8 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
316
328
|
}
|
|
317
329
|
}
|
|
318
330
|
if (identity.root) {
|
|
331
|
+
if (!options.preserveProfile)
|
|
332
|
+
await request('identity/profile', { fullName: fullName ?? null, name: name ?? null, surname: surname ?? null });
|
|
319
333
|
// The Human root signs the binding; the person's own Messenger identity is the room Owner and issues the Owner invitation.
|
|
320
334
|
if (!proofAlreadyConfirmed)
|
|
321
335
|
await proveWithHumanRoot(attach, identity.root, payload, hostWorkspaceId, options.contactWaitMs ?? 10000, options.replacingWorkspace !== undefined);
|
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.68",
|
|
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",
|