@bridge_gpt/mcp-server 0.2.56 → 0.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -242,6 +242,20 @@ npx -y @bridge_gpt/mcp-server doctor
242
242
  `doctor` is strictly read-only. It reports what it found — configs, registrations,
243
243
  credential availability, prerequisites — and changes nothing.
244
244
 
245
+ **Codex approval profile.** When selecting Codex during `install`, opt in at the
246
+ prompt or pass `--tools=codex --codex-profile=on` in a scripted install. Bridge
247
+ creates `$CODEX_HOME/bridge.config.toml` (default `~/.codex/bridge.config.toml`)
248
+ with approval settings for the tools used by its review and implementation
249
+ workflows. Bridge-spawned Codex workers then select it automatically, including
250
+ the fresh implementation session after review. Pass `--codex-profile=off` to
251
+ decline; the installer leaves any existing profile content intact. If a profile
252
+ already contains user-authored or conflicting content, the installer leaves it
253
+ untouched and reports that workers cannot use it. Run `doctor --agent codex` to
254
+ inspect the current status. To use the profile in a Codex session you open
255
+ yourself, run `codex --profile bridge`. Bridge `--auto` controls recipe gates;
256
+ Codex MCP tool approvals are a separate setting. The profile does not change
257
+ global `sandbox_mode` or `approval_policy`.
258
+
245
259
  </details>
246
260
 
247
261
  <details>
