@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
package/src/display.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type MetaSynergyHiddenReason = "managed" | "policy"
|
|
2
|
+
|
|
3
|
+
interface IdentifierValueOptions {
|
|
4
|
+
missing?: string
|
|
5
|
+
unknown?: string
|
|
6
|
+
hiddenReason?: MetaSynergyHiddenReason | null
|
|
7
|
+
showStart?: number
|
|
8
|
+
showEnd?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface IdentifierListOptions extends IdentifierValueOptions {
|
|
12
|
+
separator?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export namespace MetaSynergyDisplay {
|
|
16
|
+
export function identifier(value: string | null | undefined, options?: IdentifierValueOptions): string {
|
|
17
|
+
if (value === null || value === undefined || value.length === 0) {
|
|
18
|
+
return options?.missing ?? "none"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (options?.hiddenReason) {
|
|
22
|
+
return maskIdentifier(value, options)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function maybeIdentifier(value: unknown, options?: IdentifierValueOptions): string {
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return value == null ? (options?.missing ?? "none") : (options?.unknown ?? "unknown")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return identifier(value, options)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function identifierList(values: Array<string> | undefined, options?: IdentifierListOptions): string {
|
|
37
|
+
if (!values || values.length === 0) {
|
|
38
|
+
return options?.missing ?? "none"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const separator = options?.separator ?? ", "
|
|
42
|
+
return values.map((value) => identifier(value, options)).join(separator)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function maskIdentifier(
|
|
46
|
+
value: string,
|
|
47
|
+
options?: { hiddenReason?: MetaSynergyHiddenReason | null; showStart?: number; showEnd?: number },
|
|
48
|
+
): string {
|
|
49
|
+
const showStart = options?.showStart ?? defaultPrefixLength(value)
|
|
50
|
+
const showEnd = options?.showEnd ?? defaultSuffixLength(value)
|
|
51
|
+
|
|
52
|
+
if (value.length <= showStart + showEnd + 3) {
|
|
53
|
+
return `${value.slice(0, Math.max(1, Math.min(4, value.length)))}...`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `${value.slice(0, showStart)}...${value.slice(-showEnd)}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function defaultPrefixLength(value: string) {
|
|
60
|
+
if (value.startsWith("env_")) return 8
|
|
61
|
+
if (value.startsWith("ses_")) return 8
|
|
62
|
+
return 8
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultSuffixLength(value: string) {
|
|
66
|
+
if (value.startsWith("env_")) return 7
|
|
67
|
+
if (value.startsWith("ses_")) return 6
|
|
68
|
+
return 8
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -83,14 +83,21 @@ export class ProcessRegistry {
|
|
|
83
83
|
return this.#backgroundResult(launched.record, envID, request.description, "Background")
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
if (request.
|
|
86
|
+
if (request.yieldSeconds && request.yieldSeconds > 0) {
|
|
87
|
+
const yieldMs = request.yieldSeconds * 1000
|
|
87
88
|
const autoBackground = await Promise.race([
|
|
88
89
|
this.#waitForExit(launched.record.processId).then(() => false),
|
|
89
|
-
Platform.sleep(
|
|
90
|
+
Platform.sleep(yieldMs).then(() => !launched.record.exited),
|
|
90
91
|
])
|
|
91
92
|
if (autoBackground) {
|
|
92
93
|
launched.record.backgrounded = true
|
|
93
|
-
return this.#backgroundResult(
|
|
94
|
+
return this.#backgroundResult(
|
|
95
|
+
launched.record,
|
|
96
|
+
envID,
|
|
97
|
+
request.description,
|
|
98
|
+
"Auto-Background",
|
|
99
|
+
request.yieldSeconds,
|
|
100
|
+
)
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -547,10 +554,12 @@ export class ProcessRegistry {
|
|
|
547
554
|
envID: string,
|
|
548
555
|
description: string,
|
|
549
556
|
mode: "Background" | "Auto-Background",
|
|
550
|
-
|
|
557
|
+
yieldSeconds?: number,
|
|
551
558
|
): MetaProtocolBash.Result {
|
|
552
559
|
const prefix =
|
|
553
|
-
mode === "Auto-Background"
|
|
560
|
+
mode === "Auto-Background"
|
|
561
|
+
? `Command auto-backgrounded after ${yieldSeconds}s.`
|
|
562
|
+
: "Command started in background."
|
|
554
563
|
return {
|
|
555
564
|
title: `[${mode}] ${description}`,
|
|
556
565
|
metadata: {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import os from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises"
|
|
4
|
+
import { applyEdits, modify } from "jsonc-parser"
|
|
5
|
+
import z from "zod"
|
|
6
|
+
import { MetaSynergyStore, type MetaSynergyAuthState } from "../state/store"
|
|
7
|
+
|
|
8
|
+
export type MetaSynergyHolosAuthSource = "shared" | "legacy-migrated"
|
|
9
|
+
|
|
10
|
+
export const HOLOS_API_HOST = "api.holosai.io"
|
|
11
|
+
export const HOLOS_PORTAL_HOST = "www.holosai.io"
|
|
12
|
+
export const HOLOS_URL = `https://${HOLOS_API_HOST}`
|
|
13
|
+
export const HOLOS_WS_URL = `wss://${HOLOS_API_HOST}`
|
|
14
|
+
export const HOLOS_PORTAL_URL = `https://${HOLOS_PORTAL_HOST}`
|
|
15
|
+
|
|
16
|
+
const JSONC_FORMATTING = {
|
|
17
|
+
insertSpaces: true,
|
|
18
|
+
tabSize: 2,
|
|
19
|
+
eol: "\n",
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
const SynergyHolosAuth = z.object({
|
|
23
|
+
type: z.literal("holos"),
|
|
24
|
+
agentId: z.string(),
|
|
25
|
+
agentSecret: z.string(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const SynergyAuthRecord = z.record(z.string(), z.unknown())
|
|
29
|
+
const SynergyConfigSetMetadata = z.object({ active: z.string().min(1).default("default") })
|
|
30
|
+
|
|
31
|
+
export namespace MetaSynergyHolosAuth {
|
|
32
|
+
export function synergyRoot() {
|
|
33
|
+
return path.join(process.env.SYNERGY_TEST_HOME || os.homedir(), ".synergy")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sharedAuthPath() {
|
|
37
|
+
return path.join(synergyRoot(), "data", "auth", "api-key.json")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function configMetadataPath() {
|
|
41
|
+
return path.join(synergyRoot(), "config", "config-set.json")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function globalConfigPath() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(configMetadataPath(), "utf8")
|
|
47
|
+
const metadata = SynergyConfigSetMetadata.parse(JSON.parse(raw))
|
|
48
|
+
return metadata.active === "default"
|
|
49
|
+
? path.join(synergyRoot(), "config", "synergy.jsonc")
|
|
50
|
+
: path.join(synergyRoot(), "config", "config-sets", metadata.active, "synergy.jsonc")
|
|
51
|
+
} catch {
|
|
52
|
+
return path.join(synergyRoot(), "config", "synergy.jsonc")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function inspect(): Promise<
|
|
57
|
+
{ auth: MetaSynergyAuthState; source: MetaSynergyHolosAuthSource } | { auth: undefined; source: null }
|
|
58
|
+
> {
|
|
59
|
+
const shared = await loadShared()
|
|
60
|
+
if (shared) {
|
|
61
|
+
return {
|
|
62
|
+
auth: shared,
|
|
63
|
+
source: "shared",
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const legacy = await loadLegacy()
|
|
68
|
+
if (!legacy) {
|
|
69
|
+
return {
|
|
70
|
+
auth: undefined,
|
|
71
|
+
source: null,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await saveShared(legacy)
|
|
76
|
+
return {
|
|
77
|
+
auth: legacy,
|
|
78
|
+
source: "legacy-migrated",
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function load(): Promise<MetaSynergyAuthState | undefined> {
|
|
83
|
+
return (await inspect()).auth
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function save(auth: MetaSynergyAuthState): Promise<void> {
|
|
87
|
+
await saveShared(auth)
|
|
88
|
+
await MetaSynergyStore.saveLegacyAuth(auth)
|
|
89
|
+
await configureHolos()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function configureHolos(): Promise<void> {
|
|
93
|
+
const filePath = await globalConfigPath()
|
|
94
|
+
const source = await loadGlobalConfigSource(filePath)
|
|
95
|
+
const next = applyEdits(
|
|
96
|
+
source,
|
|
97
|
+
modify(
|
|
98
|
+
source,
|
|
99
|
+
["holos"],
|
|
100
|
+
{
|
|
101
|
+
enabled: true,
|
|
102
|
+
apiUrl: HOLOS_URL,
|
|
103
|
+
wsUrl: HOLOS_WS_URL,
|
|
104
|
+
portalUrl: HOLOS_PORTAL_URL,
|
|
105
|
+
},
|
|
106
|
+
{ formattingOptions: JSONC_FORMATTING },
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
111
|
+
await writeFile(filePath, next.endsWith("\n") ? next : `${next}\n`)
|
|
112
|
+
await chmod(filePath, 0o600).catch(() => undefined)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function clear(): Promise<void> {
|
|
116
|
+
await removeShared()
|
|
117
|
+
await MetaSynergyStore.clearLegacyAuth()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function loadShared(): Promise<MetaSynergyAuthState | undefined> {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = SynergyAuthRecord.parse(JSON.parse(await readFile(sharedAuthPath(), "utf8")))
|
|
123
|
+
const holos = SynergyHolosAuth.safeParse(parsed.holos)
|
|
124
|
+
if (!holos.success) return undefined
|
|
125
|
+
return {
|
|
126
|
+
agentID: holos.data.agentId,
|
|
127
|
+
agentSecret: holos.data.agentSecret,
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadLegacy(): Promise<MetaSynergyAuthState | undefined> {
|
|
135
|
+
return await MetaSynergyStore.loadLegacyAuth()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function saveShared(auth: MetaSynergyAuthState): Promise<void> {
|
|
139
|
+
const filePath = sharedAuthPath()
|
|
140
|
+
let data: Record<string, unknown> = {}
|
|
141
|
+
try {
|
|
142
|
+
data = SynergyAuthRecord.parse(JSON.parse(await readFile(filePath, "utf8")))
|
|
143
|
+
} catch {
|
|
144
|
+
data = {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const next = {
|
|
148
|
+
...data,
|
|
149
|
+
holos: {
|
|
150
|
+
type: "holos",
|
|
151
|
+
agentId: auth.agentID,
|
|
152
|
+
agentSecret: auth.agentSecret,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
157
|
+
await writeFile(filePath, JSON.stringify(next, null, 2) + "\n")
|
|
158
|
+
await chmod(filePath, 0o600)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function removeShared(): Promise<void> {
|
|
162
|
+
let data: Record<string, unknown>
|
|
163
|
+
try {
|
|
164
|
+
data = SynergyAuthRecord.parse(JSON.parse(await readFile(sharedAuthPath(), "utf8")))
|
|
165
|
+
} catch {
|
|
166
|
+
await unlink(sharedAuthPath()).catch(() => undefined)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
delete data.holos
|
|
171
|
+
await mkdir(path.dirname(sharedAuthPath()), { recursive: true })
|
|
172
|
+
await writeFile(sharedAuthPath(), JSON.stringify(data, null, 2) + "\n")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function loadGlobalConfigSource(filePath: string): Promise<string> {
|
|
176
|
+
try {
|
|
177
|
+
const source = await readFile(filePath, "utf8")
|
|
178
|
+
return source.trim().length > 0 ? source : "{}\n"
|
|
179
|
+
} catch {
|
|
180
|
+
return "{}\n"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/holos/login.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import process from "node:process"
|
|
2
2
|
import { createServer, type IncomingMessage } from "node:http"
|
|
3
3
|
import { spawn } from "node:child_process"
|
|
4
|
-
import {
|
|
4
|
+
import { createInterface } from "node:readline/promises"
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
6
|
+
import { MetaSynergyStore, type MetaSynergyAuthState } from "../state/store"
|
|
7
|
+
import { HOLOS_PORTAL_URL, HOLOS_URL, MetaSynergyHolosAuth } from "./auth"
|
|
5
8
|
import { MetaSynergyHolosProtocol } from "./protocol"
|
|
6
9
|
|
|
7
|
-
const
|
|
8
|
-
const HOLOS_URL = `https://${HOLOS_HOST}`
|
|
10
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000
|
|
9
11
|
|
|
10
12
|
export namespace MetaSynergyHolosLogin {
|
|
11
13
|
export function createBindURL(input: { callbackURL: string; state: string }) {
|
|
12
14
|
return (
|
|
13
|
-
`${
|
|
15
|
+
`${HOLOS_PORTAL_URL}/api/v1/holos/agent_tunnel/bind/start` +
|
|
14
16
|
`?local_callback=${encodeURIComponent(input.callbackURL)}` +
|
|
15
17
|
`&state=${encodeURIComponent(input.state)}`
|
|
16
18
|
)
|
|
@@ -27,6 +29,57 @@ export namespace MetaSynergyHolosLogin {
|
|
|
27
29
|
return { valid: true }
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
export async function loginWithExistingCredentials(auth: MetaSynergyAuthState): Promise<{ agentID: string }> {
|
|
33
|
+
const verification = await verifySecret(auth.agentSecret)
|
|
34
|
+
if (!verification.valid) {
|
|
35
|
+
throw new Error(`Credential validation failed: ${verification.reason}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await MetaSynergyHolosAuth.save(auth)
|
|
39
|
+
return { agentID: auth.agentID }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function promptForExistingCredentials(): Promise<MetaSynergyAuthState | null> {
|
|
43
|
+
const agentID = await promptText("Agent ID: ")
|
|
44
|
+
if (!agentID) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const agentSecret = await promptSecret("Agent Secret: ")
|
|
49
|
+
if (!agentSecret) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
agentID,
|
|
55
|
+
agentSecret,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function promptLoginMode(): Promise<"browser" | "existing" | null> {
|
|
60
|
+
if (!input.isTTY || !output.isTTY) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
while (true) {
|
|
65
|
+
output.write(
|
|
66
|
+
["Choose login mode:", " 1) Browser login", " 2) Import existing agent credentials", "Select [1]: "].join(
|
|
67
|
+
"\n",
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const answer = await readLine()
|
|
72
|
+
const normalized = answer.trim().toLowerCase()
|
|
73
|
+
if (normalized === "" || normalized === "1" || normalized === "browser" || normalized === "b") {
|
|
74
|
+
return "browser"
|
|
75
|
+
}
|
|
76
|
+
if (normalized === "2" || normalized === "existing" || normalized === "import" || normalized === "i") {
|
|
77
|
+
return "existing"
|
|
78
|
+
}
|
|
79
|
+
output.write("Invalid selection. Enter 1 or 2.\n\n")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
30
83
|
export async function login(): Promise<{ agentID: string }> {
|
|
31
84
|
await MetaSynergyStore.ensureRoot()
|
|
32
85
|
const state = crypto.randomUUID()
|
|
@@ -75,7 +128,7 @@ export namespace MetaSynergyHolosLogin {
|
|
|
75
128
|
const timer = setTimeout(() => {
|
|
76
129
|
server.close()
|
|
77
130
|
reject(new Error("Login timed out."))
|
|
78
|
-
},
|
|
131
|
+
}, LOGIN_TIMEOUT_MS)
|
|
79
132
|
timer.unref?.()
|
|
80
133
|
})
|
|
81
134
|
|
|
@@ -113,12 +166,10 @@ export namespace MetaSynergyHolosLogin {
|
|
|
113
166
|
throw new Error("Holos exchange did not return an agent secret.")
|
|
114
167
|
}
|
|
115
168
|
|
|
116
|
-
await
|
|
169
|
+
return await loginWithExistingCredentials({
|
|
117
170
|
agentID: exchangeBody.data.agent_id,
|
|
118
171
|
agentSecret,
|
|
119
172
|
})
|
|
120
|
-
|
|
121
|
-
return { agentID: exchangeBody.data.agent_id }
|
|
122
173
|
}
|
|
123
174
|
}
|
|
124
175
|
|
|
@@ -143,6 +194,95 @@ async function launchBrowser(url: string): Promise<void> {
|
|
|
143
194
|
child.unref()
|
|
144
195
|
}
|
|
145
196
|
|
|
197
|
+
async function promptText(prompt: string): Promise<string | null> {
|
|
198
|
+
if (!input.isTTY || !output.isTTY) {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
output.write(prompt)
|
|
203
|
+
const answer = await readLine()
|
|
204
|
+
const value = answer.trim()
|
|
205
|
+
return value ? value : null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function promptSecret(prompt: string): Promise<string | null> {
|
|
209
|
+
if (!input.isTTY || !output.isTTY) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
output.write(prompt)
|
|
214
|
+
const secret = await readSecretLine()
|
|
215
|
+
output.write("\n")
|
|
216
|
+
const value = secret.trim()
|
|
217
|
+
return value ? value : null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function readLine(): Promise<string> {
|
|
221
|
+
const rl = createInterface({ input, output, terminal: false })
|
|
222
|
+
try {
|
|
223
|
+
return await rl.question("")
|
|
224
|
+
} finally {
|
|
225
|
+
rl.close()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function readSecretLine(): Promise<string> {
|
|
230
|
+
if (!input.isTTY) {
|
|
231
|
+
return await readLine()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const previousRawMode = typeof input.setRawMode === "function" ? input.isRaw : undefined
|
|
235
|
+
const chunks: string[] = []
|
|
236
|
+
|
|
237
|
+
return await new Promise<string>((resolve, reject) => {
|
|
238
|
+
const cleanup = () => {
|
|
239
|
+
input.off("data", onData)
|
|
240
|
+
input.off("error", onError)
|
|
241
|
+
if (typeof input.setRawMode === "function") {
|
|
242
|
+
input.setRawMode(Boolean(previousRawMode))
|
|
243
|
+
}
|
|
244
|
+
input.pause()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const finish = () => {
|
|
248
|
+
cleanup()
|
|
249
|
+
resolve(chunks.join(""))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const onError = (error: Error) => {
|
|
253
|
+
cleanup()
|
|
254
|
+
reject(error)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const onData = (chunk: Buffer | string) => {
|
|
258
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8")
|
|
259
|
+
for (const char of text) {
|
|
260
|
+
if (char === "\r" || char === "\n") {
|
|
261
|
+
finish()
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (char === "\u0003") {
|
|
265
|
+
cleanup()
|
|
266
|
+
reject(new Error("Cancelled"))
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
if (char === "\u007f" || char === "\b") {
|
|
270
|
+
chunks.pop()
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
chunks.push(char)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeof input.setRawMode === "function") {
|
|
278
|
+
input.setRawMode(true)
|
|
279
|
+
}
|
|
280
|
+
input.resume()
|
|
281
|
+
input.on("data", onData)
|
|
282
|
+
input.on("error", onError)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
146
286
|
function htmlPage(input: { title: string; status: "success" | "failed"; heading: string; message: string }): string {
|
|
147
287
|
const accent = input.status === "success" ? "#86efac" : "#fca5a5"
|
|
148
288
|
const badge = input.status === "success" ? "Connected" : "Error"
|
package/src/inbound/handler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MetaProtocolEnvelope, MetaProtocolError, MetaProtocolSession } from "@ericsanchezok/meta-protocol"
|
|
2
|
-
import type
|
|
2
|
+
import { HolosCallerSchema, type HolosCaller } from "../types"
|
|
3
3
|
import { RPCHandler } from "../rpc/handler"
|
|
4
4
|
import { RPCRequestSchema, type RPCResult } from "../rpc/schema"
|
|
5
5
|
import { SessionManager } from "../session/manager"
|
|
@@ -14,12 +14,13 @@ export class MetaSynergyInboundHandler {
|
|
|
14
14
|
readonly decideOpen: (input: { caller: HolosCaller; label?: string }) => Promise<SessionOpenDecision>,
|
|
15
15
|
) {}
|
|
16
16
|
|
|
17
|
-
async handle(input: { caller: HolosCaller; body: unknown }): Promise<RPCResult> {
|
|
17
|
+
async handle(input: { caller: HolosCaller | unknown; body: unknown }): Promise<RPCResult> {
|
|
18
18
|
try {
|
|
19
|
+
const caller = HolosCallerSchema.parse(input.caller)
|
|
19
20
|
const request = RPCRequestSchema.parse(input.body)
|
|
20
21
|
MetaSynergyLog.info("inbound.request.accepted", {
|
|
21
|
-
callerAgentID:
|
|
22
|
-
callerOwnerUserID:
|
|
22
|
+
callerAgentID: caller.agentID,
|
|
23
|
+
callerOwnerUserID: caller.ownerUserID,
|
|
23
24
|
tool: request.tool,
|
|
24
25
|
action: request.action,
|
|
25
26
|
requestID: request.requestID,
|
|
@@ -29,13 +30,13 @@ export class MetaSynergyInboundHandler {
|
|
|
29
30
|
})
|
|
30
31
|
|
|
31
32
|
if (request.tool === "session") {
|
|
32
|
-
return this.#handleSession(
|
|
33
|
+
return this.#handleSession(caller, request)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
this.sessions.validateCaller(
|
|
36
|
+
this.sessions.validateCaller(caller, request.sessionID)
|
|
36
37
|
const result = await this.rpc.handle(request)
|
|
37
38
|
MetaSynergyLog.info("inbound.request.completed", {
|
|
38
|
-
callerAgentID:
|
|
39
|
+
callerAgentID: caller.agentID,
|
|
39
40
|
tool: request.tool,
|
|
40
41
|
action: request.action,
|
|
41
42
|
requestID: request.requestID,
|
|
@@ -44,8 +45,13 @@ export class MetaSynergyInboundHandler {
|
|
|
44
45
|
return result
|
|
45
46
|
} catch (error) {
|
|
46
47
|
if (isEnvelopeError(error)) {
|
|
48
|
+
const callerAgentID =
|
|
49
|
+
typeof input.caller === "object" && input.caller !== null && "agentID" in input.caller
|
|
50
|
+
? String((input.caller as { agentID?: unknown }).agentID ?? "unknown")
|
|
51
|
+
: "unknown"
|
|
47
52
|
MetaSynergyLog.warn("inbound.request.failed.envelope", {
|
|
48
|
-
callerAgentID
|
|
53
|
+
callerAgentID,
|
|
54
|
+
|
|
49
55
|
code: error.code,
|
|
50
56
|
message: error.message,
|
|
51
57
|
details: error.details,
|
|
@@ -62,8 +68,13 @@ export class MetaSynergyInboundHandler {
|
|
|
62
68
|
)
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
const callerAgentID =
|
|
72
|
+
typeof input.caller === "object" && input.caller !== null && "agentID" in input.caller
|
|
73
|
+
? String((input.caller as { agentID?: unknown }).agentID ?? "unknown")
|
|
74
|
+
: "unknown"
|
|
65
75
|
MetaSynergyLog.error("inbound.request.failed.unexpected", {
|
|
66
|
-
callerAgentID
|
|
76
|
+
callerAgentID,
|
|
77
|
+
|
|
67
78
|
error: error instanceof Error ? error.message : String(error),
|
|
68
79
|
})
|
|
69
80
|
return errorResult(undefined, "host_internal_error", error instanceof Error ? error.message : String(error))
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MetaSynergyStore } from "../state/store"
|
|
2
|
+
import { MetaSynergyStateMigrations } from "../state/migration"
|
|
3
|
+
import type { MetaSynergyMigration } from "./types"
|
|
4
|
+
export type { MetaSynergyMigration } from "./types"
|
|
5
|
+
|
|
6
|
+
export namespace MetaSynergyMigrationRunner {
|
|
7
|
+
export async function run(): Promise<void> {
|
|
8
|
+
const applied = await MetaSynergyStore.loadMigrationLog()
|
|
9
|
+
const migrations = collect().sort((left, right) => left.id.localeCompare(right.id))
|
|
10
|
+
|
|
11
|
+
for (const migration of migrations) {
|
|
12
|
+
if (applied[migration.id]) continue
|
|
13
|
+
await migration.run()
|
|
14
|
+
applied[migration.id] = Date.now()
|
|
15
|
+
await MetaSynergyStore.saveMigrationLog(applied)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collect(): MetaSynergyMigration[] {
|
|
21
|
+
return [...MetaSynergyStateMigrations.migrations]
|
|
22
|
+
}
|