feat: add selective Workdir symlink policies

This commit is contained in:
2026-09-14 19:09:12 +09:00
parent d2cb50d081
commit 8a3e06bc81
34 changed files with 1017 additions and 155 deletions
+38
View File
@@ -1599,12 +1599,30 @@ pub struct ScopeRule {
/// direct children. Defaults to `true`.
#[serde(default = "default_recursive")]
pub recursive: bool,
/// Which path identity an allow rule uses when symbolic links are
/// encountered. Deny rules always inspect both identities.
#[serde(default)]
pub symlink_policy: SymlinkPolicy,
}
fn default_recursive() -> bool {
true
}
/// Symbolic-link identity used by one filesystem allow rule.
///
/// `Resolved` is the least authority and the default: access is matched
/// against the provider-resolved target. `Logical` intentionally grants the
/// path as presented through the Workdir, even when it aliases another target.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum SymlinkPolicy {
#[default]
Resolved,
Logical,
}
/// Permission lattice used by [`ScopeRule`].
///
/// The derived `Ord` instance follows declaration order, so
@@ -1623,6 +1641,25 @@ pub enum Permission {
mod tests {
use super::*;
#[test]
fn scope_rule_defaults_to_resolved_symlink_policy() {
let rule: ScopeRule = serde_json::from_value(serde_json::json!({
"target": "/workspace",
"permission": "read"
}))
.unwrap();
assert!(rule.recursive);
assert_eq!(rule.symlink_policy, SymlinkPolicy::Resolved);
let logical: ScopeRule = serde_json::from_value(serde_json::json!({
"target": "/workspace",
"permission": "read",
"symlink_policy": "logical"
}))
.unwrap();
assert_eq!(logical.symlink_policy, SymlinkPolicy::Logical);
}
#[test]
fn worker_state_snapshot_apply_is_monotonic_and_detects_conflicts() {
let mut current = WorkerStateSnapshot::initial(4);
@@ -2462,6 +2499,7 @@ mod tests {
target: "/tmp/work".into(),
permission: Permission::Write,
recursive: true,
symlink_policy: Default::default(),
}],
});
let json = serde_json::to_string(&method).unwrap();
+3 -2
View File
@@ -11,8 +11,8 @@ use crate::{
PasteArtifactRef, PendingSubmissionSummary, PendingSubmissionsSnapshot, Permission,
RewindSummary, RewindTarget, RewindTargetId, RunResult, ScopeRule, Segment, SessionContentPart,
SessionEntryProvenance, SessionMessageRole, SessionSnapshot, SessionSnapshotEntry,
SessionSnapshotEntryData, SessionToolAttachment, SubmissionDisposition, ToolResultDisposition,
TurnResult, UploadedFileAvailability, UploadedFileRef, WorkerBusyState,
SessionSnapshotEntryData, SessionToolAttachment, SubmissionDisposition, SymlinkPolicy,
ToolResultDisposition, TurnResult, UploadedFileAvailability, UploadedFileRef, WorkerBusyState,
WorkerCommandAcknowledgement, WorkerCommandDisposition, WorkerCommandEnvelope,
WorkerCommandKind, WorkerEvent, WorkerMaintenanceState, WorkerRunState, WorkerState,
WorkerStateSnapshot, WorkerStatus,
@@ -64,6 +64,7 @@ pub fn generated_protocol_types() -> String {
push_decl::<ToolResultDisposition>(&cfg, &mut output);
push_decl::<ErrorCode>(&cfg, &mut output);
push_decl::<Permission>(&cfg, &mut output);
push_decl::<SymlinkPolicy>(&cfg, &mut output);
push_decl::<InFlightToolCallState>(&cfg, &mut output);
push_decl::<CommandStatus>(&cfg, &mut output);
push_decl::<CommandStream>(&cfg, &mut output);