agentschat-mcp 0.36.8 → 0.36.9
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/CHANGELOG.md +24 -1
- package/README.md +23 -5
- package/codex/README.md +98 -12
- package/codex/app-server.ts +37 -7
- package/codex/bots-config.ts +3 -2
- package/codex/config.ts +24 -9
- package/codex/manager.ts +1 -1
- package/codex/run.ts +16 -6
- package/codex/runtime-home.ts +65 -3
- package/dist/codex-bots.js +34 -8
- package/dist/codex-bridge.js +169 -27
- package/dist/server.js +111 -45
- package/package.json +2 -1
- package/skills/onboarding.md +96 -12
- package/src/identity.ts +7 -1
- package/src/profile-selection.ts +45 -0
- package/src/server.ts +32 -25
package/dist/codex-bots.js
CHANGED
|
@@ -911,6 +911,13 @@ function reasoningEffort(value) {
|
|
|
911
911
|
throw new Error("effort must be a nonempty model-supported reasoning effort name");
|
|
912
912
|
return value.trim();
|
|
913
913
|
}
|
|
914
|
+
function modelName(value) {
|
|
915
|
+
if (value === undefined)
|
|
916
|
+
return;
|
|
917
|
+
if (typeof value !== "string" || !value.trim())
|
|
918
|
+
throw new Error("model must be a nonempty model ID");
|
|
919
|
+
return value.trim();
|
|
920
|
+
}
|
|
914
921
|
function permissionMode(value) {
|
|
915
922
|
if (value === undefined)
|
|
916
923
|
return "full-access";
|
|
@@ -918,6 +925,13 @@ function permissionMode(value) {
|
|
|
918
925
|
throw new Error("permissions must be full-access or read-only");
|
|
919
926
|
return value;
|
|
920
927
|
}
|
|
928
|
+
function codexHomeMode(value) {
|
|
929
|
+
if (value === undefined)
|
|
930
|
+
return "linked";
|
|
931
|
+
if (value !== "linked" && value !== "isolated" && value !== "auth-only")
|
|
932
|
+
throw new Error("codex_home_mode must be linked, isolated or auth-only");
|
|
933
|
+
return value;
|
|
934
|
+
}
|
|
921
935
|
function readJson(file) {
|
|
922
936
|
try {
|
|
923
937
|
return JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -938,10 +952,10 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
938
952
|
const project = opts.settings ?? (existsSync(configFile) ? readJson(configFile) : {});
|
|
939
953
|
if (!project || typeof project !== "object" || Array.isArray(project))
|
|
940
954
|
throw new Error("Invalid project config");
|
|
941
|
-
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
955
|
+
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "state_root", "codex_home_mode"]);
|
|
942
956
|
if (Object.keys(project).some((k) => !allowed.has(k)))
|
|
943
957
|
throw new Error("Unknown project config field (credentials belong in a private profile)");
|
|
944
|
-
for (const k of ["profile", "agent_id", "api_url", "ws_url"])
|
|
958
|
+
for (const k of ["profile", "agent_id", "api_url", "ws_url", "state_root"])
|
|
945
959
|
if (project[k] !== undefined && (typeof project[k] !== "string" || !project[k].trim()))
|
|
946
960
|
throw new Error(`Invalid project ${k}`);
|
|
947
961
|
const localProfile = join(cwd, ".agentschat/profile.json");
|
|
@@ -957,8 +971,9 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
957
971
|
const mcp = doc.mcp_servers?.agentschat;
|
|
958
972
|
if (mcp && mcp.enabled !== false) {
|
|
959
973
|
const args = Array.isArray(mcp.args) ? mcp.args : [];
|
|
960
|
-
const index = args.
|
|
961
|
-
|
|
974
|
+
const index = args.findIndex((arg) => typeof arg === "string" && (arg === "--profile" || arg.startsWith("--profile=")));
|
|
975
|
+
const flagProfile = index < 0 ? undefined : args[index] === "--profile" ? args[index + 1] : args[index].slice("--profile=".length);
|
|
976
|
+
codexProfile = index >= 0 ? flagProfile : mcp.env?.AGENTSCHAT_PROFILE ?? mcp.env?.AGENTCHAT_PROFILE;
|
|
962
977
|
if ((index >= 0 || codexProfile !== undefined) && (typeof codexProfile !== "string" || !codexProfile.trim() || codexProfile.startsWith("--")))
|
|
963
978
|
throw new Error("Invalid project MCP profile selector");
|
|
964
979
|
}
|
|
@@ -1003,12 +1018,18 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
1003
1018
|
}
|
|
1004
1019
|
const canonicalApi = new URL(apiUrl).href.replace(/\/$/, "");
|
|
1005
1020
|
const key = createHash("sha256").update(JSON.stringify([cwd, canonicalApi, profile.agent_id])).digest("hex").slice(0, 24);
|
|
1021
|
+
const root = opts.stateRoot ?? project.state_root;
|
|
1022
|
+
if (root !== undefined && (typeof root !== "string" || !root.trim()))
|
|
1023
|
+
throw new Error("Invalid state_root");
|
|
1024
|
+
const stateRoot = root === undefined ? join(home, ".agentschat/codex-bridge") : root.startsWith("~/") ? join(home, root.slice(2)) : resolve(cwd, root);
|
|
1006
1025
|
return {
|
|
1007
1026
|
cwd,
|
|
1008
1027
|
profileFile,
|
|
1009
1028
|
source,
|
|
1029
|
+
codexHomeMode: codexHomeMode(opts.codexHomeMode ?? project.codex_home_mode),
|
|
1010
1030
|
permissions: permissionMode(project.permissions),
|
|
1011
1031
|
effort: reasoningEffort(project.effort),
|
|
1032
|
+
model: modelName(project.model),
|
|
1012
1033
|
agentId: profile.agent_id,
|
|
1013
1034
|
token: profile.token,
|
|
1014
1035
|
apiUrl: canonicalApi,
|
|
@@ -1016,7 +1037,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
1016
1037
|
channels: strings(project.channels, "channels"),
|
|
1017
1038
|
senders: strings(project.senders, "senders"),
|
|
1018
1039
|
codexBin: opts.codexBin ?? "codex",
|
|
1019
|
-
stateDir: join(
|
|
1040
|
+
stateDir: join(stateRoot, key)
|
|
1020
1041
|
};
|
|
1021
1042
|
}
|
|
1022
1043
|
|
|
@@ -1048,7 +1069,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
|
|
|
1048
1069
|
const names = new Set, identities = new Set;
|
|
1049
1070
|
const bots = [];
|
|
1050
1071
|
for (const bot of doc.bots) {
|
|
1051
|
-
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
1072
|
+
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "state_root", "codex_home_mode"]);
|
|
1052
1073
|
if (!text(bot.name) || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(bot.name) || names.has(bot.name))
|
|
1053
1074
|
throw new Error("Bot names must be unique simple labels");
|
|
1054
1075
|
names.add(bot.name);
|
|
@@ -1064,9 +1085,14 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
|
|
|
1064
1085
|
mkdirSync(defaultDir, { recursive: true, mode: 448 });
|
|
1065
1086
|
const cwd = realpathSync2(bot.workdir ? path(bot.workdir) : defaultDir);
|
|
1066
1087
|
const settings = {};
|
|
1067
|
-
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"])
|
|
1088
|
+
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "codex_home_mode"])
|
|
1068
1089
|
if (bot[k] !== undefined)
|
|
1069
1090
|
settings[k] = bot[k];
|
|
1091
|
+
if (bot.state_root !== undefined) {
|
|
1092
|
+
if (!text(bot.state_root))
|
|
1093
|
+
throw new Error("Invalid state_root");
|
|
1094
|
+
settings.state_root = path(bot.state_root);
|
|
1095
|
+
}
|
|
1070
1096
|
const config = resolveConfig({ cwd, profile: bot.profile, settings, codexBin: doc.codex_bin }, {}, home);
|
|
1071
1097
|
const identity = JSON.stringify([config.apiUrl, config.agentId]);
|
|
1072
1098
|
if (identities.has(identity))
|
|
@@ -1130,7 +1156,7 @@ function removeDeadLock(file) {
|
|
|
1130
1156
|
}
|
|
1131
1157
|
}
|
|
1132
1158
|
function save() {
|
|
1133
|
-
writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, effort: w.config.effort, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
|
|
1159
|
+
writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, effort: w.config.effort, model: w.config.model, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
|
|
1134
1160
|
renameSync(statusFile + ".tmp", statusFile);
|
|
1135
1161
|
}
|
|
1136
1162
|
async function stop(w) {
|
package/dist/codex-bridge.js
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// codex/runtime-home.ts
|
|
3
|
-
import { existsSync, mkdirSync, lstatSync, readlinkSync, realpathSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, accessSync, constants, lstatSync, readlinkSync, realpathSync, symlinkSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
function
|
|
5
|
+
import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
function executablePath(bin) {
|
|
7
|
+
const candidates = isAbsolute(bin) || bin.includes("/") ? [resolve(bin)] : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join(dir, bin));
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
try {
|
|
10
|
+
accessSync(candidate, constants.X_OK);
|
|
11
|
+
return realpathSync(candidate);
|
|
12
|
+
} catch {}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertIndependentTree(home, allowedAuthOrigin, codexExecutable) {
|
|
16
|
+
if (lstatSync(home).isSymbolicLink())
|
|
17
|
+
throw new Error("Independent Codex home must not be a symlink");
|
|
18
|
+
const inspect = (dir) => {
|
|
19
|
+
for (const name of readdirSync(dir)) {
|
|
20
|
+
const path = join(dir, name), info = lstatSync(path);
|
|
21
|
+
if (info.isSymbolicLink()) {
|
|
22
|
+
if (dir === home && name === "auth.json" && allowedAuthOrigin && resolve(home, readlinkSync(path)) === allowedAuthOrigin)
|
|
23
|
+
continue;
|
|
24
|
+
const local = relative(home, path).split(sep).join("/");
|
|
25
|
+
if (codexExecutable && /^tmp\/arg0\/codex-arg0[A-Za-z0-9_-]+\/(apply_patch|applypatch|codex-execve-wrapper|codex-linux-sandbox)$/.test(local)) {
|
|
26
|
+
try {
|
|
27
|
+
if (realpathSync(path) === codexExecutable)
|
|
28
|
+
continue;
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
throw new Error("Independent Codex home must not link configuration or runtime state");
|
|
32
|
+
}
|
|
33
|
+
if (info.isDirectory())
|
|
34
|
+
inspect(path);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
inspect(home);
|
|
38
|
+
}
|
|
39
|
+
function prepareRuntimeHome(stateDir, source = process.env.CODEX_HOME || join(homedir(), ".codex"), mode = "linked", codexBin = "codex") {
|
|
40
|
+
if (mode !== "linked" && mode !== "isolated" && mode !== "auth-only")
|
|
41
|
+
throw new Error("Invalid Codex home mode");
|
|
42
|
+
if (mode === "isolated") {
|
|
43
|
+
const home2 = join(stateDir, "codex-home-isolated");
|
|
44
|
+
mkdirSync(home2, { recursive: true, mode: 448 });
|
|
45
|
+
assertIndependentTree(home2, undefined, executablePath(codexBin));
|
|
46
|
+
return { home: realpathSync(home2) };
|
|
47
|
+
}
|
|
48
|
+
if (mode === "auth-only") {
|
|
49
|
+
const legacyHome2 = realpathSync(source);
|
|
50
|
+
const origin = join(legacyHome2, "auth.json");
|
|
51
|
+
if (!lstatSync(origin).isFile())
|
|
52
|
+
throw new Error("auth-only requires an existing regular auth.json; authorize its use explicitly");
|
|
53
|
+
const home2 = join(stateDir, "codex-home-auth-only");
|
|
54
|
+
mkdirSync(home2, { recursive: true, mode: 448 });
|
|
55
|
+
if (lstatSync(home2).isSymbolicLink() || realpathSync(home2) === legacyHome2)
|
|
56
|
+
throw new Error("auth-only home must be independent");
|
|
57
|
+
for (const name of ["config.toml", "AGENTS.md", "rules", "plugins", "hooks.json"]) {
|
|
58
|
+
try {
|
|
59
|
+
lstatSync(join(home2, name));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e.code === "ENOENT")
|
|
62
|
+
continue;
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("auth-only home contains unexpected configuration; use a fresh state root");
|
|
66
|
+
}
|
|
67
|
+
const skills = join(home2, "skills");
|
|
68
|
+
if (existsSync(skills) && (!lstatSync(skills).isDirectory() || readdirSync(skills).some((name) => name !== ".system")))
|
|
69
|
+
throw new Error("auth-only home contains unexpected custom skills");
|
|
70
|
+
assertIndependentTree(home2, origin, executablePath(codexBin));
|
|
71
|
+
const target = join(home2, "auth.json");
|
|
72
|
+
try {
|
|
73
|
+
if (!lstatSync(target).isSymbolicLink() || resolve(home2, readlinkSync(target)) !== origin)
|
|
74
|
+
throw new Error("Unexpected auth-only login binding");
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e.code !== "ENOENT")
|
|
77
|
+
throw e;
|
|
78
|
+
symlinkSync(origin, target);
|
|
79
|
+
}
|
|
80
|
+
assertIndependentTree(home2, origin, executablePath(codexBin));
|
|
81
|
+
return { home: realpathSync(home2) };
|
|
82
|
+
}
|
|
7
83
|
const legacyHome = existsSync(source) ? realpathSync(source) : resolve(source);
|
|
8
84
|
const home = join(stateDir, "codex-home");
|
|
9
85
|
mkdirSync(home, { recursive: true, mode: 448 });
|
|
@@ -102,7 +178,7 @@ async function getOnboardingStatus(base, agentId, token, request = fetch) {
|
|
|
102
178
|
|
|
103
179
|
// codex/gui-channel.ts
|
|
104
180
|
import { randomUUID } from "node:crypto";
|
|
105
|
-
import { mkdirSync as mkdirSync2, writeFileSync, readFileSync, renameSync, readdirSync, rmSync } from "node:fs";
|
|
181
|
+
import { mkdirSync as mkdirSync2, writeFileSync, readFileSync, renameSync, readdirSync as readdirSync2, rmSync } from "node:fs";
|
|
106
182
|
import { join as join2 } from "node:path";
|
|
107
183
|
function payload(result) {
|
|
108
184
|
if (result?.isError || result?.success === false || result?.error)
|
|
@@ -138,7 +214,7 @@ class GuiChannel {
|
|
|
138
214
|
return JSON.parse(readFileSync(this.path(id), "utf8"));
|
|
139
215
|
}
|
|
140
216
|
list() {
|
|
141
|
-
return
|
|
217
|
+
return readdirSync2(this.directory).filter((f) => /^[0-9a-f-]{36}\.json$/.test(f)).map((f) => this.get(f.slice(0, -5)));
|
|
142
218
|
}
|
|
143
219
|
enqueue(threadId, text, hostId) {
|
|
144
220
|
if (!this.allowedThreads.includes(threadId))
|
|
@@ -223,12 +299,12 @@ import { join as join9 } from "node:path";
|
|
|
223
299
|
// codex/bots-config.ts
|
|
224
300
|
import { readFileSync as readFileSync3, realpathSync as realpathSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
225
301
|
import { homedir as homedir3 } from "node:os";
|
|
226
|
-
import { dirname, isAbsolute as
|
|
302
|
+
import { dirname, isAbsolute as isAbsolute3, join as join4, resolve as resolve3 } from "node:path";
|
|
227
303
|
|
|
228
304
|
// codex/config.ts
|
|
229
305
|
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
230
306
|
import { homedir as homedir2 } from "node:os";
|
|
231
|
-
import { isAbsolute, join as join3, resolve as resolve2 } from "node:path";
|
|
307
|
+
import { isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
232
308
|
import { createHash } from "node:crypto";
|
|
233
309
|
|
|
234
310
|
// src/identity.ts
|
|
@@ -491,7 +567,7 @@ function skipVoid(ctx, banNewLines, banComments) {
|
|
|
491
567
|
skipComment(ctx);
|
|
492
568
|
}
|
|
493
569
|
}
|
|
494
|
-
function skipUntil(ctx,
|
|
570
|
+
function skipUntil(ctx, sep2, end) {
|
|
495
571
|
let ptr = ctx.p;
|
|
496
572
|
if (!end) {
|
|
497
573
|
ptr = indexOfNewline(ctx.s, ptr);
|
|
@@ -502,7 +578,7 @@ function skipUntil(ctx, sep, end) {
|
|
|
502
578
|
let c = ctx.s.charCodeAt(ctx.p);
|
|
503
579
|
if (c === 35) {
|
|
504
580
|
skipComment(ctx);
|
|
505
|
-
} else if (c === end || c ===
|
|
581
|
+
} else if (c === end || c === sep2) {
|
|
506
582
|
return;
|
|
507
583
|
}
|
|
508
584
|
}
|
|
@@ -1124,6 +1200,13 @@ function reasoningEffort(value) {
|
|
|
1124
1200
|
throw new Error("effort must be a nonempty model-supported reasoning effort name");
|
|
1125
1201
|
return value.trim();
|
|
1126
1202
|
}
|
|
1203
|
+
function modelName(value) {
|
|
1204
|
+
if (value === undefined)
|
|
1205
|
+
return;
|
|
1206
|
+
if (typeof value !== "string" || !value.trim())
|
|
1207
|
+
throw new Error("model must be a nonempty model ID");
|
|
1208
|
+
return value.trim();
|
|
1209
|
+
}
|
|
1127
1210
|
function permissionMode(value) {
|
|
1128
1211
|
if (value === undefined)
|
|
1129
1212
|
return "full-access";
|
|
@@ -1131,6 +1214,13 @@ function permissionMode(value) {
|
|
|
1131
1214
|
throw new Error("permissions must be full-access or read-only");
|
|
1132
1215
|
return value;
|
|
1133
1216
|
}
|
|
1217
|
+
function codexHomeMode(value) {
|
|
1218
|
+
if (value === undefined)
|
|
1219
|
+
return "linked";
|
|
1220
|
+
if (value !== "linked" && value !== "isolated" && value !== "auth-only")
|
|
1221
|
+
throw new Error("codex_home_mode must be linked, isolated or auth-only");
|
|
1222
|
+
return value;
|
|
1223
|
+
}
|
|
1134
1224
|
function readJson(file) {
|
|
1135
1225
|
try {
|
|
1136
1226
|
return JSON.parse(readFileSync2(file, "utf8"));
|
|
@@ -1151,10 +1241,10 @@ function resolveConfig(opts, env = process.env, home = homedir2()) {
|
|
|
1151
1241
|
const project = opts.settings ?? (existsSync2(configFile) ? readJson(configFile) : {});
|
|
1152
1242
|
if (!project || typeof project !== "object" || Array.isArray(project))
|
|
1153
1243
|
throw new Error("Invalid project config");
|
|
1154
|
-
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
1244
|
+
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "state_root", "codex_home_mode"]);
|
|
1155
1245
|
if (Object.keys(project).some((k) => !allowed.has(k)))
|
|
1156
1246
|
throw new Error("Unknown project config field (credentials belong in a private profile)");
|
|
1157
|
-
for (const k of ["profile", "agent_id", "api_url", "ws_url"])
|
|
1247
|
+
for (const k of ["profile", "agent_id", "api_url", "ws_url", "state_root"])
|
|
1158
1248
|
if (project[k] !== undefined && (typeof project[k] !== "string" || !project[k].trim()))
|
|
1159
1249
|
throw new Error(`Invalid project ${k}`);
|
|
1160
1250
|
const localProfile = join3(cwd, ".agentschat/profile.json");
|
|
@@ -1170,8 +1260,9 @@ function resolveConfig(opts, env = process.env, home = homedir2()) {
|
|
|
1170
1260
|
const mcp = doc.mcp_servers?.agentschat;
|
|
1171
1261
|
if (mcp && mcp.enabled !== false) {
|
|
1172
1262
|
const args = Array.isArray(mcp.args) ? mcp.args : [];
|
|
1173
|
-
const index = args.
|
|
1174
|
-
|
|
1263
|
+
const index = args.findIndex((arg) => typeof arg === "string" && (arg === "--profile" || arg.startsWith("--profile=")));
|
|
1264
|
+
const flagProfile = index < 0 ? undefined : args[index] === "--profile" ? args[index + 1] : args[index].slice("--profile=".length);
|
|
1265
|
+
codexProfile = index >= 0 ? flagProfile : mcp.env?.AGENTSCHAT_PROFILE ?? mcp.env?.AGENTCHAT_PROFILE;
|
|
1175
1266
|
if ((index >= 0 || codexProfile !== undefined) && (typeof codexProfile !== "string" || !codexProfile.trim() || codexProfile.startsWith("--")))
|
|
1176
1267
|
throw new Error("Invalid project MCP profile selector");
|
|
1177
1268
|
}
|
|
@@ -1189,7 +1280,7 @@ function resolveConfig(opts, env = process.env, home = homedir2()) {
|
|
|
1189
1280
|
let profileFile;
|
|
1190
1281
|
if (selector.startsWith("~/"))
|
|
1191
1282
|
profileFile = join3(home, selector.slice(2));
|
|
1192
|
-
else if (
|
|
1283
|
+
else if (isAbsolute2(selector) || selector.includes("/"))
|
|
1193
1284
|
profileFile = resolve2(cwd, selector);
|
|
1194
1285
|
else {
|
|
1195
1286
|
const name = selector.endsWith(".json") ? selector : `${selector}.json`;
|
|
@@ -1216,12 +1307,18 @@ function resolveConfig(opts, env = process.env, home = homedir2()) {
|
|
|
1216
1307
|
}
|
|
1217
1308
|
const canonicalApi = new URL(apiUrl).href.replace(/\/$/, "");
|
|
1218
1309
|
const key = createHash("sha256").update(JSON.stringify([cwd, canonicalApi, profile.agent_id])).digest("hex").slice(0, 24);
|
|
1310
|
+
const root = opts.stateRoot ?? project.state_root;
|
|
1311
|
+
if (root !== undefined && (typeof root !== "string" || !root.trim()))
|
|
1312
|
+
throw new Error("Invalid state_root");
|
|
1313
|
+
const stateRoot = root === undefined ? join3(home, ".agentschat/codex-bridge") : root.startsWith("~/") ? join3(home, root.slice(2)) : resolve2(cwd, root);
|
|
1219
1314
|
return {
|
|
1220
1315
|
cwd,
|
|
1221
1316
|
profileFile,
|
|
1222
1317
|
source,
|
|
1318
|
+
codexHomeMode: codexHomeMode(opts.codexHomeMode ?? project.codex_home_mode),
|
|
1223
1319
|
permissions: permissionMode(project.permissions),
|
|
1224
1320
|
effort: reasoningEffort(project.effort),
|
|
1321
|
+
model: modelName(project.model),
|
|
1225
1322
|
agentId: profile.agent_id,
|
|
1226
1323
|
token: profile.token,
|
|
1227
1324
|
apiUrl: canonicalApi,
|
|
@@ -1229,7 +1326,7 @@ function resolveConfig(opts, env = process.env, home = homedir2()) {
|
|
|
1229
1326
|
channels: strings(project.channels, "channels"),
|
|
1230
1327
|
senders: strings(project.senders, "senders"),
|
|
1231
1328
|
codexBin: opts.codexBin ?? "codex",
|
|
1232
|
-
stateDir: join3(
|
|
1329
|
+
stateDir: join3(stateRoot, key)
|
|
1233
1330
|
};
|
|
1234
1331
|
}
|
|
1235
1332
|
|
|
@@ -1256,12 +1353,12 @@ function loadBots(file = defaultRegistry(), home = homedir3()) {
|
|
|
1256
1353
|
for (const k of ["default_workdir", "codex_bin"])
|
|
1257
1354
|
if (doc[k] !== undefined && !text(doc[k]))
|
|
1258
1355
|
throw new Error(`Invalid ${k}`);
|
|
1259
|
-
const path = (value) => value.startsWith("~/") ? join4(home, value.slice(2)) :
|
|
1356
|
+
const path = (value) => value.startsWith("~/") ? join4(home, value.slice(2)) : isAbsolute3(value) ? value : resolve3(dirname(file), value);
|
|
1260
1357
|
const defaultDir = doc.default_workdir ? path(doc.default_workdir) : join4(home, ".agentschat/workspace");
|
|
1261
1358
|
const names = new Set, identities = new Set;
|
|
1262
1359
|
const bots = [];
|
|
1263
1360
|
for (const bot of doc.bots) {
|
|
1264
|
-
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
1361
|
+
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "state_root", "codex_home_mode"]);
|
|
1265
1362
|
if (!text(bot.name) || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(bot.name) || names.has(bot.name))
|
|
1266
1363
|
throw new Error("Bot names must be unique simple labels");
|
|
1267
1364
|
names.add(bot.name);
|
|
@@ -1277,9 +1374,14 @@ function loadBots(file = defaultRegistry(), home = homedir3()) {
|
|
|
1277
1374
|
mkdirSync3(defaultDir, { recursive: true, mode: 448 });
|
|
1278
1375
|
const cwd = realpathSync3(bot.workdir ? path(bot.workdir) : defaultDir);
|
|
1279
1376
|
const settings = {};
|
|
1280
|
-
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"])
|
|
1377
|
+
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort", "model", "codex_home_mode"])
|
|
1281
1378
|
if (bot[k] !== undefined)
|
|
1282
1379
|
settings[k] = bot[k];
|
|
1380
|
+
if (bot.state_root !== undefined) {
|
|
1381
|
+
if (!text(bot.state_root))
|
|
1382
|
+
throw new Error("Invalid state_root");
|
|
1383
|
+
settings.state_root = path(bot.state_root);
|
|
1384
|
+
}
|
|
1283
1385
|
const config = resolveConfig({ cwd, profile: bot.profile, settings, codexBin: doc.codex_bin }, {}, home);
|
|
1284
1386
|
const identity = JSON.stringify([config.apiUrl, config.agentId]);
|
|
1285
1387
|
if (identities.has(identity))
|
|
@@ -1310,6 +1412,7 @@ class AppServer {
|
|
|
1310
1412
|
permissions;
|
|
1311
1413
|
runtime;
|
|
1312
1414
|
effort;
|
|
1415
|
+
model;
|
|
1313
1416
|
onFatal;
|
|
1314
1417
|
closed = false;
|
|
1315
1418
|
child;
|
|
@@ -1317,13 +1420,17 @@ class AppServer {
|
|
|
1317
1420
|
pending = new Map;
|
|
1318
1421
|
active;
|
|
1319
1422
|
threadPermissions = new Map;
|
|
1423
|
+
initialized = false;
|
|
1424
|
+
readonlyFilesystemReported = false;
|
|
1425
|
+
diagnosticTail = "";
|
|
1320
1426
|
disabledMcp = {};
|
|
1321
|
-
constructor(bin = "codex", args = ["app-server", "--listen", "stdio://"], _legacyTurnTimeoutMs, permissions = "full-access", runtime, effort) {
|
|
1427
|
+
constructor(bin = "codex", args = ["app-server", "--listen", "stdio://"], _legacyTurnTimeoutMs, permissions = "full-access", runtime, effort, model) {
|
|
1322
1428
|
this.bin = bin;
|
|
1323
1429
|
this.args = args;
|
|
1324
1430
|
this.permissions = permissions;
|
|
1325
1431
|
this.runtime = runtime;
|
|
1326
1432
|
this.effort = effort;
|
|
1433
|
+
this.model = model;
|
|
1327
1434
|
}
|
|
1328
1435
|
get namespace() {
|
|
1329
1436
|
return this.runtime?.home;
|
|
@@ -1336,10 +1443,16 @@ class AppServer {
|
|
|
1336
1443
|
}
|
|
1337
1444
|
const args = this.runtime && this.args[0] === "app-server" ? [...this.args, "-c", `sqlite_home=${JSON.stringify(this.runtime.home)}`] : this.args;
|
|
1338
1445
|
this.child = spawn(this.bin, args, { env, stdio: "pipe" });
|
|
1339
|
-
this.child.stderr.
|
|
1446
|
+
this.child.stderr.on("data", (chunk) => {
|
|
1447
|
+
if (this.initialized)
|
|
1448
|
+
return;
|
|
1449
|
+
this.diagnosticTail = (this.diagnosticTail + chunk.toString()).slice(-4096);
|
|
1450
|
+
if (/read-only file system|\bEROFS\b|os error 30/i.test(this.diagnosticTail))
|
|
1451
|
+
this.readonlyFilesystemReported = true;
|
|
1452
|
+
});
|
|
1340
1453
|
this.child.stdin.on("error", () => this.fatal(new Error("Codex input pipe closed")));
|
|
1341
1454
|
this.child.on("error", () => this.fatal(new Error("Could not start Codex app-server")));
|
|
1342
|
-
this.child.on("exit", () => this.fatal(new Error("Codex app-server exited")));
|
|
1455
|
+
this.child.on("exit", () => this.fatal(new Error(!this.initialized && this.readonlyFilesystemReported ? "Codex app-server exited before initialization after reporting a read-only filesystem. CODEX_HOME must be writable (including installation_id); sqlite_home/log_dir overrides alone are insufficient. Use an authorized writable state_root and dedicated home; authentication is a separate step." : "Codex app-server exited")));
|
|
1343
1456
|
createInterface({ input: this.child.stdout }).on("line", (line) => {
|
|
1344
1457
|
try {
|
|
1345
1458
|
this.receive(JSON.parse(line));
|
|
@@ -1349,6 +1462,20 @@ class AppServer {
|
|
|
1349
1462
|
});
|
|
1350
1463
|
await this.request("initialize", { clientInfo: { name: "agentschat_bridge", version: "0.1.0" } });
|
|
1351
1464
|
this.write({ method: "initialized" });
|
|
1465
|
+
this.initialized = true;
|
|
1466
|
+
this.diagnosticTail = "";
|
|
1467
|
+
}
|
|
1468
|
+
async authenticationStatus() {
|
|
1469
|
+
try {
|
|
1470
|
+
const result = await this.request("account/read", { refreshToken: false });
|
|
1471
|
+
if (result?.account && typeof result.account === "object")
|
|
1472
|
+
return "authenticated";
|
|
1473
|
+
if (result?.requiresOpenaiAuth === false)
|
|
1474
|
+
return "not-required";
|
|
1475
|
+
if (result?.requiresOpenaiAuth === true && result?.account === null)
|
|
1476
|
+
return "required";
|
|
1477
|
+
} catch {}
|
|
1478
|
+
return "unknown";
|
|
1352
1479
|
}
|
|
1353
1480
|
write(value) {
|
|
1354
1481
|
if (this.closed || !this.child || this.child.exitCode !== null || this.child.stdin.destroyed)
|
|
@@ -1377,7 +1504,12 @@ class AppServer {
|
|
|
1377
1504
|
const waiter = this.pending.get(message.id);
|
|
1378
1505
|
if (waiter) {
|
|
1379
1506
|
this.pending.delete(message.id);
|
|
1380
|
-
|
|
1507
|
+
if (message.error) {
|
|
1508
|
+
const detail = typeof message.error.message === "string" ? message.error.message : "";
|
|
1509
|
+
const error = waiter.method === "thread/resume" && /already has an active writer/i.test(detail) ? new ThreadBusyError : /app-server socket directory must be a user-owned directory with mode 0700/i.test(detail) ? new Error("Host sandbox rejected the app-server socket directory ownership or mode 0700. This executor may protect the fixed daemon socket directory; use a supported executor or host configuration. Do not weaken the sandbox or change existing host directory permissions. Model execution is not verified.") : /fs sandbox helper failed/i.test(detail) ? new Error("Host filesystem sandbox helper failed. Initialization and authentication do not prove model execution; use a supported host without bypassing sandbox restrictions.") : new Error(`App-server request rejected (${message.error.code})`);
|
|
1510
|
+
waiter.reject(error);
|
|
1511
|
+
} else
|
|
1512
|
+
waiter.resolve(message.result);
|
|
1381
1513
|
}
|
|
1382
1514
|
return;
|
|
1383
1515
|
}
|
|
@@ -1418,6 +1550,7 @@ class AppServer {
|
|
|
1418
1550
|
const r = await this.request(existing ? "thread/resume" : "thread/start", {
|
|
1419
1551
|
...existing ? { threadId: existing } : { ephemeral },
|
|
1420
1552
|
cwd,
|
|
1553
|
+
...this.model ? { model: this.model } : {},
|
|
1421
1554
|
approvalPolicy: "never",
|
|
1422
1555
|
sandbox: permissions === "full-access" ? "danger-full-access" : "read-only",
|
|
1423
1556
|
...permissions === "read-only" ? { config: { mcp_servers: this.disabledMcp } } : {},
|
|
@@ -1461,7 +1594,7 @@ class AppServer {
|
|
|
1461
1594
|
});
|
|
1462
1595
|
completed.catch(() => {});
|
|
1463
1596
|
try {
|
|
1464
|
-
const r = await this.request("turn/start", { threadId: thread, approvalPolicy: "never", sandboxPolicy: { type: permissions === "full-access" ? "dangerFullAccess" : "readOnly" }, input: [{ type: "text", text }], ...effort ? { effort } : {} });
|
|
1597
|
+
const r = await this.request("turn/start", { threadId: thread, approvalPolicy: "never", sandboxPolicy: { type: permissions === "full-access" ? "dangerFullAccess" : "readOnly" }, input: [{ type: "text", text }], ...effort ? { effort } : {}, ...this.model ? { model: this.model } : {} });
|
|
1465
1598
|
const active = this.active;
|
|
1466
1599
|
if (!active)
|
|
1467
1600
|
return await completed;
|
|
@@ -2319,7 +2452,11 @@ CWD/.agentschat/profile.json > CWD/.codex/config.toml MCP profile > AGENTSCHAT_P
|
|
|
2319
2452
|
Only the exact CWD is searched. Named profiles live in ~/.agentschat (legacy ~/.agentchat).
|
|
2320
2453
|
An optional project agent_id must match the selected profile; it cannot replace it.
|
|
2321
2454
|
Credentials: private profile JSON {agent_id, token}, chmod 600; never put keys in argv.
|
|
2322
|
-
Project config fields: profile, agent_id, channels, senders, api_url, ws_url, permissions, effort.
|
|
2455
|
+
Project config fields: profile, agent_id, channels, senders, api_url, ws_url, permissions, effort, state_root, codex_home_mode.
|
|
2456
|
+
--state-root DIRECTORY relocates private state while retaining project/server/identity isolation.
|
|
2457
|
+
--codex-home-mode linked|isolated|auth-only: linked (default) reuses operator configuration/login; isolated imports nothing; auth-only explicitly links only auth.json.
|
|
2458
|
+
CODEX_HOME itself must be writable (including installation_id); sqlite_home/log_dir overrides alone do not suffice.
|
|
2459
|
+
Any credential reuse or independent login needs operator authorization; isolated initialization is not authenticated readiness.
|
|
2323
2460
|
--onboarding-status checks authentication/ownership and prints safe claim/chat links; it does not send messages.
|
|
2324
2461
|
--check validates identity and official app-server initialization without opening chat.
|
|
2325
2462
|
Live DMs and exact mentions trigger replies; channels/senders restrict this further.
|
|
@@ -2337,6 +2474,8 @@ async function main() {
|
|
|
2337
2474
|
const { values } = parseArgs({ options: {
|
|
2338
2475
|
"codex-bridge": { type: "boolean" },
|
|
2339
2476
|
cwd: { type: "string" },
|
|
2477
|
+
"state-root": { type: "string" },
|
|
2478
|
+
"codex-home-mode": { type: "string" },
|
|
2340
2479
|
conversations: { type: "boolean" },
|
|
2341
2480
|
"read-conversation": { type: "string" },
|
|
2342
2481
|
"gui-thread": { type: "string" },
|
|
@@ -2367,6 +2506,8 @@ async function main() {
|
|
|
2367
2506
|
console.log(JSON.stringify(receipt));
|
|
2368
2507
|
return;
|
|
2369
2508
|
}
|
|
2509
|
+
if ((values.bot || values["managed-worker"]) && (values["state-root"] || values["codex-home-mode"]))
|
|
2510
|
+
throw new Error("For managed bots, configure state_root/codex_home_mode in the bot registry");
|
|
2370
2511
|
const snapshot = values["managed-worker"] ? await new Promise((resolve4, reject) => {
|
|
2371
2512
|
if (!process.connected) {
|
|
2372
2513
|
reject(new Error("Managed worker needs parent IPC"));
|
|
@@ -2378,7 +2519,7 @@ async function main() {
|
|
|
2378
2519
|
resolve4(config);
|
|
2379
2520
|
});
|
|
2380
2521
|
}) : undefined;
|
|
2381
|
-
const c = snapshot ?? (values.bot ? loadBots(values.registry).find((b) => b.name === values.bot) : resolveConfig({ cwd: values.cwd, profile: values.profile, codexBin: values["codex-bin"] }));
|
|
2522
|
+
const c = snapshot ?? (values.bot ? loadBots(values.registry).find((b) => b.name === values.bot) : resolveConfig({ cwd: values.cwd, profile: values.profile, codexBin: values["codex-bin"], stateRoot: values["state-root"], codexHomeMode: values["codex-home-mode"] }));
|
|
2382
2523
|
if (!c)
|
|
2383
2524
|
throw new Error("Bot is absent or disabled");
|
|
2384
2525
|
if (values["onboarding-status"]) {
|
|
@@ -2394,8 +2535,8 @@ async function main() {
|
|
|
2394
2535
|
return;
|
|
2395
2536
|
}
|
|
2396
2537
|
console.log(JSON.stringify({ cwd: c.cwd, agent_id: c.agentId, profile: c.profileFile, source: c.source, stateDir: c.stateDir }));
|
|
2397
|
-
const runtime = prepareRuntimeHome(c.stateDir);
|
|
2398
|
-
codex = new AppServer(c.codexBin, undefined, undefined, c.permissions, runtime, c.effort);
|
|
2538
|
+
const runtime = prepareRuntimeHome(c.stateDir, undefined, c.codexHomeMode, c.codexBin);
|
|
2539
|
+
codex = new AppServer(c.codexBin, undefined, undefined, c.permissions, runtime, c.effort, c.model);
|
|
2399
2540
|
if (values.conversations || values["read-conversation"]) {
|
|
2400
2541
|
const state = JSON.parse(readFileSync8(join9(c.stateDir, "state.json"), "utf8"));
|
|
2401
2542
|
const channels = state.channels ?? {};
|
|
@@ -2417,6 +2558,7 @@ async function main() {
|
|
|
2417
2558
|
if (values.check) {
|
|
2418
2559
|
await codex.start();
|
|
2419
2560
|
console.log("Official app-server initialization: OK (no chat connection or generation)");
|
|
2561
|
+
console.log(JSON.stringify({ authentication: await codex.authenticationStatus(), generation_verified: false, reply_verified: false, codex_home_mode: c.codexHomeMode ?? "linked" }));
|
|
2420
2562
|
codex.close();
|
|
2421
2563
|
return;
|
|
2422
2564
|
}
|