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
3 changes: 3 additions & 0 deletions .go-arch-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ deps:
- go-stdlib
- zap
- color
- go-hclog

infra-plugin:
mayDependOn:
Expand All @@ -461,9 +462,11 @@ deps:
- proto-plugin
- pkg-httpx
- pkg-registry
- infra-logger
canUse:
- go-stdlib
- yaml
- zap
- go-hclog
- go-plugin
- go-grpc
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ A Go CLI tool for orchestrating AI agents (Claude, Gemini, Codex, GitHub Copilot
- **Actionable Error Hints** - Context-aware suggestions ("Did you mean?") with fuzzy matching, suppressible via `--no-hints`
- **Audit Trail** - Structured JSONL audit log with paired start/end entries per execution, secret masking, configurable path, and atomic writes
- **Distributed Tracing** - OpenTelemetry integration for visibility into workflow execution with spans for steps, agents, parallel/loop blocks, and shell commands; export to any OTLP-compatible backend (Jaeger, Grafana Tempo, Honeycomb) via `--otel-exporter` and `--otel-service-name` flags
- **Plugin System** - Extend AWF with custom operations, validators, and step types via gRPC plugins (HashiCorp go-plugin); validators run custom rules during `awf validate`, custom step types register new `type:` values for workflow steps; event system enables plugins to subscribe to core lifecycle events (`workflow.*`, `step.*`) and emit custom inter-plugin events with glob pattern matching, per-plugin buffered channels, and cycle detection; includes `sdk.Serve()` entry point for plugin authors, and install/update/remove from GitHub Releases with checksum verification
- **Plugin System** - Extend AWF with custom operations, validators, and step types via gRPC plugins (HashiCorp go-plugin); automatic mutual TLS (AutoMTLS) encryption for all host-plugin communication with zero configuration; SHA-256 binary integrity verification at launch time blocks tampered or corrupted plugins; plugin subprocess log and stdout/stderr forwarding to AWF's structured logger for crash diagnostics; validators run custom rules during `awf validate`, custom step types register new `type:` values for workflow steps; event system enables plugins to subscribe to core lifecycle events (`workflow.*`, `step.*`) and emit custom inter-plugin events with glob pattern matching, per-plugin buffered channels, and cycle detection; includes `sdk.Serve()` entry point for plugin authors, and install/update/remove from GitHub Releases with checksum verification
- **Workflow Packs** - Share reusable workflows and prompts via `awf workflow install owner/repo[@version]` from GitHub Releases with manifest validation, checksum verification, and atomic installation; execute with `awf run pack/workflow` namespace syntax; `{{.awf.prompts_dir}}` and `{{.awf.scripts_dir}}` resolve context-aware with 3-tier resolution (user override → pack embedded → global); `call_workflow` within packs resolves relative to pack root; `--global` flag for user-level installation; `awf workflow remove <pack>` for cleanup; source metadata tracking and plugin dependency warnings
- **Built-in GitHub Plugin** - Declarative GitHub operations (get_issue, create_pr, batch) with auth fallback and concurrent execution
- **Built-in HTTP Operation** - Declarative REST API calls (GET, POST, PUT, DELETE) with configurable timeout, response capture, and retryable status codes
Expand Down Expand Up @@ -124,6 +124,7 @@ AWF is a powerful orchestration tool that grants AI agents and workflows direct
| `awf plugin list` | List installed plugins |
| `awf plugin install <owner/repo>` | Install a plugin from GitHub Releases |
| `awf plugin update [name]` | Update an installed plugin |
| `awf plugin verify [name]` | Verify plugin binary integrity (check/update SHA-256 checksums) |
| `awf plugin remove <name>` | Remove an installed plugin |
| `awf plugin search [query]` | Search for plugins on GitHub |
| `awf plugin enable <name>` | Enable a plugin |
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Learn how to use AWF effectively:
- [Notification Operations](user-guide/workflow-syntax.md#notification-operations) - Built-in notification plugin with desktop and webhook backends
- [Retry Configuration](user-guide/retry.md) - Automatic retry with backoff strategies, delay capping, and exit code filtering
- [Templates](user-guide/templates.md) - Reusable workflow templates
- [Plugins](user-guide/plugins.md) - Extend AWF with custom operations, validators, and step types
- [Plugins](user-guide/plugins.md) - Extend AWF with custom operations, validators, and step types; transport security (AutoMTLS, binary integrity verification) and log forwarding
- [Plugin Events](user-guide/plugin-events.md) - Real-time event reactivity between plugins and core
- [Workflow Packs](user-guide/workflow-packs.md) - Install, execute (`awf run pack/workflow`), and manage reusable workflow packs with 3-tier path resolution
- [Terminal UI (TUI)](user-guide/tui.md) - Interactive dashboard for workflow browsing, monitoring, history, and agent conversations
Expand Down
16 changes: 16 additions & 0 deletions docs/reference/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,22 @@ awf run high-throughput

---

### EXECUTION.PLUGIN.CHECKSUM_MISMATCH

**Description:** The SHA-256 checksum of a plugin binary does not match the stored value recorded at install time. The plugin was refused before any code from the binary executed.

**Resolution:** The plugin binary may have been corrupted, accidentally overwritten, or tampered with. Reinstall the plugin with `awf plugin install <owner/repo> --force` to get a fresh copy with a valid checksum. If you intentionally replaced the binary (e.g., local development), run `awf plugin verify --update <name>` to recompute and store the new checksum.

**Example:**
```bash
awf run my-workflow
# Error [EXECUTION.PLUGIN.CHECKSUM_MISMATCH]: plugin "awf-plugin-jira" binary hash mismatch (expected a3f9d4..., got x1y2z3...)
```

**Related codes:** `EXECUTION.COMMAND.FAILED`

---

## SYSTEM Category (Exit Code 4)

Infrastructure and system-level failures.
Expand Down
78 changes: 78 additions & 0 deletions docs/user-guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ title: "CLI Commands"
| `awf plugin list` | List installed plugins |
| `awf plugin install <owner/repo>` | Install a plugin from GitHub releases |
| `awf plugin update [name]` | Update an installed plugin |
| `awf plugin verify [name]` | Verify plugin binary integrity (check/update SHA-256 checksums) |
| `awf plugin remove <name>` | Remove an installed plugin |
| `awf plugin search [query]` | Search for plugins on GitHub |
| `awf plugin enable <name>` | Enable a plugin |
Expand Down Expand Up @@ -913,6 +914,7 @@ awf plugin <subcommand> [flags]
| `list` | List all plugins (use `--operations` to show provided operations) |
| `install <owner/repo>` | Install a plugin from GitHub releases |
| `update [name]` | Update an installed plugin to the latest version |
| `verify [name]` | Verify plugin binary integrity (check/update SHA-256 checksums) |
| `remove <name>` | Remove an installed plugin |
| `search [query]` | Search for available plugins on GitHub |
| `enable <name>` | Enable a disabled plugin |
Expand Down Expand Up @@ -1075,6 +1077,82 @@ awf plugin update --all

---

## awf plugin verify

Verify the integrity of plugin binaries using SHA-256 checksums.

```bash
awf plugin verify [plugin-names...] [flags]
```

### Arguments

| Argument | Description |
|----------|-------------|
| `plugin-names` | One or more plugin names to verify (optional; omit to verify all plugins) |

### Flags

| Flag | Description |
|------|-------------|
| `--update` | Recompute and update stored checksums for the specified plugins |
| `-f, --format` | Output format (text, json) |

### Description

Verifies that installed plugin binaries have not been modified or corrupted by comparing their SHA-256 checksums against stored values. Checksums are recorded automatically during `awf plugin install`. For manually placed or locally-built plugins, use `--update` to compute and store checksums.

**Without arguments:** Verifies all installed plugins.

**With plugin names:** Verifies only the named plugins.

**Without `--update`:** Read-only verification; reports pass/fail/missing status.

**With `--update`:** Recomputes checksums for the named plugins and updates the stored values. Useful for locally-built or manually installed plugins.

### Output

Each plugin is reported with one of three statuses:

| Status | Symbol | Meaning |
|--------|--------|---------|
| Pass | `✓` | Binary matches the stored checksum |
| Fail | `✗` | Binary does NOT match the stored checksum (possible tampering or corruption) |
| Missing | `!` | No stored checksum (plugin will launch without verification) |

Output includes: plugin name, status, expected hash, and actual hash.

### Examples

```bash
# Verify all plugins
awf plugin verify

# Verify a specific plugin
awf plugin verify jira

# Verify multiple plugins
awf plugin verify jira metrics custom

# Verify and update checksums for a locally-built plugin
awf plugin verify custom --update

# Verify all and update a specific plugin's checksum
awf plugin verify --update jira

# JSON output for scripting
awf plugin verify -f json
```

### Errors

| Error | Cause |
|-------|-------|
| `plugin "<name>" is not installed` | Named plugin not found in installed plugins |
| `checksum mismatch` | Plugin binary does not match the stored checksum (possible corruption or tampering) |

---

## awf plugin remove

Remove an installed plugin.
Expand Down
95 changes: 95 additions & 0 deletions docs/user-guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,101 @@ awf plugin enable awf-plugin-github

Plugin state persists across AWF restarts.

#### Verify Plugin Integrity

Verify that installed plugin binaries have not been modified or corrupted:

```bash
# Verify all plugins
awf plugin verify

# Verify a specific plugin
awf plugin verify jira

# Verify and update stored checksums (useful for manually installed or local plugins)
awf plugin verify jira --update
```

The verify command checks the SHA-256 checksum of each plugin binary against a stored value. For plugins installed via `awf plugin install`, the checksum is recorded automatically at install time. For manually placed or locally-built plugins, use `--update` to compute and store their checksums.

**Output example:**

```
Plugin Status Expected Hash Actual Hash
awf-plugin-jira ✓ pass a3f9d4c5e8b2f1g7h8i9j0k1l2m3n4o5p a3f9d4c5e8b2f1g7h8i9j0k1l2m3n4o5p
awf-plugin-metrics ✗ fail b2e8c3d7f6a4h5i9j2k3l4m5n6o7p8q9r x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6n7
awf-plugin-custom ! miss (no stored checksum) c9h8i7j6k5l4m3n2o1p0q1r2s3t4u5v6w
```

- **✓ pass** - Binary matches the stored checksum (integrity verified)
- **✗ fail** - Binary does not match; the plugin may be corrupted or tampered with
- **! miss** - No checksum stored; plugin will launch without verification (use `--update` to enable verification)

### Plugin Security

AWF implements multiple security layers for plugin execution:

#### Automatic Mutual TLS (AutoMTLS)

All host-plugin communication uses automatic mutual TLS encryption by default. AWF and plugin binaries automatically generate ephemeral certificates at startup — no manual key management is required.

**Benefits:**
- Prevents network sniffing of plugin data on shared infrastructure
- Protects secrets passed through plugin communication
- Transparent to end users and plugin authors

**Backward compatibility:** If a plugin binary is built with an older SDK that doesn't support AutoMTLS, the connection automatically downgrades to plaintext with a warning in the logs. The plugin continues to function.

#### Binary Integrity Verification

AWF verifies the SHA-256 checksum of each plugin binary before launching it. This prevents execution of corrupted or tampered binaries.

**When verification happens:**
- For plugins installed via `awf plugin install`: checksum is verified automatically at runtime using the stored value from install time
- For manually placed plugins: use `awf plugin verify --update` to enable checksum verification

**When verification is skipped:**
- Plugins without a stored checksum launch with a warning recommending checksum verification
- This allows existing plugins installed before this feature to continue functioning

**Example: Detecting a Tampered Plugin**

```bash
# After installing a plugin, its checksum is stored
$ awf plugin install myorg/awf-plugin-jira
✓ Installed awf-plugin-jira v1.2.0

# If the binary is modified later (e.g., by disk corruption or supply chain attack)
$ echo "malware" >> ~/.local/share/awf/plugins/awf-plugin-jira/awf-plugin-jira
$ awf run my-workflow
Error: plugin "awf-plugin-jira" checksum mismatch
Expected: a3f9d4c5e8b2f1g7h8i9j0k1l2m3n4o5p
Actual: x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6n7

# The plugin is refused and workflow execution stops
```

#### Plugin Output Forwarding

Plugin logs and subprocess output (stdout/stderr) are forwarded to AWF's log output with structured context. This aids debugging when plugins crash or behave unexpectedly.

**Plugin sources:**
- Plugin-emitted structured logs (via hclog)
- Plugin panic output
- Direct writes to stdout/stderr

**Log level:** Plugin output is forwarded at the INFO level for structured logs and WARN level for panic/output capture. AWF's configured log level (e.g., `--quiet`, `--verbose`) filters what appears in the final output.

**Example:**

```bash
$ awf run workflow --verbose
[INFO] Starting plugin awf-plugin-metrics...
[INFO] plugin=awf-plugin-metrics: Listening on port 50051
[INFO] plugin=awf-plugin-metrics: Registered collectors: cpu, memory, disk
[WARN] plugin=awf-plugin-jira: Deprecated API v2 used — upgrade to v3 recommended
```

### Using Plugin Operations

Plugins register custom operations that can be used in workflow steps:
Expand Down
3 changes: 3 additions & 0 deletions internal/domain/errors/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ const (

// ErrorCodeExecutionEventBufferFull indicates the event buffer capacity was exceeded.
ErrorCodeExecutionEventBufferFull ErrorCode = "EXECUTION.EVENT.BUFFER_FULL"

// ErrorCodeExecutionPluginChecksumMismatch indicates plugin binary checksum verification failed.
ErrorCodeExecutionPluginChecksumMismatch ErrorCode = "EXECUTION.PLUGIN.CHECKSUM_MISMATCH"
)

// Error code constants for SYSTEM category (exit code 4).
Expand Down
17 changes: 17 additions & 0 deletions internal/domain/errors/codes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,23 @@ func TestErrorCode_Taxonomy_Coverage(t *testing.T) {
})
}

func TestErrorCodeExecutionPluginChecksumMismatch_Constant(t *testing.T) {
assert.Equal(t, "EXECUTION.PLUGIN.CHECKSUM_MISMATCH", string(errors.ErrorCodeExecutionPluginChecksumMismatch))
}

func TestErrorCodeExecutionPluginChecksumMismatch_ExitCode(t *testing.T) {
assert.Equal(t, 3, errors.ErrorCodeExecutionPluginChecksumMismatch.ExitCode())
}

func TestErrorCodeExecutionPluginChecksumMismatch_IsValid(t *testing.T) {
code := errors.ErrorCodeExecutionPluginChecksumMismatch

assert.True(t, code.IsValid())
assert.Equal(t, "EXECUTION", code.Category())
assert.Equal(t, "PLUGIN", code.Subcategory())
assert.Equal(t, "CHECKSUM_MISMATCH", code.Specific())
}

func TestErrorCode_Taxonomy_Subcategories(t *testing.T) {
// Verify expected subcategories exist
subcategoryTests := []struct {
Expand Down
2 changes: 2 additions & 0 deletions internal/domain/pluginmodel/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ type PluginState struct {
Config map[string]any `json:"config,omitempty"`
DisabledAt int64 `json:"disabled_at,omitempty"`
SourceData map[string]any `json:"source_data,omitempty"`
Checksum string `json:"checksum,omitempty"`
ChecksumAt int64 `json:"checksum_at,omitempty"`
}

func NewPluginState() *PluginState {
Expand Down
52 changes: 52 additions & 0 deletions internal/domain/pluginmodel/state_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pluginmodel_test

import (
"encoding/json"
"testing"

"github.com/awf-project/cli/internal/domain/pluginmodel"
Expand Down Expand Up @@ -36,3 +37,54 @@ func TestPluginState_SourceDataAcceptsArbitraryMetadata(t *testing.T) {
assert.Equal(t, "awf-plugin-echo", state.SourceData["repo"])
assert.Equal(t, 42, state.SourceData["stars"])
}

func TestNewPluginState_ChecksumEmptyAndZeroByDefault(t *testing.T) {
state := pluginmodel.NewPluginState()

assert.Empty(t, state.Checksum)
assert.Zero(t, state.ChecksumAt)
}

func TestPluginState_JSONRoundtrip_PreservesChecksumFields(t *testing.T) {
state := &pluginmodel.PluginState{
Enabled: true,
Checksum: "a3f5b2c8d1e4f7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7c0d3e6f9a2",
ChecksumAt: 1735689600,
}

data, err := json.Marshal(state)
require.NoError(t, err)

var restored pluginmodel.PluginState
err = json.Unmarshal(data, &restored)
require.NoError(t, err)

assert.Equal(t, state.Checksum, restored.Checksum)
assert.Equal(t, state.ChecksumAt, restored.ChecksumAt)
}

func TestPluginState_JSONUnmarshal_BackwardCompatWithoutChecksumFields(t *testing.T) {
payload := `{"enabled":true,"config":{"key":"value"}}`

var state pluginmodel.PluginState
err := json.Unmarshal([]byte(payload), &state)
require.NoError(t, err)

assert.Empty(t, state.Checksum)
assert.Zero(t, state.ChecksumAt)
}

func TestPluginState_JSONMarshal_OmitsChecksumFieldsWhenEmpty(t *testing.T) {
state := pluginmodel.NewPluginState()

data, err := json.Marshal(state)
require.NoError(t, err)

var raw map[string]any
require.NoError(t, json.Unmarshal(data, &raw))

_, hasChecksum := raw["checksum"]
_, hasChecksumAt := raw["checksum_at"]
assert.False(t, hasChecksum, "checksum field should be omitted when empty")
assert.False(t, hasChecksumAt, "checksum_at field should be omitted when zero")
}
Loading
Loading