@ericsanchezok/meta-synergy 1.1.26 → 1.2.18

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.
@@ -0,0 +1,162 @@
1
+ import { readFile, writeFile } from "node:fs/promises"
2
+ import { MetaSynergyStore } from "./state/store"
3
+
4
+ export interface MetaSynergyLocalOwnerRegistryState {
5
+ ownerIDs: string[]
6
+ activeOwnerID?: string
7
+ leaseExpiresAt?: number
8
+ }
9
+
10
+ export interface MetaSynergyOwnerRegistryState {
11
+ local: MetaSynergyLocalOwnerRegistryState
12
+ }
13
+
14
+ const DEFAULT_REGISTRY: MetaSynergyOwnerRegistryState = {
15
+ local: {
16
+ ownerIDs: [],
17
+ },
18
+ }
19
+
20
+ export namespace MetaSynergyOwnerRegistry {
21
+ export function hydrate(input: unknown): MetaSynergyOwnerRegistryState {
22
+ const parsed = isRecord(input) ? input : undefined
23
+ if (isLegacySynergyOwnerRecord(parsed)) {
24
+ const ownerID = parsed.agentId ? `synergy:${parsed.agentId}` : parsed.owner
25
+ return {
26
+ local: {
27
+ ownerIDs: [ownerID],
28
+ activeOwnerID: ownerID,
29
+ },
30
+ }
31
+ }
32
+
33
+ const local = isRecord(parsed?.local) ? parsed.local : undefined
34
+ const ownerIDs = uniqueStrings(Array.isArray(local?.ownerIDs) ? local.ownerIDs : undefined)
35
+ const activeOwnerID =
36
+ typeof local?.activeOwnerID === "string" && local.activeOwnerID.length > 0 ? local.activeOwnerID : undefined
37
+ const leaseExpiresAt =
38
+ typeof local?.leaseExpiresAt === "number" && Number.isFinite(local.leaseExpiresAt)
39
+ ? local.leaseExpiresAt
40
+ : undefined
41
+
42
+ if (activeOwnerID && !ownerIDs.includes(activeOwnerID)) {
43
+ ownerIDs.unshift(activeOwnerID)
44
+ }
45
+
46
+ return {
47
+ local: {
48
+ ownerIDs,
49
+ activeOwnerID,
50
+ leaseExpiresAt,
51
+ },
52
+ }
53
+ }
54
+
55
+ export function declareLocalOwner(
56
+ registry: MetaSynergyOwnerRegistryState,
57
+ ownerID: string,
58
+ options?: { leaseExpiresAt?: number },
59
+ ) {
60
+ const normalizedOwnerID = normalizeOwnerID(ownerID)
61
+ if (!registry.local.ownerIDs.includes(normalizedOwnerID)) {
62
+ registry.local.ownerIDs.unshift(normalizedOwnerID)
63
+ }
64
+ registry.local.activeOwnerID = normalizedOwnerID
65
+ registry.local.leaseExpiresAt = normalizeLeaseExpiresAt(options?.leaseExpiresAt)
66
+ return snapshot(registry)
67
+ }
68
+
69
+ export function releaseLocalOwner(registry: MetaSynergyOwnerRegistryState, ownerID?: string) {
70
+ const normalizedOwnerID = ownerID ? normalizeOwnerID(ownerID) : undefined
71
+ if (!normalizedOwnerID || registry.local.activeOwnerID === normalizedOwnerID) {
72
+ registry.local.activeOwnerID = undefined
73
+ registry.local.leaseExpiresAt = undefined
74
+ }
75
+ return snapshot(registry)
76
+ }
77
+
78
+ export function hasActiveLocalOwner(registry: MetaSynergyOwnerRegistryState, now = Date.now()) {
79
+ const hydrated = hydrate(registry)
80
+ if (!hydrated.local.activeOwnerID) return false
81
+ if (hydrated.local.leaseExpiresAt !== undefined && hydrated.local.leaseExpiresAt <= now) {
82
+ return false
83
+ }
84
+ return true
85
+ }
86
+
87
+ export function activeOwnerExpired(registry: MetaSynergyOwnerRegistryState, now = Date.now()) {
88
+ const hydrated = hydrate(registry)
89
+ return (
90
+ hydrated.local.activeOwnerID !== undefined &&
91
+ hydrated.local.leaseExpiresAt !== undefined &&
92
+ hydrated.local.leaseExpiresAt <= now
93
+ )
94
+ }
95
+
96
+ export function snapshot(registry: MetaSynergyOwnerRegistryState) {
97
+ const hydrated = hydrate(registry)
98
+ return {
99
+ local: {
100
+ ownerIDs: hydrated.local.ownerIDs,
101
+ activeOwnerID: hydrated.local.activeOwnerID ?? null,
102
+ leaseExpiresAt: hydrated.local.leaseExpiresAt ?? null,
103
+ owned: hasActiveLocalOwner(hydrated),
104
+ },
105
+ }
106
+ }
107
+
108
+ export function defaultRegistry() {
109
+ return hydrate(DEFAULT_REGISTRY)
110
+ }
111
+
112
+ export async function loadFile() {
113
+ try {
114
+ const parsed = JSON.parse(await readFile(MetaSynergyStore.ownerRegistryPath(), "utf8")) as unknown
115
+ return hydrate(parsed)
116
+ } catch {
117
+ return defaultRegistry()
118
+ }
119
+ }
120
+
121
+ export async function saveFile(registry: MetaSynergyOwnerRegistryState) {
122
+ await MetaSynergyStore.ensureRoot()
123
+ await writeFile(MetaSynergyStore.ownerRegistryPath(), JSON.stringify(snapshot(registry), null, 2) + "\n")
124
+ }
125
+ }
126
+
127
+ function normalizeOwnerID(ownerID: string) {
128
+ const normalized = ownerID.trim()
129
+ if (!normalized) {
130
+ throw new Error("Owner ID is required.")
131
+ }
132
+ return normalized
133
+ }
134
+
135
+ function normalizeLeaseExpiresAt(value: number | undefined) {
136
+ if (value === undefined) return undefined
137
+ if (!Number.isFinite(value)) {
138
+ throw new Error("Lease expiration must be a finite timestamp.")
139
+ }
140
+ return value
141
+ }
142
+
143
+ function uniqueStrings(values: unknown[] | undefined): string[] {
144
+ return [
145
+ ...new Set(
146
+ values?.filter((value): value is string => typeof value === "string" && value.length > 0) ??
147
+ DEFAULT_REGISTRY.local.ownerIDs,
148
+ ),
149
+ ]
150
+ }
151
+
152
+ function isRecord(value: unknown): value is Record<string, unknown> {
153
+ return typeof value === "object" && value !== null
154
+ }
155
+
156
+ function isLegacySynergyOwnerRecord(value: Record<string, unknown> | undefined): value is {
157
+ owner: string
158
+ agentId?: string
159
+ version?: number
160
+ } {
161
+ return typeof value?.owner === "string" && (value.version === undefined || typeof value.version === "number")
162
+ }
package/src/runtime.ts CHANGED
@@ -1,11 +1,16 @@
1
- import { MetaProtocolEnv } from "@ericsanchezok/meta-protocol"
2
1
  import process from "node:process"
