@ericsanchezok/meta-synergy 1.1.6 → 1.1.8
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/package.json +4 -1
- package/src/cli.ts +66 -0
- package/src/holos/client.ts +146 -0
- package/src/holos/envelope.ts +68 -0
- package/src/holos/login.ts +128 -0
- package/src/holos/protocol.ts +38 -0
- package/src/inbound/handler.ts +200 -0
- package/src/index.ts +7 -0
- package/src/log.ts +30 -0
- package/src/menu.swift +266 -0
- package/src/rpc/handler.ts +83 -11
- package/src/rpc/schema.ts +8 -1
- package/src/runtime.ts +318 -0
- package/src/session/manager.ts +252 -0
- package/src/state/store.ts +96 -0
- package/src/types.ts +7 -0
- package/test/rpc-handler.test.ts +10 -0
- package/test/session-manager.test.ts +37 -0
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@ericsanchezok/meta-synergy",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"meta-synergy": "./src/cli.ts"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"import": "./dist/index.js",
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import { MetaSynergyRuntime } from "./runtime"
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const command = process.argv[2] ?? "start"
|
|
7
|
+
const runtime = await MetaSynergyRuntime.create()
|
|
8
|
+
|
|
9
|
+
switch (command) {
|
|
10
|
+
case "start":
|
|
11
|
+
await runtime.start()
|
|
12
|
+
return
|
|
13
|
+
case "login": {
|
|
14
|
+
const result = await runtime.login()
|
|
15
|
+
console.log(`Logged in as ${result.agentID}`)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
case "status": {
|
|
19
|
+
const status = await runtime.status()
|
|
20
|
+
console.log(JSON.stringify(status, null, 2))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
case "logout":
|
|
24
|
+
await runtime.logout()
|
|
25
|
+
console.log("Logged out from Holos.")
|
|
26
|
+
return
|
|
27
|
+
case "enable":
|
|
28
|
+
await runtime.setCollaborationEnabled(true)
|
|
29
|
+
console.log("Collaboration enabled.")
|
|
30
|
+
return
|
|
31
|
+
case "disable":
|
|
32
|
+
await runtime.setCollaborationEnabled(false)
|
|
33
|
+
console.log("Collaboration disabled.")
|
|
34
|
+
return
|
|
35
|
+
case "kick": {
|
|
36
|
+
await runtime.requestKick(false)
|
|
37
|
+
console.log("Requested current collaboration session to close.")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
case "block": {
|
|
41
|
+
await runtime.requestKick(true)
|
|
42
|
+
console.log("Requested current collaboration session to close and block the collaborator.")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
case "reconnect":
|
|
46
|
+
await runtime.requestReconnect()
|
|
47
|
+
console.log("Requested Holos reconnect.")
|
|
48
|
+
return
|
|
49
|
+
case "menu":
|
|
50
|
+
await new Promise<void>((resolve, reject) => {
|
|
51
|
+
const child = spawn("swift", ["src/menu.swift"], { stdio: "inherit" })
|
|
52
|
+
child.once("exit", (code) => {
|
|
53
|
+
if ((code ?? 0) === 0) resolve()
|
|
54
|
+
else reject(new Error(`Menu exited with code ${code ?? 0}`))
|
|
55
|
+
})
|
|
56
|
+
child.once("error", reject)
|
|
57
|
+
})
|
|
58
|
+
return
|
|
59
|
+
default:
|
|
60
|
+
console.error(`Unknown command: ${command}`)
|
|
61
|
+
console.error("Usage: bun meta-synergy [start|login|status|logout|enable|disable|kick|block|reconnect|menu]")
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await main()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { MetaProtocolBridge } from "@ericsanchezok/meta-protocol"
|
|
2
|
+
import type { RPCResult } from "../rpc/schema"
|
|
3
|
+
import { MetaSynergyHolosEnvelope } from "./envelope"
|
|
4
|
+
import { MetaSynergyHolosProtocol } from "./protocol"
|
|
5
|
+
import type { MetaSynergyInboundHandler } from "../inbound/handler"
|
|
6
|
+
import { MetaSynergyLog } from "../log"
|
|
7
|
+
|
|
8
|
+
const HOLOS_HOST = "www.holosai.io"
|
|
9
|
+
const HOLOS_URL = `https://${HOLOS_HOST}`
|
|
10
|
+
const HOLOS_WS_URL = `wss://${HOLOS_HOST}`
|
|
11
|
+
|
|
12
|
+
export class MetaSynergyHolosClient {
|
|
13
|
+
#ws: WebSocket | null = null
|
|
14
|
+
#heartbeat: ReturnType<typeof setInterval> | null = null
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
readonly auth: { agentID: string; agentSecret: string },
|
|
18
|
+
readonly inbound: MetaSynergyInboundHandler,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async connect() {
|
|
22
|
+
const token = await fetchWsToken(this.auth.agentSecret)
|
|
23
|
+
const endpoint = `${HOLOS_WS_URL}/api/v1/holos/agent_tunnel/ws?token=${token}`
|
|
24
|
+
MetaSynergyLog.info("holos.connect.begin", {
|
|
25
|
+
agentID: this.auth.agentID,
|
|
26
|
+
endpoint,
|
|
27
|
+
})
|
|
28
|
+
const ws = new WebSocket(endpoint)
|
|
29
|
+
this.#ws = ws
|
|
30
|
+
|
|
31
|
+
await new Promise<void>((resolve, reject) => {
|
|
32
|
+
let opened = false
|
|
33
|
+
ws.addEventListener("open", () => {
|
|
34
|
+
opened = true
|
|
35
|
+
MetaSynergyLog.info("holos.connect.open", {
|
|
36
|
+
agentID: this.auth.agentID,
|
|
37
|
+
})
|
|
38
|
+
resolve()
|
|
39
|
+
})
|
|
40
|
+
ws.addEventListener("error", (error) => {
|
|
41
|
+
MetaSynergyLog.error("holos.connect.error", {
|
|
42
|
+
agentID: this.auth.agentID,
|
|
43
|
+
error: String(error),
|
|
44
|
+
})
|
|
45
|
+
if (!opened) reject(new Error("Failed to connect to Holos websocket."))
|
|
46
|
+
})
|
|
47
|
+
ws.addEventListener("close", () => {
|
|
48
|
+
MetaSynergyLog.warn("holos.connect.closed_before_open", {
|
|
49
|
+
agentID: this.auth.agentID,
|
|
50
|
+
})
|
|
51
|
+
if (!opened) reject(new Error("Holos websocket closed before opening."))
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
ws.addEventListener("message", (event) => {
|
|
56
|
+
void this.#handleMessage(String(event.data))
|
|
57
|
+
})
|
|
58
|
+
ws.addEventListener("close", () => {
|
|
59
|
+
MetaSynergyLog.warn("holos.socket.closed", {
|
|
60
|
+
agentID: this.auth.agentID,
|
|
61
|
+
})
|
|
62
|
+
if (this.#heartbeat) clearInterval(this.#heartbeat)
|
|
63
|
+
this.#heartbeat = null
|
|
64
|
+
this.#ws = null
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
this.#heartbeat = setInterval(() => {
|
|
68
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
69
|
+
ws.send(MetaSynergyHolosEnvelope.ping())
|
|
70
|
+
}
|
|
71
|
+
}, 60_000)
|
|
72
|
+
this.#heartbeat.unref?.()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async disconnect() {
|
|
76
|
+
MetaSynergyLog.info("holos.disconnect", {
|
|
77
|
+
agentID: this.auth.agentID,
|
|
78
|
+
})
|
|
79
|
+
if (this.#heartbeat) clearInterval(this.#heartbeat)
|
|
80
|
+
this.#heartbeat = null
|
|
81
|
+
this.#ws?.close()
|
|
82
|
+
this.#ws = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
connected() {
|
|
86
|
+
return this.#ws?.readyState === WebSocket.OPEN
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async #handleMessage(raw: string) {
|
|
90
|
+
MetaSynergyLog.info("holos.message.received.raw", {
|
|
91
|
+
raw,
|
|
92
|
+
})
|
|
93
|
+
const parsed = MetaSynergyHolosEnvelope.parse(raw)
|
|
94
|
+
if (!parsed) {
|
|
95
|
+
MetaSynergyLog.warn("holos.message.ignored.unparsed")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
MetaSynergyLog.info("holos.message.received.parsed", {
|
|
100
|
+
event: parsed.event,
|
|
101
|
+
callerAgentID: parsed.caller.agentID,
|
|
102
|
+
callerOwnerUserID: parsed.caller.ownerUserID,
|
|
103
|
+
payload: parsed.payload,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (parsed.event !== MetaProtocolBridge.RequestEvent) {
|
|
107
|
+
MetaSynergyLog.info("holos.message.ignored.non_request_event", {
|
|
108
|
+
event: parsed.event,
|
|
109
|
+
callerAgentID: parsed.caller.agentID,
|
|
110
|
+
})
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await this.inbound.handle({ caller: parsed.caller, body: parsed.payload })
|
|
115
|
+
this.#sendResult(parsed.caller.agentID, result)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#sendResult(targetAgentID: string, result: RPCResult) {
|
|
119
|
+
if (this.#ws?.readyState !== WebSocket.OPEN) {
|
|
120
|
+
MetaSynergyLog.warn("holos.response.dropped.socket_not_open", {
|
|
121
|
+
targetAgentID,
|
|
122
|
+
result,
|
|
123
|
+
})
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
MetaSynergyLog.info("holos.response.sending", {
|
|
127
|
+
targetAgentID,
|
|
128
|
+
result,
|
|
129
|
+
})
|
|
130
|
+
this.#ws.send(MetaSynergyHolosEnvelope.response(targetAgentID, result))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchWsToken(agentSecret: string): Promise<string> {
|
|
135
|
+
const response = await fetch(`${HOLOS_URL}/api/v1/holos/agent_tunnel/ws_token`, {
|
|
136
|
+
headers: { Authorization: `Bearer ${agentSecret}` },
|
|
137
|
+
})
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Failed to get Holos ws token: ${response.status} ${response.statusText}`)
|
|
140
|
+
}
|
|
141
|
+
const body = MetaSynergyHolosProtocol.WsTokenResponse.parse(await response.json())
|
|
142
|
+
if (body.code !== 0) {
|
|
143
|
+
throw new Error(body.message ?? "Failed to get Holos ws token.")
|
|
144
|
+
}
|
|
145
|
+
return body.data.ws_token
|
|
146
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { MetaProtocolBridge } from "@ericsanchezok/meta-protocol"
|
|
2
|
+
import type { HolosCaller } from "../types"
|
|
3
|
+
import { MetaSynergyHolosProtocol } from "./protocol"
|
|
4
|
+
|
|
5
|
+
export namespace MetaSynergyHolosEnvelope {
|
|
6
|
+
export function parse(raw: string): { event: string; payload: unknown; caller: HolosCaller } | null {
|
|
7
|
+
let data: unknown
|
|
8
|
+
try {
|
|
9
|
+
data = JSON.parse(raw)
|
|
10
|
+
} catch {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parsed = MetaSynergyHolosProtocol.Envelope.safeParse(data)
|
|
15
|
+
if (!parsed.success || parsed.data.type !== "ws_send" || !parsed.data.caller) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
event: String(parsed.data.meta.event ?? ""),
|
|
21
|
+
payload: parsed.data.payload,
|
|
22
|
+
caller: {
|
|
23
|
+
type: parsed.data.caller.type,
|
|
24
|
+
agentID: parsed.data.caller.agent_id,
|
|
25
|
+
ownerUserID: parsed.data.caller.owner_user_id,
|
|
26
|
+
profile: parsed.data.caller.profile,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function request(targetAgentID: string, payload: unknown, requestID = crypto.randomUUID()): string {
|
|
32
|
+
return JSON.stringify({
|
|
33
|
+
type: "ws_send",
|
|
34
|
+
request_id: requestID,
|
|
35
|
+
meta: {
|
|
36
|
+
target_agent_id: targetAgentID,
|
|
37
|
+
event: MetaProtocolBridge.RequestEvent,
|
|
38
|
+
content_type: "application/json",
|
|
39
|
+
},
|
|
40
|
+
payload,
|
|
41
|
+
caller: null,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function response(targetAgentID: string, payload: unknown, requestID = crypto.randomUUID()): string {
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
type: "ws_send",
|
|
48
|
+
request_id: requestID,
|
|
49
|
+
meta: {
|
|
50
|
+
target_agent_id: targetAgentID,
|
|
51
|
+
event: MetaProtocolBridge.ResponseEvent,
|
|
52
|
+
content_type: "application/json",
|
|
53
|
+
},
|
|
54
|
+
payload,
|
|
55
|
+
caller: null,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ping(): string {
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
type: "ping",
|
|
62
|
+
request_id: null,
|
|
63
|
+
meta: { timestamp: Date.now() },
|
|
64
|
+
payload: null,
|
|
65
|
+
caller: null,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import process from "node:process"
|
|
2
|
+
import { createServer, type IncomingMessage } from "node:http"
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { MetaSynergyStore } from "../state/store"
|
|
5
|
+
import { MetaSynergyHolosProtocol } from "./protocol"
|
|
6
|
+
|
|
7
|
+
const HOLOS_HOST = "www.holosai.io"
|
|
8
|
+
const HOLOS_URL = `https://${HOLOS_HOST}`
|
|
9
|
+
|
|
10
|
+
export namespace MetaSynergyHolosLogin {
|
|
11
|
+
export function createBindURL(input: { callbackURL: string; state: string }) {
|
|
12
|
+
return (
|
|
13
|
+
`${HOLOS_URL}/api/v1/holos/agent_tunnel/bind/start` +
|
|
14
|
+
`?local_callback=${encodeURIComponent(input.callbackURL)}` +
|
|
15
|
+
`&state=${encodeURIComponent(input.state)}`
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function verifySecret(agentSecret: string): Promise<{ valid: true } | { valid: false; reason: string }> {
|
|
20
|
+
const response = await fetch(`${HOLOS_URL}/api/v1/holos/agent_tunnel/ws_token`, {
|
|
21
|
+
headers: { Authorization: `Bearer ${agentSecret}` },
|
|
22
|
+
})
|
|
23
|
+
const body = MetaSynergyHolosProtocol.WsTokenResponse.safeParse(await response.json())
|
|
24
|
+
if (!body.success || !response.ok || body.data.code !== 0) {
|
|
25
|
+
return { valid: false, reason: body.success ? (body.data.message ?? "Invalid response") : "Invalid response" }
|
|
26
|
+
}
|
|
27
|
+
return { valid: true }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function login(): Promise<{ agentID: string }> {
|
|
31
|
+
await MetaSynergyStore.ensureRoot()
|
|
32
|
+
const state = crypto.randomUUID()
|
|
33
|
+
const port = 19836 + Math.floor(Math.random() * 1000)
|
|
34
|
+
const callbackURL = `http://127.0.0.1:${port}/holos/login`
|
|
35
|
+
const bindURL = createBindURL({ callbackURL, state })
|
|
36
|
+
|
|
37
|
+
const callback = new Promise<{ code: string; state: string }>((resolve, reject) => {
|
|
38
|
+
const server = createServer((request, response) => {
|
|
39
|
+
const params = parseRequest(request)
|
|
40
|
+
if (!params || params.pathname !== "/holos/login") {
|
|
41
|
+
response.statusCode = 404
|
|
42
|
+
response.end("Not found")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
server.close()
|
|
47
|
+
if (!params.code || !params.state) {
|
|
48
|
+
reject(new Error("Missing login callback parameters."))
|
|
49
|
+
response.statusCode = 400
|
|
50
|
+
response.end("Login failed.")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resolve({ code: params.code, state: params.state })
|
|
55
|
+
response.end("Login successful. You can close this window.")
|
|
56
|
+
})
|
|
57
|
+
server.listen(port, "127.0.0.1")
|
|
58
|
+
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
server.close()
|
|
61
|
+
reject(new Error("Login timed out."))
|
|
62
|
+
}, 5 * 60_000)
|
|
63
|
+
timer.unref?.()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await launchBrowser(bindURL)
|
|
68
|
+
} catch {
|
|
69
|
+
console.log(`Open this URL to continue login:\n${bindURL}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const params = await callback
|
|
73
|
+
if (params.state !== state) {
|
|
74
|
+
throw new Error("State mismatch during Holos login.")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const exchangeResponse = await fetch(`${HOLOS_URL}/api/v1/holos/agent_tunnel/bind/exchange`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
code: params.code,
|
|
82
|
+
state: params.state,
|
|
83
|
+
profile: { name: "Meta Synergy Host" },
|
|
84
|
+
}),
|
|
85
|
+
})
|
|
86
|
+
if (!exchangeResponse.ok) {
|
|
87
|
+
throw new Error(`Exchange failed: ${exchangeResponse.status} ${exchangeResponse.statusText}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const exchangeBody = MetaSynergyHolosProtocol.BindExchangeResponse.parse(await exchangeResponse.json())
|
|
91
|
+
if (exchangeBody.code !== 0) {
|
|
92
|
+
throw new Error(exchangeBody.message ?? exchangeBody.msg ?? "Holos exchange failed.")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const agentSecret = exchangeBody.data.agent_secret ?? exchangeBody.data.secret
|
|
96
|
+
if (!agentSecret) {
|
|
97
|
+
throw new Error("Holos exchange did not return an agent secret.")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await MetaSynergyStore.saveAuth({
|
|
101
|
+
agentID: exchangeBody.data.agent_id,
|
|
102
|
+
agentSecret,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return { agentID: exchangeBody.data.agent_id }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseRequest(request: IncomingMessage) {
|
|
110
|
+
if (!request.url) return
|
|
111
|
+
const url = new URL(request.url, "http://127.0.0.1")
|
|
112
|
+
return {
|
|
113
|
+
pathname: url.pathname,
|
|
114
|
+
code: url.searchParams.get("code") ?? undefined,
|
|
115
|
+
state: url.searchParams.get("state") ?? undefined,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function launchBrowser(url: string): Promise<void> {
|
|
120
|
+
const command =
|
|
121
|
+
process.platform === "darwin"
|
|
122
|
+
? ["open", url]
|
|
123
|
+
: process.platform === "win32"
|
|
124
|
+
? ["cmd", "/c", "start", "", url]
|
|
125
|
+
: ["xdg-open", url]
|
|
126
|
+
const child = spawn(command[0], command.slice(1), { stdio: "ignore", detached: true })
|
|
127
|
+
child.unref()
|
|
128
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
|
|
3
|
+
export namespace MetaSynergyHolosProtocol {
|
|
4
|
+
export const Caller = z.object({
|
|
5
|
+
type: z.string(),
|
|
6
|
+
agent_id: z.string(),
|
|
7
|
+
owner_user_id: z.number(),
|
|
8
|
+
profile: z.record(z.string(), z.unknown()).optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const Envelope = z.object({
|
|
12
|
+
type: z.string(),
|
|
13
|
+
request_id: z.string().nullable(),
|
|
14
|
+
meta: z.record(z.string(), z.unknown()),
|
|
15
|
+
payload: z.unknown().nullable(),
|
|
16
|
+
caller: Caller.nullable().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const WsTokenResponse = z.object({
|
|
20
|
+
code: z.number(),
|
|
21
|
+
message: z.string().optional(),
|
|
22
|
+
data: z.object({
|
|
23
|
+
ws_token: z.string(),
|
|
24
|
+
expires_in: z.number(),
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const BindExchangeResponse = z.object({
|
|
29
|
+
code: z.number(),
|
|
30
|
+
msg: z.string().optional(),
|
|
31
|
+
message: z.string().optional(),
|
|
32
|
+
data: z.object({
|
|
33
|
+
agent_id: z.string(),
|
|
34
|
+
agent_secret: z.string().optional(),
|
|
35
|
+
secret: z.string().optional(),
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MetaProtocolBash,
|
|
3
|
+
MetaProtocolEnvelope,
|
|
4
|
+
MetaProtocolError,
|
|
5
|
+
MetaProtocolProcess,
|
|
6
|
+
MetaProtocolSession,
|
|
7
|
+
} from "@ericsanchezok/meta-protocol"
|
|
8
|
+
import type { HolosCaller } from "../types"
|
|
9
|
+
import { RPCHandler } from "../rpc/handler"
|
|
10
|
+
import { RPCRequestSchema, type RPCResult } from "../rpc/schema"
|
|
11
|
+
import { SessionManager } from "../session/manager"
|
|
12
|
+
import { MetaSynergyLog } from "../log"
|
|
13
|
+
|
|
14
|
+
export class MetaSynergyInboundHandler {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly rpc: RPCHandler,
|
|
17
|
+
readonly sessions: SessionManager,
|
|
18
|
+
readonly collaborationEnabled: () => boolean,
|
|
19
|
+
readonly confirmOpen?: (input: { caller: HolosCaller; label?: string }) => Promise<boolean>,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async handle(input: { caller: HolosCaller; body: unknown }): Promise<RPCResult> {
|
|
23
|
+
try {
|
|
24
|
+
const request = RPCRequestSchema.parse(input.body)
|
|
25
|
+
MetaSynergyLog.info("inbound.request.accepted", {
|
|
26
|
+
callerAgentID: input.caller.agentID,
|
|
27
|
+
callerOwnerUserID: input.caller.ownerUserID,
|
|
28
|
+
tool: request.tool,
|
|
29
|
+
action: request.action,
|
|
30
|
+
requestID: request.requestID,
|
|
31
|
+
envID: request.envID,
|
|
32
|
+
sessionID: "sessionID" in request ? request.sessionID : undefined,
|
|
33
|
+
payload: request.payload,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (request.tool === "session") {
|
|
37
|
+
return this.#handleSession(input.caller, request)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!this.collaborationEnabled()) {
|
|
41
|
+
MetaSynergyLog.warn("inbound.request.refused.collaboration_disabled", {
|
|
42
|
+
callerAgentID: input.caller.agentID,
|
|
43
|
+
tool: request.tool,
|
|
44
|
+
action: request.action,
|
|
45
|
+
requestID: request.requestID,
|
|
46
|
+
})
|
|
47
|
+
return errorResult(
|
|
48
|
+
{ requestID: request.requestID, tool: request.tool, action: request.action },
|
|
49
|
+
"session_refused",
|
|
50
|
+
"Collaboration is currently disabled.",
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.sessions.validateCaller(input.caller, request.sessionID)
|
|
55
|
+
const result = await this.rpc.handle(request)
|
|
56
|
+
MetaSynergyLog.info("inbound.request.completed", {
|
|
57
|
+
callerAgentID: input.caller.agentID,
|
|
58
|
+
tool: request.tool,
|
|
59
|
+
action: request.action,
|
|
60
|
+
requestID: request.requestID,
|
|
61
|
+
result,
|
|
62
|
+
})
|
|
63
|
+
return result
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isEnvelopeError(error)) {
|
|
66
|
+
MetaSynergyLog.warn("inbound.request.failed.envelope", {
|
|
67
|
+
callerAgentID: input.caller.agentID,
|
|
68
|
+
code: error.code,
|
|
69
|
+
message: error.message,
|
|
70
|
+
details: error.details,
|
|
71
|
+
})
|
|
72
|
+
return errorResult(
|
|
73
|
+
{
|
|
74
|
+
requestID: error.requestID,
|
|
75
|
+
tool: error.tool,
|
|
76
|
+
action: error.action,
|
|
77
|
+
},
|
|
78
|
+
error.code,
|
|
79
|
+
error.message,
|
|
80
|
+
error.details,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
MetaSynergyLog.error("inbound.request.failed.unexpected", {
|
|
85
|
+
callerAgentID: input.caller.agentID,
|
|
86
|
+
error: error instanceof Error ? error.message : String(error),
|
|
87
|
+
})
|
|
88
|
+
return errorResult(undefined, "host_internal_error", error instanceof Error ? error.message : String(error))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #handleSession(
|
|
93
|
+
caller: HolosCaller,
|
|
94
|
+
request: MetaProtocolSession.ExecuteRequest,
|
|
95
|
+
): Promise<MetaProtocolSession.ExecuteResult | MetaProtocolEnvelope.ErrorResult> {
|
|
96
|
+
MetaSynergyLog.info("session.request.received", {
|
|
97
|
+
callerAgentID: caller.agentID,
|
|
98
|
+
callerOwnerUserID: caller.ownerUserID,
|
|
99
|
+
action: request.payload.action,
|
|
100
|
+
requestID: request.requestID,
|
|
101
|
+
payload: request.payload,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (!this.collaborationEnabled() && request.payload.action === "open") {
|
|
105
|
+
MetaSynergyLog.warn("session.request.refused.collaboration_disabled", {
|
|
106
|
+
callerAgentID: caller.agentID,
|
|
107
|
+
requestID: request.requestID,
|
|
108
|
+
})
|
|
109
|
+
return errorResult(
|
|
110
|
+
{ requestID: request.requestID, tool: request.tool, action: request.action },
|
|
111
|
+
"session_refused",
|
|
112
|
+
"Collaboration is currently disabled.",
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (request.payload.action === "open" && !this.sessions.current()) {
|
|
117
|
+
const approved = await (this.confirmOpen?.({
|
|
118
|
+
caller,
|
|
119
|
+
label: request.payload.label,
|
|
120
|
+
}) ?? Promise.resolve(true))
|
|
121
|
+
if (!approved) {
|
|
122
|
+
MetaSynergyLog.warn("session.request.refused.user_denied", {
|
|
123
|
+
callerAgentID: caller.agentID,
|
|
124
|
+
requestID: request.requestID,
|
|
125
|
+
})
|
|
126
|
+
return errorResult(
|
|
127
|
+
{ requestID: request.requestID, tool: request.tool, action: request.action },
|
|
128
|
+
"session_refused",
|
|
129
|
+
"User denied this collaboration request.",
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let result: MetaProtocolSession.Result
|
|
135
|
+
switch (request.payload.action) {
|
|
136
|
+
case "open":
|
|
137
|
+
result = await this.sessions.open(caller, request.payload.label)
|
|
138
|
+
break
|
|
139
|
+
case "close":
|
|
140
|
+
result = await this.sessions.close(caller, request.payload.sessionID)
|
|
141
|
+
break
|
|
142
|
+
case "heartbeat":
|
|
143
|
+
result = await this.sessions.heartbeat(caller, request.payload.sessionID)
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = {
|
|
148
|
+
version: 1,
|
|
149
|
+
requestID: request.requestID,
|
|
150
|
+
ok: true,
|
|
151
|
+
tool: request.tool,
|
|
152
|
+
action: request.action,
|
|
153
|
+
result,
|
|
154
|
+
} as const
|
|
155
|
+
|
|
156
|
+
MetaSynergyLog.info("session.request.completed", {
|
|
157
|
+
callerAgentID: caller.agentID,
|
|
158
|
+
requestID: request.requestID,
|
|
159
|
+
action: request.payload.action,
|
|
160
|
+
status: result.metadata.status,
|
|
161
|
+
sessionID: result.metadata.sessionID,
|
|
162
|
+
result,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return response
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function errorResult(
|
|
170
|
+
request:
|
|
171
|
+
| {
|
|
172
|
+
requestID?: string
|
|
173
|
+
tool?: MetaProtocolEnvelope.Tool
|
|
174
|
+
action?: string
|
|
175
|
+
}
|
|
176
|
+
| undefined,
|
|
177
|
+
code: MetaProtocolError.Code,
|
|
178
|
+
message: string,
|
|
179
|
+
details?: unknown,
|
|
180
|
+
): MetaProtocolEnvelope.ErrorResult {
|
|
181
|
+
return {
|
|
182
|
+
version: 1,
|
|
183
|
+
requestID: request?.requestID || crypto.randomUUID(),
|
|
184
|
+
ok: false,
|
|
185
|
+
tool: request?.tool,
|
|
186
|
+
action: request?.action,
|
|
187
|
+
error: { code, message, details },
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isEnvelopeError(error: unknown): error is {
|
|
192
|
+
requestID?: string
|
|
193
|
+
tool?: MetaProtocolEnvelope.Tool
|
|
194
|
+
action?: string
|
|
195
|
+
code: MetaProtocolError.Code
|
|
196
|
+
message: string
|
|
197
|
+
details?: unknown
|
|
198
|
+
} {
|
|
199
|
+
return typeof error === "object" && error !== null && "code" in error && "message" in error
|
|
200
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export * from "./types"
|
|
2
|
+
export * from "./log"
|
|
2
3
|
export * from "./host"
|
|
3
4
|
export * from "./platform"
|
|
5
|
+
export * from "./runtime"
|
|
4
6
|
export * from "./client/holos-client"
|
|
7
|
+
export * from "./session/manager"
|
|
8
|
+
export * from "./inbound/handler"
|
|
9
|
+
export * from "./holos/client"
|
|
10
|
+
export * from "./holos/login"
|
|
11
|
+
export * from "./state/store"
|
|
5
12
|
export * from "./rpc/schema"
|
|
6
13
|
export * from "./rpc/handler"
|
|
7
14
|
export * from "./exec/bash-runner"
|