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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ changes; the linked API and deployment documentation contains operational detail
already signed with. See `docs/tag-spec.md` §10.

### Added
- Source-built `dbl-studio` authoring tool with a typed workflow compiler,
local visual editor integration and MCP tools. Workflows support network
actions, processing, conditions, bounded repetition and custom Go blocks.
See [workflow authoring](docs/development/workflow-authoring.md).
- Coordinated workflow experiments for two or three executors, with typed
send/await channels and retained connection manifests for the console's
measurement map. Peer messaging requires directly reachable IPv4 TCP
listeners and is plaintext; see [execution limits](docs/development/workflows.md).
- The executor answers pre-disclosure tag verification queries over the
control session (`VerifyTags`): for one group of captured packets of one
epoch it reports which candidate run, if any, reproduces every tag, without
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ CI_RACE_PACKAGES = \
./internal/executor/ratelimit/fallback \
./internal/executor/scheduler/memory \
./internal/executor/transport/rpc \
./internal/readiness
./internal/readiness \
./internal/studio \
./pkg/workflow/guest
CI_RACE_TIMEOUT ?= 5m
CI_RACE_BUDGET_SECONDS ?= 240
CI_RACE_PACKAGE_PARALLEL ?= 2
Expand Down
2 changes: 2 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ github.com/google/uuid v1.6.0 - BSD-3-Clause
github.com/hashicorp/yamux v0.1.2 - MPL-2.0
github.com/labstack/echo/v4 v4.15.4 - MIT
github.com/maxmind/mmdbwriter v1.2.0 - Apache-2.0 OR MIT
github.com/modelcontextprotocol/go-sdk v1.7.0 - Apache-2.0 AND MIT (see module LICENSE)
github.com/netsec-ethz/scion-apps v0.6.0 - Apache-2.0
github.com/oschwald/maxminddb-golang/v2 v2.6.0 - ISC
github.com/pelletier/go-toml/v2 v2.2.4 - MIT
Expand All @@ -25,6 +26,7 @@ github.com/soheilhy/cmux v0.1.5 - Apache-2.0
github.com/tetratelabs/wazero v1.12.0 - Apache-2.0
go.uber.org/zap v1.27.0 - MIT
golang.org/x/crypto v0.57.0 - BSD-3-Clause
golang.org/x/net v0.60.0 - BSD-3-Clause
golang.org/x/oauth2 v0.36.0 - BSD-3-Clause
golang.org/x/sync v0.23.0 - BSD-3-Clause
golang.org/x/sys v0.48.0 - BSD-3-Clause
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The versioned guides in [`docs/`](docs/README.md) are published at the [document
| Use the CLI | [CLI reference](docs/cli.md) |
| Write a Go application client | [Go client library](docs/client.md) |
| Write a debuglet | [Write a debuglet](docs/debuglets.md) |
| Build visually or with an MCP client | [Workflow and experiment authoring](docs/development/workflow-authoring.md) |
| Integrate with the HTTP API | [OpenAPI contract](api/openapi.yaml) and [API guide](docs/api.md) |
| Understand the code and protocol | [Architecture](docs/architecture.md) |
| Operate a service | [Managed services](docs/operations/services.md) |
Expand Down
150 changes: 150 additions & 0 deletions cmd/dbl-studio/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ETH Zurich

// Command dbl-studio edits and compiles visual measurement workflows locally.
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"syscall"

"github.com/netsec-ethz/debuglet/internal/studio"
"github.com/netsec-ethz/debuglet/pkg/workflow"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "dbl-studio:", err)
os.Exit(1)
}
}

