mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 14:22:57 +00:00
Wave 1 Phase 02 — pure runtime + sandbox. Dispatcher wiring, edition gate,
and DB migration land in Phase 03.
Handler:
- internal/hooks/handlers/script.go — ScriptHandler with two-layer semaphore
(global 10 / per-tenant 3), hashicorp/golang-lru v2 program cache (cap 500),
InvalidateHook hook for Phase 03, watchdog goroutine interrupting on ctx
cancel, sanitized error text (strips \n + goja " at <frame>" inline tails)
- internal/hooks/handlers/script_sandbox.go — deny-all/allowlist hardening,
SetMaxCallStackSize(256), prototype-chain nullify running BEFORE deny pass
so Function.prototype.constructor can still be reached
- internal/hooks/handlers/script_runtime.go — LRU compile, JSON round-trip +
deep-freeze event binding, 4KiB truncating stdout capture, strict return
parser enforcing {decision, reason?, updatedInput?}
Types:
- HandlerScript HandlerType = "script"
- DecisionAsk / DecisionDefer reserved (Wave 1 treats as block + warn)
- hooks.ScriptResult + WithScriptResult / ScriptResultFrom ctx helpers for
Phase 03 dispatcher to apply builtin-source UpdatedInput
Tests (go test -race green):
- script_test.go — 12 unit cases (happy/error/timeout/ask/defer/truncate/H1)
- script_sandbox_corpus_test.go — 27 cases: escape primitives (constructor
chain, Reflect, Proxy, Symbol, Promise, __proto__ walk, eval, Function,
Date chain, JSON replacer, toJSON DoS), resource bombs (recursion, loop,
memory, ReDoS), typeof sanity (Reflect/Proxy/Symbol/Promise/GoError/
globalThis/eval/Function undefined), mutation defense (toolInput/rawInput
frozen, Go-side map unchanged)
Deps: github.com/dop251/goja, github.com/hashicorp/golang-lru/v2.
Gates: go vet clean, go test -race clean, go build ./... + sqliteonly clean.
43 lines
1.7 KiB
Go
43 lines
1.7 KiB
Go
package hooks
|
|
|
|
import "context"
|
|
|
|
// ScriptResult carries non-decision outputs from a script handler execution
|
|
// (reason text, updatedInput proposal, captured stdout). The dispatcher
|
|
// provisions one via context before calling Handler.Execute; the handler
|
|
// populates it on success. Phase 03 wires this into the dispatcher so that
|
|
// only builtin-source hooks can apply UpdatedInput to the pipeline state.
|
|
//
|
|
// Fields are read-write within a single Execute call; not concurrent-safe
|
|
// (each Execute gets its own instance via a context key).
|
|
type ScriptResult struct {
|
|
// Reason is the human-readable explanation returned by the script.
|
|
Reason string
|
|
// UpdatedInput is a proposed replacement for Event.ToolInput. Dispatcher
|
|
// applies only when cfg.Source == "builtin" (source-tier capability).
|
|
UpdatedInput map[string]any
|
|
// Stdout is the captured console.log / console.error output, bounded by
|
|
// handlers.MaxStdoutBytes (truncated with a marker when exceeded).
|
|
Stdout string
|
|
}
|
|
|
|
// ctxScriptResultKey is the private context-value key.
|
|
type ctxScriptResultKey struct{}
|
|
|
|
// WithScriptResult returns a ctx carrying r. The dispatcher calls this before
|
|
// invoking a script handler so the handler can write its non-decision outputs
|
|
// without widening the Handler interface.
|
|
func WithScriptResult(ctx context.Context, r *ScriptResult) context.Context {
|
|
return context.WithValue(ctx, ctxScriptResultKey{}, r)
|
|
}
|
|
|
|
// ScriptResultFrom retrieves the result pointer from ctx, or nil when absent.
|
|
// Handler code must be nil-tolerant; non-script or Phase-02 standalone paths
|
|
// won't provision one.
|
|
func ScriptResultFrom(ctx context.Context) *ScriptResult {
|
|
if v, ok := ctx.Value(ctxScriptResultKey{}).(*ScriptResult); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|