Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

## Unreleased

- Reflex 编译工具公开完整产物字段和 `js:` 源码约束,回放失败时返回剩余原生调用,语义评审超限时报告各部分大小。纯离线回放在 5 秒后中断空转源码并返回可修复诊断,避免单个草稿占住编译任务。
- 工具返回部分输出时保留失败原因;后台 subagent 仅返回错误时也通知父任务。联网搜索要求实际搜索执行记录,失败时保留后端原因。
- browser、passive 与 Katana 可由入口直接导入组装,`full` 保留为 aiscan 的全量组合;原生 `record` 可独立于 `full` 构建。
- Go 源码 API:工具结果与定义直接使用 `aop.ToolResult` / `aop.ToolDefinition`;Provider 类型和构造函数改从 `agent/provider` 引用,停止原因从 `agent/hooks` 引用。Context hook 直接返回 `[]*aop.Message`,nil 表示不替换。删除重复转导出别名,AOP wire 字段保持兼容。
- JEV 原生调用与契约统一使用 `core/tool.NativeCall`、`NativeContract` 和 `NativeContracts`,访问分类直接使用 `NativeRead` / `NativeEffect` / `NativeUnsupported`;删除重复类型、转换和测试专用回放入口。
- passive 的 `recon` 声明与连接检查迁至 `exts/passive/config`,运行实现不再依赖 scanner engine;CyberHub 索引查询迁至 `tools/cyberhub`。原有配置键与 CLI 参数保持兼容。

## v1.0.0-rc8 — 工作台重构、节点接入与任务复用
Expand Down
4 changes: 4 additions & 0 deletions docs/jev.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ flowchart LR

后台 Claim 生成和编译的单次 LLM 请求使用 30 分钟兜底期限,独立于前台 Provider 的短请求超时。总编译期限 `compilation_timeout` 默认 `0`,允许普通编译 Agent 持续生成、验证和修复;显式配置正值才增加整体兜底。扩展关闭或宿主取消仍立即终止请求。超时用于回收异常挂起,不用于限制正常编译耗时。

每次纯离线回放最多执行 5 秒;它只读取已有结果、枚举有限语义分支,不调用真实工具或模型。生成源码空转或计算超限时返回 `replay_execution_limit`,编译 Agent 可以继续修复;这不改变整体编译期限或真实工具的执行期限。

编译器沿用宿主 Agent 的临时 Provider 错误重试策略;取消、非重试错误和已耗尽的上下文仍终止。`finish_reason: length` 表示不完整输出,即使包含看似完整的工具调用也不会执行;编译器将输出预算从 16,384 逐步扩大到 65,536 后重新生成,并保留每次请求的用量。上下文不足或最高预算仍耗尽时报告明确失败,保留 Claim 和编译诊断。

JEV 判定使用 32 KiB 的私有证据投影,编译回放另保留完整的宿主调用/结果轨迹。投影省略早期证据不会删除编译器的实际记录;自动归纳和普通 `jev compile` 使用相同边界。独立语义评审共享一份当前实际轨迹,系统约束和此前用户要求各保留一次;报告保留已校验的 evidence/path 引用。回放的修复诊断由验证器直接读取,语义评审只接收分支结果与共享原生调用,避免逐边界复制结果、诊断和机制资格摘要。超出 Claim 上下文限制仍返回 `review_input_limit`,不能通过截断证据或跳过审查发布。

回放失败的工具反馈提供尚未消费的 `remaining_recorded_calls`,保留失败读取、最后确认、原生参数及顺序。语义评审超限的诊断提供各部分字节数,帮助定位重复输出;所有验证门槛仍保留。

编译产物的 `when` 描述用户任务入口适用性,`decide` 描述函数负责的工作和完成/交接条件。使用参数的产物必须声明 schema,独立评审检查所有必需值能在入口取得;函数自己创建的资源名与已经存在的句柄有不同来源,原生检查才能发现的地址由函数内部取得。Claim 的错误选项不会被拼入 Reflex 的适用描述。

