@ericsanchezok/meta-synergy 1.1.26 → 1.2.17
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 +47 -0
- package/install +444 -0
- package/package.json +7 -6
- package/script/build.ts +93 -0
- package/src/cli-backend.ts +118 -16
- package/src/cli.ts +178 -33
- package/src/control/schema.ts +25 -0
- package/src/display.ts +70 -0
- package/src/exec/process-registry.ts +14 -5
- package/src/holos/auth.ts +183 -0
- package/src/holos/login.ts +148 -8
- package/src/inbound/handler.ts +20 -9
- package/src/index.ts +1 -0
- package/src/migration/index.ts +22 -0
- package/src/migration/types.ts +5 -0
- package/src/owner-registry.ts +162 -0
- package/src/runtime.ts +283 -33
- package/src/service.ts +169 -77
- package/src/state/migration.ts +19 -0
- package/src/state/store.ts +53 -7
- package/src/types.ts +8 -0
- package/test/cli-backend-auth.test.ts +86 -0
- package/test/cli-backend-mode.test.ts +49 -0
- package/test/control-socket.test.ts +123 -0
- package/test/holos-auth.test.ts +117 -0
- package/test/migration.test.ts +58 -0
- package/test/runtime-managed-mode.test.ts +111 -0
- package/script/publish.ts +0 -38
|
@@ -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.
|
|
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
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|