///|
/// Errors returned while decoding a JSON policy document.
pub(all) suberror PolicyJsonError {
InvalidJson
ExpectedObject
MissingField(String)
InvalidField(String)
UnsupportedSchemaVersion(Int)
UnknownField(String)
InvalidPolicy
} derive(Debug, Eq)
///|
/// Builds a policy from a JSON document.
///
/// The document must declare `"schema_version": 1`. Unknown fields are
/// rejected so a typo cannot silently weaken a policy. `allowed_tools` is
/// required; other array fields default to empty and numeric limits use the
/// same defaults as `Policy::new`.
pub fn Policy::from_json(source : String) -> Policy raise PolicyJsonError {
let value = @json.parse(source) catch {
_ => raise PolicyJsonError::InvalidJson
}
let fields = json_object(value)
ensure_known_fields(
fields,
[
"schema_version", "allowed_tools", "read_roots", "write_roots", "protected_paths",
"network_rules", "command_rules", "resource_rules", "tool_quotas", "max_calls",
"max_operation_bytes", "max_io_bytes", "approval_required_tools",
],
"",
)
let schema_version = required_int(fields, "schema_version")
if schema_version != 1 {
raise PolicyJsonError::UnsupportedSchemaVersion(schema_version)
}
let allowed_tools = required_string_array(fields, "allowed_tools")
let read_roots = optional_string_array(fields, "read_roots")
let write_roots = optional_string_array(fields, "write_roots")
let protected_paths = optional_string_array(fields, "protected_paths")
let approval_required_tools = optional_string_array(
fields, "approval_required_tools",
)
let network_rules = []
for value in optional_array(fields, "network_rules") {
let rule_fields = json_object(value)
ensure_known_fields(rule_fields, ["host", "allowed_ports"], "network_rules")
let host = required_string(rule_fields, "host")
let allowed_ports = required_int_array(rule_fields, "allowed_ports")
let rule = NetworkRule::new(host~, allowed_ports~) catch {
_ => raise PolicyJsonError::InvalidField("network_rules")
}
network_rules.push(rule)
}
let command_rules = []
for value in optional_array(fields, "command_rules") {
let rule_fields = json_object(value)
ensure_known_fields(
rule_fields,
["program", "argument_prefix"],
"command_rules",
)
let program = required_string(rule_fields, "program")
let argument_prefix = required_string_array(rule_fields, "argument_prefix")
let rule = CommandRule::new(program~, argument_prefix~) catch {
_ => raise PolicyJsonError::InvalidField("command_rules")
}
command_rules.push(rule)
}
let resource_rules = []
for value in optional_array(fields, "resource_rules") {
let rule_fields = json_object(value)
ensure_known_fields(
rule_fields,
["tool", "action", "resource_prefix"],
"resource_rules",
)
let tool = required_string(rule_fields, "tool")
let action = required_string(rule_fields, "action")
let resource_prefix = required_string(rule_fields, "resource_prefix")
let rule = ResourceRule::new(tool~, action~, resource_prefix~) catch {
_ => raise PolicyJsonError::InvalidField("resource_rules")
}
resource_rules.push(rule)
}
let tool_quotas = []
for value in optional_array(fields, "tool_quotas") {
let quota_fields = json_object(value)
ensure_known_fields(quota_fields, ["tool", "max_calls"], "tool_quotas")
let tool = required_string(quota_fields, "tool")
let max_calls = required_int(quota_fields, "max_calls")
let quota = ToolQuota::new(tool~, max_calls~) catch {
_ => raise PolicyJsonError::InvalidField("tool_quotas")
}
tool_quotas.push(quota)
}
let max_calls = optional_int(fields, "max_calls", 100)
let max_operation_bytes = optional_int(fields, "max_operation_bytes", 1048576)
let max_io_bytes = optional_int(fields, "max_io_bytes", 1048576)
Policy::new(
allowed_tools,
read_roots~,
write_roots~,
protected_paths~,
network_rules~,
command_rules~,
tool_quotas~,
max_calls~,
max_operation_bytes~,
max_io_bytes~,
approval_required_tools~,
resource_rules~,
) catch {
_ => raise PolicyJsonError::InvalidPolicy
}
}
///|
fn ensure_known_fields(
fields : Map[String, Json],
known : Array[String],
context : String,
) -> Unit raise PolicyJsonError {
for name, _ in fields {
if !known.contains(name) {
let full_name = if context == "" { name } else { context + "." + name }
raise PolicyJsonError::UnknownField(full_name)
}
}
}
///|
fn json_object(value : Json) -> Map[String, Json] raise PolicyJsonError {
match value {
Json::Object(fields) => fields
_ => raise PolicyJsonError::ExpectedObject
}
}
///|
fn required_json(
fields : Map[String, Json],
name : String,
) -> Json raise PolicyJsonError {
match fields.get(name) {
Some(value) => value
None => raise PolicyJsonError::MissingField(name)
}
}
///|
fn required_string(
fields : Map[String, Json],
name : String,
) -> String raise PolicyJsonError {
match required_json(fields, name) {
Json::String(value) => value
_ => raise PolicyJsonError::InvalidField(name)
}
}
///|
fn optional_string_array(
fields : Map[String, Json],
name : String,
) -> Array[String] raise PolicyJsonError {
match fields.get(name) {
None => []
Some(value) => json_string_array(value, name)
}
}
///|
fn required_string_array(
fields : Map[String, Json],
name : String,
) -> Array[String] raise PolicyJsonError {
json_string_array(required_json(fields, name), name)
}
///|
fn json_string_array(
value : Json,
name : String,
) -> Array[String] raise PolicyJsonError {
match value {
Json::Array(values) => {
let result = []
for item in values {
match item {
Json::String(text) => result.push(text)
_ => raise PolicyJsonError::InvalidField(name)
}
}
result
}
_ => raise PolicyJsonError::InvalidField(name)
}
}
///|
fn optional_array(
fields : Map[String, Json],
name : String,
) -> Array[Json] raise PolicyJsonError {
match fields.get(name) {
None => []
Some(Json::Array(values)) => values
Some(_) => raise PolicyJsonError::InvalidField(name)
}
}
///|
fn required_int_array(
fields : Map[String, Json],
name : String,
) -> Array[Int] raise PolicyJsonError {
match required_json(fields, name) {
Json::Array(values) => {
let result = []
for value in values {
result.push(json_int(value, name))
}
result
}
_ => raise PolicyJsonError::InvalidField(name)
}
}
///|
fn optional_int(
fields : Map[String, Json],
name : String,
default : Int,
) -> Int raise PolicyJsonError {
match fields.get(name) {
None => default
Some(value) => json_int(value, name)
}
}
///|
fn required_int(
fields : Map[String, Json],
name : String,
) -> Int raise PolicyJsonError {
json_int(required_json(fields, name), name)
}
///|
fn json_int(value : Json, name : String) -> Int raise PolicyJsonError {
match value {
Json::Number(number, ..) => {
let integer = number.to_int()
if integer.to_double() == number {
integer
} else {
raise PolicyJsonError::InvalidField(name)
}
}
_ => raise PolicyJsonError::InvalidField(name)
}
}
///|
/// Serializes a session summary and its audit events as JSON.
pub fn Session::audit_json(self : Session) -> String {
let output = StringBuilder()
output.write_string("{\n \"attempts\": ")
output.write_string(self.attempts().to_string())
output.write_string(",\n \"used_bytes\": ")
output.write_string(self.used_bytes().to_string())
output.write_string(",\n \"remaining_calls\": ")
output.write_string(self.remaining_calls().to_string())
output.write_string(",\n \"remaining_bytes\": ")
output.write_string(self.remaining_bytes().to_string())
output.write_string(",\n \"events\": [")
let first = Ref(true)
for event in self.audit_log() {
if first.val {
first.val = false
} else {
output.write_char(',')
}
output.write_string("\n ")
append_audit_event_json(output, event)
}
if !first.val {
output.write_char('\n')
output.write_string(" ")
}
output.write_string("]\n}")
output.to_string()
}
///|
/// Serializes one audit event as a standalone JSON record.
pub fn AuditEvent::to_json(self : AuditEvent) -> String {
let output = StringBuilder()
append_audit_event_json(output, self)
output.to_string()
}
///|
/// Exports the audit log as newline-delimited JSON records.
///
/// Records retain their sequence order and are suitable for append-only log
/// sinks. An empty session returns an empty string; non-empty output ends each
/// record with a newline.
pub fn Session::audit_jsonl(self : Session) -> String {
let output = StringBuilder()
for event in self.audit_log() {
output.write_string(event.to_json())
output.write_char('\n')
}
output.to_string()
}
///|
fn append_audit_event_json(output : StringBuilder, event : AuditEvent) -> Unit {
output.write_string("{\"sequence\": ")
output.write_string(event.sequence.to_string())
output.write_string(", \"operation\": ")
output.write_string(event.operation.to_json())
output.write_string(", \"decision\": ")
append_json_string(output, event.decision.summary())
output.write_string(", \"charged_bytes\": ")
output.write_string(event.charged_bytes.to_string())
output.write_char('}')
}
///|
fn append_operation_json(output : StringBuilder, operation : Operation) -> Unit {
match operation {
ReadFile(path~, estimated_bytes~) => {
output.write_string("{\"tool\": \"fs.read\", \"path\": ")
append_json_string(output, path)
output.write_string(", \"estimated_bytes\": ")
output.write_string(estimated_bytes.to_string())
output.write_char('}')
}
WriteFile(path~, bytes~) => {
output.write_string("{\"tool\": \"fs.write\", \"path\": ")
append_json_string(output, path)
output.write_string(", \"bytes\": ")
output.write_string(bytes.to_string())
output.write_char('}')
}
Connect(host~, port~) => {
output.write_string("{\"tool\": \"net.connect\", \"host\": ")
append_json_string(output, host)
output.write_string(", \"port\": ")
output.write_string(port.to_string())
output.write_char('}')
}
RunCommand(program~, arguments~, estimated_output_bytes~) => {
output.write_string("{\"tool\": \"process.run\", \"program\": ")
append_json_string(output, program)
output.write_string(", \"arguments\": [")
let first = Ref(true)
for argument in arguments {
if first.val {
first.val = false
} else {
output.write_string(", ")
}
append_json_string(output, argument)
}
output.write_string("], \"estimated_output_bytes\": ")
output.write_string(estimated_output_bytes.to_string())
output.write_char('}')
}
Invoke(tool~, estimated_output_bytes~) => {
output.write_string("{\"tool\": ")
append_json_string(output, tool)
output.write_string(", \"estimated_output_bytes\": ")
output.write_string(estimated_output_bytes.to_string())
output.write_char('}')
}
ResourceAccess(tool~, action~, resource~, estimated_bytes~) => {
output.write_string("{\"tool\": ")
append_json_string(output, tool)
output.write_string(", \"action\": ")
append_json_string(output, action)
output.write_string(", \"resource\": ")
append_json_string(output, resource)
output.write_string(", \"estimated_bytes\": ")
output.write_string(estimated_bytes.to_string())
output.write_char('}')
}
}
}
///|
fn append_json_string(output : StringBuilder, value : String) -> Unit {
output.write_char('"')
for char in value {
match char {
'"' => output.write_string("\\\"")
'\\' => output.write_string("\\\\")
'\b' => output.write_string("\\b")
'\f' => output.write_string("\\f")
'\n' => output.write_string("\\n")
'\r' => output.write_string("\\r")
'\t' => output.write_string("\\t")
_ => {
let code = char.to_int()
if code < 0x20 {
output.write_string("\\u00")
output.write_char(hex_digit(code / 16))
output.write_char(hex_digit(code % 16))
} else {
output.write_char(char)
}
}
}
}
output.write_char('"')
}
///|
fn hex_digit(value : Int) -> Char {
match value {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => 'a'
11 => 'b'
12 => 'c'
13 => 'd'
14 => 'e'
15 => 'f'
_ => '0'
}
}