ptc

package
v0.31.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

诊断面:出错位置与面向模型的修复建议。

为什么诊断要带位置

本包的调用方是模型。模型写的程序出了错,只回一句「invalid result」对它几乎没有 可用信息——它不知道是哪一行,也不知道该改什么,只能原样重写一遍碰运气。 而「第 12 行」这一个数字,能把重试从盲猜变成定点修。

对照 OpenCode packages/codemode:它的 Diagnostic 是 `{ kind, message, location{line,column}, suggestions[] }`,把位置与建议列为 契约的一部分。本包此前只有 ErrorKind 一个类别串,是这条差距的另一半。

位置怎么来

语法错误一条来源、运行期一条来源,两条的可得性**不一样**,这是事实不是缺陷:

  • 语法错误:goja.CompilerSyntaxError 嵌的 CompilerError 带 File 与 Offset, 而 (*goja.File).Position 是 goja/file 包里的**导出**方法,直接得行列。 拿到后折算回程序内行号(包裹只多一行)。

  • 运行期异常:只有当错误经 (*Runtime).RunProgram 直接返回时, 才会拿到 *goja.Exception,其 Stack() 里有程序帧可定位。

    但本包的程序体是 **async IIFE**:函数体里抛出的异常会变成 Promise rejection, RunProgram 本身正常返回,rejection 原因只是一个值,拿不到 goja 侧的历史栈。 goja 未提供「rejection 处回溯调用栈」的公开面。

所以运行期异常的 Location **经常是空的**。这是工具能力边界,如实空着—— 编一个位置出来会让模型去改一行没问题的代码,比没有位置更坏。 宿主若有更强的定位手段(如绑定调用自带行列),可在包装层补齐 Location。

Package ptc 程序化工具编排运行时(批三百零五,deepseek-harness ptc-runtime 对标 P1 试点,设计档 docs/PTC_RUNTIME_DESIGN_20261011.md):模型写一段程序,进程内 goja VM 执行,程序内经 bindings 调用宿主工具,只把归并后的小结果带回上下文——N 轮 ReAct 往返塌缩成 1 轮,原始数据不过模型面。

与 dsh 的取舍(spec §2):goja 进程内 VM(纯 Go 零 CGO,无 Node 分发面)/保守 JS 子集(无 fetch/fs/require,能力只来自 bindings)/VM 边界+三重预算替代 OS 沙盘:

  • 墙钟 deadline(vm.Interrupt 中断运行中的代码);
  • 绑定调用次数上限(MaxCalls);
  • 绑定输出累计字节上限(MaxBytes)。

契约(dsh 同款):程序作为 async 函数体执行(顶层 await/return 可用),完成值即 结果且必须 JSON 可序列化;**失败是结果字段不是异常**(Result.Error/ErrorKind, kind 含 abort 语义)——报告失败是调用方的事,不是崩溃路径。沙盘事实(预算余量/ 中断原因/调用清单)与程序成败分开陈述——程序跑通不等于限制都被执行了。

返回值超限不在本包治理:run_program 是装饰链一员,budgetTool 输出预算+spill 存档 +read_spill 取回同一套预算语义自动生效(spec §2「不另造面」)。

执行点与守卫的分工

程序里的 host.<tool>(...) 与普通工具调用**走同一套 invoke.Stage 判定环** (见 Options.Stages)。此前两者是分开的,于是本包成了整条链上唯一绕过披露门、 审计门的路径——而本包恰恰是对外推荐的那条路,于是「看起来更安全的路径反而最不安全」。

**出站守卫(netguard)不在判定环上,这是有意的。** 判定环判的是「这次调用要不要 放行」,netguard 判的是「这个 URL 能不能发出去」,是两个执行点。把 URL 守卫塞进 逐调用的判定环,就得去猜「哪个参数字段是 URL」——那是领域知识,内核不该有。

两者能这么分工,靠的是一条沙箱性质:**程序本身够不着网络**(无 fetch / XMLHttpRequest / WebSocket / require / process …),出站只可能发生在宿主绑定的实现 里,而那是宿主自己的代码、由宿主自己接 netguard。这条性质由 sandbox_test.go 的 用例钉住:谁给 VM 加了 fetch,谁就得重新回答「netguard 该接在哪」。

