-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Drop poisoned shell-wrapper restore commands #6012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -356,6 +356,24 @@ nonisolated struct SurfaceResumeBindingSnapshot: Codable, Equatable, Sendable { | |
| autoResume == true | ||
| } | ||
|
|
||
| var trustedForSessionRestore: SurfaceResumeBindingSnapshot? { | ||
| isPoisonedAgentHookShellWrapperResume ? nil : self | ||
| } | ||
|
|
||
| var isPoisonedAgentHookShellWrapperResume: Bool { | ||
| guard isAgentHookBinding, | ||
| let tokens = SurfaceResumeCommandCanonicalizer.tokens(from: command) else { | ||
| return false | ||
| } | ||
| let commandStart = SurfaceResumeCommandCanonicalizer.commandStartIndexAfterCwdGuard(tokens) | ||
| guard commandStart + 1 < tokens.endIndex else { return false } | ||
| let executable = (tokens[commandStart] as NSString).lastPathComponent.lowercased() | ||
| let shells: Set<String> = ["sh", "bash", "zsh", "dash", "fish", "csh", "tcsh", "ksh"] | ||
| guard shells.contains(executable) else { return false } | ||
| let resumeWord = tokens[commandStart + 1] | ||
| return resumeWord == "resume" || resumeWord == "--resume" || resumeWord.hasPrefix("--resume=") | ||
| } | ||
|
|
||
| func shouldYieldToDetectedSurfaceResumeBinding(_ detectedBinding: SurfaceResumeBindingSnapshot) -> Bool { | ||
| detectedBinding.isProcessDetected && (isProcessDetected || isAgentHookBinding) | ||
| } | ||
|
|
@@ -679,6 +697,17 @@ enum SurfaceResumeCommandCanonicalizer { | |
| return ((rawValue as NSString).expandingTildeInPath as NSString).standardizingPath | ||
| } | ||
|
|
||
| static func commandStartIndexAfterCwdGuard(_ tokens: [String]) -> Int { | ||
| guard let first = tokens.first, | ||
| first == "{" || first == "cd" else { | ||
| return tokens.startIndex | ||
| } | ||
| guard let andIndex = tokens.firstIndex(of: "&&") else { | ||
| return tokens.startIndex | ||
| } | ||
| return tokens.index(after: andIndex) | ||
|
Comment on lines
+700
to
+708
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Handle the repo’s This helper only skips guards terminated by 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| static func shellQuoted(_ value: String) -> String { | ||
| guard !value.isEmpty else { return "''" } | ||
| let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+-=./:@%") | ||
|
|
@@ -1867,6 +1896,89 @@ struct AppSessionSnapshot: Codable, Sendable { | |
| var windows: [SessionWindowSnapshot] | ||
| } | ||
|
|
||
| enum SessionSnapshotRepairer { | ||
| struct Result { | ||
| var snapshot: AppSessionSnapshot | ||
| var didRepair: Bool | ||
| } | ||
|
|
||
| static func repair(_ snapshot: AppSessionSnapshot) -> Result { | ||
| var didRepair = false | ||
| var repaired = snapshot | ||
| repaired.windows = repaired.windows.map { window in | ||
| repair(window, didRepair: &didRepair) | ||
| } | ||
| return Result(snapshot: repaired, didRepair: didRepair) | ||
| } | ||
|
|
||
| private static func repair( | ||
| _ window: SessionWindowSnapshot, | ||
| didRepair: inout Bool | ||
| ) -> SessionWindowSnapshot { | ||
| var repaired = window | ||
| repaired.tabManager.workspaces = repaired.tabManager.workspaces.map { workspace in | ||
| repair(workspace, didRepair: &didRepair) | ||
| } | ||
| return repaired | ||
| } | ||
|
|
||
| private static func repair( | ||
| _ workspace: SessionWorkspaceSnapshot, | ||
| didRepair: inout Bool | ||
| ) -> SessionWorkspaceSnapshot { | ||
| var repaired = workspace | ||
| repaired.panels = repaired.panels.map { panel in | ||
| repair(panel, workspaceDirectory: workspace.currentDirectory, didRepair: &didRepair) | ||
| } | ||
| return repaired | ||
| } | ||
|
|
||
| private static func repair( | ||
| _ panel: SessionPanelSnapshot, | ||
| workspaceDirectory: String, | ||
| didRepair: inout Bool | ||
| ) -> SessionPanelSnapshot { | ||
| guard var terminal = panel.terminal else { return panel } | ||
| let fallbackWorkingDirectory = firstNormalizedDirectory( | ||
| terminal.workingDirectory, | ||
| panel.directory, | ||
| workspaceDirectory | ||
| ) | ||
|
|
||
| if let resumeBinding = terminal.resumeBinding { | ||
| let trustedBinding = resumeBinding.trustedForSessionRestore | ||
| if trustedBinding == nil { | ||
| didRepair = true | ||
| } | ||
| terminal.resumeBinding = trustedBinding | ||
| } | ||
|
|
||
| if let agent = terminal.agent { | ||
| let repairedAgent = agent.repairedForSessionRestore( | ||
| fallbackWorkingDirectory: fallbackWorkingDirectory | ||
| ) | ||
| if agent.launchCommand != repairedAgent.launchCommand | ||
| || agent.workingDirectory != repairedAgent.workingDirectory { | ||
| didRepair = true | ||
| } | ||
| terminal.agent = repairedAgent | ||
| } | ||
|
|
||
| var repaired = panel | ||
| repaired.terminal = terminal | ||
| return repaired | ||
| } | ||
|
|
||
| private static func firstNormalizedDirectory(_ candidates: String?...) -> String? { | ||
| for candidate in candidates { | ||
| if let normalized = SurfaceResumeCommandCanonicalizer.normalizedCWD(candidate) { | ||
| return normalized | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| enum SessionPersistenceStore { | ||
| enum SnapshotLoadOutcome { | ||
| case loaded(AppSessionSnapshot) | ||
|
|
@@ -1885,7 +1997,11 @@ enum SessionPersistenceStore { | |
| guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return .unusable } | ||
| guard snapshot.version == SessionSnapshotSchema.currentVersion else { return .unusable } | ||
| guard !snapshot.windows.isEmpty else { return .unusable } | ||
| return .loaded(snapshot) | ||
| let repairResult = SessionSnapshotRepairer.repair(snapshot) | ||
| if repairResult.didRepair { | ||
| _ = save(repairResult.snapshot, fileURL: fileURL) | ||
| } | ||
| return .loaded(repairResult.snapshot) | ||
| } | ||
|
|
||
| static func load(fileURL: URL? = nil) -> AppSessionSnapshot? { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Preserve
.ignoreby keeping the repaired snapshot cwd nil.When
trustedLaunchCommandForSessionRestoreis nil, this repair path backfillsworkingDirectoryfromfallbackWorkingDirectorywithout checkingregistration?.cwd == .ignore. That reintroduces a saved cwd into snapshots that are supposed to restore from the caller’s current directory, and downstream restore code in this file explicitly relies onworkingDirectorybeing nil for.ignoreagents.Possible fix
let trustedLaunchCommand = trustedLaunchCommandForSessionRestore var repaired = self repaired.launchCommand = trustedLaunchCommand + + if registration?.cwd == .ignore { + repaired.workingDirectory = nil + return repaired + } let fallbackWorkingDirectory = Self.normalizedWorkingDirectory(fallbackWorkingDirectory)Based on learnings: “Registrations with cwd: .ignore set resumeWorkingDirectory to nil, suppressing both the cwd guard in the resume command and the terminal working directory at placement time.”
🤖 Prompt for AI Agents
Source: Learnings