Give loopd's tool port approvals, its own clock and plain denials
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
+138
-9
@@ -7,7 +7,20 @@ use crate::llama::ToolSchema;
|
||||
/// The seam every tool call goes through. `loopd` answers `find_tool` and `call_tool` itself;
|
||||
/// anything else, including grants, is passed to the port (`brokerd`).
|
||||
pub trait ToolPort: Send + Sync {
|
||||
fn call(&self, request: &proto::ToolRequest) -> proto::ToolResponse;
|
||||
/// Returns the final answer, never `PendingApproval`. If the broker says the call is waiting
|
||||
/// for the owner, the port calls `on_pending` once and goes on waiting.
|
||||
fn call(
|
||||
&self,
|
||||
request: &proto::ToolRequest,
|
||||
on_pending: &mut dyn FnMut(&Pending),
|
||||
) -> proto::ToolResponse;
|
||||
}
|
||||
|
||||
/// One waiting call, as the broker reports it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Pending {
|
||||
pub approval: u64,
|
||||
pub expires: proto::Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -26,7 +39,9 @@ impl Registry {
|
||||
Registry { entries }
|
||||
}
|
||||
|
||||
/// The tool set for the M2b epoch: `clock` (core) and `echo` (not core).
|
||||
/// The tool set for the M2b epoch: `clock` (core) and `echo` (not core). This is the test
|
||||
/// registry: `echo` exists only in `FakeTools` and the recorded conversations, and `loopd
|
||||
/// serve` never uses it.
|
||||
pub fn m2b() -> Registry {
|
||||
Registry::new(vec![
|
||||
Entry {
|
||||
@@ -40,6 +55,33 @@ impl Registry {
|
||||
])
|
||||
}
|
||||
|
||||
/// The tool set for the M3a epoch: `clock` (core), then the four tools `brokerd` decides on.
|
||||
/// The core entries match `m2b()`, so the tools array is unchanged.
|
||||
pub fn m3a() -> Registry {
|
||||
Registry::new(vec![
|
||||
Entry {
|
||||
schema: clock_schema(),
|
||||
core: true,
|
||||
},
|
||||
Entry {
|
||||
schema: read_file_schema(),
|
||||
core: false,
|
||||
},
|
||||
Entry {
|
||||
schema: write_file_schema(),
|
||||
core: false,
|
||||
},
|
||||
Entry {
|
||||
schema: shell_schema(),
|
||||
core: false,
|
||||
},
|
||||
Entry {
|
||||
schema: http_fetch_schema(),
|
||||
core: false,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/// The fixed tools array: core entries, then `find_tool`, then `call_tool`.
|
||||
pub fn core_schemas(&self) -> Vec<ToolSchema> {
|
||||
let mut schemas: Vec<ToolSchema> = self
|
||||
@@ -76,6 +118,7 @@ impl Registry {
|
||||
|
||||
pub const FIND_TOOL: &str = "find_tool";
|
||||
pub const CALL_TOOL: &str = "call_tool";
|
||||
pub const CLOCK: &str = "clock";
|
||||
|
||||
pub fn clock_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
@@ -102,6 +145,65 @@ pub fn echo_schema() -> ToolSchema {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_file_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "read_file".to_string(),
|
||||
description: "Read a text file. Needs a grant from the owner for the path.".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "The absolute path of the file." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_file_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "write_file".to_string(),
|
||||
description: "Write a text file, replacing it. Needs a grant from the owner for the path."
|
||||
.to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "The absolute path of the file." },
|
||||
"content": { "type": "string", "description": "The whole new content of the file." }
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shell_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "shell".to_string(),
|
||||
description: "Run a shell command in a sandbox. Needs a grant from the owner.".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": { "type": "string", "description": "The command line to run." },
|
||||
"cwd": { "type": "string", "description": "The absolute path of the directory to run it in." }
|
||||
},
|
||||
"required": ["command"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn http_fetch_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "http_fetch".to_string(),
|
||||
description: "Fetch an https URL. Needs a grant from the owner for the host.".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": { "type": "string", "description": "The URL. It must start with https://" }
|
||||
},
|
||||
"required": ["url"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_tool_schema() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "find_tool".to_string(),
|
||||
@@ -157,6 +259,9 @@ pub fn dispatch(registry: &Registry, name: &str, arguments: &str) -> Dispatch {
|
||||
if name == CALL_TOOL {
|
||||
return dispatch_call_tool(registry, arguments);
|
||||
}
|
||||
if name == CLOCK {
|
||||
return Dispatch::Local(proto::Timestamp::now().to_rfc3339());
|
||||
}
|
||||
Dispatch::Port {
|
||||
tool: name.to_string(),
|
||||
arguments: arguments.to_string(),
|
||||
@@ -224,6 +329,32 @@ pub fn cap_result(text: &str, cap: usize) -> (String, bool) {
|
||||
(result, true)
|
||||
}
|
||||
|
||||
/// The one fixed sentence a denial reaches the model as, named by its reason.
|
||||
pub fn denial_text(reason: proto::DenyReason) -> &'static str {
|
||||
match reason {
|
||||
proto::DenyReason::NoGrant => "Denied: no grant allows this call.",
|
||||
proto::DenyReason::GrantExpired => "Denied: the grant for this call has expired.",
|
||||
proto::DenyReason::TaintTooHigh => {
|
||||
"Denied: this session has seen data too sensitive for this call."
|
||||
}
|
||||
proto::DenyReason::DeniedByGrant => "Denied: a grant forbids this call.",
|
||||
proto::DenyReason::ApprovalRefused => "Denied: the owner refused this call.",
|
||||
proto::DenyReason::ApprovalExpired => {
|
||||
"Denied: the approval request expired without an answer."
|
||||
}
|
||||
proto::DenyReason::InvalidArguments => "Denied: the arguments are not valid for this tool.",
|
||||
proto::DenyReason::GrantsInvalid => {
|
||||
"Denied: the grant files have an error; the owner has been told."
|
||||
}
|
||||
proto::DenyReason::AuditUnavailable => {
|
||||
"Denied: the audit log cannot be written; the owner has been told."
|
||||
}
|
||||
proto::DenyReason::StateUnreadable => {
|
||||
"Denied: this session's broker state is damaged; the owner has been told."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeTools {
|
||||
calls: Mutex<Vec<proto::ToolRequest>>,
|
||||
}
|
||||
@@ -247,19 +378,17 @@ impl FakeTools {
|
||||
}
|
||||
|
||||
impl ToolPort for FakeTools {
|
||||
fn call(&self, request: &proto::ToolRequest) -> proto::ToolResponse {
|
||||
fn call(
|
||||
&self,
|
||||
request: &proto::ToolRequest,
|
||||
_on_pending: &mut dyn FnMut(&Pending),
|
||||
) -> proto::ToolResponse {
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap_or_else(|p| p.into_inner())
|
||||
.push(request.clone());
|
||||
|
||||
match request.tool.as_str() {
|
||||
"clock" => proto::ToolResponse::Result {
|
||||
content: proto::Timestamp::now().to_rfc3339(),
|
||||
class: proto::DataClass::Public,
|
||||
untrusted: false,
|
||||
truncated: false,
|
||||
},
|
||||
"echo" => match serde_json::from_str::<serde_json::Value>(&request.arguments) {
|
||||
Ok(serde_json::Value::Object(obj)) => {
|
||||
match obj.get("text").and_then(|v| v.as_str()) {
|
||||
|
||||
Reference in New Issue
Block a user