子进程执行后端:把程序放到**宿主进程之外**跑,用内核兜住资源。

它解决什么

进程内 goja VM(见 ptc.go)跑在宿主进程里,而三闸(墙钟 / 调用数 / 输出字节) **没有内存闸**——Go 运行时没有「只给一个 VM 限内存」的开关,goja 公开 API 也没有内存限制(只有 SetMaxCallStackSize,那是栈深)。故一个快速分配的程序 能在 deadline 到点**之前**把宿主进程撑爆,而宿主里还跑着编排引擎、事件总线、 别的环节。

本后端把执行搬出宿主进程:子进程里跑 goja,用**内核的地址空间上限** (RLIMIT_AS)兑现内存上限。程序被内核杀掉时,父侧从管道 EOF 认出 「执行域异常终止」——不是「程序返回空」。

两条通道 × 四个单向管道

启动通道:父发 Start → 子;子回 Result → 父
调用通道:子发 Request → 父;父回 Response → 子

**两条通道必须是两条连接**(见 guardx/wire 的包注释)。又因为 `os.Pipe` 是**单向**的,故每条通道用两条单向管道——共 4 条。 用 socketpair 能省成 2 条,但那是 Unix 专有;`os.Pipe` 跨平台。

文件描述符分配:

子进程:fd0=启动读  fd1=启动写  fd3=调用请求写  fd4=调用响应读
父进程:启动写 / 启动读 / 调用请求读 / 调用响应写

判定环留在父侧

程序里的 host.<tool>() 与普通工具调用走**同一套** invoke.Stage 判定环—— 而判定环与宿主工具都在父侧。子进程里只有一个「发请求」的桩,判定在父侧做。 故本后端**不弱化**门禁:子进程形态下判定环照走。

Index

Constants

View Source
const (
	StatusOK     = "ok"     // 正常返回
	StatusError  = "error"  // 执行体报错
	StatusDenied = "denied" // 被判定环拒绝,**未触达执行体**
)

CallRecord 的 Status 取值。

View Source
const (
	DefaultTimeout  = 30 * time.Second
	DefaultMaxCalls = 50
	DefaultMaxBytes = 256 << 10 // 256KB:程序可归并的原始数据合理上限(超限=该缩小采集)
)

三预算缺省(conf ptc.* 覆盖;run_program 构造时注入)。

View Source
const ProviderID = "ptc-goja"

ProviderID 本包执行面在 sandbox 登记簿里的标识。

View Source
const SubprocessProviderID = "ptc-subprocess"

SubprocessProviderID 子进程后端的标识(登记在 policy 的 sandbox_providers 段)。

Variables

View Source
var ErrInvalidSignature = errors.New("绑定调用形态非法")

ErrInvalidSignature 形态串非法(装配期缺陷,非程序问题)。

View Source
var ErrMemoryLimitUnsupported = errors.New("ptc: 本环境无法施加内存上限")

ErrMemoryLimitUnsupported 本环境无法施加内存上限。

它是**独立**于「执行域异常终止」的一类失败:前者是「这个环境做不到」 (宿主该换环境或降要求),后者是「执行域中途没了」(可能是兜底生效了)。 合成一类的话,宿主只能一律当故障处理。

Functions

func MaybeRunChild

func MaybeRunChild()

MaybeRunChild 若本进程是执行沙箱的子进程,则跑完子进程主循环并退出; 否则直接返回,宿主继续正常启动。

**必须放在宿主 main 的最前面**:命中时它会 os.Exit,宿主其余启动逻辑不执行。

func MemoryLimitSupported

func MemoryLimitSupported() bool

MemoryLimitSupported 报告**当前环境**能否施加内存上限(cgroup v2 的 memory.max)。

它是**探测**出来的,不是按平台猜的:同一个 Linux 机器,有 cgroup v2 且可写的 就能;容器里没委派 memory 控制器就不能;非 Linux 一律不能。

宿主为什么需要它