运行时参数提取直接接收原始约束文本、当前证据和 schema,省去源码与样例。截断时将输出预算从 2,048 逐步增加到 8,192,全部尝试计入前台用量,部分值不进入执行。宿主提供一次提取专用的唯一名称,仅供 schema 明确描述的新建资源使用;已有句柄和业务字段仍须来自当前证据。运行时审查直接读取原始约束文本,按 schema 解释字段的编码形式;绑定判定只接收当前调用的工具协议,宿主继续验证完整原生契约及效果身份。结构化原生结果直接保存为 JSON,避免重复转义挤出实际轨迹,原始日志和 JavaScript 读取接口保留原值。接管及验证的整体期限使用 30 分钟兜底。参数交接失败且没有保留或派发任何效果时,释放普通执行;同一输入下不反复选择这个失败函数,新用户输入或不同函数可再尝试。存在已知或未知效果时继续保护日志,避免重复写入。
Expand Down
3 changes: 2 additions & 1 deletion exts/jev/binding_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"sync"

coretool "github.com/chainreactors/cyber/core/tool"
"github.com/santhosh-tekuri/jsonschema/v6"
)

Expand All @@ -21,7 +22,7 @@ func (localSchemas) Load(url string) (any, error) {
}

// Compile only registered schemas; validation never fetches schema URLs.
func validateBindingSchema(candidate NativeCall, capabilities map[string]any) error {
func validateBindingSchema(candidate coretool.NativeCall, capabilities map[string]any) error {
tools, _ := capabilities["tools"].([]any)
for _, value := range tools {
tool, ok := value.(map[string]any)
Expand Down
47 changes: 22 additions & 25 deletions exts/jev/boundaries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import (
"time"

"github.com/chainreactors/cyber/core/decision"
coretool "github.com/chainreactors/cyber/core/tool"

"github.com/chainreactors/cyber/agent"
"github.com/chainreactors/cyber/agent/hooks"
"github.com/chainreactors/cyber/agent/provider"
jevapi "github.com/chainreactors/cyber/agent/provider/jev"
"github.com/chainreactors/cyber/aop"
"github.com/chainreactors/cyber/core/operation"
coretool "github.com/chainreactors/cyber/core/tool"
)

func qualifiedLaboratory(t *testing.T, e *Extension, caps map[string]any) Program {
Expand Down Expand Up @@ -153,7 +153,7 @@ func TestReflexMissingNativeEvidenceDoesNotPersistDraft(t *testing.T) {

func TestReflexKnownFailureAndUnknownResolution(t *testing.T) {
s := laboratorySuite()
c := NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`), Step: "add"}
c := coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`), Step: "add"}
c, _ = prepareBinding(c)
l := newEffectLedger()
_, _, _ = l.reserve("task", c)
Expand All @@ -163,26 +163,26 @@ func TestReflexKnownFailureAndUnknownResolution(t *testing.T) {
if _, _, err := l.reserve("task", second); err == nil {
t.Fatal("unknown outcome allowed another mutation")
}
read, _ := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab status other"}`), Read: true})
l.reconcile(s.native(), read, map[string]any{"data": map[string]any{"complete": true}})
read, _ := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab status other"}`), Read: true})
l.reconcile(s.Contracts, read, map[string]any{"data": map[string]any{"complete": true}})
if _, _, err := l.reserve("task", second); err == nil {
t.Fatal("unrelated status resolved an effect")
}
read.Arguments = json.RawMessage(`{"command":"lab status actor"}`)
read, _ = prepareBinding(read)
l.reconcile(s.native(), read, map[string]any{"call_id": "actual-read", "data": map[string]any{"complete": true}})
l.reconcile(s.Contracts, read, map[string]any{"call_id": "actual-read", "data": map[string]any{"complete": true}})
if _, cached, err := l.reserve("task", c); err != nil || !cached {
t.Fatal("resolved occurrence redispatched")
}
if _, cached, err := l.reserve("task", second); err != nil || cached {
t.Fatal("resolved effect did not allow the next occurrence")
}
contract := s.Contracts["lab"]
contract.Outcome = func(NativeCall, map[string]any) string { return "not_applied" }
contract.Outcome = func(coretool.NativeCall, map[string]any) string { return "not_applied" }
s.Contracts["lab"] = contract
l.complete("task", second, map[string]any{"is_error": true}, errors.New("explicit rejection"))
l.classifyOutcome("task", second, s.native(), map[string]any{"is_error": true}, "lab")
if _, _, err := l.reserve("task", NativeCall{Step: "another"}); err != nil {
l.classifyOutcome("task", second, s.Contracts, map[string]any{"is_error": true}, "lab")
if _, _, err := l.reserve("task", coretool.NativeCall{Step: "another"}); err != nil {
t.Fatal("trusted definite rejection was treated as unknown")
}
}
Expand All @@ -191,17 +191,17 @@ func TestReflexOutcomeUsesDeclaredContract(t *testing.T) {
s := laboratorySuite()
other := s.Contracts["lab"]
other.ID = "other"
other.Outcome = func(NativeCall, map[string]any) string { return "applied" }
other.Resolve = func(NativeCall, NativeCall, map[string]any) bool { return true }
other.Outcome = func(coretool.NativeCall, map[string]any) string { return "applied" }
other.Resolve = func(coretool.NativeCall, coretool.NativeCall, map[string]any) bool { return true }
s.Contracts[other.ID] = other
l := newEffectLedger()
c, _ := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`), Step: "add"})
c, _ := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`), Step: "add"})
_, _, _ = l.reserve("task", c)
result := map[string]any{"is_error": false, "data": map[string]any{"status": 503}}
l.complete("task", c, result, nil)
l.classifyOutcome("task", c, s.native(), result, "lab")
read, _ := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab status wrong"}`), Read: true})
l.reconcile(s.native(), read, map[string]any{"data": map[string]any{"complete": true}})
l.classifyOutcome("task", c, s.Contracts, result, "lab")
read, _ := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab status wrong"}`), Read: true})
l.reconcile(s.Contracts, read, map[string]any{"data": map[string]any{"complete": true}})
next := c
next.Occurrence = 1
if _, _, err := l.reserve("task", next); err == nil {
Expand Down Expand Up @@ -314,16 +314,16 @@ func TestReflexEvidencePathsTraverseCurrentArrays(t *testing.T) {

func TestReflexShellAndEvidenceBoundaries(t *testing.T) {
for _, script := range []string{"lab add $ACTOR", "lab add $(whoami)", "lab add actor > file", "lab add actor; lab add actor", "lab add *", "lab add ~", "lab add {a,b}"} {
c, err := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(jsonText(map[string]any{"command": script})), Argv: []string{"lab", "add", "forged"}})
c, err := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(jsonText(map[string]any{"command": script})), Argv: []string{"lab", "add", "forged"}})
if err != nil || len(c.Argv) != 0 {
t.Fatalf("opaque shell gained trusted argv: %s %v", script, c.Argv)
}
if _, err := laboratorySuiteAccess(c); err == nil {
if _, err := laboratorySuite().Contracts.Access(c); err == nil {
t.Fatal("opaque shell was admitted")
}
}
a, _ := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`)})
b, _ := prepareBinding(NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":" lab add 'actor' "}`)})
a, _ := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":"lab add actor"}`)})
b, _ := prepareBinding(coretool.NativeCall{Name: "bash", Arguments: json.RawMessage(`{"command":" lab add 'actor' "}`)})
if logicalBinding(a) != logicalBinding(b) {
t.Fatal("cosmetic shell changes altered operation identity")
}
Expand All @@ -337,11 +337,6 @@ func TestReflexShellAndEvidenceBoundaries(t *testing.T) {
}
}

