Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/provider-core/src/server/failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ it("keeps unrecognized reasons and provider text out of category messages", () =
);
});

it("names a managed Codex policy that refuses the thread's access mode", () => {
const message = makeProviderFailure({
cause: {
_tag: "ProviderAdapterTurnStartError",
cause: {
_tag: "CodexAppServerRequestError",
method: "turn/start",
code: -32600,
message:
"invalid thread settings override: invalid value for `sandbox_mode`: `DangerFullAccess` is not in the allowed set [ReadOnly, WorkspaceWrite] (set by /etc/codex/requirements.toml)",
},
},
}).message;
assert.notInclude(message, "Retry the turn");
assert.match(message, /requirements|policy/iu);
});

it("uses the innermost category without carrying an outer reason into it", () => {
assert.equal(
makeProviderFailure({
Expand Down
16 changes: 14 additions & 2 deletions packages/provider-core/src/server/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const PROVIDER_FAILURE_CATEGORIES: Partial<
const CODEX_GOAL_VALIDATION_MESSAGE =
/^goal objective must (?:not be empty|be at most \d{1,6} characters)$/u;

// codex_config::ConstraintError::InvalidValue for a managed requirements.toml or cloud policy.
const CODEX_ACCESS_REQUIREMENT_REJECTION =
/invalid value for `(approval_policy|sandbox_mode)`: `([A-Za-z]{1,32})` is not in the allowed set /u;

const PROVIDER_FAILURE_REASONS: Record<ProviderAdapterFailureReason, string> = {
previous_turn_active: "the previous turn is still running",
attachment_unreadable: "an attachment could not be read",
Expand Down Expand Up @@ -119,8 +123,8 @@ function curatedCauseMessages(cause: unknown): {
case "ProviderCompactionUnsupportedError":
return { known: stringField(cause, "message"), category: categoryMessage() };
case "CodexAppServerRequestError": {
// Only Codex's fixed-shape goal validation text is actionable and
// safe to show; other request error text stays hidden.
// Only Codex's fixed-shape goal validation and managed-requirements
// text is actionable and safe to show; other request error text stays hidden.
const rejection = stringField(cause, "message");
if (
(cause as Record<string, unknown>).method === "thread/goal/set" &&
Expand All @@ -129,6 +133,14 @@ function curatedCauseMessages(cause: unknown): {
) {
return { known: `Codex rejected the goal: ${rejection}.`, category: categoryMessage() };
}
const access =
rejection === undefined ? null : CODEX_ACCESS_REQUIREMENT_REJECTION.exec(rejection);
if (access !== null) {
return {
known: `Codex's managed requirements do not allow \`${access[1]}\` \`${access[2]}\`, which this thread's access mode uses. Switch the thread to an access mode the policy allows.`,
category: categoryMessage(),
};
}
break;
}
case "AcpProcessExitedError": {
Expand Down
Loading