宿主要做「档位登记」——它得知道**当前环境**这个后端兜不兜得住, 否则会在登记簿里选中一个兜不住的后端,然后在运行期收到 「选了它却用不了」这种最难排查的失败。

这与「按平台猜」的写法的差别是实测出来的:同一台 Linux 机器, 换个容器跑(有没有委派 memory 控制器)结论就不同。

func WithEnv

func WithEnv(ctx context.Context, env RunEnv) context.Context

WithEnv 注入 PTC 环境(runner 装配点调用)。

Types

type Binding

type Binding struct {
	Name string
	// Signature JS 调用形态,如 "get_logs(host, count)"。由宿主用自己的工具目录
	// 翻译而来——ptc 不认识任何 schema 类型。
	Signature string
	Call      func(ctx context.Context, argsJSON string) (string, error)
}

Binding 一个宿主函数绑定(工具即绑定):Call 收程序传来的 JSON 参数文本,回工具 输出原文(不截断——与模型直读同一信任级;注入嫌疑扫描在装饰链上照常生效)。

Signature 可选,给出后模型按普通函数调用而不必手写 JSON:`get_logs(host, count)` 会被挂成 `host.get_logs(host, count)`,ptc 负责把形参打包成 JSON 再交给 Call。 为空则退化为 `host.<name>(argsJSON)`,与既有行为逐字一致。详见 signature.go。

type CallRecord

type CallRecord struct {
	Name   string `json:"name"`
	Status string `json:"status"` // ok|error
	Bytes  int    `json:"bytes"`
	MS     int64  `json:"ms"`
	Error  string `json:"error,omitempty"`
}

CallRecord 一次绑定调用记账(ptc_run 明细事件与审计的账目面:程序把大输出「洗」成 小结果时,调用与返回字节数在此不缺)。

type Discovered

type Discovered struct {
	Name        string `json:"name"`
	Signature   string `json:"signature,omitempty"`
	Description string `json:"description,omitempty"`
}

Discovered 程序内发现到的一个工具。

type Discoverer

type Discoverer func(query string) []Discovered

Discoverer 程序内的工具发现入口。

为什么需要它

渐进披露(tooldisclose 那种检索换 schema)与程序化执行此前是**两半没接上**的: 检索在模型面(下一轮对话里查),程序在程序面(只能用装配时给的那张表)。 模型想在程序里换个工具,得退回一轮 ReAct——Code Mode 收益被抵掉一半。

宿主把自己的检索器接进来,程序内即可 discover("重启")。ptc 不依赖任何目录包: 「工具目录长什么样」是宿主的模型,不是内核的。

type ExecutionAborted

type ExecutionAborted struct {
	// Err 底层错误(管道 EOF 等)。
	Err error
	// Exit 子进程退出状态描述(如 "signal: killed" / "exit status 1")。
	Exit string
	// Signaled 子进程是否被**信号**终止(被内核杀掉)。
	//
	// Unix 上被信号终止时 `ExitCode()` 返回 -1;Windows 上退出码恒为实际值。
	// 故这个判据跨平台成立,不需要按平台分实现。
	Signaled bool
	// Stderr 子进程 stderr 的尾部(崩溃原因就在这里),**已脱敏**。
	//
	// 没有它,宿主只看到一个退出码——而退出码分不清「被内核杀掉(兜底生效)」
	// 与「自己 fatal 退出(启动失败或 runtime 报错)」。这是**死因**,不是日志。
	//
	// 为什么是脱敏后的:子进程常把连接串、API key、token 打进 stderr
	// (MCP server 启动时的配置回显最常见),而这段文本会**原样**进诊断报告
	// 与错误消息——那等于凭据泄漏面。脱敏用上游 guardx/logredact(凭据形态打码),
	// 本仓 pii 管的是个人信息,两者互补不重叠。
	//
	// 脱敏发生在**写入这个字段时**(不是 Error() 渲染时):字段是公开的,
	// 宿主可以直接读 e.Stderr——只在渲染面脱敏等于给宿主留了原文出口。
	Stderr string
}

ExecutionAborted 执行域异常终止:子进程没给出结果就结束了。

