blob: 3d54fd39f27437bda66e918483d42519298aabeb [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::Error;
use crate::name;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::input::MessageInput;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
mod attachment;
pub use attachment::CopyAttachment;
mod interaction;
mod message;
pub use message::{
AnswerCursor, Enqueue, Excerpt, InteractionKind, MessageObservation, MessageReceipt,
MessageResult, MessageState, PendingInteraction, SessionMessage,
};
mod removal;
mod root;
pub use interaction::{OfferInteraction, Prompt};
pub use removal::{RemovalReceipt, RemoveSession, RemovedSession};
pub use root::{CreateRoot, RootApproval, RootTemplate, Settings as RootSettings};
/// Business identity belongs to the package/scope namespace, never an activation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Submit {
pub operation_id: String,
pub session_id: String,
pub content: MessageInput,
/// This execution only; never changes the Session's default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub orchestration_mode: Option<maka_agent_facts::execution::BehaviorId>,
}
impl Submit {
pub fn validate(&self) -> Result<(), Error> {
name(&self.operation_id)?;
name(&self.session_id)?;
maka_agent_facts::message::validate_sources(&self.content, &[])
.map_err(|reason| Error::Invalid(reason.into()))
}
pub fn digest(&self) -> Result<String, Error> {
self.validate()?;
let bytes = serde_json::to_vec(self).map_err(|error| Error::Invalid(error.to_string()))?;
Ok(format!("sha256:{}", hex::encode(Sha256::digest(bytes))))
}
}
/// Resume one sealed physical Run. The operation identity is package/scope-local;
/// an exact retry returns the same continuation, never the Session's new tip.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Resume {
pub operation_id: String,
pub source: Invocation,
}
impl Resume {
pub fn validate(&self) -> Result<(), Error> {
for value in [
&self.operation_id,
&self.source.session_id,
&self.source.turn_id,
&self.source.run_id,
&self.source.invocation_id,
] {
name(value)?;
}
Ok(())
}
}
/// Acceptance is not completion. Outcomes remain in the Host's canonical log.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Receipt {
pub invocation: Invocation,
pub message_id: String,
pub content_digest: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateChild {
pub operation_id: String,
pub parent_session_id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox_mode: Option<maka_agent_facts::execution::SandboxMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bound_tools: Option<std::collections::BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<Target>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace: Option<ChildWorkspace>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChildWorkspace {
Inherit,
IsolatedGit,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum Target {
Model {
model: maka_agent_facts::execution::ModelBinding,
thinking_level: Option<maka_agent_facts::execution::ThinkingLevel>,
},
Executor {
executor_id: maka_agent_facts::executor::ExecutorId,
#[serde(default)]
settings: maka_agent_facts::executor::Settings,
},
}
impl Target {
pub fn validate(&self) -> Result<(), Error> {
if let Self::Model { model, .. } = self {
name(&model.connection_id)?;
name(&model.connection_slug)?;
name(&model.model)?;
}
if let Self::Executor { settings, .. } = self {
settings
.validate()
.map_err(|error| Error::Invalid(error.into()))?;
}
Ok(())
}
}
impl CreateChild {
pub fn validate(&self) -> Result<(), Error> {
name(&self.operation_id)?;
name(&self.parent_session_id)?;
if self.name.trim().is_empty()
|| self.name.len() > 256
|| self.name.chars().any(char::is_control)
{
return Err(Error::Invalid("invalid child Session name".into()));
}
if let Some(tools) = &self.bound_tools {
for tool in tools {
name(tool)?;
}
}
if let Some(target) = &self.target {
target.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChildSession {
pub session_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkspacePatch {
pub artifact_id: String,
pub session_id: String,
pub turn_id: String,
pub bytes: u64,
pub base_commit: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
pub enum Progress {
Pending,
Running,
WaitingForUser,
Paused,
Ended {
outcome: maka_agent_facts::event::InvocationOutcome,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Observation {
pub receipt: Receipt,
pub progress: Progress,
/// Exact log fence from the same snapshot as progress; use it for event pages.
pub through_sequence: u64,
/// Canonical terminal record from this same observation, absent while unfinished.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terminal_event_id: Option<String>,
/// Stable identity of the current blocking interaction set or handoff pause.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attention_id: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EventPage {
pub events: Vec<maka_agent_facts::event::StoredEvent>,
pub through_sequence: u64,
pub next_after: Option<u64>,
}
/// Registered extension names visible to this Session, narrowed by its tool ceiling.
/// An advertisement is neither a frozen model request nor permission to call it.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SessionCapabilities {
pub tools: std::collections::BTreeSet<String>,
pub executors: Vec<maka_agent_facts::executor::ExecutorId>,
}
/// The latest logical execution is an observation, not a durable business identity. Capture
/// the invocation when recording a control intent; never retarget a retry.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Activity {
pub execution: Option<CurrentExecution>,
/// Includes queued work and cleanup, not just a live model request.
pub busy: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CurrentExecution {
/// May already be terminal; it remains a safe identity for exact control.
pub invocation: Invocation,
/// Frozen behavior, not the current Session default.
pub behavior: Option<maka_agent_facts::execution::BehaviorId>,
pub progress: Progress,
}
/// A bounded read of an artifact belonging to the accepted operation's Turn.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadArtifact {
pub operation_id: String,
pub artifact_id: String,
pub offset: u64,
pub limit: usize,
}
impl ReadArtifact {
pub fn validate(&self) -> Result<(), Error> {
name(&self.operation_id)?;
name(&self.artifact_id)?;
if self.offset >= (1 << 53) || !(1..=64 * 1024).contains(&self.limit) {
return Err(Error::Invalid("invalid execution artifact window".into()));
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ArtifactChunk {
pub bytes: Vec<u8>,
pub total_bytes: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Configure {
pub session_id: String,
pub expected_revision: u64,
pub target: Target,
}
impl Configure {
pub fn validate(&self) -> Result<(), Error> {
name(&self.session_id)?;
if self.expected_revision >= 1 << 53 {
return Err(Error::Invalid("invalid Session revision".into()));
}
self.target.validate()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum Configured {
Committed {
session: Box<crate::session::View>,
},
RevisionConflict {
expected_revision: u64,
actual_revision: u64,
},
}
/// A Host-authorized, instance-bound interface. Plugins cannot supply another
/// namespace or activation with a command; Host binds both when granting it.
pub trait Access: Send + Sync {
/// Restore a Host-recorded consent reference, never plugin-supplied boundary data.
fn restore(
&self,
id: maka_capabilities::authorization::Id,
) -> futures_util::future::BoxFuture<'_, Result<std::sync::Arc<dyn Commands>, CommandError>>;
/// Borrow a real Host call's execution authority. Entry placement and a
/// caller-supplied Session identity never authorize execution on their own.
fn acquire(
&self,
call: maka_capabilities::call::Scope,
) -> futures_util::future::BoxFuture<'_, Result<std::sync::Arc<dyn Commands>, CommandError>>;
}
/// Durable decision when abandoning an owned revision draft.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RevisionDisposition {
Abandoned,
Retained,
}
/// An acquired execution capability retains its captured permission ceiling.
pub trait Commands: Send + Sync + std::any::Any {
/// Remove an authorized revision family; every directly removed member must
/// be covered. Accepted work settles independently of the caller's lifetime.
fn remove_session(
&self,
request: RemoveSession,
) -> futures_util::future::BoxFuture<'_, Result<RemovedSession, CommandError>>;
/// Read only an accepted removal. No catalog record or active execution is
/// required; current plugin identity and access are still checked.
fn removal_receipt(
&self,
session_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Option<RemovalReceipt>, CommandError>>;
/// Advisory count; removal recomputes and authorizes the plan at commit.
fn preview_removal(
&self,
session_id: String,
) -> futures_util::future::BoxFuture<'_, Result<u64, CommandError>>;
/// Copy an immutable attachment between two Host-issued capabilities. Both
/// endpoints are reauthorized; a source Session ID alone grants nothing.
fn copy_attachment(
&self,
source: std::sync::Arc<dyn Commands>,
request: CopyAttachment,
) -> futures_util::future::BoxFuture<
'_,
Result<maka_agent_facts::attachment::AttachmentRef, CommandError>,
>;
/// Immutable user input that opened this logical execution. Handoff successors
/// resolve the same root; unrelated Runs are never substituted. Non-message
/// inputs return None. This is a read, not authority to forward attachments.
fn input(
&self,
invocation: Invocation,
) -> futures_util::future::BoxFuture<'_, Result<Option<MessageInput>, CommandError>>;
fn resume(
&self,
request: Resume,
) -> futures_util::future::BoxFuture<'_, Result<Invocation, CommandError>>;
/// Replace an idle Session's model/Executor selection without changing its
/// workspace, behavior or permission ceiling. Revision mismatch is explicit.
/// Every committed choice advances the revision, including identical values.
fn configure(
&self,
input: Configure,
) -> futures_util::future::BoxFuture<'_, Result<Configured, CommandError>>;
/// Inspect an exact message in an authorized Session, including non-plugin input.
/// Absence is not proof of cancellation or delivery.
fn read_message(
&self,
message: SessionMessage,
) -> futures_util::future::BoxFuture<'_, Result<Option<MessageState>, CommandError>>;
fn enqueue(
&self,
request: Enqueue,
) -> futures_util::future::BoxFuture<'_, Result<MessageReceipt, CommandError>>;
fn message(&self, operation_id: String) -> futures_util::future::BoxFuture<'_, MessageResult>;
fn retract(&self, operation_id: String) -> futures_util::future::BoxFuture<'_, MessageResult>;
/// Exact retries reuse the canonical offer, including its outcome after the Run ended.
fn offer_interaction(
&self,
request: OfferInteraction,
) -> futures_util::future::BoxFuture<
'_,
Result<maka_agent_facts::interaction::InteractionRecord, CommandError>,
>;
/// Only this package/scope's offers, within currently authorized Sessions.
fn interaction(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<
'_,
Result<Option<maka_agent_facts::interaction::InteractionRecord>, CommandError>,
>;
/// Cancellation stops observation, never withdraws an accepted offer.
fn wait_interaction(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<
'_,
Result<maka_agent_facts::interaction::InteractionOutcome, CommandError>,
>;
/// Withdraw an offer without replacing an answer or other canonical terminal outcome.
fn close_interaction(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<
'_,
Result<maka_agent_facts::interaction::InteractionRecord, CommandError>,
>;
/// Current configuration of an authorized Session; not a grant or an
/// unrestricted catalog. Returns no plugin-owned domain state.
fn session(
&self,
session_id: String,
) -> futures_util::future::BoxFuture<'_, Result<crate::session::View, CommandError>>;
fn capabilities(
&self,
session_id: String,
) -> futures_util::future::BoxFuture<'_, Result<SessionCapabilities, CommandError>>;
fn activity(
&self,
session_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Activity, CommandError>>;
/// Stop the captured logical execution, including its handoff successors.
/// Completion confirms the stop request, not that all cleanup has finished.
/// Repeating it never stops a later Turn in the same Session.
fn stop(
&self,
invocation: Invocation,
) -> futures_util::future::BoxFuture<'_, Result<(), CommandError>>;
/// Check current instance and Session boundaries without submitting work.
fn validate_authority(&self) -> futures_util::future::BoxFuture<'_, Result<(), CommandError>>;
fn create_root(
&self,
request: CreateRoot,
) -> futures_util::future::BoxFuture<'_, Result<ChildSession, CommandError>>;
/// Recover a root created by this namespace without replaying its creation settings.
/// Rechecks the current workspace and source ceilings. Absence is only an
/// observation: a concurrent creation may still commit with this operation ID.
fn restore_root(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Option<ChildSession>, CommandError>>;
/// Abandon an unused revision created by this namespace. Accepted work
/// retains it; repeating an abandonment returns the durable decision.
fn abandon_revision(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<'_, Result<RevisionDisposition, CommandError>>;
/// Host-captured constraints suitable for persisting with a business intent.
fn boundaries(&self) -> Result<Vec<SessionBoundary>, CommandError>;
/// Export a settled child workspace once; exact retries return its immutable Artifact.
fn workspace_patch(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Option<WorkspacePatch>, CommandError>>;
/// Missing artifacts and artifacts from another Turn are both absent.
fn artifact(
&self,
request: ReadArtifact,
) -> futures_util::future::BoxFuture<'_, Result<Option<ArtifactChunk>, CommandError>>;
fn event(
&self,
operation_id: String,
event_id: String,
through: u64,
) -> futures_util::future::BoxFuture<
'_,
Result<Option<maka_agent_facts::event::StoredEvent>, CommandError>,
>;
/// Invalidation only. The canonical query remains authoritative.
fn changes(&self) -> Result<tokio::sync::watch::Receiver<u64>, CommandError>;
fn events(
&self,
operation_id: String,
after: u64,
through: u64,
) -> futures_util::future::BoxFuture<'_, Result<EventPage, CommandError>>;
/// Recover access to an existing child using its original creation request.
/// Never creates a Session or workspace; current parent/child ceilings still apply.
fn restore_child(
&self,
request: CreateChild,
) -> futures_util::future::BoxFuture<'_, Result<Option<ChildSession>, CommandError>>;
fn create_child(
&self,
request: CreateChild,
) -> futures_util::future::BoxFuture<'_, Result<ChildSession, CommandError>>;
fn submit(
&self,
request: Submit,
) -> futures_util::future::BoxFuture<'_, Result<Receipt, CommandError>>;
fn query(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Observation, CommandError>>;
fn cancel(
&self,
operation_id: String,
) -> futures_util::future::BoxFuture<'_, Result<Observation, CommandError>>;
}
use maka_capabilities::authorization::{CommandError, SessionBoundary};