1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use bamboo_agent_core::tools::{ToolCall, ToolResult};
use bamboo_agent_core::{AgentEvent, Session};
use tokio::sync::mpsc;
pub(super) async fn maybe_apply_workspace_update(
session: &mut Session,
tool_call: &ToolCall,
result: &ToolResult,
session_id: &str,
project_resolver: Option<&crate::project_context::ProjectContextResolver>,
event_tx: &mpsc::Sender<AgentEvent>,
) {
if let Some(mut update) =
super::super::super::workspace_context::extract_workspace_path_from_tool_result(
tool_call, result,
)
{
if super::super::super::workspace_context::should_apply_workspace_update(session, tool_call)
{
// The server's Project-aware Workspace tool performs this exact
// ownership check against the confinement-resolved destination
// before mutating global workspace state. Trust its structured
// result so a registry race after successful invocation cannot
// leave global state changed while the live session refuses the
// same update. Implicit Write/Edit relocations still need the
// runner-side check because those tools do not own Workspace CAS.
let explicit_workspace_tool =
super::super::super::workspace_context::is_explicit_workspace_tool(tool_call);
if !explicit_workspace_tool {
if let Some(resolver) = project_resolver {
let workspace = std::path::Path::new(&update.path);
match resolver.workspace_owner(workspace).await {
Ok(owner) => {
let current = match crate::project_context::ProjectContextResolver::session_project_identity(session) {
crate::project_context::SessionProjectIdentity::Assigned(project_id) => Some(project_id),
crate::project_context::SessionProjectIdentity::Unassigned => None,
crate::project_context::SessionProjectIdentity::Invalid { raw, message } => {
let error = serde_json::json!({
"code": "invalid_project_identity",
"message": format!(
"Session carries an invalid Project identity '{raw}': {message}"
),
})
.to_string();
let _ = event_tx
.send(AgentEvent::ToolError {
tool_call_id: tool_call.id.clone(),
error,
})
.await;
return;
}
};
if owner.is_some() && owner != current {
let error = serde_json::json!({
"code": "project_workspace_conflict",
"message": "Tool result workspace belongs to another Project; session workspace was not changed",
"owner_project_id": owner,
"session_project_id": current,
})
.to_string();
tracing::warn!(
session_id,
tool = %tool_call.function.name,
%error,
"blocked implicit cross-Project workspace update"
);
let _ = event_tx
.send(AgentEvent::ToolError {
tool_call_id: tool_call.id.clone(),
error,
})
.await;
return;
}
if owner.is_some() && owner == current {
update.binding_status =
crate::project_context::WorkspaceBindingStatus::Registered;
}
}
Err(error) => {
let error_kind = match &error {
crate::project_context::ProjectContextError::Source(_) => "source",
crate::project_context::ProjectContextError::IdentityMismatch {
..
} => "identity_mismatch",
crate::project_context::ProjectContextError::WorkspaceConflict {
..
} => "workspace_conflict",
crate::project_context::ProjectContextError::UnassignedWorkspaceConflict {
..
} => "unassigned_workspace_conflict",
crate::project_context::ProjectContextError::InvalidProjectIdentity {
..
} => "invalid_project_identity",
crate::project_context::ProjectContextError::ProjectUnavailable {
..
} => "project_unavailable",
crate::project_context::ProjectContextError::ProjectPathMissing {
..
} => "project_path_missing",
crate::project_context::ProjectContextError::ProjectPathUnavailable {
..
} => "project_path_unavailable",
crate::project_context::ProjectContextError::WorkspaceInvalid {
..
} => "workspace_invalid",
};
tracing::warn!(
session_id,
tool = %tool_call.function.name,
error_kind,
"failed closed while checking implicit workspace ownership"
);
return;
}
}
}
}
super::super::super::workspace_context::apply_workspace_path_to_session(
session,
&update.path,
update.binding_status,
);
tracing::info!(
session_id,
tool = %tool_call.function.name,
workspace_changed = true,
"updated session workspace metadata"
);
}
}
}