Skip to content
Open
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
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ uv run python -m scripts.repl

### Module System

Twelve compiled-in modules are registered in `pkg/app/app.go`:
Thirteen compiled-in modules are registered in `pkg/app/app.go`:
- `clickhouse`
- `prometheus`
- `loki`
Expand All @@ -106,6 +106,7 @@ Twelve compiled-in modules are registered in `pkg/app/app.go`:
- `ethnode`
- `cbt`
- `benchmarkoor`
- `buildoor` (devnet builder instances: per-slot action plans, jq transforms, slot outcomes)
- `tracoor`
- `block_archive`
- `datasets` (dataset knowledge packs, lives in `datasets/` at the repo root)
Expand Down Expand Up @@ -148,6 +149,12 @@ CLI commands and groups include:
- `benchmarkoor`
- `block-archive`
- `build` (GitHub Actions Docker image builder)
- `buildoor` — per-slot action plans on devnet buildoor instances (jq payload/bid/envelope
transforms, plan/results inspection); server-side ops resolve instances via the
network's buildoor overview service. Reads are open and go direct; mutations route
through a proxy advertising buildoor (the proxy holds a CF Access service token and
mints per-devnet authenticatoor JWTs), or direct with an explicit caller token.
The same operations back the sandbox Python module (`from ethpandaops import buildoor`)
- `cbt`
- `clickhouse`
- `config`
Expand Down Expand Up @@ -264,6 +271,7 @@ modules/
ethnode/ # Ethnode module
cbt/ # CBT module
benchmarkoor/ # Benchmarkoor module
buildoor/ # Buildoor module (per-slot action plans on devnet builders)
tracoor/ # Tracoor module
block_archive/ # Block archive module
runbooks/ # Embedded markdown runbooks
Expand Down
30 changes: 30 additions & 0 deletions modules/buildoor/examples.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package buildoor

import (
_ "embed"
"fmt"
"strings"

"gopkg.in/yaml.v3"

"github.com/ethpandaops/panda/pkg/types"
)

//go:embed examples.yaml
var examplesYAML []byte

var queryExamples map[string]types.ExampleCategory

func init() {
if err := yaml.Unmarshal(examplesYAML, &queryExamples); err != nil {
panic(fmt.Sprintf("failed to parse buildoor examples.yaml: %v", err))
}

for key, category := range queryExamples {
for i := range category.Examples {
category.Examples[i].Query = strings.TrimSpace(category.Examples[i].Query)
}

queryExamples[key] = category
}
}
57 changes: 57 additions & 0 deletions modules/buildoor/examples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
buildoor_action_plans:
name: Buildoor Action Plans
description: Discover devnet builder instances, hot-patch payloads with jq transforms, verify slot outcomes
examples:
- name: Discover buildoor instances
description: List networks with buildoor deployments and each network's builder instances
query: |
from ethpandaops import buildoor

networks = buildoor.list_networks()
print(f"Networks with buildoor: {[n['name'] for n in networks]}")

instances = buildoor.list_instances(networks[0]["name"])
for inst in instances:
overview = buildoor.get_overview(networks[0]["name"], inst["name"])
print(f"{inst['name']}: current_slot={overview.get('current_slot')}")

- name: Validate and apply a payload transform
description: Test a jq expression (e.g. gas limit hot-patch), then set it on future slots
query: |
from ethpandaops import buildoor

network, instance = "glamsterdam-devnet-7", "lighthouse-geth-1"

# Dry-run the expression through buildoor's production jq path first.
result = buildoor.test_transform(network, instance, "payload", ".gas_limit = 300000000")
assert not result.get("error"), result
print(f"validated against {result['input_source']}")

# The panda proxy credentials the mutation when it advertises buildoor;
# pass token=... with a personal authenticatoor bearer to go direct.
# Plans freeze ~1 slot ahead: target slots at least 2 ahead of current.
current = buildoor.get_overview(network, instance)["current_slot"]
applied = buildoor.set_transforms(
network, instance,
slots=[current + 2, current + 3],
payload=".gas_limit = 300000000",
)
print(f"planned slots: {applied['slots']}")

- name: Verify what a planned slot did
description: Read the attempt-level outcome (build, bids, reveals, inclusion) and the frozen plan a slot ran with
query: |
from ethpandaops import buildoor

network, instance = "glamsterdam-devnet-7", "lighthouse-geth-1"
slot = 65000 # a slot that already executed

results = buildoor.get_slot_results(network, instance, slot, slot)
for record in results["results"]:
build = record.get("build") or {}
print(f"slot {record['slot']}: build={build.get('status')}")
# For a payload transform like .gas_limit = N, confirm it landed:
print(f" built gas_limit: {build.get('gas_limit')}")
print(f" bids: {len(record.get('bids', []))}, "
f"reveals: {len(record.get('reveal_attempts', []))}, "
f"included: {record.get('inclusion') is not None}")
119 changes: 119 additions & 0 deletions modules/buildoor/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package buildoor exposes devnet buildoor builder instances to the sandbox:
// per-slot action plans, jq transforms, and slot outcome history over the
// server-side buildoor.* operations.
package buildoor