func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
if len(args) == 0 {
return errors.New("usage: dbl-studio catalog|validate|build|validate-experiment|build-experiment|serve|mcp [options]")
}
command := args[0]
if command == "--help" || command == "-h" || command == "help" {
_, err := fmt.Fprintln(stdout, "usage: dbl-studio catalog|validate|build|validate-experiment|build-experiment|serve|mcp [options]\nUse <command> -h for options.")
return err
}
flags := flag.NewFlagSet("dbl-studio "+command, flag.ContinueOnError)
flags.SetOutput(stderr)
sdk := flags.String("sdk", "", "trusted Debuglet source checkout (required for compilation)")
input := flags.String("input", "-", "workflow JSON file, or - for stdin")
output := flags.String("output", "-", "bundle JSON file, or - for stdout")
address := flags.String("listen", "127.0.0.1:8090", "loopback HTTP address")
ui := flags.String("ui", "", "built console directory containing index.html")
dispatcher := flags.String("dispatcher", "", "owned loopback development dispatcher origin for the console API proxy")
caFile := flags.String("ca-file", "", "additional PEM CA for the fixed dispatcher HTTPS connection")
workspace := flags.String("workspace", "", "directory for MCP workflow and bundle files")
if err := flags.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
if flags.NArg() != 0 {
return errors.New("unexpected positional arguments")
}
var builder *studio.Builder
if *sdk != "" {
var err error
builder, err = studio.NewBuilder(*sdk)
if err != nil {
return err
}
}
switch command {
case "catalog":
return json.NewEncoder(stdout).Encode(workflow.Catalog())
case "serve":
return studio.Serve(ctx, builder, studio.ServerConfig{Address: *address, UI: *ui, Dispatcher: *dispatcher, CAFile: *caFile}, func(address string) { fmt.Fprintln(stderr, "Open", address) })
case "mcp":
if *workspace == "" {
return errors.New("mcp requires --workspace for editable workflows and build bundles")
}
return studio.MCP(ctx, builder, *workspace)
case "validate", "build", "validate-experiment", "build-experiment":
var reader io.Reader = os.Stdin
if *input != "-" {
file, err := os.Open(*input)
if err != nil {
return err
}
defer file.Close()
reader = file
}
var result any
var err error
if command == "validate-experiment" || command == "build-experiment" {
experiment, decodeErr := studio.DecodeExperiment(reader)
if decodeErr != nil {
return decodeErr
}
if command == "validate-experiment" {
diagnostics := workflow.ValidateExperiment(experiment)
if diagnostics == nil {
diagnostics = []workflow.ExperimentDiagnostic{}
}
if err := json.NewEncoder(stdout).Encode(map[string]any{"diagnostics": diagnostics}); err != nil {
return err
}
if len(diagnostics) > 0 {
return errors.New("experiment is invalid")
}
return nil
}
if builder == nil {
return errors.New("build-experiment requires --sdk")
}
result, err = builder.BuildExperiment(ctx, experiment)
} else {
wf, decodeErr := studio.DecodeWorkflow(reader)
if decodeErr != nil {
return decodeErr
}
if command == "validate" {
diagnostics := workflow.Validate(wf)
if diagnostics == nil {
diagnostics = []workflow.Diagnostic{}
}
if err := json.NewEncoder(stdout).Encode(map[string]any{"diagnostics": diagnostics}); err != nil {
return err
}
if len(diagnostics) > 0 {
return errors.New("workflow is invalid")
}
return nil
}
if builder == nil {
return errors.New("build requires --sdk pointing to the matching Debuglet checkout")
}
result, err = builder.Build(ctx, wf)
}
if err != nil {
return err
}
var writer io.Writer = stdout
if *output != "-" {
file, err := os.OpenFile(*output, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer file.Close()
writer = file
}
return json.NewEncoder(writer).Encode(result)
default:
return fmt.Errorf("unknown command %q (use --help for commands)", command)
}
}
22 changes: 22 additions & 0 deletions cmd/dbl-studio/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ETH Zurich

package main

import (
"bytes"
"context"
"path/filepath"
"testing"
)