为什么把它做成结构化错误,而不是一句文案

「子进程没了」有**两种完全不同的原因**,宿主对它们的处置也不同:

  • **被内核杀掉**(信号终止):多半是它申请了超限资源——内存兜底**生效了**。 这是**预期行为**,不是故障;宿主该记一笔「兜底起作用了」。
  • **自己退出**(非零退出码):子进程**启动失败**——设不上地址空间上限、 fd 无效、协议不匹配。这是**故障**,宿主该报「这个环境不支持这种执行形态」。

只回一句「执行域异常终止」的话,宿主分不出这两者,只能一律当故障处理—— 于是「兜底生效」这种正常结局会被误报成故障,而真正该修的启动失败被淹没。

为什么父侧不能先 Kill 再取状态

我们自己 Kill 掉的子进程与被内核杀掉的子进程,退出状态**都是**「信号终止」。 只有当子进程**自己先结束**时,取到的才是真死因。故取状态必须**先于**收刀 (见 subprocess.go 的收尾顺序)。

func (*ExecutionAborted) Error

func (e *ExecutionAborted) Error() string

Error 实现 error。

func (*ExecutionAborted) Unwrap

func (e *ExecutionAborted) Unwrap() error

Unwrap 让调用方能取到底层错误(管道 EOF)。

type Options

type Options struct {
	Timeout  time.Duration
	MaxCalls int
	MaxBytes int
	// Discover 可选:暴露程序内工具发现入口 discover(query)。
	// 为 nil 则**根本不暴露**该全局——没装配的东西不该以空壳形式出现在沙箱里,
	// 那会让模型以为「没有别的工具了」而放弃检索。
	Discover Discoverer
	// Stages 每次绑定调用前必过的判定环,与普通工具调用走的是**同一套** Stage。
	//
	// # 为什么这条是必需的而不是可选优化
	//
	// 程序里的 host.<tool>(...) 直达宿主工具。若这里不接判定环,本包就成了
	// 「整条链上唯一绕过所有门禁的路径」:披露门不判、审计门不判、SSRF 出站门不判。
	// 而本包恰恰是对外推荐的那条路(N 轮 ReAct 塌缩成 1 轮、原始数据不过模型面),
	// 于是**看起来更安全的路径反而最不安全**——这正是 OpenCode 2.0 #52333 的形状:
	// 信任边界移动了,沙箱没跟着移动。
	//
	// 为 nil 时不接任何判定,行为与本包引入 Stages 之前逐字一致(不破坏既有调用方),
	// 但那条路径因此**不带门禁**——接入方须自行确认这是有意的。
	Stages []invoke.Stage
}

Options 三预算闸(≤0=各自缺省)。

type Position

type Position struct {
	Line   int
	Column int
}

Position 程序内的出错位置。Line/Column 均为 1-based;零值表示位置不可得。

func (Position) Describe

func (p Position) Describe() string

Describe 位置的模型可读渲染:「第 N 行第 M 列」(1-based)。 位置不可得(零值)时返回空串——调用方据此省略位置段,而不是渲染出 「第 0 行」这种假位置(见本文件头:假位置比没有位置更坏)。

func (Position) Valid

func (p Position) Valid() bool

Valid 位置是否可得。

type Provider

type Provider struct {
	// Bindings 挂给程序的宿主绑定。
	Bindings []Binding
	// Discover 程序内工具发现入口(nil = 不暴露该全局)。
	Discover Discoverer
	// Stages 每次绑定调用前必过的判定环(nil = 不接判定)。
	Stages []invoke.Stage
}

Provider 把本包的执行面适配成 sandbox.Provider。

为什么适配器在本包而不是 sandbox 包

适配器要调 Run,而 Run 在本包。放进 sandbox 就成了 sandbox → ptc → sandbox 的 环(Go 的 import 环直接编译不过)。方向只能是 ptc → sandbox: **沙箱机制不认识任何后端,后端认识沙箱**。

档位为什么是 LevelProcess 而不是 LevelOS

goja 是**进程内解释器**:能力面靠 JS 子集 + 绑定白名单裁剪,资源面靠三重预算, 但这层约束由解释器自己执行,不是内核强制的。解释器自身的缺陷可以越过它。

