talentos 1.0.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/README.md +31 -0
- package/dist/agent/adapters.js +363 -0
- package/dist/agent/gateway.js +70 -0
- package/dist/agent/mcp-entry.js +11 -0
- package/dist/agent/serve.js +254 -0
- package/dist/agent/sessions.js +36 -0
- package/dist/agent-config.js +105 -0
- package/dist/agent-http.js +182 -0
- package/dist/agent-link.js +49 -0
- package/dist/agent-service.js +152 -0
- package/dist/args.js +4 -1
- package/dist/commands/agent-serve.js +45 -0
- package/dist/commands/agent.js +278 -0
- package/dist/gateway-install.js +343 -0
- package/dist/gateway-version.js +45 -0
- package/dist/index.js +24 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,6 +17,9 @@ same server over stdio for clients that cannot speak streamable HTTP.
|
|
|
17
17
|
Every command below pins `talentos@1`, the major that speaks the hosted MCP endpoint.
|
|
18
18
|
The `0.x` releases are the retired stdio wrapper and no longer work.
|
|
19
19
|
|
|
20
|
+
Release 1.1.0 includes Rooms agent linking, the gateway, and the `create_deck`
|
|
21
|
+
MCP permission. `talentos@1` picks it up automatically after publication.
|
|
22
|
+
|
|
20
23
|
```bash
|
|
21
24
|
npx talentos@1 init
|
|
22
25
|
```
|
|
@@ -77,6 +80,34 @@ before it is sent, and stored only once TalentOS accepts it.
|
|
|
77
80
|
|
|
78
81
|
## Commands
|
|
79
82
|
|
|
83
|
+
### Agents in Rooms
|
|
84
|
+
|
|
85
|
+
`talentos agent serve --agent <id>` runs the linked provider in its private
|
|
86
|
+
work folder. It keeps one turn active, resumes the same thread from
|
|
87
|
+
`~/.talentos/agents/<id>/sessions.json`, and posts final text automatically.
|
|
88
|
+
The service heartbeats every 20 seconds and stops the child if its lease is
|
|
89
|
+
lost. A required CLI update installs under the agent directory between turns.
|
|
90
|
+
Signing-key provisioning, real staging latency and provider resume proofs are
|
|
91
|
+
tracked in [Agent access](../../docs/features/agent-access/README.md).
|
|
92
|
+
|
|
93
|
+
Use the setup prompt from TalentOS to link Claude Code or Codex. The fragment
|
|
94
|
+
token is private; never paste it into logs or commits. Provider CLI login is
|
|
95
|
+
required even when its desktop app is signed in. Approve the user service
|
|
96
|
+
installation interactively or pass `--yes` after approval.
|
|
97
|
+
|
|
98
|
+
Linking waits for a recent gateway heartbeat before reporting Connected. Real
|
|
99
|
+
provider connection, latency and resume proofs remain staged after publication.
|
|
100
|
+
If the service cannot start, use the printed foreground
|
|
101
|
+
`talentos agent serve --agent <id>` command.
|
|
102
|
+
|
|
103
|
+
`agent status` and `agent list` show agents linked on this machine. Select one
|
|
104
|
+
with `--agent <id>` for `agent logs` or `agent unlink`. Unlink disconnects the
|
|
105
|
+
agent remotely and removes its local service and configuration.
|
|
106
|
+
|
|
107
|
+
Re-running the same link repairs the install and reuses its key. Keep the
|
|
108
|
+
private pending receipt if a response is lost; it permits recovery after the
|
|
109
|
+
one-use link has been consumed.
|
|
110
|
+
|
|
80
111
|
```
|
|
81
112
|
talentos init [--for claude|cursor|codex|all] [--no-pull] [--name <device>]
|
|
82
113
|
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { writePrivateFile } from "../agent-config.js";
|
|
9
|
+
import { redactAgentOutput, } from "../agent-link.js";
|
|
10
|
+
import { gatewayLauncherPath } from "../gateway-install.js";
|
|
11
|
+
const CHILD_ENV = [
|
|
12
|
+
"HOME",
|
|
13
|
+
"PATH",
|
|
14
|
+
"USER",
|
|
15
|
+
"LOGNAME",
|
|
16
|
+
"LANG",
|
|
17
|
+
"LC_ALL",
|
|
18
|
+
"TMPDIR",
|
|
19
|
+
"TEMP",
|
|
20
|
+
"TMP",
|
|
21
|
+
"SystemRoot",
|
|
22
|
+
"XDG_CONFIG_HOME",
|
|
23
|
+
"XDG_CACHE_HOME",
|
|
24
|
+
"CODEX_HOME",
|
|
25
|
+
"CLAUDE_CONFIG_DIR",
|
|
26
|
+
];
|
|
27
|
+
const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
|
|
28
|
+
const CODEX_VERSION = /\b\d+\.\d+\.\d+\b/u;
|
|
29
|
+
const DONT_ASK = /\bdontAsk\b/u;
|
|
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",
|
|
34
|
+
"read_room",
|
|
35
|
+
"list_room_files",
|
|
36
|
+
"read_file",
|
|
37
|
+
"suggest_change",
|
|
38
|
+
"create_file",
|
|
39
|
+
"create_deck",
|
|
40
|
+
"post_message",
|
|
41
|
+
"share_link",
|
|
42
|
+
"whoami",
|
|
43
|
+
];
|
|
44
|
+
const CLAUDE_TOOLS = ROOM_TOOLS.map((tool) => `mcp__talentos__${tool}`).join(",");
|
|
45
|
+
export function childEnvironment(source = process.env) {
|
|
46
|
+
return Object.fromEntries(CHILD_ENV.filter((key) => source[key] !== undefined).map((key) => [
|
|
47
|
+
key,
|
|
48
|
+
source[key],
|
|
49
|
+
]));
|
|
50
|
+
}
|
|
51
|
+
export async function verifyClaudeCode() {
|
|
52
|
+
const execute = promisify(execFile);
|
|
53
|
+
const options = {
|
|
54
|
+
env: childEnvironment(),
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
maxBuffer: 256_000,
|
|
57
|
+
};
|
|
58
|
+
let version = "unknown";
|
|
59
|
+
try {
|
|
60
|
+
const detected = await execute("claude", ["--version"], options);
|
|
61
|
+
version = CLAUDE_VERSION.exec(detected.stdout.trim())?.[0] ?? "unknown";
|
|
62
|
+
const help = await execute("claude", ["--help"], options);
|
|
63
|
+
if (version !== "unknown" && DONT_ASK.test(help.stdout)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Never include provider stderr, which may contain login details.
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
|
|
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
|
+
}
|
|
143
|
+
export function readProviderEvent(provider, value) {
|
|
144
|
+
if (!value || typeof value !== "object") {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
const event = value;
|
|
148
|
+
if (provider === "claude_code") {
|
|
149
|
+
if (event.type === "result") {
|
|
150
|
+
if (event.is_error) {
|
|
151
|
+
return { failed: true };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...(typeof event.session_id === "string"
|
|
155
|
+
? { sessionId: event.session_id }
|
|
156
|
+
: {}),
|
|
157
|
+
...(typeof event.result === "string" ? { text: event.result } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const message = event.message;
|
|
161
|
+
if (event.type === "assistant" &&
|
|
162
|
+
Array.isArray(message?.content) &&
|
|
163
|
+
message.content.some((part) => part.type === "tool_use")) {
|
|
164
|
+
return { status: "Using room tools…" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (event.type === "thread.started" &&
|
|
169
|
+
typeof event.thread_id === "string") {
|
|
170
|
+
return { sessionId: event.thread_id };
|
|
171
|
+
}
|
|
172
|
+
if (event.type === "turn.failed" || event.type === "error") {
|
|
173
|
+
return { failed: true };
|
|
174
|
+
}
|
|
175
|
+
const item = event.item;
|
|
176
|
+
if (event.type === "item.completed" &&
|
|
177
|
+
item?.type === "agent_message" &&
|
|
178
|
+
typeof item.text === "string") {
|
|
179
|
+
return { text: item.text };
|
|
180
|
+
}
|
|
181
|
+
if (event.type === "item.started" && item?.type === "mcp_tool_call") {
|
|
182
|
+
return { status: "Using room tools…" };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
export async function runProvider(input) {
|
|
188
|
+
const { config, job, workDir, sessionId, signal } = input;
|
|
189
|
+
const prompt = JSON.stringify(job.packet), mcpPath = join(dirname(workDir), ".talentos-mcp.json");
|
|
190
|
+
const catalogPath = join(dirname(workDir), ".talentos-codex-models.json");
|
|
191
|
+
let command, args;
|
|
192
|
+
if (config.provider === "claude_code") {
|
|
193
|
+
await writePrivateFile(mcpPath, JSON.stringify({
|
|
194
|
+
mcpServers: {
|
|
195
|
+
talentos: {
|
|
196
|
+
type: "http",
|
|
197
|
+
url: new URL("/mcp", config.apiUrl).href,
|
|
198
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
command = "claude";
|
|
203
|
+
args = [
|
|
204
|
+
"-p",
|
|
205
|
+
prompt,
|
|
206
|
+
"--output-format",
|
|
207
|
+
"stream-json",
|
|
208
|
+
"--verbose",
|
|
209
|
+
"--mcp-config",
|
|
210
|
+
mcpPath,
|
|
211
|
+
"--strict-mcp-config",
|
|
212
|
+
"--append-system-prompt",
|
|
213
|
+
job.packet.instructions,
|
|
214
|
+
"--tools",
|
|
215
|
+
"",
|
|
216
|
+
"--allowedTools",
|
|
217
|
+
CLAUDE_TOOLS,
|
|
218
|
+
"--disallowedTools",
|
|
219
|
+
"Bash,Agent,Task",
|
|
220
|
+
"--permission-mode",
|
|
221
|
+
"dontAsk",
|
|
222
|
+
];
|
|
223
|
+
if (sessionId) {
|
|
224
|
+
args.push("--resume", sessionId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
command = "codex";
|
|
229
|
+
const launcher = gatewayLauncherPath();
|
|
230
|
+
const useLauncher = process.platform !== "win32" && existsSync(launcher);
|
|
231
|
+
await writePrivateFile(catalogPath, await codexRoomCatalog());
|
|
232
|
+
// Resume has a different flag surface. Root overrides are inherited by
|
|
233
|
+
// both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
|
|
234
|
+
args = [
|
|
235
|
+
"-c",
|
|
236
|
+
'sandbox_mode="read-only"',
|
|
237
|
+
"-c",
|
|
238
|
+
'approval_policy="never"',
|
|
239
|
+
"-c",
|
|
240
|
+
"features.shell_tool=false",
|
|
241
|
+
"-c",
|
|
242
|
+
"features.unified_exec=false",
|
|
243
|
+
"-c",
|
|
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)}`,
|
|
267
|
+
"-c",
|
|
268
|
+
`mcp_servers.talentos.args=${JSON.stringify([...(useLauncher ? [] : [fileURLToPath(new URL("../index.js", import.meta.url))]), "agent", "mcp", "--agent", config.agentId])}`,
|
|
269
|
+
"exec",
|
|
270
|
+
];
|
|
271
|
+
if (sessionId) {
|
|
272
|
+
args.push("resume", sessionId);
|
|
273
|
+
}
|
|
274
|
+
args.push("--json", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules", prompt);
|
|
275
|
+
}
|
|
276
|
+
const child = spawn(command, args, {
|
|
277
|
+
cwd: workDir,
|
|
278
|
+
env: childEnvironment(),
|
|
279
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
280
|
+
detached: process.platform !== "win32",
|
|
281
|
+
});
|
|
282
|
+
let killTimer;
|
|
283
|
+
const stop = () => {
|
|
284
|
+
const pid = child.pid;
|
|
285
|
+
if (!pid || child.exitCode !== null || child.signalCode !== null) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const kill = (signal) => {
|
|
289
|
+
try {
|
|
290
|
+
if (process.platform === "win32") {
|
|
291
|
+
child.kill(signal);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
process.kill(-pid, signal);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
/* The process group may have already exited. */
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
kill("SIGTERM");
|
|
302
|
+
killTimer ??= setTimeout(() => kill("SIGKILL"), 5000);
|
|
303
|
+
};
|
|
304
|
+
const ended = new Promise((resolve) => {
|
|
305
|
+
child.once("error", () => resolve(null));
|
|
306
|
+
child.once("close", resolve);
|
|
307
|
+
});
|
|
308
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
309
|
+
if (signal.aborted) {
|
|
310
|
+
stop();
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
let text = "", failed = false, sawResult = false;
|
|
314
|
+
if (!child.stdout) {
|
|
315
|
+
throw new Error("Provider output is unavailable.");
|
|
316
|
+
}
|
|
317
|
+
const lines = createInterface({ input: child.stdout });
|
|
318
|
+
for await (const line of lines) {
|
|
319
|
+
if (line.length > 1_000_000) {
|
|
320
|
+
throw new Error("Provider output exceeded its limit.");
|
|
321
|
+
}
|
|
322
|
+
let value;
|
|
323
|
+
try {
|
|
324
|
+
value = JSON.parse(line);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const event = readProviderEvent(config.provider, value);
|
|
330
|
+
if (event.failed) {
|
|
331
|
+
failed = true;
|
|
332
|
+
}
|
|
333
|
+
if (event.sessionId) {
|
|
334
|
+
await input.remember(event.sessionId);
|
|
335
|
+
}
|
|
336
|
+
if (event.status) {
|
|
337
|
+
input.status(event.status);
|
|
338
|
+
}
|
|
339
|
+
if (event.text !== undefined) {
|
|
340
|
+
text = event.text.slice(0, 12_000);
|
|
341
|
+
}
|
|
342
|
+
const type = value?.type;
|
|
343
|
+
if (type === "result" || type === "turn.completed") {
|
|
344
|
+
sawResult = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const code = await ended;
|
|
348
|
+
if (code !== 0 || failed || !sawResult || signal.aborted) {
|
|
349
|
+
throw new Error("Provider could not finish this turn. Check its login and quota.");
|
|
350
|
+
}
|
|
351
|
+
return { text: redactAgentOutput(text, [config.apiKey]) };
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
signal.removeEventListener("abort", stop);
|
|
355
|
+
stop();
|
|
356
|
+
await ended;
|
|
357
|
+
if (killTimer) {
|
|
358
|
+
clearTimeout(killTimer);
|
|
359
|
+
}
|
|
360
|
+
await rm(mcpPath, { force: true });
|
|
361
|
+
await rm(catalogPath, { force: true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConvexClient } from "convex/browser";
|
|
2
|
+
import { makeFunctionReference } from "convex/server";
|
|
3
|
+
import { isSecureUrl } from "../agent-link.js";
|
|
4
|
+
export class GatewayError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status) {
|
|
7
|
+
super(status === 401 || status === 409
|
|
8
|
+
? "Agent is no longer available. Re-link it from TalentOS."
|
|
9
|
+
: "Agent gateway request failed; retrying.");
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class AgentGateway {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
if (!isSecureUrl(new URL(config.apiUrl))) {
|
|
17
|
+
throw new Error("Agent gateway needs HTTPS or localhost.");
|
|
18
|
+
}
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
async request(path, body = {}, signal) {
|
|
22
|
+
const response = await fetch(new URL(path, this.config.apiUrl), {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
signal: signal
|
|
30
|
+
? AbortSignal.any([signal, AbortSignal.timeout(15_000)])
|
|
31
|
+
: AbortSignal.timeout(15_000),
|
|
32
|
+
redirect: "error",
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new GatewayError(response.status);
|
|
36
|
+
}
|
|
37
|
+
return response.status === 204 ? null : (await response.json());
|
|
38
|
+
}
|
|
39
|
+
async subscribe(wake, fallback) {
|
|
40
|
+
const initial = await this.request("/agent-gateway/token");
|
|
41
|
+
if (!(initial && isSecureUrl(new URL(initial.convexUrl)))) {
|
|
42
|
+
throw new Error("Agent push is unavailable.");
|
|
43
|
+
}
|
|
44
|
+
const client = new ConvexClient(initial.convexUrl, { logger: false });
|
|
45
|
+
let cached = initial;
|
|
46
|
+
client.setAuth(async ({ forceRefreshToken }) => {
|
|
47
|
+
if (forceRefreshToken || cached.expiresAt - Date.now() < 60_000) {
|
|
48
|
+
const renewed = await this.request("/agent-gateway/token");
|
|
49
|
+
if (!renewed) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
cached = renewed;
|
|
53
|
+
}
|
|
54
|
+
return cached.token;
|
|
55
|
+
}, (authenticated) => {
|
|
56
|
+
if (!authenticated) {
|
|
57
|
+
fallback();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const unsubscribe = client.onUpdate(makeFunctionReference("externalAgents/jobs:pendingForAgent"), {}, (count) => {
|
|
61
|
+
if (count > 0) {
|
|
62
|
+
wake();
|
|
63
|
+
}
|
|
64
|
+
}, () => fallback());
|
|
65
|
+
return async () => {
|
|
66
|
+
unsubscribe();
|
|
67
|
+
await client.close();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { localAgents } from "../agent-config.js";
|
|
2
|
+
import { parseArgv } from "../args.js";
|
|
3
|
+
import { AgentClient } from "../client.js";
|
|
4
|
+
import { startMcpProxy } from "../mcp-proxy.js";
|
|
5
|
+
// A local stdio entry avoids putting the bearer key in Codex's argv or child
|
|
6
|
+
// environment. Only the MCP proxy reads the linked-agent credential file.
|
|
7
|
+
const config = (await localAgents()).find((agent) => agent.agentId === parseArgv(process.argv.slice(2)).options.agent);
|
|
8
|
+
if (!config) {
|
|
9
|
+
throw new Error("Linked agent configuration is unavailable.");
|
|
10
|
+
}
|
|
11
|
+
await startMcpProxy(new AgentClient({ apiUrl: config.apiUrl, apiKey: config.apiKey }));
|