mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Show runtime permissions in status panel
This commit is contained in:
parent
9842ce38c4
commit
07c29add11
38 changed files with 920 additions and 54 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { ReasoningSummary, RuntimeConfigSnapshot, Verbosity } from "../../domain/runtime/config";
|
||||
import type { RuntimeApprovalPolicy, RuntimePermissionState, RuntimeSandboxPolicy } from "../../domain/runtime/permissions";
|
||||
import { approvalsReviewerOrNull, parseServiceTier } from "../../domain/runtime/policy";
|
||||
|
||||
interface ConfigLayerRecord {
|
||||
|
|
@ -28,11 +29,59 @@ export function runtimeConfigSnapshotFromAppServerConfig(response: ConfigReadRes
|
|||
verbosity: verbosityOrNull(config["model_verbosity"]),
|
||||
serviceTier: parseServiceTier(config["service_tier"]),
|
||||
approvalsReviewer: approvalsReviewerOrNull(config["approvals_reviewer"]),
|
||||
startupPermissions: startupPermissionsFromConfig(config),
|
||||
modelContextWindow: numberOrNull(config["model_context_window"]),
|
||||
autoCompactTokenLimit: numberOrNull(config["model_auto_compact_token_limit"]),
|
||||
};
|
||||
}
|
||||
|
||||
function startupPermissionsFromConfig(config: Record<string, unknown>): RuntimePermissionState {
|
||||
return {
|
||||
approvalPolicy: approvalPolicyOrNull(config["approval_policy"]),
|
||||
sandboxPolicy: sandboxPolicyFromConfig(config),
|
||||
activePermissionProfile: permissionProfileFromConfig(config),
|
||||
};
|
||||
}
|
||||
|
||||
function permissionProfileFromConfig(config: Record<string, unknown>): RuntimePermissionState["activePermissionProfile"] {
|
||||
if (typeof config["sandbox_mode"] === "string") return null;
|
||||
const profile = nonEmptyStringOrNull(config["default_permissions"]);
|
||||
return profile ? { id: profile, extends: null } : null;
|
||||
}
|
||||
|
||||
function sandboxPolicyFromConfig(config: Record<string, unknown>): RuntimeSandboxPolicy | null {
|
||||
const mode = config["sandbox_mode"];
|
||||
if (mode === "danger-full-access") return { type: "dangerFullAccess" };
|
||||
if (mode === "read-only") return { type: "readOnly", networkAccess: false };
|
||||
if (mode !== "workspace-write") return null;
|
||||
|
||||
const workspaceWrite = recordOrNull(config["sandbox_workspace_write"]);
|
||||
return {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: stringArrayOrEmpty(workspaceWrite?.["writable_roots"]),
|
||||
networkAccess: booleanOrFalse(workspaceWrite?.["network_access"]),
|
||||
excludeTmpdirEnvVar: booleanOrFalse(workspaceWrite?.["exclude_tmpdir_env_var"]),
|
||||
excludeSlashTmp: booleanOrFalse(workspaceWrite?.["exclude_slash_tmp"]),
|
||||
};
|
||||
}
|
||||
|
||||
function approvalPolicyOrNull(value: unknown): RuntimeApprovalPolicy | null {
|
||||
if (value === "untrusted" || value === "on-failure" || value === "on-request" || value === "never") return value;
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const granular = (value as Record<string, unknown>)["granular"];
|
||||
if (!granular || typeof granular !== "object") return null;
|
||||
const granularRecord = granular as Record<string, unknown>;
|
||||
return {
|
||||
granular: {
|
||||
sandbox_approval: booleanOrFalse(granularRecord["sandbox_approval"]),
|
||||
rules: booleanOrFalse(granularRecord["rules"]),
|
||||
skill_approval: booleanOrFalse(granularRecord["skill_approval"]),
|
||||
request_permissions: booleanOrFalse(granularRecord["request_permissions"]),
|
||||
mcp_elicitations: booleanOrFalse(granularRecord["mcp_elicitations"]),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function selectedConfigProfile(layers: ConfigReadResult["layers"]): string | null {
|
||||
let selected: string | null = null;
|
||||
if (!layers) return null;
|
||||
|
|
@ -55,6 +104,18 @@ function numberOrNull(value: unknown): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function recordOrNull(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
function stringArrayOrEmpty(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
||||
}
|
||||
|
||||
function booleanOrFalse(value: unknown): boolean {
|
||||
return value === true;
|
||||
}
|
||||
|
||||
function reasoningSummaryOrNull(value: unknown): ReasoningSummary | null {
|
||||
return value === "auto" || value === "concise" || value === "detailed" || value === "none" ? value : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { normalizeReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import { type RuntimePermissionState, runtimePermissionStateOrDefault } from "../../domain/runtime/permissions";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "../../domain/runtime/policy";
|
||||
import { parseServiceTier } from "../../domain/runtime/policy";
|
||||
import type { RuntimeServiceTierRequest, RuntimeSettingsPatch } from "../../domain/runtime/thread-settings";
|
||||
|
|
@ -34,13 +35,14 @@ interface ThreadConversationSummaryPage {
|
|||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
interface ThreadActivationResponse {
|
||||
interface ThreadActivationResponse extends Partial<RuntimePermissionState> {
|
||||
thread: ThreadRecord;
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
serviceTier: ServiceTier | null;
|
||||
approvalsReviewer: ApprovalsReviewer | null;
|
||||
reasoningEffort: string | null;
|
||||
sandbox?: RuntimePermissionState["sandboxPolicy"];
|
||||
}
|
||||
|
||||
export interface AppServerStartThreadOptions {
|
||||
|
|
@ -215,6 +217,11 @@ export async function restoreArchivedThread(client: AppServerRequestClient, thre
|
|||
}
|
||||
|
||||
export function threadActivationSnapshotFromAppServerResponse(response: ThreadActivationResponse): ThreadActivationSnapshot {
|
||||
const permissions = runtimePermissionStateOrDefault({
|
||||
approvalPolicy: response.approvalPolicy ?? null,
|
||||
sandboxPolicy: response.sandbox ?? response.sandboxPolicy ?? null,
|
||||
activePermissionProfile: response.activePermissionProfile ?? null,
|
||||
});
|
||||
return {
|
||||
thread: threadFromThreadRecord(response.thread),
|
||||
cwd: response.cwd,
|
||||
|
|
@ -222,6 +229,7 @@ export function threadActivationSnapshotFromAppServerResponse(response: ThreadAc
|
|||
reasoningEffort: normalizeReasoningEffort(response.reasoningEffort),
|
||||
serviceTier: parseServiceTier(response.serviceTier),
|
||||
approvalsReviewer: response.approvalsReviewer,
|
||||
...permissions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ReasoningEffort } from "../catalog/metadata";
|
||||
import { cloneRuntimePermissionState, initialRuntimePermissionState, type RuntimePermissionState } from "./permissions";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "./policy";
|
||||
|
||||
export type ReasoningSummary = "auto" | "concise" | "detailed" | "none";
|
||||
|
|
@ -13,6 +14,7 @@ export interface RuntimeConfigSnapshot {
|
|||
readonly verbosity: Verbosity | null;
|
||||
readonly serviceTier: ServiceTier | null;
|
||||
readonly approvalsReviewer: ApprovalsReviewer | null;
|
||||
readonly startupPermissions: RuntimePermissionState;
|
||||
readonly modelContextWindow: number | null;
|
||||
readonly autoCompactTokenLimit: number | null;
|
||||
}
|
||||
|
|
@ -27,13 +29,17 @@ export function emptyRuntimeConfigSnapshot(): RuntimeConfigSnapshot {
|
|||
verbosity: null,
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
startupPermissions: initialRuntimePermissionState(),
|
||||
modelContextWindow: null,
|
||||
autoCompactTokenLimit: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneRuntimeConfigSnapshot(config: RuntimeConfigSnapshot): RuntimeConfigSnapshot {
|
||||
return { ...config };
|
||||
return {
|
||||
...config,
|
||||
startupPermissions: cloneRuntimePermissionState(config.startupPermissions),
|
||||
};
|
||||
}
|
||||
|
||||
export function runtimeConfigOrDefault(runtimeConfig: RuntimeConfigSnapshot | null): RuntimeConfigSnapshot {
|
||||
|
|
|
|||
74
src/domain/runtime/permissions.ts
Normal file
74
src/domain/runtime/permissions.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
export type RuntimeApprovalPolicy =
|
||||
| "untrusted"
|
||||
| "on-failure"
|
||||
| "on-request"
|
||||
| "never"
|
||||
| {
|
||||
granular: {
|
||||
sandbox_approval: boolean;
|
||||
rules: boolean;
|
||||
skill_approval: boolean;
|
||||
request_permissions: boolean;
|
||||
mcp_elicitations: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
interface RuntimeActivePermissionProfile {
|
||||
readonly id: string;
|
||||
readonly extends: string | null;
|
||||
}
|
||||
|
||||
type RuntimeNetworkAccess = "restricted" | "enabled";
|
||||
|
||||
export type RuntimeSandboxPolicy =
|
||||
| { readonly type: "dangerFullAccess" }
|
||||
| { readonly type: "readOnly"; readonly networkAccess: boolean }
|
||||
| { readonly type: "externalSandbox"; readonly networkAccess: RuntimeNetworkAccess }
|
||||
| {
|
||||
readonly type: "workspaceWrite";
|
||||
readonly writableRoots: readonly string[];
|
||||
readonly networkAccess: boolean;
|
||||
readonly excludeTmpdirEnvVar: boolean;
|
||||
readonly excludeSlashTmp: boolean;
|
||||
};
|
||||
|
||||
export interface RuntimePermissionState {
|
||||
readonly approvalPolicy: RuntimeApprovalPolicy | null;
|
||||
readonly sandboxPolicy: RuntimeSandboxPolicy | null;
|
||||
readonly activePermissionProfile: RuntimeActivePermissionProfile | null;
|
||||
}
|
||||
|
||||
export function initialRuntimePermissionState(): RuntimePermissionState {
|
||||
return {
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function runtimePermissionStateOrDefault(value: Partial<RuntimePermissionState> | null | undefined): RuntimePermissionState {
|
||||
return {
|
||||
approvalPolicy: value?.approvalPolicy ? cloneRuntimeApprovalPolicy(value.approvalPolicy) : null,
|
||||
sandboxPolicy: value?.sandboxPolicy ? cloneRuntimeSandboxPolicy(value.sandboxPolicy) : null,
|
||||
activePermissionProfile: value?.activePermissionProfile ? { ...value.activePermissionProfile } : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneRuntimePermissionState(value: RuntimePermissionState): RuntimePermissionState {
|
||||
return runtimePermissionStateOrDefault(value);
|
||||
}
|
||||
|
||||
function cloneRuntimeApprovalPolicy(value: RuntimeApprovalPolicy): RuntimeApprovalPolicy {
|
||||
if (typeof value === "string") return value;
|
||||
return {
|
||||
granular: { ...value.granular },
|
||||
};
|
||||
}
|
||||
|
||||
function cloneRuntimeSandboxPolicy(value: RuntimeSandboxPolicy): RuntimeSandboxPolicy {
|
||||
if (value.type !== "workspaceWrite") return { ...value };
|
||||
return {
|
||||
...value,
|
||||
writableRoots: [...value.writableRoots],
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ReasoningEffort } from "../catalog/metadata";
|
||||
import type { RuntimeApprovalPolicy } from "./permissions";
|
||||
import type { ApprovalsReviewer } from "./policy";
|
||||
|
||||
export type RuntimeServiceTierRequest = string | null | undefined;
|
||||
|
|
@ -15,7 +16,9 @@ export interface CollaborationMode {
|
|||
|
||||
export interface RuntimeSettingsPatch {
|
||||
cwd?: string | null;
|
||||
approvalPolicy?: RuntimeApprovalPolicy | null;
|
||||
approvalsReviewer?: ApprovalsReviewer | null;
|
||||
permissions?: string | null;
|
||||
model?: string | null;
|
||||
serviceTier?: string | null;
|
||||
effort?: ReasoningEffort | null;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import type { ReasoningEffort } from "../catalog/metadata";
|
||||
import type { RuntimePermissionState } from "../runtime/permissions";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "../runtime/policy";
|
||||
import type { Thread } from "./model";
|
||||
|
||||
export interface ThreadActivationSnapshot {
|
||||
export interface ThreadActivationSnapshot extends RuntimePermissionState {
|
||||
thread: Thread;
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { normalizeReasoningEffort, type ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { type RuntimePermissionState, runtimePermissionStateOrDefault } from "../../../../domain/runtime/permissions";
|
||||
import { parseServiceTier, type ServiceTier } from "../../../../domain/runtime/policy";
|
||||
import type { ServerInitialization } from "../../../../domain/server/initialization";
|
||||
import type { ThreadActivationSnapshot } from "../../../../domain/threads/activation";
|
||||
|
|
@ -19,12 +20,22 @@ interface ResumedThreadActionParams {
|
|||
interface ResumedThreadFromActiveRuntimeParams {
|
||||
thread: Thread;
|
||||
cwd: string;
|
||||
runtime: Pick<ActiveThreadRuntimeState, "model" | "reasoningEffort" | "serviceTier" | "serviceTierKnown" | "approvalsReviewer">;
|
||||
runtime: Pick<
|
||||
ActiveThreadRuntimeState,
|
||||
| "model"
|
||||
| "reasoningEffort"
|
||||
| "serviceTier"
|
||||
| "serviceTierKnown"
|
||||
| "approvalsReviewer"
|
||||
| "approvalPolicy"
|
||||
| "sandboxPolicy"
|
||||
| "activePermissionProfile"
|
||||
>;
|
||||
listedThreads?: readonly Thread[];
|
||||
items?: readonly MessageStreamItem[];
|
||||
}
|
||||
|
||||
export interface ActiveThreadResumedAction {
|
||||
export interface ActiveThreadResumedAction extends RuntimePermissionState {
|
||||
type: "active-thread/resumed";
|
||||
thread: Thread;
|
||||
cwd: string;
|
||||
|
|
@ -39,7 +50,7 @@ export interface ActiveThreadResumedAction {
|
|||
preserveRequestedRuntimeSettings?: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveThreadSettingsAppliedAction {
|
||||
export interface ActiveThreadSettingsAppliedAction extends RuntimePermissionState {
|
||||
type: "active-thread/settings-applied";
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
|
|
@ -49,7 +60,7 @@ export interface ActiveThreadSettingsAppliedAction {
|
|||
approvalsReviewer: ActiveThreadRuntimeState["approvalsReviewer"];
|
||||
}
|
||||
|
||||
export interface ActiveThreadSettingsAppliedActionSettings {
|
||||
export interface ActiveThreadSettingsAppliedActionSettings extends RuntimePermissionState {
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
effort: string | null;
|
||||
|
|
@ -114,6 +125,9 @@ export function resumedThreadActionFromActiveRuntime(params: ResumedThreadFromAc
|
|||
reasoningEffort: params.runtime.reasoningEffort,
|
||||
serviceTier: params.runtime.serviceTier,
|
||||
approvalsReviewer: params.runtime.approvalsReviewer,
|
||||
approvalPolicy: params.runtime.approvalPolicy,
|
||||
sandboxPolicy: params.runtime.sandboxPolicy,
|
||||
activePermissionProfile: params.runtime.activePermissionProfile,
|
||||
},
|
||||
serviceTierKnown: params.runtime.serviceTierKnown,
|
||||
...(params.listedThreads ? { listedThreads: params.listedThreads } : {}),
|
||||
|
|
@ -123,6 +137,7 @@ export function resumedThreadActionFromActiveRuntime(params: ResumedThreadFromAc
|
|||
|
||||
export function resumedThreadAction(params: ResumedThreadActionParams): ActiveThreadResumedAction {
|
||||
const { response } = params;
|
||||
const permissions = runtimePermissionStateOrDefault(response);
|
||||
return {
|
||||
type: "active-thread/resumed",
|
||||
thread: response.thread,
|
||||
|
|
@ -132,6 +147,7 @@ export function resumedThreadAction(params: ResumedThreadActionParams): ActiveTh
|
|||
serviceTier: response.serviceTier,
|
||||
serviceTierKnown: params.serviceTierKnown ?? true,
|
||||
approvalsReviewer: response.approvalsReviewer,
|
||||
...permissions,
|
||||
...(params.items ? { items: params.items } : {}),
|
||||
...(params.listedThreads ? { listedThreads: upsertThread(params.listedThreads, response.thread) } : {}),
|
||||
...(params.preserveRequestedRuntimeSettings ? { preserveRequestedRuntimeSettings: true } : {}),
|
||||
|
|
@ -139,6 +155,7 @@ export function resumedThreadAction(params: ResumedThreadActionParams): ActiveTh
|
|||
}
|
||||
|
||||
export function activeThreadSettingsAppliedAction(settings: ActiveThreadSettingsAppliedActionSettings): ActiveThreadSettingsAppliedAction {
|
||||
const permissions = runtimePermissionStateOrDefault(settings);
|
||||
return {
|
||||
type: "active-thread/settings-applied",
|
||||
cwd: settings.cwd,
|
||||
|
|
@ -147,5 +164,6 @@ export function activeThreadSettingsAppliedAction(settings: ActiveThreadSettings
|
|||
collaborationMode: settings.collaborationMode.mode,
|
||||
serviceTier: parseServiceTier(settings.serviceTier),
|
||||
approvalsReviewer: settings.approvalsReviewer,
|
||||
...permissions,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,6 +328,9 @@ function reduceActiveThreadResumedTransition(state: ChatState, action: ActiveThr
|
|||
collaborationMode: initialActiveChatRuntimeState().collaborationMode,
|
||||
serviceTier: action.serviceTier,
|
||||
approvalsReviewer: action.approvalsReviewer,
|
||||
approvalPolicy: action.approvalPolicy,
|
||||
sandboxPolicy: action.sandboxPolicy,
|
||||
activePermissionProfile: action.activePermissionProfile,
|
||||
},
|
||||
},
|
||||
turn: initialTurnState(),
|
||||
|
|
@ -353,6 +356,9 @@ function reduceActiveThreadSettingsAppliedTransition(state: ChatState, action: A
|
|||
collaborationMode: action.collaborationMode,
|
||||
serviceTier: action.serviceTier,
|
||||
approvalsReviewer: action.approvalsReviewer,
|
||||
approvalPolicy: action.approvalPolicy,
|
||||
sandboxPolicy: action.sandboxPolicy,
|
||||
activePermissionProfile: action.activePermissionProfile,
|
||||
},
|
||||
pending: {
|
||||
...state.runtime.pending,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, type ModelMetadata, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
|
||||
import { cloneRuntimePermissionState, type RuntimePermissionState } from "../../../../domain/runtime/permissions";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "../../../../domain/runtime/policy";
|
||||
import { effectiveCollaborationMode, type PendingRuntimeIntent, type RequestedFastMode } from "./intent";
|
||||
import type { RuntimeSnapshot } from "./snapshot";
|
||||
|
|
@ -25,7 +26,6 @@ interface FastModeResolution {
|
|||
|
||||
interface AutoReviewResolution {
|
||||
readonly active: boolean;
|
||||
readonly reviewer: ApprovalsReviewer | null;
|
||||
readonly source: RuntimeValueSource;
|
||||
}
|
||||
|
||||
|
|
@ -37,14 +37,23 @@ interface CollaborationModeResolution {
|
|||
readonly blockedReason: "missing-model" | null;
|
||||
}
|
||||
|
||||
interface RuntimePermissionsResolution {
|
||||
readonly scope: "new-thread" | "current-thread";
|
||||
readonly configured: RuntimePermissionState;
|
||||
readonly active: RuntimePermissionState | null;
|
||||
readonly effective: RuntimePermissionState;
|
||||
readonly reviewer: RuntimeLayeredValue<ApprovalsReviewer>;
|
||||
readonly source: RuntimeValueSource;
|
||||
}
|
||||
|
||||
export interface RuntimeControlsResolution {
|
||||
readonly model: RuntimeLayeredValue<string>;
|
||||
readonly reasoningEffort: RuntimeLayeredValue<ReasoningEffort>;
|
||||
readonly approvalsReviewer: RuntimeLayeredValue<ApprovalsReviewer>;
|
||||
readonly autoReview: AutoReviewResolution;
|
||||
readonly serviceTier: RuntimeLayeredValue<ServiceTier>;
|
||||
readonly fastMode: FastModeResolution;
|
||||
readonly collaborationMode: CollaborationModeResolution;
|
||||
readonly permissions: RuntimePermissionsResolution;
|
||||
readonly supportedReasoningEfforts: readonly ReasoningEffort[];
|
||||
}
|
||||
|
||||
|
|
@ -59,37 +68,100 @@ export function resolveRuntimeControls(snapshot: RuntimeSnapshot, config: Runtim
|
|||
active: snapshot.active.reasoningEffort,
|
||||
pending: snapshot.pending.reasoningEffort,
|
||||
});
|
||||
const approvalsReviewer = resolveRuntimeValue({
|
||||
const reviewer = resolveRuntimeValue({
|
||||
configured: config.approvalsReviewer,
|
||||
active: snapshot.active.approvalsReviewer,
|
||||
pending: snapshot.pending.approvalsReviewer,
|
||||
pending: snapshot.pending.permissions.reviewer,
|
||||
});
|
||||
const autoReview = resolveAutoReview(approvalsReviewer);
|
||||
const serviceTiers = findModelMetadataByIdOrName(snapshot.availableModels, model.effective)?.serviceTiers ?? [];
|
||||
const serviceTier = resolveServiceTier(snapshot, config);
|
||||
const fastMode = resolveFastMode(snapshot.pending.fastMode, serviceTier, serviceTiers);
|
||||
const collaborationMode = resolveCollaborationMode(snapshot, model.effective);
|
||||
const permissions = resolveRuntimePermissions(snapshot, config, reviewer);
|
||||
const autoReview = resolveAutoReview(permissions.reviewer);
|
||||
|
||||
return {
|
||||
model,
|
||||
reasoningEffort,
|
||||
approvalsReviewer,
|
||||
autoReview,
|
||||
serviceTier,
|
||||
fastMode,
|
||||
collaborationMode,
|
||||
permissions,
|
||||
supportedReasoningEfforts: supportedEffortsForModelMetadata(findModelMetadataByIdOrName(snapshot.availableModels, model.effective)),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAutoReview(approvalsReviewer: RuntimeLayeredValue<ApprovalsReviewer>): AutoReviewResolution {
|
||||
function resolveAutoReview(reviewer: RuntimeLayeredValue<ApprovalsReviewer>): AutoReviewResolution {
|
||||
return {
|
||||
active: approvalsReviewer.effective === "auto_review" || approvalsReviewer.effective === "guardian_subagent",
|
||||
reviewer: approvalsReviewer.effective,
|
||||
source: approvalsReviewer.source,
|
||||
active: reviewer.effective === "auto_review" || reviewer.effective === "guardian_subagent",
|
||||
source: reviewer.source,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimePermissions(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigSnapshot,
|
||||
reviewer: RuntimeLayeredValue<ApprovalsReviewer>,
|
||||
): RuntimePermissionsResolution {
|
||||
const configured = cloneRuntimePermissionState(config.startupPermissions);
|
||||
const scope = snapshot.activeThreadId ? "current-thread" : "new-thread";
|
||||
const baseSource = snapshot.activeThreadId ? "active-thread" : "config";
|
||||
if (snapshot.activeThreadId) {
|
||||
const active = cloneRuntimePermissionState(snapshot.active);
|
||||
const { effective, source } = resolveRuntimePermissionState(active, configured, snapshot.pending.permissions, baseSource);
|
||||
return {
|
||||
scope,
|
||||
configured,
|
||||
active,
|
||||
effective,
|
||||
reviewer,
|
||||
source,
|
||||
};
|
||||
}
|
||||
const { effective, source } = resolveRuntimePermissionState(configured, configured, snapshot.pending.permissions, baseSource);
|
||||
return {
|
||||
scope,
|
||||
configured,
|
||||
active: null,
|
||||
effective,
|
||||
reviewer,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimePermissionState(
|
||||
base: RuntimePermissionState,
|
||||
configured: RuntimePermissionState,
|
||||
pending: RuntimeSnapshot["pending"]["permissions"],
|
||||
baseSource: RuntimeValueSource,
|
||||
): Pick<RuntimePermissionsResolution, "effective" | "source"> {
|
||||
let effective = cloneRuntimePermissionState(base);
|
||||
let source = baseSource;
|
||||
const approvalPolicy = runtimePermissionIntentValue(pending.approvalPolicy, configured.approvalPolicy);
|
||||
if (approvalPolicy !== undefined) {
|
||||
effective = { ...effective, approvalPolicy };
|
||||
source = "pending";
|
||||
}
|
||||
const configuredPermissionProfile = configured.activePermissionProfile?.id ?? null;
|
||||
const permissionProfile = runtimePermissionIntentValue(pending.permissionProfile, configuredPermissionProfile);
|
||||
if (permissionProfile !== undefined) {
|
||||
effective = {
|
||||
...effective,
|
||||
sandboxPolicy: permissionProfile === configuredPermissionProfile ? configured.sandboxPolicy : null,
|
||||
activePermissionProfile: permissionProfile ? { id: permissionProfile, extends: null } : null,
|
||||
};
|
||||
source = "pending";
|
||||
}
|
||||
return { effective: cloneRuntimePermissionState(effective), source };
|
||||
}
|
||||
|
||||
function runtimePermissionIntentValue<T>(intent: PendingRuntimeIntent<T>, configured: T | null): T | null | undefined {
|
||||
if (intent.kind === "set") return intent.value;
|
||||
if (intent.kind === "resetToConfig") return configured;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveRuntimeValue<T>(input: {
|
||||
configured: T | null | undefined;
|
||||
active: T | null | undefined;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { normalizeReasoningEffort, type ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import {
|
||||
initialRuntimePermissionState,
|
||||
type RuntimeApprovalPolicy,
|
||||
type RuntimePermissionState,
|
||||
} from "../../../../domain/runtime/permissions";
|
||||
import { type ApprovalsReviewer, parseServiceTier, type ServiceTier } from "../../../../domain/runtime/policy";
|
||||
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
|
||||
import {
|
||||
|
|
@ -16,7 +21,7 @@ export interface ChatRuntimeState {
|
|||
readonly pending: PendingRuntimeIntentState;
|
||||
}
|
||||
|
||||
export interface ActiveThreadRuntimeState {
|
||||
export interface ActiveThreadRuntimeState extends RuntimePermissionState {
|
||||
readonly serviceTierKnown: boolean;
|
||||
readonly model: string | null;
|
||||
readonly reasoningEffort: ReasoningEffort | null;
|
||||
|
|
@ -28,13 +33,20 @@ export interface ActiveThreadRuntimeState {
|
|||
export interface PendingRuntimeIntentState {
|
||||
readonly model: PendingRuntimeIntent<string>;
|
||||
readonly reasoningEffort: PendingRuntimeIntent<ReasoningEffort>;
|
||||
readonly approvalsReviewer: PendingRuntimeIntent<ApprovalsReviewer>;
|
||||
readonly permissions: PendingRuntimePermissionIntentState;
|
||||
readonly collaborationMode: CollaborationModeSelection;
|
||||
readonly fastMode: PendingRuntimeIntent<RequestedFastMode>;
|
||||
}
|
||||
|
||||
interface PendingRuntimePermissionIntentState {
|
||||
readonly approvalPolicy: PendingRuntimeIntent<RuntimeApprovalPolicy>;
|
||||
readonly permissionProfile: PendingRuntimeIntent<string>;
|
||||
readonly reviewer: PendingRuntimeIntent<ApprovalsReviewer>;
|
||||
}
|
||||
|
||||
export function initialActiveChatRuntimeState(): ActiveThreadRuntimeState {
|
||||
return {
|
||||
...initialRuntimePermissionState(),
|
||||
serviceTierKnown: false,
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
|
|
@ -56,12 +68,20 @@ function initialPendingRuntimeIntentState(): PendingRuntimeIntentState {
|
|||
return {
|
||||
model: unchangedRuntimeIntent(),
|
||||
reasoningEffort: unchangedRuntimeIntent(),
|
||||
approvalsReviewer: unchangedRuntimeIntent(),
|
||||
permissions: initialPendingRuntimePermissionIntentState(),
|
||||
collaborationMode: "default",
|
||||
fastMode: unchangedRuntimeIntent(),
|
||||
};
|
||||
}
|
||||
|
||||
function initialPendingRuntimePermissionIntentState(): PendingRuntimePermissionIntentState {
|
||||
return {
|
||||
approvalPolicy: unchangedRuntimeIntent(),
|
||||
permissionProfile: unchangedRuntimeIntent(),
|
||||
reviewer: unchangedRuntimeIntent(),
|
||||
};
|
||||
}
|
||||
|
||||
export function initialChatRuntimeState(): ChatRuntimeState {
|
||||
return {
|
||||
active: initialActiveChatRuntimeState(),
|
||||
|
|
@ -114,14 +134,20 @@ export function clearRequestedFastModeRuntimeState(state: ChatRuntimeState): Cha
|
|||
export function requestApprovalsReviewerRuntimeState(state: ChatRuntimeState, approvalsReviewer: ApprovalsReviewer): ChatRuntimeState {
|
||||
return {
|
||||
...state,
|
||||
pending: { ...state.pending, approvalsReviewer: setRuntimeIntentValue(approvalsReviewer) },
|
||||
pending: {
|
||||
...state.pending,
|
||||
permissions: { ...state.pending.permissions, reviewer: setRuntimeIntentValue(approvalsReviewer) },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function clearRequestedApprovalsReviewerRuntimeState(state: ChatRuntimeState): ChatRuntimeState {
|
||||
return {
|
||||
...state,
|
||||
pending: { ...state.pending, approvalsReviewer: unchangedRuntimeIntent() },
|
||||
pending: {
|
||||
...state.pending,
|
||||
permissions: { ...state.pending.permissions, reviewer: unchangedRuntimeIntent() },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +170,7 @@ export function commitAppliedRuntimeSettingsPatchState(state: ChatRuntimeState,
|
|||
...("effort" in update ? { reasoningEffort: normalizeReasoningEffort(update.effort) } : {}),
|
||||
...("serviceTier" in update ? { serviceTier: parseServiceTier(update.serviceTier), serviceTierKnown: true } : {}),
|
||||
...("approvalsReviewer" in update ? { approvalsReviewer: update.approvalsReviewer ?? null } : {}),
|
||||
...("approvalPolicy" in update ? { approvalPolicy: update.approvalPolicy ?? null } : {}),
|
||||
...(update.collaborationMode ? { collaborationMode: update.collaborationMode.mode } : {}),
|
||||
},
|
||||
pending: {
|
||||
|
|
@ -151,7 +178,12 @@ export function commitAppliedRuntimeSettingsPatchState(state: ChatRuntimeState,
|
|||
...("model" in update ? { model: unchangedRuntimeIntent<string>() } : {}),
|
||||
...("effort" in update ? { reasoningEffort: unchangedRuntimeIntent<ReasoningEffort>() } : {}),
|
||||
...("serviceTier" in update ? { fastMode: unchangedRuntimeIntent<RequestedFastMode>() } : {}),
|
||||
...("approvalsReviewer" in update ? { approvalsReviewer: unchangedRuntimeIntent<ApprovalsReviewer>() } : {}),
|
||||
permissions: {
|
||||
...state.pending.permissions,
|
||||
...("approvalPolicy" in update ? { approvalPolicy: unchangedRuntimeIntent<RuntimeApprovalPolicy>() } : {}),
|
||||
...("permissions" in update ? { permissionProfile: unchangedRuntimeIntent<string>() } : {}),
|
||||
...("approvalsReviewer" in update ? { reviewer: unchangedRuntimeIntent<ApprovalsReviewer>() } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,11 +60,25 @@ export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: R
|
|||
"serviceTier",
|
||||
runtimeSettingsPatchValue(serviceTierPatchIntent(snapshot, resolution, "thread-update")),
|
||||
);
|
||||
if (snapshot.pending.approvalsReviewer.kind !== "unchanged") {
|
||||
if (snapshot.pending.permissions.approvalPolicy.kind !== "unchanged") {
|
||||
applyRuntimeSettingsPatchValue(
|
||||
update,
|
||||
"approvalPolicy",
|
||||
runtimeSettingsPatchValue(runtimeSettingsPatchIntentFromPending(snapshot.pending.permissions.approvalPolicy)),
|
||||
);
|
||||
}
|
||||
if (snapshot.pending.permissions.permissionProfile.kind !== "unchanged") {
|
||||
applyRuntimeSettingsPatchValue(
|
||||
update,
|
||||
"permissions",
|
||||
runtimeSettingsPatchValue(runtimeSettingsPatchIntentFromPending(snapshot.pending.permissions.permissionProfile)),
|
||||
);
|
||||
}
|
||||
if (snapshot.pending.permissions.reviewer.kind !== "unchanged") {
|
||||
applyRuntimeSettingsPatchValue(
|
||||
update,
|
||||
"approvalsReviewer",
|
||||
runtimeSettingsPatchValue(runtimeSettingsPatchIntentFromPending(snapshot.pending.approvalsReviewer)),
|
||||
runtimeSettingsPatchValue(runtimeSettingsPatchIntentFromPending(snapshot.pending.permissions.reviewer)),
|
||||
);
|
||||
}
|
||||
if (resolution.collaborationMode.dirty) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { Thread } from "../../../../domain/threads/model";
|
|||
import { threadRowCoreProjection } from "../../../threads/list/row-projection";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { appServerDiagnosticSections } from "../../presentation/runtime/diagnostic-sections";
|
||||
import { runtimePermissionDetails } from "../../presentation/runtime/permission-sections";
|
||||
import { rateLimitSummary } from "../../presentation/runtime/status";
|
||||
import { toolInventoryDiagnosticSections } from "../../presentation/runtime/tool-inventory-diagnostic-sections";
|
||||
import { Toolbar, type ToolbarActions, type ToolbarThreadRow, type ToolbarViewModel } from "../../ui/toolbar";
|
||||
|
|
@ -75,6 +76,10 @@ function chatPanelToolbarProjection(input: ToolbarViewModelInput): ToolbarViewMo
|
|||
const projection = toolbarStateProjection(input);
|
||||
const limit = rateLimitSummary(snapshot, input.nowMs);
|
||||
const diagnostics = model.diagnostics.value;
|
||||
const permissions = runtimePermissionDetails({
|
||||
snapshot,
|
||||
vaultPath: input.vaultPath,
|
||||
});
|
||||
return {
|
||||
newChatDisabled: projection.newChatDisabled,
|
||||
chatActionsOpen: projection.chatActionsOpen,
|
||||
|
|
@ -85,6 +90,8 @@ function chatPanelToolbarProjection(input: ToolbarViewModelInput): ToolbarViewMo
|
|||
openPanel: projection.openPanel,
|
||||
threads: projection.threads,
|
||||
connectLabel: input.connected ? "Reconnect" : "Connect",
|
||||
runtimePermissionsTitle: permissions.title,
|
||||
runtimePermissions: permissions.sections,
|
||||
diagnostics: appServerDiagnosticSections({
|
||||
connected: input.connected,
|
||||
configuredCommand: input.configuredCommand,
|
||||
|
|
|
|||
113
src/features/chat/presentation/runtime/permission-sections.ts
Normal file
113
src/features/chat/presentation/runtime/permission-sections.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { runtimeConfigOrDefault } from "../../../../domain/runtime/config";
|
||||
import type { RuntimePermissionState, RuntimeSandboxPolicy } from "../../../../domain/runtime/permissions";
|
||||
import { resolveRuntimeControls } from "../../domain/runtime/resolution";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
|
||||
interface RuntimePermissionRow {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RuntimePermissionSection {
|
||||
title: string;
|
||||
rows: RuntimePermissionRow[];
|
||||
}
|
||||
|
||||
interface RuntimePermissionDetails {
|
||||
title: string;
|
||||
sections: RuntimePermissionSection[];
|
||||
}
|
||||
|
||||
interface RuntimePermissionSectionsInput {
|
||||
snapshot: RuntimeSnapshot;
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function runtimePermissionDetails(input: RuntimePermissionSectionsInput): RuntimePermissionDetails {
|
||||
const config = runtimeConfigOrDefault(input.snapshot.runtimeConfig);
|
||||
const resolution = resolveRuntimeControls(input.snapshot, config);
|
||||
return {
|
||||
title: permissionPanelTitle(resolution.permissions.scope),
|
||||
sections: [
|
||||
{
|
||||
title: "",
|
||||
rows: permissionRows(resolution.permissions.effective, resolution.permissions.reviewer.effective, input.vaultPath),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function permissionPanelTitle(scope: "new-thread" | "current-thread"): string {
|
||||
return scope === "current-thread" ? "Permissions: Current Thread" : "Permissions: New Thread";
|
||||
}
|
||||
|
||||
function permissionRows(permissions: RuntimePermissionState, reviewer: string | null, vaultPath: string): RuntimePermissionRow[] {
|
||||
return [
|
||||
{ label: "Profile", value: profileLabel(permissions) },
|
||||
...extendsRows(permissions),
|
||||
{ label: "Sandbox", value: sandboxLabel(permissions.sandboxPolicy) },
|
||||
{ label: "Network", value: networkLabel(permissions.sandboxPolicy) },
|
||||
{ label: "Extra writable roots", value: writableRootsLabel(permissions.sandboxPolicy, vaultPath) },
|
||||
{ label: "Approval policy", value: approvalPolicyLabel(permissions.approvalPolicy) },
|
||||
{
|
||||
label: "Reviewer",
|
||||
value: reviewer ?? "(Codex default)",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function profileLabel(permissions: RuntimePermissionState): string {
|
||||
if (permissions.activePermissionProfile) return permissions.activePermissionProfile.id;
|
||||
if (permissions.sandboxPolicy) return "(legacy sandbox)";
|
||||
return "(not reported)";
|
||||
}
|
||||
|
||||
function extendsRows(permissions: RuntimePermissionState): RuntimePermissionRow[] {
|
||||
const extendsProfile = permissions.activePermissionProfile?.extends;
|
||||
return extendsProfile ? [{ label: "Extends", value: extendsProfile }] : [];
|
||||
}
|
||||
|
||||
function sandboxLabel(sandbox: RuntimeSandboxPolicy | null): string {
|
||||
if (!sandbox) return "(not reported)";
|
||||
switch (sandbox.type) {
|
||||
case "dangerFullAccess":
|
||||
return "danger-full-access";
|
||||
case "readOnly":
|
||||
return "read-only";
|
||||
case "workspaceWrite":
|
||||
return "workspace-write";
|
||||
case "externalSandbox":
|
||||
return "external sandbox";
|
||||
}
|
||||
}
|
||||
|
||||
function networkLabel(sandbox: RuntimeSandboxPolicy | null): string {
|
||||
if (!sandbox) return "(not reported)";
|
||||
if (sandbox.type === "dangerFullAccess") return "allowed";
|
||||
if (sandbox.type === "externalSandbox") return sandbox.networkAccess;
|
||||
return sandbox.networkAccess ? "allowed" : "blocked";
|
||||
}
|
||||
|
||||
function writableRootsLabel(sandbox: RuntimeSandboxPolicy | null, vaultPath: string): string {
|
||||
if (!sandbox) return "(not reported)";
|
||||
if (sandbox.type === "dangerFullAccess") return "all files";
|
||||
if (sandbox.type !== "workspaceWrite") return "-";
|
||||
if (sandbox.writableRoots.length === 0) return "(none)";
|
||||
return sandbox.writableRoots.map((root) => displayRoot(root, vaultPath)).join(", ");
|
||||
}
|
||||
|
||||
function displayRoot(root: string, vaultPath: string): string {
|
||||
if (root === vaultPath) return "Vault";
|
||||
if (vaultPath && root.startsWith(`${vaultPath}/`)) return `Vault/${root.slice(vaultPath.length + 1)}`;
|
||||
return root;
|
||||
}
|
||||
|
||||
function approvalPolicyLabel(policy: RuntimePermissionState["approvalPolicy"]): string {
|
||||
if (!policy) return "(not reported)";
|
||||
if (typeof policy === "string") return policy;
|
||||
|
||||
const enabled = Object.entries(policy.granular)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key.replace(/_/g, " "));
|
||||
return enabled.length ? `granular: ${enabled.join(", ")}` : "granular";
|
||||
}
|
||||
|
|
@ -21,15 +21,15 @@ export interface ToolbarThreadRow {
|
|||
} | null;
|
||||
}
|
||||
|
||||
interface ToolbarDiagnosticRow {
|
||||
interface ToolbarStatusRow {
|
||||
label: string;
|
||||
value: string;
|
||||
level?: "normal" | "warning" | "error";
|
||||
}
|
||||
|
||||
interface ToolbarDiagnosticSection {
|
||||
interface ToolbarStatusSection {
|
||||
title: string;
|
||||
rows: ToolbarDiagnosticRow[];
|
||||
rows: ToolbarStatusRow[];
|
||||
}
|
||||
|
||||
export interface ToolbarViewModel {
|
||||
|
|
@ -42,8 +42,10 @@ export interface ToolbarViewModel {
|
|||
openPanel: "history" | "chat-actions" | "status" | null;
|
||||
threads: ToolbarThreadRow[];
|
||||
connectLabel: string;
|
||||
diagnostics: ToolbarDiagnosticSection[];
|
||||
toolInventory: ToolbarDiagnosticSection[];
|
||||
runtimePermissionsTitle: string;
|
||||
runtimePermissions: ToolbarStatusSection[];
|
||||
diagnostics: ToolbarStatusSection[];
|
||||
toolInventory: ToolbarStatusSection[];
|
||||
}
|
||||
|
||||
interface ToolbarPrimaryActions {
|
||||
|
|
@ -194,6 +196,7 @@ function StatusPanel({ model, actions }: { model: ToolbarViewModel; actions: Too
|
|||
</div>
|
||||
<RateLimitPanel rateLimit={model.rateLimit} />
|
||||
<DiagnosticSectionsPanel title="Connection" sections={model.diagnostics} />
|
||||
<DiagnosticSectionsPanel title={model.runtimePermissionsTitle} sections={model.runtimePermissions} />
|
||||
<DiagnosticSectionsPanel title="Codex capabilities" sections={model.toolInventory} />
|
||||
</>
|
||||
);
|
||||
|
|
@ -247,7 +250,7 @@ function RateLimitPanel({ rateLimit }: { rateLimit: RateLimitSummary | null }):
|
|||
);
|
||||
}
|
||||
|
||||
function DiagnosticSectionsPanel({ title, sections }: { title: string; sections: ToolbarDiagnosticSection[] }): UiNode {
|
||||
function DiagnosticSectionsPanel({ title, sections }: { title: string; sections: ToolbarStatusSection[] }): UiNode {
|
||||
return (
|
||||
<div className="codex-panel__connection-diagnostics">
|
||||
<div className="codex-panel__connection-diagnostics-title">{title}</div>
|
||||
|
|
@ -258,7 +261,7 @@ function DiagnosticSectionsPanel({ title, sections }: { title: string; sections:
|
|||
);
|
||||
}
|
||||
|
||||
function DiagnosticRows({ rows }: { rows: ToolbarDiagnosticRow[] }): UiNode {
|
||||
function DiagnosticRows({ rows }: { rows: ToolbarStatusRow[] }): UiNode {
|
||||
return (
|
||||
<dl className="codex-panel__connection-diagnostics-list">
|
||||
{rows.map((row) => (
|
||||
|
|
@ -268,16 +271,16 @@ function DiagnosticRows({ rows }: { rows: ToolbarDiagnosticRow[] }): UiNode {
|
|||
);
|
||||
}
|
||||
|
||||
function DiagnosticSection({ section }: { section: ToolbarDiagnosticSection }): UiNode {
|
||||
function DiagnosticSection({ section }: { section: ToolbarStatusSection }): UiNode {
|
||||
return (
|
||||
<>
|
||||
<div className="codex-panel__connection-diagnostics-section">{section.title}</div>
|
||||
{section.title ? <div className="codex-panel__connection-diagnostics-section">{section.title}</div> : null}
|
||||
<DiagnosticRows rows={section.rows} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DiagnosticRow({ row }: { row: ToolbarDiagnosticRow }): UiNode {
|
||||
function DiagnosticRow({ row }: { row: ToolbarStatusRow }): UiNode {
|
||||
const level = row.level ?? "normal";
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ describe("app-server thread activation", () => {
|
|||
serviceTier: "fast",
|
||||
approvalsReviewer: "user",
|
||||
reasoningEffort: "high",
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
||||
activePermissionProfile: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ describe("app-server thread settings", () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(appServerRuntimeSettingsPatch({ permissions: ":workspace" })).toEqual({ permissions: ":workspace" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ describe("chat app-server actions", () => {
|
|||
expect(stateStore.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(stateStore.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
|
||||
expect(stateStore.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(stateStore.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(stateStore.getState().runtime.pending.permissions.reviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(stateStore.getState().runtime.pending.collaborationMode).toBe("plan");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1703,6 +1703,9 @@ describe("ChatInboundHandler", () => {
|
|||
expect(handler.currentState().runtime.active.model).toBe("gpt-5.5");
|
||||
expect(handler.currentState().runtime.active.serviceTier).toBe("fast");
|
||||
expect(handler.currentState().runtime.active.approvalsReviewer).toBe("auto_review");
|
||||
expect(handler.currentState().runtime.active.approvalPolicy).toBe("on-request");
|
||||
expect(handler.currentState().runtime.active.sandboxPolicy).toEqual({ type: "readOnly", networkAccess: false });
|
||||
expect(handler.currentState().runtime.active.activePermissionProfile).toBeNull();
|
||||
expect(chatStateMessageStreamItems(handler.currentState())).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -1713,6 +1716,9 @@ describe("ChatInboundHandler", () => {
|
|||
state = chatStateWith(state, { runtime: { active: { model: "gpt-active" } } });
|
||||
state = chatStateWith(state, { runtime: { active: { serviceTier: "flex" } } });
|
||||
state = chatStateWith(state, { runtime: { active: { approvalsReviewer: "user" } } });
|
||||
state = chatStateWith(state, { runtime: { active: { approvalPolicy: "on-request" } } });
|
||||
state = chatStateWith(state, { runtime: { active: { sandboxPolicy: { type: "readOnly", networkAccess: false } } } });
|
||||
state = chatStateWith(state, { runtime: { active: { activePermissionProfile: null } } });
|
||||
const handler = handlerForState(state);
|
||||
|
||||
handler.handleNotification({
|
||||
|
|
@ -1744,6 +1750,9 @@ describe("ChatInboundHandler", () => {
|
|||
expect(handler.currentState().runtime.active.model).toBe("gpt-active");
|
||||
expect(handler.currentState().runtime.active.serviceTier).toBe("flex");
|
||||
expect(handler.currentState().runtime.active.approvalsReviewer).toBe("user");
|
||||
expect(handler.currentState().runtime.active.approvalPolicy).toBe("on-request");
|
||||
expect(handler.currentState().runtime.active.sandboxPolicy).toEqual({ type: "readOnly", networkAccess: false });
|
||||
expect(handler.currentState().runtime.active.activePermissionProfile).toBeNull();
|
||||
});
|
||||
|
||||
it("syncs null service tier from settings notifications", () => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ function createHost(overrides: Partial<ChatReconnectActionsHost> = {}) {
|
|||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread" } as never,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -151,6 +151,9 @@ describe("submitComposer", () => {
|
|||
const { host, interruptTurn, showLatest, stateStore } = createHost("");
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const streamingPlanItem = (id: string): MessageStreamItem => ({
|
|||
function resumeThread(stateStore: ChatStateStore, items: readonly MessageStreamItem[]): void {
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread", cliVersion: "test" } as never,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ describe("executeSlashCommandWithState", () => {
|
|||
});
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread", "Thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -106,6 +109,9 @@ describe("executeSlashCommandWithState", () => {
|
|||
const { compactThread, host, stateStore } = createHost({ connectionAvailable: () => false });
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread", "Thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ function createHost(overrides: TurnSubmissionHostOverrides = {}) {
|
|||
function resumeThread(stateStore: ReturnType<typeof createChatStateStore>) {
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(store.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
|
||||
expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(store.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(store.getState().runtime.pending.permissions.reviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(store.getState().runtime.pending.collaborationMode).toBe("default");
|
||||
expect(messages).toEqual([
|
||||
"Fast mode on for subsequent turns.",
|
||||
|
|
@ -425,6 +425,9 @@ function threadSettings(
|
|||
collaborationMode: "default",
|
||||
serviceTier,
|
||||
approvalsReviewer,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ describe("chat thread resume helpers", () => {
|
|||
serviceTier: "fast",
|
||||
serviceTierKnown: true,
|
||||
approvalsReviewer: "user",
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
excludeTmpdirEnvVar: false,
|
||||
excludeSlashTmp: false,
|
||||
},
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
},
|
||||
listedThreads: [existing],
|
||||
items: [loading],
|
||||
|
|
@ -32,6 +41,15 @@ describe("chat thread resume helpers", () => {
|
|||
serviceTier: "fast",
|
||||
serviceTierKnown: true,
|
||||
approvalsReviewer: "user",
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
excludeTmpdirEnvVar: false,
|
||||
excludeSlashTmp: false,
|
||||
},
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
items: [loading],
|
||||
});
|
||||
expect(action.listedThreads?.map((thread) => thread.id)).toEqual(["thread", "existing"]);
|
||||
|
|
@ -47,6 +65,15 @@ describe("chat thread resume helpers", () => {
|
|||
|
||||
expect(action).toMatchObject({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
excludeTmpdirEnvVar: false,
|
||||
excludeSlashTmp: false,
|
||||
},
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
thread: resumed,
|
||||
preserveRequestedRuntimeSettings: true,
|
||||
});
|
||||
|
|
@ -62,6 +89,15 @@ function responseFixture(thread: Thread): ThreadActivationSnapshot {
|
|||
cwd: "/vault",
|
||||
approvalsReviewer: "user",
|
||||
reasoningEffort: "high",
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
excludeTmpdirEnvVar: false,
|
||||
excludeSlashTmp: false,
|
||||
},
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe("chatReducer", () => {
|
|||
expect(next.runtime.pending.model).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.reasoningEffort).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.fastMode).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.approvalsReviewer).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.permissions.reviewer).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.collaborationMode).toBe("default");
|
||||
expect(activeTurnId(next)).toBeNull();
|
||||
expect(chatStateMessageStreamItems(next)).toEqual([]);
|
||||
|
|
@ -102,6 +102,9 @@ describe("chatReducer", () => {
|
|||
|
||||
const next = chatReducer(state, {
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("resumed-thread"),
|
||||
cwd: "/vault",
|
||||
model: "gpt-5.1",
|
||||
|
|
@ -142,6 +145,9 @@ describe("chatReducer", () => {
|
|||
|
||||
const next = chatReducer(state, {
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("started-thread"),
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
|
|
@ -159,7 +165,7 @@ describe("chatReducer", () => {
|
|||
expect(next.runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(next.runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
|
||||
expect(next.runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(next.runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(next.runtime.pending.permissions.reviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(next.runtime.pending.collaborationMode).toBe("plan");
|
||||
expect(next.runtime.active.collaborationMode).toBeNull();
|
||||
});
|
||||
|
|
@ -170,6 +176,9 @@ describe("chatReducer", () => {
|
|||
|
||||
const next = chatReducer(state, {
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("resumed-thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -620,7 +629,7 @@ describe("chatReducer", () => {
|
|||
expect(next.runtime.active.serviceTierKnown).toBe(true);
|
||||
expect(next.runtime.pending.fastMode).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.active.approvalsReviewer).toBe("auto_review");
|
||||
expect(next.runtime.pending.approvalsReviewer).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.pending.permissions.reviewer).toEqual({ kind: "unchanged" });
|
||||
expect(next.runtime.active.collaborationMode).toBe("plan");
|
||||
});
|
||||
|
||||
|
|
@ -641,6 +650,9 @@ describe("chatReducer", () => {
|
|||
it("preserves unknown service tier state when resuming from active runtime", () => {
|
||||
const state = chatReducer(chatStateFixture(), {
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread"),
|
||||
cwd: "/vault",
|
||||
model: "gpt-5.1",
|
||||
|
|
@ -664,7 +676,7 @@ describe("chatReducer", () => {
|
|||
|
||||
expect(state.runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(state.runtime.active.serviceTier).toBe("flex");
|
||||
expect(state.runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(state.runtime.pending.permissions.reviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(state.runtime.active.approvalsReviewer).toBe("user");
|
||||
});
|
||||
|
||||
|
|
@ -683,7 +695,7 @@ describe("chatReducer", () => {
|
|||
expect(state.runtime.pending.model).toEqual({ kind: "resetToConfig" });
|
||||
expect(state.runtime.pending.reasoningEffort).toEqual({ kind: "resetToConfig" });
|
||||
expect(state.runtime.pending.fastMode).toEqual({ kind: "unchanged" });
|
||||
expect(state.runtime.pending.approvalsReviewer).toEqual({ kind: "unchanged" });
|
||||
expect(state.runtime.pending.permissions.reviewer).toEqual({ kind: "unchanged" });
|
||||
});
|
||||
|
||||
it("stores updates through ChatStateStore without mutating the initial snapshot", () => {
|
||||
|
|
@ -703,6 +715,9 @@ describe("chatReducer", () => {
|
|||
|
||||
panelA.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread-a"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -716,6 +731,9 @@ describe("chatReducer", () => {
|
|||
|
||||
panelB.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread-b"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ describe("createActiveThreadIdentitySync", () => {
|
|||
const { controller, host, restoredClear, stateStore } = createController();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -66,6 +69,9 @@ describe("createActiveThreadIdentitySync", () => {
|
|||
const { controller, host, restoredClear, stateStore } = createController();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("active"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -101,6 +107,9 @@ describe("createActiveThreadIdentitySync", () => {
|
|||
const { controller, host, restoredRename, stateStore } = createController();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: thread("thread", "Old"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -187,6 +187,9 @@ describe("createGoalActions", () => {
|
|||
const startThread = vi.fn().mockImplementation(async () => {
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread-new", name: null, preview: "Plan release", archived: false, createdAt: 1, updatedAt: 1 },
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ function activation(threadId: string, overrides: Partial<ThreadResumeSnapshot> =
|
|||
serviceTier: null,
|
||||
approvalsReviewer: "user",
|
||||
reasoningEffort: null,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
},
|
||||
rolloutPath: null,
|
||||
initialHistoryPage: null,
|
||||
|
|
@ -117,6 +120,9 @@ describe("ResumeActions", () => {
|
|||
const { actions, host, resumeThread, stateStore } = createActions();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("active"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -159,6 +165,9 @@ describe("ResumeActions", () => {
|
|||
await actions.resumeThread("thread");
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: second.activation.thread,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ describe("thread management actions", () => {
|
|||
host.threadTransport.compactThread.mockReturnValue(compact.promise);
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("source"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -97,6 +100,9 @@ describe("thread management actions", () => {
|
|||
});
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("other"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -235,6 +241,9 @@ describe("thread management actions", () => {
|
|||
host.threadTransport.forkThread.mockReturnValue(fork.promise);
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("source"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -255,6 +264,9 @@ describe("thread management actions", () => {
|
|||
});
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("other"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -313,6 +325,9 @@ describe("thread management actions", () => {
|
|||
const host = hostMock({ items: turnItems() });
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("source"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -345,6 +360,9 @@ describe("thread management actions", () => {
|
|||
host.threadTransport.rollbackThread.mockReturnValue(rollback.promise);
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("source"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -366,6 +384,9 @@ describe("thread management actions", () => {
|
|||
});
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("other"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
@ -391,6 +412,9 @@ describe("thread management actions", () => {
|
|||
});
|
||||
host.stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: panelThread("source"),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {
|
|||
function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: threadId, cliVersion: "test" } as never,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ describe("createReconnectAction", () => {
|
|||
const stateStore = createChatStateStore();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread-1" } as never,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ describe("createChatPanelSessionGraph actions", () => {
|
|||
});
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: threadFixture({ id: "thread-1", preview: "Active" }),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
const store = createChatStateStore(chatStateFixture());
|
||||
store.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread-1", preview: "", archived: false, createdAt: 1, updatedAt: 1, name: "Thread" },
|
||||
cwd: "/repo",
|
||||
model: null,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,49 @@ describe("chat panel surface projections", () => {
|
|||
unmountUiRoot(parent);
|
||||
});
|
||||
|
||||
it("renders new thread permission baseline in the status panel before a thread is active", () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { ui: { toolbarPanel: "status-panel" } });
|
||||
state = chatStateWith(state, {
|
||||
connection: {
|
||||
runtimeConfig: runtimeConfigFixture({
|
||||
default_permissions: ":workspace",
|
||||
approval_policy: "on-request",
|
||||
approvals_reviewer: "user",
|
||||
}),
|
||||
},
|
||||
});
|
||||
state = chatStateWith(state, {
|
||||
runtime: {
|
||||
pending: {
|
||||
permissions: {
|
||||
approvalPolicy: { kind: "unchanged" },
|
||||
permissionProfile: { kind: "unchanged" },
|
||||
reviewer: { kind: "set", value: "auto_review" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const parent = renderWithShellReadModel(state, (readModel) =>
|
||||
h(ChatPanelToolbar, {
|
||||
model: readModel.toolbar,
|
||||
surface: toolbarSurfaceFixture(),
|
||||
actions: toolbarActionsFixture(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(parent.textContent).toContain("Permissions: New Thread");
|
||||
expect(
|
||||
[...parent.querySelectorAll(".codex-panel__connection-diagnostics-section")].map((section) => section.textContent),
|
||||
).not.toContain("New thread");
|
||||
expect(parent.textContent).toContain(":workspace");
|
||||
expect(parent.textContent).toContain("on-request");
|
||||
expect(parent.textContent).toContain("auto_review");
|
||||
expect(parent.textContent).not.toContain("user -> auto_review");
|
||||
unmountUiRoot(parent);
|
||||
});
|
||||
|
||||
it("builds composer meta from context and runtime state", () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { activeThread: { id: "thread-1" } });
|
||||
|
|
|
|||
|
|
@ -167,7 +167,11 @@ describe("Toolbar decisions", () => {
|
|||
toolbarActions({ refreshStatus }),
|
||||
);
|
||||
|
||||
expect(parent.querySelector(".codex-panel__connection-diagnostics-title")?.textContent).toBe("Connection");
|
||||
expect([...parent.querySelectorAll(".codex-panel__connection-diagnostics-title")].map((title) => title.textContent)).toEqual([
|
||||
"Connection",
|
||||
"Permissions: Current Thread",
|
||||
"Codex capabilities",
|
||||
]);
|
||||
expect(parent.textContent).toContain("Codex capabilities");
|
||||
expect(parent.textContent).toContain("Tool providers");
|
||||
expect(parent.textContent).toContain("Process");
|
||||
|
|
@ -186,6 +190,44 @@ describe("Toolbar decisions", () => {
|
|||
expect(refreshStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders runtime permissions in the status menu without severity styling", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
mountToolbar(
|
||||
parent,
|
||||
toolbarModel({
|
||||
statusPanelOpen: true,
|
||||
openPanel: "status",
|
||||
runtimePermissionsTitle: "Permissions: Current Thread",
|
||||
runtimePermissions: [
|
||||
{
|
||||
title: "",
|
||||
rows: [
|
||||
{ label: "Profile", value: ":workspace" },
|
||||
{ label: "Sandbox", value: "workspace-write" },
|
||||
{ label: "Network", value: "blocked" },
|
||||
{ label: "Extra writable roots", value: "Vault" },
|
||||
{ label: "Approval policy", value: "on-request" },
|
||||
{ label: "Reviewer", value: "auto_review" },
|
||||
],
|
||||
},
|
||||
],
|
||||
toolInventory: [{ title: "Tool providers", rows: [{ label: "Tool providers", value: "loaded" }] }],
|
||||
}),
|
||||
toolbarActions(),
|
||||
);
|
||||
|
||||
expect(parent.textContent).toContain("Permissions: Current Thread");
|
||||
expect(
|
||||
[...parent.querySelectorAll(".codex-panel__connection-diagnostics-section")].map((section) => section.textContent),
|
||||
).not.toContain("Current thread");
|
||||
expect(parent.textContent).toContain(":workspace");
|
||||
expect(parent.textContent).toContain("workspace-write");
|
||||
expect(parent.textContent).toContain("auto_review");
|
||||
expect(parent.querySelector(".codex-panel__connection-diagnostics-row--warning")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__connection-diagnostics-row--error")).toBeNull();
|
||||
});
|
||||
|
||||
it("copies raw debug details from the status menu", () => {
|
||||
const parent = document.createElement("div");
|
||||
const copyDebugDetails = vi.fn();
|
||||
|
|
@ -362,6 +404,8 @@ function toolbarModel(overrides: Partial<ToolbarViewModel> = {}): ToolbarViewMod
|
|||
openPanel: null,
|
||||
threads: [{ title: "Thread", threadId: "thread", selected: true, disabled: false, canArchive: true, rename: null }],
|
||||
connectLabel: "Reconnect",
|
||||
runtimePermissionsTitle: "Permissions: Current Thread",
|
||||
runtimePermissions: [{ title: "", rows: [{ label: "Thread", value: "(none)" }] }],
|
||||
diagnostics: [{ title: "Process", rows: [{ label: "Codex App Server", value: "codex-cli/test" }] }],
|
||||
toolInventory: [{ title: "Tool providers", rows: [{ label: "Tool providers", value: "not loaded", level: "warning" }] }],
|
||||
...overrides,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,202 @@ describe("runtime settings", () => {
|
|||
expect(compactReasoningEffortLabel(null)).toBe("default");
|
||||
});
|
||||
|
||||
it("keeps startup permission defaults in the runtime config snapshot", () => {
|
||||
expect(
|
||||
runtimeConfigFixture({
|
||||
default_permissions: ":workspace",
|
||||
approval_policy: "on-request",
|
||||
approvals_reviewer: "auto_review",
|
||||
}),
|
||||
).toMatchObject({
|
||||
approvalsReviewer: "auto_review",
|
||||
startupPermissions: {
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("deep-clones granular startup approval policy in runtime config snapshots", () => {
|
||||
const config = runtimeConfigFixture({
|
||||
approval_policy: {
|
||||
granular: {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
skill_approval: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cloned = runtimeConfigOrDefault(config);
|
||||
const originalPolicy = config.startupPermissions.approvalPolicy;
|
||||
const clonedPolicy = cloned.startupPermissions.approvalPolicy;
|
||||
|
||||
expect(clonedPolicy).toEqual(originalPolicy);
|
||||
if (!originalPolicy || typeof originalPolicy === "string" || !clonedPolicy || typeof clonedPolicy === "string") {
|
||||
throw new Error("expected granular approval policy");
|
||||
}
|
||||
expect(clonedPolicy).not.toBe(originalPolicy);
|
||||
expect(clonedPolicy.granular).not.toBe(originalPolicy.granular);
|
||||
});
|
||||
|
||||
it("uses legacy sandbox config instead of default permissions when both are reported", () => {
|
||||
expect(
|
||||
runtimeConfigFixture({
|
||||
default_permissions: ":workspace",
|
||||
approval_policy: "on-request",
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/vault"],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
startupPermissions: {
|
||||
activePermissionProfile: null,
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves permissions through runtime controls without falling active threads back to config", () => {
|
||||
const configured = runtimeSnapshot({
|
||||
runtimeConfig: runtimeConfigFixture({
|
||||
default_permissions: ":workspace",
|
||||
approval_policy: "on-request",
|
||||
}),
|
||||
});
|
||||
expect(resolveRuntimeControls(configured, snapshotConfig(configured)).permissions).toMatchObject({
|
||||
scope: "new-thread",
|
||||
source: "config",
|
||||
active: null,
|
||||
reviewer: { effective: null, source: "none" },
|
||||
effective: {
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: null,
|
||||
},
|
||||
});
|
||||
|
||||
const activeUnreported = runtimeSnapshot({
|
||||
activeThreadId: "thread",
|
||||
runtimeConfig: runtimeConfigFixture({
|
||||
default_permissions: ":workspace",
|
||||
approval_policy: "on-request",
|
||||
}),
|
||||
});
|
||||
expect(resolveRuntimeControls(activeUnreported, snapshotConfig(activeUnreported)).permissions).toMatchObject({
|
||||
scope: "current-thread",
|
||||
source: "active-thread",
|
||||
reviewer: { effective: null, source: "none" },
|
||||
effective: {
|
||||
activePermissionProfile: null,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
},
|
||||
});
|
||||
|
||||
const activeReported = runtimeSnapshot({
|
||||
activeThreadId: "thread",
|
||||
active: {
|
||||
approvalPolicy: "never",
|
||||
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
||||
activePermissionProfile: { id: ":read-only", extends: null },
|
||||
approvalsReviewer: "user",
|
||||
},
|
||||
pending: { permissions: { reviewer: setRuntimeIntentValue("auto_review") } },
|
||||
});
|
||||
expect(resolveRuntimeControls(activeReported, snapshotConfig(activeReported)).permissions).toMatchObject({
|
||||
scope: "current-thread",
|
||||
source: "active-thread",
|
||||
reviewer: { effective: "auto_review", source: "pending" },
|
||||
effective: {
|
||||
activePermissionProfile: { id: ":read-only", extends: null },
|
||||
approvalPolicy: "never",
|
||||
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps permission display scope separate from pending permission source", () => {
|
||||
const snapshot = runtimeSnapshot({
|
||||
activeThreadId: "thread",
|
||||
active: {
|
||||
approvalPolicy: "on-request",
|
||||
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
||||
activePermissionProfile: null,
|
||||
},
|
||||
pending: {
|
||||
permissions: {
|
||||
approvalPolicy: setRuntimeIntentValue("never"),
|
||||
permissionProfile: setRuntimeIntentValue(":workspace"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveRuntimeControls(snapshot, snapshotConfig(snapshot)).permissions).toMatchObject({
|
||||
scope: "current-thread",
|
||||
source: "pending",
|
||||
effective: {
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
approvalPolicy: "never",
|
||||
sandboxPolicy: null,
|
||||
},
|
||||
});
|
||||
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
|
||||
update: {
|
||||
approvalPolicy: "never",
|
||||
permissions: ":workspace",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves permission profile reset intent back to legacy sandbox config", () => {
|
||||
const snapshot = runtimeSnapshot({
|
||||
activeThreadId: "thread",
|
||||
runtimeConfig: runtimeConfigFixture({
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/vault"],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
},
|
||||
}),
|
||||
active: {
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
sandboxPolicy: null,
|
||||
},
|
||||
pending: { permissions: { permissionProfile: resetRuntimeIntentToConfig() } },
|
||||
});
|
||||
|
||||
expect(resolveRuntimeControls(snapshot, snapshotConfig(snapshot)).permissions).toMatchObject({
|
||||
scope: "current-thread",
|
||||
source: "pending",
|
||||
effective: {
|
||||
activePermissionProfile: null,
|
||||
sandboxPolicy: {
|
||||
type: "workspaceWrite",
|
||||
writableRoots: ["/vault"],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
|
||||
update: { permissions: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps runtime defaults, resets, and collaboration mode semantics distinct", () => {
|
||||
const snapshot = runtimeSnapshot({
|
||||
pending: {
|
||||
|
|
@ -152,7 +348,7 @@ describe("runtime settings", () => {
|
|||
const reviewerSnapshot = runtimeSnapshot({
|
||||
pending: {
|
||||
collaborationMode: "plan",
|
||||
approvalsReviewer: setRuntimeIntentValue("auto_review"),
|
||||
permissions: { reviewer: setRuntimeIntentValue("auto_review") },
|
||||
},
|
||||
});
|
||||
const activeRuntimeSnapshot = runtimeSnapshot({
|
||||
|
|
@ -187,7 +383,7 @@ describe("runtime settings", () => {
|
|||
|
||||
it("resolves auto-review mode from requested, active, then effective config", () => {
|
||||
const requested = runtimeSnapshot({
|
||||
pending: { approvalsReviewer: setRuntimeIntentValue("user") },
|
||||
pending: { permissions: { reviewer: setRuntimeIntentValue("user") } },
|
||||
active: { approvalsReviewer: "auto_review" },
|
||||
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "guardian_subagent" }),
|
||||
});
|
||||
|
|
@ -223,7 +419,7 @@ describe("runtime settings", () => {
|
|||
|
||||
it("uses requested reviewer above active and configured reviewers", () => {
|
||||
const snapshot = runtimeSnapshot({
|
||||
pending: { approvalsReviewer: setRuntimeIntentValue("user") },
|
||||
pending: { permissions: { reviewer: setRuntimeIntentValue("user") } },
|
||||
active: { approvalsReviewer: "user" },
|
||||
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "auto_review" }),
|
||||
});
|
||||
|
|
@ -367,7 +563,7 @@ describe("runtime settings", () => {
|
|||
...active.pending,
|
||||
model: setRuntimeIntentValue("gpt-pending"),
|
||||
reasoningEffort: setRuntimeIntentValue("low"),
|
||||
approvalsReviewer: setRuntimeIntentValue("guardian_subagent"),
|
||||
permissions: { reviewer: setRuntimeIntentValue("guardian_subagent") },
|
||||
fastMode: setRuntimeIntentValue("enabled"),
|
||||
},
|
||||
});
|
||||
|
|
@ -375,19 +571,19 @@ describe("runtime settings", () => {
|
|||
expect(resolveRuntimeControls(configured, snapshotConfig(configured))).toMatchObject({
|
||||
model: { effective: "gpt-config", source: "config" },
|
||||
reasoningEffort: { effective: "medium", source: "config" },
|
||||
approvalsReviewer: { effective: "auto_review", source: "config" },
|
||||
permissions: { reviewer: { effective: "auto_review", source: "config" } },
|
||||
serviceTier: { effective: "fast", source: "config" },
|
||||
});
|
||||
expect(resolveRuntimeControls(active, snapshotConfig(active))).toMatchObject({
|
||||
model: { effective: "gpt-active", source: "active-thread" },
|
||||
reasoningEffort: { effective: "high", source: "active-thread" },
|
||||
approvalsReviewer: { effective: "user", source: "active-thread" },
|
||||
permissions: { reviewer: { effective: "user", source: "active-thread" } },
|
||||
serviceTier: { effective: "flex", source: "active-thread" },
|
||||
});
|
||||
expect(resolveRuntimeControls(pending, snapshotConfig(pending))).toMatchObject({
|
||||
model: { effective: "gpt-pending", source: "pending" },
|
||||
reasoningEffort: { effective: "low", source: "pending" },
|
||||
approvalsReviewer: { effective: "guardian_subagent", source: "pending" },
|
||||
permissions: { reviewer: { effective: "guardian_subagent", source: "pending" } },
|
||||
serviceTier: { effective: "fast", source: "pending" },
|
||||
fastMode: { active: true, source: "pending", serviceTierRequestValue: "fast" },
|
||||
});
|
||||
|
|
@ -420,7 +616,7 @@ describe("runtime settings", () => {
|
|||
});
|
||||
|
||||
it("resolves requested approval reviewer without adding it to turn runtime settings", () => {
|
||||
const snapshot = runtimeSnapshot({ pending: { approvalsReviewer: setRuntimeIntentValue("auto_review") } });
|
||||
const snapshot = runtimeSnapshot({ pending: { permissions: { reviewer: setRuntimeIntentValue("auto_review") } } });
|
||||
|
||||
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(true);
|
||||
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
|
||||
|
|
@ -663,7 +859,9 @@ describe("runtime settings", () => {
|
|||
|
||||
interface RuntimeSnapshotPatch extends Partial<Omit<RuntimeSnapshot, "active" | "pending">> {
|
||||
active?: Partial<RuntimeSnapshot["active"]>;
|
||||
pending?: Partial<RuntimeSnapshot["pending"]>;
|
||||
pending?: Partial<Omit<RuntimeSnapshot["pending"], "permissions">> & {
|
||||
permissions?: Partial<RuntimeSnapshot["pending"]["permissions"]>;
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeSnapshot(overrides: RuntimeSnapshotPatch = {}): RuntimeSnapshot {
|
||||
|
|
@ -683,11 +881,18 @@ function runtimeSnapshot(overrides: RuntimeSnapshotPatch = {}): RuntimeSnapshot
|
|||
collaborationMode: null,
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
},
|
||||
pending: {
|
||||
model: { kind: "unchanged" },
|
||||
reasoningEffort: { kind: "unchanged" },
|
||||
approvalsReviewer: { kind: "unchanged" },
|
||||
permissions: {
|
||||
approvalPolicy: { kind: "unchanged" },
|
||||
permissionProfile: { kind: "unchanged" },
|
||||
reviewer: { kind: "unchanged" },
|
||||
},
|
||||
collaborationMode: "default",
|
||||
fastMode: { kind: "unchanged" },
|
||||
},
|
||||
|
|
@ -707,6 +912,10 @@ function runtimeSnapshot(overrides: RuntimeSnapshotPatch = {}): RuntimeSnapshot
|
|||
pending: {
|
||||
...snapshot.pending,
|
||||
...pending,
|
||||
permissions: {
|
||||
...snapshot.pending.permissions,
|
||||
...(pending?.permissions ?? {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,18 @@ function fastMode(serviceTier: string | null, serviceTiers: ModelMetadata["servi
|
|||
collaborationMode: "default",
|
||||
serviceTier,
|
||||
approvalsReviewer: null,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
},
|
||||
pending: {
|
||||
model: { kind: "unchanged" },
|
||||
reasoningEffort: { kind: "unchanged" },
|
||||
approvalsReviewer: { kind: "unchanged" },
|
||||
permissions: {
|
||||
approvalPolicy: { kind: "unchanged" },
|
||||
permissionProfile: { kind: "unchanged" },
|
||||
reviewer: { kind: "unchanged" },
|
||||
},
|
||||
collaborationMode: "default",
|
||||
fastMode: { kind: "unchanged" },
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue