@ours.network/fleet 1.2.0-nightly.44 → 1.2.0-nightly.46

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.
@@ -10,6 +10,7 @@ export declare class FleetNotificationProducer {
10
10
  private readonly queues;
11
11
  private readonly timer;
12
12
  private readonly existing;
13
+ private cleanup?;
13
14
  constructor(roots: string[], stateDir: string, config: ProducerConfig, warn?: (line: string) => void);
14
15
  private names;
15
16
  poll(): void;
@@ -1,7 +1,42 @@
1
- import { closeSync, existsSync, lstatSync, openSync, readSync, readdirSync, statSync } from 'node:fs';
1
+ import { roomsDir } from '../rooms-tasks/room-state.js';
2
+ import { drainNotificationTargetCleanup } from './target-cleanup.js';
3
+ import { closeSync, existsSync, lstatSync, openSync, readSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
4
  import { join } from 'node:path';
3
5
  import { createHash } from 'node:crypto';
4
6
  import { NotificationOutbox } from './outbox.js';
7
+ /** Unlike a UI inventory, unread classification must not treat unreadable records as absence. */
8
+ function taskRoleAssociations() {
9
+ const dir = roomsDir(), result = new Map();
10
+ let files;
11
+ try {
12
+ files = readdirSync(dir);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return result;
17
+ throw error;
18
+ }
19
+ for (const name of files.filter(name => name.endsWith('.json'))) {
20
+ const file = join(dir, name), stat = lstatSync(file);
21
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
22
+ throw Error('Unreadable room association');
23
+ const room = JSON.parse(readFileSync(file, 'utf8'));
24
+ if (!room || typeof room.room_id !== 'string' || !Array.isArray(room.member_seats))
25
+ throw Error('Invalid room association');
26
+ if (room.task_id === undefined)
27
+ continue;
28
+ if (typeof room.task_id !== 'string' || !/^[A-Za-z0-9_-]{1,120}$/.test(room.task_id))
29
+ throw Error('Invalid task association');
30
+ for (const seat of room.member_seats) {
31
+ if (typeof seat.role_name !== 'string')
32
+ throw Error('Invalid member association');
33
+ if (result.has(seat.role_name) && result.get(seat.role_name) !== room.task_id)
34
+ throw Error('Ambiguous task association');
35
+ result.set(seat.role_name, room.task_id);
36
+ }
37
+ }
38
+ return result;
39
+ }
5
40
  /** Read durable supervisor ledgers without restarting agents or attaching a
6
41
  * conversation controller. Each tick reads at most 256 KiB per role, from its
7
42
  * checkpoint; it never loads a complete transcript or requests model history. */
@@ -13,6 +48,7 @@ export class FleetNotificationProducer {
13
48
  queues = new Map();
14
49
  timer;
15
50
  existing = new Set();
51
+ cleanup;
16
52
  constructor(roots, stateDir, config, warn = () => { }) {
17
53
  this.roots = roots;
18
54
  this.stateDir = stateDir;
@@ -34,6 +70,49 @@ export class FleetNotificationProducer {
34
70
  }
35
71
  }
36
72
  poll() {
73
+ this.cleanup ??= drainNotificationTargetCleanup(this.config, undefined, undefined, { beforeDelete: async (url) => {
74
+ const name = new URL(url, 'https://ours.invalid').searchParams.get('chat');
75
+ if (!name || !/^[A-Za-z0-9_-]+$/.test(name))
76
+ return false;
77
+ // A recreated or retained role still has a valid target. Filesystem errors cannot prove removal.
78
+ for (const root of this.roots) {
79
+ try {
80
+ lstatSync(join(root, name));
81
+ return false;
82
+ }
83
+ catch (error) {
84
+ if (error.code !== 'ENOENT')
85
+ throw error;
86
+ }
87
+ }
88
+ for (const root of this.roots) {
89
+ const roleDir = join(root, name), queue = this.queues.get(roleDir);
90
+ if (queue) {
91
+ await queue.retire();
92
+ this.queues.delete(roleDir);
93
+ }
94
+ else {
95
+ const file = join(this.stateDir, createHash('sha256').update(roleDir).digest('hex') + '.json');
96
+ if (existsSync(file)) {
97
+ const queue = new NotificationOutbox(file, this.config, async () => { throw Error('retired target'); });
98
+ try {
99
+ await queue.retire();
100
+ }
101
+ finally {
102
+ await queue.close();
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return true;
108
+ } }).catch(() => { }).finally(() => { this.cleanup = undefined; });
109
+ let taskRoles;
110
+ try {
111
+ taskRoles = taskRoleAssociations();
112
+ }
113
+ catch {
114
+ this.warn('Notification task association unavailable; delivering unclassified events');
115
+ }
37
116
  for (const root of this.roots)
38
117
  for (const roleId of this.names(root)) {
39
118
  const roleDir = join(root, roleId), dir = join(roleDir, '.conversation');
@@ -108,7 +187,7 @@ export class FleetNotificationProducer {
108
187
  if (['message.chunk', 'message.replace'].includes(event.kind) && event.promptId && event.messageId && current.prompts.includes(prompt))
109
188
  current.messages[prompt] = event.messageId;
110
189
  if (event.kind === 'turn.completed' && event.promptId && current.prompts.includes(prompt)) {
111
- if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { url: `/fleet/chats?chat=${encodeURIComponent(roleId)}&detail=1#fleet-message-${encodeURIComponent(current.messages[prompt] ?? event.promptId)}` }))
190
+ if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { ...(taskRoles ? { taskId: taskRoles.get(roleId) ?? null } : {}), url: `/fleet/chats?chat=${encodeURIComponent(roleId)}&detail=1#fleet-message-${encodeURIComponent(current.messages[prompt] ?? event.promptId)}` }))
112
191
  break;
113
192
  current.prompts = current.prompts.filter(p => p !== prompt);
114
193
  delete current.messages[prompt];
@@ -125,5 +204,5 @@ export class FleetNotificationProducer {
125
204
  }
126
205
  }
127
206
  async drain() { await Promise.all([...this.queues.values()].map(q => q.drain())); }
128
- async close() { clearInterval(this.timer); await Promise.all([...this.queues.values()].map(q => q.close())); }
207
+ async close() { clearInterval(this.timer); await this.cleanup; await Promise.all([...this.queues.values()].map(q => q.close())); }
129
208
  }
@@ -3,6 +3,7 @@ export interface NotificationInput {
3
3
  title: string;
4
4
  body: string;
5
5
  url: string;
6
+ taskId?: string | null;
6
7
  }
7
8
  export interface ProducerConfig {
8
9
  /** Service base URL. With a gateway credential it is the server gateway's notifications route. */
@@ -29,6 +30,7 @@ export declare class NotificationOutbox {
29
30
  cursor?: number;
30
31
  checkpoint?: unknown;
31
32
  private running?;
33
+ private retired;
32
34
  private timer;
33
35
  constructor(file: string, config: ProducerConfig, project: (value: unknown, id: string) => Promise<NotificationInput>, warn?: (message: string) => void);
34
36
  private persist;
@@ -37,6 +39,8 @@ export declare class NotificationOutbox {
37
39
  enqueue(id: string, value: unknown, cursor?: number | undefined): boolean;
38
40
  drain(): Promise<void>;
39
41
  private drainOnce;
42
+ /** Wait for the current send before clearing queued sends and retiring its target. */
43
+ retire(): Promise<void>;
40
44
  close(): Promise<void>;
41
45
  }
42
46
  export {};
@@ -66,6 +66,7 @@ export class NotificationOutbox {
66
66
  cursor;
67
67
  checkpoint;
68
68
  running;
69
+ retired = false;
69
70
  timer;
70
71
  constructor(file, config, project, warn = () => { }) {
71
72
  this.file = file;
@@ -106,6 +107,8 @@ export class NotificationOutbox {
106
107
  advance(cursor) { this.persist(this.entries, cursor); }
107
108
  saveCheckpoint(checkpoint) { this.persist(this.entries, this.cursor, checkpoint); }
108
109
  enqueue(id, value, cursor = this.cursor) {
110
+ if (this.retired)
111
+ return false;
109
112
  if (this.entries.some(e => e.id === id)) {
110
113
  if (cursor !== this.cursor)
111
114
  this.advance(cursor);
@@ -117,10 +120,14 @@ export class NotificationOutbox {
117
120
  return true;
118
121
  }
119
122
  drain() {
123
+ if (this.retired)
124
+ return Promise.resolve();
120
125
  return this.running ??= this.drainOnce().finally(() => { this.running = undefined; });
121
126
  }
122
127
  async drainOnce() {
123
128
  for (const entry of [...this.entries].slice(0, 32)) {
129
+ if (this.retired)
130
+ break;
124
131
  try {
125
132
  const value = entry.payload ?? await this.project(entry.value, entry.id);
126
133
  // Freeze the canonical projection before any network attempt. A crash
@@ -145,5 +152,7 @@ export class NotificationOutbox {
145
152
  }
146
153
  }
147
154
  }
155
+ /** Wait for the current send before clearing queued sends and retiring its target. */
156
+ async retire() { this.retired = true; clearInterval(this.timer); await this.running; this.persist([]); }
148
157
  async close() { clearInterval(this.timer); await this.running; }
149
158
  }
@@ -0,0 +1,8 @@
1
+ import { type ProducerConfig } from './outbox.js';
2
+ /** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
3
+ export declare function retireNotificationTarget(url: string, config?: ProducerConfig | undefined, dir?: string, request?: typeof fetch): Promise<void>;
4
+ /** Persisted retries also run from the ordinary web notification producer after restart. */
5
+ export declare function drainNotificationTargetCleanup(config: ProducerConfig, dir?: string, request?: typeof fetch, options?: {
6
+ retain?: boolean;
7
+ beforeDelete?: (url: string) => Promise<boolean>;
8
+ }): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { stateRoot } from '../paths.js';
6
+ import { producerConfig } from './outbox.js';
7
+ const cleanupDir = () => join(stateRoot(), 'notifications', 'removed-targets');
8
+ /** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
9
+ export async function retireNotificationTarget(url, config = producerConfig(), dir = cleanupDir(), request = fetch) {
10
+ if (!config)
11
+ return;
12
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
13
+ replaceFileAtomically(join(dir, createHash('sha256').update(url).digest('hex') + '.json'), JSON.stringify({ url }) + '\n', 0o600);
14
+ await drainNotificationTargetCleanup(config, dir, request, { retain: true });
15
+ }
16
+ /** Persisted retries also run from the ordinary web notification producer after restart. */
17
+ export async function drainNotificationTargetCleanup(config, dir = cleanupDir(), request = fetch, options = {}) {
18
+ if (!existsSync(dir))
19
+ return;
20
+ for (const file of readdirSync(dir).filter(file => /^[a-f0-9]{64}\.json$/.test(file)).slice(0, 32)) {
21
+ try {
22
+ const path = join(dir, file), value = JSON.parse(readFileSync(path, 'utf8'));
23
+ if (typeof value.url !== 'string' || !value.url.startsWith('/fleet/chats?'))
24
+ continue;
25
+ if (options.beforeDelete && !(await options.beforeDelete(value.url))) {
26
+ rmSync(path, { force: true });
27
+ continue;
28
+ }
29
+ const response = await request(config.origin + '/api/v1/delete-target', { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10000),
30
+ headers: { ...(config.gatewayCredential ? { 'X-Ours-Api-Token': config.gatewayCredential, 'X-Ours-Notifications-Producer': config.token } : { Authorization: `Bearer ${config.token}` }), 'Content-Type': 'application/json' }, body: JSON.stringify(value) });
31
+ await response.body?.cancel();
32
+ if (!response.ok)
33
+ break;
34
+ if (!options.retain)
35
+ rmSync(path, { force: true });
36
+ }
37
+ catch {
38
+ break; /* No credentials or response bodies are included in logs. */
39
+ }
40
+ }
41
+ }
package/dist/ops.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { retireNotificationTarget } from './notifications/target-cleanup.js';
1
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync, } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { randomUUID, createHash } from 'node:crypto';
@@ -5,6 +6,7 @@ import { agentDir, watchdogsRoot } from './paths.js';
5
6
  import { findRole } from './config.js';
6
7
  import { getAdapter } from './harness/registry.js';
7
8
  import { generateBriefing } from './briefing.js';
9
+ import { managedCliBriefing } from './managed-cli.js';
8
10
  import { resetRestartLedger } from './runner.js';
9
11
  import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
10
12
  import { realExec } from './exec.js';
@@ -28,7 +30,7 @@ export function applyRole(role, opts = {}) {
28
30
  // brought up from so the supervised process can reload the SAME file instead of
29
31
  // silently falling back to the default ~/fleet.yaml. Temp roles snapshot their
30
32
  // whole resolved role into role.yaml instead and don't need this.
31
- if (!opts.temp)
33
+ if (!opts.temp || opts.configPath)
32
34
  writeFileSync(join(dir, '.config-path'), (opts.configPath ?? '') + '\n');
33
35
  writeFileSync(join(dir, '.identity'), role.identity + '\n');
34
36
  if (role.cwd)
@@ -43,6 +45,7 @@ export function applyRole(role, opts = {}) {
43
45
  routinesPath: join(dir, 'ROUTINES.md'), briefingBody,
44
46
  identityGuarantee: opts.identityGuarantee,
45
47
  temporaryIdentity: opts.temp === true,
48
+ managedCli: managedCliBriefing(role, opts.configPath, opts.temp === true),
46
49
  }));
47
50
  if (opts.fresh)
48
51
  for (const f of ['.booted', '.session-id', '.exit-status'])
@@ -255,6 +258,7 @@ export async function rmRole(cfg, name, deps) {
255
258
  rmSync(agentDir(name), { recursive: true, force: true });
256
259
  if (generatedSource && existsSync(role.sourceFile)) {
257
260
  unlinkSync(role.sourceFile);
261
+ await retireNotificationTarget(`/fleet/chats?chat=${encodeURIComponent(name)}`).catch(() => { });
258
262
  deps.log(`removed ${role.sourceFile}`);
259
263
  }
260
264
  deps.log(`removed '${name}' (its ours identity is left intact)`);
@@ -103,6 +103,7 @@ export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
103
103
  isolation: role.isolation ?? null,
104
104
  worklog: role.worklog ?? null,
105
105
  authProxy: role.auth_proxy ?? null,
106
+ managedCli: role.managed_cli ?? [],
106
107
  oversee: role.oversee ?? [],
107
108
  harnessOptions: redactSensitive(role.harness_options ?? null),
108
109
  env: {
package/dist/runner.js CHANGED
@@ -2,6 +2,7 @@ import { createLayoutControl } from './rooms-tasks/layout-control.js';
2
2
  import { TemporaryChatIdle } from './temp-idle.js';
3
3
  import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
4
4
  import { prepareManagedHarness } from './agent-ours/harness.js';
5
+ import { prepareManagedCliLaunch } from './managed-cli.js';
5
6
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, realpathSync } from 'node:fs';
6
7
  import { join } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
@@ -522,7 +523,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
522
523
  writeFileSync(bootedFile, `${new Date(deps.now()).toISOString()} ${mode}\n`);
523
524
  const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
524
525
  role = { ...role, monitor: { ...role.monitor, mode: 'fleet' } };
525
- const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
526
+ // Native policy for Fleet commands run from the harness command sandbox. This
527
+ // is the same step for configured, spawned and task-room agents alike.
528
+ const prep = prepareManagedCliLaunch(role, {
529
+ // A temporary agent has no config path threaded through its detached
530
+ // supervisor; spawn records the one it was created from beside its snapshot.
531
+ stateDir: dir, runCwd, configPath: configPath ?? resolveConfigPath(dir), temp,
532
+ prep: await adapter.prepareSession(role, { stateDir: dir, runCwd }), log: deps.log,
533
+ });
526
534
  const sessionBackend = role.session ?? 'acp';
527
535
  const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
528
536
  let launch = adapter.agentSession.prepareLaunch(role, prep);
package/dist/spawn.js CHANGED
@@ -358,7 +358,7 @@ async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
358
358
  if (role.roomMemberStartup)
359
359
  role.roomMemberStartup = { ...role.roomMemberStartup, invite: '' };
360
360
  onStage?.('writing_role');
361
- const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
361
+ const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified', configPath: o.configPath });
362
362
  const provenance = buildProvenance({
363
363
  role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
364
364
  settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
@@ -6,3 +6,14 @@ export declare function readTunnelSetupStdin(input?: AsyncIterable<Buffer | stri
6
6
  export declare function readTunnelSetupArgument(input: string): string;
7
7
  /** Redeem the grant by HTTPS POST; long-lived connector credentials remain off argv and URLs. */
8
8
  export declare function redeemTunnelSetup(input: string, request?: typeof fetch): Promise<WorkspacePayload>;
9
+ /** Inspect only public destination metadata; this never consumes a one-use grant. */
10
+ export declare function inspectTunnelSetup(input: string, request?: typeof fetch): Promise<{
11
+ workspaceId: string;
12
+ serverCid: string;
13
+ appOrigin: string;
14
+ } | undefined>;
15
+ export declare function verifyTunnelSetupTarget(target: {
16
+ workspaceId: string;
17
+ serverCid: string;
18
+ appOrigin: string;
19
+ } | undefined, payload: WorkspacePayload): void;
@@ -80,3 +80,45 @@ export async function redeemTunnelSetup(input, request = fetch) {
80
80
  throw Error('Tunnel setup response origin mismatch');
81
81
  return payload;
82
82
  }
83
+ /** Inspect only public destination metadata; this never consumes a one-use grant. */
84
+ export async function inspectTunnelSetup(input, request = fetch) {
85
+ let version;
86
+ try {
87
+ version = JSON.parse(Buffer.from(input.trim(), 'base64url').toString()).version;
88
+ }
89
+ catch { }
90
+ if (version === 1) {
91
+ const p = decodeWorkspacePayload(input);
92
+ return { workspaceId: p.challenge.workspaceId, serverCid: p.serverCid, appOrigin: p.appOrigin };
93
+ }
94
+ const grant = decodeTunnelSetupGrant(input);
95
+ let response;
96
+ try {
97
+ response = await request(grant.appOrigin + '/account-api/workspace-install-inspect', { method: 'POST', redirect: 'error', credentials: 'omit', signal: AbortSignal.timeout(15000), headers: { Origin: grant.appOrigin, 'Content-Type': 'application/json' }, body: JSON.stringify({ code: grant.code }) });
98
+ }
99
+ catch {
100
+ throw Error('Could not inspect the account setup command; existing setup and grant preserved');
101
+ }
102
+ if (response.status === 404 || response.status === 405) {
103
+ await response.body?.cancel();
104
+ return;
105
+ }
106
+ if (!response.ok) {
107
+ await response.body?.cancel();
108
+ throw Error('Setup command inspection failed; existing setup and grant preserved');
109
+ }
110
+ let target;
111
+ try {
112
+ target = await response.json();
113
+ }
114
+ catch {
115
+ throw Error('Invalid setup command inspection response');
116
+ }
117
+ if (!target || Object.keys(target).sort().join(',') !== 'serverCid,workspaceId' || typeof target.workspaceId !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(target.workspaceId) || typeof target.serverCid !== 'string' || !/^[a-f0-9]{64}$/i.test(target.serverCid))
118
+ throw Error('Invalid setup command inspection response');
119
+ return { ...target, appOrigin: grant.appOrigin };
120
+ }
121
+ export function verifyTunnelSetupTarget(target, payload) {
122
+ if (target && (target.workspaceId !== payload.challenge.workspaceId || target.serverCid.toUpperCase() !== payload.serverCid.toUpperCase() || target.appOrigin !== payload.appOrigin))
123
+ throw Error('Redeemed setup command differs from inspected destination; existing setup preserved');
124
+ }
@@ -25,5 +25,7 @@ export declare class WorkspaceDeviceStore {
25
25
  valid(id: string): boolean;
26
26
  list(): WorkspaceDevice[];
27
27
  revoke(id: string): void;
28
+ /** Retire account links without changing the installation's stable host ID or other data. */
29
+ revokeAll(): void;
28
30
  close(): void;
29
31
  }
@@ -80,5 +80,18 @@ export class WorkspaceDeviceStore {
80
80
  valid(id) { return Boolean(this.db.prepare('SELECT id FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(id, this.now())); }
81
81
  list() { return this.db.prepare('SELECT id,label,createdAt,lastUsedAt,expiresAt,revokedAt FROM devices ORDER BY createdAt DESC LIMIT 256').all().map(row => ({ id: String(row.id), label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: Number(row.lastUsedAt), expiresAt: Number(row.expiresAt), revokedAt: row.revokedAt === null ? null : Number(row.revokedAt) })); }
82
82
  revoke(id) { this.db.prepare('UPDATE devices SET revokedAt=? WHERE id=? AND revokedAt IS NULL').run(this.now(), id); this.db.prepare('DELETE FROM enrollments WHERE issuer=?').run(id); }
83
+ /** Retire account links without changing the installation's stable host ID or other data. */
84
+ revokeAll() {
85
+ this.db.exec('BEGIN IMMEDIATE');
86
+ try {
87
+ this.db.prepare('UPDATE devices SET revokedAt=? WHERE revokedAt IS NULL').run(this.now());
88
+ this.db.exec('DELETE FROM enrollments');
89
+ this.db.exec('COMMIT');
90
+ }
91
+ catch (error) {
92
+ this.db.exec('ROLLBACK');
93
+ throw error;
94
+ }
95
+ }
83
96
  close() { this.db.close(); }
84
97
  }
@@ -6,7 +6,7 @@ export interface WorkspacePayload {
6
6
  rootName: string;
7
7
  name: string;
8
8
  surname: string;
9
- connectorToken: string;
9
+ connectorToken?: string;
10
10
  invitation: string;
11
11
  serverCid: string;
12
12
  challenge: {
@@ -17,7 +17,11 @@ export interface WorkspacePayload {
17
17
  };
18
18
  }
19
19
  export declare function readWorkspacePayload(file: string): WorkspacePayload;
20
- export declare function decodeWorkspacePayload(input: string): WorkspacePayload;
20
+ export declare function decodeWorkspacePayload(input: string, options?: {
21
+ allowExpired?: boolean;
22
+ }): WorkspacePayload;
23
+ /** Credential-free renewal is allowed only on the host already holding this private connector. */
24
+ export declare function checkWorkspaceConnector(payload: WorkspacePayload): void;
21
25
  type DaemonClient = Pick<OursClient, 'listIdentities' | 'chooseIdentity' | 'addContact' | 'listContacts' | 'sendCommand' | 'removeContact' | 'releaseLease' | 'close'>;
22
26
  /** How tunnel setup reaches this host's daemon; replaced only by tests. */
23
27
  export type AttachDaemonClient = (options: AttachOursClientOptions) => Promise<DaemonClient>;
@@ -29,6 +33,7 @@ export declare function checkWorkspaceConfiguration(configuration?: string, atta
29
33
  export declare function enrollWorkspace(payload: WorkspacePayload, configuration?: string, options?: {
30
34
  migrateAppOrigin?: boolean;
31
35
  preserveProfile?: boolean;
36
+ replacingWorkspace?: string;
32
37
  attach?: AttachDaemonClient;
33
38
  contactWaitMs?: number;
34
39
  afterInvitation?: () => void;
@@ -51,4 +56,27 @@ export declare function configureWorkspacePort(payload: Pick<WorkspacePayload, '
51
56
  * so the setup can be finished later. A removal that fails is reported and does not undo the setup.
52
57
  */
53
58
  export declare function confirmWorkspaceTarget(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number, cleanupFailed?: (error: unknown) => void): Promise<void>;
59
+ /** Retire exactly the original registration using the same signed Human root that bound it. */
60
+ export declare function unregisterWorkspace(previous: {
61
+ workspaceId: string;
62
+ hostWorkspaceId: string;
63
+ appOrigin: string;
64
+ serverCid: string;
65
+ proofRootCid?: string;
66
+ }, payload: WorkspacePayload, operationNonce: string, attach?: AttachDaemonClient, options?: {
67
+ request?: typeof fetch;
68
+ waitMs?: number;
69
+ checkOnly?: boolean;
70
+ beforeSend?: () => void;
71
+ operationExpiresAt?: number;
72
+ onReceipt?: (status: {
73
+ deleted?: boolean;
74
+ retired?: boolean;
75
+ absent?: boolean;
76
+ rejected?: boolean;
77
+ expired?: boolean;
78
+ deadlineEnforced?: boolean;
79
+ oldBindingPresent?: boolean;
80
+ }) => void;
81
+ }): Promise<string>;
54
82
  export {};