blob: 42e309639b77d1f8b1ad645ddd940ae5022b64da [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::goal::{Arm, Control, Error, Goal, Meter, Report, Repository, Saved, Status, invalid};
use maka_agent_facts::event::Invocation;
use maka_agent_facts::language::Language;
use maka_agent_ports::execution::Progress;
use maka_agent_ports::usage;
use maka_capabilities::authorization::Capability;
use maka_capabilities::authorization::CommandError;
use maka_capabilities::authorization::Id;
use maka_capabilities::authorization::Target;
use maka_capabilities::background::BackgroundWork;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::{Mutex, Notify};
use tokio_util::sync::CancellationToken;
pub struct Owner {
pub executions: Arc<dyn maka_agent_ports::execution::Access>,
authorizations: Arc<dyn maka_capabilities::authorization::Access>,
preferences: Arc<dyn maka_agent_ports::preferences::Preferences>,
usage: Arc<dyn usage::Usage>,
pub repo: Repository,
gate: Mutex<()>,
pending: AtomicBool,
wake: Notify,
/// Moves on every durable Goal write, for views that follow it.
pub revisions: tokio::sync::watch::Sender<u64>,
}
impl BackgroundWork for Owner {
fn is_pending(&self) -> bool {
self.pending.load(Ordering::Acquire)
}
fn wake(&self) {
self.wake.notify_one()
}
}
impl Owner {
pub fn new(
storage: Arc<dyn maka_capabilities::storage::Store>,
executions: Arc<dyn maka_agent_ports::execution::Access>,
authorizations: Arc<dyn maka_capabilities::authorization::Access>,
preferences: Arc<dyn maka_agent_ports::preferences::Preferences>,
usage: Arc<dyn usage::Usage>,
) -> Arc<Self> {
Arc::new(Self {
repo: Repository::new(storage),
executions,
authorizations,
preferences,
usage,
gate: Mutex::new(()),
pending: AtomicBool::new(true),
wake: Notify::new(),
revisions: tokio::sync::watch::channel(0).0,
})
}
/// A write that views following Goals should see.
fn written(&self) {
self.revisions.send_modify(|revision| *revision += 1);
}
pub fn changed(&self) {
self.pending.store(true, Ordering::Release);
self.wake.notify_one();
}
pub async fn privacy(&self) -> Result<(), Error> {
if self
.preferences
.read()
.await
.map_err(invalid)?
.privacy
.incognito_active
{
Err(invalid("Goal continuation is disabled in incognito mode"))
} else {
Ok(())
}
}
pub async fn grant(&self, id: Id, session: &str) -> Result<(), Error> {
let owned = self.authorizations.open(id).await?;
let valid = owned.grant.request.target
== Target::Session {
session_id: session.into(),
}
&& [Capability::Executions, Capability::ReadUsage]
.iter()
.all(|c| owned.grant.request.capabilities.contains(c));
owned.call.finish().await?;
if valid {
Ok(())
} else {
Err(invalid(
"Approve execution and usage access for this Session in Goal settings",
))
}
}
pub async fn meter(&self, id: Id, session: &str) -> Result<Meter, Error> {
let owned = self.authorizations.open(id).await?;
let result = async {
let page = self
.usage
.activity(
owned.call.scope(),
usage::Read::Start {
filter: usage::Filter {
from: 0.0,
to: 253402300799999.0,
session_id: Some(session.into()),
activity: Default::default(),
},
},
)
.await?;
let summary = self.usage.summary(owned.call.scope(), page.cursor).await?;
Ok::<_, Error>(Meter {
known: summary
.models
.input
.known
.saturating_add(summary.models.output.known),
missing: summary
.models
.input
.missing
.saturating_add(summary.models.output.missing),
})
}
.await;
owned.call.finish().await?;
result
}
pub async fn arm(&self, session: &str, arm: Arm) -> Result<Goal, Error> {
self.privacy().await?;
self.grant(arm.grant, session).await?;
// A retry is a read of the original durable intent, even while it is running.
if let Some(saved) = self.repo.read(session).await?
&& saved.goal.id == arm.operation_id
{
let _gate = self.gate.lock().await;
return self.repo.arm(session, arm, saved.goal.baseline).await;
}
let commands = self.executions.restore(arm.grant).await?;
if commands.activity(session.into()).await?.busy {
return Err(invalid(
"Wait for the current Session execution to settle before creating a Goal",
));
}
let baseline = self.meter(arm.grant, session).await?;
let _gate = self.gate.lock().await;
let goal = self.repo.arm(session, arm, baseline).await?;
self.written();
self.changed();
Ok(goal)
}
pub async fn control(
&self,
session: &str,
id: uuid::Uuid,
revision: u64,
action: Control,
grant: Option<Id>,
) -> Result<Goal, Error> {
let mut saved = self
.repo
.read(session)
.await?
.ok_or_else(|| invalid("No Goal"))?;
if saved.revision != revision || saved.goal.id != id {
return Err(invalid("Goal changed; refresh before applying control"));
}
if let Some(grant) = grant {
if !matches!(action, Control::Resume | Control::Cancel) {
return Err(invalid(
"A renewed grant is only accepted with resume or cancel",
));
}
self.grant(grant, session).await?;
saved.goal.arm.grant = grant;
saved.goal.authority_blocked = false;
}
if action == Control::Resume {
self.privacy().await?;
self.grant(saved.goal.arm.grant, session).await?;
let commands = self.executions.restore(saved.goal.arm.grant).await?;
if commands
.activity(session.into())
.await?
.execution
.is_some_and(|e| matches!(e.progress, Progress::Paused))
{
return Err(invalid(
"Resume the paused Host execution through Session controls before resuming the Goal",
));
}
saved.goal.authority_blocked = false;
}
saved.goal.control(action)?;
let goal = saved.goal.clone();
let _gate = self.gate.lock().await;
self.repo.save(saved).await?;
self.written();
self.changed();
Ok(goal)
}
pub async fn report(&self, invocation: &Invocation, report: Report) -> Result<Goal, Error> {
if report.note.trim().is_empty() || report.note.len() > 4096 {
return Err(invalid("Provide a checkpoint or evidence of 1–4096 bytes"));
}
let mut saved = self
.repo
.read(&invocation.session_id)
.await?
.ok_or_else(|| invalid("No Goal"))?;
if saved.goal.status != Status::Active {
return Err(invalid("Goal is not active"));
}
let pending = saved
.goal
.pending
.as_ref()
.ok_or_else(|| invalid("No Goal-owned execution"))?;
let commands = self.executions.restore(saved.goal.arm.grant).await?;
let observation = commands.query(pending.request.operation_id.clone()).await?;
if observation.receipt.invocation != *invocation {
return Err(invalid("This execution does not belong to the Goal"));
}
saved.goal.report = Some(report);
let goal = saved.goal.clone();
let _gate = self.gate.lock().await;
self.repo.save(saved).await?;
self.written();
self.changed();
Ok(goal)
}
pub async fn run(self: Arc<Self>, stop: CancellationToken) -> Result<(), String> {
let mut sweep = 0_u64;
loop {
if stop.is_cancelled() {
return Ok(());
}
let sessions = {
let _gate = self.gate.lock().await;
let sessions = self.repo.sessions().await;
if let Ok(sessions) = &sessions {
self.pending.store(
sessions.iter().any(|saved| saved.goal.keeps_host_awake()),
Ordering::Release,
);
}
sessions
};
let sessions = match sessions {
Ok(sessions) => sessions,
Err(Error::Storage(maka_capabilities::storage::StoreError::Retired)) => {
return Ok(());
}
Err(_) => {
// A transient storage failure must not silently kill the owner.
self.pending.store(true, Ordering::Release);
tokio::select! {
_ = stop.cancelled() => return Ok(()),
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {}
}
continue;
}
};
for mut saved in sessions {
if saved.goal.status == Status::CancellationUnknown && !sweep.is_multiple_of(60) {
continue;
}
if stop.is_cancelled() {
return Ok(());
}
if let Err(error) = self.tick(saved.clone()).await {
// No new operation identity on transient or uncertain outcomes.
if matches!(
error,
Error::Authority(
CommandError::Denied
| CommandError::Revoked
| CommandError::Invalid(_)
| CommandError::Conflict
)
) {
// Failure belongs to the observed revision. A concurrent control,
// renewed grant or replacement Goal must not inherit it.
saved.goal.authority_blocked = true;
if saved.goal.status == Status::Active {
saved.goal.status = Status::Blocked;
}
saved.goal.note = error.to_string();
let _ = self.repo.save(saved).await;
self.written();
}
}
}
sweep = sweep.wrapping_add(1);
tokio::select! {
_ = stop.cancelled() => return Ok(()),
_ = self.wake.notified() => {},
_ = tokio::time::sleep(std::time::Duration::from_millis(500)) => {}
}
}
}
async fn tick(&self, mut saved: Saved) -> Result<(), Error> {
let session_id = saved.goal.session_id.clone();
let session = session_id.as_str();
let goal = &mut saved.goal;
if goal.status == Status::Cancelled && goal.pending.as_ref().is_some_and(|p| !p.dispatched)
{
goal.pending = None;
self.repo.save(saved).await?;
self.written();
return Ok(());
}
let commands = self.executions.restore(goal.arm.grant).await?;
if let Some(pending) = goal.pending.clone() {
let observation = match commands.query(pending.request.operation_id.clone()).await {
Ok(o) => o,
Err(CommandError::NotFound) if goal.status == Status::Active => {
if self.privacy().await.is_err() {
goal.status = Status::Paused;
goal.note = (match goal.arm.language {
Language::En => "Incognito mode stopped Goal continuation",
Language::ZhCn => "隐私模式已停止目标继续执行",
Language::ZhTw => "隱私模式已停止目標繼續執行",
})
.into();
self.repo.save(saved).await?;
self.written();
return Ok(());
}
// Commit the dispatch boundary under the same short gate as controls.
// Never hold this gate while awaiting Host execution admission.
{
let _gate = self.gate.lock().await;
let mut current = self
.repo
.read(session)
.await?
.ok_or_else(|| invalid("Goal disappeared"))?;
if current.goal.id != goal.id || current.goal.status != Status::Active {
return Ok(());
}
let Some(next) = current.goal.pending.as_mut() else {
return Ok(());
};
if next.request != pending.request {
return Ok(());
}
next.dispatched = true;
self.repo.save(current).await?;
self.written();
}
let result = commands.submit(pending.request.clone()).await;
if matches!(
&result,
Err(CommandError::Busy
| CommandError::Draining
| CommandError::Denied
| CommandError::Revoked
| CommandError::Invalid(_)
| CommandError::Conflict)
) {
let _gate = self.gate.lock().await;
if let Some(mut current) = self.repo.read(session).await?
&& let Some(next) = current.goal.pending.as_mut()
&& next.request == pending.request
{
next.dispatched = false;
self.repo.save(current).await?;
self.written();
}
}
result?;
// Cancellation can race the dispatch commitment. Settle it promptly
// after admission rather than waiting for the next owner sweep.
if let Some(current) = self.repo.read(session).await?
&& current.goal.id == goal.id
&& matches!(
current.goal.status,
Status::Cancelled | Status::CancellationUnknown
)
{
commands
.cancel(pending.request.operation_id.clone())
.await?;
}
return Ok(());
}
Err(CommandError::NotFound) => {
if goal.status == Status::Cancelled {
if !pending.dispatched {
goal.pending = None;
} else {
goal.status = Status::CancellationUnknown;
goal.note = (match goal.arm.language {
Language::En => "Cancellation acceptance is unknown. Only the original receipt will be checked; no request is resubmitted. This does not keep Host awake.",
Language::ZhCn => "取消是否被接纳未知。只检查原始回执,不重新提交请求。此状态不会使 Host 保持唤醒。",
Language::ZhTw => "取消是否被接納未知。只檢查原始回執,不重新提交請求。此狀態不會使 Host 保持喚醒。",
})
.into();
}
self.repo.save(saved).await?;
self.written();
}
return Ok(());
}
Err(e) => return Err(e.into()),
};
if matches!(goal.status, Status::Cancelled | Status::CancellationUnknown)
&& !matches!(
observation.progress,
Progress::Ended { .. } | Progress::Paused
)
{
commands.cancel(pending.request.operation_id).await?;
return Ok(());
}
match observation.progress {
Progress::Ended { outcome } => {
let usage = self
.meter(goal.arm.grant, session)
.await
.map_err(|e| e.to_string());
goal.settled(outcome, usage);
self.repo.save(saved).await?;
self.written();
self.changed();
return Ok(());
}
Progress::Paused => {
goal.handoff_paused();
self.repo.save(saved).await?;
self.written();
self.changed();
return Ok(());
}
Progress::WaitingForUser => return Ok(()),
_ => return Ok(()),
}
}
if goal.status != Status::Active {
return Ok(());
}
if self.privacy().await.is_err() {
goal.status = Status::Paused;
goal.note = (match goal.arm.language {
Language::En => "Incognito mode stopped Goal continuation",
Language::ZhCn => "隐私模式已停止目标继续执行",
Language::ZhTw => "隱私模式已停止目標繼續執行",
})
.into();
self.repo.save(saved).await?;
self.written();
return Ok(());
}
if commands.activity(session.into()).await?.busy {
return Ok(());
}
goal.meter(self.meter(goal.arm.grant, session).await?);
if goal.status == Status::Active {
goal.reserve()?;
}
self.repo.save(saved).await?;
self.written();
self.changed();
Ok(())
}
}