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 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 }));