talentos 1.1.0 → 1.1.2
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 +130 -20
- package/dist/agent/gateway.js +1 -1
- package/dist/agent/mcp-entry.js +2 -1
- package/dist/agent/serve.js +26 -39
- package/dist/agent-config.js +7 -4
- package/dist/agent-http.js +16 -4
- package/dist/agent-service.js +37 -20
- package/dist/commands/agent-serve.js +71 -17
- package/dist/commands/agent.js +72 -8
- package/dist/gateway-install.js +343 -0
- package/dist/gateway-version.js +45 -0
- package/dist/index.js +4 -2
- package/dist/provider-binary.js +47 -0
- 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,8 @@ 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";
|
|
11
|
+
import { providerPathEnv, resolveProviderBinary } from "../provider-binary.js";
|
|
9
12
|
const CHILD_ENV = [
|
|
10
13
|
"HOME",
|
|
11
14
|
"PATH",
|
|
@@ -23,8 +26,12 @@ const CHILD_ENV = [
|
|
|
23
26
|
"CLAUDE_CONFIG_DIR",
|
|
24
27
|
];
|
|
25
28
|
const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
|
|
29
|
+
const CODEX_VERSION = /\b\d+\.\d+\.\d+\b/u;
|
|
26
30
|
const DONT_ASK = /\bdontAsk\b/u;
|
|
27
|
-
|
|
31
|
+
// Kept package-local for the published CLI. The launch tests compare both
|
|
32
|
+
// adapters against the server's exported ROOM_AGENT_TOOLS contract.
|
|
33
|
+
const ROOM_TOOLS = [
|
|
34
|
+
"list_rooms",
|
|
28
35
|
"read_room",
|
|
29
36
|
"list_room_files",
|
|
30
37
|
"read_file",
|
|
@@ -33,27 +40,31 @@ const CLAUDE_TOOLS = [
|
|
|
33
40
|
"create_deck",
|
|
34
41
|
"post_message",
|
|
35
42
|
"share_link",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export function childEnvironment(source = process.env) {
|
|
40
|
-
return
|
|
41
|
-
key
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
"whoami",
|
|
44
|
+
];
|
|
45
|
+
const CLAUDE_TOOLS = ROOM_TOOLS.map((tool) => `mcp__talentos__${tool}`).join(",");
|
|
46
|
+
export function childEnvironment(source = process.env, binary) {
|
|
47
|
+
return {
|
|
48
|
+
...Object.fromEntries(CHILD_ENV.filter((key) => source[key] !== undefined).map((key) => [
|
|
49
|
+
key,
|
|
50
|
+
source[key],
|
|
51
|
+
])),
|
|
52
|
+
PATH: providerPathEnv(binary, source.HOME),
|
|
53
|
+
};
|
|
44
54
|
}
|
|
45
|
-
export async function verifyClaudeCode() {
|
|
55
|
+
export async function verifyClaudeCode(configured) {
|
|
56
|
+
const binary = resolveProviderBinary("claude_code", configured);
|
|
46
57
|
const execute = promisify(execFile);
|
|
47
58
|
const options = {
|
|
48
|
-
env: childEnvironment(),
|
|
59
|
+
env: childEnvironment(process.env, binary),
|
|
49
60
|
timeout: 5000,
|
|
50
61
|
maxBuffer: 256_000,
|
|
51
62
|
};
|
|
52
63
|
let version = "unknown";
|
|
53
64
|
try {
|
|
54
|
-
const detected = await execute(
|
|
65
|
+
const detected = await execute(binary, ["--version"], options);
|
|
55
66
|
version = CLAUDE_VERSION.exec(detected.stdout.trim())?.[0] ?? "unknown";
|
|
56
|
-
const help = await execute(
|
|
67
|
+
const help = await execute(binary, ["--help"], options);
|
|
57
68
|
if (version !== "unknown" && DONT_ASK.test(help.stdout)) {
|
|
58
69
|
return;
|
|
59
70
|
}
|
|
@@ -63,6 +74,78 @@ export async function verifyClaudeCode() {
|
|
|
63
74
|
}
|
|
64
75
|
throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
|
|
65
76
|
}
|
|
77
|
+
export async function verifyCodex(configured) {
|
|
78
|
+
const binary = resolveProviderBinary("codex", configured);
|
|
79
|
+
const execute = promisify(execFile);
|
|
80
|
+
const options = {
|
|
81
|
+
env: childEnvironment(process.env, binary),
|
|
82
|
+
timeout: 5000,
|
|
83
|
+
maxBuffer: 256_000,
|
|
84
|
+
};
|
|
85
|
+
let version = "unknown";
|
|
86
|
+
try {
|
|
87
|
+
const detected = await execute(binary, ["--version"], options);
|
|
88
|
+
version = CODEX_VERSION.exec(detected.stdout)?.[0] ?? "unknown";
|
|
89
|
+
for (const args of [
|
|
90
|
+
["exec", "--help"],
|
|
91
|
+
["exec", "resume", "--help"],
|
|
92
|
+
]) {
|
|
93
|
+
const help = await execute(binary, args, options);
|
|
94
|
+
if (![
|
|
95
|
+
"--json",
|
|
96
|
+
"--skip-git-repo-check",
|
|
97
|
+
"--ignore-user-config",
|
|
98
|
+
"--ignore-rules",
|
|
99
|
+
].every((flag) => help.stdout.includes(flag))) {
|
|
100
|
+
throw new Error("Unsupported Codex flags");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (version !== "unknown") {
|
|
104
|
+
await codexRoomCatalog(binary);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Provider diagnostics can contain login details.
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Update Codex (${version}) to a version supporting isolated room turns, exec/resume flags and a local model catalog.`);
|
|
112
|
+
}
|
|
113
|
+
async function codexRoomCatalog(binary) {
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await promisify(execFile)(binary, ["debug", "models", "--bundled"], {
|
|
116
|
+
env: childEnvironment(process.env, binary),
|
|
117
|
+
timeout: 5000,
|
|
118
|
+
maxBuffer: 4_000_000,
|
|
119
|
+
});
|
|
120
|
+
const catalog = JSON.parse(stdout);
|
|
121
|
+
if (!Array.isArray(catalog.models) || catalog.models.length === 0) {
|
|
122
|
+
throw new Error("Missing model catalog");
|
|
123
|
+
}
|
|
124
|
+
const models = catalog.models.map((model) => {
|
|
125
|
+
if (!model ||
|
|
126
|
+
typeof model !== "object" ||
|
|
127
|
+
!("slug" in model) ||
|
|
128
|
+
!("apply_patch_tool_type" in model)) {
|
|
129
|
+
throw new Error("Unsupported model catalog");
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...model,
|
|
133
|
+
// apply_patch has no feature toggle: its exposure comes from model
|
|
134
|
+
// metadata. A local catalog removes it before tool registration.
|
|
135
|
+
apply_patch_tool_type: null,
|
|
136
|
+
shell_type: "disabled",
|
|
137
|
+
experimental_supported_tools: [],
|
|
138
|
+
// Code-mode-only models route MCP through native execution. Expose
|
|
139
|
+
// allowed MCP functions directly while native execution stays off.
|
|
140
|
+
tool_mode: "direct",
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
return JSON.stringify({ ...catalog, models });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new Error("Update Codex to a version supporting a local room-only model catalog.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
66
149
|
export function readProviderEvent(provider, value) {
|
|
67
150
|
if (!value || typeof value !== "object") {
|
|
68
151
|
return {};
|
|
@@ -110,6 +193,8 @@ export function readProviderEvent(provider, value) {
|
|
|
110
193
|
export async function runProvider(input) {
|
|
111
194
|
const { config, job, workDir, sessionId, signal } = input;
|
|
112
195
|
const prompt = JSON.stringify(job.packet), mcpPath = join(dirname(workDir), ".talentos-mcp.json");
|
|
196
|
+
const catalogPath = join(dirname(workDir), ".talentos-codex-models.json");
|
|
197
|
+
const binary = resolveProviderBinary(config.provider, config.providerPath);
|
|
113
198
|
let command, args;
|
|
114
199
|
if (config.provider === "claude_code") {
|
|
115
200
|
await writePrivateFile(mcpPath, JSON.stringify({
|
|
@@ -121,7 +206,7 @@ export async function runProvider(input) {
|
|
|
121
206
|
},
|
|
122
207
|
},
|
|
123
208
|
}));
|
|
124
|
-
command =
|
|
209
|
+
command = binary;
|
|
125
210
|
args = [
|
|
126
211
|
"-p",
|
|
127
212
|
prompt,
|
|
@@ -147,13 +232,15 @@ export async function runProvider(input) {
|
|
|
147
232
|
}
|
|
148
233
|
}
|
|
149
234
|
else {
|
|
150
|
-
command =
|
|
151
|
-
const
|
|
235
|
+
command = binary;
|
|
236
|
+
const launcher = gatewayLauncherPath();
|
|
237
|
+
const useLauncher = process.platform !== "win32" && existsSync(launcher);
|
|
238
|
+
await writePrivateFile(catalogPath, await codexRoomCatalog(binary));
|
|
152
239
|
// Resume has a different flag surface. Root overrides are inherited by
|
|
153
240
|
// both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
|
|
154
241
|
args = [
|
|
155
242
|
"-c",
|
|
156
|
-
'sandbox_mode="
|
|
243
|
+
'sandbox_mode="read-only"',
|
|
157
244
|
"-c",
|
|
158
245
|
'approval_policy="never"',
|
|
159
246
|
"-c",
|
|
@@ -161,9 +248,31 @@ export async function runProvider(input) {
|
|
|
161
248
|
"-c",
|
|
162
249
|
"features.unified_exec=false",
|
|
163
250
|
"-c",
|
|
164
|
-
|
|
251
|
+
'web_search="disabled"',
|
|
252
|
+
"-c",
|
|
253
|
+
"features.view_image=false",
|
|
254
|
+
"-c",
|
|
255
|
+
"features.image_generation=false",
|
|
256
|
+
"-c",
|
|
257
|
+
"features.apps=false",
|
|
258
|
+
"-c",
|
|
259
|
+
"features.plugins=false",
|
|
260
|
+
"-c",
|
|
261
|
+
"features.multi_agent=false",
|
|
262
|
+
"-c",
|
|
263
|
+
"features.skip_host_skill_discovery=true",
|
|
264
|
+
"-c",
|
|
265
|
+
"memories.use_memories=false",
|
|
266
|
+
"-c",
|
|
267
|
+
"memories.generate_memories=false",
|
|
268
|
+
"-c",
|
|
269
|
+
`model_catalog_json=${JSON.stringify(catalogPath)}`,
|
|
270
|
+
"-c",
|
|
271
|
+
`mcp_servers.talentos.enabled_tools=${JSON.stringify(ROOM_TOOLS)}`,
|
|
272
|
+
"-c",
|
|
273
|
+
`mcp_servers.talentos.command=${JSON.stringify(useLauncher ? launcher : process.execPath)}`,
|
|
165
274
|
"-c",
|
|
166
|
-
`mcp_servers.talentos.args=${JSON.stringify([
|
|
275
|
+
`mcp_servers.talentos.args=${JSON.stringify([...(useLauncher ? [] : [fileURLToPath(new URL("../index.js", import.meta.url))]), "agent", "mcp", "--agent", config.agentId])}`,
|
|
167
276
|
"exec",
|
|
168
277
|
];
|
|
169
278
|
if (sessionId) {
|
|
@@ -173,7 +282,7 @@ export async function runProvider(input) {
|
|
|
173
282
|
}
|
|
174
283
|
const child = spawn(command, args, {
|
|
175
284
|
cwd: workDir,
|
|
176
|
-
env: childEnvironment(),
|
|
285
|
+
env: childEnvironment(process.env, binary),
|
|
177
286
|
stdio: ["ignore", "pipe", "ignore"],
|
|
178
287
|
detached: process.platform !== "win32",
|
|
179
288
|
});
|
|
@@ -256,5 +365,6 @@ export async function runProvider(input) {
|
|
|
256
365
|
clearTimeout(killTimer);
|
|
257
366
|
}
|
|
258
367
|
await rm(mcpPath, { force: true });
|
|
368
|
+
await rm(catalogPath, { force: true });
|
|
259
369
|
}
|
|
260
370
|
}
|
package/dist/agent/gateway.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isSecureUrl } from "../agent-link.js";
|
|
|
4
4
|
export class GatewayError extends Error {
|
|
5
5
|
status;
|
|
6
6
|
constructor(status) {
|
|
7
|
-
super(status === 401 || status ===
|
|
7
|
+
super(status === 401 || status === 410
|
|
8
8
|
? "Agent is no longer available. Re-link it from TalentOS."
|
|
9
9
|
: "Agent gateway request failed; retrying.");
|
|
10
10
|
this.status = status;
|
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,29 +49,13 @@ 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
|
-
export async function serveAgent(config, directory, version, stop) {
|
|
58
|
+
export async function serveAgent(config, directory, version, stop, verify) {
|
|
84
59
|
const release = await acquireLock(directory);
|
|
85
60
|
try {
|
|
86
61
|
const workDir = join(directory, "work"), sessions = new SessionMap(directory), gateway = new AgentGateway(config);
|
|
@@ -97,7 +72,8 @@ export async function serveAgent(config, directory, version, stop) {
|
|
|
97
72
|
let minimum = version, lastJobAt = Date.now(), push = false, dispose;
|
|
98
73
|
let terminalError;
|
|
99
74
|
const failService = (error) => {
|
|
100
|
-
if (error instanceof GatewayError &&
|
|
75
|
+
if (error instanceof GatewayError &&
|
|
76
|
+
(error.status === 401 || error.status === 410)) {
|
|
101
77
|
terminalError = error;
|
|
102
78
|
service.abort();
|
|
103
79
|
active?.cancel.abort();
|
|
@@ -144,12 +120,14 @@ export async function serveAgent(config, directory, version, stop) {
|
|
|
144
120
|
}
|
|
145
121
|
};
|
|
146
122
|
let heartbeats;
|
|
123
|
+
let updateAfter = 0;
|
|
147
124
|
const watchdog = setInterval(() => {
|
|
148
125
|
if (active && Date.now() >= active.deadline) {
|
|
149
126
|
active.cancel.abort();
|
|
150
127
|
}
|
|
151
128
|
}, 1000);
|
|
152
129
|
try {
|
|
130
|
+
await verify?.();
|
|
153
131
|
await heartbeat();
|
|
154
132
|
try {
|
|
155
133
|
push = true;
|
|
@@ -164,8 +142,17 @@ export async function serveAgent(config, directory, version, stop) {
|
|
|
164
142
|
}
|
|
165
143
|
heartbeats = heartbeatLoop();
|
|
166
144
|
while (!signal.aborted) {
|
|
167
|
-
if (needsUpdate(version, minimum)) {
|
|
168
|
-
|
|
145
|
+
if (needsUpdate(version, minimum) && Date.now() >= updateAfter) {
|
|
146
|
+
if (process.platform === "win32") {
|
|
147
|
+
throw new Error(`Gateway update required; re-run npx talentos@${minimum} agent serve --agent ${config.agentId}. Windows gateways update in the foreground.`);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return await installGatewayUpdate(directory, minimum, signal);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
updateAfter = Date.now() + 60_000;
|
|
154
|
+
console.error("Gateway update unavailable. Serving the current version; retrying in one minute.");
|
|
155
|
+
}
|
|
169
156
|
}
|
|
170
157
|
wakePending = false;
|
|
171
158
|
let job;
|
package/dist/agent-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { redactAgentOutput } from "./agent-link.js";
|
|
5
5
|
import { warn } from "./format.js";
|
|
6
6
|
import { homeDir } from "./install.js";
|
|
@@ -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 {
|
|
@@ -95,6 +95,9 @@ function isLinkedAgent(value) {
|
|
|
95
95
|
typeof config.name === "string" &&
|
|
96
96
|
typeof config.linkHash === "string" &&
|
|
97
97
|
typeof config.linkedAt === "number" &&
|
|
98
|
+
(config.providerPath === undefined ||
|
|
99
|
+
(typeof config.providerPath === "string" &&
|
|
100
|
+
isAbsolute(config.providerPath))) &&
|
|
98
101
|
(config.provider === "claude_code" || config.provider === "codex"));
|
|
99
102
|
}
|
|
100
103
|
export function isMissing(error) {
|
package/dist/agent-http.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { childEnvironment } from "./agent/adapters.js";
|
|
2
3
|
import { isSecureUrl, } from "./agent-link.js";
|
|
4
|
+
import { resolveProviderBinary } from "./provider-binary.js";
|
|
3
5
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
4
6
|
const HEARTBEAT_WINDOW_MS = 60_000;
|
|
5
7
|
export class AgentHttpError extends Error {
|
|
6
8
|
status;
|
|
7
9
|
retryAfterMs;
|
|
8
10
|
constructor(operation, response) {
|
|
9
|
-
super(
|
|
11
|
+
super(response.status === 426
|
|
12
|
+
? "Update TalentOS CLI: run npx --yes --prefer-online talentos@latest agent link again with your setup URL."
|
|
13
|
+
: `${operation} (HTTP ${response.status}). Ask for a fresh setup prompt if this link expired.`);
|
|
10
14
|
this.status = response.status;
|
|
11
15
|
const seconds = Number(response.headers.get("Retry-After"));
|
|
12
16
|
this.retryAfterMs = Number.isFinite(seconds)
|
|
@@ -37,11 +41,12 @@ export async function setupApiOrigin(link, override) {
|
|
|
37
41
|
}
|
|
38
42
|
return apiOrigin(apiUrl);
|
|
39
43
|
}
|
|
40
|
-
export function inspectProvider(provider) {
|
|
41
|
-
const binary = provider
|
|
44
|
+
export function inspectProvider(provider, configured) {
|
|
45
|
+
const binary = resolveProviderBinary(provider, configured);
|
|
42
46
|
const version = spawnSync(binary, ["--version"], {
|
|
43
47
|
encoding: "utf8",
|
|
44
48
|
timeout: REQUEST_TIMEOUT_MS,
|
|
49
|
+
env: childEnvironment(process.env, binary),
|
|
45
50
|
});
|
|
46
51
|
if (version.status !== 0 || !version.stdout?.trim()) {
|
|
47
52
|
throw new Error(`Install the official ${binary} CLI and sign in before linking.`);
|
|
@@ -52,6 +57,7 @@ export function inspectProvider(provider) {
|
|
|
52
57
|
const login = spawnSync(binary, args, {
|
|
53
58
|
encoding: "utf8",
|
|
54
59
|
timeout: REQUEST_TIMEOUT_MS,
|
|
60
|
+
env: childEnvironment(process.env, binary),
|
|
55
61
|
});
|
|
56
62
|
if (login.status !== 0) {
|
|
57
63
|
throw new Error(`Sign in through the official ${binary} CLI before linking.`);
|
|
@@ -92,7 +98,13 @@ export async function redeemAgent(apiUrl, body) {
|
|
|
92
98
|
typeof value.name !== "string") {
|
|
93
99
|
throw new Error("TalentOS returned an invalid agent link. Ask for a fresh prompt.");
|
|
94
100
|
}
|
|
95
|
-
|
|
101
|
+
const retiredAgentIds = "retiredAgentIds" in value ? value.retiredAgentIds : undefined;
|
|
102
|
+
if (retiredAgentIds !== undefined &&
|
|
103
|
+
(!Array.isArray(retiredAgentIds) ||
|
|
104
|
+
retiredAgentIds.some((id) => typeof id !== "string"))) {
|
|
105
|
+
throw new Error("TalentOS returned an invalid agent link. Ask for a fresh prompt.");
|
|
106
|
+
}
|
|
107
|
+
return { agentId: value.agentId, name: value.name, retiredAgentIds };
|
|
96
108
|
}
|
|
97
109
|
export async function fetchAgentStatus(agent) {
|
|
98
110
|
const response = await safeFetch(`${apiOrigin(agent.apiUrl)}/agent-link/status`, { headers: { Authorization: `Bearer ${agent.apiKey}` } });
|
package/dist/agent-service.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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 } from "./gateway-install.js";
|
|
6
6
|
import { homeDir } from "./install.js";
|
|
7
|
+
import { providerPathEnv } from "./provider-binary.js";
|
|
7
8
|
export function runCommand(command, args) {
|
|
8
9
|
return (spawnSync(command, args, { stdio: "ignore", timeout: 15_000 }).status ?? 1);
|
|
9
10
|
}
|
|
@@ -25,18 +26,24 @@ export async function installAgentService(agent, options = {}) {
|
|
|
25
26
|
return { path: "", started: false };
|
|
26
27
|
}
|
|
27
28
|
const args = [
|
|
28
|
-
|
|
29
|
-
options.cliPath ?? fileURLToPath(new URL("./index.js", import.meta.url)),
|
|
29
|
+
gatewayLauncherPath(home),
|
|
30
30
|
"agent",
|
|
31
31
|
"serve",
|
|
32
32
|
"--agent",
|
|
33
33
|
agent.agentId,
|
|
34
34
|
];
|
|
35
35
|
const workDir = agentWorkDir(agent.agentId, home);
|
|
36
|
+
const pathEnv = providerPathEnv(undefined, home, false);
|
|
36
37
|
const content = platform === "darwin"
|
|
37
|
-
? launchdUnit(name, args, workDir, logPath)
|
|
38
|
-
: systemdUnit(args, workDir, logPath);
|
|
38
|
+
? launchdUnit(name, args, workDir, logPath, pathEnv)
|
|
39
|
+
: systemdUnit(args, workDir, logPath, pathEnv);
|
|
39
40
|
const changed = await writePrivateFile(path, content);
|
|
41
|
+
if (options.reload === false) {
|
|
42
|
+
if (platform === "linux" && changed) {
|
|
43
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
44
|
+
}
|
|
45
|
+
return { path, started: true };
|
|
46
|
+
}
|
|
40
47
|
if (platform === "darwin") {
|
|
41
48
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
42
49
|
const loaded = run("launchctl", ["print", `${domain}/${name}`]) === 0;
|
|
@@ -71,26 +78,34 @@ export async function uninstallAgentService(agent, options = {}) {
|
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
73
80
|
const run = options.run ?? runCommand;
|
|
74
|
-
if (platform === "
|
|
75
|
-
run("launchctl", [
|
|
76
|
-
"bootout",
|
|
77
|
-
`gui/${options.uid ?? process.getuid?.() ?? 0}`,
|
|
78
|
-
path,
|
|
79
|
-
]);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
81
|
+
if (platform === "linux") {
|
|
82
82
|
run("systemctl", [
|
|
83
83
|
"--user",
|
|
84
84
|
"disable",
|
|
85
|
-
"--now",
|
|
86
85
|
`talentos-agent@${agent.agentId}.service`,
|
|
87
86
|
]);
|
|
88
87
|
}
|
|
88
|
+
// Delete before asking the supervisor to stop us: it can send SIGTERM immediately.
|
|
89
89
|
await rm(path, { force: true });
|
|
90
|
-
if (platform === "
|
|
90
|
+
if (platform === "darwin") {
|
|
91
|
+
unloadLaunchAgent(agent, options);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
91
94
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
95
|
+
run("systemctl", [
|
|
96
|
+
"--user",
|
|
97
|
+
"--no-block",
|
|
98
|
+
"stop",
|
|
99
|
+
`talentos-agent@${agent.agentId}.service`,
|
|
100
|
+
]);
|
|
92
101
|
}
|
|
93
102
|
}
|
|
103
|
+
export function unloadLaunchAgent(agent, options = {}) {
|
|
104
|
+
(options.run ?? runCommand)("launchctl", [
|
|
105
|
+
"bootout",
|
|
106
|
+
`gui/${options.uid ?? process.getuid?.() ?? 0}/${serviceName(agent.agentId)}`,
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
94
109
|
function serviceName(agentId) {
|
|
95
110
|
return `so.talentos.agent.${agentId}`;
|
|
96
111
|
}
|
|
@@ -103,7 +118,7 @@ function servicePath(agentId, platform, home) {
|
|
|
103
118
|
? join(home, ".config", "systemd", "user", `talentos-agent@${agentId}.service`)
|
|
104
119
|
: "";
|
|
105
120
|
}
|
|
106
|
-
function launchdUnit(name, args, workDir, logPath) {
|
|
121
|
+
function launchdUnit(name, args, workDir, logPath, pathEnv) {
|
|
107
122
|
const xml = (value) => value
|
|
108
123
|
.replaceAll("&", "&")
|
|
109
124
|
.replaceAll("<", "<")
|
|
@@ -116,14 +131,15 @@ function launchdUnit(name, args, workDir, logPath) {
|
|
|
116
131
|
<key>Label</key><string>${xml(name)}</string>
|
|
117
132
|
<key>ProgramArguments</key><array>${args.map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
|
|
118
133
|
<key>WorkingDirectory</key><string>${xml(workDir)}</string>
|
|
119
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(
|
|
134
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(pathEnv)}</string></dict>
|
|
135
|
+
<key>ThrottleInterval</key><integer>60</integer>
|
|
120
136
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
121
137
|
<key>StandardOutPath</key><string>${xml(logPath)}</string>
|
|
122
138
|
<key>StandardErrorPath</key><string>${xml(logPath)}</string>
|
|
123
139
|
</dict></plist>
|
|
124
140
|
`;
|
|
125
141
|
}
|
|
126
|
-
function systemdUnit(args, workDir, logPath) {
|
|
142
|
+
function systemdUnit(args, workDir, logPath, pathEnv) {
|
|
127
143
|
const quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("\n", "\\n").replaceAll("\r", "\\r")}"`;
|
|
128
144
|
return `[Unit]
|
|
129
145
|
Description=TalentOS agent gateway
|
|
@@ -133,9 +149,10 @@ After=network-online.target
|
|
|
133
149
|
Type=simple
|
|
134
150
|
ExecStart=${args.map(quote).join(" ")}
|
|
135
151
|
WorkingDirectory=${quote(workDir)}
|
|
136
|
-
Environment=${quote(`PATH=${
|
|
152
|
+
Environment=${quote(`PATH=${pathEnv}`)}
|
|
137
153
|
Restart=always
|
|
138
|
-
RestartSec=
|
|
154
|
+
RestartSec=60
|
|
155
|
+
RestartPreventExitStatus=78
|
|
139
156
|
StandardOutput=${quote(`append:${logPath}`)}
|
|
140
157
|
StandardError=${quote(`append:${logPath}`)}
|
|
141
158
|
|