故诚实的档位是 LevelProcess。把它标成 LevelOS 就是让档位表说谎—— 而档位表是宿主做风险判断的依据,说谎的成本落在宿主身上。

func (Provider) Exec

Exec 执行一次程序。

失败是结果字段不是异常:sandbox.Failure 带回本包的失败分类(ErrorKind), 调用方据此区分「程序写错了」与「门禁拒了」——这两者给出的处置完全不同, 合成一条 runtime 的话,调用方会去改那段本来没问题的程序。

func (Provider) ID

func (p Provider) ID() string

ID 后端标识。

func (Provider) Level

func (p Provider) Level() sandbox.Level

Level 本包提供的隔离档位。

type Result

type Result struct {
	OK        bool
	Value     string // return 值的 JSON 文本(成功时恒合法 JSON,undefined→"null")
	Error     string
	ErrorKind string
	// Location 出错位置(1-based 行列)。取不到时为零值——不猜:假的行号会让模型
	// 去改一行根本没问题的代码,比没有行号更坏。
	Location Position
	// Suggestions 面向模型的修复建议,由 ErrorKind 唯一决定(成功时为 nil)。
	Suggestions []string
	Calls       int
	Bytes       int
	CallLog     []CallRecord
	// contains filtered or unexported fields
}

Result 执行结果:失败是结果字段不是异常。ErrorKind: ""=成功 / deadline / budget_calls / budget_bytes / runtime(JS 异常或拒绝)/ pending(await 永不落定——VM 无定时器,此类程序永不完成)/ bad_result(return 值 不可 JSON 序列化)/ syntax(程序体语法错误,预检期即拒,带 goja 行号)/ binding_collision(绑定撞名,见 findBindingCollision)/ stage_denied(判定环判定拒绝)/ stage_failed(判定环自身故障,见 stageOutcome)。

stage_denied 与 stage_failed 分开不是分类洁癖:前者是**正常裁决**,程序改不掉它, 只能换工具或走审批;后者是**宿主门禁自己坏了**,程序更不该改它。合成一个 runtime 的话,调用方会去改那段本来没问题的程序,而真正该修的是守卫代码。

Location 与 Suggestions 是给**模型**读的诊断面:调用方是模型,只回一个类别串 对它几乎不可用——它不知道该改哪一行,也不知道该改成什么。详见 diagnostic.go。

func Run

func Run(ctx context.Context, program string, bindings []Binding, opts Options) (res Result)

Run 执行一段 PTC 程序:program 作 async 函数体(顶层 await/return 可用)。 ctx 取消=中断(ErrorKind=runtime 包 ctx 错误语义);deadline 到点=中断。 恒返回 Result 不 panic——失控循环由墙钟闸治理(spec §5)。

⚠️ 本函数是**裸执行入口**:它不经 `sandbox` 登记簿

直调它会**同时**跳过三层强制:

  • **档位下限**——`Registry.Resolve` 选不到满足要求的后端就拒绝;
  • **预算可兑现性**——兜不住内存的后端不被选中;
  • **审批门**——`gatecheck.Managed` 那层(将来接上时)。

声明还在、强制没了,而**门禁全绿**——说谎的不是档位表,是**执行路径**。

故**生产代码请走登记簿**:

sandbox.Execute(ctx, reg, 要求的档位, sandbox.Request{Program: program, Budget: b})

其中后端是 `ptc.Provider`(本包的执行面适配成 `sandbox.Provider` 协议)。 本仓的门禁 `harness-sandbox-execution-path` 会把生产代码里直调本函数的 调用点报出来(判定面:限定符调用;改 import 别名绕不过去)。

本函数**仍然公开**:它是「不经登记簿的进程内执行」这个能力本身, 测试与一次性脚本可以直接用;只是**生产路径**不该走它。

⚠️ **内存不受三闸治理**(此处此前写的是「失控循环/内存风险由三闸治理」,是错的): 三闸是墙钟 / 绑定调用数 / 输出字节数,**没有内存闸**;而 goja 的公开 API 也没有任何内存限制(只有 SetMaxCallStackSize,那是调用栈深度)。 故一个快速分配的程序能在 deadline 到点**之前**把宿主进程撑爆——宿主里还跑着 编排引擎与别的环节。兜住它只能把执行搬出宿主进程(跨域执行), 设计见 docs/SANDBOX-DESIGN-20261021.md。

绑定调用 deadline 语义(批三百零七,P2):阻塞中的 Go 绑定调用(如变更类工具的 审批等待)vm.Interrupt 打不断——中断驱动 goroutine 先 cancel 运行期 ctx 再 Interrupt,绑定调用带 runCtx 出去:墙钟到点阻塞调用被 ctx 断掉(CallRecord 留 error 痕),程序随后被 Interrupt 收口。审批挂起=终止+已执行步骤留痕(spec §4 P2 取舍:暂停恢复复杂度不值)。

func (Result) Describe

func (r Result) Describe() string

Describe 把结果渲染为**模型可读**的诊断文本(宿主回喂面)。

本包的调用方是模型:失败只回一个 ErrorKind 类别串,模型不知道改哪、怎么改。 本方法把 Result 的诊断面折成一段纯文本,供宿主在「程序修复回喂环」里原样 塞回下一轮程序合成(诊断措辞与 harness 的分类口径单源,宿主不再自写渲染—— argus internal/qa 的 describePtcFailure 是首笔收编)。

各段:

  • 失败:类别、错误原文、位置(可得时才有此行,见 Position)、修复建议、 调用链摘要(哪些绑定调用失败——归因「程序的问题还是工具的问题」的账面);
  • 成功但 Value=="null":点破「没有 return 值」(undefined 折算为 null 是 本包契约)——「忘写 return」是模型侧高频缺陷,不点破它会把白跑当完成;
  • 其余成功:一行摘要。

建议优先取 Result.Suggestions(Run 已按 ErrorKind 填好,宿主可增补自己的 上下文建议);为空时按 ErrorKind 补查建议表——手工构造的 Result 也能渲染出 完整建议,两个来源同一张表,措辞不会漂移。

文本面向模型,不面向运维:预算余量、耗时等沙盘事实不在其中(Result 字段 本身就是审计面)。本方法不携带宿主策略(是否重试、是否回退),那是调用方 的裁决。

type RunEnv

type RunEnv struct {
	Table []ToolTableEntry
	OnRun func(Result)
}

RunEnv 一次运行的 PTC 环境(批三百零五 ctx 缝):runner 在装配完成点(全部工具追加 之后)把本环节模型可见的最终工具表快照进 ctx——「程序内可达=本环节模型可达」恒等式 由快照时点保证,不在组合根持全局表(per-run 表只活在当次 Run 里)。run_program 于 调用期取出构绑定;OnRun=ptc_run 明细事件回调(nil=不发)。

func EnvOf

func EnvOf(ctx context.Context) RunEnv

EnvOf 取 PTC 环境(零值=未装配——run_program 如实回包不假跑)。

type SubprocessOptions

type SubprocessOptions struct {
	// Program 被执行的程序体(由子进程里的 goja 解释)。
	Program string
	// Bindings 宿主工具绑定。**实现留在父侧**——子进程只拿到名字与调用形态。
	Bindings []Binding
	// Stages 每次绑定调用前必过的判定环(与普通工具调用同一套)。
	Stages []invoke.Stage
	// Timeout 墙钟期限。<=0 用 DefaultTimeout。
	Timeout time.Duration
	// MaxCalls 调用数上限。<=0 用 DefaultMaxCalls。
	MaxCalls int
	// MaxBytes 输出字节上限。<=0 用 DefaultMaxBytes。
	MaxBytes int
	// MaxMemory 内存上限(字节)。<=0 = 不要求。
	MaxMemory int64
	// Discover 可选:程序内工具发现入口。
	Discover Discoverer
	// Egress 子进程的出口要求(零值 = 不限制,行为与旧版逐字一致)。
	//
	// 为什么这个字段必须在**配置**上而不是靠子进程自己读环境:子进程起来之后
	// 它的出站流量完全在本进程 netguard 之外,父侧唯一能施加影响的时点就是
	// spawn 之前。装配(谁决定这一档)归宿主;兑现与判据归 egress。
	Egress egress.Spec
	// EgressBackend 兑现 Egress 声明的那个对象(通常是宿主起的强制代理)。
	//
	// # 为什么必须有这一格
	//
	// Egress 只写「要求哪一档」,它本身不会跑到任何地方去。而在 spawn 之前,
	// 父侧唯一能做的事就是问一句「**你**能兑现这一档吗」——没有这一问,
	// 声明与执行之间就没有任何信号:宿主写了「只经代理」,子进程照样裸奔出网,
	// 而报告里那一条显示「已声明」。
	//
	// # nil 的含义是「没人声明」,不是「不限制」
	//
	// 不填它而 Egress 又要求了限制 ⇒ **拒绝执行**(fail closed)。安全判据里
	// 沉默必须按最弱处理:「没人反对」不等于「做得到」。不要求限制时它不被问。
	//
	// 判据在 egress.Spec.SatisfiedBy,本字段只负责把兑现者带到位——判定与
	// 声明面同源,不在本包另立一套。
	EgressBackend egress.Enforcer
	// ExePath 子进程可执行文件路径。空 = 用宿主自己的可执行文件(re-exec)。
	//
	// **re-exec 宿主自己的二进制**是默认:随包发小二进制要宿主额外安装,
	// 接入成本高一个量级。代价是宿主要在 main 里加一行 `ptc.MaybeRunChild()`。
	ExePath string
}

SubprocessOptions 子进程后端的配置。

type SubprocessProvider

type SubprocessProvider struct {
	// contains filtered or unexported fields
}

SubprocessProvider 子进程执行后端,实现 sandbox.Provider。

档位仍是 **LevelProcess**:能力面没变(还是 JS 子集 + 绑定白名单), 变的是**资源面**——子进程 + 内核上限比进程内强。这正是 Capable 存在的理由: 档位(能力面)与支持面(资源兜底兑现哪几项)是**两个正交维度**。

func NewSubprocessProvider

func NewSubprocessProvider(opts SubprocessOptions) *SubprocessProvider

NewSubprocessProvider 构造子进程后端。

func (*SubprocessProvider) Exec

Exec 实现 sandbox.Provider:起子进程、跑一次程序、收结果。

func (*SubprocessProvider) ID

func (p *SubprocessProvider) ID() string

ID 实现 sandbox.Provider。

func (*SubprocessProvider) Level

func (p *SubprocessProvider) Level() sandbox.Level

Level 实现 sandbox.Provider。

**LevelProcess 而不是 LevelOS**:本后端没有内核级的文件/网络裁剪—— 能力面与进程内解释器同级,强的只是资源兜底那一维。标成 LevelOS 就是让 档位表说谎,而档位表是宿主做风险判断的依据。

func (*SubprocessProvider) Supports

func (p *SubprocessProvider) Supports(b sandbox.Budget) bool

Supports 实现 sandbox.Capable。

本后端**能兑现内存上限**(子进程 + cgroup v2 的 memory.max),故 MaxMemory > 0 时也支持。 进程内后端**不实现** Capable(兜不住),两者在登记簿里由此区分。

**判据是「当前环境兜不兜得住」,不是「配没配上限」**: 要求了内存上限而当前环境建不出带 memory.max 的 cgroup(如 macOS—— 它没有 cgroup,seatbelt 裁的是能力面不裁内存)⇒ **不支持**。 若这里报支持,登记簿就会选中本后端,然后 Exec 里因建不出 cgroup 而失败—— 宿主等到的却是「选了它却用不了」这种最难排查的形态。

方向是**保守的**:兜不住就报兜不住,不报「大概能兜」。

type ToolTableEntry

type ToolTableEntry struct {
	Name string
	Run  func(ctx context.Context, args string) (string, error)
}

ToolTableEntry 一个可绑定工具:Name=工具名(JS 侧 host.<净化名>),Run=调用 (args=JSON 文本,回工具输出原文)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL