talentos 0.3.0 → 1.1.0
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 +145 -103
- package/dist/agent/adapters.js +260 -0
- package/dist/agent/gateway.js +70 -0
- package/dist/agent/mcp-entry.js +10 -0
- package/dist/agent/serve.js +269 -0
- package/dist/agent/sessions.js +36 -0
- package/dist/agent-config.js +105 -0
- package/dist/agent-http.js +176 -0
- package/dist/agent-link.js +49 -0
- package/dist/agent-service.js +145 -0
- package/dist/args.js +52 -0
- package/dist/client.js +87 -0
- package/dist/commands/agent-serve.js +42 -0
- package/dist/commands/agent.js +234 -0
- package/dist/commands/ask.js +74 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/mcp.js +48 -0
- package/dist/commands/memory.js +61 -0
- package/dist/commands/routines.js +44 -0
- package/dist/commands/skills.js +183 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/config.js +70 -0
- package/dist/format.js +54 -0
- package/dist/index.js +139 -91
- package/dist/install.js +253 -0
- package/dist/login.js +86 -0
- package/dist/mcp-proxy.js +17 -0
- package/dist/skill-files.js +109 -0
- package/package.json +12 -4
- package/dist/api.js +0 -210
package/README.md
CHANGED
|
@@ -1,126 +1,168 @@
|
|
|
1
1
|
# talentos
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
step type, summary, and tools.
|
|
23
|
-
- **`create_skill`** (`{ name, summary, instructions, successCriteria?,
|
|
24
|
-
requestedTools? }`) — create a new Skill from plain-text instructions.
|
|
25
|
-
Returns the new `skillId` and its version. The Skill is private to the API
|
|
26
|
-
key's owner, who curates it in their TalentOS Skills library.
|
|
27
|
-
- **`update_skill`** (`{ skillId, name, summary, instructions,
|
|
28
|
-
successCriteria?, requestedTools? }`) — replace one of the API key owner's
|
|
29
|
-
private Skills with a complete new body. Only that owner's private Skills are
|
|
30
|
-
writable; published and shared Skills stay human-owned.
|
|
31
|
-
|
|
32
|
-
The server holds no state and stores nothing. Every call is a single
|
|
33
|
-
authenticated request to the HTTP API; all authorization and visibility rules
|
|
34
|
-
live server-side.
|
|
35
|
-
|
|
36
|
-
## Configuration
|
|
37
|
-
|
|
38
|
-
Only `TALENTOS_API_KEY` is required — the server exits immediately with a message
|
|
39
|
-
on stderr if it's missing. `TALENTOS_API_URL` is optional: it defaults to the
|
|
40
|
-
production TalentOS deployment, so most users never set it.
|
|
41
|
-
|
|
42
|
-
| Variable | Required | Description |
|
|
43
|
-
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
|
|
44
|
-
| `TALENTOS_API_KEY` | yes | An API key you create in the TalentOS admin UI. Sent as `Authorization: Bearer <key>`. Never logged. |
|
|
45
|
-
| `TALENTOS_API_URL` | no | Override the API base URL to target a non-production deployment. Defaults to the production `https://<name>.convex.site` origin. Must be the `.convex.site` domain, **not** `.convex.cloud`. |
|
|
46
|
-
|
|
47
|
-
## Install
|
|
48
|
-
|
|
49
|
-
Nothing to install ahead of time — `npx` fetches the published package on first
|
|
50
|
-
run and caches it:
|
|
3
|
+
The TalentOS CLI. It connects the coding agent on your machine to your team's
|
|
4
|
+
governed brain, for four jobs:
|
|
5
|
+
|
|
6
|
+
1. **Push** a skill your agent wrote into TalentOS, then share it with the company.
|
|
7
|
+
2. **Pull** the company's skills into your agent's skill folder.
|
|
8
|
+
3. **Memory** — read and write what you and the team know.
|
|
9
|
+
4. **Delegate** work to your Talent so it runs inside TalentOS governance
|
|
10
|
+
(connectors, approvals, audit) and reports back.
|
|
11
|
+
|
|
12
|
+
It is an MCP client for the remote TalentOS server. `talentos mcp` also runs that
|
|
13
|
+
same server over stdio for clients that cannot speak streamable HTTP.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Every command below pins `talentos@1`, the major that speaks the hosted MCP endpoint.
|
|
18
|
+
The `0.x` releases are the retired stdio wrapper and no longer work.
|
|
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.
|
|
51
22
|
|
|
52
23
|
```bash
|
|
53
|
-
npx
|
|
24
|
+
npx talentos@1 init
|
|
54
25
|
```
|
|
55
26
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
27
|
+
`init` is the whole setup in one command. It:
|
|
28
|
+
|
|
29
|
+
1. **Signs you in** — skipped when the key already on this machine still works.
|
|
30
|
+
2. **Detects your coding agents** — Claude Code (the `claude` command on your
|
|
31
|
+
PATH, or `~/.claude`), Cursor (`~/.cursor` or `./.cursor`), Codex (`~/.codex`).
|
|
32
|
+
`--for claude|cursor|codex|all` overrides detection.
|
|
33
|
+
3. **Writes the MCP config** for each of them, at user level (see the table below).
|
|
34
|
+
4. **Pulls the company's skills** into `./.agents/skills` — the folder Cursor and
|
|
35
|
+
Codex auto-load — plus `./.claude/skills` when Claude Code is here. `--no-pull`
|
|
36
|
+
skips this.
|
|
37
|
+
|
|
38
|
+
Then ask your agent to call the `talentos` `whoami` tool. Re-running `init` is
|
|
39
|
+
safe: the config entry is replaced (so a new key takes effect) and skills are
|
|
40
|
+
overwritten by slug.
|
|
60
41
|
|
|
61
|
-
|
|
42
|
+
| agent | what `init` writes |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Claude Code | `claude mcp add --scope user --transport http talentos <url> --header ...` (prints the command if `claude` is not on your PATH) |
|
|
45
|
+
| Cursor | `~/.cursor/mcp.json` → `mcpServers.talentos` (other servers kept, mode 0600) |
|
|
46
|
+
| Codex | `~/.codex/config.toml` → `[mcp_servers.talentos]`, the stdio proxy carrying `TALENTOS_API_KEY` and `TALENTOS_API_URL` (mode 0600) |
|
|
47
|
+
|
|
48
|
+
**Nothing is written inside the repo except pulled skills.** Your API key lives in
|
|
49
|
+
`~/.config/talentos/config.json` and in those user-level agent configs, never in
|
|
50
|
+
a file your team could check in.
|
|
51
|
+
|
|
52
|
+
## Login on its own
|
|
62
53
|
|
|
63
54
|
```bash
|
|
64
|
-
|
|
65
|
-
-e TALENTOS_API_KEY=tos_your_key_here \
|
|
66
|
-
-- npx -y talentos
|
|
55
|
+
npx talentos@1 login
|
|
67
56
|
```
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
58
|
+
`login` mints an API key **on your machine**, registers only its SHA-256 hash,
|
|
59
|
+
and asks you to approve the device in TalentOS. The raw key is written to
|
|
60
|
+
`~/.config/talentos/config.json` (mode 0600) and never leaves the machine.
|
|
61
|
+
|
|
62
|
+
It prints an eight-character code and, when the server returns an approval link,
|
|
63
|
+
opens it. Otherwise open TalentOS yourself, go to **Settings → Agents & CLI**, and
|
|
64
|
+
enter the code there.
|
|
65
|
+
|
|
66
|
+
Already have a key? `npx talentos@1 login --key tos_...` — it is checked for shape
|
|
67
|
+
before it is sent, and stored only once TalentOS accepts it.
|
|
68
|
+
|
|
69
|
+
## Connect one agent by hand
|
|
70
|
+
|
|
71
|
+
`talentos mcp install <client>` prints the snippet for your client; add
|
|
72
|
+
`--apply` to write it for you (the same user-level write `init` does):
|
|
73
|
+
|
|
74
|
+
| client | what you get |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `claude` | a `claude mcp add --scope user --transport http` command |
|
|
77
|
+
| `cursor` | the `~/.cursor/mcp.json` entry |
|
|
78
|
+
| `codex` | the `~/.codex/config.toml` entry (stdio, via `npx -y talentos@1 mcp`, with the API url in its env) |
|
|
79
|
+
| `json` | a generic `mcpServers` entry (print only) |
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
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
|
+
|
|
88
111
|
```
|
|
112
|
+
talentos init [--for claude|cursor|codex|all] [--no-pull] [--name <device>]
|
|
113
|
+
|
|
114
|
+
talentos login [--key <key>] [--api-url <url>] [--name <device>]
|
|
115
|
+
talentos logout
|
|
116
|
+
talentos whoami
|
|
89
117
|
|
|
90
|
-
|
|
118
|
+
talentos mcp # stdio proxy to the remote server
|
|
119
|
+
talentos mcp install <claude|cursor|codex|json> [--apply]
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
talentos skills list [--mine|--company] [-q <query>]
|
|
122
|
+
talentos skills get <id|slug> [--out <dir>] # prints SKILL.md, or writes the package
|
|
123
|
+
talentos skills push <SKILL.md|dir> [--id <skillId>]
|
|
124
|
+
talentos skills pull [--for claude|cursor|codex|all] [--global] [--to <dir>] [--mine|--company]
|
|
125
|
+
talentos skills share <id> # refuses: Share this Skill from TalentOS.
|
|
126
|
+
|
|
127
|
+
talentos memory search "<query>" [--personal|--company]
|
|
128
|
+
talentos memory add "<fact>" [--company]
|
|
129
|
+
|
|
130
|
+
talentos ask "<message>" [--thread <id>] [--no-wait]
|
|
131
|
+
talentos routines list
|
|
132
|
+
talentos routines run <id>
|
|
133
|
+
```
|
|
94
134
|
|
|
95
|
-
|
|
96
|
-
|
|
135
|
+
`skills pull` writes to `./.agents/skills` by default. `--for claude` writes to
|
|
136
|
+
`./.claude/skills`, `--for all` writes both, and `--global` puts the same folders
|
|
137
|
+
under your home directory; an explicit `--to` wins over all of them.
|
|
97
138
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```
|
|
139
|
+
One pull fetches at most 100 skills; if it hits that ceiling it says so on stderr
|
|
140
|
+
— narrow it with `--mine`, `--company` or `-q <query>`. Two skills that share a
|
|
141
|
+
slug both land: the second goes to `<slug>-<last 6 of its id>`.
|
|
102
142
|
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
After `skills push`, `skills get`, `ask` and `routines run`, the last line is
|
|
144
|
+
`Open: <url>` — the deep link to that skill or thread in TalentOS.
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
Every command takes `--json` to print the raw structured result instead of text.
|
|
147
|
+
`talentos --help` and `talentos <group> --help` print usage.
|
|
108
148
|
|
|
109
|
-
|
|
149
|
+
A skill package is a `SKILL.md` (YAML frontmatter with `name` and `description`,
|
|
150
|
+
body = the instructions) plus any supporting files next to it. `push` accepts the
|
|
151
|
+
file or the folder that contains it; a folder is walked recursively, skipping
|
|
152
|
+
`node_modules` and `.git`, capped at 100 text files and 512 KiB.
|
|
110
153
|
|
|
111
|
-
|
|
112
|
-
> a Skill for candidate outreach.
|
|
154
|
+
## Config and environment
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
| | |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `~/.config/talentos/config.json` | `{ apiUrl, apiKey }`, mode 0600 (honours `XDG_CONFIG_HOME`) |
|
|
159
|
+
| `TALENTOS_API_KEY` | overrides the stored key |
|
|
160
|
+
| `TALENTOS_API_URL` | overrides the stored API url (default `https://woozy-oyster-769.convex.site`) |
|
|
161
|
+
| `--api-url` | overrides both, for one command |
|
|
116
162
|
|
|
117
|
-
##
|
|
163
|
+
## Exit codes
|
|
118
164
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- **Rate limited (429)** — the key is over its requests-per-minute budget;
|
|
124
|
-
wait a moment and retry.
|
|
125
|
-
- **Could not reach the API** — `TALENTOS_API_URL` is wrong or unreachable;
|
|
126
|
-
confirm it is the `.convex.site` URL.
|
|
165
|
+
| code | meaning |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| 0 | success |
|
|
168
|
+
| 1 | any failure — one line on stderr, nothing on stdout |
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { writePrivateFile } from "../agent-config.js";
|
|
8
|
+
import { redactAgentOutput, } from "../agent-link.js";
|
|
9
|
+
const CHILD_ENV = [
|
|
10
|
+
"HOME",
|
|
11
|
+
"PATH",
|
|
12
|
+
"USER",
|
|
13
|
+
"LOGNAME",
|
|
14
|
+
"LANG",
|
|
15
|
+
"LC_ALL",
|
|
16
|
+
"TMPDIR",
|
|
17
|
+
"TEMP",
|
|
18
|
+
"TMP",
|
|
19
|
+
"SystemRoot",
|
|
20
|
+
"XDG_CONFIG_HOME",
|
|
21
|
+
"XDG_CACHE_HOME",
|
|
22
|
+
"CODEX_HOME",
|
|
23
|
+
"CLAUDE_CONFIG_DIR",
|
|
24
|
+
];
|
|
25
|
+
const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
|
|
26
|
+
const DONT_ASK = /\bdontAsk\b/u;
|
|
27
|
+
const CLAUDE_TOOLS = [
|
|
28
|
+
"read_room",
|
|
29
|
+
"list_room_files",
|
|
30
|
+
"read_file",
|
|
31
|
+
"suggest_change",
|
|
32
|
+
"create_file",
|
|
33
|
+
"create_deck",
|
|
34
|
+
"post_message",
|
|
35
|
+
"share_link",
|
|
36
|
+
]
|
|
37
|
+
.map((tool) => `mcp__talentos__${tool}`)
|
|
38
|
+
.join(",");
|
|
39
|
+
export function childEnvironment(source = process.env) {
|
|
40
|
+
return Object.fromEntries(CHILD_ENV.filter((key) => source[key] !== undefined).map((key) => [
|
|
41
|
+
key,
|
|
42
|
+
source[key],
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
export async function verifyClaudeCode() {
|
|
46
|
+
const execute = promisify(execFile);
|
|
47
|
+
const options = {
|
|
48
|
+
env: childEnvironment(),
|
|
49
|
+
timeout: 5000,
|
|
50
|
+
maxBuffer: 256_000,
|
|
51
|
+
};
|
|
52
|
+
let version = "unknown";
|
|
53
|
+
try {
|
|
54
|
+
const detected = await execute("claude", ["--version"], options);
|
|
55
|
+
version = CLAUDE_VERSION.exec(detected.stdout.trim())?.[0] ?? "unknown";
|
|
56
|
+
const help = await execute("claude", ["--help"], options);
|
|
57
|
+
if (version !== "unknown" && DONT_ASK.test(help.stdout)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Never include provider stderr, which may contain login details.
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
|
|
65
|
+
}
|
|
66
|
+
export function readProviderEvent(provider, value) {
|
|
67
|
+
if (!value || typeof value !== "object") {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const event = value;
|
|
71
|
+
if (provider === "claude_code") {
|
|
72
|
+
if (event.type === "result") {
|
|
73
|
+
if (event.is_error) {
|
|
74
|
+
return { failed: true };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...(typeof event.session_id === "string"
|
|
78
|
+
? { sessionId: event.session_id }
|
|
79
|
+
: {}),
|
|
80
|
+
...(typeof event.result === "string" ? { text: event.result } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const message = event.message;
|
|
84
|
+
if (event.type === "assistant" &&
|
|
85
|
+
Array.isArray(message?.content) &&
|
|
86
|
+
message.content.some((part) => part.type === "tool_use")) {
|
|
87
|
+
return { status: "Using room tools…" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (event.type === "thread.started" &&
|
|
92
|
+
typeof event.thread_id === "string") {
|
|
93
|
+
return { sessionId: event.thread_id };
|
|
94
|
+
}
|
|
95
|
+
if (event.type === "turn.failed" || event.type === "error") {
|
|
96
|
+
return { failed: true };
|
|
97
|
+
}
|
|
98
|
+
const item = event.item;
|
|
99
|
+
if (event.type === "item.completed" &&
|
|
100
|
+
item?.type === "agent_message" &&
|
|
101
|
+
typeof item.text === "string") {
|
|
102
|
+
return { text: item.text };
|
|
103
|
+
}
|
|
104
|
+
if (event.type === "item.started" && item?.type === "mcp_tool_call") {
|
|
105
|
+
return { status: "Using room tools…" };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
export async function runProvider(input) {
|
|
111
|
+
const { config, job, workDir, sessionId, signal } = input;
|
|
112
|
+
const prompt = JSON.stringify(job.packet), mcpPath = join(dirname(workDir), ".talentos-mcp.json");
|
|
113
|
+
let command, args;
|
|
114
|
+
if (config.provider === "claude_code") {
|
|
115
|
+
await writePrivateFile(mcpPath, JSON.stringify({
|
|
116
|
+
mcpServers: {
|
|
117
|
+
talentos: {
|
|
118
|
+
type: "http",
|
|
119
|
+
url: new URL("/mcp", config.apiUrl).href,
|
|
120
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
command = "claude";
|
|
125
|
+
args = [
|
|
126
|
+
"-p",
|
|
127
|
+
prompt,
|
|
128
|
+
"--output-format",
|
|
129
|
+
"stream-json",
|
|
130
|
+
"--verbose",
|
|
131
|
+
"--mcp-config",
|
|
132
|
+
mcpPath,
|
|
133
|
+
"--strict-mcp-config",
|
|
134
|
+
"--append-system-prompt",
|
|
135
|
+
job.packet.instructions,
|
|
136
|
+
"--tools",
|
|
137
|
+
"",
|
|
138
|
+
"--allowedTools",
|
|
139
|
+
CLAUDE_TOOLS,
|
|
140
|
+
"--disallowedTools",
|
|
141
|
+
"Bash,Agent,Task",
|
|
142
|
+
"--permission-mode",
|
|
143
|
+
"dontAsk",
|
|
144
|
+
];
|
|
145
|
+
if (sessionId) {
|
|
146
|
+
args.push("--resume", sessionId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
command = "codex";
|
|
151
|
+
const entry = fileURLToPath(new URL("./mcp-entry.js", import.meta.url));
|
|
152
|
+
// Resume has a different flag surface. Root overrides are inherited by
|
|
153
|
+
// both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
|
|
154
|
+
args = [
|
|
155
|
+
"-c",
|
|
156
|
+
'sandbox_mode="workspace-write"',
|
|
157
|
+
"-c",
|
|
158
|
+
'approval_policy="never"',
|
|
159
|
+
"-c",
|
|
160
|
+
"features.shell_tool=false",
|
|
161
|
+
"-c",
|
|
162
|
+
"features.unified_exec=false",
|
|
163
|
+
"-c",
|
|
164
|
+
`mcp_servers.talentos.command=${JSON.stringify(process.execPath)}`,
|
|
165
|
+
"-c",
|
|
166
|
+
`mcp_servers.talentos.args=${JSON.stringify([entry, config.agentId])}`,
|
|
167
|
+
"exec",
|
|
168
|
+
];
|
|
169
|
+
if (sessionId) {
|
|
170
|
+
args.push("resume", sessionId);
|
|
171
|
+
}
|
|
172
|
+
args.push("--json", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules", prompt);
|
|
173
|
+
}
|
|
174
|
+
const child = spawn(command, args, {
|
|
175
|
+
cwd: workDir,
|
|
176
|
+
env: childEnvironment(),
|
|
177
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
178
|
+
detached: process.platform !== "win32",
|
|
179
|
+
});
|
|
180
|
+
let killTimer;
|
|
181
|
+
const stop = () => {
|
|
182
|
+
const pid = child.pid;
|
|
183
|
+
if (!pid || child.exitCode !== null || child.signalCode !== null) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const kill = (signal) => {
|
|
187
|
+
try {
|
|
188
|
+
if (process.platform === "win32") {
|
|
189
|
+
child.kill(signal);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
process.kill(-pid, signal);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* The process group may have already exited. */
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
kill("SIGTERM");
|
|
200
|
+
killTimer ??= setTimeout(() => kill("SIGKILL"), 5000);
|
|
201
|
+
};
|
|
202
|
+
const ended = new Promise((resolve) => {
|
|
203
|
+
child.once("error", () => resolve(null));
|
|
204
|
+
child.once("close", resolve);
|
|
205
|
+
});
|
|
206
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
207
|
+
if (signal.aborted) {
|
|
208
|
+
stop();
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
let text = "", failed = false, sawResult = false;
|
|
212
|
+
if (!child.stdout) {
|
|
213
|
+
throw new Error("Provider output is unavailable.");
|
|
214
|
+
}
|
|
215
|
+
const lines = createInterface({ input: child.stdout });
|
|
216
|
+
for await (const line of lines) {
|
|
217
|
+
if (line.length > 1_000_000) {
|
|
218
|
+
throw new Error("Provider output exceeded its limit.");
|
|
219
|
+
}
|
|
220
|
+
let value;
|
|
221
|
+
try {
|
|
222
|
+
value = JSON.parse(line);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const event = readProviderEvent(config.provider, value);
|
|
228
|
+
if (event.failed) {
|
|
229
|
+
failed = true;
|
|
230
|
+
}
|
|
231
|
+
if (event.sessionId) {
|
|
232
|
+
await input.remember(event.sessionId);
|
|
233
|
+
}
|
|
234
|
+
if (event.status) {
|
|
235
|
+
input.status(event.status);
|
|
236
|
+
}
|
|
237
|
+
if (event.text !== undefined) {
|
|
238
|
+
text = event.text.slice(0, 12_000);
|
|
239
|
+
}
|
|
240
|
+
const type = value?.type;
|
|
241
|
+
if (type === "result" || type === "turn.completed") {
|
|
242
|
+
sawResult = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const code = await ended;
|
|
246
|
+
if (code !== 0 || failed || !sawResult || signal.aborted) {
|
|
247
|
+
throw new Error("Provider could not finish this turn. Check its login and quota.");
|
|
248
|
+
}
|
|
249
|
+
return { text: redactAgentOutput(text, [config.apiKey]) };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
signal.removeEventListener("abort", stop);
|
|
253
|
+
stop();
|
|
254
|
+
await ended;
|
|
255
|
+
if (killTimer) {
|
|
256
|
+
clearTimeout(killTimer);
|
|
257
|
+
}
|
|
258
|
+
await rm(mcpPath, { force: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
import { localAgents } from "../agent-config.js";
|
|
2
|
+
import { AgentClient } from "../client.js";
|
|
3
|
+
import { startMcpProxy } from "../mcp-proxy.js";
|
|
4
|
+
// A local stdio entry avoids putting the bearer key in Codex's argv or child
|
|
5
|
+
// environment. Only the MCP proxy reads the linked-agent credential file.
|
|
6
|
+
const config = (await localAgents()).find((agent) => agent.agentId === process.argv[2]);
|
|
7
|
+
if (!config) {
|
|
8
|
+
throw new Error("Linked agent configuration is unavailable.");
|
|
9
|
+
}
|
|
10
|
+
await startMcpProxy(new AgentClient({ apiUrl: config.apiUrl, apiKey: config.apiKey }));
|