blob: 6b200698854e9ed6a3de9e5a891798c92b60e17d [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 super::*;
use cap_std::fs::{Dir, OpenOptions};
use maka_assistant::instructions::{
InstructionFile, WorkspaceInstruction, parse_instruction_files,
};
use std::{io::Read, path::Path};
pub(super) fn open_home(
configuration: Option<&AssistantConfiguration>,
) -> Result<Option<Arc<Dir>>, String> {
configuration
.and_then(|configuration| configuration.home.as_ref())
.map(|home| {
if !Path::new(home).is_absolute() {
return Err("agent_instruction_home_invalid".into());
}
Dir::open_ambient_dir(home, cap_std::ambient_authority())
.map(Arc::new)
.map_err(|_| "agent_instruction_home_unavailable".into())
})
.transpose()
}
fn read(directory: &Dir, agents: bool) -> Vec<WorkspaceInstruction> {
let mut sources = Vec::new();
for name in [
InstructionFile::Agents,
InstructionFile::Claude,
InstructionFile::Gemini,
]
.into_iter()
.filter(|name| agents || *name != InstructionFile::Agents)
{
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use cap_std::fs::OpenOptionsExt;
options.custom_flags(libc::O_NONBLOCK);
}
// Relative capability opens cannot follow a link outside this directory.
let Ok(file) = directory.open_with(name.name(), &options) else {
continue;
};
let Ok(metadata) = file.metadata() else {
continue;
};
if !metadata.is_file() {
continue;
}
let length = metadata.len();
let mut bytes = Vec::new();
if (&file).take(length).read_to_end(&mut bytes).is_ok()
&& u64::try_from(bytes.len()).ok() == Some(length)
{
sources.push((name, String::from_utf8_lossy(&bytes).into_owned()));
}
}
parse_instruction_files(sources)
}
impl Owner {
/// Capture once at message acceptance. Recovery replays the persisted basis.
pub(super) async fn capture_behavior(
&self,
workspace: &Workspace,
snapshot: &RuntimePolicySnapshot,
language: Language,
) -> Result<Option<SystemPrompt>, String> {
let role = self.behavior.map(|behavior| {
let mut source = self.revision.clone();
source.kind = SourceKind::PromptSection;
SystemPrompt {
text: match behavior {
BuiltinBehavior::WorkHub => crate::workhub::prompt(language).into(),
},
policy_revision: snapshot.revision,
sources: vec![source],
}
});
if self.settings.assistant.is_none() {
return Ok(role);
}
let preferences = maka_agent_ports::preferences::Snapshot {
revision: snapshot.revision,
privacy: snapshot.policy.privacy.clone(),
personalization: snapshot.policy.personalization.clone(),
workspace_instructions: snapshot.policy.workspace_instructions.enabled,
};
let (global_instruction, instructions) = if preferences.workspace_instructions {
let workspace = workspace.clone();
let home = self.instruction_home.clone();
tokio::task::spawn_blocking(move || {
let project = maka_fs_tools::workspace::open_directory(
Path::new(&workspace.path),
&workspace.identity,
)
.map_err(|_| "agent_workspace_unavailable")?;
let mut global_instruction = String::new();
let mut global = Vec::new();
if let Some(home) = home {
if let Ok(source) = global_instructions::selected(&home)
&& let Ok(Some(text)) = global_instructions::read_selected(&home, source)
{
global_instruction = parse_instruction_files([(
InstructionFile::Agents,
String::from_utf8_lossy(&text).into_owned(),
)])
.pop()
.map(|instruction| instruction.text)
.unwrap_or_default();
}
if let Ok(directory) = home.open_dir(".maka") {
global.extend(read(&directory, false));
}
}
Ok::<_, String>((
global_instruction,
maka_assistant::prompt::workspace::render(
global,
read(&project, true),
language,
),
))
})
.await
.map_err(|_| "agent_instructions_unavailable")??
} else {
(String::new(), String::new())
};
let mut prompt =
maka_assistant::prompt::compose(preferences, language, &global_instruction);
let mut source = self.revision.clone();
source.kind = SourceKind::PromptSection;
prompt.sources.push(source);
if let Some(behavior) = &role {
prompt.text.push_str("\n\n");
prompt.text.push_str(&behavior.text);
prompt.sources.extend(behavior.sources.clone());
}
if !instructions.is_empty() {
prompt.text.push_str("\n\n");
prompt.text.push_str(&instructions);
}
prompt.validate().map_err(|_| "agent_prompt_invalid")?;
Ok(Some(prompt))
}
}