talentos 1.1.0 → 1.1.1
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/dist/agent/adapters.js +111 -8
- package/dist/agent/mcp-entry.js +2 -1
- package/dist/agent/serve.js +22 -37
- package/dist/agent-config.js +3 -3
- package/dist/agent-http.js +7 -1
- package/dist/agent-service.js +17 -10
- package/dist/commands/agent-serve.js +16 -13
- package/dist/commands/agent.js +50 -6
- package/dist/gateway-install.js +343 -0
- package/dist/gateway-version.js +45 -0
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/dist/agent/adapters.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { rm } from "node:fs/promises";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
@@ -6,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { writePrivateFile } from "../agent-config.js";
|
|
8
9
|
import { redactAgentOutput, } from "../agent-link.js";
|
|
10
|
+
import { gatewayLauncherPath } from "../gateway-install.js";
|
|
9
11
|
const CHILD_ENV = [
|
|
10
12
|
"HOME",
|
|
11
13
|
"PATH",
|
|
@@ -23,8 +25,12 @@ const CHILD_ENV = [
|
|
|
23
25
|
"CLAUDE_CONFIG_DIR",
|
|
24
26
|
];
|
|
25
27
|
const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
|
|
28
|
+
const CODEX_VERSION = /\b\d+\.\d+\.\d+\b/u;
|
|
26
29
|
const DONT_ASK = /\bdontAsk\b/u;
|
|
27
|
-
|
|
30
|
+
// Kept package-local for the published CLI. The launch tests compare both
|
|
31
|
+
// adapters against the server's exported ROOM_AGENT_TOOLS contract.
|
|
32
|
+
const ROOM_TOOLS = [
|
|
33
|
+
"list_rooms",
|
|
28
34
|
"read_room",
|
|
29
35
|
"list_room_files",
|
|
30
36
|
"read_file",
|
|
@@ -33,9 +39,9 @@ const CLAUDE_TOOLS = [
|
|
|
33
39
|
"create_deck",
|
|
34
40
|
"post_message",
|
|
35
41
|
"share_link",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
"whoami",
|
|
43
|
+
];
|
|
44
|
+
const CLAUDE_TOOLS = ROOM_TOOLS.map((tool) => `mcp__talentos__${tool}`).join(",");
|
|
39
45
|
export function childEnvironment(source = process.env) {
|
|
40
46
|
return Object.fromEntries(CHILD_ENV.filter((key) => source[key] !== undefined).map((key) => [
|
|
41
47
|
key,
|
|
@@ -63,6 +69,77 @@ export async function verifyClaudeCode() {
|
|
|
63
69
|
}
|
|
64
70
|
throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
|
|
65
71
|
}
|
|
72
|
+
export async function verifyCodex() {
|
|
73
|
+
const execute = promisify(execFile);
|
|
74
|
+
const options = {
|
|
75
|
+
env: childEnvironment(),
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
maxBuffer: 256_000,
|
|
78
|
+
};
|
|
79
|
+
let version = "unknown";
|
|
80
|
+
try {
|
|
81
|
+
const detected = await execute("codex", ["--version"], options);
|
|
82
|
+
version = CODEX_VERSION.exec(detected.stdout)?.[0] ?? "unknown";
|
|
83
|
+
for (const args of [
|
|
84
|
+
["exec", "--help"],
|
|
85
|
+
["exec", "resume", "--help"],
|
|
86
|
+
]) {
|
|
87
|
+
const help = await execute("codex", args, options);
|
|
88
|
+
if (![
|
|
89
|
+
"--json",
|
|
90
|
+
"--skip-git-repo-check",
|
|
91
|
+
"--ignore-user-config",
|
|
92
|
+
"--ignore-rules",
|
|
93
|
+
].every((flag) => help.stdout.includes(flag))) {
|
|
94
|
+
throw new Error("Unsupported Codex flags");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (version !== "unknown") {
|
|
98
|
+
await codexRoomCatalog();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Provider diagnostics can contain login details.
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Update Codex (${version}) to a version supporting isolated room turns, exec/resume flags and a local model catalog.`);
|
|
106
|
+
}
|
|
107
|
+
async function codexRoomCatalog() {
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await promisify(execFile)("codex", ["debug", "models", "--bundled"], {
|
|
110
|
+
env: childEnvironment(),
|
|
111
|
+
timeout: 5000,
|
|
112
|
+
maxBuffer: 4_000_000,
|
|
113
|
+
});
|
|
114
|
+
const catalog = JSON.parse(stdout);
|
|
115
|
+
if (!Array.isArray(catalog.models) || catalog.models.length === 0) {
|
|
116
|
+
throw new Error("Missing model catalog");
|
|
117
|
+
}
|
|
118
|
+
const models = catalog.models.map((model) => {
|
|
119
|
+
if (!model ||
|
|
120
|
+
typeof model !== "object" ||
|
|
121
|
+
!("slug" in model) ||
|
|
122
|
+
!("apply_patch_tool_type" in model)) {
|
|
123
|
+
throw new Error("Unsupported model catalog");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...model,
|
|
127
|
+
// apply_patch has no feature toggle: its exposure comes from model
|
|
128
|
+
// metadata. A local catalog removes it before tool registration.
|
|
129
|
+
apply_patch_tool_type: null,
|
|
130
|
+
shell_type: "disabled",
|
|
131
|
+
experimental_supported_tools: [],
|
|
132
|
+
// Code-mode-only models route MCP through native execution. Expose
|
|
133
|
+
// allowed MCP functions directly while native execution stays off.
|
|
134
|
+
tool_mode: "direct",
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
return JSON.stringify({ ...catalog, models });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw new Error("Update Codex to a version supporting a local room-only model catalog.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
66
143
|
export function readProviderEvent(provider, value) {
|
|
67
144
|
if (!value || typeof value !== "object") {
|
|
68
145
|
return {};
|
|
@@ -110,6 +187,7 @@ export function readProviderEvent(provider, value) {
|
|
|
110
187
|
export async function runProvider(input) {
|
|
111
188
|
const { config, job, workDir, sessionId, signal } = input;
|
|
112
189
|
const prompt = JSON.stringify(job.packet), mcpPath = join(dirname(workDir), ".talentos-mcp.json");
|
|
190
|
+
const catalogPath = join(dirname(workDir), ".talentos-codex-models.json");
|
|
113
191
|
let command, args;
|
|
114
192
|
if (config.provider === "claude_code") {
|
|
115
193
|
await writePrivateFile(mcpPath, JSON.stringify({
|
|
@@ -148,12 +226,14 @@ export async function runProvider(input) {
|
|
|
148
226
|
}
|
|
149
227
|
else {
|
|
150
228
|
command = "codex";
|
|
151
|
-
const
|
|
229
|
+
const launcher = gatewayLauncherPath();
|
|
230
|
+
const useLauncher = process.platform !== "win32" && existsSync(launcher);
|
|
231
|
+
await writePrivateFile(catalogPath, await codexRoomCatalog());
|
|
152
232
|
// Resume has a different flag surface. Root overrides are inherited by
|
|
153
233
|
// both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
|
|
154
234
|
args = [
|
|
155
235
|
"-c",
|
|
156
|
-
'sandbox_mode="
|
|
236
|
+
'sandbox_mode="read-only"',
|
|
157
237
|
"-c",
|
|
158
238
|
'approval_policy="never"',
|
|
159
239
|
"-c",
|
|
@@ -161,9 +241,31 @@ export async function runProvider(input) {
|
|
|
161
241
|
"-c",
|
|
162
242
|
"features.unified_exec=false",
|
|
163
243
|
"-c",
|
|
164
|
-
|
|
244
|
+
'web_search="disabled"',
|
|
245
|
+
"-c",
|
|
246
|
+
"features.view_image=false",
|
|
247
|
+
"-c",
|
|
248
|
+
"features.image_generation=false",
|
|
249
|
+
"-c",
|
|
250
|
+
"features.apps=false",
|
|
251
|
+
"-c",
|
|
252
|
+
"features.plugins=false",
|
|
253
|
+
"-c",
|
|
254
|
+
"features.multi_agent=false",
|
|
255
|
+
"-c",
|
|
256
|
+
"features.skip_host_skill_discovery=true",
|
|
257
|
+
"-c",
|
|
258
|
+
"memories.use_memories=false",
|
|
259
|
+
"-c",
|
|
260
|
+
"memories.generate_memories=false",
|
|
261
|
+
"-c",
|
|
262
|
+
`model_catalog_json=${JSON.stringify(catalogPath)}`,
|
|
263
|
+
"-c",
|
|
264
|
+
`mcp_servers.talentos.enabled_tools=${JSON.stringify(ROOM_TOOLS)}`,
|
|
265
|
+
"-c",
|
|
266
|
+
`mcp_servers.talentos.command=${JSON.stringify(useLauncher ? launcher : process.execPath)}`,
|
|
165
267
|
"-c",
|
|
166
|
-
`mcp_servers.talentos.args=${JSON.stringify([
|
|
268
|
+
`mcp_servers.talentos.args=${JSON.stringify([...(useLauncher ? [] : [fileURLToPath(new URL("../index.js", import.meta.url))]), "agent", "mcp", "--agent", config.agentId])}`,
|
|
167
269
|
"exec",
|
|
168
270
|
];
|
|
169
271
|
if (sessionId) {
|
|
@@ -256,5 +358,6 @@ export async function runProvider(input) {
|
|
|
256
358
|
clearTimeout(killTimer);
|
|
257
359
|
}
|
|
258
360
|
await rm(mcpPath, { force: true });
|
|
361
|
+
await rm(catalogPath, { force: true });
|
|
259
362
|
}
|
|
260
363
|
}
|
package/dist/agent/mcp-entry.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { localAgents } from "../agent-config.js";
|
|
2
|
+
import { parseArgv } from "../args.js";
|
|
2
3
|
import { AgentClient } from "../client.js";
|
|
3
4
|
import { startMcpProxy } from "../mcp-proxy.js";
|
|
4
5
|
// A local stdio entry avoids putting the bearer key in Codex's argv or child
|
|
5
6
|
// environment. Only the MCP proxy reads the linked-agent credential file.
|
|
6
|
-
const config = (await localAgents()).find((agent) => agent.agentId === process.argv
|
|
7
|
+
const config = (await localAgents()).find((agent) => agent.agentId === parseArgv(process.argv.slice(2)).options.agent);
|
|
7
8
|
if (!config) {
|
|
8
9
|
throw new Error("Linked agent configuration is unavailable.");
|
|
9
10
|
}
|
package/dist/agent/serve.js
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
5
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
-
import {
|
|
5
|
+
import { installSharedGateway } from "../gateway-install.js";
|
|
6
|
+
import { compareGatewayVersions } from "../gateway-version.js";
|
|
7
|
+
import { runProvider } from "./adapters.js";
|
|
7
8
|
import { AgentGateway, GatewayError } from "./gateway.js";
|
|
8
9
|
import { SessionMap } from "./sessions.js";
|
|
9
10
|
const HEARTBEAT_MS = 20_000;
|
|
10
11
|
const LEASE_SAFETY_MS = 55_000;
|
|
11
12
|
const IDLE_MS = 30 * 60_000;
|
|
12
|
-
const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
13
13
|
export function needsUpdate(current, minimum) {
|
|
14
|
-
|
|
15
|
-
if (!b) {
|
|
14
|
+
if (compareGatewayVersions(minimum, minimum) === undefined) {
|
|
16
15
|
return false;
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
for (let i = 1; i <= 3; i++) {
|
|
22
|
-
if (Number(a[i]) !== Number(b[i])) {
|
|
23
|
-
return Number(a[i]) < Number(b[i]);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
17
|
+
return (compareGatewayVersions(current, minimum) ?? -1) < 0;
|
|
27
18
|
}
|
|
28
19
|
export function claimPollInterval(lastJobAt, now = Date.now()) {
|
|
29
20
|
return now - lastJobAt >= IDLE_MS ? 10_000 : 2000;
|
|
@@ -58,27 +49,11 @@ async function acquireLock(directory) {
|
|
|
58
49
|
};
|
|
59
50
|
}
|
|
60
51
|
export async function installGatewayUpdate(directory, minimum, signal) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await new Promise((resolve, reject) => {
|
|
66
|
-
const child = spawn("npm", [
|
|
67
|
-
"install",
|
|
68
|
-
"--prefix",
|
|
69
|
-
destination,
|
|
70
|
-
"--omit=dev",
|
|
71
|
-
"--ignore-scripts",
|
|
72
|
-
"--no-audit",
|
|
73
|
-
"--no-fund",
|
|
74
|
-
`talentos@${minimum}`,
|
|
75
|
-
], { env: childEnvironment(), stdio: "ignore", signal });
|
|
76
|
-
child.once("error", () => reject(new Error("Gateway update could not start.")));
|
|
77
|
-
child.once("close", (code) => code === 0
|
|
78
|
-
? resolve()
|
|
79
|
-
: reject(new Error("Gateway update failed. Retry when the network is available.")));
|
|
52
|
+
return await installSharedGateway({
|
|
53
|
+
home: dirname(dirname(dirname(directory))),
|
|
54
|
+
version: minimum,
|
|
55
|
+
signal,
|
|
80
56
|
});
|
|
81
|
-
return join(destination, "node_modules", "talentos", "dist", "index.js");
|
|
82
57
|
}
|
|
83
58
|
export async function serveAgent(config, directory, version, stop) {
|
|
84
59
|
const release = await acquireLock(directory);
|
|
@@ -144,6 +119,7 @@ export async function serveAgent(config, directory, version, stop) {
|
|
|
144
119
|
}
|
|
145
120
|
};
|
|
146
121
|
let heartbeats;
|
|
122
|
+
let updateAfter = 0;
|
|
147
123
|
const watchdog = setInterval(() => {
|
|
148
124
|
if (active && Date.now() >= active.deadline) {
|
|
149
125
|
active.cancel.abort();
|
|
@@ -164,8 +140,17 @@ export async function serveAgent(config, directory, version, stop) {
|
|
|
164
140
|
}
|
|
165
141
|
heartbeats = heartbeatLoop();
|
|
166
142
|
while (!signal.aborted) {
|
|
167
|
-
if (needsUpdate(version, minimum)) {
|
|
168
|
-
|
|
143
|
+
if (needsUpdate(version, minimum) && Date.now() >= updateAfter) {
|
|
144
|
+
if (process.platform === "win32") {
|
|
145
|
+
throw new Error(`Gateway update required; re-run npx talentos@${minimum} agent serve --agent ${config.agentId}. Windows gateways update in the foreground.`);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return await installGatewayUpdate(directory, minimum, signal);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
updateAfter = Date.now() + 60_000;
|
|
152
|
+
console.error("Gateway update unavailable. Serving the current version; retrying in one minute.");
|
|
153
|
+
}
|
|
169
154
|
}
|
|
170
155
|
wakePending = false;
|
|
171
156
|
let job;
|
package/dist/agent-config.js
CHANGED
|
@@ -22,11 +22,11 @@ export function agentLogPath(agentId, home = homeDir()) {
|
|
|
22
22
|
agentConfigPath(agentId, home);
|
|
23
23
|
return join(agentsDir(home), agentId, "gateway.log");
|
|
24
24
|
}
|
|
25
|
-
export async function writePrivateFile(path, content) {
|
|
25
|
+
export async function writePrivateFile(path, content, mode = 0o600) {
|
|
26
26
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
27
27
|
try {
|
|
28
28
|
if ((await readFile(path, "utf8")) === content) {
|
|
29
|
-
await chmod(path,
|
|
29
|
+
await chmod(path, mode);
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -37,7 +37,7 @@ export async function writePrivateFile(path, content) {
|
|
|
37
37
|
}
|
|
38
38
|
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
39
39
|
try {
|
|
40
|
-
await writeFile(temporary, content, { mode
|
|
40
|
+
await writeFile(temporary, content, { mode, flag: "wx" });
|
|
41
41
|
await rename(temporary, path);
|
|
42
42
|
}
|
|
43
43
|
finally {
|
package/dist/agent-http.js
CHANGED
|
@@ -92,7 +92,13 @@ export async function redeemAgent(apiUrl, body) {
|
|
|
92
92
|
typeof value.name !== "string") {
|
|
93
93
|
throw new Error("TalentOS returned an invalid agent link. Ask for a fresh prompt.");
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
const retiredAgentIds = "retiredAgentIds" in value ? value.retiredAgentIds : undefined;
|
|
96
|
+
if (retiredAgentIds !== undefined &&
|
|
97
|
+
(!Array.isArray(retiredAgentIds) ||
|
|
98
|
+
retiredAgentIds.some((id) => typeof id !== "string"))) {
|
|
99
|
+
throw new Error("TalentOS returned an invalid agent link. Ask for a fresh prompt.");
|
|
100
|
+
}
|
|
101
|
+
return { agentId: value.agentId, name: value.name, retiredAgentIds };
|
|
96
102
|
}
|
|
97
103
|
export async function fetchAgentStatus(agent) {
|
|
98
104
|
const response = await safeFetch(`${apiOrigin(agent.apiUrl)}/agent-link/status`, { headers: { Authorization: `Bearer ${agent.apiKey}` } });
|
package/dist/agent-service.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { chmod, mkdir, open, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { agentLogPath, agentWorkDir, writePrivateFile, } from "./agent-config.js";
|
|
5
|
+
import { gatewayLauncherPath, servicePathEnv } from "./gateway-install.js";
|
|
6
6
|
import { homeDir } from "./install.js";
|
|
7
7
|
export function runCommand(command, args) {
|
|
8
8
|
return (spawnSync(command, args, { stdio: "ignore", timeout: 15_000 }).status ?? 1);
|
|
@@ -25,8 +25,7 @@ export async function installAgentService(agent, options = {}) {
|
|
|
25
25
|
return { path: "", started: false };
|
|
26
26
|
}
|
|
27
27
|
const args = [
|
|
28
|
-
|
|
29
|
-
options.cliPath ?? fileURLToPath(new URL("./index.js", import.meta.url)),
|
|
28
|
+
gatewayLauncherPath(home),
|
|
30
29
|
"agent",
|
|
31
30
|
"serve",
|
|
32
31
|
"--agent",
|
|
@@ -34,9 +33,15 @@ export async function installAgentService(agent, options = {}) {
|
|
|
34
33
|
];
|
|
35
34
|
const workDir = agentWorkDir(agent.agentId, home);
|
|
36
35
|
const content = platform === "darwin"
|
|
37
|
-
? launchdUnit(name, args, workDir, logPath)
|
|
38
|
-
: systemdUnit(args, workDir, logPath);
|
|
36
|
+
? launchdUnit(name, args, workDir, logPath, servicePathEnv(home))
|
|
37
|
+
: systemdUnit(args, workDir, logPath, servicePathEnv(home));
|
|
39
38
|
const changed = await writePrivateFile(path, content);
|
|
39
|
+
if (options.reload === false) {
|
|
40
|
+
if (platform === "linux" && changed) {
|
|
41
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
42
|
+
}
|
|
43
|
+
return { path, started: true };
|
|
44
|
+
}
|
|
40
45
|
if (platform === "darwin") {
|
|
41
46
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
42
47
|
const loaded = run("launchctl", ["print", `${domain}/${name}`]) === 0;
|
|
@@ -103,7 +108,7 @@ function servicePath(agentId, platform, home) {
|
|
|
103
108
|
? join(home, ".config", "systemd", "user", `talentos-agent@${agentId}.service`)
|
|
104
109
|
: "";
|
|
105
110
|
}
|
|
106
|
-
function launchdUnit(name, args, workDir, logPath) {
|
|
111
|
+
function launchdUnit(name, args, workDir, logPath, pathEnv) {
|
|
107
112
|
const xml = (value) => value
|
|
108
113
|
.replaceAll("&", "&")
|
|
109
114
|
.replaceAll("<", "<")
|
|
@@ -116,14 +121,15 @@ function launchdUnit(name, args, workDir, logPath) {
|
|
|
116
121
|
<key>Label</key><string>${xml(name)}</string>
|
|
117
122
|
<key>ProgramArguments</key><array>${args.map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
|
|
118
123
|
<key>WorkingDirectory</key><string>${xml(workDir)}</string>
|
|
119
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(
|
|
124
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(pathEnv)}</string></dict>
|
|
125
|
+
<key>ThrottleInterval</key><integer>60</integer>
|
|
120
126
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
121
127
|
<key>StandardOutPath</key><string>${xml(logPath)}</string>
|
|
122
128
|
<key>StandardErrorPath</key><string>${xml(logPath)}</string>
|
|
123
129
|
</dict></plist>
|
|
124
130
|
`;
|
|
125
131
|
}
|
|
126
|
-
function systemdUnit(args, workDir, logPath) {
|
|
132
|
+
function systemdUnit(args, workDir, logPath, pathEnv) {
|
|
127
133
|
const quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("\n", "\\n").replaceAll("\r", "\\r")}"`;
|
|
128
134
|
return `[Unit]
|
|
129
135
|
Description=TalentOS agent gateway
|
|
@@ -133,9 +139,10 @@ After=network-online.target
|
|
|
133
139
|
Type=simple
|
|
134
140
|
ExecStart=${args.map(quote).join(" ")}
|
|
135
141
|
WorkingDirectory=${quote(workDir)}
|
|
136
|
-
Environment=${quote(`PATH=${
|
|
142
|
+
Environment=${quote(`PATH=${pathEnv}`)}
|
|
137
143
|
Restart=always
|
|
138
|
-
RestartSec=
|
|
144
|
+
RestartSec=60
|
|
145
|
+
RestartPreventExitStatus=78
|
|
139
146
|
StandardOutput=${quote(`append:${logPath}`)}
|
|
140
147
|
StandardError=${quote(`append:${logPath}`)}
|
|
141
148
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
2
|
import { dirname } from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { verifyClaudeCode, verifyCodex } from "../agent/adapters.js";
|
|
5
4
|
import { serveAgent } from "../agent/serve.js";
|
|
6
5
|
import { agentWorkDir, localAgents } from "../agent-config.js";
|
|
6
|
+
import { installAgentService } from "../agent-service.js";
|
|
7
|
+
import { installSharedGateway } from "../gateway-install.js";
|
|
7
8
|
export async function runAgentServe(parsed) {
|
|
8
9
|
const id = parsed.options.agent;
|
|
9
10
|
const agents = await localAgents();
|
|
@@ -14,26 +15,28 @@ export async function runAgentServe(parsed) {
|
|
|
14
15
|
if (!config) {
|
|
15
16
|
throw new Error("Choose a linked agent with `talentos agent serve --agent <id>`.");
|
|
16
17
|
}
|
|
18
|
+
try {
|
|
19
|
+
if (process.platform !== "win32") {
|
|
20
|
+
await installSharedGateway();
|
|
21
|
+
}
|
|
22
|
+
await installAgentService(config, { reload: false });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error("Gateway installation unavailable. Serving the loaded version; retry talentos agent repair when the installation is available.");
|
|
26
|
+
}
|
|
17
27
|
if (config.provider === "claude_code") {
|
|
18
28
|
await verifyClaudeCode();
|
|
19
29
|
}
|
|
30
|
+
else {
|
|
31
|
+
await verifyCodex();
|
|
32
|
+
}
|
|
20
33
|
const stop = new AbortController();
|
|
21
34
|
const abort = () => stop.abort();
|
|
22
35
|
process.once("SIGTERM", abort);
|
|
23
36
|
process.once("SIGINT", abort);
|
|
24
37
|
const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
25
38
|
try {
|
|
26
|
-
|
|
27
|
-
if (updated && !stop.signal.aborted) {
|
|
28
|
-
await new Promise((resolve, reject) => {
|
|
29
|
-
const child = spawn(process.execPath, [updated, "agent", "serve", "--agent", config.agentId], { env: childEnvironment(), stdio: "inherit", signal: stop.signal });
|
|
30
|
-
child.once("error", () => reject(new Error("Updated gateway could not start.")));
|
|
31
|
-
child.once("close", (code) => {
|
|
32
|
-
process.exitCode = code ?? 1;
|
|
33
|
-
resolve();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|
|
39
|
+
await serveAgent(config, dirname(agentWorkDir(config.agentId)), version, stop.signal);
|
|
37
40
|
}
|
|
38
41
|
finally {
|
|
39
42
|
process.removeListener("SIGTERM", abort);
|
package/dist/commands/agent.js
CHANGED
|
@@ -7,9 +7,11 @@ import { AgentHttpError, fetchAgentStatus, inspectProvider, redeemAgent, setupAp
|
|
|
7
7
|
import { parseAgentLink, redactAgentOutput } from "../agent-link.js";
|
|
8
8
|
import { installAgentService, uninstallAgentService, } from "../agent-service.js";
|
|
9
9
|
import { emit, emitJson, renderTable, warn } from "../format.js";
|
|
10
|
+
import { installSharedGateway } from "../gateway-install.js";
|
|
10
11
|
import { homeDir } from "../install.js";
|
|
11
12
|
import { defaultDeviceName, hashKey, mintKey } from "../login.js";
|
|
12
|
-
|
|
13
|
+
const MAX_PREVIOUS_AGENT_IDS = 100;
|
|
14
|
+
export async function runAgent(parsed, dependencies = {}) {
|
|
13
15
|
switch (parsed.action) {
|
|
14
16
|
case "link": {
|
|
15
17
|
if (!(parsed.options.yes || (await approveInstall()))) {
|
|
@@ -38,6 +40,29 @@ export async function runAgent(parsed) {
|
|
|
38
40
|
}
|
|
39
41
|
return await command.runAgentServe(parsed);
|
|
40
42
|
}
|
|
43
|
+
case "repair": {
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
throw new Error("Windows agents run in the foreground. Use talentos agent serve.");
|
|
46
|
+
}
|
|
47
|
+
const agents = await selectAgents(parsed, false, dependencies.home);
|
|
48
|
+
if (!agents.length) {
|
|
49
|
+
throw new Error("No linked agent matches. Run talentos agent list.");
|
|
50
|
+
}
|
|
51
|
+
await installSharedGateway({
|
|
52
|
+
home: dependencies.home,
|
|
53
|
+
source: dependencies.gatewaySource,
|
|
54
|
+
});
|
|
55
|
+
for (const agent of agents) {
|
|
56
|
+
const service = await (dependencies.install ??
|
|
57
|
+
((config) => installAgentService(config, { home: dependencies.home })))(agent);
|
|
58
|
+
emit(`Repaired ${redactAgentOutput(agent.name)} (${agent.agentId}).${service.started ? "" : " Start talentos agent serve in the foreground."}`);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
case "mcp": {
|
|
63
|
+
await import("../agent/mcp-entry.js");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
41
66
|
case "status":
|
|
42
67
|
case "list":
|
|
43
68
|
return await showAgents(parsed);
|
|
@@ -46,7 +71,7 @@ export async function runAgent(parsed) {
|
|
|
46
71
|
case "logs":
|
|
47
72
|
return await showLogs(parsed);
|
|
48
73
|
default:
|
|
49
|
-
throw new Error("Use talentos agent link | status | list | unlink | logs.");
|
|
74
|
+
throw new Error("Use talentos agent link | repair | status | list | unlink | logs.");
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
@@ -56,12 +81,17 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
56
81
|
const link = parseAgentLink(parsed.options.url ?? "");
|
|
57
82
|
const home = dependencies.home ?? homeDir();
|
|
58
83
|
const linkHash = hashKey(link.token);
|
|
59
|
-
|
|
84
|
+
const savedAgents = await localAgents(home);
|
|
85
|
+
let agent = savedAgents.find((item) => item.linkHash === linkHash && item.provider === link.provider);
|
|
60
86
|
if (agent) {
|
|
61
87
|
await storeAgent(agent, home);
|
|
62
88
|
}
|
|
63
89
|
else {
|
|
64
90
|
const apiUrl = await setupApiOrigin(link, parsed.options["api-url"] ?? process.env.TALENTOS_API_URL);
|
|
91
|
+
const candidates = savedAgents
|
|
92
|
+
.filter((item) => item.provider === link.provider && item.apiUrl === apiUrl)
|
|
93
|
+
.sort((a, b) => b.linkedAt - a.linkedAt)
|
|
94
|
+
.slice(0, MAX_PREVIOUS_AGENT_IDS);
|
|
65
95
|
const pendingPath = join(agentsDir(home), `.pending-${linkHash}.json`);
|
|
66
96
|
let apiKey;
|
|
67
97
|
let recovered;
|
|
@@ -104,6 +134,7 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
104
134
|
const identity = recovered ??
|
|
105
135
|
(await redeemAgent(apiUrl, {
|
|
106
136
|
token: link.token,
|
|
137
|
+
previousAgentIds: candidates.map((item) => item.agentId),
|
|
107
138
|
keyHash: hashKey(apiKey),
|
|
108
139
|
provider: link.provider,
|
|
109
140
|
providerVersion: (dependencies.probe ?? inspectProvider)(link.provider),
|
|
@@ -121,9 +152,22 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
121
152
|
linkedAt: Date.now(),
|
|
122
153
|
};
|
|
123
154
|
await storeAgent(agent, home);
|
|
155
|
+
for (const old of candidates) {
|
|
156
|
+
if (old.agentId !== agent.agentId &&
|
|
157
|
+
identity.retiredAgentIds?.includes(old.agentId)) {
|
|
158
|
+
await uninstallAgentService(old, { home });
|
|
159
|
+
await rm(agentConfigPath(old.agentId, home), { force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
124
162
|
await rm(pendingPath, { force: true });
|
|
125
163
|
}
|
|
126
|
-
const service = await (dependencies.install ??
|
|
164
|
+
const service = await (dependencies.install ??
|
|
165
|
+
(async (config) => {
|
|
166
|
+
if (process.platform !== "win32") {
|
|
167
|
+
await installSharedGateway({ home });
|
|
168
|
+
}
|
|
169
|
+
return await installAgentService(config, { home });
|
|
170
|
+
}))(agent);
|
|
127
171
|
return { agent, service };
|
|
128
172
|
}
|
|
129
173
|
async function showAgents(parsed) {
|
|
@@ -204,9 +248,9 @@ async function showLogs(parsed) {
|
|
|
204
248
|
await log.close();
|
|
205
249
|
}
|
|
206
250
|
}
|
|
207
|
-
async function selectAgents(parsed, single = false) {
|
|
251
|
+
async function selectAgents(parsed, single = false, home) {
|
|
208
252
|
const selector = parsed.options.agent ?? parsed.args[0];
|
|
209
|
-
const agents = (await localAgents()).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
|
|
253
|
+
const agents = (await localAgents(home)).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
|
|
210
254
|
if (single && agents.length > 1) {
|
|
211
255
|
throw new Error("Several agents are linked. Select one by name or --agent <id>.");
|
|
212
256
|
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { cp, link, mkdir, readdir, readFile, readlink, realpath, rename, rm, stat, symlink, } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { writePrivateFile } from "./agent-config.js";
|
|
9
|
+
import { compareGatewayVersions } from "./gateway-version.js";
|
|
10
|
+
import { homeDir } from "./install.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const TRANSIENT_BIN = /(?:\/_npx\/|node_modules\/\.bin|node-gyp-bin|\/Cellar\/|\/\.nvm\/versions\/)/;
|
|
13
|
+
const NPM_ENV = new Set([
|
|
14
|
+
"HOME",
|
|
15
|
+
"USER",
|
|
16
|
+
"LOGNAME",
|
|
17
|
+
"LANG",
|
|
18
|
+
"LC_ALL",
|
|
19
|
+
"TMPDIR",
|
|
20
|
+
"TEMP",
|
|
21
|
+
"TMP",
|
|
22
|
+
"SystemRoot",
|
|
23
|
+
"HTTP_PROXY",
|
|
24
|
+
"HTTPS_PROXY",
|
|
25
|
+
"NO_PROXY",
|
|
26
|
+
"NODE_EXTRA_CA_CERTS",
|
|
27
|
+
]);
|
|
28
|
+
export function gatewayLauncherPath(home = homeDir()) {
|
|
29
|
+
return join(home, ".talentos", "bin", "talentos-gateway");
|
|
30
|
+
}
|
|
31
|
+
export function servicePathEnv(home = homeDir()) {
|
|
32
|
+
return [
|
|
33
|
+
...new Set([
|
|
34
|
+
...(process.env.PATH ?? "")
|
|
35
|
+
.split(":")
|
|
36
|
+
.filter((entry) => entry.startsWith("/") && !TRANSIENT_BIN.test(entry)),
|
|
37
|
+
join(home, ".local/bin"),
|
|
38
|
+
join(home, ".npm-global/bin"),
|
|
39
|
+
"/opt/homebrew/bin",
|
|
40
|
+
"/usr/local/bin",
|
|
41
|
+
"/usr/bin",
|
|
42
|
+
"/bin",
|
|
43
|
+
]),
|
|
44
|
+
].join(":");
|
|
45
|
+
}
|
|
46
|
+
// No user paths are interpolated into executable shell text. The Node hint is data.
|
|
47
|
+
const LAUNCHER = `#!/bin/sh
|
|
48
|
+
set -f
|
|
49
|
+
root="\${0%/*}/.."
|
|
50
|
+
agent=
|
|
51
|
+
next_agent=
|
|
52
|
+
for argument in "$@"; do
|
|
53
|
+
if [ "$next_agent" = yes ]; then agent=$argument; break; fi
|
|
54
|
+
if [ "$argument" = --agent ]; then next_agent=yes; fi
|
|
55
|
+
done
|
|
56
|
+
node_missing=
|
|
57
|
+
case "$agent" in
|
|
58
|
+
''|*[!a-zA-Z0-9_-]*) ;;
|
|
59
|
+
*) node_missing="$root/agents/$agent/.node-missing" ;;
|
|
60
|
+
esac
|
|
61
|
+
qualifies() {
|
|
62
|
+
[ -n "$1" ] && [ -x "$1" ] && "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' >/dev/null 2>&1
|
|
63
|
+
}
|
|
64
|
+
start() {
|
|
65
|
+
if [ -n "$node_missing" ]; then /bin/rm -f "$node_missing"; fi
|
|
66
|
+
node=$1
|
|
67
|
+
shift
|
|
68
|
+
PATH="\${node%/*}:\${PATH:-/usr/bin:/bin}"
|
|
69
|
+
export PATH
|
|
70
|
+
exec "$node" "$root/cli/current/node_modules/talentos/dist/index.js" "$@"
|
|
71
|
+
}
|
|
72
|
+
node_hint=
|
|
73
|
+
IFS= read -r node_hint < "$root/cli/node-path" || :
|
|
74
|
+
if qualifies "\${TALENTOS_NODE:-}"; then
|
|
75
|
+
start "$TALENTOS_NODE" "$@"
|
|
76
|
+
fi
|
|
77
|
+
if qualifies "$node_hint"; then
|
|
78
|
+
start "$node_hint" "$@"
|
|
79
|
+
fi
|
|
80
|
+
old_ifs=$IFS
|
|
81
|
+
IFS=:
|
|
82
|
+
for dir in \${PATH:-}; do
|
|
83
|
+
IFS=$old_ifs
|
|
84
|
+
if qualifies "$dir/node"; then
|
|
85
|
+
start "$dir/node" "$@"
|
|
86
|
+
fi
|
|
87
|
+
IFS=:
|
|
88
|
+
done
|
|
89
|
+
IFS=$old_ifs
|
|
90
|
+
for candidate in /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
91
|
+
if qualifies "$candidate"; then
|
|
92
|
+
start "$candidate" "$@"
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
# nvm has no stable system bin; discover surviving installations at startup.
|
|
96
|
+
set +f
|
|
97
|
+
for candidate in "$root"/../.nvm/versions/node/*/bin/node; do
|
|
98
|
+
if qualifies "$candidate"; then start "$candidate" "$@"; fi
|
|
99
|
+
done
|
|
100
|
+
message='TalentOS gateway requires Node.js 20 or newer. Install Node, then run talentos agent repair.'
|
|
101
|
+
if [ -z "$node_missing" ]; then
|
|
102
|
+
echo "$message" >&2
|
|
103
|
+
elif (set -C; : > "$node_missing") 2>/dev/null; then
|
|
104
|
+
echo "$message" >> "$root/agents/$agent/gateway.log"
|
|
105
|
+
fi
|
|
106
|
+
exit 78
|
|
107
|
+
`;
|
|
108
|
+
async function manifestVersion(root) {
|
|
109
|
+
return JSON.parse(await readFile(join(root, "package.json"), "utf8")).version;
|
|
110
|
+
}
|
|
111
|
+
async function copyPackage(source, destination) {
|
|
112
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
113
|
+
for (const name of ["package.json", "dist", "node_modules"]) {
|
|
114
|
+
try {
|
|
115
|
+
await cp(join(source, name), join(destination, name), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
dereference: true,
|
|
118
|
+
force: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (name !== "node_modules" ||
|
|
123
|
+
error.code !== "ENOENT") {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function installed(root, version) {
|
|
130
|
+
try {
|
|
131
|
+
if ((await manifestVersion(root)) !== version ||
|
|
132
|
+
!(await readFile(join(root, "dist/index.js"))).length) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const { dependencies = {} } = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
136
|
+
// The SDK has no root entrypoint. Load the same exports the CLI uses;
|
|
137
|
+
// imports also catch missing transitive dependencies in partial installs.
|
|
138
|
+
const imports = Object.keys(dependencies).flatMap((name) => name === "@modelcontextprotocol/sdk"
|
|
139
|
+
? [
|
|
140
|
+
`${name}/client/index.js`,
|
|
141
|
+
`${name}/server/index.js`,
|
|
142
|
+
`${name}/types.js`,
|
|
143
|
+
]
|
|
144
|
+
: [name]);
|
|
145
|
+
if (imports.length) {
|
|
146
|
+
await exec(process.execPath, [
|
|
147
|
+
"--input-type=module",
|
|
148
|
+
"--eval",
|
|
149
|
+
`await Promise.all(${JSON.stringify(imports)}.map(name => import(name)))`,
|
|
150
|
+
], { cwd: root, timeout: 15_000 });
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error.killed) {
|
|
156
|
+
throw new Error("Gateway dependency check timed out. Keeping the current version; retry agent repair shortly.");
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function installLock(root, signal) {
|
|
162
|
+
const path = join(root, ".install-lock");
|
|
163
|
+
const pending = join(root, `.lock-${randomUUID()}`);
|
|
164
|
+
const contents = `${process.pid}\n${randomUUID()}`;
|
|
165
|
+
// Publish a populated lock atomically so a second installer never sees an
|
|
166
|
+
// empty new lock and mistakes it for one left behind by a crash.
|
|
167
|
+
await writePrivateFile(pending, contents);
|
|
168
|
+
try {
|
|
169
|
+
for (let attempt = 0; attempt < 600; attempt++) {
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
try {
|
|
172
|
+
await link(pending, path);
|
|
173
|
+
return async () => {
|
|
174
|
+
if ((await readFile(path, "utf8").catch(() => "")) === contents) {
|
|
175
|
+
await rm(path, { force: true });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code !== "EEXIST") {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const lock = await readFile(path, "utf8");
|
|
185
|
+
const metadata = await stat(path);
|
|
186
|
+
const age = Date.now() - metadata.mtimeMs;
|
|
187
|
+
const pid = Number(lock.split("\n")[0]);
|
|
188
|
+
let stale = !lock.trim() || age > 5 * 60_000;
|
|
189
|
+
if (!stale && pid > 0) {
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, 0);
|
|
192
|
+
}
|
|
193
|
+
catch (failure) {
|
|
194
|
+
stale = failure.code === "ESRCH";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (stale && (await readFile(path, "utf8")) === lock) {
|
|
198
|
+
const latest = await stat(path);
|
|
199
|
+
if (latest.dev === metadata.dev && latest.ino === metadata.ino) {
|
|
200
|
+
await rm(path, { force: true });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code !== "ENOENT") {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
await delay(200, undefined, { signal });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Another gateway installation is still running. Retry agent repair shortly.");
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await rm(pending, { force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export async function installSharedGateway(options = {}) {
|
|
221
|
+
const home = options.home ?? homeDir();
|
|
222
|
+
const source = options.source ?? fileURLToPath(new URL("../", import.meta.url));
|
|
223
|
+
const ownVersion = await manifestVersion(source);
|
|
224
|
+
let version = options.version ?? ownVersion;
|
|
225
|
+
if (compareGatewayVersions(version, version) === undefined) {
|
|
226
|
+
throw new Error("Unsupported gateway version.");
|
|
227
|
+
}
|
|
228
|
+
const root = join(home, ".talentos/cli");
|
|
229
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
230
|
+
const release = await installLock(root, options.signal);
|
|
231
|
+
const temporary = join(root, `.install-${randomUUID()}`);
|
|
232
|
+
const pointer = join(root, `.current-${randomUUID()}`);
|
|
233
|
+
let stagedVersion;
|
|
234
|
+
try {
|
|
235
|
+
const active = await readlink(join(root, "current")).catch(() => "");
|
|
236
|
+
if ((compareGatewayVersions(active, version) ?? 0) > 0 &&
|
|
237
|
+
(await installed(join(root, active, "node_modules/talentos"), active))) {
|
|
238
|
+
version = active;
|
|
239
|
+
}
|
|
240
|
+
const destination = join(root, version);
|
|
241
|
+
const pkg = join(destination, "node_modules/talentos");
|
|
242
|
+
if (!(await installed(pkg, version))) {
|
|
243
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
244
|
+
// Local builds have not been published. Copy resolved dependencies too,
|
|
245
|
+
// so pnpm's symlinks cannot strand the installed gateway in a worktree.
|
|
246
|
+
if (version === ownVersion &&
|
|
247
|
+
!source.includes("/node_modules/talentos")) {
|
|
248
|
+
await copyPackage(source, join(temporary, "node_modules/talentos"));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const npmEnv = {
|
|
252
|
+
PATH: `${process.env.PATH ?? servicePathEnv()}:${dirname(process.execPath)}`,
|
|
253
|
+
};
|
|
254
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
255
|
+
if ((NPM_ENV.has(key) || key.toLowerCase().startsWith("npm_config_")) &&
|
|
256
|
+
value) {
|
|
257
|
+
npmEnv[key] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await exec("npm", [
|
|
262
|
+
"install",
|
|
263
|
+
"--prefix",
|
|
264
|
+
temporary,
|
|
265
|
+
"--omit=dev",
|
|
266
|
+
"--ignore-scripts",
|
|
267
|
+
"--no-audit",
|
|
268
|
+
"--no-fund",
|
|
269
|
+
`talentos@${version}`,
|
|
270
|
+
], { env: npmEnv, signal: options.signal, timeout: 90_000 });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
if (version === ownVersion) {
|
|
274
|
+
await rm(temporary, { recursive: true, force: true });
|
|
275
|
+
await copyPackage(source, join(temporary, "node_modules/talentos"));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
throw new Error("Gateway update failed. Serving the current version; retrying when the network is available.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!(await installed(join(temporary, "node_modules/talentos"), version))) {
|
|
283
|
+
throw new Error("Gateway package is incomplete. Restore its runtime dependencies or retry agent repair online.");
|
|
284
|
+
}
|
|
285
|
+
await rm(destination, { recursive: true, force: true });
|
|
286
|
+
await rename(temporary, destination);
|
|
287
|
+
if (version !== active) {
|
|
288
|
+
stagedVersion = destination;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let previous;
|
|
292
|
+
try {
|
|
293
|
+
previous = await readlink(join(root, "current"));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
/* First install. */
|
|
297
|
+
}
|
|
298
|
+
await symlink(version, pointer);
|
|
299
|
+
await rename(pointer, join(root, "current"));
|
|
300
|
+
stagedVersion = undefined;
|
|
301
|
+
const launcher = gatewayLauncherPath(home);
|
|
302
|
+
await writePrivateFile(launcher, LAUNCHER, 0o700);
|
|
303
|
+
let nodePath = options.nodePath ?? process.execPath;
|
|
304
|
+
for (const stable of [
|
|
305
|
+
"/opt/homebrew/bin/node",
|
|
306
|
+
"/usr/local/bin/node",
|
|
307
|
+
"/usr/bin/node",
|
|
308
|
+
]) {
|
|
309
|
+
if ((await realpath(stable).catch(() => "")) === nodePath) {
|
|
310
|
+
nodePath = stable;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Preserve the link-time hint during updates and repairs.
|
|
315
|
+
const hint = join(root, "node-path");
|
|
316
|
+
try {
|
|
317
|
+
await readFile(hint);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
await writePrivateFile(hint, `${nodePath}\n`);
|
|
321
|
+
}
|
|
322
|
+
const keep = new Set([version, previous]);
|
|
323
|
+
const oldVersions = (await readdir(root))
|
|
324
|
+
.filter((entry) => compareGatewayVersions(entry, entry) !== undefined && !keep.has(entry))
|
|
325
|
+
.sort((a, b) => compareGatewayVersions(b, a) ?? 0);
|
|
326
|
+
// A repeat repair retains the other installed version.
|
|
327
|
+
if (previous === version) {
|
|
328
|
+
oldVersions.shift();
|
|
329
|
+
}
|
|
330
|
+
for (const old of oldVersions) {
|
|
331
|
+
await rm(join(root, old), { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
return launcher;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
await rm(temporary, { recursive: true, force: true });
|
|
337
|
+
await rm(pointer, { force: true });
|
|
338
|
+
if (stagedVersion) {
|
|
339
|
+
await rm(stagedVersion, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
await release();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/;
|
|
2
|
+
const NUMERIC_IDENTIFIER = /^\d+$/;
|
|
3
|
+
export function compareGatewayVersions(a, b) {
|
|
4
|
+
const left = VERSION.exec(a), right = VERSION.exec(b);
|
|
5
|
+
if (!(left && right)) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
for (let i = 1; i <= 3; i++) {
|
|
9
|
+
const difference = Number(left[i]) - Number(right[i]);
|
|
10
|
+
if (difference) {
|
|
11
|
+
return difference;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (left[4] === right[4]) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (!left[4]) {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
if (!right[4]) {
|
|
21
|
+
return -1;
|
|
22
|
+
}
|
|
23
|
+
const l = left[4].split("."), r = right[4].split(".");
|
|
24
|
+
for (let i = 0; i < Math.max(l.length, r.length); i++) {
|
|
25
|
+
const x = l[i], y = r[i];
|
|
26
|
+
if (x === undefined) {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
if (y === undefined) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
if (x === y) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const xn = NUMERIC_IDENTIFIER.test(x), yn = NUMERIC_IDENTIFIER.test(y);
|
|
36
|
+
if (xn && yn) {
|
|
37
|
+
return Number(x) - Number(y);
|
|
38
|
+
}
|
|
39
|
+
if (xn !== yn) {
|
|
40
|
+
return xn ? -1 : 1;
|
|
41
|
+
}
|
|
42
|
+
return x < y ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ Usage: talentos <command> [options]
|
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
11
|
init Connect this machine's agents to TalentOS
|
|
12
|
-
agent <link|status|list|unlink|logs> Bring your own agent into Rooms
|
|
12
|
+
agent <link|repair|status|list|unlink|logs> Bring your own agent into Rooms
|
|
13
13
|
login Authorize this machine
|
|
14
14
|
logout Forget the key stored on this machine
|
|
15
15
|
whoami Show the member and org this key acts as
|
|
@@ -32,6 +32,7 @@ const GROUP_USAGE = {
|
|
|
32
32
|
talentos agent status | list [--json] [--agent <id>]
|
|
33
33
|
talentos agent unlink [name] [--agent <id>]
|
|
34
34
|
talentos agent logs [--agent <id>]
|
|
35
|
+
talentos agent repair [--agent <id>]
|
|
35
36
|
talentos agent serve --agent <id>
|
|
36
37
|
|
|
37
38
|
The link command checks the provider's CLI login, mints the key locally, and
|
package/package.json
CHANGED