blob: e8d09a2ca9fc2df6c1c3db977ebdcd2107c6363d [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.
*/
pub use maka_sandbox::{Approval as ApprovalPolicy, Mode as SandboxMode};
use serde::{Deserialize, Serialize};
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum CollaborationMode {
#[default]
Agent,
Plan,
}
/// A native tool surface selected by the Host, independent of plugin tool names.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolProfile {
#[serde(rename = "headless-coding-v1")]
HeadlessCodingV1,
}
mod behavior;
pub use behavior::BehaviorId;
mod tool_policy;
pub use tool_policy::{CodeLanguage, EditingTools};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingLevel {
Off,
Minimal,
Low,
Medium,
High,
Xhigh,
Max,
Ultra,
}
impl ThinkingLevel {
/// Display order; support still comes from the selected model's declaration.
pub const ALL: [Self; 8] = [
Self::Off,
Self::Minimal,
Self::Low,
Self::Medium,
Self::High,
Self::Xhigh,
Self::Max,
Self::Ultra,
];
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolMode {
#[default]
Direct,
CodeMode,
}
/// Public model identity. Credentials and provider handles never belong here.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ModelBinding {
pub connection_id: String,
pub connection_slug: String,
pub model: String,
}
/// Actual Host-composed instructions admitted with this invocation, not a
/// pointer to mutable root settings or a replacement for conversation history.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemPrompt {
pub text: String,
pub policy_revision: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sources: Vec<crate::composition::SourceRevision>,
}
impl SystemPrompt {
pub fn validate(&self) -> Result<(), &'static str> {
if self.text.trim().is_empty()
|| self.policy_revision > crate::configuration::validation::MAX_SAFE_INTEGER
{
return Err("invalid system prompt");
}
Ok(())
}
}
/// Immutable admission context, committed in the opening before dispatch.
/// Later Session mutations must not reinterpret relative paths or grants.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvocationConfiguration {
/// Frozen effective instructions for automatic and manual context compaction.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction_prompt: Option<String>,
/// Frozen file-editing policy. Older invocations retain their original unfiltered tools.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub editing_tools: Option<EditingTools>,
/// Frozen discovery policy for this invocation, including continuations.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub tool_discovery: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_composition: Option<ToolComposition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<SystemPrompt>,
pub cwd: String,
/// Captured from the workspace marker, never reconstructed from a later path.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_identity: Option<WorkspaceIdentity>,
pub workspace_origin: WorkspaceOrigin,
pub sandbox_mode: SandboxMode,
pub approval_policy: ApprovalPolicy,
/// Identifies the admitted boundary even when a later configuration returns
/// to the same modes and paths. Equality of values does not restore a grant.
pub boundary_revision: u64,
pub collaboration_mode: CollaborationMode,
pub orchestration_mode: BehaviorId,
pub tool_mode: ToolMode,
/// Frozen Code Mode interpreter. Historical invocations used JavaScript.
#[serde(default, skip_serializing_if = "CodeLanguage::is_javascript")]
pub code_language: CodeLanguage,
/// None means there was no Session-owned model binding (e.g. local Code).
pub model: Option<ModelBinding>,
pub thinking_level: Option<ThinkingLevel>,
#[serde(default, skip_serializing_if = "crate::language::Language::is_english")]
pub language: crate::language::Language,
}
/// Admission evidence for Host-owned handlers whose behavior is not in schema.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolComposition {
pub clients: crate::capability::ClientComposition,
#[serde(default)]
pub native_tools: NativeToolSet,
#[serde(default)]
pub editing_tools: EditingTools,
/// Frozen Client capabilities usable by Host services, not advertised to the model.
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
pub private_clients: std::collections::BTreeSet<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bound_tools: Option<std::collections::BTreeSet<String>>,
}
/// Host-owned handlers retain their access boundary across recovery. A plugin
/// may narrow the baseline, but cannot replace a core handler with its own.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NativeToolSet {
#[default]
Workspace,
Attachments,
}
/// User-selected workspace locator, before Host path resolution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum WorkspaceTarget {
Project { project_id: String },
HostPath { path: String },
}
/// Host-resolved location; a locator alone never authorizes filesystem access.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkspaceProjection {
pub target: WorkspaceTarget,
pub host_cwd: String,
}
/// Host-issued provenance recorded with execution authority, never inferred from
/// the display projection's path or supplied by a workspace locator.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceOrigin {
Selected,
Allocated,
}
/// Intrinsic workspace identity is distinct from its current filesystem location.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct WorkspaceIdentity(String);
/// Filesystem-object observation for directory consent, not an execution workspace marker.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct DirectoryIdentity(String);
impl TryFrom<String> for DirectoryIdentity {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.strip_prefix("sha256:").is_none_or(|hex| {
hex.len() != 64
|| !hex
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}) {
return Err("invalid directory identity");
}
Ok(Self(value))
}
}
impl From<DirectoryIdentity> for String {
fn from(identity: DirectoryIdentity) -> Self {
identity.0
}
}
impl WorkspaceIdentity {
pub fn from_marker_id(id: &str) -> Result<Self, &'static str> {
let uuid = uuid::Uuid::parse_str(id).map_err(|_| "invalid workspace UUID")?;
if id.len() != 36
|| !uuid.hyphenated().to_string().eq_ignore_ascii_case(id)
|| !(1..=8).contains(&uuid.get_version_num())
|| uuid.get_variant() != uuid::Variant::RFC4122
{
return Err("invalid workspace UUID");
}
// Preserve an existing marker's exact spelling, including uppercase UUIDs.
Ok(Self(format!("workspace:v1:{id}")))
}
pub fn marker_id(&self) -> &str {
&self.0["workspace:v1:".len()..]
}
}
impl TryFrom<String> for WorkspaceIdentity {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_marker_id(
value
.strip_prefix("workspace:v1:")
.ok_or("invalid workspace identity")?,
)
}
}
impl From<WorkspaceIdentity> for String {
fn from(identity: WorkspaceIdentity) -> Self {
identity.0
}
}
#[cfg(test)]
mod language_tests {
use super::*;
use crate::language::Language;
#[test]
fn canonical_english_default_and_chinese_invocation_fields() {
const ORIGINAL: &str = r#"{"cwd":"/tmp","workspace_origin":"selected","sandbox_mode":"workspace-write","approval_policy":{"kind":"on-request"},"boundary_revision":0,"collaboration_mode":"agent","orchestration_mode":"default","tool_mode":"direct","model":null,"thinking_level":null}"#;
let mut configuration: InvocationConfiguration = serde_json::from_str(ORIGINAL).unwrap();
assert_eq!(configuration.language, Language::En);
assert_eq!(serde_json::to_string(&configuration).unwrap(), ORIGINAL);
for (language, tag) in [(Language::ZhCn, "zh-CN"), (Language::ZhTw, "zh-TW")] {
configuration.language = language;
let encoded = serde_json::to_value(&configuration).unwrap();
assert_eq!(encoded["language"], tag);
assert_eq!(
serde_json::from_value::<InvocationConfiguration>(encoded)
.unwrap()
.language,
language
);
}
}
}