agentschat-mcp 0.35.0 → 0.36.4

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/codex/owner.ts ADDED
@@ -0,0 +1,35 @@
1
+ /** Resolve only this authenticated bot's owner; callers must compare sender IDs. */
2
+ export async function getBotOwner(
3
+ base: string,
4
+ agentId: string,
5
+ token: string,
6
+ request: typeof fetch = fetch,
7
+ ): Promise<string | null> {
8
+ try {
9
+ const origin = base.replace(/\/+$/, "");
10
+ const options: RequestInit = {
11
+ method: "GET",
12
+ headers: { Authorization: `Bearer ${token}` },
13
+ cache: "no-store",
14
+ redirect: "error",
15
+ signal: AbortSignal.timeout(8_000),
16
+ };
17
+ const [statusResponse, entitlementResponse] = await Promise.all([
18
+ request(`${origin}/api/account/onboarding`, options),
19
+ request(`${origin}/api/me/entitlements`, options),
20
+ ]);
21
+ if (!statusResponse.ok || !entitlementResponse.ok) return null;
22
+ const [status, entitlement] = await Promise.all([
23
+ statusResponse.json(),
24
+ entitlementResponse.json(),
25
+ ]);
26
+ if (!status || Array.isArray(status) || status.agent_id !== agentId || status.claimed !== true) return null;
27
+ if (!entitlement || Array.isArray(entitlement)) return null;
28
+ const owner = entitlement.owner_account_id;
29
+ return typeof owner === "string" && owner.trim().length > 0 && owner !== agentId ? owner : null;
30
+ } catch {
31
+ // Missing endpoints, failed authentication and unavailable ownership all fail closed.
32
+ // Never retain a previous owner or expose response bodies / credentials in diagnostics.
33
+ return null;
34
+ }
35
+ }
package/codex/run.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import { getBotOwner } from "./owner.ts";
2
+ import { getOnboardingStatus } from "../src/onboarding-status.ts";
3
+ import { GuiChannel } from "./gui-channel.ts";
4
+ import { readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
1
7
  import { loadBots } from "./bots-config.ts";
2
8
  import type { BridgeConfig } from "./config.ts";
3
9
  import { parseArgs } from "node:util";
@@ -17,20 +23,30 @@ CWD/.agentschat/profile.json > CWD/.codex/config.toml MCP profile > AGENTSCHAT_P
17
23
  Only the exact CWD is searched. Named profiles live in ~/.agentschat (legacy ~/.agentchat).
18
24
  An optional project agent_id must match the selected profile; it cannot replace it.
19
25
  Credentials: private profile JSON {agent_id, token}, chmod 600; never put keys in argv.
20
- Project config fields: profile, agent_id, channels, senders, api_url, ws_url.
26
+ Project config fields: profile, agent_id, channels, senders, api_url, ws_url, permissions.
27
+ --onboarding-status checks authentication/ownership and prints safe claim/chat links; it does not send messages.
21
28
  --check validates identity and official app-server initialization without opening chat.
22
29
  Live DMs and exact mentions trigger replies; channels/senders restrict this further.
23
- Read-only Codex turns; inherited MCP servers disabled. No offline message replay.
30
+ Server-verified owner requests use full access by default; other senders stay read-only. Set permissions: "read-only" to disable writes and inherited MCP. No offline message replay.
24
31
  State: ~/.agentschat/codex-bridge/<project-server-identity hash>/ (private).
32
+ GUI outbox: --gui-thread THREAD_ID --gui-message-file PATH; --gui-status lists receipts.
33
+ Requires an authorized GUI host to dispatch; enqueue alone does not wake a task.
25
34
  See codex/README.md for setup, verification, limitations and recovery.
26
35
  `;
27
36
 
28
37
  let codex: AppServer | undefined, bridge: Bridge | undefined, transport: AgentsChatTransport | undefined;
29
38
  async function main() {
30
39
  const { values } = parseArgs({ options: { "codex-bridge": { type: "boolean" }, cwd: { type: "string" },
31
- "managed-worker": { type: "boolean" }, registry: { type: "string" }, bot: { type: "string" }, profile: { type: "string" }, "codex-bin": { type: "string" }, check: { type: "boolean" },
40
+ "gui-thread": { type: "string" }, "gui-message-file": { type: "string" }, "gui-status": { type: "boolean" }, "managed-worker": { type: "boolean" }, registry: { type: "string" }, bot: { type: "string" }, profile: { type: "string" }, "codex-bin": { type: "string" }, check: { type: "boolean" }, "onboarding-status": { type: "boolean" },
32
41
  help: { type: "boolean", short: "h" } }, strict: true });
33
42
  if (values.help) { console.log(HELP); return; }
43
+ if (values["gui-thread"] || values["gui-message-file"] || values["gui-status"]) {
44
+ const channel = new GuiChannel(join(homedir(), ".agentschat/codex-gui-outbox"), values["gui-thread"] ? [values["gui-thread"]] : []);
45
+ if (values["gui-status"]) { console.log(JSON.stringify(channel.list().map(({prompt, ...receipt}) => receipt))); return; }
46
+ if (!values["gui-thread"] || !values["gui-message-file"]) throw new Error("GUI submission requires --gui-thread and --gui-message-file");
47
+ const {prompt, ...receipt} = channel.enqueue(values["gui-thread"], readFileSync(values["gui-message-file"], "utf8"));
48
+ console.log(JSON.stringify(receipt)); return;
49
+ }
34
50
  const snapshot = values["managed-worker"] ? await new Promise<BridgeConfig>((resolve, reject) => {
35
51
  if (!process.connected) { reject(new Error("Managed worker needs parent IPC")); return; }
36
52
  const timer = setTimeout(() => reject(new Error("Parent configuration missing")), 10000);
@@ -38,11 +54,17 @@ async function main() {
38
54
  }) : undefined;
39
55
  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"] }));
40
56
  if (!c) throw new Error("Bot is absent or disabled");
57
+ if (values["onboarding-status"]) {
58
+ const status = await getOnboardingStatus(c.apiUrl, c.agentId, c.token);
59
+ console.log(JSON.stringify({...status, workdir: c.cwd, profile_file: c.profileFile, permissions: c.permissions,
60
+ startup_service: "check manager --status separately", reply_verified: false}));
61
+ return;
62
+ }
41
63
  console.log(JSON.stringify({ cwd: c.cwd, agent_id: c.agentId, profile: c.profileFile, source: c.source, stateDir: c.stateDir }));
42
- codex = new AppServer(c.codexBin);
64
+ codex = new AppServer(c.codexBin, undefined, undefined, c.permissions);
43
65
  if (values.check) { await codex.start(); console.log("Official app-server initialization: OK (no chat connection or generation)"); codex.close(); return; }
44
66
  transport = new AgentsChatTransport(c, m => { bridge!.accept(m); });
45
- bridge = new Bridge(c, codex, (chat, text) => transport!.send(chat, text), console.error, (chat, active) => transport!.setTyping(chat, active));
67
+ bridge = new Bridge(c, codex, (chat, text) => transport!.send(chat, text), console.error, (chat, active) => transport!.setTyping(chat, active), () => getBotOwner(c.apiUrl, c.agentId, c.token));
46
68
  let stopping = false;
47
69
  const stop = async () => { if (stopping) return; stopping = true; if (values["managed-worker"]) { const deadline = setTimeout(() => { try { process.kill(-process.pid, "SIGKILL"); } catch {} }, 20000); deadline.unref(); } bridge?.pause(); transport?.stop(); codex?.close(); await bridge?.stop(); if (process.connected) process.disconnect?.(); };
48
70
  codex.onFatal = () => { console.error("Codex backend stopped; pending inbox preserved. Restart the bridge after checking failed entries."); process.exitCode = 1; void stop(); };
@@ -67,7 +67,7 @@ For the bundled Hermes adaptation skill and profile-specific setup commands, rea
67
67
  [`skills/onboarding.md` §4](../skills/onboarding.md). Upgrades from older connectors
68
68
  must follow the [0.34.0 migration notes](../CHANGELOG.md).
69
69
 
70
- **0.35.0 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
70
+ **0.36.4 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
71
71
  Requires Node ≥22, Bun ≥1.0 for installing/building, and a configured Hermes
72
72
  v0.21.1 profile. In a reviewed `AgentsChatProtocol/mcp-plugin` checkout:
73
73
 
@@ -904,6 +904,13 @@ function parse(toml, { maxDepth = 1000, integersAsBigInt } = {}) {
904
904
  */
905
905
 
906
906
  // codex/config.ts
907
+ function permissionMode(value) {
908
+ if (value === undefined)
909
+ return "full-access";
910
+ if (value !== "full-access" && value !== "read-only")
911
+ throw new Error("permissions must be full-access or read-only");
912
+ return value;
913
+ }
907
914
  function readJson(file) {
908
915
  try {
909
916
  return JSON.parse(readFileSync(file, "utf8"));
@@ -924,7 +931,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
924
931
  const project = opts.settings ?? (existsSync(configFile) ? readJson(configFile) : {});
925
932
  if (!project || typeof project !== "object" || Array.isArray(project))
926
933
  throw new Error("Invalid project config");
927
- const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url"]);
934
+ const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
928
935
  if (Object.keys(project).some((k) => !allowed.has(k)))
929
936
  throw new Error("Unknown project config field (credentials belong in a private profile)");
930
937
  for (const k of ["profile", "agent_id", "api_url", "ws_url"])
@@ -993,6 +1000,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
993
1000
  cwd,
994
1001
  profileFile,
995
1002
  source,
1003
+ permissions: permissionMode(project.permissions),
996
1004
  agentId: profile.agent_id,
997
1005
  token: profile.token,
998
1006
  apiUrl: canonicalApi,
@@ -1032,7 +1040,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
1032
1040
  const names = new Set, identities = new Set;
1033
1041
  const bots = [];
1034
1042
  for (const bot of doc.bots) {
1035
- fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url"]);
1043
+ fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
1036
1044
  if (!text(bot.name) || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(bot.name) || names.has(bot.name))
1037
1045
  throw new Error("Bot names must be unique simple labels");
1038
1046
  names.add(bot.name);
@@ -1048,7 +1056,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
1048
1056
  mkdirSync(defaultDir, { recursive: true, mode: 448 });
1049
1057
  const cwd = realpathSync2(bot.workdir ? path(bot.workdir) : defaultDir);
1050
1058
  const settings = {};
1051
- for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url"])
1059
+ for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions"])
1052
1060
  if (bot[k] !== undefined)
1053
1061
  settings[k] = bot[k];
1054
1062
  const config = resolveConfig({ cwd, profile: bot.profile, settings, codexBin: doc.codex_bin }, {}, home);