1use std::{collections::BTreeSet, sync::Arc};
3
4use bamboo_agent_core::{storage::Storage, AgentError, Session};
5use bamboo_domain::{
6 canonical_json_bytes, has_scoped_loop_consumption, scoped_loop_consumptions,
7 scoped_loop_control_input, scoped_loop_objective_origins, scoped_loop_operation_fingerprint,
8 LoopControlAction, RuntimeSessionPersistence, ScopedLoopControlBinding, ScopedLoopControlClaim,
9 ScopedLoopControlConsumption, ScopedLoopControlInput, SessionActivationPort, SessionInboxPort,
10 SupervisorReference, MAX_SCOPED_LOOP_CONSUMPTIONS, SCOPED_LOOP_CONSUMPTION_KEY,
11 SCOPED_LOOP_UNCERTAIN_KEY,
12};
13use sha2::{Digest, Sha256};
14
15use super::{
16 counted_loop, AgentLoopConfig, LoopControlConfig, LoopControlRequest, LoopState, LoopStatus,
17};
18
19#[derive(Debug, Clone)]
23pub struct SupervisorLoopControlGrant {
24 id: uuid::Uuid,
25 supervisor: SupervisorReference,
26 supervisor_tool_authority_revision: u64,
27 link_revision: u64,
28 allowed_actions: BTreeSet<LoopControlAction>,
29 max_iterations: u32,
30 max_total_rounds: u32,
31}
32
33impl SupervisorLoopControlGrant {
34 pub fn new(
35 supervisor: SupervisorReference,
36 supervisor_tool_authority_revision: u64,
37 link_revision: u64,
38 allowed_actions: impl IntoIterator<Item = LoopControlAction>,
39 max_iterations: u32,
40 max_total_rounds: u32,
41 ) -> Result<Self, AgentError> {
42 let grant = Self {
43 id: uuid::Uuid::new_v4(),
44 supervisor,
45 supervisor_tool_authority_revision,
46 link_revision,
47 allowed_actions: allowed_actions.into_iter().collect(),
48 max_iterations,
49 max_total_rounds,
50 };
51 grant.validate(
52 counted_loop::MAX_LOOP_ITERATIONS,
53 counted_loop::MAX_LOOP_ROUNDS,
54 )?;
55 Ok(grant)
56 }
57
58 pub fn grant_id(&self) -> uuid::Uuid {
59 self.id
60 }
61
62 pub(super) fn validate(&self, iterations: u32, rounds: u32) -> Result<(), AgentError> {
63 if self.supervisor.session_id != bamboo_domain::DEFAULT_SUPERVISOR_SESSION_ID
64 || self.supervisor.incarnation_id.is_nil()
65 || self.link_revision == 0
66 || self.allowed_actions.is_empty()
67 || !(1..=iterations.min(counted_loop::MAX_LOOP_ITERATIONS))
68 .contains(&self.max_iterations)
69 || !(self.max_iterations..=rounds.min(counted_loop::MAX_LOOP_ROUNDS))
70 .contains(&self.max_total_rounds)
71 {
72 return Err(super::fatal("invalid separate Supervisor Loop grant"));
73 }
74 Ok(())
75 }
76}
77
78impl LoopControlConfig {
79 pub(crate) async fn register_supervisor_control(
80 &self,
81 session: &Session,
82 storage: &Arc<dyn Storage>,
83 ) -> Result<(), AgentError> {
84 let Some(grant) = &self.supervisor_grant else {
85 return Ok(());
86 };
87 if !storage.supports_scoped_loop_transport_retirement() {
88 return Err(super::fatal(
89 "Supervisor Loop transport retirement unsupported",
90 ));
91 }
92 self.validate_current_owner(session).await?;
93 grant.validate(self.max_iterations, self.max_total_rounds)?;
94 let ownership = self
95 .owner
96 .ownership()
97 .ok_or_else(|| super::fatal("target owner missing"))?;
98 let execution = self
99 .execution
100 .get()
101 .ok_or_else(|| super::fatal("pipeline owner missing"))?;
102 if execution.cancel.is_cancelled() {
103 return Err(AgentError::Cancelled);
104 }
105 let binding = ScopedLoopControlBinding {
106 grant_id: grant.id,
107 supervisor: grant.supervisor.clone(),
108 supervisor_tool_authority_revision: grant.supervisor_tool_authority_revision,
109 target_session_id: session.id.clone(),
110 target_created_at: session.created_at,
111 target_metadata_version: session.metadata_version,
112 target_project_id: session
113 .project_id_meta()
114 .ok_or_else(|| super::fatal("target Project missing"))?
115 .parse()
116 .map_err(|_| super::fatal("target Project invalid"))?,
117 link_revision: grant.link_revision,
118 owner_run_id: ownership.run_id().into(),
119 execution_id: execution.id.clone(),
120 allowed_actions: grant.allowed_actions.clone(),
121 max_iterations: grant.max_iterations,
122 max_total_rounds: grant.max_total_rounds,
123 };
124 binding
125 .validate()
126 .map_err(|_| super::fatal("invalid live Supervisor binding"))?;
127 ownership
128 .router()
129 .bind_supervisor_loop_control(
130 &ownership,
131 binding.clone(),
132 execution.cancel.clone(),
133 self.stop.clone(),
134 )
135 .await
136 .map_err(|_| super::fatal("Supervisor Loop owner binding failed"))?;
137 self.live_supervisor_binding
138 .set(binding)
139 .map_err(|_| super::fatal("Supervisor Loop binding already consumed"))?;
140 Ok(())
141 }
142}
143
144fn runtime_authority(
145 policy: &LoopControlConfig,
146) -> Result<Arc<dyn SessionActivationPort>, AgentError> {
147 let owner = policy
148 .owner
149 .ownership()
150 .ok_or_else(|| super::fatal("target owner missing"))?;
151 Ok(owner.router().clone())
152}
153
154fn cancellation_wins(policy: &LoopControlConfig, error: AgentError) -> AgentError {
155 if policy
156 .execution_cancel()
157 .is_ok_and(|token| token.is_cancelled())
158 {
159 AgentError::Cancelled
160 } else {
161 error
162 }
163}
164
165async fn checkpoint_scoped(
169 session: &mut Session,
170 config: &AgentLoopConfig,
171 bindings: &[ScopedLoopControlBinding],
172 input: Option<(Arc<dyn SessionInboxPort>, ScopedLoopControlClaim)>,
173) -> Result<(), AgentError> {
174 let policy = config
175 .loop_control
176 .as_ref()
177 .ok_or_else(|| super::fatal("remote checkpoint owner absent"))?;
178 let persistence = config
179 .persistence
180 .as_ref()
181 .ok_or_else(|| super::fatal("durable checkpoint required"))?;
182 if session.metadata.contains_key(SCOPED_LOOP_UNCERTAIN_KEY) {
183 return Err(super::fatal(
184 "prior Supervisor Loop checkpoint remains unknown",
185 ));
186 }
187 let id = session.id.clone();
188 let birth = session.created_at;
189 let result = persistence
190 .checkpoint_scoped_loop_control(session, bindings, runtime_authority(policy)?, input)
191 .await;
192 if let Err(error) = result {
193 match persistence.load_loop_control_checkpoint(&id).await {
194 Ok(Some(canonical)) if canonical.id == id && canonical.created_at == birth => {
195 *session = canonical;
196 session.metadata.remove(SCOPED_LOOP_UNCERTAIN_KEY);
197 }
198 _ => {
199 session.metadata.insert(
200 SCOPED_LOOP_UNCERTAIN_KEY.into(),
201 "unconfirmed checkpoint; strict reload failed".into(),
202 );
203 }
204 }
205 if policy.execution_cancel()?.is_cancelled() {
206 return Err(AgentError::Cancelled);
207 }
208 return Err(AgentError::Budget(format!(
209 "scoped Loop checkpoint failed/unknown; no further admission: {error}"
210 )));
211 }
212 Ok(())
213}
214
215pub(crate) async fn persist_current_checkpoint(
219 session: &mut Session,
220 config: &AgentLoopConfig,
221) -> Result<(), AgentError> {
222 if session.metadata.contains_key(SCOPED_LOOP_UNCERTAIN_KEY) {
223 return Err(super::fatal(
224 "unknown scoped checkpoint forbids snapshot publication",
225 ));
226 }
227 let bindings = scoped_loop_objective_origins(session)
228 .map_err(|_| super::fatal("invalid remote objective origins"))?;
229 let running = counted_loop::read_loop_state(session)?
230 .is_some_and(|state| matches!(state.status, LoopStatus::Running | LoopStatus::Finalizing));
231 if bindings.is_empty() || !running {
232 counted_loop::persist_checkpoint(session, config.persistence.as_deref()).await
233 } else {
234 checkpoint_scoped(session, config, &bindings, None).await?;
235 if config
236 .loop_control
237 .as_ref()
238 .ok_or_else(|| super::fatal("target owner absent"))?
239 .execution_cancel()?
240 .is_cancelled()
241 {
242 return Err(AgentError::Cancelled);
243 }
244 Ok(())
245 }
246}
247
248pub(crate) async fn drain(
249 session: &mut Session,
250 config: &AgentLoopConfig,
251 rounds_spent: u32,
252) -> Result<(), AgentError> {
253 let Some(policy) = config.loop_control.as_ref() else {
254 return Ok(());
255 };
256 let Some(binding) = policy.live_supervisor_binding.get() else {
257 return Ok(());
258 };
259 let inbox = config
260 .session_inbox
261 .as_ref()
262 .ok_or_else(|| super::fatal("Supervisor Loop Inbox absent"))?;
263 let persistence = config
264 .persistence
265 .as_ref()
266 .ok_or_else(|| super::fatal("Supervisor Loop persistence absent"))?;
267 for _ in 0..MAX_SCOPED_LOOP_CONSUMPTIONS {
268 if policy.execution_cancel()?.is_cancelled() {
269 return Err(AgentError::Cancelled);
270 }
271 let generation_stopped = counted_loop::read_loop_state(session)?.is_some_and(|state| {
276 state.owner_execution_id == binding.execution_id
277 && state
278 .control
279 .as_ref()
280 .is_some_and(|control| control.stop_requested)
281 });
282 if policy.stop.is_cancelled() || generation_stopped {
283 return Ok(());
284 }
285 policy
286 .validate_current_owner(session)
287 .await
288 .map_err(|error| cancellation_wins(policy, error))?;
289 let claims = persistence
290 .claim_scoped_loop_controls(session, inbox.clone(), &binding.owner_run_id, 1)
291 .await
292 .map_err(|_| {
293 cancellation_wins(policy, super::fatal("typed Supervisor Loop claim failed"))
294 })?;
295 if policy.execution_cancel()?.is_cancelled() {
296 return Err(AgentError::Cancelled);
297 }
298 if policy.stop.is_cancelled() {
299 return Ok(());
300 }
301 let Some(claim) = claims.into_iter().next() else {
302 return Ok(());
303 };
304 apply_input(
305 session,
306 config,
307 policy,
308 binding,
309 inbox.clone(),
310 claim,
311 rounds_spent,
312 )
313 .await
314 .map_err(|error| cancellation_wins(policy, error))?;
315 }
316 Ok(())
318}
319
320#[allow(clippy::too_many_arguments)]
321async fn apply_input(
322 session: &mut Session,
323 config: &AgentLoopConfig,
324 policy: &LoopControlConfig,
325 binding: &ScopedLoopControlBinding,
326 inbox: Arc<dyn SessionInboxPort>,
327 claim: ScopedLoopControlClaim,
328 rounds_spent: u32,
329) -> Result<(), AgentError> {
330 let envelope = claim.envelope().clone();
331 let input = scoped_loop_control_input(&envelope)
332 .map_err(|_| super::fatal("invalid typed Loop input"))?
333 .ok_or_else(|| super::fatal("Loop claim did not contain typed control"))?;
334 if &input.binding != binding {
335 return Err(super::fatal("remote grant/owner changed"));
336 }
337 let storage = config
338 .storage
339 .as_ref()
340 .ok_or_else(|| super::fatal("canonical Loop authority absent"))?;
341 let authority = storage
342 .load_scoped_loop_session(binding, runtime_authority(policy)?)
343 .await;
344 if policy.execution_cancel()?.is_cancelled() {
345 return Err(AgentError::Cancelled);
346 }
347 if policy.stop.is_cancelled() {
350 return Ok(());
351 }
352 authority
353 .map_err(|_| super::fatal("Supervisor Loop authority revoked before application"))?
354 .ok_or_else(|| super::fatal("Supervisor Loop target disappeared"))?;
355 policy
356 .validate_current_owner(session)
357 .await
358 .map_err(|error| cancellation_wins(policy, error))?;
359 let mut records = scoped_loop_consumptions(session)
360 .map_err(|_| super::fatal("malformed Loop consumption proof"))?;
361 records.retain(|record| {
362 record.binding.owner_run_id == binding.owner_run_id
363 && record.binding.execution_id == binding.execution_id
364 });
365 let matching = has_scoped_loop_consumption(session, &envelope);
366 let mut staged = session.clone();
367 let mut accepted = None;
368 if !matching {
369 if records.len() >= MAX_SCOPED_LOOP_CONSUMPTIONS {
370 return Err(super::fatal("Supervisor Loop receipt capacity exhausted"));
371 }
372 let proposal = propose(session, config, policy, &input, rounds_spent).await?;
373 let (receipt, rejection) = match proposal {
374 Ok(state) => {
375 let receipt = state
376 .control
377 .as_ref()
378 .and_then(|control| control.operations.last())
379 .ok_or_else(|| super::fatal("shared Loop proposal lost receipt"))?
380 .receipt
381 .clone();
382 counted_loop::stage_state(&mut staged, state.clone())?;
383 accepted = Some(state);
384 (
385 Some(
386 serde_json::to_value(receipt)
387 .map_err(|_| super::fatal("receipt encoding failed"))?,
388 ),
389 None,
390 )
391 }
392 Err(code) => (None, Some(code.to_string())),
393 };
394 records.push(ScopedLoopControlConsumption {
395 input_id: envelope.id.clone(),
396 input_sha256: hex::encode(Sha256::digest(canonical_json_bytes(
397 &envelope.idempotency_semantics(),
398 ))),
399 binding: binding.clone(),
400 operation_id: input.operation_id().into(),
401 configured: receipt.is_some(),
402 receipt,
403 rejection,
404 });
405 let encoded = serde_json::to_string(&records)
406 .map_err(|_| super::fatal("consumption encoding failed"))?;
407 if encoded.len() > 64 * 1024 {
408 return Err(super::fatal("Loop consumption checkpoint exceeds bound"));
409 }
410 staged
411 .metadata
412 .insert(SCOPED_LOOP_CONSUMPTION_KEY.into(), encoded);
413 }
414 staged
415 .session_inbox_admission_mut()
416 .record(envelope.id.clone(), claim.generation());
417 let mut bindings = scoped_loop_objective_origins(&staged)
418 .map_err(|_| super::fatal("invalid objective provenance"))?;
419 if !bindings.contains(binding) {
420 bindings.push(binding.clone());
421 }
422 if policy.execution_cancel()?.is_cancelled() {
423 return Err(AgentError::Cancelled);
424 }
425 if policy.stop.is_cancelled() {
426 return Ok(());
427 }
428 let checkpoint = checkpoint_scoped(&mut staged, config, &bindings, Some((inbox, claim))).await;
431 *session = staged;
434 if checkpoint.is_err() {
435 if !session.metadata.contains_key(SCOPED_LOOP_UNCERTAIN_KEY)
439 && has_scoped_loop_consumption(session, &envelope)
440 {
441 if let Some(state) = accepted.as_ref().filter(|state| {
442 counted_loop::read_loop_state(session)
443 .ok()
444 .flatten()
445 .is_some_and(|actual| actual.generation == state.generation)
446 }) {
447 let _ = super::install_command_effects(policy, state, &input.command);
448 }
449 }
450 return checkpoint;
451 }
452 if let Some(state) = accepted {
453 super::install_command_effects(policy, &state, &input.command).map_err(super::fatal)?;
454 }
455 if policy.execution_cancel()?.is_cancelled() {
456 return Err(AgentError::Cancelled);
457 }
458 Ok(())
459}
460
461async fn propose(
462 session: &Session,
463 config: &AgentLoopConfig,
464 policy: &LoopControlConfig,
465 input: &ScopedLoopControlInput,
466 rounds_spent: u32,
467) -> Result<Result<LoopState, &'static str>, AgentError> {
468 input
469 .validate()
470 .map_err(|_| super::fatal("remote command outside host grant"))?;
471 let execution = policy
472 .execution
473 .get()
474 .ok_or_else(|| super::fatal("pipeline execution absent"))?;
475 let current = counted_loop::read_loop_state(session)?;
476 let hash = scoped_loop_operation_fingerprint(&input.binding, &input.command);
477 let prior = current
478 .as_ref()
479 .and_then(|state| state.control.as_ref())
480 .and_then(|control| {
481 control
482 .operations
483 .iter()
484 .find(|operation| operation.receipt.operation_id == input.operation_id())
485 });
486 if let Some(prior) = prior {
487 if prior.origin.as_ref() != Some(&input.binding) || prior.payload_sha256 != hash {
488 return Ok(Err("operation_payload_or_origin_conflict"));
489 }
490 if policy.generation() != Some(prior.receipt.generation.as_str())
491 || current
492 .as_ref()
493 .is_none_or(|state| state.owner_execution_id != execution.id)
494 {
495 return Ok(Err("generation_not_current_activation"));
496 }
497 return Ok(Err("committed_operation_requires_existing_consumption"));
500 }
501 if policy.stop.is_cancelled() && !matches!(input.command, LoopControlRequest::Stop { .. }) {
502 return Ok(Err("host_stop_requested"));
503 }
504 if matches!(input.command, LoopControlRequest::Start { .. }) {
505 if let Some(reason) = super::suspension::start_refusal(session, config).await {
506 return Ok(Err(reason));
507 }
508 policy.validate_current_owner(session).await?;
509 if execution.cancel.is_cancelled() {
510 return Err(AgentError::Cancelled);
511 }
512 }
513 Ok(super::accept_command_at_boundary(
514 policy,
515 config,
516 current,
517 &input.command,
518 &execution.id,
519 rounds_spent,
520 hash,
521 Some(&input.binding),
522 ))
523}
524
525impl LoopControlConfig {
526 pub(crate) fn bind_transport_cleanup(
529 &self,
530 registration: &mut crate::session_activation::SessionRunRegistration,
531 storage: Arc<dyn Storage>,
532 inbox: Option<Arc<dyn SessionInboxPort>>,
533 owner: Option<bamboo_domain::storage::RootActorRuntimeWrite>,
534 ) {
535 if self.supervisor_grant.is_none() {
536 return;
537 }
538 let policy = self.clone();
539 registration.set_scoped_transport_cleanup(move || {
540 let policy = policy.clone();
541 let storage = storage.clone();
542 let inbox = inbox.clone();
543 let owner = owner.clone();
544 async move {
545 policy
546 .retire_transport(&storage, inbox, owner.as_ref())
547 .await
548 }
549 });
550 }
551
552 async fn retire_transport(
553 &self,
554 storage: &Arc<dyn Storage>,
555 inbox: Option<Arc<dyn SessionInboxPort>>,
556 owner: Option<&bamboo_domain::storage::RootActorRuntimeWrite>,
557 ) -> std::io::Result<()> {
558 if self
559 .transport_retired
560 .load(std::sync::atomic::Ordering::Acquire)
561 {
562 return Ok(());
563 }
564 let Some(binding) = self.live_supervisor_binding.get() else {
565 return Ok(());
566 };
567 let ownership = self
568 .owner
569 .ownership()
570 .ok_or_else(|| std::io::Error::other("target retirement owner missing"))?;
571 ownership.close_scoped_transport(binding).await?;
572 let inbox =
573 inbox.ok_or_else(|| std::io::Error::other("target retirement Inbox missing"))?;
574 let retained_binding = binding.clone();
575 storage
576 .retire_scoped_loop_transport(bamboo_domain::storage::ScopedLoopTransportRetirement {
577 binding,
578 inbox,
579 owner,
580 retain: Arc::new(move || {
581 ownership.retain_closed_scoped_transport(&retained_binding)
582 }),
583 })
584 .await?;
585 self.transport_retired
588 .store(true, std::sync::atomic::Ordering::Release);
589 Ok(())
590 }
591}
592
593pub(crate) async fn retire_terminal_transport(config: &AgentLoopConfig) -> Result<(), AgentError> {
594 let Some(policy) = config
595 .loop_control
596 .as_ref()
597 .filter(|policy| policy.supervisor_grant_id().is_some())
598 else {
599 return Ok(());
600 };
601 let storage = config
602 .storage
603 .as_ref()
604 .ok_or_else(|| super::fatal("target retirement storage missing"))?;
605 policy
606 .retire_transport(
607 storage,
608 config.session_inbox.clone(),
609 config
610 .persistence
611 .as_ref()
612 .and_then(|persistence| persistence.root_actor_writer())
613 .as_ref(),
614 )
615 .await
616 .map_err(|error| super::fatal(&format!("target transport retirement failed: {error}")))
617}