func laboratorySuiteAccess(c NativeCall) (coretool.NativeAccess, error) {
s := laboratorySuite()
return s.access(c)
}

func TestReflexCompilerEffortAndCancellationOwnItsLifetime(t *testing.T) {
e := testLaboratory(t)
e.config.DeclarationEffort = "none"
Expand Down Expand Up @@ -376,7 +371,7 @@ func TestReflexReportChecksCurrentRequest(t *testing.T) {
}

func TestReflexPureSemanticCapabilityAndFreshArguments(t *testing.T) {
s := VerificationSuite{ID: "semantic", Version: "1", Contracts: map[string]NativeContract{}, CheckInput: func(input, args map[string]any) error {
s := VerificationSuite{ID: "semantic", Version: "1", Contracts: coretool.NativeContracts{}, CheckInput: func(input, args map[string]any) error {
if args == nil || input["user"] != fmt.Sprint(args["mode"])+":"+fmt.Sprint(args["value"]) {
return errors.New("arguments do not represent this request")
}
Expand All @@ -403,7 +398,9 @@ func TestReflexPureSemanticCapabilityAndFreshArguments(t *testing.T) {
Judge: func(claim jevapi.Claim) (*decision.Evaluation, error) {
return &decision.Evaluation{Value: &decision.Evaluation_Choice{Choice: mode}}, nil
},
Execute: func(NativeCall) (map[string]any, error) { return nil, errors.New("pure capability must not dispatch") },
Execute: func(coretool.NativeCall) (map[string]any, error) {
return nil, errors.New("pure capability must not dispatch")
},
Check: func(run VerificationRun) error {
if run.Error != nil {
return run.Error
Expand Down
70 changes: 10 additions & 60 deletions exts/jev/business_oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"sync"

jevapi "github.com/chainreactors/cyber/agent/provider/jev"
Expand All @@ -20,7 +19,7 @@ func independentRuntimeJudgments(req inferenceRequest) map[string]inferenceAnswe
var payload struct {
State struct {
Arguments map[string]any `json:"arguments"`
Call NativeCall `json:"call"`
Call coretool.NativeCall `json:"call"`
Report map[string]any `json:"report"`
Evidence map[string]map[string]any `json:"evidence"`
} `json:"state"`
Expand Down Expand Up @@ -50,25 +49,18 @@ func independentRuntimeJudgments(req inferenceRequest) map[string]inferenceAnswe

var testTrajectories sync.Map

type NativeContract struct {
ID, Version, Description string
Classify func(NativeCall) (coretool.NativeAccess, error)
Outcome func(NativeCall, map[string]any) string
Resolve func(NativeCall, NativeCall, map[string]any) bool
}

// VerificationCase supplies a fresh simulator and independent postcondition.
// It must never use the foreground Executor or user resources.
type VerificationCase struct {
ID string
Input map[string]any
Arguments map[string]any
Judge func(jevapi.Claim) (*decision.Evaluation, error)
Execute func(NativeCall) (map[string]any, error)
Execute func(coretool.NativeCall) (map[string]any, error)
Check func(VerificationRun) error
}
type VerificationRun struct {
Calls []NativeCall
Calls []coretool.NativeCall
Evidence map[string]map[string]any
Output map[string]any
Error error
Expand All @@ -85,7 +77,7 @@ type VerificationReport struct {
type VerificationCall struct {
Input map[string]any
Arguments map[string]any
Call NativeCall
Call coretool.NativeCall
Effects map[string]any
Evidence map[string]map[string]any
}
Expand All @@ -94,7 +86,7 @@ type VerificationSuite struct {
ID string
Version string
Description string
Contracts map[string]NativeContract
Contracts coretool.NativeContracts
Cases func(map[string]any) []VerificationCase
CheckInput func(map[string]any, map[string]any) error
CheckCall func(VerificationCall) error
Expand All @@ -112,7 +104,7 @@ func (h testRegistryHandle) Register(s VerificationSuite) error {
r, _ := testRegistries.LoadOrStore(h.e, &VerificationRegistry{suites: map[string]*VerificationSuite{}})
r.(*VerificationRegistry).suites[s.ID] = &s
h.e.contracts = coretool.NewNativeContractRegistry()
for _, c := range s.native() {
for _, c := range s.Contracts {
if err := h.e.contracts.Register(c); err != nil {
return err
}
Expand All @@ -133,48 +125,6 @@ func (h testRegistryHandle) suite(id string) *VerificationSuite {
}
return nil
}
func (s *VerificationSuite) native() coretool.NativeContracts {
n := coretool.NativeContracts{}
for id, c := range s.Contracts {
n[id] = coretool.NativeContract{ID: c.ID, Version: c.Version, Description: c.Description, Classify: func(call coretool.NativeCall) (coretool.NativeAccess, error) { return c.Classify(NativeCall(call)) }, Outcome: func(call coretool.NativeCall, r map[string]any) string {
if c.Outcome == nil {
return "unknown"
}
return c.Outcome(NativeCall(call), r)
}, Resolve: func(a, b coretool.NativeCall, r map[string]any) bool {
return c.Resolve != nil && c.Resolve(NativeCall(a), NativeCall(b), r)
}}
}
return n
}
func (s *VerificationSuite) access(c NativeCall) (coretool.NativeAccess, error) {
ids := make([]string, 0, len(s.Contracts))
for id := range s.Contracts {
ids = append(ids, id)
}
sort.Strings(ids)
result := UnsupportedAccess
for _, id := range ids {
a, err := s.Contracts[id].Classify(c)
if err != nil {
return UnsupportedAccess, err
}
if a == UnsupportedAccess {
continue
}
if a != ReadAccess && a != EffectAccess {
return UnsupportedAccess, errors.New("invalid native access classification")
}
if result != UnsupportedAccess && result != a {
return UnsupportedAccess, errors.New("conflicting native contracts")
}
result = a
}
if result == UnsupportedAccess {
return result, errors.New("unsupported native operation")
}
return result, nil
}
func qualifyIndependent(e *Extension, ctx context.Context, r *Program, caps map[string]any, suiteIDs ...string) error {
if err := r.validate(); err != nil {
return err
Expand Down Expand Up @@ -223,11 +173,11 @@ func qualifyIndependent(e *Extension, ctx context.Context, r *Program, caps map[
rows := []map[string]any{{"role": "user", "text": jsonText(input)}}
run := VerificationRun{Evidence: map[string]map[string]any{}}
ledger := newEffectLedger()
execute := func(call NativeCall) (map[string]any, error) {
execute := func(call coretool.NativeCall) (map[string]any, error) {
if len(run.Calls) >= maxCandidates {
return nil, handoffError{"native call budget reached"}
}
if err := r.validateCall(s.native(), call, c.Arguments); err != nil {
if err := r.validateCall(s.Contracts, call, c.Arguments); err != nil {
return nil, err
}
if err := s.CheckCall(VerificationCall{Input: input, Arguments: c.Arguments, Call: call, Effects: ledger.summary(), Evidence: cloneEvidence(run.Evidence)}); err != nil {
Expand All @@ -252,9 +202,9 @@ func qualifyIndependent(e *Extension, ctx context.Context, r *Program, caps map[
}
if !call.Read {
ledger.complete("case", call, result, err)
ledger.classifyOutcome("case", call, s.native(), result, r.Steps[call.Step].Contract)
ledger.classifyOutcome("case", call, s.Contracts, result, r.Steps[call.Step].Contract)
} else {
ledger.reconcile(s.native(), call, result)
ledger.reconcile(s.Contracts, call, result)
}
return result, err
}
Expand Down
Loading
Loading