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
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ data class AppLaunchStateSnapshot(
private const val PREFS_NAME = "litter.launchState"
private const val APPROVAL_POLICY_KEY = "litter.approvalPolicy"
private const val SANDBOX_MODE_KEY = "litter.sandboxMode"
private const val DEFAULT_APPROVAL_POLICY = "inherit"
private const val DEFAULT_SANDBOX_MODE = "inherit"
private const val DEFAULT_APPROVAL_POLICY = "never"
private const val DEFAULT_SANDBOX_MODE = "danger-full-access"
private const val CUSTOM_PERMISSION_VALUE = "custom"

class AppLaunchState(context: Context) {
Expand Down Expand Up @@ -145,7 +145,7 @@ class AppLaunchState(context: Context) {
if (threadKey != null) {
permissionOverride(threadKey)?.let { permission ->
permission.rawApprovalPolicy ?: askForApprovalFromWireValue(permission.approvalPolicy)
}
} ?: askForApprovalFromWireValue(snapshot.value.approvalPolicy)
} else {
askForApprovalFromWireValue(snapshot.value.approvalPolicy)
}
Expand All @@ -155,7 +155,7 @@ class AppLaunchState(context: Context) {
permissionOverride(threadKey)?.let { permission ->
permission.rawSandboxPolicy?.toLaunchSandboxMode()
?: sandboxModeFromWireValue(permission.sandboxMode)
}
} ?: sandboxModeFromWireValue(snapshot.value.sandboxMode)
} else {
sandboxModeFromWireValue(snapshot.value.sandboxMode)
}
Expand All @@ -164,7 +164,7 @@ class AppLaunchState(context: Context) {
if (threadKey != null) {
permissionOverride(threadKey)?.let { permission ->
permission.rawSandboxPolicy ?: sandboxModeFromWireValue(permission.sandboxMode)?.toTurnSandboxPolicy()
}
} ?: sandboxModeFromWireValue(snapshot.value.sandboxMode)?.toTurnSandboxPolicy()
} else {
sandboxModeValue()?.toTurnSandboxPolicy()
}
Expand Down Expand Up @@ -203,14 +203,14 @@ class AppLaunchState(context: Context) {

fun selectedApprovalPolicy(threadKey: ThreadKey? = null): String =
if (threadKey != null) {
permissionOverride(threadKey)?.approvalPolicy ?: DEFAULT_APPROVAL_POLICY
permissionOverride(threadKey)?.approvalPolicy ?: snapshot.value.approvalPolicy
} else {
snapshot.value.approvalPolicy
}

fun selectedSandboxMode(threadKey: ThreadKey? = null): String =
if (threadKey != null) {
permissionOverride(threadKey)?.sandboxMode ?: DEFAULT_SANDBOX_MODE
permissionOverride(threadKey)?.sandboxMode ?: snapshot.value.sandboxMode
} else {
snapshot.value.sandboxMode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,11 @@ class VoiceRuntimeController {

is uniffi.codex_mobile_client.HandoffAction.SendTurn -> {
try {
val payload = AppComposerPayload(text = action.transcript)
val payload = AppComposerPayload(
text = action.transcript,
approvalPolicy = appModel.launchState.approvalPolicyValue(),
sandboxPolicy = appModel.launchState.turnSandboxPolicy(),
)
appModel.startTurn(
ThreadKey(serverId = action.targetServerId, threadId = action.threadId),
payload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.litter.android.state.canBrowseDirectories
import kotlinx.coroutines.launch
import uniffi.codex_mobile_client.AbsolutePath
import uniffi.codex_mobile_client.AppExecCommandRequest
import uniffi.codex_mobile_client.AppSandboxPolicy

@Composable
fun DirectoryPickerSheet(
Expand Down Expand Up @@ -124,7 +125,7 @@ fun DirectoryPickerSheet(
disableTimeout = false,
timeoutMs = null,
cwd = "/tmp",
sandboxPolicy = null,
sandboxPolicy = AppSandboxPolicy.DangerFullAccess,
),
)
}.getOrNull()
Expand Down Expand Up @@ -157,7 +158,7 @@ fun DirectoryPickerSheet(
disableTimeout = false,
timeoutMs = null,
cwd = normalizedPath,
sandboxPolicy = null,
sandboxPolicy = AppSandboxPolicy.DangerFullAccess,
),
)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/ios/Sources/Litter/CarPlay/CarPlayVoiceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ final class CarPlayVoiceManager {
cwd: cwd,
model: nil,
approvalPolicy: .never,
sandboxMode: nil
sandboxMode: .dangerFullAccess
)
if let session = voiceActions.activeVoiceSession {
pushActiveSession(session)
Expand Down
4 changes: 2 additions & 2 deletions apps/ios/Sources/Litter/Models/AppLifecycleController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ final class AppLifecycleController {
let cwd = existing?.info.cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
let config = AppThreadLaunchConfig(
model: existing?.resolvedModel,
approvalPolicy: nil,
sandbox: nil,
approvalPolicy: .never,
sandbox: .dangerFullAccess,
developerInstructions: nil,
persistExtendedHistory: true
)
Expand Down
14 changes: 7 additions & 7 deletions apps/ios/Sources/Litter/Models/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ final class AppState {
}

init() {
approvalPolicy = UserDefaults.standard.string(forKey: Self.approvalPolicyKey) ?? "inherit"
sandboxMode = UserDefaults.standard.string(forKey: Self.sandboxModeKey) ?? "inherit"
approvalPolicy = UserDefaults.standard.string(forKey: Self.approvalPolicyKey) ?? "never"
sandboxMode = UserDefaults.standard.string(forKey: Self.sandboxModeKey) ?? "danger-full-access"
}

func toggleSessionFolder(_ folderPath: String) {
Expand All @@ -60,21 +60,21 @@ final class AppState {
func approvalPolicy(for threadKey: ThreadKey?) -> String {
guard let threadKey else { return approvalPolicy }
return threadPermissionOverrides[permissionKey(for: threadKey)]?.approvalPolicy
?? Self.inheritPermissionValue
?? approvalPolicy
}

func sandboxMode(for threadKey: ThreadKey?) -> String {
guard let threadKey else { return sandboxMode }
return threadPermissionOverrides[permissionKey(for: threadKey)]?.sandboxMode
?? Self.inheritPermissionValue
?? sandboxMode
}

func launchApprovalPolicy(for threadKey: ThreadKey?) -> AppAskForApproval? {
guard let threadKey else {
return AppAskForApproval(wireValue: approvalPolicy)
}
guard let permissions = threadPermissionOverrides[permissionKey(for: threadKey)] else {
return nil
return AppAskForApproval(wireValue: approvalPolicy)
}
return permissions.rawApprovalPolicy ?? AppAskForApproval(wireValue: permissions.approvalPolicy)
}
Expand All @@ -84,7 +84,7 @@ final class AppState {
return AppSandboxMode(wireValue: sandboxMode)
}
guard let permissions = threadPermissionOverrides[permissionKey(for: threadKey)] else {
return nil
return AppSandboxMode(wireValue: sandboxMode)
}
return permissions.rawSandboxPolicy?.launchOverrideMode
?? AppSandboxMode(wireValue: permissions.sandboxMode)
Expand All @@ -95,7 +95,7 @@ final class AppState {
return TurnSandboxPolicy(mode: sandboxMode)?.ffiValue
}
guard let permissions = threadPermissionOverrides[permissionKey(for: threadKey)] else {
return nil
return TurnSandboxPolicy(mode: sandboxMode)?.ffiValue
}
return permissions.rawSandboxPolicy ?? TurnSandboxPolicy(mode: permissions.sandboxMode)?.ffiValue
}
Expand Down
51 changes: 45 additions & 6 deletions apps/ios/Sources/Litter/Models/VoiceRuntimeController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ final class VoiceRuntimeController: VoiceActions {
@ObservationIgnored private var voiceOutputDecayToken: UUID?
@ObservationIgnored private var voiceStopRequestedThreadKey: ThreadKey?
@ObservationIgnored private var lastHandledVoiceEndRequestToken: String?
@ObservationIgnored private let fallbackApprovalPolicy = "never"
@ObservationIgnored private let fallbackSandboxMode = "danger-full-access"

init() {
voiceSessionCoordinator.onEvent = { [weak self] event in
Expand Down Expand Up @@ -220,6 +222,7 @@ final class VoiceRuntimeController: VoiceActions {
model: String? = nil
) async throws {
let appModel = requireAppModel()
let launchPolicy = currentLaunchPolicy()
syncHandoffServers()
await cleanupKnownRealtimeVoiceSessions(beforeStartingOn: key)

Expand All @@ -229,8 +232,8 @@ final class VoiceRuntimeController: VoiceActions {
serverId: key.serverId,
params: AppThreadLaunchConfig(
model: model,
approvalPolicy: nil,
sandbox: nil,
approvalPolicy: launchPolicy.approvalPolicy,
sandbox: launchPolicy.sandboxMode,
developerInstructions: nil,
persistExtendedHistory: true
).threadResumeRequest(threadId: key.threadId, cwdOverride: nil)
Expand Down Expand Up @@ -451,13 +454,14 @@ final class VoiceRuntimeController: VoiceActions {

private func executeHandoffStartThread(handoffId: String, serverId: String, cwd: String) async {
guard let appModel else { return }
let launchPolicy = currentLaunchPolicy()
do {
let key = try await appModel.client.startThread(
serverId: serverId,
params: AppThreadLaunchConfig(
model: handoffModel,
approvalPolicy: nil,
sandbox: nil,
approvalPolicy: launchPolicy.approvalPolicy,
sandbox: launchPolicy.sandboxMode,
developerInstructions: nil,
persistExtendedHistory: true
).threadStartRequest(cwd: cwd)
Expand All @@ -484,15 +488,16 @@ final class VoiceRuntimeController: VoiceActions {
fastMode: Bool
) async {
guard let appModel else { return }
let launchPolicy = currentLaunchPolicy()
let key = ThreadKey(serverId: serverId, threadId: threadId)
do {
try await appModel.startTurn(
key: key,
payload: AppComposerPayload(
text: transcript,
additionalInputs: [],
approvalPolicy: nil,
sandboxPolicy: nil,
approvalPolicy: launchPolicy.approvalPolicy,
sandboxPolicy: sandboxPolicy(from: launchPolicy.sandboxMode),
model: model,
effort: ReasoningEffort(wireValue: effort),
serviceTier: fastMode ? .fast : nil
Expand Down Expand Up @@ -815,6 +820,40 @@ final class VoiceRuntimeController: VoiceActions {
syncVoiceCallActivity()
}
}

private func currentLaunchPolicy() -> (approvalPolicy: AppAskForApproval?, sandboxMode: AppSandboxMode?) {
let defaults = UserDefaults.standard
let approvalRaw = defaults
.string(forKey: "litter.approvalPolicy")?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
let sandboxRaw = defaults
.string(forKey: "litter.sandboxMode")?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()

let approval = AppAskForApproval(wireValue: approvalRaw?.isEmpty == false ? approvalRaw : fallbackApprovalPolicy)
let sandbox = AppSandboxMode(wireValue: sandboxRaw?.isEmpty == false ? sandboxRaw : fallbackSandboxMode)
return (approval, sandbox)
}

private func sandboxPolicy(from mode: AppSandboxMode?) -> AppSandboxPolicy? {
guard let mode else { return nil }
switch mode {
case .dangerFullAccess:
return .dangerFullAccess
case .readOnly:
return .readOnly(access: .fullAccess, networkAccess: false)
case .workspaceWrite:
return .workspaceWrite(
writableRoots: [],
readOnlyAccess: .fullAccess,
networkAccess: false,
excludeTmpdirEnvVar: false,
excludeSlashTmp: false
)
}
}
}

private extension VoiceRuntimeController {
Expand Down
4 changes: 2 additions & 2 deletions apps/ios/Sources/Litter/Views/DirectoryPickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ private final class DirectoryPickerSheetModel {
disableTimeout: false,
timeoutMs: nil,
cwd: path,
sandboxPolicy: nil
sandboxPolicy: .dangerFullAccess
)
)
guard serverId == lastLoadedServerId else { return }
Expand Down Expand Up @@ -356,7 +356,7 @@ private final class DirectoryPickerSheetModel {
disableTimeout: false,
timeoutMs: nil,
cwd: "/tmp",
sandboxPolicy: nil
sandboxPolicy: .dangerFullAccess
)
)
if response.exitCode == 0 {
Expand Down
4 changes: 2 additions & 2 deletions apps/ios/Sources/Litter/Views/SubagentCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,8 @@ private struct SubagentDetailSheet: View {
key: threadKey,
launchConfig: AppThreadLaunchConfig(
model: nil,
approvalPolicy: nil,
sandbox: nil,
approvalPolicy: .never,
sandbox: .dangerFullAccess,
developerInstructions: nil,
persistExtendedHistory: true
),
Expand Down
Loading