@ccpocket/bridge 1.71.0 → 1.72.1

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.
@@ -284,10 +284,10 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
284
284
  path: string;
285
285
  }>;
286
286
  }): Promise<void>;
287
- approve(toolUseId?: string): void;
288
- approveAlways(toolUseId?: string): void;
289
- reject(toolUseId?: string, _message?: string): void;
290
- answer(toolUseId: string, result: string): void;
287
+ approve(toolUseId?: string): boolean;
288
+ approveAlways(toolUseId?: string): boolean;
289
+ reject(toolUseId?: string, _message?: string): boolean;
290
+ answer(toolUseId: string, result: string): boolean;
291
291
  /**
292
292
  * Install a plugin or begin connector authentication proposed by Codex.
293
293
  * The elicitation remains pending while external app authentication is
@@ -326,6 +326,7 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
326
326
  private handlePlanRejected;
327
327
  private bootstrap;
328
328
  private resolveWritableRootsConfig;
329
+ private readConfigPermissions;
329
330
  private initializeRpcConnection;
330
331
  readProfileConfig(cwd?: string): Promise<CodexProfileConfig>;
331
332
  readConfigRequirements(): Promise<CodexConfigRequirements>;
@@ -1,7 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { realpathSync, statSync } from "node:fs";
3
4
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
5
6
  import { rm, writeFile } from "node:fs/promises";
6
7
  import { createCodexTransport, buildCodexSpawnSpec, } from "./codex-transport.js";
7
8
  import { codexCliJoinTarget } from "./codex-app-server-config.js";
@@ -144,7 +145,7 @@ export class CodexProcess extends EventEmitter {
144
145
  return this.transport?.isRunning ?? false;
145
146
  }
146
147
  get approvalPolicy() {
147
- return this._approvalPolicy ?? "on-request";
148
+ return approvalPolicyForClient(this._approvalPolicy);
148
149
  }
149
150
  get approvalsReviewer() {
150
151
  return normalizeApprovalsReviewerForClient(this._approvalsReviewer);
@@ -595,73 +596,78 @@ export class CodexProcess extends EventEmitter {
595
596
  if (this.pendingPlanCompletion &&
596
597
  toolUseId === this.pendingPlanCompletion.toolUseId) {
597
598
  this.handlePlanApproved();
598
- return;
599
+ return true;
599
600
  }
600
601
  const pending = this.resolvePendingApproval(toolUseId);
601
602
  if (!pending) {
602
603
  // Fallback: McpElicitation lives in pendingUserInputs
603
604
  if (this.approveUserInput(toolUseId, "Accept"))
604
- return;
605
+ return true;
605
606
  console.log("[codex-process] approve() called but no pending permission requests");
606
- return;
607
+ return false;
607
608
  }
608
609
  this.pendingApprovals.delete(pending.toolUseId);
609
610
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "accept"));
610
- this.emitToolResult(pending.toolUseId, "Approved");
611
+ this.emitToolResult(pending.toolUseId, "Approved", "approved");
611
612
  if (this.pendingApprovals.size === 0) {
612
613
  this.setStatus("running");
613
614
  }
615
+ return true;
614
616
  }
615
617
  approveAlways(toolUseId) {
616
618
  const pending = this.resolvePendingApproval(toolUseId);
617
619
  if (!pending) {
618
620
  // Fallback: McpElicitation lives in pendingUserInputs
619
- if (this.approveUserInput(toolUseId, "Allow for this session"))
620
- return;
621
+ if (this.approveUserInput(toolUseId, "Allow for this session")) {
622
+ return true;
623
+ }
621
624
  console.log("[codex-process] approveAlways() called but no pending permission requests");
622
- return;
625
+ return false;
623
626
  }
624
627
  this.pendingApprovals.delete(pending.toolUseId);
625
628
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "acceptForSession"));
626
- this.emitToolResult(pending.toolUseId, "Approved (always)");
629
+ this.emitToolResult(pending.toolUseId, "Approved (always)", "approved_for_session");
627
630
  if (this.pendingApprovals.size === 0) {
628
631
  this.setStatus("running");
629
632
  }
633
+ return true;
630
634
  }
631
635
  reject(toolUseId, _message) {
632
636
  // Check if this is a plan completion rejection
633
637
  if (this.pendingPlanCompletion &&
634
638
  toolUseId === this.pendingPlanCompletion.toolUseId) {
635
639
  this.handlePlanRejected(_message);
636
- return;
640
+ return true;
637
641
  }
638
642
  const pending = this.resolvePendingApproval(toolUseId);
639
643
  if (!pending) {
640
644
  // Fallback: McpElicitation lives in pendingUserInputs
641
645
  if (this.rejectUserInput(toolUseId, "Decline"))
642
- return;
646
+ return true;
643
647
  console.log("[codex-process] reject() called but no pending permission requests");
644
- return;
648
+ return false;
645
649
  }
646
650
  this.pendingApprovals.delete(pending.toolUseId);
647
651
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, resolveApprovalRejectDecision(pending)));
648
- this.emitToolResult(pending.toolUseId, "Rejected");
652
+ this.emitToolResult(pending.toolUseId, "Rejected", "rejected");
649
653
  if (this.pendingApprovals.size === 0) {
650
654
  this.setStatus("running");
651
655
  }
656
+ return true;
652
657
  }
653
658
  answer(toolUseId, result) {
654
659
  const pending = this.resolvePendingUserInput(toolUseId);
655
660
  if (!pending) {
656
661
  console.log("[codex-process] answer() called but no pending AskUserQuestion");
657
- return;
662
+ return false;
658
663
  }
659
664
  this.pendingUserInputs.delete(pending.toolUseId);
660
665
  this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
661
- this.emitToolResult(pending.toolUseId, "Answered");
666
+ this.emitToolResult(pending.toolUseId, "Answered", "answered");
662
667
  if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
663
668
  this.setStatus("running");
664
669
  }
670
+ return true;
665
671
  }
666
672
  /**
667
673
  * Install a plugin or begin connector authentication proposed by Codex.
@@ -780,11 +786,12 @@ export class CodexProcess extends EventEmitter {
780
786
  };
781
787
  }
782
788
  /** Emit a synthetic tool_result so history replay can match it to a permission_request. */
783
- emitToolResult(toolUseId, content) {
789
+ emitToolResult(toolUseId, content, permissionOutcome) {
784
790
  this.emitMessage({
785
791
  type: "tool_result",
786
792
  toolUseId,
787
793
  content,
794
+ ...(permissionOutcome ? { permissionOutcome } : {}),
788
795
  });
789
796
  }
790
797
  resolvePendingApproval(toolUseId) {
@@ -820,7 +827,9 @@ export class CodexProcess extends EventEmitter {
820
827
  }
821
828
  this.pendingUserInputs.delete(pending.toolUseId);
822
829
  this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
823
- this.emitToolResult(pending.toolUseId, "Approved");
830
+ this.emitToolResult(pending.toolUseId, "Approved", result === "Allow for this session"
831
+ ? "approved_for_session"
832
+ : "approved");
824
833
  if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
825
834
  this.setStatus("running");
826
835
  }
@@ -860,7 +869,7 @@ export class CodexProcess extends EventEmitter {
860
869
  return false;
861
870
  this.pendingUserInputs.delete(pending.toolUseId);
862
871
  this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, resolveUserInputRejectResult(pending, result)));
863
- this.emitToolResult(pending.toolUseId, "Rejected");
872
+ this.emitToolResult(pending.toolUseId, "Rejected", "rejected");
864
873
  if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
865
874
  this.setStatus("running");
866
875
  }