2
+ import { MetaSynergyControlClient } from "./control/client"
3
3
  import { MetaSynergyControlServer } from "./control/server"
4
+ import type { MetaSynergyControlRequest } from "./control/schema"
5
+ import { MetaSynergyDisplay } from "./display"
6
+ import { MetaSynergyMigrationRunner } from "./migration"
4
7
  import { MetaSynergyHost } from "./host"
5
8
  import { MetaSynergyInboundHandler, type SessionOpenDecision } from "./inbound/handler"
6
9
  import { MetaSynergyHolosClient } from "./holos/client"
10
+ import { MetaSynergyHolosAuth, type MetaSynergyHolosAuthSource } from "./holos/auth"
7
11
  import { MetaSynergyHolosLogin } from "./holos/login"
8
12
  import { MetaSynergyLog } from "./log"
13
+ import { MetaSynergyOwnerRegistry } from "./owner-registry"
9
14
  import { RPCHandler } from "./rpc/handler"
10
15
  import { MetaSynergyLocalService } from "./service/local"
11
16
  import { SessionManager } from "./session/manager"
@@ -29,6 +34,8 @@ export class MetaSynergyRuntime {
29
34
  #reconnectAttempt = 0
30
35
  #stopping = false
31
36
  #manualReconnectInFlight = false
37
+ #runPromise: Promise<void> | null = null
38
+ #resolveRunPromise: (() => void) | null = null
32
39
 
33
40
  private constructor(
34
41
  host: MetaSynergyHost,
@@ -45,6 +52,7 @@ export class MetaSynergyRuntime {
45
52
  }
46
53
 
47
54
  static async create() {
55
+ await MetaSynergyMigrationRunner.run()
48
56
  const state = await MetaSynergyStore.loadState()
49
57
  const envID = state.envID ?? `env_${crypto.randomUUID()}`
50
58
  const hostSessionID = state.hostSessionID ?? crypto.randomUUID()
@@ -73,15 +81,28 @@ export class MetaSynergyRuntime {
73
81
  const inbound = new MetaSynergyInboundHandler(rpc, sessions, (input) => runtime.decideSessionOpen(input))
74
82
  const control = new MetaSynergyControlServer((request) => runtime.handleControlRequest(request))
75
83
  runtime = new MetaSynergyRuntime(host, sessions, rpc, inbound, control)
84
+ const ownerRegistry = await MetaSynergyOwnerRegistry.loadFile()
76
85
  runtime.state = {
77
86
  ...state,
78
87
  envID,
79
88
  hostSessionID,
89
+ ownerRegistry,
80
90
  currentSession: undefined,
81
91
  logs: {
82
92
  filePath: state.logs.filePath || MetaSynergyStore.logsPath(),
83
93
  },
84
94
  }
95
+
96
+ if (
97
+ runtime.state.runtimeMode === "managed" &&
98
+ !MetaSynergyOwnerRegistry.hasActiveLocalOwner(runtime.state.ownerRegistry)
99
+ ) {
100
+ runtime.state.runtimeMode = "standalone"
101
+ runtime.state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(runtime.state.ownerRegistry)
102
+ MetaSynergyOwnerRegistry.releaseLocalOwner(runtime.state.ownerRegistry)
103
+ runtime.state.connectionStatus = "disconnected"
104
+ }
105
+
85
106
  await MetaSynergyStore.saveState(runtime.state)
86
107
  return runtime
87
108
  }
@@ -93,6 +114,9 @@ export class MetaSynergyRuntime {
93
114
  async start(input?: { printLogs?: boolean }) {
94
115
  const state = requireState(this)
95
116
  this.#stopping = false
117
+ this.#runPromise = new Promise<void>((resolve) => {
118
+ this.#resolveRunPromise = resolve
119
+ })
96
120
  state.service.desiredState = "running"
97
121
  state.service.runtimeStatus = "starting"
98
122
  state.service.pid = process.pid
@@ -100,39 +124,55 @@ export class MetaSynergyRuntime {
100
124
  state.service.startedAt = state.service.startedAt ?? Date.now()
101
125
  state.service.stoppedAt = undefined
102
126
  state.logs.filePath = MetaSynergyStore.logsPath()
103
- state.connectionStatus = "connecting"
127
+ state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(state.ownerRegistry)
128
+ if (state.runtimeMode === "standalone") {
129
+ MetaSynergyOwnerRegistry.releaseLocalOwner(state.ownerRegistry)
130
+ await MetaSynergyOwnerRegistry.saveFile(state.ownerRegistry)
131
+ }
132
+ state.connectionStatus = state.runtimeMode === "managed" ? "disconnected" : "connecting"
104
133
  MetaSynergyLog.configure({ printToConsole: state.service.printLogs })
105
134
  await MetaSynergyStore.saveState(state)
106
135
  await this.control.start()
107
136
 
108
- const auth = await MetaSynergyStore.loadAuth()
109
- if (!auth) {
110
- console.log("No Holos credentials found in ~/.meta-synergy. Starting login flow.")
111
- await this.login()
112
- }
137
+ this.#startLoops()
138
+ if (state.runtimeMode === "standalone") {
139
+ const auth = await MetaSynergyHolosAuth.load()
140
+ if (!auth) {
141
+ console.log("No Holos credentials found. Starting login flow.")
142
+ await this.login()
143
+ }
113
144
 
114
- const nextAuth = await MetaSynergyStore.loadAuth()
115
- if (!nextAuth) {
116
- throw new Error("Meta Synergy could not load credentials after login.")
117
- }
145
+ const nextAuth = await MetaSynergyHolosAuth.load()
146
+ if (!nextAuth) {
147
+ throw new Error("Meta Synergy could not load credentials after login.")
148
+ }
118
149
 
119
- this.#startLoops()
120
- this.#ensureClient(nextAuth)
121
- try {
122
- await this.#connectClient()
123
- } catch (error) {
124
- MetaSynergyLog.error("runtime.connection.initial_connect_failed", {
125
- error: error instanceof Error ? error.message : String(error),
126
- })
150
+ this.#ensureClient(nextAuth)
151
+ try {
152
+ await this.#connectClient()
153
+ } catch (error) {
154
+ MetaSynergyLog.error("runtime.connection.initial_connect_failed", {
155
+ error: error instanceof Error ? error.message : String(error),
156
+ })
157
+ await this.#setConnectionStatus("disconnected")
158
+ this.#scheduleReconnect("initial_connect_failed")
159
+ }
160
+
161
+ console.log(
162
+ `MetaSynergy running as ${MetaSynergyDisplay.identifier(nextAuth.agentID, { hiddenReason: "policy" })}`,
163
+ )
164
+ } else {
165
+ this.#clearReconnectTimer()
166
+ this.#client = null
127
167
  await this.#setConnectionStatus("disconnected")
128
- this.#scheduleReconnect("initial_connect_failed")
168
+ console.log("MetaSynergy running in managed mode")
129
169
  }
130
170
 
131
171
  state.service.runtimeStatus = "running"
132
172
  await MetaSynergyStore.saveState(state)
133
173
 
134
- console.log(`MetaSynergy running as ${nextAuth.agentID}`)
135
174
  console.log(`envID: ${this.host.envID}`)
175
+ console.log(`Mode: ${state.runtimeMode}`)
136
176
  console.log(`Holos: ${this.state?.connectionStatus ?? "disconnected"}`)
137
177
  console.log(`Status: ${this.sessions.current() ? "busy" : "idle"}`)
138
178
 
@@ -143,12 +183,13 @@ export class MetaSynergyRuntime {
143
183
  void this.stopServerProcess({ exit: true })
144
184
  })
145
185
 
146
- await new Promise(() => undefined)
186
+ await this.#runPromise
147
187
  }
148
188
 
149
189
  async stopServerProcess(input?: { exit?: boolean }) {
150
190
  this.#stopping = true
151
191
  this.#clearReconnectTimer()
192
+ await this.#releaseManagedOwner("service_stop")
152
193
  this.#stopLoops()
153
194
  if (this.state) {
154
195
  this.state.connectionStatus = "disconnected"
@@ -163,6 +204,9 @@ export class MetaSynergyRuntime {
163
204
  await this.#client?.disconnect().catch(() => undefined)
164
205
  this.#client = null
165
206
  await this.control.stop()
207
+ this.#resolveRunPromise?.()
208
+ this.#resolveRunPromise = null
209
+ this.#runPromise = null
166
210
  if (input?.exit) {
167
211
  process.exit(0)
168
212
  }
@@ -174,7 +218,9 @@ export class MetaSynergyRuntime {
174
218
 
175
219
  async logout() {
176
220
  await this.stopServerProcess()
177
- await MetaSynergyStore.clearAuth()
221
+ if (requireState(this).runtimeMode !== "managed") {
222
+ await MetaSynergyHolosAuth.clear()
223
+ }
178
224
  }
179
225
 
180
226
  async setCollaborationEnabled(enabled: boolean) {
@@ -224,6 +270,16 @@ export class MetaSynergyRuntime {
224
270
  }
225
271
 
226
272
  async reconnect() {
273
+ const state = requireState(this)
274
+ if (state.runtimeMode === "managed") {
275
+ return {
276
+ requested: false,
277
+ succeeded: false,
278
+ reason: "Holos is disabled in managed mode",
279
+ service: this.getServiceSnapshot(),
280
+ }
281
+ }
282
+
227
283
  const succeeded = await this.#reconnectClient()
228
284
  if (!succeeded) {
229
285
  this.#scheduleReconnect("manual_reconnect_failed")
@@ -244,6 +300,88 @@ export class MetaSynergyRuntime {
244
300
  }
245
301
  }
246
302
 
303
+ async getMode() {
304
+ const state = requireState(this)
305
+ return {
306
+ mode: state.runtimeMode,
307
+ ownership: this.getOwnershipSnapshot(),
308
+ connectionStatus: state.connectionStatus,
309
+ service: this.getServiceSnapshot(),
310
+ }
311
+ }
312
+
313
+ async enterManagedMode(input?: { ownerID?: string; leaseExpiresAt?: number }) {
314
+ const state = requireState(this)
315
+ state.runtimeMode = "managed"
316
+ state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(state.ownerRegistry)
317
+ MetaSynergyOwnerRegistry.declareLocalOwner(
318
+ state.ownerRegistry,
319
+ input?.ownerID ?? state.envID ?? this.host.envID ?? crypto.randomUUID(),
320
+ { leaseExpiresAt: input?.leaseExpiresAt },
321
+ )
322
+ await MetaSynergyOwnerRegistry.saveFile(state.ownerRegistry)
323
+ this.#clearReconnectTimer()
324
+ this.#reconnectAttempt = 0
325
+ await this.#client?.disconnect().catch(() => undefined)
326
+ this.#client = null
327
+ await this.#setConnectionStatus("disconnected")
328
+ await MetaSynergyStore.saveState(state)
329
+ return {
330
+ mode: state.runtimeMode,
331
+ ownership: this.getOwnershipSnapshot(),
332
+ connectionStatus: state.connectionStatus,
333
+ service: this.getServiceSnapshot(),
334
+ }
335
+ }
336
+
337
+ async enterStandaloneMode(input?: { ownerID?: string; reason?: string }) {
338
+ const state = requireState(this)
339
+ const previousMode = state.runtimeMode
340
+ state.runtimeMode = "standalone"
341
+ state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(state.ownerRegistry)
342
+ MetaSynergyOwnerRegistry.releaseLocalOwner(state.ownerRegistry, input?.ownerID)
343
+ await MetaSynergyOwnerRegistry.saveFile(state.ownerRegistry)
344
+ this.#clearReconnectTimer()
345
+ this.#reconnectAttempt = 0
346
+ if (this.sessions.current()) {
347
+ this.sessions.kickCurrent(false)
348
+ }
349
+ await this.#client?.disconnect().catch(() => undefined)
350
+ this.#client = null
351
+ await this.#setConnectionStatus("disconnected")
352
+ await MetaSynergyStore.saveState(state)
353
+
354
+ MetaSynergyLog.info("runtime.mode.changed", {
355
+ from: previousMode,
356
+ to: state.runtimeMode,
357
+ reason: input?.reason,
358
+ })
359
+
360
+ if (state.service.runtimeStatus === "running" && !this.#stopping) {
361
+ void this.#resumeStandaloneConnection(input?.reason)
362
+ }
363
+
364
+ return {
365
+ mode: state.runtimeMode,
366
+ ownership: this.getOwnershipSnapshot(),
367
+ connectionStatus: state.connectionStatus,
368
+ service: this.getServiceSnapshot(),
369
+ }
370
+ }
371
+
372
+ async releaseManagedMode(input?: { ownerID?: string; reason?: string }) {
373
+ const state = requireState(this)
374
+ if (state.runtimeMode !== "managed") {
375
+ return {
376
+ mode: state.runtimeMode,
377
+ ownership: this.getOwnershipSnapshot(),
378
+ connectionStatus: state.connectionStatus,
379
+ service: this.getServiceSnapshot(),
380
+ }
381
+ }
382
+ return await this.enterStandaloneMode({ ownerID: input?.ownerID, reason: input?.reason ?? "release_requested" })
383
+ }
384
+
247
385
  async getApproval() {
248
386
  const state = requireState(this)
249
387
  return { mode: state.approvalMode }
@@ -355,6 +493,11 @@ export class MetaSynergyRuntime {
355
493
  }
356
494
  }
357
495
 
496
+ getOwnershipSnapshot() {
497
+ const state = requireState(this)
498
+ return MetaSynergyOwnerRegistry.snapshot(state.ownerRegistry)
499
+ }
500
+
358
501
  async getStatusSnapshot() {
359
502
  const state = requireState(this)
360
503
  return {
@@ -363,6 +506,8 @@ export class MetaSynergyRuntime {
363
506
  hostSessionID: state.hostSessionID ?? this.host.hostSessionID,
364
507
  label: state.label,
365
508
  },
509
+ runtimeMode: state.runtimeMode,
510
+ ownership: this.getOwnershipSnapshot(),
366
511
  connectionStatus: state.connectionStatus,
367
512
  collaborationEnabled: state.collaborationEnabled,
368
513
  approvalMode: state.approvalMode,
@@ -375,10 +520,14 @@ export class MetaSynergyRuntime {
375
520
  }
376
521
 
377
522
  async getStatusPayload() {
378
- const auth = await MetaSynergyStore.loadAuth()
379
523
  const state = requireState(this)
524
+ const authInfo = await MetaSynergyHolosAuth.inspect()
380
525
  return {
381
- auth: sanitizeAuth(auth),
526
+ auth: sanitizeAuth(authInfo.auth, authInfo.source, {
527
+ hiddenReason: state.runtimeMode === "managed" ? "managed" : null,
528
+ }),
529
+ mode: state.runtimeMode,
530
+ ownership: this.getOwnershipSnapshot(),
382
531
  state: sanitizeState(state),
383
532
  session: state.currentSession ?? null,
384
533
  envID: state.envID ?? null,
@@ -392,7 +541,7 @@ export class MetaSynergyRuntime {
392
541
  }
393
542
  }
394
543
 
395
- async handleControlRequest(request: { action: string; [key: string]: unknown }) {
544
+ async handleControlRequest(request: MetaSynergyControlRequest) {
396
545
  switch (request.action) {
397
546
  case "ping":
398
547
  return null
@@ -405,6 +554,28 @@ export class MetaSynergyRuntime {
405
554
  return null
406
555
  case "runtime.status":
407
556
  return await this.getStatusPayload()
557
+ case "runtime.mode":
558
+ return await this.getMode()
559
+ case "runtime.enter_managed":
560
+ return await this.enterManagedMode()
561
+ case "runtime.enter_managed_mode":
562
+ return await this.enterManagedMode({
563
+ ownerID: controlOwnerID(request),
564
+ })
565
+ case "runtime.set_mode":
566
+ if (request.mode === "managed") {
567
+ return await this.enterManagedMode({
568
+ ownerID: controlOwnerID(request),
569
+ leaseExpiresAt: controlLeaseExpiresAt(request),
570
+ })
571
+ }
572
+ return await this.enterStandaloneMode({ ownerID: controlOwnerID(request), reason: "control_set_mode" })
573
+ case "runtime.release_managed":
574
+ return await this.releaseManagedMode({
575
+ ownerID: controlOwnerID(request),
576
+ reason: "control_release_managed",
577
+ })
578
+
408
579
  case "runtime.reconnect":
409
580
  return await this.reconnect()
410
581
  case "collaboration.status":
@@ -423,6 +594,11 @@ export class MetaSynergyRuntime {
423
594
  return await this.getSessionStatus()
424
595
  case "session.kick":
425
596
  return await this.kickCurrentSession(Boolean(request.block))
597
+ case "meta.execute":
598
+ return await this.inbound.handle({
599
+ caller: request.caller,
600
+ body: request.body,
601
+ })
426
602
  case "approval.get":
427
603
  return await this.getApproval()
428
604
  case "approval.set":
@@ -443,8 +619,10 @@ export class MetaSynergyRuntime {
443
619
  since: request.since as string | undefined,
444
620
  maxBytes: request.maxBytes as number | undefined,
445
621
  })
446
- default:
447
- throw new Error(`Unsupported control action: ${request.action}`)
622
+ default: {
623
+ const unsupported: never = request
624
+ throw new Error(`Unsupported control action: ${String(unsupported)}`)
625
+ }
448
626
  }
449
627
  }
450
628
 
@@ -598,7 +776,13 @@ export class MetaSynergyRuntime {
598
776
  }
599
777
  }, 30_000)
600
778
  idleTimer.unref?.()
601
- this.#timers = [idleTimer]
779
+
780
+ const ownershipTimer = setInterval(() => {
781
+ void this.#watchManagedOwnership()
782
+ }, 5_000)
783
+ ownershipTimer.unref?.()
784
+
785
+ this.#timers = [idleTimer, ownershipTimer]
602
786
  }
603
787
 
604
788
  #stopLoops() {
@@ -608,7 +792,11 @@ export class MetaSynergyRuntime {
608
792
 
609
793
  async #connectClient() {
610
794
  if (!this.state) return
611
- const auth = await MetaSynergyStore.loadAuth()
795
+ if (this.state.runtimeMode === "managed") {
796
+ await this.#setConnectionStatus("disconnected")
797
+ throw new Error("Holos is disabled in managed mode.")
798
+ }
799
+ const auth = await MetaSynergyHolosAuth.load()
612
800
  if (!auth) {
613
801
  this.state.connectionStatus = "disconnected"
614
802
  await MetaSynergyStore.saveState(this.state)
@@ -670,7 +858,7 @@ export class MetaSynergyRuntime {
670
858
 
671
859
  async #handleClientClosed(input: { intentional: boolean }) {
672
860
  await this.#setConnectionStatus("disconnected")
673
- if (input.intentional || this.#stopping || this.#manualReconnectInFlight) {
861
+ if (input.intentional || this.#stopping || this.#manualReconnectInFlight || this.state?.runtimeMode === "managed") {
674
862
  return
675
863
  }
676
864
  this.#scheduleReconnect("socket_closed")
@@ -678,6 +866,7 @@ export class MetaSynergyRuntime {
678
866
 
679
867
  #scheduleReconnect(reason: string) {
680
868
  if (this.#stopping || this.#manualReconnectInFlight || this.#reconnectTimer) return
869
+ if (!this.state || this.state.runtimeMode === "managed") return
681
870
 
682
871
  const attempt = this.#reconnectAttempt + 1
683
872
  const delayMs = Math.min(60_000, 2_000 * 2 ** (attempt - 1))
@@ -711,12 +900,49 @@ export class MetaSynergyRuntime {
711
900
  }
712
901
  }
713
902
 
903
+ async #resumeStandaloneConnection(reason?: string) {
904
+ const state = this.state
905
+ if (!state || state.runtimeMode !== "standalone" || this.#stopping) return
906
+ const auth = await MetaSynergyHolosAuth.load()
907
+ if (!auth) {
908
+ MetaSynergyLog.warn("runtime.connection.recover_skipped_missing_auth", { reason })
909
+ return
910
+ }
911
+ try {
912
+ await this.#connectClient()
913
+ } catch (error) {
914
+ MetaSynergyLog.error("runtime.connection.recover_failed", {
915
+ reason,
916
+ error: error instanceof Error ? error.message : String(error),
917
+ })
918
+ this.#scheduleReconnect(reason ?? "standalone_transition_failed")
919
+ }
920
+ }
921
+
714
922
  #clearReconnectTimer() {
715
923
  if (!this.#reconnectTimer) return
716
924
  clearTimeout(this.#reconnectTimer)
717
925
  this.#reconnectTimer = null
718
926
  }
719
927
 
928
+ async #releaseManagedOwner(reason: string) {
929
+ if (!this.state || this.state.runtimeMode !== "managed") return
930
+ this.state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(this.state.ownerRegistry)
931
+ MetaSynergyOwnerRegistry.releaseLocalOwner(this.state.ownerRegistry)
932
+ await MetaSynergyOwnerRegistry.saveFile(this.state.ownerRegistry)
933
+ await MetaSynergyStore.saveState(this.state)
934
+ MetaSynergyLog.info("runtime.managed.owner_released", { reason })
935
+ }
936
+
937
+ async #watchManagedOwnership() {
938
+ if (!this.state || this.#stopping || this.state.runtimeMode !== "managed") return
939
+ this.state.ownerRegistry = MetaSynergyOwnerRegistry.hydrate(this.state.ownerRegistry)
940
+ if (MetaSynergyOwnerRegistry.hasActiveLocalOwner(this.state.ownerRegistry)) {
941
+ return
942
+ }
943
+ await this.enterStandaloneMode({ reason: "managed_owner_lost" })
944
+ }
945
+
720
946
  async #setConnectionStatus(status: "disconnected" | "connecting" | "connected") {
721
947
  if (!this.state) return
722
948
  this.state.connectionStatus = status
@@ -724,20 +950,44 @@ export class MetaSynergyRuntime {
724
950
  }
725
951
  }
726
952
 
727
- function sanitizeAuth(auth: { agentID: string; agentSecret: string } | undefined) {
953
+ function controlOwnerID(request: { owner?: unknown; ownerAgentId?: unknown }) {
954
+ if (typeof request.ownerAgentId === "string" && request.ownerAgentId.length > 0) return request.ownerAgentId
955
+ if (typeof request.owner === "string" && request.owner.length > 0) return request.owner
956
+ return undefined
957
+ }
958
+
959
+ function controlLeaseExpiresAt(request: { leaseExpiresAt?: unknown }) {
960
+ return typeof request.leaseExpiresAt === "number" && Number.isFinite(request.leaseExpiresAt)
961
+ ? request.leaseExpiresAt
962
+ : undefined
963
+ }
964
+
965
+ function sanitizeAuth(
966
+ auth: { agentID: string; agentSecret: string } | undefined,
967
+ source: MetaSynergyHolosAuthSource | null,
968
+ options?: { hiddenReason?: "managed" | "policy" | null },
969
+ ) {
728
970
  return auth
729
971
  ? {
730
972
  loggedIn: true,
731
- agentID: auth.agentID,
973
+ agentID: MetaSynergyDisplay.identifier(auth.agentID, {
974
+ hiddenReason: options?.hiddenReason,
975
+ }),
976
+ source,
977
+ hiddenReason: options?.hiddenReason ?? null,
732
978
  }
733
979
  : {
734
980
  loggedIn: false,
735
981
  agentID: null,
982
+ source: null,
983
+ hiddenReason: null,
736
984
  }
737
985
  }
738
986
 
739
987
  function sanitizeState(state: MetaSynergyState) {
740
988
  return {
989
+ runtimeMode: state.runtimeMode,
990
+ ownerRegistry: MetaSynergyOwnerRegistry.snapshot(state.ownerRegistry),
741
991
  collaborationEnabled: state.collaborationEnabled,
742
992
  approvalMode: state.approvalMode,
743
993
  trusted: {