@ours.network/fleet 1.2.0-nightly.37 → 1.2.0-nightly.38
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/build-info.json +4 -4
- package/dist/workspace-enrollment.d.ts +11 -3
- package/dist/workspace-enrollment.js +157 -24
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.38",
|
|
3
|
+
"buildId": "2eaafbd42c9d",
|
|
4
|
+
"commit": "86c7af960f9b2cfdbf4dcddee25bc7303e78780c",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-02T21:
|
|
6
|
+
"builtAt": "2026-10-02T21:39:35.791Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"workspace.enroll.preserve-profile-v1",
|
|
9
9
|
"cowork.http-management-v1",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
|
|
1
2
|
export interface WorkspacePayload {
|
|
2
3
|
version: 1;
|
|
3
4
|
appOrigin: string;
|
|
@@ -16,18 +17,24 @@ export interface WorkspacePayload {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export declare function readWorkspacePayload(file: string): WorkspacePayload;
|
|
20
|
+
type DaemonClient = Pick<OursClient, 'listIdentities' | 'chooseIdentity' | 'addContact' | 'listContacts' | 'sendCommand' | 'removeContact' | 'releaseLease' | 'close'>;
|
|
21
|
+
/** How tunnel setup reaches this host's daemon; replaced only by tests. */
|
|
22
|
+
export type AttachDaemonClient = (options: AttachOursClientOptions) => Promise<DaemonClient>;
|
|
19
23
|
/**
|
|
20
24
|
* Check the Fleet configuration tunnel setup will use, before the one-time command is used.
|
|
21
25
|
* Reads only; an existing Owner is compared with this host's own Messenger identity.
|
|
22
26
|
*/
|
|
23
|
-
export declare function checkWorkspaceConfiguration(configuration?: string): Promise<void>;
|
|
27
|
+
export declare function checkWorkspaceConfiguration(configuration?: string, attach?: AttachDaemonClient): Promise<void>;
|
|
24
28
|
export declare function enrollWorkspace(payload: WorkspacePayload, configuration?: string, options?: {
|
|
25
29
|
migrateAppOrigin?: boolean;
|
|
26
30
|
preserveProfile?: boolean;
|
|
31
|
+
attach?: AttachDaemonClient;
|
|
32
|
+
contactWaitMs?: number;
|
|
33
|
+
afterInvitation?: () => void;
|
|
27
34
|
}): Promise<{
|
|
28
35
|
origin: string;
|
|
29
36
|
hostWorkspaceId: string;
|
|
30
|
-
rootCid:
|
|
37
|
+
rootCid: string;
|
|
31
38
|
}>;
|
|
32
39
|
/** Only the existing signed-root receipt authorizes the scoped tunnel target. */
|
|
33
40
|
/**
|
|
@@ -35,7 +42,7 @@ export declare function enrollWorkspace(payload: WorkspacePayload, configuration
|
|
|
35
42
|
* that binding, remove exactly that contact from this host's Messenger, so it does not stay in the
|
|
36
43
|
* person's chats. The server identity is the one recorded with the binding; nothing else is touched.
|
|
37
44
|
*/
|
|
38
|
-
export declare function removeEnrollmentContact(): Promise<boolean>;
|
|
45
|
+
export declare function removeEnrollmentContact(attach?: AttachDaemonClient): Promise<boolean>;
|
|
39
46
|
export declare function configureWorkspacePort(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number): Promise<void>;
|
|
40
47
|
/**
|
|
41
48
|
* Point the tunnel at this host's port. The account answers only for a confirmed signed binding, and
|
|
@@ -43,3 +50,4 @@ export declare function configureWorkspacePort(payload: Pick<WorkspacePayload, '
|
|
|
43
50
|
* so the setup can be finished later. A removal that fails is reported and does not undo the setup.
|
|
44
51
|
*/
|
|
45
52
|
export declare function confirmWorkspaceTarget(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number, cleanupFailed?: (error: unknown) => void): Promise<void>;
|
|
53
|
+
export {};
|
|
@@ -5,7 +5,9 @@ import { readClientProfile } from './client-profile.js';
|
|
|
5
5
|
import { stateRoot, defaultConfigPath, workspaceOwnerInvite } from './paths.js';
|
|
6
6
|
import { replaceFileAtomically } from './atomic-file.js';
|
|
7
7
|
import { loadConfig, splitRootFor } from './config.js';
|
|
8
|
-
import { parse, stringify } from 'yaml';
|
|
8
|
+
import { parse, parseDocument, stringify } from 'yaml';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
9
11
|
import { preflightInitPaths } from './init-wizard.js';
|
|
10
12
|
import { WebAccessStore } from './web/access.js';
|
|
11
13
|
import { WorkspaceDeviceStore } from './web/workspace-devices.js';
|
|
@@ -71,14 +73,105 @@ function hostMessenger() {
|
|
|
71
73
|
const secret = readFileSync(profile.credentialPath, 'utf8').trim();
|
|
72
74
|
return async (path, value) => { const response = await fetch(new URL('api/' + path, base), { method: value === undefined ? 'GET' : 'POST', headers: { 'X-Ours-Api-Token': secret, ...(value === undefined ? {} : { 'Content-Type': 'application/json', 'Origin': base.origin, 'X-Ours-Messenger-CSRF': '1' }) }, ...(value === undefined ? {} : { body: JSON.stringify(value) }), signal: AbortSignal.timeout(value === undefined ? 15000 : 60000), redirect: 'error' }); if (!response.ok) {
|
|
73
75
|
await response.body?.cancel();
|
|
74
|
-
throw Error('Workspace Messenger enrollment failed (HTTP ' + response.status + '); request a fresh setup payload before retrying');
|
|
76
|
+
throw Object.assign(Error('Workspace Messenger enrollment failed (HTTP ' + response.status + '); request a fresh setup payload before retrying'), { status: response.status });
|
|
75
77
|
} return response.json(); };
|
|
76
78
|
}
|
|
79
|
+
const daemon = (attach, purpose) => { const profile = readClientProfile(); return attach({ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId, credentialPath: profile.credentialPath, sessionMode: 'external', leaseToken: `ours-fleet-workspace-${purpose}-${process.pid}-${randomUUID()}`, env: {} }); };
|
|
80
|
+
const CID = /^[a-f0-9]{64}$/i, same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
81
|
+
/**
|
|
82
|
+
* Who this host's Messenger is. Messenger either runs as the Human root itself, or as the person's own
|
|
83
|
+
* identity and then names the root the daemon describes for it. That is accepted only when this host's
|
|
84
|
+
* daemon, asked by Fleet itself, lists exactly one Human root, that root is the named one, and the
|
|
85
|
+
* Messenger identity is listed as a permanent identity under it.
|
|
86
|
+
*/
|
|
87
|
+
async function messengerIdentity(request, attach) {
|
|
88
|
+
const bound = await request('workspace/enrollment-identity');
|
|
89
|
+
if (bound.rootCid === undefined)
|
|
90
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true };
|
|
91
|
+
if (typeof bound.cid !== 'string' || !CID.test(bound.cid) || typeof bound.rootCid !== 'string' || !CID.test(bound.rootCid) || same(bound.cid, bound.rootCid))
|
|
92
|
+
throw Error('Bound Messenger identity unavailable');
|
|
93
|
+
const root = await humanRoot(attach, bound.rootCid, bound.cid);
|
|
94
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, root };
|
|
95
|
+
}
|
|
96
|
+
/** This host's only Human root, as its daemon lists it; optionally with a permanent identity that must be listed under it. */
|
|
97
|
+
async function humanRoot(attach, rootCid, under) {
|
|
98
|
+
const client = await daemon(attach, 'identities');
|
|
99
|
+
const rows = await client.listIdentities().finally(() => client.close());
|
|
100
|
+
const roots = rows.filter(row => 'kind' in row && row.kind === 'root'), root = roots[0], row = under === undefined ? undefined : rows.find(row => 'cid' in row && same(row.cid, under));
|
|
101
|
+
if (roots.length !== 1 || !('kind' in root) || !same(root.cid, rootCid) || (under !== undefined && (!row || !('kind' in row) || row.kind !== 'role' || row.temp !== null)))
|
|
102
|
+
throw Error("This host's Messenger identity is not a permanent identity under this host's Human root. Tunnel setup changed nothing.");
|
|
103
|
+
return { name: root.name, cid: root.cid };
|
|
104
|
+
}
|
|
105
|
+
/** Run one operation as the Human root and always give the root back, whatever happens. */
|
|
106
|
+
async function asHumanRoot(attach, root, purpose, use) {
|
|
107
|
+
const client = await daemon(attach, purpose);
|
|
108
|
+
let done = false;
|
|
109
|
+
try {
|
|
110
|
+
const bound = await client.chooseIdentity({ name: root.name, force: false }).catch((error) => {
|
|
111
|
+
throw Error(`This host's Human root cannot be used right now (${error.code ?? error.message}). Close the session that holds it, then run setup again. Nothing was changed.`);
|
|
112
|
+
});
|
|
113
|
+
if (!same(bound.cid, root.cid))
|
|
114
|
+
throw Error("This host's Human root changed while setup was running. Nothing was changed.");
|
|
115
|
+
const result = await use(client);
|
|
116
|
+
// The operation is not reported as done while the root may still be held. The release is attempted exactly once.
|
|
117
|
+
done = true;
|
|
118
|
+
const released = await client.releaseLease();
|
|
119
|
+
if (released.failed > 0)
|
|
120
|
+
throw Error("This host's Human root was not given back completely. Run setup again.");
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
// After a failure the root is released once as well; what failed first is what is reported.
|
|
125
|
+
if (!done)
|
|
126
|
+
await client.releaseLease().catch(() => { });
|
|
127
|
+
await client.close().catch(() => { });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** The signed workspace binding, sent by the Human root itself: the same command Messenger sends when it runs as the root. */
|
|
131
|
+
async function proveWithHumanRoot(attach, root, payload, hostWorkspaceId, contactWaitMs) {
|
|
132
|
+
const serverCid = payload.serverCid.toUpperCase();
|
|
133
|
+
await asHumanRoot(attach, root, 'proof', async (client) => {
|
|
134
|
+
const peer = await client.addContact({ invite: payload.invitation });
|
|
135
|
+
if (peer.cid.toUpperCase() !== serverCid)
|
|
136
|
+
throw Error('Enrollment server identity mismatch');
|
|
137
|
+
const deadline = Date.now() + contactWaitMs;
|
|
138
|
+
while (!(await client.listContacts()).contacts.some(contact => contact.container_id.toUpperCase() === serverCid)) {
|
|
139
|
+
if (Date.now() >= deadline)
|
|
140
|
+
throw Error('Enrollment contact is not ready; obtain a fresh setup payload before retrying');
|
|
141
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
142
|
+
}
|
|
143
|
+
const outcome = await client.sendCommand({ contact: serverCid, command: 'bind-workspace', arguments: { type: 'ours.app.bind-workspace.v1', accountId: payload.challenge.accountId, workspaceId: payload.challenge.workspaceId, nonce: payload.challenge.nonce, hostWorkspaceId } });
|
|
144
|
+
if (!('sent' in outcome) || !outcome.sent)
|
|
145
|
+
throw Error('Workspace proof was not sent');
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* True when the Owner entry is the one tunnel setup manages and it still names the Human root of an
|
|
150
|
+
* installation whose Messenger now runs as the person's own identity under that root. Only then is the
|
|
151
|
+
* entry moved to that identity; an Owner entry anyone else wrote is never touched.
|
|
152
|
+
*/
|
|
153
|
+
function managedRootOwner(configuration, rooms, root) {
|
|
154
|
+
if (!same(rooms.owner.expected_cid, root.cid) || rooms.owner.public_invite_file !== workspaceOwnerInvite() || rooms.defaults?.attach_owner === false)
|
|
155
|
+
return false;
|
|
156
|
+
if (!existsSync(join(stateRoot(), 'workspace', 'binding.json')))
|
|
157
|
+
return false;
|
|
158
|
+
let written;
|
|
159
|
+
try {
|
|
160
|
+
written = parseDocument(readFileSync(configuration, 'utf8')).toJS()?.rooms;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const entry = written?.owner, defaults = written?.defaults, keys = (value) => Object.keys(value).sort().join();
|
|
166
|
+
// Exactly the Owner entry and defaults tunnel setup writes; anything added or changed beside them is someone's own configuration.
|
|
167
|
+
return !!written && keys(written) === 'defaults,owner' && !!entry && keys(entry) === 'expected_cid,provider,public_invite_file,role' && entry.provider === 'messenger-server' && entry.role === 'Owner' && typeof entry.expected_cid === 'string' && same(entry.expected_cid, root.cid)
|
|
168
|
+
&& !!defaults && keys(defaults) === 'attach_owner,close_when_task_done' && defaults.attach_owner === true && defaults.close_when_task_done === true;
|
|
169
|
+
}
|
|
77
170
|
/**
|
|
78
171
|
* Check the Fleet configuration tunnel setup will use, before the one-time command is used.
|
|
79
172
|
* Reads only; an existing Owner is compared with this host's own Messenger identity.
|
|
80
173
|
*/
|
|
81
|
-
export async function checkWorkspaceConfiguration(configuration = defaultConfigPath()) {
|
|
174
|
+
export async function checkWorkspaceConfiguration(configuration = defaultConfigPath(), attach = attachOursClient) {
|
|
82
175
|
const paths = preflightInitPaths(configuration);
|
|
83
176
|
if (!paths.manifestExisted) {
|
|
84
177
|
if (paths.rootExisted)
|
|
@@ -92,19 +185,20 @@ export async function checkWorkspaceConfiguration(configuration = defaultConfigP
|
|
|
92
185
|
throw Error(`${paths.configPath}: rooms.defaults.attach_owner is disabled; enable it before tunnel setup.`);
|
|
93
186
|
if (!loaded.rooms.owner.public_invite_file)
|
|
94
187
|
throw Error(`${paths.configPath}: rooms.owner must use public_invite_file before tunnel setup.`);
|
|
95
|
-
let cid;
|
|
188
|
+
let cid, root;
|
|
96
189
|
try {
|
|
97
|
-
cid =
|
|
190
|
+
({ cid, root } = await messengerIdentity(hostMessenger(), attach));
|
|
98
191
|
}
|
|
99
192
|
catch {
|
|
100
193
|
cid = undefined;
|
|
101
194
|
}
|
|
102
195
|
if (typeof cid !== 'string' || !/^[a-f0-9]{64}$/i.test(cid))
|
|
103
196
|
throw Error(`${paths.configPath} names a room Owner, and this host's Messenger identity cannot be read to compare with it. Start Messenger on this host first.`);
|
|
104
|
-
if (loaded.rooms.owner.expected_cid.toLowerCase() !== cid.toLowerCase() && !recordsLostEnrollment(paths.configPath))
|
|
197
|
+
if (loaded.rooms.owner.expected_cid.toLowerCase() !== cid.toLowerCase() && !recordsLostEnrollment(paths.configPath) && !(root && managedRootOwner(paths.configPath, loaded.rooms, root)))
|
|
105
198
|
throw Error(`${paths.configPath}: rooms.owner.expected_cid names a different Owner than this host's Messenger identity. Tunnel setup does not replace an existing Owner.`);
|
|
106
199
|
}
|
|
107
200
|
export async function enrollWorkspace(payload, configuration = defaultConfigPath(), options = {}) {
|
|
201
|
+
const attach = options.attach ?? attachOursClient;
|
|
108
202
|
const request = hostMessenger();
|
|
109
203
|
if (new WebAccessStore().read().mode === 'none')
|
|
110
204
|
throw Error('Workspace enrollment requires protected web access; enable pairing or password before setup');
|
|
@@ -131,12 +225,15 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
131
225
|
throw Error('Existing rooms.defaults.attach_owner is disabled; enable automatic Human Owner admission before workspace setup');
|
|
132
226
|
if (loaded.rooms && !loaded.rooms.owner.public_invite_file)
|
|
133
227
|
throw Error('Existing room owner configuration must be reviewed before workspace enrollment');
|
|
134
|
-
const identity = await request
|
|
228
|
+
const identity = await messengerIdentity(request, attach);
|
|
135
229
|
if (!/^[a-f0-9]{64}$/i.test(identity.cid))
|
|
136
230
|
throw Error('Bound Messenger identity unavailable');
|
|
231
|
+
// An installation that ran Messenger as the Human root and now runs it as the person's own identity keeps its managed Owner entry, moved to that identity.
|
|
232
|
+
const moved = Boolean(loaded.rooms) && !lost && !!identity.root && !same(loaded.rooms.owner.expected_cid, identity.cid) && managedRootOwner(resolve(configuration), loaded.rooms, identity.root)
|
|
233
|
+
&& JSON.parse(readFileSync(previousFile, 'utf8')).serverCid?.toUpperCase() === payload.serverCid.toUpperCase();
|
|
137
234
|
if (options.preserveProfile && identity.preserveProfile !== true)
|
|
138
235
|
throw Error('Existing-host setup requires Messenger profile-preservation support; update the host Messenger before retrying');
|
|
139
|
-
if (loaded.rooms && !lost && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
236
|
+
if (loaded.rooms && !lost && !moved && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
140
237
|
throw Error('Retained room owner CID conflicts with the Human root');
|
|
141
238
|
const inviteFile = loaded.rooms?.owner.public_invite_file || join(dir, 'owner.invite');
|
|
142
239
|
if (existsSync(inviteFile)) {
|
|
@@ -148,14 +245,30 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
148
245
|
const hostWorkspaceId = devices.workspaceId;
|
|
149
246
|
devices.close();
|
|
150
247
|
const { name, surname, ...proofPayload } = payload;
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
248
|
+
let result, ownerCid;
|
|
249
|
+
if (identity.root) {
|
|
250
|
+
// The Human root signs the binding; the person's own Messenger identity is the room Owner and issues the Owner invitation.
|
|
251
|
+
await proveWithHumanRoot(attach, identity.root, payload, hostWorkspaceId, options.contactWaitMs ?? 10000);
|
|
252
|
+
const invite = await request('invites', { mode: 'public' });
|
|
253
|
+
if (typeof invite.blob !== 'string' || !invite.blob)
|
|
254
|
+
throw Error('Malformed workspace enrollment response');
|
|
255
|
+
const after = await request('workspace/enrollment-identity');
|
|
256
|
+
if (typeof after.cid !== 'string' || !same(after.cid, identity.cid) || typeof after.rootCid !== 'string' || !same(after.rootCid, identity.root.cid))
|
|
257
|
+
throw Error("This host's Messenger identity changed while setup was running; local configuration is unchanged");
|
|
258
|
+
result = { submitted: true, rootCid: identity.root.cid, ownerInvite: invite.blob };
|
|
259
|
+
ownerCid = identity.cid;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
result = await request('workspace/enroll', options.preserveProfile ? { ...proofPayload, hostWorkspaceId, preserveProfile: true } : { ...payload, hostWorkspaceId });
|
|
263
|
+
if (!result.submitted || !/^[a-f0-9]{64}$/i.test(result.rootCid) || typeof result.ownerInvite !== 'string')
|
|
264
|
+
throw Error('Malformed workspace enrollment response');
|
|
265
|
+
// Whatever the configuration said before, what is recorded from here on is this host's own root.
|
|
266
|
+
if (result.rootCid.toLowerCase() !== identity.cid.toLowerCase())
|
|
267
|
+
throw Error('Workspace enrollment answered for a different root than this host identity; local configuration is unchanged');
|
|
268
|
+
if (loaded.rooms && !lost && loaded.rooms.owner.expected_cid.toLowerCase() !== result.rootCid.toLowerCase())
|
|
269
|
+
throw Error('Retained room owner CID conflicts with the Human root');
|
|
270
|
+
ownerCid = result.rootCid;
|
|
271
|
+
}
|
|
159
272
|
if (migrating) {
|
|
160
273
|
const deadline = Date.now() + 15000;
|
|
161
274
|
while (true) {
|
|
@@ -173,16 +286,24 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
173
286
|
}
|
|
174
287
|
}
|
|
175
288
|
replaceFileAtomically(inviteFile, result.ownerInvite + '\n', 0o600);
|
|
289
|
+
options.afterInvitation?.();
|
|
176
290
|
const split = splitRootFor(configuration);
|
|
177
291
|
mkdirSync(split, { recursive: true, mode: 0o700 });
|
|
178
292
|
if (!loaded.rooms)
|
|
179
|
-
replaceFileAtomically(configuration, readFileSync(configuration, 'utf8') + '\n' + stringify(generatedRooms(
|
|
180
|
-
else if (lost && loaded.rooms.owner.expected_cid.toLowerCase() !==
|
|
181
|
-
replaceFileAtomically(configuration, `api_version: ${GENERATED_VERSION}\n\n` + stringify(generatedRooms(
|
|
293
|
+
replaceFileAtomically(configuration, readFileSync(configuration, 'utf8') + '\n' + stringify(generatedRooms(ownerCid, inviteFile)), 0o600);
|
|
294
|
+
else if (lost && loaded.rooms.owner.expected_cid.toLowerCase() !== ownerCid.toLowerCase())
|
|
295
|
+
replaceFileAtomically(configuration, `api_version: ${GENERATED_VERSION}\n\n` + stringify(generatedRooms(ownerCid, inviteFile)), 0o600);
|
|
296
|
+
else if (moved) {
|
|
297
|
+
// The invitation above and this entry are two files. A setup interrupted between them leaves the entry naming the root,
|
|
298
|
+
// which is this same case again: running setup once more writes both.
|
|
299
|
+
const document = parseDocument(readFileSync(configuration, 'utf8'));
|
|
300
|
+
document.setIn(['rooms', 'owner', 'expected_cid'], ownerCid);
|
|
301
|
+
replaceFileAtomically(configuration, document.toString(), 0o600);
|
|
302
|
+
}
|
|
182
303
|
replaceFileAtomically(join(dir, 'connector'), payload.connectorToken + '\n', 0o600);
|
|
183
304
|
const origin = `https://${payload.hostname}`;
|
|
184
305
|
replaceFileAtomically(join(dir, 'tunnel.json'), JSON.stringify({ origin, tokenFile: join(dir, 'connector') }) + '\n', 0o600);
|
|
185
|
-
replaceFileAtomically(previousFile, JSON.stringify({ workspaceId: payload.challenge.workspaceId, hostWorkspaceId, appOrigin, serverCid: payload.serverCid.toUpperCase() }) + '\n', 0o600);
|
|
306
|
+
replaceFileAtomically(previousFile, JSON.stringify({ workspaceId: payload.challenge.workspaceId, hostWorkspaceId, appOrigin, serverCid: payload.serverCid.toUpperCase(), ...(identity.root ? { proofRootCid: identity.root.cid } : {}) }) + '\n', 0o600);
|
|
186
307
|
loadConfig(configuration);
|
|
187
308
|
return { origin, hostWorkspaceId, rootCid: result.rootCid };
|
|
188
309
|
}
|
|
@@ -192,18 +313,30 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
192
313
|
* that binding, remove exactly that contact from this host's Messenger, so it does not stay in the
|
|
193
314
|
* person's chats. The server identity is the one recorded with the binding; nothing else is touched.
|
|
194
315
|
*/
|
|
195
|
-
export async function removeEnrollmentContact() {
|
|
196
|
-
let serverCid;
|
|
316
|
+
export async function removeEnrollmentContact(attach = attachOursClient) {
|
|
317
|
+
let serverCid, proofRootCid;
|
|
197
318
|
try {
|
|
198
|
-
serverCid = JSON.parse(readFileSync(join(stateRoot(), 'workspace', 'binding.json'), 'utf8'))
|
|
319
|
+
({ serverCid, proofRootCid } = JSON.parse(readFileSync(join(stateRoot(), 'workspace', 'binding.json'), 'utf8')));
|
|
199
320
|
}
|
|
200
321
|
catch {
|
|
201
322
|
return false;
|
|
202
323
|
}
|
|
203
324
|
if (typeof serverCid !== 'string' || !/^[A-F0-9]{64}$/i.test(serverCid))
|
|
204
325
|
return false;
|
|
326
|
+
const contact = serverCid.toUpperCase();
|
|
327
|
+
// The contact belongs to whoever sent the binding. When that was the Human root itself, the binding records it.
|
|
328
|
+
if (typeof proofRootCid === 'string' && CID.test(proofRootCid)) {
|
|
329
|
+
try {
|
|
330
|
+
const root = await humanRoot(attach, proofRootCid);
|
|
331
|
+
await asHumanRoot(attach, root, 'cleanup', client => client.removeContact({ contact }));
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
throw Error('the Human root did not remove it');
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
205
338
|
try {
|
|
206
|
-
await hostMessenger()('contacts/remove', { contact
|
|
339
|
+
await hostMessenger()('contacts/remove', { contact });
|
|
207
340
|
}
|
|
208
341
|
catch {
|
|
209
342
|
throw Error('Messenger did not remove it');
|
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.38",
|
|
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",
|