@@ -929,12 +938,21 @@ export class CodexProcess extends EventEmitter {
929
938
  ? (await this.readConfigRequirements()).autoReviewDisabled
930
939
  : options?.autoReviewDisabledByPolicy === true;
931
940
  this._autoReviewDisabledByPolicy = autoReviewDisabled;
932
- const effectiveApprovalsReviewer = autoReviewDisabled
933
- ? "user"
934
- : options?.approvalsReviewer;
935
941
  const effectiveCodexPermissionsMode = autoReviewDisabled && options?.codexPermissionsMode === "autoReview"
936
942
  ? "default"
937
943
  : options?.codexPermissionsMode;
944
+ // thread/resume preserves the thread's prior permission overrides when
945
+ // these fields are omitted. Custom mode and named profiles mean the
946
+ // current config.toml should win, so resolve it and pass the effective
947
+ // values explicitly on resume.
948
+ const configResumePermissions = options?.threadId &&
949
+ (effectiveCodexPermissionsMode === "custom" || options.profile)
950
+ ? await this.readConfigPermissions(projectPath, options.profile)
951
+ : undefined;
952
+ const effectiveApprovalsReviewer = autoReviewDisabled
953
+ ? "user"
954
+ : (configResumePermissions?.approvalsReviewer ??
955
+ options?.approvalsReviewer);
938
956
  if (autoReviewDisabled) {
939
957
  console.warn("[codex-process] Auto-review disabled by managed Browser Use policy");
940
958
  }
@@ -943,15 +961,17 @@ export class CodexProcess extends EventEmitter {
943
961
  ? undefined
944
962
  : normalizeApprovalsReviewerForAppServer(effectiveApprovalsReviewer);
945
963
  this._codexPermissionsMode = effectiveCodexPermissionsMode;
946
- const requestedApprovalPolicy = options?.approvalPolicy
947
- ? normalizeApprovalPolicy(options.approvalPolicy)
964
+ const configuredApprovalPolicy = configResumePermissions?.approvalPolicy ?? options?.approvalPolicy;
965
+ const requestedApprovalPolicy = configuredApprovalPolicy
966
+ ? normalizeApprovalPolicyForRpc(configuredApprovalPolicy)
948
967
  : undefined;
949
968
  const requestedApprovalsReviewer = effectiveApprovalsReviewer === undefined
950
969
  ? undefined
951
970
  : normalizeApprovalsReviewerForAppServer(effectiveApprovalsReviewer);
952
971
  const requestedClientApprovalsReviewer = normalizeApprovalsReviewerForClient(effectiveApprovalsReviewer);
953
- const requestedSandboxMode = options?.sandboxMode
954
- ? normalizeSandboxMode(options.sandboxMode)
972
+ const configuredSandboxMode = configResumePermissions?.sandboxMode ?? options?.sandboxMode;
973
+ const requestedSandboxMode = configuredSandboxMode
974
+ ? normalizeSandboxMode(configuredSandboxMode)
955
975
  : undefined;
956
976
  const threadParams = {
957
977
  cwd: projectPath,
@@ -1025,14 +1045,15 @@ export class CodexProcess extends EventEmitter {
1025
1045
  this.startModel = thread.model;
1026
1046
  }
1027
1047
  const resolvedSettings = extractResolvedSettingsFromThreadResponse(response);
1048
+ const activeApprovalPolicy = resolvedSettings.approvalPolicy ?? requestedApprovalPolicy;
1028
1049
  const resolvedApprovalsReviewer = autoReviewDisabled
1029
1050
  ? "user"
1030
1051
  : resolvedSettings.approvalsReviewer;
1031
1052
  if (resolvedSettings.model) {
1032
1053
  this.startModel = resolvedSettings.model;
1033
1054
  }
1034
- if (resolvedSettings.approvalPolicy) {
1035
- this._approvalPolicy = resolvedSettings.approvalPolicy;
1055
+ if (activeApprovalPolicy) {
1056
+ this._approvalPolicy = activeApprovalPolicy;
1036
1057
  }
1037
1058
  if (resolvedApprovalsReviewer) {
1038
1059
  this._approvalsReviewer = normalizeApprovalsReviewerForAppServer(resolvedApprovalsReviewer);
@@ -1049,9 +1070,9 @@ export class CodexProcess extends EventEmitter {
1049
1070
  ...(sanitizeCodexModel(this.startModel)
1050
1071
  ? { model: sanitizeCodexModel(this.startModel) }
1051
1072
  : {}),
1052
- ...(resolvedSettings.approvalPolicy ?? options?.approvalPolicy
1073
+ ...(activeApprovalPolicy
1053
1074
  ? {
1054
- approvalPolicy: resolvedSettings.approvalPolicy ?? requestedApprovalPolicy,
1075
+ approvalPolicy: approvalPolicyForClient(activeApprovalPolicy),
1055
1076
  }
1056
1077
  : {}),
1057
1078
  ...(resolvedApprovalsReviewer ?? effectiveApprovalsReviewer
@@ -1121,6 +1142,14 @@ export class CodexProcess extends EventEmitter {
1121
1142
  const configuredRoots = extractWritableRootsFromConfigRead(response);
1122
1143
  return normalizeWritableRoots([...configuredRoots, ...normalizedAdditional], this.platform);
1123
1144
  }
1145
+ async readConfigPermissions(projectPath, profile) {
1146
+ const projectLookupPaths = resolveConfigProjectLookupPaths(projectPath);
1147
+ const response = await this.request("config/read", {
1148
+ includeLayers: false,
1149
+ cwd: projectPath,
1150
+ });
1151
+ return extractPermissionsFromConfigRead(response, projectLookupPaths, this.platform, profile);
1152
+ }
1124
1153
  async initializeRpcConnection() {
1125
1154
  await this.request("initialize", {
1126
1155
  clientInfo: {
@@ -1365,7 +1394,7 @@ export class CodexProcess extends EventEmitter {
1365
1394
  input,
1366
1395
  };
1367
1396
  if (this._approvalPolicy) {
1368
- params.approvalPolicy = normalizeApprovalPolicy(this._approvalPolicy);
1397
+ params.approvalPolicy = normalizeApprovalPolicyForRpc(this._approvalPolicy);
1369
1398
  }
1370
1399
  if (this._approvalsReviewer) {
1371
1400
  params.approvalsReviewer = normalizeApprovalsReviewerForAppServer(this._approvalsReviewer);
@@ -2591,6 +2620,122 @@ function extractWritableRootsFromConfigRead(response) {
2591
2620
  return [];
2592
2621
  return writableRoots.filter((root) => typeof root === "string");
2593
2622
  }
2623
+ function extractPermissionsFromConfigRead(response, projectLookupPaths, platform, profile) {
2624
+ const config = asRecord(asRecord(response)?.config);
2625
+ const profiles = asRecord(config?.profiles);
2626
+ const configuredDefaultProfile = typeof config?.profile === "string" && config.profile.trim()
2627
+ ? config.profile.trim()
2628
+ : undefined;
2629
+ const requestedProfile = profile?.trim() || undefined;
2630
+ const selectedProfile = requestedProfile ?? configuredDefaultProfile;
2631
+ const profileConfig = selectedProfile
2632
+ ? asRecord(profiles?.[selectedProfile])
2633
+ : undefined;
2634
+ const configuredValue = (key) => profileConfig?.[key] ?? config?.[key];
2635
+ const projectTrust = findProjectTrust(config, projectLookupPaths, platform);
2636
+ return {
2637
+ approvalPolicy: parseConfigApprovalPolicy(configuredValue("approval_policy")) ??
2638
+ (projectTrust === "untrusted" ? "untrusted" : "on-request"),
2639
+ approvalsReviewer: parseConfigApprovalsReviewer(configuredValue("approvals_reviewer")) ??
2640
+ "user",
2641
+ sandboxMode: parseConfigSandboxMode(configuredValue("sandbox_mode")) ??
2642
+ (projectTrust !== undefined && platform !== "win32"
2643
+ ? "workspace-write"
2644
+ : "read-only"),
2645
+ };
2646
+ }
2647
+ function findProjectTrust(config, projectLookupPaths, platform) {
2648
+ const projects = asRecord(config?.projects);
2649
+ if (!projects)
2650
+ return undefined;
2651
+ const projectEntries = Object.entries(projects);
2652
+ for (const lookupPath of projectLookupPaths) {
2653
+ const normalizedLookupPath = normalizeConfigPath(lookupPath, platform);
2654
+ for (const [configuredPath, rawProject] of projectEntries) {
2655
+ if (normalizeConfigPath(configuredPath, platform) !== normalizedLookupPath) {
2656
+ continue;
2657
+ }
2658
+ const trustLevel = asRecord(rawProject)?.trust_level;
2659
+ if (trustLevel === "trusted" || trustLevel === "untrusted") {
2660
+ return trustLevel;
2661
+ }
2662
+ }
2663
+ }
2664
+ return undefined;
2665
+ }
2666
+ function resolveConfigProjectLookupPaths(projectPath) {
2667
+ const lookupPaths = new Set();
2668
+ let canonicalPath;
2669
+ try {
2670
+ canonicalPath = realpathSync(projectPath);
2671
+ lookupPaths.add(canonicalPath);
2672
+ lookupPaths.add(projectPath);
2673
+ }
2674
+ catch {
2675
+ return [projectPath];
2676
+ }
2677
+ let candidate = canonicalPath;
2678
+ while (true) {
2679
+ try {
2680
+ statSync(join(candidate, ".git"));
2681
+ lookupPaths.add(candidate);
2682
+ break;
2683
+ }
2684
+ catch {
2685
+ const parent = dirname(candidate);
2686
+ if (parent === candidate)
2687
+ break;
2688
+ candidate = parent;
2689
+ }
2690
+ }
2691
+ return [...lookupPaths];
2692
+ }
2693
+ function normalizeConfigPath(value, platform) {
2694
+ const normalized = resolvePlatformPath(value, platform);
2695
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
2696
+ }
2697
+ function parseConfigApprovalPolicy(value) {
2698
+ switch (value) {
2699
+ case "never":
2700
+ case "on-request":
2701
+ case "on-failure":
2702
+ case "untrusted":
2703
+ return value;
2704
+ default:
2705
+ return isGranularApprovalPolicy(value) ? { ...value } : undefined;
2706
+ }
2707
+ }
2708
+ function isGranularApprovalPolicy(value) {
2709
+ const granular = asRecord(asRecord(value)?.granular);
2710
+ return (granular !== undefined &&
2711
+ typeof granular.sandbox_approval === "boolean" &&
2712
+ typeof granular.rules === "boolean" &&
2713
+ typeof granular.mcp_elicitations === "boolean" &&
2714
+ (granular.skill_approval === undefined ||
2715
+ typeof granular.skill_approval === "boolean") &&
2716
+ (granular.request_permissions === undefined ||
2717
+ typeof granular.request_permissions === "boolean"));
2718
+ }
2719
+ function parseConfigApprovalsReviewer(value) {
2720
+ switch (value) {
2721
+ case "user":
2722
+ case "auto_review":
2723
+ case "guardian_subagent":
2724
+ return value;
2725
+ default:
2726
+ return undefined;
2727
+ }
2728
+ }
2729
+ function parseConfigSandboxMode(value) {
2730
+ switch (value) {
2731
+ case "read-only":
2732
+ case "workspace-write":
2733
+ case "danger-full-access":
2734
+ return value;
2735
+ default:
2736
+ return undefined;
2737
+ }
2738
+ }
2594
2739
  function normalizeWritableRoots(roots, platform) {
2595
2740
  const normalized = new Map();
2596
2741
  for (const root of roots) {
@@ -2618,6 +2763,16 @@ function normalizeApprovalPolicy(value) {
2618
2763
  return "never";
2619
2764
  }
2620
2765
  }
2766
+ function normalizeApprovalPolicyForRpc(value) {
2767
+ return typeof value === "string" ? normalizeApprovalPolicy(value) : value;
2768
+ }
2769
+ function approvalPolicyForClient(value) {
2770
+ // Mobile's legacy mode model cannot represent granular policies. They still
2771
+ // request approval conditionally, so on-request is the closest UI projection.
2772
+ return typeof value === "string"
2773
+ ? value
2774
+ : "on-request";
2775
+ }
2621
2776
  function normalizeApprovalsReviewerForAppServer(value) {
2622
2777
  switch (value) {
2623
2778
  case "auto_review":
@@ -2738,9 +2893,7 @@ function extractResolvedSettingsFromThreadResponse(response) {
2738
2893
  return {
2739
2894
  model: sanitizeCodexModel(response.model)
2740
2895
  ?? sanitizeCodexModel(thread?.model),
2741
- approvalPolicy: typeof response.approvalPolicy === "string"
2742
- ? response.approvalPolicy
2743
- : undefined,
2896
+ approvalPolicy: parseConfigApprovalPolicy(response.approvalPolicy),
2744
2897
  approvalsReviewer: typeof response.approvalsReviewer === "string"
2745
2898
  ? response.approvalsReviewer
2746
2899
  : undefined,