talentos 1.1.5 → 1.1.6
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 +26 -80
- package/dist/agent/codex-policy.js +67 -0
- package/dist/agent/codex-probe.js +424 -0
- package/dist/agent/serve.js +3 -0
- package/dist/commands/agent-serve.js +14 -11
- package/package.json +1 -1
package/dist/agent/adapters.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
5
4
|
import { dirname, join } from "node:path";
|
|
6
5
|
import { createInterface } from "node:readline";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +9,8 @@ import { writePrivateFile } from "../agent-config.js";
|
|
|
10
9
|
import { redactAgentOutput, } from "../agent-link.js";
|
|
11
10
|
import { gatewayLauncherPath } from "../gateway-install.js";
|
|
12
11
|
import { providerPathEnv, resolveProviderBinary } from "../provider-binary.js";
|
|
12
|
+
import { codexRoomArgs, ROOM_TOOLS } from "./codex-policy.js";
|
|
13
|
+
import { CodexBoundaryError, probeCodexBoundary } from "./codex-probe.js";
|
|
13
14
|
const CHILD_ENV = [
|
|
14
15
|
"HOME",
|
|
15
16
|
"PATH",
|
|
@@ -26,26 +27,9 @@ const CHILD_ENV = [
|
|
|
26
27
|
"CODEX_HOME",
|
|
27
28
|
"CLAUDE_CONFIG_DIR",
|
|
28
29
|
];
|
|
29
|
-
// Shared with the preflight: older Codex parses agents.enabled as a role.
|
|
30
|
-
const CODEX_ROOM_GATES = ["agents.enabled=false", "features.goals=false"];
|
|
31
30
|
const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
|
|
32
31
|
const CODEX_VERSION = /\b\d+\.\d+\.\d+\b/u;
|
|
33
32
|
const DONT_ASK = /\bdontAsk\b/u;
|
|
34
|
-
// Kept package-local for the published CLI. The launch tests compare both
|
|
35
|
-
// adapters against the server's exported ROOM_AGENT_TOOLS contract.
|
|
36
|
-
const ROOM_TOOLS = [
|
|
37
|
-
"list_rooms",
|
|
38
|
-
"read_room",
|
|
39
|
-
"list_room_files",
|
|
40
|
-
"read_file",
|
|
41
|
-
"get_skill",
|
|
42
|
-
"suggest_change",
|
|
43
|
-
"create_file",
|
|
44
|
-
"create_deck",
|
|
45
|
-
"post_message",
|
|
46
|
-
"share_link",
|
|
47
|
-
"whoami",
|
|
48
|
-
];
|
|
49
33
|
const CLAUDE_TOOLS = ROOM_TOOLS.map((tool) => `mcp__talentos__${tool}`).join(",");
|
|
50
34
|
export function childEnvironment(source = process.env, binary) {
|
|
51
35
|
return {
|
|
@@ -78,12 +62,13 @@ export async function verifyClaudeCode(configured) {
|
|
|
78
62
|
}
|
|
79
63
|
throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
|
|
80
64
|
}
|
|
81
|
-
export async function verifyCodex(configured) {
|
|
65
|
+
export async function verifyCodex(configured, signal) {
|
|
82
66
|
const binary = resolveProviderBinary("codex", configured);
|
|
83
67
|
const execute = promisify(execFile);
|
|
84
68
|
const options = {
|
|
85
69
|
env: childEnvironment(process.env, binary),
|
|
86
70
|
timeout: 5000,
|
|
71
|
+
signal,
|
|
87
72
|
maxBuffer: 256_000,
|
|
88
73
|
};
|
|
89
74
|
let version = "unknown";
|
|
@@ -105,26 +90,14 @@ export async function verifyCodex(configured) {
|
|
|
105
90
|
}
|
|
106
91
|
}
|
|
107
92
|
if (version !== "unknown") {
|
|
108
|
-
await codexRoomCatalog(binary);
|
|
109
|
-
const directory = await mkdtemp(join(tmpdir(), "talentos-codex-probe-"));
|
|
110
|
-
try {
|
|
111
|
-
await execute(binary, [
|
|
112
|
-
...CODEX_ROOM_GATES.flatMap((gate) => ["-c", gate]),
|
|
113
|
-
"features",
|
|
114
|
-
"list",
|
|
115
|
-
], {
|
|
116
|
-
...options,
|
|
117
|
-
cwd: directory,
|
|
118
|
-
env: { ...options.env, CODEX_HOME: directory },
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
await rm(directory, { recursive: true, force: true });
|
|
123
|
-
}
|
|
93
|
+
await probeCodexBoundary(binary, version, await codexRoomCatalog(binary), signal);
|
|
124
94
|
return;
|
|
125
95
|
}
|
|
126
96
|
}
|
|
127
|
-
catch {
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof CodexBoundaryError) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
128
101
|
// Provider diagnostics can contain login details.
|
|
129
102
|
}
|
|
130
103
|
throw new Error(`Update Codex (${version}) to a version supporting isolated room turns, exec/resume flags and a local model catalog.`);
|
|
@@ -157,6 +130,7 @@ async function codexRoomCatalog(binary) {
|
|
|
157
130
|
// Code-mode-only models route MCP through native execution. Expose
|
|
158
131
|
// allowed MCP functions directly while native execution stays off.
|
|
159
132
|
tool_mode: "direct",
|
|
133
|
+
supports_search_tool: false,
|
|
160
134
|
};
|
|
161
135
|
});
|
|
162
136
|
return JSON.stringify({ ...catalog, models });
|
|
@@ -258,49 +232,21 @@ export async function runProvider(input) {
|
|
|
258
232
|
await writePrivateFile(catalogPath, await codexRoomCatalog(binary));
|
|
259
233
|
// Resume has a different flag surface. Root overrides are inherited by
|
|
260
234
|
// both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
|
|
261
|
-
args =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"-c",
|
|
277
|
-
"features.apps=false",
|
|
278
|
-
"-c",
|
|
279
|
-
"features.plugins=false",
|
|
280
|
-
"-c",
|
|
281
|
-
"features.multi_agent=false",
|
|
282
|
-
// Codex also gates collaboration separately from the legacy feature.
|
|
283
|
-
...CODEX_ROOM_GATES.flatMap((gate) => ["-c", gate]),
|
|
284
|
-
"-c",
|
|
285
|
-
"features.skip_host_skill_discovery=true",
|
|
286
|
-
"-c",
|
|
287
|
-
"memories.use_memories=false",
|
|
288
|
-
"-c",
|
|
289
|
-
"memories.generate_memories=false",
|
|
290
|
-
"-c",
|
|
291
|
-
`model_catalog_json=${JSON.stringify(catalogPath)}`,
|
|
292
|
-
"-c",
|
|
293
|
-
`mcp_servers.talentos.enabled_tools=${JSON.stringify(ROOM_TOOLS)}`,
|
|
294
|
-
"-c",
|
|
295
|
-
`mcp_servers.talentos.command=${JSON.stringify(useLauncher ? launcher : process.execPath)}`,
|
|
296
|
-
"-c",
|
|
297
|
-
`mcp_servers.talentos.args=${JSON.stringify([...(useLauncher ? [] : [fileURLToPath(new URL("../index.js", import.meta.url))]), "agent", "mcp", "--agent", config.agentId])}`,
|
|
298
|
-
"exec",
|
|
299
|
-
];
|
|
300
|
-
if (sessionId) {
|
|
301
|
-
args.push("resume", sessionId);
|
|
302
|
-
}
|
|
303
|
-
args.push("--json", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules", prompt);
|
|
235
|
+
args = codexRoomArgs({
|
|
236
|
+
catalogPath,
|
|
237
|
+
mcpCommand: useLauncher ? launcher : process.execPath,
|
|
238
|
+
mcpArgs: [
|
|
239
|
+
...(useLauncher
|
|
240
|
+
? []
|
|
241
|
+
: [fileURLToPath(new URL("../index.js", import.meta.url))]),
|
|
242
|
+
"agent",
|
|
243
|
+
"mcp",
|
|
244
|
+
"--agent",
|
|
245
|
+
config.agentId,
|
|
246
|
+
],
|
|
247
|
+
prompt,
|
|
248
|
+
sessionId,
|
|
249
|
+
});
|
|
304
250
|
}
|
|
305
251
|
const child = spawn(command, args, {
|
|
306
252
|
cwd: workDir,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const ROOM_TOOLS = [
|
|
2
|
+
"list_rooms",
|
|
3
|
+
"read_room",
|
|
4
|
+
"list_room_files",
|
|
5
|
+
"read_file",
|
|
6
|
+
"get_skill",
|
|
7
|
+
"suggest_change",
|
|
8
|
+
"create_file",
|
|
9
|
+
"create_deck",
|
|
10
|
+
"post_message",
|
|
11
|
+
"share_link",
|
|
12
|
+
"whoami",
|
|
13
|
+
];
|
|
14
|
+
export function codexRoomArgs(input) {
|
|
15
|
+
const { catalogPath, mcpCommand, mcpArgs, prompt, sessionId } = input;
|
|
16
|
+
const args = [
|
|
17
|
+
"-c",
|
|
18
|
+
'sandbox_mode="read-only"',
|
|
19
|
+
"-c",
|
|
20
|
+
'approval_policy="never"',
|
|
21
|
+
"-c",
|
|
22
|
+
"features.shell_tool=false",
|
|
23
|
+
"-c",
|
|
24
|
+
"features.unified_exec=false",
|
|
25
|
+
"-c",
|
|
26
|
+
'web_search="disabled"',
|
|
27
|
+
"-c",
|
|
28
|
+
"features.view_image=false",
|
|
29
|
+
"-c",
|
|
30
|
+
"features.image_generation=false",
|
|
31
|
+
"-c",
|
|
32
|
+
"features.apps=false",
|
|
33
|
+
"-c",
|
|
34
|
+
"features.plugins=false",
|
|
35
|
+
"-c",
|
|
36
|
+
"features.multi_agent=false",
|
|
37
|
+
// Codex also gates collaboration separately from the legacy feature.
|
|
38
|
+
"-c",
|
|
39
|
+
"agents.enabled=false",
|
|
40
|
+
"-c",
|
|
41
|
+
"features.goals=false",
|
|
42
|
+
"-c",
|
|
43
|
+
"tools.experimental_request_user_input.enabled=false",
|
|
44
|
+
"-c",
|
|
45
|
+
"features.tool_search=false",
|
|
46
|
+
"-c",
|
|
47
|
+
"features.skip_host_skill_discovery=true",
|
|
48
|
+
"-c",
|
|
49
|
+
"memories.use_memories=false",
|
|
50
|
+
"-c",
|
|
51
|
+
"memories.generate_memories=false",
|
|
52
|
+
"-c",
|
|
53
|
+
`model_catalog_json=${JSON.stringify(catalogPath)}`,
|
|
54
|
+
"-c",
|
|
55
|
+
`mcp_servers.talentos.enabled_tools=${JSON.stringify(ROOM_TOOLS)}`,
|
|
56
|
+
"-c",
|
|
57
|
+
`mcp_servers.talentos.command=${JSON.stringify(mcpCommand)}`,
|
|
58
|
+
"-c",
|
|
59
|
+
`mcp_servers.talentos.args=${JSON.stringify(mcpArgs)}`,
|
|
60
|
+
"exec",
|
|
61
|
+
];
|
|
62
|
+
if (sessionId) {
|
|
63
|
+
args.push("resume", sessionId);
|
|
64
|
+
}
|
|
65
|
+
args.push("--json", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules", prompt);
|
|
66
|
+
return args;
|
|
67
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdtemp, readFile, realpath, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { writePrivateFile } from "../agent-config.js";
|
|
9
|
+
import { homeDir } from "../install.js";
|
|
10
|
+
import { ProviderBinaryError, providerPathEnv } from "../provider-binary.js";
|
|
11
|
+
import { codexRoomArgs, ROOM_TOOLS } from "./codex-policy.js";
|
|
12
|
+
const PROBE_MS = 30_000;
|
|
13
|
+
const MAX_BYTES = 4_000_000;
|
|
14
|
+
const PROBE_REVISION = 2;
|
|
15
|
+
const RESOURCE_REFUSAL = /Method not found|unknown MCP server/u;
|
|
16
|
+
const INPUT_TYPES = new Set([
|
|
17
|
+
"message",
|
|
18
|
+
"reasoning",
|
|
19
|
+
"compaction",
|
|
20
|
+
"function_call",
|
|
21
|
+
"function_call_output",
|
|
22
|
+
"tool_search_call",
|
|
23
|
+
"additional_tools",
|
|
24
|
+
"tool_search_output",
|
|
25
|
+
]);
|
|
26
|
+
// These route only to the sole tools-only MCP server, which has no resource
|
|
27
|
+
// handlers. The loopback probe exercises their refusal on fresh and resume.
|
|
28
|
+
const RESOURCE_HELPERS = [
|
|
29
|
+
"list_mcp_resources", // Sole tools-only MCP server: no resources to list.
|
|
30
|
+
"list_mcp_resource_templates", // Sole tools-only MCP server: no templates.
|
|
31
|
+
"read_mcp_resource", // MCP RPC only; the proxy has no resources/read handler.
|
|
32
|
+
];
|
|
33
|
+
export class CodexBoundaryError extends ProviderBinaryError {
|
|
34
|
+
constructor(version) {
|
|
35
|
+
super(`Codex ${version} cannot safely run room jobs. Update Codex, then run talentos agent repair. If this persists, report this version to TalentOS support.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function record(value) {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
40
|
+
throw new Error("Invalid tool envelope");
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/** Names retain their namespace: another MCP server cannot impersonate a room tool. */
|
|
45
|
+
export function codexAdvertisedTools(value) {
|
|
46
|
+
const request = record(value);
|
|
47
|
+
if (request.functions !== undefined) {
|
|
48
|
+
throw new Error("Unknown top-level schema container");
|
|
49
|
+
}
|
|
50
|
+
const names = [];
|
|
51
|
+
function inspect(value, namespace = "") {
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
throw new Error("Invalid tools");
|
|
54
|
+
}
|
|
55
|
+
for (const item of value) {
|
|
56
|
+
const tool = record(item);
|
|
57
|
+
if (tool.type === "namespace") {
|
|
58
|
+
if (namespace ||
|
|
59
|
+
tool.functions !== undefined ||
|
|
60
|
+
tool.function !== undefined ||
|
|
61
|
+
tool.namespace !== undefined ||
|
|
62
|
+
!["functions", "mcp__talentos"].includes(String(tool.name))) {
|
|
63
|
+
throw new Error("Unknown namespace");
|
|
64
|
+
}
|
|
65
|
+
inspect(tool.tools, String(tool.name));
|
|
66
|
+
}
|
|
67
|
+
else if (tool.type === "function" && typeof tool.name === "string") {
|
|
68
|
+
if (tool.tools !== undefined ||
|
|
69
|
+
tool.functions !== undefined ||
|
|
70
|
+
tool.function !== undefined ||
|
|
71
|
+
(namespace &&
|
|
72
|
+
tool.namespace !== undefined &&
|
|
73
|
+
tool.namespace !== namespace)) {
|
|
74
|
+
throw new Error("Conflicting tool envelope");
|
|
75
|
+
}
|
|
76
|
+
const ns = namespace ||
|
|
77
|
+
(typeof tool.namespace === "string" ? tool.namespace : "");
|
|
78
|
+
const name = tool.name;
|
|
79
|
+
const allowed = ns === "mcp__talentos"
|
|
80
|
+
? ROOM_TOOLS.includes(name)
|
|
81
|
+
: ns === "functions" && RESOURCE_HELPERS.includes(name);
|
|
82
|
+
if (!(allowed && tool.parameters) ||
|
|
83
|
+
typeof tool.parameters !== "object" ||
|
|
84
|
+
Array.isArray(tool.parameters)) {
|
|
85
|
+
throw new Error("Unexpected callable tool");
|
|
86
|
+
}
|
|
87
|
+
names.push(`${ns}.${name}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new Error("Unknown tool schema");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (request.tools !== undefined) {
|
|
95
|
+
inspect(request.tools);
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(request.input)) {
|
|
98
|
+
throw new Error("Missing input");
|
|
99
|
+
}
|
|
100
|
+
for (const item of request.input) {
|
|
101
|
+
const input = record(item);
|
|
102
|
+
if (typeof input.type !== "string" || !INPUT_TYPES.has(input.type)) {
|
|
103
|
+
throw new Error("Unknown input envelope");
|
|
104
|
+
}
|
|
105
|
+
if (input.type === "additional_tools" ||
|
|
106
|
+
input.type === "tool_search_output") {
|
|
107
|
+
inspect(input.tools);
|
|
108
|
+
}
|
|
109
|
+
else if (input.tools !== undefined || input.functions !== undefined) {
|
|
110
|
+
throw new Error("Unknown schema envelope");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return names;
|
|
114
|
+
}
|
|
115
|
+
export function probeResponse(response, item) {
|
|
116
|
+
const result = {
|
|
117
|
+
id: "probe",
|
|
118
|
+
object: "response",
|
|
119
|
+
status: "completed",
|
|
120
|
+
output: [item],
|
|
121
|
+
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
|
122
|
+
};
|
|
123
|
+
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
124
|
+
for (const event of [
|
|
125
|
+
{
|
|
126
|
+
type: "response.created",
|
|
127
|
+
response: { ...result, status: "in_progress", output: [] },
|
|
128
|
+
},
|
|
129
|
+
{ type: "response.output_item.added", output_index: 0, item },
|
|
130
|
+
{ type: "response.output_item.done", output_index: 0, item },
|
|
131
|
+
{ type: "response.completed", response: result },
|
|
132
|
+
]) {
|
|
133
|
+
response.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
134
|
+
}
|
|
135
|
+
response.end();
|
|
136
|
+
}
|
|
137
|
+
async function execute(binary, args, cwd, env, signal) {
|
|
138
|
+
signal.throwIfAborted();
|
|
139
|
+
const child = spawn(binary, args, {
|
|
140
|
+
cwd,
|
|
141
|
+
env,
|
|
142
|
+
detached: process.platform !== "win32",
|
|
143
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
144
|
+
});
|
|
145
|
+
const kill = () => {
|
|
146
|
+
try {
|
|
147
|
+
if (process.platform === "win32") {
|
|
148
|
+
child.kill("SIGKILL");
|
|
149
|
+
}
|
|
150
|
+
else if (child.pid) {
|
|
151
|
+
process.kill(-child.pid, "SIGKILL");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* The child may already have exited. */
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
signal.addEventListener("abort", kill, { once: true });
|
|
159
|
+
try {
|
|
160
|
+
return await new Promise((resolve, reject) => {
|
|
161
|
+
let output = "";
|
|
162
|
+
child.stdout.on("data", (chunk) => {
|
|
163
|
+
output += chunk.toString();
|
|
164
|
+
if (Buffer.byteLength(output) > MAX_BYTES) {
|
|
165
|
+
kill();
|
|
166
|
+
reject(new Error("Probe output limit"));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
child.once("error", reject);
|
|
170
|
+
child.once("close", (code) => code === 0 && !signal.aborted
|
|
171
|
+
? resolve(output)
|
|
172
|
+
: reject(new Error("Probe failed")));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
signal.removeEventListener("abort", kill);
|
|
177
|
+
kill();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function digest(value) {
|
|
181
|
+
return createHash("sha256").update(value).digest("hex");
|
|
182
|
+
}
|
|
183
|
+
async function identity(binary) {
|
|
184
|
+
const target = await realpath(binary);
|
|
185
|
+
const info = await stat(target);
|
|
186
|
+
return JSON.stringify([
|
|
187
|
+
binary,
|
|
188
|
+
target,
|
|
189
|
+
info.dev,
|
|
190
|
+
info.ino,
|
|
191
|
+
info.size,
|
|
192
|
+
info.mtimeMs,
|
|
193
|
+
info.ctimeMs,
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
/** A synthetic turn uses no owner login or live API. Inconclusive probes never permit service. */
|
|
197
|
+
export async function probeCodexBoundary(binary, version, catalog, stop) {
|
|
198
|
+
try {
|
|
199
|
+
await proveCodexBoundary(binary, version, catalog, stop);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new CodexBoundaryError(version);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function proveCodexBoundary(binary, version, catalog, stop) {
|
|
206
|
+
const before = await identity(binary);
|
|
207
|
+
const key = digest(JSON.stringify([
|
|
208
|
+
PROBE_REVISION,
|
|
209
|
+
RESOURCE_HELPERS,
|
|
210
|
+
before,
|
|
211
|
+
version,
|
|
212
|
+
catalog,
|
|
213
|
+
codexRoomArgs({
|
|
214
|
+
catalogPath: "catalog",
|
|
215
|
+
mcpCommand: "mcp",
|
|
216
|
+
mcpArgs: [],
|
|
217
|
+
prompt: "probe",
|
|
218
|
+
}),
|
|
219
|
+
]));
|
|
220
|
+
const cache = join(homeDir(), ".talentos", "codex-boundary", `${key}.json`);
|
|
221
|
+
try {
|
|
222
|
+
if ((await readFile(cache, "utf8")) === JSON.stringify({ key, safe: true })) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* Missing or corrupt cache requires a new proof. */
|
|
228
|
+
}
|
|
229
|
+
const directory = await mkdtemp(join(tmpdir(), "talentos-codex-boundary-"));
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
const timer = setTimeout(() => controller.abort(), PROBE_MS);
|
|
232
|
+
const signal = stop
|
|
233
|
+
? AbortSignal.any([stop, controller.signal])
|
|
234
|
+
: controller.signal;
|
|
235
|
+
const server = createServer();
|
|
236
|
+
let failure;
|
|
237
|
+
let turn = 0;
|
|
238
|
+
let requests = 0;
|
|
239
|
+
const observations = [new Set(), new Set()];
|
|
240
|
+
const nonce = randomUUID();
|
|
241
|
+
const marker = `room-probe-${nonce}`;
|
|
242
|
+
const calls = [[], []];
|
|
243
|
+
const finished = [false, false];
|
|
244
|
+
const turnSteps = [];
|
|
245
|
+
let canaryUrl = "";
|
|
246
|
+
const canary = join(directory, "private-canary");
|
|
247
|
+
try {
|
|
248
|
+
await writeFile(canary, nonce, { mode: 0o600 });
|
|
249
|
+
const catalogPath = join(directory, "catalog.json");
|
|
250
|
+
await writeFile(catalogPath, catalog, { mode: 0o600 });
|
|
251
|
+
const mcp = join(directory, "mcp.mjs");
|
|
252
|
+
// Match startMcpProxy's sole capability and handlers. No resources, owner
|
|
253
|
+
// credentials, filesystem readers or generic network tool can be reached.
|
|
254
|
+
await writeFile(mcp, `import {Server} from ${JSON.stringify(import.meta.resolve("@modelcontextprotocol/sdk/server/index.js"))};
|
|
255
|
+
import {StdioServerTransport} from ${JSON.stringify(import.meta.resolve("@modelcontextprotocol/sdk/server/stdio.js"))};
|
|
256
|
+
import {ListToolsRequestSchema,CallToolRequestSchema} from ${JSON.stringify(import.meta.resolve("@modelcontextprotocol/sdk/types.js"))};
|
|
257
|
+
const server=new Server({name:'talentos',version:'probe'},{capabilities:{tools:{}}});
|
|
258
|
+
server.setRequestHandler(ListToolsRequestSchema,async()=>({tools:${JSON.stringify([...ROOM_TOOLS, "search_memory", "run_routine"].map((name) => ({ name, description: name, inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } })))}}));
|
|
259
|
+
server.setRequestHandler(CallToolRequestSchema,async r=>({content:[{type:'text',text:r.params.name==='read_room'?${JSON.stringify(marker)}:'private-tool-refused'}]}));
|
|
260
|
+
await server.connect(new StdioServerTransport());`, { mode: 0o600 });
|
|
261
|
+
server.on("request", async (request, response) => {
|
|
262
|
+
try {
|
|
263
|
+
if (request.method !== "POST" ||
|
|
264
|
+
request.url !== "/v1/responses" ||
|
|
265
|
+
++requests > 20) {
|
|
266
|
+
throw new Error("Unexpected probe request");
|
|
267
|
+
}
|
|
268
|
+
let raw = "";
|
|
269
|
+
for await (const chunk of request) {
|
|
270
|
+
raw += chunk.toString();
|
|
271
|
+
if (Buffer.byteLength(raw) > MAX_BYTES) {
|
|
272
|
+
throw new Error("Request limit");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const body = record(JSON.parse(raw));
|
|
276
|
+
for (const name of codexAdvertisedTools(body)) {
|
|
277
|
+
observations[turn]?.add(name);
|
|
278
|
+
}
|
|
279
|
+
const helpers = RESOURCE_HELPERS.filter((name) => observations[turn]?.has(`functions.${name}`));
|
|
280
|
+
const steps = turnSteps[turn] ?? [
|
|
281
|
+
"read_room",
|
|
282
|
+
...helpers,
|
|
283
|
+
...(helpers.includes("read_mcp_resource")
|
|
284
|
+
? ["read_mcp_resource", "read_mcp_resource"]
|
|
285
|
+
: []),
|
|
286
|
+
];
|
|
287
|
+
turnSteps[turn] = steps;
|
|
288
|
+
if (helpers.some((name) => !steps.includes(name))) {
|
|
289
|
+
throw new Error("Helper surface changed during proof");
|
|
290
|
+
}
|
|
291
|
+
const input = body.input;
|
|
292
|
+
const completed = calls[turn] ?? [];
|
|
293
|
+
if (completed.length) {
|
|
294
|
+
const output = input.find((item) => item.type === "function_call_output" &&
|
|
295
|
+
item.call_id === `${turn}-${completed.length - 1}`);
|
|
296
|
+
if (!output) {
|
|
297
|
+
throw new Error("Missing tool result");
|
|
298
|
+
}
|
|
299
|
+
const text = JSON.stringify(output.output);
|
|
300
|
+
const previous = completed.at(-1);
|
|
301
|
+
if (previous === "read_room") {
|
|
302
|
+
if (!text.includes(marker)) {
|
|
303
|
+
throw new Error("Room tool did not execute");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else if (previous === "list_mcp_resources" ||
|
|
307
|
+
previous === "list_mcp_resource_templates") {
|
|
308
|
+
const field = previous === "list_mcp_resources"
|
|
309
|
+
? "resources"
|
|
310
|
+
: "resourceTemplates";
|
|
311
|
+
if (output.output !== JSON.stringify({ [field]: [] })) {
|
|
312
|
+
throw new Error("Resource listing is not empty");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (typeof output.output !== "string" ||
|
|
316
|
+
!output.output.startsWith("resources/read failed:") ||
|
|
317
|
+
!RESOURCE_REFUSAL.test(output.output) ||
|
|
318
|
+
text.includes(nonce)) {
|
|
319
|
+
throw new Error("Resource helper did not refuse");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const name = steps[completed.length];
|
|
323
|
+
if (!name) {
|
|
324
|
+
finished[turn] = true;
|
|
325
|
+
probeResponse(response, {
|
|
326
|
+
type: "message",
|
|
327
|
+
id: "answer",
|
|
328
|
+
role: "assistant",
|
|
329
|
+
status: "completed",
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "output_text",
|
|
333
|
+
text: "Boundary verified.",
|
|
334
|
+
annotations: [],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
completed.push(name);
|
|
341
|
+
const reads = completed.filter((tool) => tool === "read_mcp_resource").length;
|
|
342
|
+
probeResponse(response, {
|
|
343
|
+
type: "function_call",
|
|
344
|
+
id: `${turn}-${completed.length - 1}`,
|
|
345
|
+
call_id: `${turn}-${completed.length - 1}`,
|
|
346
|
+
name,
|
|
347
|
+
namespace: name === "read_room" ? "mcp__talentos" : "functions",
|
|
348
|
+
arguments: JSON.stringify(name === "read_mcp_resource"
|
|
349
|
+
? {
|
|
350
|
+
server: reads === 3 ? "unknown_owner_server" : "talentos",
|
|
351
|
+
uri: reads > 1 ? canaryUrl : pathToFileURL(canary).href,
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
354
|
+
status: "completed",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
failure = error;
|
|
359
|
+
response.writeHead(400).end();
|
|
360
|
+
controller.abort();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
server.once("error", reject);
|
|
365
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
366
|
+
});
|
|
367
|
+
const address = server.address();
|
|
368
|
+
if (!address || typeof address === "string") {
|
|
369
|
+
throw new Error("Missing probe port");
|
|
370
|
+
}
|
|
371
|
+
canaryUrl = `http://127.0.0.1:${address.port}/private-canary`;
|
|
372
|
+
const env = {
|
|
373
|
+
HOME: directory,
|
|
374
|
+
CODEX_HOME: directory,
|
|
375
|
+
XDG_CONFIG_HOME: directory,
|
|
376
|
+
XDG_CACHE_HOME: directory,
|
|
377
|
+
PATH: providerPathEnv(binary, directory),
|
|
378
|
+
TALENTOS_PROBE_KEY: "loopback-only",
|
|
379
|
+
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
380
|
+
};
|
|
381
|
+
let session;
|
|
382
|
+
for (turn = 0; turn < 2; turn++) {
|
|
383
|
+
const args = codexRoomArgs({
|
|
384
|
+
catalogPath,
|
|
385
|
+
mcpCommand: process.execPath,
|
|
386
|
+
mcpArgs: [mcp],
|
|
387
|
+
prompt: "Verify the room boundary.",
|
|
388
|
+
sessionId: session,
|
|
389
|
+
});
|
|
390
|
+
args.unshift("-c", 'model_provider="talentos_probe"', "-c", 'model_providers.talentos_probe.name="Loopback"', "-c", `model_providers.talentos_probe.base_url="http://127.0.0.1:${address.port}/v1"`, "-c", 'model_providers.talentos_probe.wire_api="responses"', "-c", 'model_providers.talentos_probe.env_key="TALENTOS_PROBE_KEY"');
|
|
391
|
+
const output = await execute(binary, args, directory, env, signal);
|
|
392
|
+
if (failure ||
|
|
393
|
+
!finished[turn] ||
|
|
394
|
+
calls[turn]?.length !== turnSteps[turn]?.length ||
|
|
395
|
+
!observations[turn]?.has("mcp__talentos.read_room")) {
|
|
396
|
+
throw new Error("Incomplete boundary proof");
|
|
397
|
+
}
|
|
398
|
+
const events = output
|
|
399
|
+
.trim()
|
|
400
|
+
.split("\n")
|
|
401
|
+
.map((line) => record(JSON.parse(line)));
|
|
402
|
+
const thread = events.find((event) => event.type === "thread.started")?.thread_id;
|
|
403
|
+
if (typeof thread !== "string" ||
|
|
404
|
+
(session && session !== thread) ||
|
|
405
|
+
!events.some((event) => event.type === "turn.completed")) {
|
|
406
|
+
throw new Error("Missing resumed session");
|
|
407
|
+
}
|
|
408
|
+
session = thread;
|
|
409
|
+
}
|
|
410
|
+
if ((await identity(binary)) !== before) {
|
|
411
|
+
throw new Error("Codex changed during probe");
|
|
412
|
+
}
|
|
413
|
+
signal.throwIfAborted();
|
|
414
|
+
// Failure to persist an optimization does not invalidate a completed proof.
|
|
415
|
+
await writePrivateFile(cache, JSON.stringify({ key, safe: true })).catch(() => undefined);
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
controller.abort();
|
|
420
|
+
server.closeAllConnections();
|
|
421
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
422
|
+
await rm(directory, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
}
|
package/dist/agent/serve.js
CHANGED
|
@@ -154,6 +154,9 @@ export async function serveAgent(config, directory, version, stop, verify) {
|
|
|
154
154
|
console.error("Gateway update unavailable. Serving the current version; retrying in one minute.");
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
if (config.provider === "codex") {
|
|
158
|
+
await verify?.();
|
|
159
|
+
}
|
|
157
160
|
wakePending = false;
|
|
158
161
|
let job;
|
|
159
162
|
const claimStartedAt = Date.now();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { verifyClaudeCode, verifyCodex } from "../agent/adapters.js";
|
|
4
|
+
import { CodexBoundaryError } from "../agent/codex-probe.js";
|
|
4
5
|
import { AgentGateway, GatewayError } from "../agent/gateway.js";
|
|
5
6
|
import { serveAgent } from "../agent/serve.js";
|
|
6
7
|
import { agentWorkDir, localAgents, storeAgent, } from "../agent-config.js";
|
|
@@ -36,7 +37,7 @@ export async function runAgentServe(parsed, options = {}) {
|
|
|
36
37
|
try {
|
|
37
38
|
// Authenticate without publishing presence, including when the binary is gone.
|
|
38
39
|
await fetchAgentStatus(config);
|
|
39
|
-
await serveAgent(config, dirname(agentWorkDir(config.agentId, options.home)), version, stop.signal, () => verifyAgent(agent, options));
|
|
40
|
+
await serveAgent(config, dirname(agentWorkDir(config.agentId, options.home)), version, stop.signal, () => verifyAgent(agent, options, stop.signal));
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
42
43
|
if ((error instanceof GatewayError || error instanceof AgentHttpError) &&
|
|
@@ -52,7 +53,7 @@ export async function runAgentServe(parsed, options = {}) {
|
|
|
52
53
|
process.removeListener("SIGINT", abort);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
async function verifyAgent(config, options) {
|
|
56
|
+
async function verifyAgent(config, options, signal) {
|
|
56
57
|
try {
|
|
57
58
|
let binary;
|
|
58
59
|
try {
|
|
@@ -70,20 +71,22 @@ async function verifyAgent(config, options) {
|
|
|
70
71
|
await verifyClaudeCode(binary);
|
|
71
72
|
}
|
|
72
73
|
else {
|
|
73
|
-
await verifyCodex(binary);
|
|
74
|
+
await verifyCodex(binary, signal);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
78
|
if (error instanceof ProviderBinaryError) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
(reportError
|
|
84
|
-
|
|
79
|
+
if (!(error instanceof CodexBoundaryError)) {
|
|
80
|
+
try {
|
|
81
|
+
await new AgentGateway(config).request("/agent-gateway/provider-unavailable");
|
|
82
|
+
}
|
|
83
|
+
catch (reportError) {
|
|
84
|
+
if (reportError instanceof GatewayError &&
|
|
85
|
+
(reportError.status === 401 || reportError.status === 410)) {
|
|
86
|
+
throw reportError;
|
|
87
|
+
}
|
|
88
|
+
// Older servers may not support diagnostics; local shutdown still applies.
|
|
85
89
|
}
|
|
86
|
-
// Older servers may not support diagnostics; local shutdown still applies.
|
|
87
90
|
}
|
|
88
91
|
console.error(error.message);
|
|
89
92
|
// launchd cannot exclude exit 78; unload but retain the plist for repair.
|
package/package.json
CHANGED