moonpod

    A transport-neutral resource policy and audit engine for MoonBit hosts

    policy
    audit
    plugin
    workflow
    agent
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    1

    Dependencies

    #MoonPod

    MoonBit 通用策略执行与审计引擎:让宿主在执行资源操作前,先得到可解释、可验证的决策。

    CI

    MoonPod 是一个纯 MoonBit、无 I/O 副作用的策略核心。它不假设调用方一定是 AI Agent,也不直接启动进程或访问文件系统;宿主把一次待执行操作描述成 Operation,MoonPod 返回 Allow、Deny 或 ApprovalRequired,并保存带顺序号的 审计事件。这个边界让同一套规则可以放在 IDE 插件、CI 任务、桌面自动化、服务 连接器或 Agent Runtime 前面。

    #为什么需要通用策略层

    把“允许某个工具”写成一个布尔开关通常不够用:文件需要目录范围,命令需要参数 前缀,网络访问需要主机和端口,插件或业务服务还需要动作与资源范围。MoonPod 把这些约束放进一次统一的会话判定中,默认拒绝未知能力,并把调用次数、单次与 会话字节预算、人工审批和策略收窄放在同一个可测试模型里。宿主仍负责操作系统 权限、容器或 Wasm 隔离,MoonPod 负责“这一次调用是否符合策略”。

    #核心模型

    • 内置操作: ReadFile、WriteFile、Connect 和 RunCommand,分别提供路径、 主机—端口、程序名和逐项参数前缀约束。
    • 通用资源操作: ResourceAccess(tool, action, resource, estimated_bytes) 配合 ResourceRule,可表达 plugin.invoke / run / tenant/acme、业务 API 路径或工作流资源,而不必为每种领域增加一个枚举分支。
    • 会话与审计: Session::authorize 是唯一的决策入口;会话关闭会取消未处理 的审批,请求、最终决定和消耗字节都可通过 audit_json() 导出。
    • 策略组合: Policy::restrict 只计算两份策略的共同权限,规则只能收窄、不能 扩权,适合把组织策略与任务策略叠加。

    #一个通用资源规则

    资源前缀按 / 分段匹配,精确资源或其子资源才能通过:

    let policy = @moonpod.Policy::new(
    ["plugin.invoke"],
    resource_rules=[
    @moonpod.ResourceRule::new(
    tool="plugin.invoke",
    action="run",
    resource_prefix="tenant/acme/reports",
    ),
    ],
    )
    let session = @moonpod.Session::new(policy)
    session.authorize(@moonpod.ResourceAccess(
    tool="plugin.invoke",
    action="run",
    resource="tenant/acme/reports/monthly",
    estimated_bytes=512,
    ))

    同一接口可以换成 CI 的 build / workspace/project-a 或桌面自动化的 calendar.read / tenant/acme,调用方只需负责把自己的资源命名规范传进来。

    #JSON 策略与审计导出

    命令行通过 --policy-json 加载策略,并把演示调用和审计摘要写到标准输出:

    moon run cmd/main -- --policy-json '{"schema_version":1,"allowed_tools":["fs.read"],"read_roots":["/workspace"]}' > audit.json

    JSON 策略当前使用严格的 schema_version: 1 格式;缺少版本号、版本不支持或出现 未知字段都会在任何操作执行前被拒绝。除 allowed_tools 外,还可配置 read_roots、write_roots、protected_paths、 network_rules、command_rules、resource_rules、tool_quotas、审批工具和三类 预算。无效 JSON 或字段类型错误会在任何操作执行前被拒绝。

    单个操作也可以使用 Operation::to_json 和 Operation::from_json 在宿主边界传输; 编解码覆盖文件、网络、命令、通用调用和资源访问,并拒绝缺字段或未知字段。

    命令行也可以按顺序评估一批操作;输出中的 events 会保留每次调用的决定和消耗:

    moon run cmd/main -- --policy-json '{"schema_version":1,"allowed_tools":["fs.read","net.connect"],"read_roots":["/workspace"],"network_rules":[{"host":"api.example.com","allowed_ports":[443]}]}' --operations-json '[{"tool":"fs.read","path":"/workspace/README.md","estimated_bytes":128},{"tool":"net.connect","host":"api.example.com","port":443}]' > batch-audit.json

    追加式日志可加上 --audit-jsonl,让每条审计事件独占一行,便于直接写入 日志收集器或追加到现有文件:

    moon run cmd/main -- --policy-json '{"schema_version":1,"allowed_tools":["fs.read"],"read_roots":["/workspace"]}' --operations-json '[{"tool":"fs.read","path":"/workspace/README.md","estimated_bytes":128}]' --audit-jsonl >> audit.jsonl

    #与 MoonPermit 的生态关系

    可选子包 moonpermit_adapter 将 doffice/moonpermit 的 Permit 安全地转换为 MoonPod Policy。MoonPermit 负责从结构化计划推导最小 授权、权限包含和委托证明;MoonPod 负责在宿主边界逐次拦截真实调用。适配器只 接受不会造成扩权的共同子集,无法无损表达的精确路径、过期授权、网络或 Secret 权限会失败关闭。MoonPod 不依赖 MoonPermit,也不重复实现其计划编译器。

    #安装

    从 Mooncakes 安装 v0.2.0:

    moon add yadidyc/moonpod@0.2.0

    然后在使用 MoonPod 的包中声明根包依赖:

    import {
    "yadidyc/moonpod",
    }

    MoonBit 代码中通过 @moonpod 使用公开 API。若需要从源码运行完整测试与示例:

    git clone https://github.com/yadidyc/moonpod.git cd moonpod moon check --target all --deny-warn moon test --target all --deny-warn

    #快速开始与边界

    moon check moon test moon run cmd/main

    演示命令会先运行工作区策略,再运行一个与 Agent 无关的构建产物策略: workspace/project-a/report.json 被允许读取,而同样的请求换成 workspace/project-b/report.json 会被拒绝。这两个结果来自同一个 ResourceRule 的前缀边界,便于在没有真实执行器的环境中复核策略行为。

    接入宿主时遵循:映射请求 → 调用 Session::authorize → 仅对 Allow 执行副作用 → 持久化审计快照。路径检查是词法级的,不解析符号链接;命令不会经过 shell 解析;当前项目也不充当进程、容器或网络执行器。完整信任边界见 docs/threat-model.md;接入步骤见 docs/integration-guide.md,安全配置和检查清单见 docs/security-guide.md。

    #可运行集成示例

    仓库提供两个不执行真实副作用的宿主集成示例:

    moon run examples/policy_guard moon run examples/batch_audit

    前者演示把宿主请求映射为 Operation 并在 Decision::is_allowed() 后才执行动作; 后者演示加载版本化 JSON 策略、评估操作批次并输出 JSONL 审计记录。详细说明见 examples/README.md。

    #项目结构与许可证

    model.mbt 操作、决策、拒绝原因和审计事件 resource_policy.mbt 通用资源规则 path_policy.mbt 跨平台路径归一化和边界判定 policy.mbt 不可变策略与规则求值 policy_restriction.mbt 策略共同权限计算 session.mbt 会话、预算、审批和审计状态 json_policy.mbt JSON 策略解码和审计导出 docs/integration-guide.md 宿主接入和 API 使用指南 docs/security-guide.md 安全配置、边界和验证清单 moonpermit_adapter MoonPermit Permit 的失败关闭适配 cmd/main 可运行演示

    Apache-2.0

    OperationJsonError

    pub(all) suberror OperationJsonError {
    InvalidJson
    ExpectedObject
    ExpectedArray
    MissingField(String)
    InvalidField(String)
    UnknownField(String)
    } derive(Eq,
    Debug
    )

    Errors returned while decoding an operation JSON document.

    OperationJsonError::equal

    OperationJsonError::not_equal

    PolicyConfigError

    pub(all) suberror PolicyConfigError {
    InvalidLimit(name~ : String, value~ : Int)
    EmptyNetworkHost
    EmptyPortSet(String)
    InvalidNetworkPort(Int)
    EmptyCommandProgram
    EmptyToolQuotaName
    EmptyApprovalToolName
    EmptyResourceRuleTool
    EmptyResourceRuleAction
    EmptyResourceRulePrefix
    } derive(Eq,
    Debug
    )

    Invalid policy configuration.

    PolicyConfigError::equal

    PolicyConfigError::not_equal

    fn PolicyConfigError::not_equal(x : PolicyConfigError, y : PolicyConfigError) -> Bool

    PolicyJsonError

    pub(all) suberror PolicyJsonError {
    InvalidJson
    ExpectedObject
    MissingField(String)
    InvalidField(String)
    UnsupportedSchemaVersion(Int)
    UnknownField(String)
    InvalidPolicy
    } derive(Eq,
    Debug
    )

    Errors returned while decoding a JSON policy document.

    PolicyJsonError::equal

    PolicyJsonError::not_equal

    fn PolicyJsonError::not_equal(x : PolicyJsonError, y : PolicyJsonError) -> Bool

    ApprovalRequest

    pub(all) struct ApprovalRequest {
    id : Int
    operation : Operation
    } derive(Eq,
    Debug
    )

    An operation waiting for a human decision.

    ApprovalRequest::equal

    ApprovalRequest::not_equal

    fn ApprovalRequest::not_equal(x : ApprovalRequest, y : ApprovalRequest) -> Bool

    AuditEvent

    pub(all) struct AuditEvent {
    sequence : Int
    operation : Operation
    decision : Decision
    charged_bytes : Int
    } derive(Eq,
    Debug
    )

    One audit entry produced by a session.

    Pending approval entries are updated with the final decision when resolved.

    AuditEvent::equal

    fn AuditEvent::equal(AuditEvent, AuditEvent) -> Bool

    AuditEvent::not_equal

    fn AuditEvent::not_equal(x : AuditEvent, y : AuditEvent) -> Bool

    AuditEvent::to_json

    fn AuditEvent::to_json(self : AuditEvent) -> String

    Serializes one audit event as a standalone JSON record.

    CommandRule

    pub struct CommandRule {
    program : String
    argument_prefix : Array[String]
    } derive(Eq,
    Debug
    )

    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.

    CommandRule::equal

    fn CommandRule::equal(CommandRule, CommandRule) -> Bool

    CommandRule::new

    fn CommandRule::new(program~ : String, argument_prefix~ : Array[String]) -> CommandRule raise PolicyConfigError

    Creates an immutable command rule.

    CommandRule::not_equal

    fn CommandRule::not_equal(x : CommandRule, y : CommandRule) -> Bool

    Decision

    pub(all) enum Decision {
    Allow
    ApprovalRequired(Int)
    ApprovalGranted(Int)
    ApprovalCancelled(Int)
    Deny(DenyReason)
    } derive(Eq,
    Debug
    )

    The policy decision returned to the host adapter.

    Decision::equal

    fn Decision::equal(Decision, Decision) -> Bool

    Decision::is_allowed

    fn Decision::is_allowed(self : Decision) -> Bool

    Returns whether this decision permits the host operation.

    Decision::not_equal

    fn Decision::not_equal(x : Decision, y : Decision) -> Bool

    Decision::summary

    fn Decision::summary(self : Decision) -> String

    Returns a stable, human-readable decision summary.

    Decision::to_repr

    DenyReason

    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(Eq,
    Debug
    )

    A machine-readable reason for rejecting an operation.

    DenyReason::equal

    fn DenyReason::equal(DenyReason, DenyReason) -> Bool

    DenyReason::not_equal

    fn DenyReason::not_equal(x : DenyReason, y : DenyReason) -> Bool

    DenyReason::summary

    fn DenyReason::summary(self : DenyReason) -> String

    Returns a stable, human-readable denial summary.

    NetworkRule

    pub struct NetworkRule {
    host : String
    allowed_ports : Array[Int]
    } derive(Eq,
    Debug
    )

    An exact host rule with a set of allowed TCP ports.

    The port array is copied so later caller mutations cannot widen a policy.

    NetworkRule::equal

    fn NetworkRule::equal(NetworkRule, NetworkRule) -> Bool

    NetworkRule::new

    fn NetworkRule::new(host~ : String, allowed_ports~ : Array[Int]) -> NetworkRule raise PolicyConfigError

    Creates a validated network rule.

    NetworkRule::not_equal

    fn NetworkRule::not_equal(x : NetworkRule, y : NetworkRule) -> Bool

    Operation

    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(Eq,
    Debug
    )

    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.

    Operation::equal

    fn Operation::equal(Operation, Operation) -> Bool

    Operation::from_json

    fn Operation::from_json(source : String) -> Operation raise OperationJsonError

    Decodes an operation object emitted by Operation::to_json.

    The tool field selects one of the built-in operations. Unknown tools with estimated_output_bytes decode as Invoke; objects with action and resource decode as ResourceAccess.

    Operation::from_json_array

    fn Operation::from_json_array(source : String) -> Array[Operation] raise OperationJsonError

    Decodes a JSON array of operations in input order.

    Operation::not_equal

    fn Operation::not_equal(x : Operation, y : Operation) -> Bool

    Operation::to_json

    fn Operation::to_json(self : Operation) -> String

    Serializes an operation using the same object shape as audit events.

    Operation::tool_name

    fn Operation::tool_name(self : Operation) -> String

    Policy

    pub struct Policy {
    allowed_tools : Array[String]
    read_roots : Array[String]
    write_roots : Array[String]
    protected_paths : Array[String]
    network_rules : Array[NetworkRule]
    command_rules : Array[CommandRule]
    resource_rules : Array[ResourceRule]
    tool_quotas : Array[ToolQuota]
    approval_required_tools : Array[String]
    max_calls : Int
    max_operation_bytes : Int
    max_io_bytes : Int
    }

    A deny-by-default policy for host operations.

    Policy::from_json

    fn Policy::from_json(source : String) -> Policy raise PolicyJsonError

    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.

    Policy::new

    fn Policy::new(allowed_tools : Array[String], read_roots? : Array[String], write_roots? : Array[String], protected_paths? : Array[String], network_rules? : Array[NetworkRule], command_rules? : Array[CommandRule], tool_quotas? : Array[ToolQuota], max_calls? : Int, max_operation_bytes? : Int, max_io_bytes? : Int, approval_required_tools? : Array[String], resource_rules? : Array[ResourceRule]) -> Policy raise PolicyConfigError

    Builds an immutable policy.

    Recognized built-in tool keys are fs.read, fs.write, net.connect, and process.run. Custom Invoke operations use their own tool name; ResourceAccess operations are constrained by resource_rules.

    Policy::restrict

    fn Policy::restrict(self : Policy, restriction : Policy) -> Policy

    Narrows this policy by intersecting its permissions with restriction.

    Allow-lists and path roots are intersected. Protected paths and approval requirements are combined, while numeric limits take the stricter value. A rule present in only one policy cannot grant a permission absent from the other policy.

    ResourceRule

    pub struct ResourceRule {
    tool : String
    action : String
    resource_prefix : String
    } derive(Eq,
    Debug
    )

    A reusable rule for a tool's action on a resource namespace.

    resource_prefix uses slash-separated identifiers. A request matches when its resource is exactly the prefix or is a descendant below that prefix. This makes the same rule useful for plugin IDs, service paths, workspace objects, and other host-defined resource names.

    ResourceRule::equal

    ResourceRule::new

    fn ResourceRule::new(tool~ : String, action~ : String, resource_prefix~ : String) -> ResourceRule raise PolicyConfigError

    Creates a validated resource rule.

    ResourceRule::not_equal

    fn ResourceRule::not_equal(x : ResourceRule, y : ResourceRule) -> Bool

    Session

    pub struct Session {
    policy : Policy
    state : SessionState
    attempts : Int
    tool_attempts : Map[String, Int]
    used_bytes : Int
    reserved_bytes : Int
    events : Array[AuditEvent]
    approvals : Array[ApprovalRequest]
    }

    Mutable state for one isolated agent run.

    Session::approve

    fn Session::approve(self : Session, request_id : Int) -> Decision

    Approves a pending operation and charges its reserved byte estimate.

    Session::attempts

    fn Session::attempts(self : Session) -> Int

    Number of attempted operations, including denied ones.

    Session::audit_json

    fn Session::audit_json(self : Session) -> String

    Serializes a session summary and its audit events as JSON.

    Session::audit_jsonl

    fn Session::audit_jsonl(self : Session) -> 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.

    Session::audit_log

    fn Session::audit_log(self : Session) -> Array[AuditEvent]

    Returns a defensive copy of the audit log.

    Session::authorize

    fn Session::authorize(self : Session, operation : Operation) -> Decision

    Authorizes an operation and records the result.

    Every attempt consumes one call slot. Operations configured for human approval reserve their byte estimate until approved or rejected. This method never performs the operation itself.

    Session::close

    fn Session::close(self : Session) -> Unit

    Closes this session. Closing an already closed session has no effect.

    Session::new

    fn Session::new(policy : Policy) -> Session

    Starts a fresh session governed by policy.

    Session::pending_approvals

    fn Session::pending_approvals(self : Session) -> Array[ApprovalRequest]

    Returns a defensive copy of operations awaiting human approval.

    Session::reject

    fn Session::reject(self : Session, request_id : Int) -> Decision

    Rejects a pending operation and releases its reserved byte estimate.

    Session::remaining_bytes

    fn Session::remaining_bytes(self : Session) -> Int

    Remaining I/O bytes, including reservations for pending approvals.

    Session::remaining_calls

    fn Session::remaining_calls(self : Session) -> Int

    Remaining call slots in this session.

    Session::state

    fn Session::state(self : Session) -> SessionState

    Returns the current lifecycle state.

    Session::used_bytes

    fn Session::used_bytes(self : Session) -> Int

    Bytes charged by allowed operations.

    SessionState

    pub(all) enum SessionState {
    Active
    Closed
    } derive(Eq,
    Debug
    )

    The lifecycle state of an isolated session.

    SessionState::equal

    SessionState::not_equal

    fn SessionState::not_equal(x : SessionState, y : SessionState) -> Bool

    ToolQuota

    pub struct ToolQuota {
    tool : String
    max_calls : Int
    } derive(Eq,
    Debug
    )

    A maximum number of attempts for one exact tool name.

    ToolQuota::equal

    fn ToolQuota::equal(ToolQuota, ToolQuota) -> Bool

    ToolQuota::new

    fn ToolQuota::new(tool~ : String, max_calls~ : Int) -> ToolQuota raise PolicyConfigError

    Creates a validated per-tool call quota.

    ToolQuota::not_equal

    fn ToolQuota::not_equal(x : ToolQuota, y : ToolQuota) -> Bool

    workspace_policy

    fn workspace_policy(root : String) -> Policy

    A practical workspace policy for local coding agents.