import (
"context"
"encoding/json"
"fmt"
"maps"

"github.com/ethpandaops/panda/pkg/cartographoor"
"github.com/ethpandaops/panda/pkg/module"
"github.com/ethpandaops/panda/pkg/types"
)

// Compile-time interface checks.
var (
_ module.Module = (*Module)(nil)
_ module.DefaultEnabled = (*Module)(nil)
_ module.CartographoorAware = (*Module)(nil)
_ module.SandboxEnvProvider = (*Module)(nil)
_ module.DatasourceInfoProvider = (*Module)(nil)
_ module.ExamplesProvider = (*Module)(nil)
_ module.PythonAPIDocsProvider = (*Module)(nil)
)

// Module implements the module.Module interface for the buildoor module.
type Module struct {
cartographoorClient cartographoor.CartographoorClient
}

// New creates a new buildoor module.
func New() *Module {
return &Module{}
}

func (m *Module) Name() string { return "buildoor" }

// DefaultEnabled implements module.DefaultEnabled.
// Buildoor is enabled by default since it requires no configuration.
func (m *Module) DefaultEnabled() bool { return true }

func (m *Module) Init(_ []byte) error { return nil }

func (m *Module) ApplyDefaults() {}

func (m *Module) Validate() error { return nil }

// SandboxEnv returns ETHPANDAOPS_BUILDOOR_NETWORKS with the network->overview
// URL mapping from cartographoor, so sandbox code can fail fast when no
// buildoor deployments exist.
func (m *Module) SandboxEnv() (map[string]string, error) {
if m.cartographoorClient == nil {
return nil, nil
}

networks := m.cartographoorClient.GetActiveNetworks()
buildoorNetworks := make(map[string]string, len(networks))

for name, network := range networks {
if network.ServiceURLs != nil && network.ServiceURLs.Buildoor != "" {
buildoorNetworks[name] = network.ServiceURLs.Buildoor
}
}

if len(buildoorNetworks) == 0 {
return nil, nil
}

networksJSON, err := json.Marshal(buildoorNetworks)
if err != nil {
return nil, fmt.Errorf("marshaling buildoor networks: %w", err)
}

return map[string]string{
"ETHPANDAOPS_BUILDOOR_NETWORKS": string(networksJSON),
}, nil
}

// DatasourceInfo returns empty since networks are the datasources,
// and those come from cartographoor.
func (m *Module) DatasourceInfo() []types.DatasourceInfo {
return nil
}

func (m *Module) Examples() map[string]types.ExampleCategory {
result := make(map[string]types.ExampleCategory, len(queryExamples))
maps.Copy(result, queryExamples)

return result
}

func (m *Module) PythonAPIDocs() map[string]types.ModuleDoc {
return map[string]types.ModuleDoc{
"buildoor": {
Description: "Drive devnet buildoor builder instances: per-slot action plans, jq transforms, slot outcomes",
Functions: map[string]types.FunctionDoc{
"list_networks": {Signature: "list_networks() -> list[dict]", Description: "List networks with buildoor deployments"},
"list_instances": {Signature: "list_instances(network) -> list[dict]", Description: "List a network's builder instances (name, url)"},
"get_overview": {Signature: "get_overview(network, instance) -> dict", Description: "Instance status incl. current_slot and service states"},
"get_action_plan": {Signature: "get_action_plan(network, instance, min_slot, max_slot) -> dict", Description: "Per-slot action plans in the inclusive range"},
"get_slot_results": {Signature: "get_slot_results(network, instance, min_slot, max_slot) -> dict", Description: "Attempt-level outcome history (build, bids, reveals, inclusion, applied plan)"},
"test_transform": {Signature: "test_transform(network, instance, target, expression, sample_slot=None) -> dict", Description: "Evaluate a jq expression against a sample payload/bid/envelope without touching any plan"},
"update_action_plan": {Signature: "update_action_plan(network, instance, updates, token=None) -> dict", Description: "Apply raw PlanUpdate mutations; credentialed by the proxy, or direct with an authenticatoor bearer token"},
"set_transforms": {Signature: "set_transforms(network, instance, token=None, slots=None, from_slot=None, to_slot=None, payload=None, bid=None, envelope=None) -> dict", Description: "Set jq transforms on future slots (>=2 ahead; '' clears one expression)"},
},
},
}
}

// SetCartographoorClient implements module.CartographoorAware.
func (m *Module) SetCartographoorClient(client cartographoor.CartographoorClient) {
m.cartographoorClient = client
}

func (m *Module) Start(_ context.Context) error { return nil }

func (m *Module) Stop(_ context.Context) error { return nil }
Loading
Loading