@@ -351,7 +365,7 @@ later boot.
351
365
  "mcpServers": {
352
366
  "bridge": {
353
367
  "command": "npx",
354
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"],
368
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
355
369
  "env": {
356
370
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
357
371
  "BAPI_REPO_NAME": "your-repo",
@@ -373,7 +387,7 @@ later boot.
373
387
  "bridge": {
374
388
  "type": "stdio",
375
389
  "command": "npx",
376
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"],
390
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
377
391
  "env": {
378
392
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
379
393
  "BAPI_REPO_NAME": "your-repo",
@@ -395,7 +409,7 @@ later boot.
395
409
  "bridge": {
396
410
  "type": "stdio",
397
411
  "command": "npx",
398
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"],
412
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
399
413
  "env": {
400
414
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
401
415
  "BAPI_REPO_NAME": "your-repo",
@@ -418,7 +432,7 @@ Windsurf only supports global MCP configuration.
418
432
  "mcpServers": {
419
433
  "bridge": {
420
434
  "command": "npx",
421
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"],
435
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
422
436
  "env": {
423
437
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
424
438
  "BAPI_REPO_NAME": "your-repo",
@@ -437,7 +451,7 @@ Windsurf only supports global MCP configuration.
437
451
  ```toml
438
452
  [mcp_servers.bridge]
439
453
  command = "npx"
440
- args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"]
454
+ args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"]
441
455
 
442
456
  [mcp_servers.bridge.env]
443
457
  BAPI_BASE_URL = "https://bridgegpt-api.com"
@@ -0,0 +1,151 @@
1
+ /** Secret-free, opt-in approval settings for Bridge-spawned Codex workers. */
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { codexBridgeTableNames } from "./mcp-host-config.js";
5
+ import { MCP_SERVER_NAME } from "./mcp-identity.js";
6
+ export { codexHome } from "./codex-home.js";
7
+ export const BRIDGE_CODEX_PROFILE = "bridge";
8
+ const PROFILE_FILE = "bridge.config.toml";
9
+ const STATE_FILE = "bridge.profile-state.json";
10
+ const MARKER = "# Bridge-managed Codex approval profile (BAPI-1214)";
11
+ /** Tools called by review-and-start, review-and-implement, and implement-ticket. */
12
+ export const BRIDGE_WORKFLOW_TOOLS = [
13
+ "ping", "get_ticket", "get_pipeline_recipe", "request_ticket_review",
14
+ "wait_for_ticket_review", "fresh_base", "generate_decision_page",
15
+ "capture_review_decisions", "request_plan_generation", "get_plan",
16
+ "get_phase_context", "record_phase_result", "create_pull_request",
17
+ "update_jira_status", "config_field", "resolve_ci_checks", "poll_ci_checks",
18
+ "wait_for_ci_checks",
19
+ ];
20
+ export function codexProfilePaths(home) {
21
+ return {
22
+ profile: path.join(home, PROFILE_FILE),
23
+ state: path.join(home, STATE_FILE),
24
+ config: path.join(home, "config.toml"),
25
+ };
26
+ }
27
+ export function renderBridgeCodexProfile(key) {
28
+ return [
29
+ MARKER,
30
+ `# Applies only when selected with codex --profile ${BRIDGE_CODEX_PROFILE}.`,
31
+ ...BRIDGE_WORKFLOW_TOOLS.flatMap((tool) => [
32
+ `[mcp_servers.${key}.tools.${tool}]`,
33
+ 'approval_mode = "auto"',
34
+ "",
35
+ ]),
36
+ ].join("\n") + "\n";
37
+ }
38
+ function fingerprint(value) {
39
+ return createHash("sha256").update(value).digest("hex");
40
+ }
41
+ async function optionalRead(readFile, filePath) {
42
+ try {
43
+ return await readFile(filePath);
44
+ }
45
+ catch (error) {
46
+ return error.code === "ENOENT" ? null : undefined;
47
+ }
48
+ }
49
+ /** Only a matching managed file and explicit enabled state can activate workers. */
50
+ export async function inspectBridgeCodexProfile(home, readFile) {
51
+ const paths = codexProfilePaths(home);
52
+ const [config, profile, state] = await Promise.all([
53
+ optionalRead(readFile, paths.config),
54
+ optionalRead(readFile, paths.profile),
55
+ optionalRead(readFile, paths.state),
56
+ ]);
57
+ if (config === undefined || profile === undefined || state === undefined)
58
+ return { status: "unreadable" };
59
+ const names = codexBridgeTableNames(config ?? "");
60
+ if (names.length > 1)
61
+ return { status: "conflict" };
62
+ const key = names[0] ?? MCP_SERVER_NAME;
63
+ if (state === null)
64
+ return { status: profile === null ? "missing" : "conflict", key };
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(state);
68
+ }
69
+ catch {
70
+ return { status: "conflict", key };
71
+ }
72
+ if (!parsed || typeof parsed !== "object" || !("enabled" in parsed) || typeof parsed.enabled !== "boolean") {
73
+ return { status: "conflict", key };
74
+ }
75
+ if (!parsed.enabled)
76
+ return { status: "declined", key };
77
+ if (profile === null || !("hash" in parsed) || typeof parsed.hash !== "string")
78
+ return { status: "conflict", key };
79
+ if (profile !== renderBridgeCodexProfile(key) || fingerprint(profile) !== parsed.hash)
80
+ return { status: "conflict", key };
81
+ return { status: "active", key };
82
+ }
83
+ export async function configureBridgeCodexProfile(home, choice, deps) {
84
+ const paths = codexProfilePaths(home);
85
+ const current = await inspectBridgeCodexProfile(home, deps.readFile);
86
+ if (choice === "decline") {
87
+ const oldState = await optionalRead(deps.readFile, paths.state);
88
+ if (oldState === undefined)
89
+ return { status: "unreadable" };
90
+ let existingHash;
91
+ if (oldState !== null) {
92
+ try {
93
+ const parsed = JSON.parse(oldState);
94
+ if (!parsed || typeof parsed.enabled !== "boolean")
95
+ return { status: "conflict" };
96
+ if (typeof parsed.hash === "string")
97
+ existingHash = parsed.hash;
98
+ }
99
+ catch {
100
+ return { status: "conflict" };
101
+ }
102
+ }
103
+ await deps.mkdir(home, { recursive: true });
104
+ await deps.writeFile(paths.state, JSON.stringify({ enabled: false, ...(existingHash ? { hash: existingHash } : {}) }) + "\n");
105
+ return { status: "declined", key: current.key };
106
+ }
107
+ if (current.status === "unreadable")
108
+ return current;
109
+ const config = await optionalRead(deps.readFile, paths.config);
110
+ if (config === undefined)
111
+ return { status: "unreadable" };
112
+ const names = codexBridgeTableNames(config ?? "");
113
+ if (names.length > 1)
114
+ return { status: "conflict" };
115
+ const key = names[0] ?? MCP_SERVER_NAME;
116
+ const existing = await optionalRead(deps.readFile, paths.profile);
117
+ const oldState = await optionalRead(deps.readFile, paths.state);
118
+ const desired = renderBridgeCodexProfile(key);
119
+ if (existing === undefined || oldState === undefined)
120
+ return { status: "unreadable", key };
121
+ if (oldState !== null) {
122
+ try {
123
+ const parsed = JSON.parse(oldState);
124
+ if (!parsed || typeof parsed.enabled !== "boolean" ||
125
+ (parsed.enabled && (typeof parsed.hash !== "string" || existing === null || parsed.hash !== fingerprint(existing)))) {
126
+ return { status: "conflict", key };
127
+ }
128
+ }
129
+ catch {
130
+ return { status: "conflict", key };
131
+ }
132
+ }
133
+ // Existing user content is never modified or implicitly adopted.
134
+ if (existing !== null && existing !== desired) {
135
+ let managed = false;
136
+ if (oldState !== null && existing.startsWith(MARKER)) {
137
+ try {
138
+ const parsed = JSON.parse(oldState);
139
+ managed = typeof parsed?.enabled === "boolean" && parsed?.hash === fingerprint(existing);
140
+ }
141
+ catch { /* malformed state cannot authorize a rewrite */ }
142
+ }
143
+ if (!managed)
144
+ return { status: "conflict", key };
145
+ }
146
+ await deps.mkdir(home, { recursive: true });
147
+ if (existing !== desired)
148
+ await deps.writeFile(paths.profile, desired);
149
+ await deps.writeFile(paths.state, JSON.stringify({ enabled: true, hash: fingerprint(desired) }) + "\n");
150
+ return { status: "active", key };
151
+ }
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ /** Codex's user config root; only an absolute CODEX_HOME is accepted. */
4
+ export function codexHome(env, homedir = os.homedir()) {
5
+ const override = env.CODEX_HOME?.trim();
6
+ return override && path.isAbsolute(override) ? override : path.join(homedir, ".codex");
7
+ }
@@ -1,15 +1,12 @@
1
1
  /** Per-process Codex MCP scope, derived from the same manifest and shim as worktree provisioning. */
2
2
  import { readBridgeConfig } from "./bridge-config.js";
3
- import os from "node:os";
4
3
  import { codexBridgeTableNames } from "./mcp-host-config.js";
5
- import { MCP_HOST_TARGETS, resolveHostTargetPath } from "./mcp-host-targets.js";
4
+ import { codexHome } from "./codex-home.js";
5
+ import { codexProfilePaths, inspectBridgeCodexProfile, BRIDGE_CODEX_PROFILE } from "./codex-bridge-profile.js";
6
6
  import { buildMcpServerEntriesForManifest, normalizeWorktreePathForRegistration, } from "./mcp-provisioning.js";
7
7
  import { MCP_SERVER_NAME } from "./mcp-identity.js";
8
- function tomlEntry(entry) {
9
- return `{ command = ${JSON.stringify(entry.command)}, args = [${entry.args.map((arg) => JSON.stringify(arg)).join(", ")}] }`;
10
- }
11
8
  /** A missing Bridge target is a launch failure, never a fallback to user-scoped config. */
12
- export async function resolveCodexLaunchScope(root, deps, codexConfigPath = resolveHostTargetPath(MCP_HOST_TARGETS.codex, { cwd: root, homedir: os.homedir() })) {
9
+ export async function resolveCodexLaunchScope(root, deps, codexConfigPath = codexProfilePaths(codexHome(deps.env ?? process.env, deps.homedir?.())).config) {
13
10
  const normalized = normalizeWorktreePathForRegistration(root, deps);
14
11
  if (!normalized.ok)
15
12
  return { ok: false, error: "Codex MCP scope unavailable: invalid repository path." };
@@ -41,9 +38,17 @@ export async function resolveCodexLaunchScope(root, deps, codexConfigPath = reso
41
38
  return { ok: false, error: "Codex MCP scope unavailable: duplicate Bridge registrations in Codex host config." };
42
39
  }
43
40
  const bridgeKey = registeredNames[0] ?? MCP_SERVER_NAME;
41
+ const home = codexHome(deps.env ?? process.env, deps.homedir?.());
42
+ const profile = await inspectBridgeCodexProfile(home, deps.readFile);
44
43
  const argv = ["-C", normalized.path];
44
+ if (profile.status === "active" && profile.key === bridgeKey)
45
+ argv.push("--profile", BRIDGE_CODEX_PROFILE);
45
46
  for (const [name, entry] of Object.entries(built.entries)) {
46
- argv.push("-c", `mcp_servers.${name === MCP_SERVER_NAME ? bridgeKey : name}=${tomlEntry(entry)}`);
47
+ const key = name === MCP_SERVER_NAME ? bridgeKey : name;
48
+ // Field-level overrides retain the profile's mcp_servers.<key>.tools policy.
49
+ argv.push("-c", `mcp_servers.${key}.command=${JSON.stringify(entry.command)}`);
50
+ argv.push("-c", `mcp_servers.${key}.args=${JSON.stringify(entry.args)}`);
51
+ argv.push("-c", `mcp_servers.${key}.enabled=true`);
47
52
  }
48
53
  return { ok: true, argv };
49
54
  }
package/build/doctor.js CHANGED
@@ -21,6 +21,7 @@ import path from "path";
21
21
  import { createDefaultStartTicketsDeps } from "./start-tickets.js";
22
22
  import { VERSION, BUILD_COMMIT } from "./version.generated.js";
23
23
  import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
24
+ import { codexHome, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
24
25
  import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
25
26
  import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
26
27
  import { parseDefaultOnEnvFlag } from "./env-flags.js";
@@ -1743,7 +1744,11 @@ export async function runDoctorCli(argv, overrides = {}) {
1743
1744
  }
1744
1745
  }
1745
1746
  if (parsed.options.agentName === "codex") {
1746
- log("Codex is not yet supported for start-tickets sessions: its Bridge MCP registration cannot be scoped to each worktree. The prerequisite checks below do not imply launch support.");
1747
+ const readCodexFile = deps.readFile ?? ((p) => readFile(p, "utf-8"));
1748
+ const profile = await inspectBridgeCodexProfile(codexHome(deps.env), readCodexFile);
1749
+ log(profile.status === "active"
1750
+ ? "Codex Bridge approval profile installed; spawned workers use --profile bridge."
1751
+ : `Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`);
1747
1752
  }
1748
1753
  log(formatDoctorPrereqSection(deps.platform, collection));
1749
1754
  // Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE