feat(security): 代审口径 A 落地(仅子代理发起的敏感调用做实质判定 + fail-closed + 理由进审计与小字)
This commit is contained in:
1 parent
fa850f9599
commit
20160c66d8
13 files changed
+1479
-83
No files matched your search
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/approvalreview"
|
||||
)
|
||||
|
||||
// 本文件是 design/11 §4「敏感操作代审」在组合根上的全部接线(裁决 13.4.3 +
|
||||
// 主人 2026-10-05 裁决的口径 A):
|
||||
// - 装配**实质判定**(是否子代理发起、判「有没有越界」、fail-closed);
|
||||
// - 构建代审门与其归属解析;
|
||||
// - 构建 loop 的准入钩子,把代审记录交网关落 audit_logs 并广播前端小字,
|
||||
// 并让判定理由出现在那行小字里。
|
||||
//
|
||||
// 判定本身与拒绝语义在 audit.Approver(边界判定、fail-closed)与
|
||||
// services/approvalreview(轻量审查模型调用)。
|
||||
|
||||
// approvalJudgeFor 构建敏感操作代审的实质判定器:复用既有审查模型范式
|
||||
// (design/12 §6 策略四的轻量审查模型,见 services/approvalreview 与
|
||||
// wire_channels.go 的 reviewFor),注入**同一个** model.Adapter,不另造 LLM 包装。
|
||||
//
|
||||
// 无模型适配器时返回 nil:门对**子代理**的敏感调用随即 fail-closed(判不出来不放行),
|
||||
// 而主会话自身的调用不受影响(口径 A 只管子代理)。
|
||||
func approvalJudgeFor(llm model.Adapter, log *slog.Logger) audit.CallJudge {
|
||||
if llm == nil {
|
||||
return nil
|
||||
}
|
||||
return approvalreview.NewLLMJudge(llm, logger.WithSource(log, "services/approvalreview"))
|
||||
}
|
||||
|
||||
// approvalExplainWithReason 把小字与判定理由拼在一起(design/11 §4 的固定句式在前,
|
||||
// 理由作括号后缀在后),使前端那行小字能说清「为什么被拒」。reason 为空时原样返回,
|
||||
// 因此放行与未判定的路径与加入判定之前逐字一致。
|
||||
func approvalExplainWithReason(explain, reason string) string {
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
return explain
|
||||
}
|
||||
return explain + "(" + reason + ")"
|
||||
}
|
||||
|
||||
// buildApprovalGate 构建敏感操作代审门(design/11 §4;裁决 13.4.3)。
|
||||
//
|
||||
// 归属解析取 Run 上下文里的会话 id 与该 turn 的角色名,使前端小字既投到
|
||||
// 正确的会话、也写明是谁想动手(design/11 §4 的「xxx(角色名)」)。会话 id 用
|
||||
// loop.FlowOwnerFromContext:子代理的敏感调用归属到**派生它的那个会话**
|
||||
// (与 design/19 §2 的 flow 归属同一口径),所以小字出现在主人正在看的对话里,
|
||||
// 而不是一个没人查看的子代理 id 下。
|
||||
func (a *app) buildApprovalGate(approver *audit.Approver) *restrict.Gate {
|
||||
return restrict.NewGate(
|
||||
auditReviewer{approver: approver},
|
||||
func(ctx context.Context) (string, string) {
|
||||
return loop.FlowOwnerFromContext(ctx), a.approvalActor(ctx)
|
||||
},
|
||||
restrict.WithGateLogger(a.log),
|
||||
)
|
||||
}
|
||||
|
||||
// toolAdmissionHooks 构建 loop 的工具准入钩子(design/11 §4;裁决 13.4.3):
|
||||
// 敏感调用先经主代理代审,代审记录交给网关落 audit_logs 并广播前端小字;
|
||||
// 未获代审的调用以软结果回灌给模型(绝不执行,绝不静默)。非敏感调用直接放行。
|
||||
//
|
||||
// 只装 CallAdmit:敏感与否取决于**该次调用的参数**(file 的 read 自主、
|
||||
// write 代审),批量钩子(BatchAdmit)看不到参数,做不出这个区分。
|
||||
// 组合根按 loop 的既有契约装配,因此这条链也覆盖子代理的敏感调用
|
||||
// (编排层派生的 loop 共享同一组钩子)——而那正是口径 A 的判定对象。
|
||||
func (a *app) toolAdmissionHooks(gate *restrict.Gate) loop.ToolLoopHooks {
|
||||
return loop.ToolLoopHooks{
|
||||
CallAdmit: func(ctx context.Context, call agent.ToolCall) *loop.ToolCallVeto {
|
||||
// 判定理由经按调用的理由盒带出来:代审门的 Reviewer 契约只回
|
||||
// (allowed, error),而小字要说清「为什么被拒」(口径 A)。
|
||||
ctx, note := audit.WithDecisionNote(ctx)
|
||||
decision := gate.Admit(ctx, call.Name, call.Args)
|
||||
if decision.Approval != nil {
|
||||
decision.Approval.Explain = approvalExplainWithReason(decision.Approval.Explain, note.Reason())
|
||||
// a.gw 在 wireFrontend 才建好;RecordApproval 对 nil 接收者安全。
|
||||
a.gw.RecordApproval(ctx, *decision.Approval)
|
||||
}
|
||||
if !decision.Allow {
|
||||
return &loop.ToolCallVeto{Block: true, Reason: decision.Reason}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// auditReviewer 把 orchestration/audit 的 Approver 适配成代审门的 Reviewer
|
||||
// (design/11 §4;裁决 13.4.3 要求复用该 Approver,而不是另起一套代审)。
|
||||
// 它把 Approval 收敛成 (allowed, error),并把被拒时的判定理由写进 ctx 上的
|
||||
// 理由盒(audit.DecisionNote),使冻结的 Reviewer 契约之外仍有一条结构化通道
|
||||
// 把「为什么被拒」送到前端小字。前端小字文案本身仍由 restrict 复用
|
||||
// audit.Approval.Explain 的固定句式产出。
|
||||
type auditReviewer struct{ approver *audit.Approver }
|
||||
|
||||
// ReviewAndApprove 执行一次代审;approver 缺席时复用编排层的 ErrNoApprover
|
||||
// (门据此 fail-closed 拦截敏感操作,绝不静默放行)。
|
||||
func (r auditReviewer) ReviewAndApprove(ctx context.Context, actor, action, describe string) (bool, error) {
|
||||
if r.approver == nil {
|
||||
return false, fmt.Errorf("review-and-approve: %w", orchestration.ErrNoApprover)
|
||||
}
|
||||
ap, err := r.approver.ReviewAndApprove(ctx, actor, action, describe)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("review-and-approve: %w", err)
|
||||
}
|
||||
if !ap.Allowed {
|
||||
audit.DecisionNoteFrom(ctx).SetReason(ap.Reason)
|
||||
}
|
||||
return ap.Allowed, nil
|
||||
}
|
||||
|
||||
// approvalActor 解析代审小字里的「谁想动手」:该 turn 绑定的角色名,
|
||||
// 未绑定时回落到默认角色;角色名解析不出来就用角色 id(小字宁可用 id
|
||||
// 也要说清是谁,但绝不编造一个名字)。
|
||||
func (a *app) approvalActor(ctx context.Context) string {
|
||||
pid := loop.SessionEnvFrom(ctx).PersonaID
|
||||
if pid == "" {
|
||||
pid = a.defaultPersonaID()
|
||||
}
|
||||
if name := a.personaName(pid); name != "" {
|
||||
return name
|
||||
}
|
||||
return pid
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit"
|
||||
)
|
||||
|
||||
// 本文件锁定组合根上口径 A(主人 2026-10-05 裁决)的接线:
|
||||
// - 子代理发起的敏感调用 ⇒ 进判定,判越界即拦下(绝不放行、绝不静默);
|
||||
// - 主会话自身的敏感调用 ⇒ 不进判定(零模型调用)、照旧放行;
|
||||
// - 判定理由经按调用的理由盒带出代审门,最终出现在前端小字里。
|
||||
|
||||
// stubJudge 是代审判定器桩:记录调用次数并给出既定结论。
|
||||
type stubJudge struct {
|
||||
verdict audit.JudgeVerdict
|
||||
err error
|
||||
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
}
|
||||
|
||||
func (j *stubJudge) JudgeSubagentCall(context.Context, audit.JudgeRequest) (audit.JudgeVerdict, error) {
|
||||
j.mu.Lock()
|
||||
j.calls++
|
||||
j.mu.Unlock()
|
||||
return j.verdict, j.err
|
||||
}
|
||||
|
||||
func (j *stubJudge) callCount() int {
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
return j.calls
|
||||
}
|
||||
|
||||
// noteSpy 包住真实的代审者,读出代审链写进 ctx 理由盒的内容——它就是
|
||||
// 「为什么被拒」带出冻结契约的那条通道的证据。
|
||||
type noteSpy struct {
|
||||
inner restrict.Reviewer
|
||||
mu sync.Mutex
|
||||
seen string
|
||||
}
|
||||
|
||||
func (s *noteSpy) ReviewAndApprove(ctx context.Context, actor, action, describe string) (bool, error) {
|
||||
allowed, err := s.inner.ReviewAndApprove(ctx, actor, action, describe)
|
||||
s.mu.Lock()
|
||||
s.seen = audit.DecisionNoteFrom(ctx).Reason()
|
||||
s.mu.Unlock()
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
func (s *noteSpy) reason() string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.seen
|
||||
}
|
||||
|
||||
// terminalCall 是一次真实的敏感调用(exec command 类,见 restrict.SensitiveCall)。
|
||||
func terminalCall() agent.ToolCall {
|
||||
return agent.ToolCall{Name: "terminal", Args: map[string]any{"command": "rm -rf /tmp/x"}}
|
||||
}
|
||||
|
||||
// admit 用一条生产同形的准入链跑一次调用。
|
||||
func admit(t *testing.T, ctx context.Context, judge audit.CallJudge, call agent.ToolCall) (*loop.ToolCallVeto, string) {
|
||||
t.Helper()
|
||||
if _, _, sensitive := restrict.SensitiveCall(call.Name, call.Args); !sensitive {
|
||||
t.Fatalf("%s 不是敏感调用,用例前提不成立", call.Name)
|
||||
}
|
||||
approver := audit.NewApprover(testLog(),
|
||||
audit.WithSubagentOrigin(loop.SubagentTaskFrom),
|
||||
audit.WithCallJudge(judge),
|
||||
)
|
||||
spy := ¬eSpy{inner: auditReviewer{approver: approver}}
|
||||
gate := restrict.NewGate(spy, func(context.Context) (string, string) { return "web:main", "昔涟" })
|
||||
a := &app{log: testLog()}
|
||||
return a.toolAdmissionHooks(gate).CallAdmit(ctx, call), spy.reason()
|
||||
}
|
||||
|
||||
// TestToolAdmission_SubagentOutOfScopeIsBlockedWithReason:子代理发起的敏感调用被
|
||||
// 判定为越界 ⇒ 拦下(软结果回灌,绝不执行),且理由经理由盒带出代审门(会进小字)。
|
||||
func TestToolAdmission_SubagentOutOfScopeIsBlockedWithReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
judge := &stubJudge{verdict: audit.JudgeVerdict{Allowed: false, Reason: "任务只要求整理导出文件,不需要执行 shell 命令"}}
|
||||
ctx := loop.WithSubagentTask(context.Background(), "整理 data/ 下的导出文件")
|
||||
|
||||
veto, reason := admit(t, ctx, judge, terminalCall())
|
||||
if veto == nil || !veto.Block {
|
||||
t.Fatalf("veto = %+v, want 拦下(越界的敏感调用绝不执行)", veto)
|
||||
}
|
||||
if !strings.Contains(veto.Reason, "not_approved") {
|
||||
t.Fatalf("回灌给模型的原因 = %q, want 既有 not_approved 体例", veto.Reason)
|
||||
}
|
||||
if judge.callCount() != 1 {
|
||||
t.Fatalf("判定调用次数 = %d, want 1", judge.callCount())
|
||||
}
|
||||
if !strings.Contains(reason, "不需要执行 shell 命令") {
|
||||
t.Fatalf("理由盒 = %q, want 判定给出的理由(小字要说清为什么被拒)", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolAdmission_SubagentInScopePasses:判定放行的子代理敏感调用照常执行。
|
||||
func TestToolAdmission_SubagentInScopePasses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
judge := &stubJudge{verdict: audit.JudgeVerdict{Allowed: true}}
|
||||
ctx := loop.WithSubagentTask(context.Background(), "清理临时目录 /tmp/x")
|
||||
|
||||
veto, reason := admit(t, ctx, judge, terminalCall())
|
||||
if veto != nil {
|
||||
t.Fatalf("veto = %+v, want 放行", veto)
|
||||
}
|
||||
if judge.callCount() != 1 {
|
||||
t.Fatalf("判定调用次数 = %d, want 1", judge.callCount())
|
||||
}
|
||||
if reason != "" {
|
||||
t.Fatalf("放行时理由盒 = %q, want 空(小字不带理由)", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolAdmission_MainSessionIsNotJudged:主会话自身的敏感调用**不进判定**
|
||||
// (零模型调用)且放行——主人自己显式要求的操作(例如读 models.json / cyrene.db)
|
||||
// 绝不能被这套判定拦死(口径 A 的另一半)。
|
||||
func TestToolAdmission_MainSessionIsNotJudged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
judge := &stubJudge{verdict: audit.JudgeVerdict{Allowed: false, Reason: "无论如何都拒绝"}}
|
||||
veto, reason := admit(t, context.Background(), judge, terminalCall())
|
||||
if veto != nil {
|
||||
t.Fatalf("veto = %+v, want 主会话调用放行", veto)
|
||||
}
|
||||
if judge.callCount() != 0 {
|
||||
t.Fatalf("判定调用次数 = %d, want 0(主会话调用零模型调用)", judge.callCount())
|
||||
}
|
||||
if reason != "" {
|
||||
t.Fatalf("理由盒 = %q, want 空", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolAdmission_FailClosedWhenJudgeUnavailable:未装配判定器(或无模型适配器)
|
||||
// 时,子代理的敏感调用一律拦下——绝不因为「没得判」就放行。
|
||||
func TestToolAdmission_FailClosedWhenJudgeUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := loop.WithSubagentTask(context.Background(), "整理导出文件")
|
||||
veto, reason := admit(t, ctx, nil, terminalCall())
|
||||
if veto == nil || !veto.Block {
|
||||
t.Fatalf("veto = %+v, want 拦下(fail-closed)", veto)
|
||||
}
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
t.Fatal("拒绝没有理由(静默拒绝)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalExplainWithReason:判定理由拼进小字固定句式之后;没有理由时逐字不变
|
||||
// (放行与主会话路径的小字与加入判定之前完全一致)。
|
||||
func TestApprovalExplainWithReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fixed := "「昔涟 想要干什么,我审查后拒绝了」"
|
||||
if got := approvalExplainWithReason(fixed, ""); got != fixed {
|
||||
t.Fatalf("无理由时 = %q, want 原样 %q", got, fixed)
|
||||
}
|
||||
if got := approvalExplainWithReason(fixed, " "); got != fixed {
|
||||
t.Fatalf("空白理由时 = %q, want 原样 %q", got, fixed)
|
||||
}
|
||||
got := approvalExplainWithReason(fixed, "这次调用与派给它的任务不一致")
|
||||
want := fixed + "(这次调用与派给它的任务不一致)"
|
||||
if got != want {
|
||||
t.Fatalf("小字 = %q, want %q", got, want)
|
||||
}
|
||||
if !strings.HasPrefix(got, fixed) {
|
||||
t.Fatalf("小字 = %q, want 以 design/11 §4 的固定句式开头", got)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
@@ -275,7 +274,15 @@ func (a *app) wireRuntime() error {
|
||||
// 工具准入门上经主代理代审(复用 orchestration 的 Approver),再由网关把
|
||||
// 结果落进 audit_logs 并广播 design/11 §4 的小字。同一个 Approver 也交给
|
||||
// 编排层(子代理代审),使两条路径共用一处审计。
|
||||
approver := audit.NewApprover(a.log)
|
||||
//
|
||||
// 代审的实质判定(主人 2026-10-05 裁决的口径 A):来源解析读编排层挂在
|
||||
// 子代理 Run 上的任务目标(loop.SubagentTaskFrom,结构化元数据,绝不猜),
|
||||
// 判定器用既有审查模型范式(services/approvalreview,同一个 model.Adapter)。
|
||||
// 两者都不在时保持既有行为:代审只记录、不判定。
|
||||
approver := audit.NewApprover(a.log,
|
||||
audit.WithSubagentOrigin(loop.SubagentTaskFrom),
|
||||
audit.WithCallJudge(approvalJudgeFor(a.modelAdapter, a.log)),
|
||||
)
|
||||
approvalGate := a.buildApprovalGate(approver)
|
||||
|
||||
a.loop = loop.New(loop.Options{
|
||||
@@ -362,80 +369,6 @@ func (a *app) attachToolPolicyStore() {
|
||||
}
|
||||
}
|
||||
|
||||
// buildApprovalGate 构建敏感操作代审门(design/11 §4;裁决 13.4.3)。
|
||||
//
|
||||
// 归属解析取 Run 上下文里的会话 id 与该 turn 的角色名,使前端小字既投到
|
||||
// 正确的会话、也写明是谁想动手(design/11 §4 的「xxx(角色名)」)。会话 id 用
|
||||
// loop.FlowOwnerFromContext:子代理的敏感调用归属到**派生它的那个会话**
|
||||
// (与 design/19 §2 的 flow 归属同一口径),所以小字出现在主人正在看的对话里,
|
||||
// 而不是一个没人查看的子代理 id 下。
|
||||
func (a *app) buildApprovalGate(approver *audit.Approver) *restrict.Gate {
|
||||
return restrict.NewGate(
|
||||
auditReviewer{approver: approver},
|
||||
func(ctx context.Context) (string, string) {
|
||||
return loop.FlowOwnerFromContext(ctx), a.approvalActor(ctx)
|
||||
},
|
||||
restrict.WithGateLogger(a.log),
|
||||
)
|
||||
}
|
||||
|
||||
// toolAdmissionHooks 构建 loop 的工具准入钩子(design/11 §4;裁决 13.4.3):
|
||||
// 敏感调用先经主代理代审,代审记录交给网关落 audit_logs 并广播前端小字;
|
||||
// 未获代审的调用以软结果回灌给模型(绝不执行)。非敏感调用直接放行。
|
||||
//
|
||||
// 只装 CallAdmit:敏感与否取决于**该次调用的参数**(file 的 read 自主、
|
||||
// write 代审),批量钩子(BatchAdmit)看不到参数,做不出这个区分。
|
||||
// 组合根按 loop 的既有契约装配,因此这条链也覆盖子代理的敏感调用
|
||||
// (编排层派生的 loop 共享同一组钩子)。
|
||||
func (a *app) toolAdmissionHooks(gate *restrict.Gate) loop.ToolLoopHooks {
|
||||
return loop.ToolLoopHooks{
|
||||
CallAdmit: func(ctx context.Context, call agent.ToolCall) *loop.ToolCallVeto {
|
||||
decision := gate.Admit(ctx, call.Name, call.Args)
|
||||
if decision.Approval != nil {
|
||||
// a.gw 在 wireFrontend 才建好;RecordApproval 对 nil 接收者安全。
|
||||
a.gw.RecordApproval(ctx, *decision.Approval)
|
||||
}
|
||||
if !decision.Allow {
|
||||
return &loop.ToolCallVeto{Block: true, Reason: decision.Reason}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// auditReviewer 把 orchestration/audit 的 Approver 适配成代审门的 Reviewer
|
||||
// (design/11 §4;裁决 13.4.3 要求复用该 Approver,而不是另起一套代审)。
|
||||
// 它只把 Approval 收敛成 (allowed, error),前端小字文案仍由
|
||||
// restrict 复用 audit.Approval.Explain 的固定句式产出。
|
||||
type auditReviewer struct{ approver *audit.Approver }
|
||||
|
||||
// ReviewAndApprove 执行一次代审;approver 缺席时复用编排层的 ErrNoApprover
|
||||
// (门据此 fail-closed 拦截敏感操作,绝不静默放行)。
|
||||
func (r auditReviewer) ReviewAndApprove(ctx context.Context, actor, action, describe string) (bool, error) {
|
||||
if r.approver == nil {
|
||||
return false, fmt.Errorf("review-and-approve: %w", orchestration.ErrNoApprover)
|
||||
}
|
||||
ap, err := r.approver.ReviewAndApprove(ctx, actor, action, describe)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("review-and-approve: %w", err)
|
||||
}
|
||||
return ap.Allowed, nil
|
||||
}
|
||||
|
||||
// approvalActor 解析代审小字里的「谁想动手」:该 turn 绑定的角色名,
|
||||
// 未绑定时回落到默认角色;角色名解析不出来就用角色 id(小字宁可用 id
|
||||
// 也要说清是谁,但绝不编造一个名字)。
|
||||
func (a *app) approvalActor(ctx context.Context) string {
|
||||
pid := loop.SessionEnvFrom(ctx).PersonaID
|
||||
if pid == "" {
|
||||
pid = a.defaultPersonaID()
|
||||
}
|
||||
if name := a.personaName(pid); name != "" {
|
||||
return name
|
||||
}
|
||||
return pid
|
||||
}
|
||||
|
||||
// buildCapacity / onCapacity 以及 capacity 的 JSON+TTL 解析都在
|
||||
// wire_capacity.go(AGENTS §10 文件 ≤500 行)。
|
||||
|
||||
|
||||
Reference in new issue
Block a user