Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ function workRowSymbolName(icon: ThreadFeedActivity["icon"]): AppSymbolName {
return { ios: "globe", android: "public" };
case "hammer":
return { ios: "hammer", android: "construction" };
case "lock":
return { ios: "lock", android: "lock" };
case "message":
return { ios: "bubble.left", android: "chat_bubble" };
case "warning":
Expand Down
12 changes: 9 additions & 3 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as Order from "effect/Order";

export interface PendingApproval {
readonly requestId: ApprovalRequestId;
readonly requestKind: "command" | "file-read" | "file-change";
readonly requestKind: "command" | "file-read" | "file-change" | "permission";
readonly createdAt: string;
readonly detail?: string;
}
Expand Down Expand Up @@ -48,6 +48,7 @@ export interface ThreadFeedActivity {
| "eye"
| "globe"
| "hammer"
| "lock"
| "message"
| "warning"
| "wrench"
Expand Down Expand Up @@ -147,6 +148,8 @@ function requestKindFromRequestType(requestType: unknown): PendingApproval["requ
case "file_change_approval":
case "apply_patch_approval":
return "file-change";
case "permission_approval":
return "permission";
default:
return null;
}
Expand Down Expand Up @@ -632,6 +635,7 @@ function workEntryIcon(entry: DerivedWorkLogEntry): ThreadFeedActivity["icon"] {
if (entry.requestKind === "command") return "command";
if (entry.requestKind === "file-read") return "eye";
if (entry.requestKind === "file-change") return "edit";
if (entry.requestKind === "permission") return "lock";
if (entry.itemType === "command_execution" || entry.command) return "command";
if (entry.itemType === "file_change" || (entry.changedFiles?.length ?? 0) > 0) return "edit";
if (entry.itemType === "web_search") return "globe";
Expand Down Expand Up @@ -967,7 +971,8 @@ function extractWorkLogRequestKind(
if (
payload?.requestKind === "command" ||
payload?.requestKind === "file-read" ||
payload?.requestKind === "file-change"
payload?.requestKind === "file-change" ||
payload?.requestKind === "permission"
) {
return payload.requestKind;
}
Expand Down Expand Up @@ -1367,7 +1372,8 @@ export function derivePendingApprovals(
const requestKind =
payload?.requestKind === "command" ||
payload?.requestKind === "file-read" ||
payload?.requestKind === "file-change"
payload?.requestKind === "file-change" ||
payload?.requestKind === "permission"
? payload.requestKind
: requestKindFromRequestType(payload?.requestType);
const detail = typeof payload?.detail === "string" ? payload.detail : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type OrchestrationProposedPlan,
type OrchestrationThread,
type OrchestrationThreadActivity,
type ProviderRequestKind,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import * as Cache from "effect/Cache";
Expand Down Expand Up @@ -298,7 +299,7 @@ function sessionStatusAllowsActiveTurn(

function requestKindFromCanonicalRequestType(
requestType: string | undefined,
): "command" | "file-read" | "file-change" | undefined {
): ProviderRequestKind | undefined {
switch (requestType) {
case "command_execution_approval":
case "exec_command_approval":
Expand All @@ -308,6 +309,8 @@ function requestKindFromCanonicalRequestType(
case "file_change_approval":
case "apply_patch_approval":
return "file-change";
case "permission_approval":
return "permission";
default:
return undefined;
}
Expand Down Expand Up @@ -388,7 +391,9 @@ export function runtimeEventToActivities(
? "File-read approval requested"
: requestKind === "file-change"
? "File-change approval requested"
: "Approval requested",
: requestKind === "permission"
? "App permission approval requested"
: "Approval requested",
payload: {
requestId: toApprovalRequestId(event.requestId),
...(requestKind ? { requestKind } : {}),
Expand Down
42 changes: 42 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,48 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
}),
);

it.effect("maps app permission approval requests to permission_approval request types", () =>
Effect.gen(function* () {
const { adapter, runtime } = yield* startLifecycleRuntime();
const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);

yield* runtime.emit({
id: asEventId("evt-app-permission-request"),
kind: "request",
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:00.000Z",
method: "item/permissions/requestApproval",
requestId: ApprovalRequestId.make("req-perm-1"),
requestKind: "permission",
turnId: asTurnId("turn-1"),
itemId: asItemId("app_1"),
payload: {
cwd: "/tmp/project",
itemId: "app_1",
permissions: { network: { enabled: true } },
reason: "Fetch data from api.example.com",
startedAtMs: 1_778_000_000_000,
threadId: "thread-1",
turnId: "turn-1",
},
} satisfies ProviderEvent);

const firstEvent = yield* Fiber.join(firstEventFiber);

NodeAssert.equal(firstEvent._tag, "Some");
if (firstEvent._tag !== "Some") {
return;
}
NodeAssert.equal(firstEvent.value.type, "request.opened");
if (firstEvent.value.type !== "request.opened") {
return;
}
NodeAssert.equal(firstEvent.value.payload.requestType, "permission_approval");
NodeAssert.equal(firstEvent.value.payload.detail, "Fetch data from api.example.com");
}),
);

it.effect("maps session/closed lifecycle events to canonical session.exited runtime events", () =>
Effect.gen(function* () {
const { adapter, runtime } = yield* startLifecycleRuntime();
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ function toRequestTypeFromMethod(method: string): CanonicalRequestType {
return "file_read_approval";
case "item/fileChange/requestApproval":
return "file_change_approval";
case "item/permissions/requestApproval":
return "permission_approval";
case "applyPatchApproval":
return "apply_patch_approval";
case "execCommandApproval":
Expand All @@ -325,6 +327,8 @@ function toRequestTypeFromKind(kind: ProviderRequestKind | undefined): Canonical
return "file_read_approval";
case "file-change":
return "file_change_approval";
case "permission":
return "permission_approval";
default:
return "unknown";
}
Expand Down Expand Up @@ -819,6 +823,13 @@ function mapToRuntimeEvents(
);
return payload?.reason ?? undefined;
}
case "item/permissions/requestApproval": {
const payload = readPayload(
EffectCodexSchema.ServerRequest__PermissionsRequestApprovalParams,
event.payload,
);
return payload?.reason ?? undefined;
Comment on lines +826 to +831

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show the permissions being granted

When reason is absent—which the protocol schema explicitly permits—or does not enumerate the requested capabilities, this returns no useful detail. runtimeEventToActivities subsequently drops the request's args, so web and mobile only show a generic “App permission approval” card even though approving may grant filesystem or network access, potentially for the entire session. Summarize payload.permissions into the canonical request detail or carry a structured canonical permission field so users can review what they are granting without adding Codex-specific parsing to each client.

AGENTS.md reference: AGENTS.md:L146-L146

Useful? React with 👍 / 👎.

}
case "applyPatchApproval": {
const payload = readPayload(
EffectCodexSchema.ServerRequest__ApplyPatchApprovalParams,
Expand Down
63 changes: 63 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,69 @@ export const makeCodexSessionRuntime = (
}),
);

yield* client.handleServerRequest("item/permissions/requestApproval", (payload) =>
Effect.gen(function* () {
const requestId = ApprovalRequestId.make(
yield* randomUUIDv4("app-permission-approval-request"),
);
const turnId = TurnId.make(payload.turnId);
const itemId = ProviderItemId.make(payload.itemId);
const decision = yield* Deferred.make<ProviderApprovalDecision>();

yield* Ref.update(pendingApprovalsRef, (current) => {
const next = new Map(current);
next.set(requestId, {
requestId,
jsonRpcId: payload.itemId,
requestKind: "permission",
turnId,
itemId,
decision,
});
return next;
});
yield* Ref.update(approvalCorrelationsRef, (current) => {
const next = new Map(current);
next.set(payload.itemId, {
requestId,
requestKind: "permission",
turnId,
itemId,
});
return next;
});

yield* emitEvent({
kind: "request",
threadId: options.threadId,
method: "item/permissions/requestApproval",
requestId,
requestKind: "permission",
...(turnId ? { turnId } : {}),
...(itemId ? { itemId } : {}),
payload,
});

const resolved = yield* Deferred.await(decision).pipe(
Effect.ensuring(
Ref.update(pendingApprovalsRef, (current) => {
const next = new Map(current);
next.delete(requestId);
return next;
}),
),
);
// Approving grants the requested profile; denying answers with an
// empty grant so the app-server treats the permission as withheld.
const grantedPermissions =
resolved === "accept" || resolved === "acceptForSession" ? payload.permissions : {};
return {
permissions: grantedPermissions,
...(resolved === "acceptForSession" ? { scope: "session" as const } : {}),
} satisfies EffectCodexSchema.PermissionsRequestApprovalResponse;
}),
);

yield* client.handleServerRequest("item/tool/requestUserInput", (payload) =>
Effect.gen(function* () {
const requestId = ApprovalRequestId.make(yield* randomUUIDv4("user-input-request"));
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova
? "Command approval"
: approval.requestKind === "file-read"
? "File read approval"
: "File change approval";
: approval.requestKind === "permission"
? "App permission approval"
: "File change approval";
const detailAriaLabel =
approval.requestKind === "command"
? "Command"
: approval.requestKind === "file-read"
? "File to read"
: "File change";
: approval.requestKind === "permission"
? "Permission request"
: "File change";

return (
<div
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ interface DerivedWorkLogEntry extends WorkLogEntry {

export interface PendingApproval {
requestId: ApprovalRequestId;
requestKind: "command" | "file-read" | "file-change";
requestKind: "command" | "file-read" | "file-change" | "permission";
createdAt: string;
detail?: string;
}
Expand Down Expand Up @@ -378,6 +378,8 @@ function requestKindFromRequestType(requestType: unknown): PendingApproval["requ
case "file_change_approval":
case "apply_patch_approval":
return "file-change";
case "permission_approval":
return "permission";
default:
return null;
}
Expand Down Expand Up @@ -418,7 +420,8 @@ export function derivePendingApprovals(
payload &&
(payload.requestKind === "command" ||
payload.requestKind === "file-read" ||
payload.requestKind === "file-change")
payload.requestKind === "file-change" ||
payload.requestKind === "permission")
? payload.requestKind
: payload
? requestKindFromRequestType(payload.requestType)
Expand Down Expand Up @@ -1674,7 +1677,8 @@ function extractWorkLogRequestKind(
if (
payload?.requestKind === "command" ||
payload?.requestKind === "file-read" ||
payload?.requestKind === "file-change"
payload?.requestKind === "file-change" ||
payload?.requestKind === "permission"
) {
return payload.requestKind;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/contracts/src/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
export const ProviderInteractionMode = Schema.Literals(["default", "plan"]);
export type ProviderInteractionMode = typeof ProviderInteractionMode.Type;
export const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = "default";
export const ProviderRequestKind = Schema.Literals(["command", "file-read", "file-change"]);
export const ProviderRequestKind = Schema.Literals([
"command",
"file-read",
"file-change",
"permission",
]);
export type ProviderRequestKind = typeof ProviderRequestKind.Type;
export const AssistantDeliveryMode = Schema.Literals(["buffered", "streaming"]);
export type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type;
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/providerRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const CanonicalRequestType = Schema.Literals([
"file_change_approval",
"apply_patch_approval",
"exec_command_approval",
"permission_approval",
"tool_user_input",
"dynamic_tool_call",
"auth_tokens_refresh",
Expand Down
1 change: 1 addition & 0 deletions packages/effect-codex-app-server/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const CodexAppServerIdentifierPurpose = Schema.Literals([
"provider-event",
"command-approval-request",
"file-change-approval-request",
"app-permission-approval-request",
"user-input-request",
]);
export type CodexAppServerIdentifierPurpose = typeof CodexAppServerIdentifierPurpose.Type;
Expand Down
Loading