mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 01:57:31 +01:00
fix(runner): classify incompatible saved provider profiles (#19334)
Classify deterministic OpenCode and Cursor Cloud saved-profile mismatches with the existing permanent provider-state class. Preserve saved state and stop same-run retries after the runner refuses that state. Verified 37 native-provider backend tests, durable classified-result replay, the server policy test, rustfmt, workspace typecheck and build. Complete hosted CI passed with Greptile 5/5 and no unresolved threads. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ffd9591ab4
commit
be5709a25c
3 files changed
+19
-3
No files matched your search
@@ -99,6 +99,13 @@ after a failed resume, it opens a fresh provider session as before. Otherwise th
|
||||
run fails with `native_provider_state_incompatible` and is not retried. A board
|
||||
Retry then continues the task in a fresh provider session in the same workspace.
|
||||
|
||||
Saved OpenCode and Cursor Cloud provider state binds the digest of its own
|
||||
qualified launch profile (command, proxy script and, for OpenCode, the
|
||||
executable, each by path and SHA-256). These providers do not re-bind. A changed
|
||||
profile fails restore with the same `provider_state_incompatible`
|
||||
classification and follows the same disposition, so the controller does not
|
||||
retry the restore or try to reattach a runner that already rejected it.
|
||||
|
||||
## Profile identity
|
||||
|
||||
Every ACPX provider's `commandDigest` attests only its own upstream release.
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::durable::{
|
||||
validate_semantic_tool_input, verify_private_directory, Command, CommandExecution,
|
||||
CommandExecutor, CursorCloudLaunchProfile, DurableRunnerConfig, DurableRunnerError,
|
||||
EventPriority, OpenCodeLaunchProfile, PolledEvent, TerminalDeliveryReconciliation,
|
||||
PROVIDER_STATE_INCOMPATIBLE,
|
||||
};
|
||||
use crate::provider_bridge::{
|
||||
authorized_tool_catalog_digest, semantic_value_digest, AuthorizedToolSet, DurableReplayFilter,
|
||||
@@ -2272,14 +2273,16 @@ impl CodexCommandExecutor {
|
||||
if state.opencode_launch_profile_digest != expected_launch_profile_digest {
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"OpenCode runner launch profile changed across durable recovery",
|
||||
));
|
||||
)
|
||||
.with_classification(PROVIDER_STATE_INCOMPATIBLE));
|
||||
}
|
||||
let expected_cursor_cloud_profile_digest =
|
||||
self.bind_cursor_cloud_launch_profile(&state.config)?;
|
||||
if state.cursor_cloud_launch_profile_digest != expected_cursor_cloud_profile_digest {
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"Cursor Cloud runner launch profile changed across durable recovery",
|
||||
));
|
||||
)
|
||||
.with_classification(PROVIDER_STATE_INCOMPATIBLE));
|
||||
}
|
||||
self.state = Some(state);
|
||||
Ok(())
|
||||
|
||||
+7
-1
@@ -7,7 +7,7 @@ use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use paperclip_runner_core::durable::{
|
||||
AcpxLaunchProfile, Command, CommandExecutor, CursorCloudLaunchProfile, DurableRunnerConfig,
|
||||
OpenCodeLaunchProfile, QualifiedLaunchArtifact,
|
||||
OpenCodeLaunchProfile, QualifiedLaunchArtifact, PROVIDER_STATE_INCOMPATIBLE,
|
||||
};
|
||||
use paperclip_runner_core::native_provider_backend::NativeProviderCommandExecutor;
|
||||
use paperclip_runner_core::provider_bridge::{authorized_tool_catalog_digest, AuthorizedTool};
|
||||
@@ -2220,6 +2220,7 @@ fn cursor_cloud_durable_recovery_rejects_a_changed_proxy_profile_without_rewriti
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Cursor Cloud runner launch profile changed across durable recovery"));
|
||||
assert_eq!(error.classification(), Some(PROVIDER_STATE_INCOMPATIBLE));
|
||||
}
|
||||
assert_eq!(fs::read(&state_path).unwrap(), saved);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
@@ -2311,6 +2312,7 @@ fn rejects_opencode_launch_profile_drift_across_fresh_recovery() {
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("launch profile changed across durable recovery"));
|
||||
assert_eq!(error.classification(), Some(PROVIDER_STATE_INCOMPATIBLE));
|
||||
let second_error = recovered
|
||||
.execute(&command(
|
||||
3,
|
||||
@@ -2321,6 +2323,10 @@ fn rejects_opencode_launch_profile_drift_across_fresh_recovery() {
|
||||
assert!(second_error
|
||||
.to_string()
|
||||
.contains("launch profile changed across durable recovery"));
|
||||
assert_eq!(
|
||||
second_error.classification(),
|
||||
Some(PROVIDER_STATE_INCOMPATIBLE)
|
||||
);
|
||||
assert_eq!(fs::read(&state_path).unwrap(), state_before_recovery);
|
||||
|
||||
let shutdown_error = recovered
|
||||
|
||||
Reference in new issue
Block a user