func TestValidateExperimentCommand(t *testing.T) {
for _, name := range []string{"three-node.json", "request-reply.json"} {
var output, diagnostics bytes.Buffer
input := filepath.Join("..", "..", "examples", "workflows", "experiments", name)
err := run(context.Background(), []string{"validate-experiment", "--input", input}, &output, &diagnostics)
if err != nil || output.String() != "{\"diagnostics\":[]}\n" {
t.Fatalf("%s: %v %s %s", name, err, &output, &diagnostics)
}
}
}
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This directory is the versioned source for the [Debuglet documentation site](htt
- [Portable results](results.md) — export retained output and admission facts for offline analysis.
- [Controlled latency experiment](research/latency-evaluation.md) — compare native and WASM probes under known local network faults.
- [Write a debuglet](debuglets.md) — execution model and Go authoring interface.
- [Build workflows and experiments](development/workflow-authoring.md) — visual editing, local compilation and MCP authoring.
- [Workflow blocks and execution](development/workflows.md) — typed connections, reusable blocks and communication between executors.
- [HTTP API](api.md) — public contract, authentication, compatibility, and deprecation.
- [Versions and compatibility](versions.md) — tested combinations, support policy, and breaking changes.
- [Linux client container](operations/client-container.md) — remote client with persistent credentials.
Expand Down
3 changes: 3 additions & 0 deletions docs/development/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ make ci-package

Use `make ci-race` for concurrency changes. `make ci-local` exercises an installed package with local dispatcher and executor roles. `make ci-kernel` needs an isolated Linux host with the required kernel capabilities.

The race lane includes the local workflow authoring service and the guest
experiment transport, alongside the dispatcher and executor lifecycle packages.

CI checks generated code, formatting, tests, race-sensitive code, package installation, local roles, and kernel integration. A passing workflow validates the checked revision on its test platform; it does not establish production readiness or remote deployment compatibility.

The workflow uses GitHub-hosted runners, read-only permissions, and no deployment secrets. See [CI images](ci-images.md) when changing the build environment.
Expand Down
156 changes: 156 additions & 0 deletions docs/development/workflow-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Build workflows and experiments

Choose **Workflow builder** to write one executor's program. Choose
**Experiment builder** to connect two or three participants, each with its own
workflow. The same graph can be edited visually, authored by an MCP client or
compiled from the command line with `dbl-studio`.

Conditions, calculations and repeats execute inside the compiled WASI program.
The dispatcher coordinates experiment readiness and stores results; it does not
compile source or orchestrate individual blocks. See the
[workflow guide](workflows.md) for HTTP bodies, processing blocks, reusable
visual groups and custom Go blocks.

`dbl-studio` is currently built from source; it is not part of the release or apt
packages. A hosted console can edit and export graphs and import compiled bundles.
Compilation needs Go and a trusted checkout of the matching SDK on the authoring
machine. The local service can combine editing and compilation in one browser tab.

## Start the editor

On Linux, install Go 1.26.9, clone this version of Debuglet, and build the
authoring tool from the repository root:

```sh
go build -o dbl-studio ./cmd/dbl-studio
```

In the [dashboard repository](https://gitlab.inf.ethz.ch/OU-PERRIG/yimin/debuglet/debuglet-dashboard),
follow its **Run locally** prerequisites first: Node.js 24.14.0, `npm ci`, and
both template builds with their pinned Go versions. Then build the console with
its path explicitly set:

```sh
VITE_BASE_PATH=/console npm run build
```

This sets both the application router and its asset URLs. A build for `/` will
not work under the studio's `/console/` route. Point the service at that build
and your owned development dispatcher origin:

```sh
./dbl-studio serve \
--sdk /path/to/debuglet \
--ui /path/to/debuglet-dashboard/dist \
--dispatcher https://127.0.0.1:8000 \
--listen 127.0.0.1:8090
```

Open the exact address printed by the command. The editor and its API use the same
loopback origin. `/api/` forwards to the one configured loopback dispatcher.
`--dispatcher` accepts a loopback IP or `localhost`, without an API path. For an owned
development dispatcher with local account sign-in enabled, this supports the
existing console login and submission flow. GitHub/CILogon sign-in through this
loopback proxy is not supported: provider callbacks and their cookies belong to
the hosted console's configured origin. For a provider-only dispatcher, build
locally, import the bundle into its hosted console, and sign in there normally.
The service never supplies login credentials or changes dispatcher policy. Use `--ca-file /path/to/dispatcher-ca.pem` when its HTTPS certificate is
signed by your own CA; certificate and hostname verification remain enabled. Do not
expose or tunnel this service to an untrusted network.

Without `--sdk`, the service can show the editor and validate graphs but cannot
compile. Without `--dispatcher`, editing and compilation work, while sign-in and
submission are unavailable. `--ui` must contain the dashboard's built `index.html`.

## Build and review a bundle

Inspect the compiler's exact block catalog with `./dbl-studio catalog`. For
example, from the core repository:

```sh
./dbl-studio validate --input examples/workflows/http-status.json
./dbl-studio build --sdk /path/to/debuglet \
--input examples/workflows/http-status.json --output http-status.bundle.json
```

A bundle contains the editable graph, its canonical JSON, generated Go, compiled
WASM, destination hints and SHA-256 digests. Import it into the visual editor to
inspect the program, then choose executors and review the normal submission
policy. Editing the graph invalidates the compiled artifact and requires rebuilding.
A digest detects changes; it does not certify that a third-party WASM implements
its claimed source. Review the bundle's origin and rebuild unfamiliar source.

Compilation uses a disposable module, the explicitly configured SDK checkout,
Go 1.26.9 and an offline build with a two-minute deadline. No graph can choose
build flags, fetch a dependency or run a generation command. One build runs at a
time. Workflows are limited to 1 MiB and bundles to 24 MiB. Install the toolchain
and prepare the trusted SDK before starting an offline authoring session.

For a multi-executor experiment, use the experiment editor or these commands:

```sh
./dbl-studio validate-experiment \
--input examples/workflows/experiments/three-node.json
./dbl-studio build-experiment --sdk /path/to/debuglet \
--input examples/workflows/experiments/three-node.json \
--output three-node.experiment-bundle.json
```

The bundle includes the editable experiment, generated source and ordinary
WASM bundle for each participant. Import it into the experiment editor, bind
participants to distinct reachable executors, then choose **Review and run**.
Review the grouped measurement's policies, timing and TEST quote before
submitting. Compilation and import do not submit a measurement.
See [experiment communication](workflows.md#experiments-with-several-executors)
for listener requirements, message limits and request/reply semantics.

The resulting **Measurement group** page shows a participant map. Select a node
to see its reported values; connection details and raw traces remain expandable.
New experiment programs include a connection manifest in their output, so the
map can load after a page reload. It describes declared communication, not
observed message delivery. For older runs, **More → Load experiment map** accepts
the compiled experiment bundle when every participant's program hash matches
the retained run. That imported map is kept only for the current page visit.

## Connect an MCP client

Configure a stdio MCP server in your client's settings:

```json
{
"mcpServers": {
"debuglet": {
"command": "/path/to/dbl-studio",
"args": [
"mcp", "--sdk", "/path/to/debuglet",
"--workspace", "/path/to/my-workflows"
]
}
}
}
```

Available tools:

| Tool | Purpose |
| --- | --- |
| `catalog` | Inspect primitive blocks, typed ports and control outputs. |
| `validate` | Check a workflow using the compiler's validator. |
| `save_workflow` | Save the editable graph under a simple name in the workspace. |
| `get_workflow` | Read that named graph for further editing. |
| `build` | Compile the graph and return a bundle path and artifact metadata. |
| `validate_experiment` | Check participant graphs and their directed message channels. |
| `build_experiment` | Compile the whole experiment and return a portable bundle path. |
| `read_artifact` | Inspect a bundle created in the current session without returning WASM bytes. |

Ask the client to compose a graph, validate it and save it. Open the saved workflow
or bundle in the visual editor, inspect its branches and source, and change it as
needed. The tools have no submission, payment or executor-management action.
They accept workflow names rather than arbitrary paths. A session retains up to
16 build artifact references; files remain in your chosen workspace after exit.

Custom Go blocks remain visible function bodies with declared ports. They execute
with the enclosing guest's permissions and limits, not in an additional per-block
sandbox. Runtime destination permissions and operator network policy still apply;
compiler destination hints do not grant network access. See
[Workflow format](workflows.md) for supported blocks and control-flow rules.
Loading