///|
/// An operation a caller wants the host to perform.
///
/// Byte estimates are charged against the session I/O budget only when the
/// operation is allowed. `ResourceAccess` provides a domain-neutral request
/// shape for caller-defined tools, actions, and resource namespaces.
pub(all) enum Operation {
ReadFile(path~ : String, estimated_bytes~ : Int)
WriteFile(path~ : String, bytes~ : Int)
Connect(host~ : String, port~ : Int)
RunCommand(
program~ : String,
arguments~ : Array[String],
estimated_output_bytes~ : Int
)
Invoke(tool~ : String, estimated_output_bytes~ : Int)
ResourceAccess(
tool~ : String,
action~ : String,
resource~ : String,
estimated_bytes~ : Int
)
} derive(Debug, Eq)
///|
/// An exact host rule with a set of allowed TCP ports.
///
/// The port array is copied so later caller mutations cannot widen a policy.
pub struct NetworkRule {
host : String
allowed_ports : Array[Int]
} derive(Debug, Eq)
///|
/// Creates a validated network rule.
pub fn NetworkRule::new(
host~ : String,
allowed_ports~ : Array[Int],
) -> NetworkRule raise PolicyConfigError {
if host == "" {
raise PolicyConfigError::EmptyNetworkHost
}
if allowed_ports.length() == 0 {
raise PolicyConfigError::EmptyPortSet(host)
}
for port in allowed_ports {
if port < 1 || port > 65535 {
raise PolicyConfigError::InvalidNetworkPort(port)
}
}
{ host, allowed_ports: allowed_ports.copy(), }
}
///|
fn NetworkRule::matches_host(self : NetworkRule, host : String) -> Bool {
self.host == host
}
///|
fn NetworkRule::allows_port(self : NetworkRule, port : Int) -> Bool {
self.allowed_ports.contains(port)
}
///|
/// An exact executable name and an allowed argument prefix.
///
/// Prefix elements are compared as complete arguments. An empty prefix permits
/// only a command with no arguments.
pub struct CommandRule {
program : String
argument_prefix : Array[String]
} derive(Debug, Eq)
///|
/// Creates an immutable command rule.
pub fn CommandRule::new(
program~ : String,
argument_prefix~ : Array[String],
) -> CommandRule raise PolicyConfigError {
if program == "" {
raise PolicyConfigError::EmptyCommandProgram
}
{ program, argument_prefix: argument_prefix.copy(), }
}
///|
fn CommandRule::matches_program(self : CommandRule, program : String) -> Bool {
self.program == program
}
///|
fn CommandRule::allows_arguments(
self : CommandRule,
arguments : Array[String],
) -> Bool {
if self.argument_prefix.length() == 0 {
arguments.length() == 0
} else {
arguments.starts_with(self.argument_prefix)
}
}
///|
/// A maximum number of attempts for one exact tool name.
pub struct ToolQuota {
tool : String
max_calls : Int
} derive(Debug, Eq)
///|
/// Creates a validated per-tool call quota.
pub fn ToolQuota::new(
tool~ : String,
max_calls~ : Int,
) -> ToolQuota raise PolicyConfigError {
if tool == "" {
raise PolicyConfigError::EmptyToolQuotaName
}
if max_calls < 0 {
raise PolicyConfigError::InvalidLimit(
name="tool quota: " + tool,
value=max_calls,
)
}
{ tool, max_calls, }
}
///|
/// The lifecycle state of an isolated session.
pub(all) enum SessionState {
Active
Closed
} derive(Debug, Eq)
///|
/// A machine-readable reason for rejecting an operation.
pub(all) enum DenyReason {
SessionClosed
ApprovalRejected(request_id~ : Int)
ApprovalNotPending(request_id~ : Int)
CallBudgetExceeded(limit~ : Int)
ToolCallQuotaExceeded(tool~ : String, limit~ : Int)
OperationByteLimitExceeded(limit~ : Int, requested~ : Int)
IoBudgetExceeded(limit~ : Int, requested~ : Int)
ToolNotAllowed(String)
PathNotAllowed(String)
HostNotAllowed(String)
PortNotAllowed(host~ : String, port~ : Int)
CommandNotAllowed(String)
CommandArgumentsNotAllowed(String)
ResourceNotAllowed(tool~ : String, action~ : String, resource~ : String)
InvalidByteEstimate(Int)
} derive(Debug, Eq)
///|
/// The policy decision returned to the host adapter.
pub(all) enum Decision {
Allow
ApprovalRequired(Int)
ApprovalGranted(Int)
ApprovalCancelled(Int)
Deny(DenyReason)
} derive(Debug, Eq)
///|
/// An operation waiting for a human decision.
pub(all) struct ApprovalRequest {
id : Int
operation : Operation
} derive(Debug, Eq)
///|
/// One audit entry produced by a session.
///
/// Pending approval entries are updated with the final decision when resolved.
pub(all) struct AuditEvent {
sequence : Int
operation : Operation
decision : Decision
charged_bytes : Int
} derive(Debug, Eq)
///|
/// Returns whether this decision permits the host operation.
pub fn Decision::is_allowed(self : Decision) -> Bool {
self is Allow || self is ApprovalGranted(_)
}
///|
/// Returns a stable, human-readable decision summary.
pub fn Decision::summary(self : Decision) -> String {
match self {
Allow => "allow"
ApprovalRequired(request_id) => "approval required: request \{request_id}"
ApprovalGranted(request_id) => "approved: request \{request_id}"
ApprovalCancelled(request_id) => "approval cancelled: request \{request_id}"
Deny(reason) => "deny: " + reason.summary()
}
}
///|
/// Returns a stable, human-readable denial summary.
pub fn DenyReason::summary(self : DenyReason) -> String {
match self {
SessionClosed => "session is closed"
ApprovalRejected(request_id~) => "approval rejected: request \{request_id}"
ApprovalNotPending(request_id~) =>
"approval request is not pending: \{request_id}"
CallBudgetExceeded(limit~) => "call budget exceeded (limit \{limit})"
ToolCallQuotaExceeded(tool~, limit~) =>
"tool call quota exceeded: \{tool} (limit \{limit})"
OperationByteLimitExceeded(limit~, requested~) =>
"operation byte limit exceeded (limit \{limit}, requested \{requested})"
IoBudgetExceeded(limit~, requested~) =>
"I/O budget exceeded (limit \{limit}, requested \{requested})"
ToolNotAllowed(tool) => "tool not allowed: \{tool}"
PathNotAllowed(path) => "path not allowed: \{path}"
HostNotAllowed(host) => "host not allowed: \{host}"
PortNotAllowed(host~, port~) => "port not allowed: \{host}:\{port}"
CommandNotAllowed(program) => "command not allowed: \{program}"
CommandArgumentsNotAllowed(program) =>
"command arguments not allowed: \{program}"
ResourceNotAllowed(tool~, action~, resource~) =>
"resource not allowed: \{tool} / \{action} / \{resource}"
InvalidByteEstimate(value) => "invalid byte estimate: \{value}"
}
}
///|
pub fn Operation::tool_name(self : Operation) -> String {
match self {
ReadFile(..) => "fs.read"
WriteFile(..) => "fs.write"
Connect(..) => "net.connect"
RunCommand(..) => "process.run"
Invoke(tool~, ..) => tool
ResourceAccess(tool~, ..) => tool
}
}
///|
fn Operation::byte_cost(self : Operation) -> Int {
match self {
ReadFile(estimated_bytes~, ..) => estimated_bytes
WriteFile(bytes~, ..) => bytes
Connect(..) => 0
RunCommand(estimated_output_bytes~, ..) => estimated_output_bytes
Invoke(estimated_output_bytes~, ..) => estimated_output_bytes
ResourceAccess(estimated_bytes~, ..) => estimated_bytes
}
}
///|
/// Copies mutable operation payloads before they enter the audit log.
fn Operation::snapshot(self : Operation) -> Operation {
match self {
RunCommand(program~, arguments~, estimated_output_bytes~) =>
RunCommand(program~, arguments=arguments.copy(), estimated_output_bytes~)
_ => self
}
}