From 843f78f0d236718e5d3db8840f5baf9d3f142371 Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Thu, 19 Feb 2026 22:11:52 -0500 Subject: [PATCH] gate: harden envelope parsing and wrkr context isolation --- core/gate/approved_scripts.go | 3 +++ core/gate/approved_scripts_test.go | 10 ++++++++++ core/gate/context_wrkr.go | 14 ++++++++++++++ core/gate/context_wrkr_test.go | 14 ++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/core/gate/approved_scripts.go b/core/gate/approved_scripts.go index 8121b7e..81becdf 100644 --- a/core/gate/approved_scripts.go +++ b/core/gate/approved_scripts.go @@ -180,6 +180,9 @@ func ReadApprovedScriptRegistry(path string) ([]schemagate.ApprovedScriptEntry, var envelopeRaw map[string]json.RawMessage if err := json.Unmarshal(content, &envelopeRaw); err == nil { if rawEntries, ok := envelopeRaw["entries"]; ok { + if err := requireJSONArray(rawEntries, "entries"); err != nil { + return nil, fmt.Errorf("parse approved script registry: %w", err) + } var envelope registryEnvelope if err := json.Unmarshal(rawEntries, &envelope.Entries); err != nil { return nil, fmt.Errorf("parse approved script registry: %w", err) diff --git a/core/gate/approved_scripts_test.go b/core/gate/approved_scripts_test.go index 47b8106..9e9332f 100644 --- a/core/gate/approved_scripts_test.go +++ b/core/gate/approved_scripts_test.go @@ -153,6 +153,16 @@ func TestReadApprovedScriptRegistryVariants(t *testing.T) { t.Fatalf("expected empty entries for empty envelope registry, got %#v", entries) } + nullEnvelopePath := filepath.Join(t.TempDir(), "null_envelope.json") + if err := os.WriteFile(nullEnvelopePath, []byte(`{"entries":null}`), 0o600); err != nil { + t.Fatalf("write null envelope registry fixture: %v", err) + } + if _, err := ReadApprovedScriptRegistry(nullEnvelopePath); err == nil { + t.Fatalf("expected null entries envelope to fail") + } else if !strings.Contains(err.Error(), "entries must be an array") { + t.Fatalf("expected entries array validation error, got: %v", err) + } + nowUTC := time.Date(2026, time.February, 5, 0, 0, 0, 0, time.UTC) legacyPath := filepath.Join(t.TempDir(), "legacy.json") legacyJSON := []byte(`[ diff --git a/core/gate/context_wrkr.go b/core/gate/context_wrkr.go index 9e20788..e311f0b 100644 --- a/core/gate/context_wrkr.go +++ b/core/gate/context_wrkr.go @@ -97,6 +97,9 @@ func parseWrkrInventory(content []byte) (map[string]WrkrToolMetadata, error) { var wrappedRaw map[string]json.RawMessage if err := json.Unmarshal(content, &wrappedRaw); err == nil { if rawItems, ok := wrappedRaw["items"]; ok { + if err := requireJSONArray(rawItems, "items"); err != nil { + return nil, fmt.Errorf("parse wrkr inventory: %w", err) + } var wrapped envelope if err := json.Unmarshal(rawItems, &wrapped.Items); err != nil { return nil, fmt.Errorf("parse wrkr inventory: %w", err) @@ -129,6 +132,17 @@ func parseWrkrInventory(content []byte) (map[string]WrkrToolMetadata, error) { return tools, nil } +func requireJSONArray(raw json.RawMessage, field string) error { + var parsed any + if err := json.Unmarshal(raw, &parsed); err != nil { + return err + } + if _, ok := parsed.([]any); !ok { + return fmt.Errorf("%s must be an array", field) + } + return nil +} + func ApplyWrkrContext(intent *schemagate.IntentRequest, toolName string, inventory map[string]WrkrToolMetadata) bool { if intent == nil || len(inventory) == 0 { return false diff --git a/core/gate/context_wrkr_test.go b/core/gate/context_wrkr_test.go index 45a7257..566acbe 100644 --- a/core/gate/context_wrkr_test.go +++ b/core/gate/context_wrkr_test.go @@ -102,6 +102,20 @@ func TestLoadWrkrInventoryAcceptsEmptyEnvelope(t *testing.T) { } } +func TestLoadWrkrInventoryRejectsNullItemsEnvelope(t *testing.T) { + workDir := t.TempDir() + path := filepath.Join(workDir, "wrkr_inventory.json") + mustWriteWrkrInventoryFile(t, path, `{"items":null}`) + + _, err := LoadWrkrInventory(path) + if err == nil { + t.Fatalf("expected null items envelope to fail") + } + if !strings.Contains(err.Error(), "items must be an array") { + t.Fatalf("expected items array validation error, got: %v", err) + } +} + func TestApplyWrkrContextAddsMetadata(t *testing.T) { intent := schemagate.IntentRequest{ Context: schemagate.IntentContext{},