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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ Unset (default) → no-op tracer, zero overhead, no network IO.
| 4 | preflight failure (missing dependency on target) | DO NOT retry — present `missing_components` array to user; offer `trond bootstrap` |
| 10 | `HUMAN_REQUIRED` — destructive op needs explicit confirmation | DO NOT silently retry with `--auto-approve` / `--confirm` unless the user has authorised that for this session |

There is no exit code for partial success. A multi-node command where some
nodes succeeded and others failed exits **1** with `error_code:
"PARTIAL_SUCCESS"` and a `failed` array naming the nodes — so branch on
`error_code`, not on the exit status alone, before retrying. Re-running the
whole command is usually wrong: the nodes that succeeded are already in the
requested state.

Every error JSON has the same shape:

```json
Expand Down Expand Up @@ -394,6 +401,9 @@ trond preflight --intent my-net.yaml -o json

# 4. Create the whole network in one shot. trond auto-wires
# node.active between siblings so peering works under auto_ports.
# SR_KEY below is the PUBLIC private-net key baked into
# private_net_config.conf's genesis. It is safe here and only here —
# never reuse it on a network that carries value.
SR_KEY=a31d54825aea2fc5127e3bd435fc2346021313005e5f304ab33372432784acae \
trond network create --intent my-net.yaml --wait -o json
# Output: {"network":"pn", "nodes":[{"name":"pn-node0", "endpoints":{...}}, ...]}
Expand Down
19 changes: 12 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ The agent-ergonomics arc landed across four sequenced PRs:

**Foundation**

- 32 CLI commands across lifecycle (apply / stop / start / restart / upgrade /
rollback / remove), configuration (validate / render / diff / docs),
observability (status / list / logs / health / diagnose / verify / inspect /
events), test-harness SDK (exec / files / wait), chaos primitives
(disconnect / connect / partition / heal), private networks (create / add /
status / destroy), environment (preflight / bootstrap), knowledge base, and
meta (version / completion / help)
- 40 top-level CLI commands (plus subcommands) across lifecycle (apply /
plan / stop / start / restart / upgrade / rollback / remove), configuration
(config validate / render / diff / docs, verify-config), observability
(status / list / logs / health / diagnose / verify / inspect / events /
doctor), test-harness SDK (exec / files / wait), chaos primitives
(disconnect / connect / partition / heal / auto-heal), private networks
(network create / add / status / destroy / upgrade), snapshots (download /
clone / list / sources / jobs / logs / stop / prune), builds from source
(build / list / inspect / prune), shadow-fork (mutate), agent interfaces
(schema / mcp / recipe list / show / validate / run), environment
(preflight / bootstrap), knowledge base, and meta (version / completion /
help)
- Declarative intent.yaml schema covering ~50 fields:
target (local/ssh, runtime, auto_ports), node (type, version, image, ports,
resources, jvm, storage, restart, extra_env, extra_args, labels, networks,
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,35 @@ These commands exist so external test tools can drive trond programmatically.
| `trond events [--follow] [--since <dur>]` | Stream the audit log as JSONL |
| `trond knowledge [topic]` | Query embedded deployment guidance |

### Agent interfaces

The commands below exist so an AI agent — or any program driving trond
without a shell — can discover the CLI, run canonical workflows, and
reconcile desired state against reality. `AGENTS.md` is the reference for
calling trond programmatically.

| Command | Description |
|---|---|
| `trond schema` | Emit the entire CLI surface as structured data: every subcommand with flags, types, defaults, examples, and the JSON Schema of its `--output json` result |
| `trond mcp` | Start a Model Context Protocol server over stdin/stdout, so MCP-aware clients call trond as tools rather than shelling out |
| `trond recipe list` / `show` / `validate` / `run` | Canonical multi-step workflows as declarative YAML — run "deploy a fresh mainnet fullnode from a snapshot" as one command instead of chaining five |
| `trond verify-config <node>` | Diff the `.conf` a running node actually uses against what trond would render now from `--intent`; read `in_sync` to detect drift |
| `trond doctor` | Report trond's own health: version, state directory, docker availability, file permissions, and optionally whether a newer release exists |

### Shadow-fork

Take a real chain-DB snapshot, replace the witness set, fund accounts and
change protocol parameters, then start a private network from that modified
state — for rehearsing hard-fork upgrades, witness-set transitions, and
contract behaviour against state that actually exists on chain.

| Command | Description |
|---|---|
| `trond shadow-fork mutate` | Apply a `fork.conf` to a halted java-tron data directory |

`scripts/poc-shadow-fork.sh` drives the whole pipeline end to end; see
`trond knowledge shadow-fork-poc`.

### Global flags

| Flag / env | Effect |
Expand All @@ -400,6 +429,8 @@ These commands exist so external test tools can drive trond programmatically.
| `--log-format text\|json` | Log format on stderr |
| `-q, --quiet` / `-v, --verbose` | Output verbosity |
| `--no-color` | Disable ANSI colors |
| `--require-private` / `TROND_REQUIRE_PRIVATE` | Refuse to mutate any non-private node — a one-way floor, see [Private-network safety](#private-network-safety-for-unattended-agents) |
| `--version` | Print version, commit and build time, then exit |
| `TROND_TEMPLATES_DIR` | Override the embedded HOCON templates |
| `TROND_SSH_ACCEPT_NEW_HOSTS=1` | TOFU mode for SSH host keys (refuses on mismatch — never trusts a key change) |

Expand Down Expand Up @@ -478,6 +509,7 @@ name: <dns-label> # required, ^[a-z0-9][a-z0-9-]{0,62}$
network: mainnet|nile|private # required
target: { ... } # required, see below
nodes: [ { ... }, ... ] # required, at least one node
monitoring: { ... } # optional, Prometheus + Grafana — see Monitoring above
```

### Target
Expand Down Expand Up @@ -505,6 +537,13 @@ nodes:
process_manager: systemd|nohup # jar runtime, default systemd
system_user: tron # jar runtime user

# Jar runtime: where to fetch FullNode.jar. http/file/ftp are refused,
# and sha256 is mandatory whenever url is set — an unverified jar is
# the whole node.
jar:
url: https://example.com/FullNode.jar # https only
sha256: <64-char lowercase hex> # required alongside url

# Witness-only
witness_key: # preferred: structured block
private_key_env: SR_PRIVATE_KEY # env var holding the hex key (inlined into HOCON at apply)
Expand Down
11 changes: 8 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

| Version | Supported |
|---|---|
| 0.1.x (alpha) | Yes |
| 0.1.x | Yes |

`0.1.x` is a `0.x` series under semantic versioning: the intent schema, the
`--output json` schemas and the exit-code contract may still change between
minor releases. Security fixes land on the newest `0.1.x`.

## Reporting a Vulnerability

Expand Down Expand Up @@ -41,9 +45,10 @@ Include:

trond incorporates the following security measures:

- **Private key protection**: Witness private keys are passed via environment variables, never stored in intent files. The `PrivateKey` type redacts values in all string representations and JSON serialization.
- **Private key protection**: Witness private keys are passed via environment variables, never written into intent files, and the `PrivateKey` type redacts them in every string representation and JSON serialization. They are not, however, absent from disk: at apply time the key is inlined into the rendered HOCON, because that is where java-tron reads `localwitness` from. That file is created 0600. Treat the rendered config as key material.
- **Public private-net key**: `private_net_config.conf` ships a *published* witness key (`a31d54…acae`, address `TM4yToQ1njkcFwi3ADY5x6dbdfNekU3rVi`) because the private network's genesis block is derived from it — without it the template cannot produce blocks. It is public, it is in this repository, and it must never be used on mainnet, Nile, or any network carrying value.
- **SSH command whitelist**: Only pre-approved commands are executed over SSH connections.
- **Audit log**: All mutating operations (apply, stop, start, remove, upgrade, rollback, network create/destroy) are logged to `~/.trond/audit.log` in append-only JSONL format.
- **Audit log**: All mutating operations (apply, stop, start, remove, upgrade, rollback, network create/destroy) are logged in append-only JSONL format to `~/.trond/audit.log`, or to `<dir>/audit.log` when `--state-dir` / `TROND_STATE_DIR` relocates the state directory.
- **Confirmation gates**: Destructive operations (remove, destroy) require explicit `--confirm` flags.
- **State file permissions**: State and audit files are created with restricted permissions (0600/0700).

Expand Down
5 changes: 4 additions & 1 deletion cmd/network/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ func init() {
Cmd.AddCommand(createCmd)
Cmd.AddCommand(statusCmd)
Cmd.AddCommand(destroyCmd)
Cmd.AddCommand(upgradeCmd)
// upgradeCmd registers itself in upgrade.go's init(), next to the
// MarkFlagRequired calls that must run with it. Adding it here too put
// it in the parent's child list twice, so `network --help` printed the
// row twice. `add` follows the same self-registering pattern.
}

// auditEvent mirrors the struct in the cmd package. The network subcommand
Expand Down
38 changes: 38 additions & 0 deletions cmd/output_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cmd

import (
"strings"
"testing"

"github.com/tronprotocol/tron-deployment/internal/output"
)

// An unrecognised --output used to fall through to the text writer, so a
// caller asking for machine-readable output got a human table and exit 0.
// It must be refused as a validation error instead.
func TestValidateFormat(t *testing.T) {
for _, ok := range []string{"text", "json"} {
if err := validateFormat("--output", ok); err != nil {
t.Errorf("%q must be accepted, got %v", ok, err)
}
}

// "yaml" is called out specifically: contracts/cli-contract.md advertised
// it as a supported value while no writer ever implemented it.
for _, bad := range []string{"yaml", "xml", "JSON", "", "tex"} {
err := validateFormat("--output", bad)
if err == nil {
t.Fatalf("%q must be rejected", bad)
}
se, isStructured := err.(*output.StructuredError)
if !isStructured {
t.Fatalf("%q: want *output.StructuredError, got %T", bad, err)
}
if se.ExitCode != output.ExitValidationError {
t.Errorf("%q: exit %d, want %d", bad, se.ExitCode, output.ExitValidationError)
}
if !strings.Contains(se.Error(), bad) {
t.Errorf("%q: message should quote the offending value, got %q", bad, se.Error())
}
}
}
36 changes: 33 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ var (
quiet bool
verbose bool
noColor bool
configFile string
stateDirFlag string
)

Expand Down Expand Up @@ -51,10 +50,22 @@ for CI pipelines and AI agents.`,
SilenceErrors: true,
// Apply --state-dir before any subcommand runs so subpackages
// (cmd/network, cmd/config) see the same base.
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Reject an unknown --output rather than falling through to text.
// The writers switch on "json" and default to text, so `-o yaml`
// (which an older contract doc advertised) or a plain typo used to
// return a human table to a caller that asked for machine output —
// silently, with exit 0. An agent then parses a table as JSON.
if err := validateFormat("--output", outputFormat); err != nil {
return err
}
if err := validateFormat("--log-format", logFormat); err != nil {
return err
}
if stateDirFlag != "" {
paths.SetBaseDir(stateDirFlag)
}
return nil
},
}

Expand All @@ -69,7 +80,11 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Suppress non-essential output")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Increase log verbosity")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable ANSI colors")
rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "Config file (default ~/.trond/config.yaml)")
// No --config flag. One was registered here and advertised in --help as
// "Config file (default ~/.trond/config.yaml)", but nothing ever read the
// variable: passing --config /nonexistent succeeded silently. There is no
// config-file feature, so the flag is gone rather than left lying about a
// setting it does not have. --state-dir below is the real knob.
rootCmd.PersistentFlags().StringVar(&stateDirFlag, "state-dir", "", "Directory for state.json, audit.log, deployments (default ~/.trond, env: TROND_STATE_DIR)")
// Persistent safety gate: refuse any mutating verb unless the node's
// network is private. A one-way floor — this flag OR a truthy
Expand Down Expand Up @@ -138,3 +153,18 @@ func mustMarkRequired(cmd *cobra.Command, name string) {
panic(fmt.Sprintf("mark flag %q required on %s: %v", name, cmd.Name(), err))
}
}

// validateFormat rejects a format value the writers cannot honour. Both
// output.Write* helpers switch on "json" and fall through to text for
// anything else, so without this an unrecognised value degrades silently:
// `-o yaml` (advertised by an old contract doc) or a typo returned a human
// table, with exit 0, to a caller that asked for machine-readable output.
func validateFormat(flag, value string) error {
switch value {
case "text", "json":
return nil
default:
return output.NewError("VALIDATION_ERROR", output.ExitValidationError,
fmt.Sprintf("unknown %s %q: expected text or json", flag, value))
}
}
6 changes: 6 additions & 0 deletions internal/knowledge/files/test-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ the genesis address `TM4yToQ1njkcFwi3ADY5x6dbdfNekU3rVi` baked into the
`private_net_config.conf` template. Use that exact key unless you also
supply a fresh genesis block.

This key is **public** — it ships in the template and in this document, so
treat any rig using it as unauthenticated: anyone who can reach the node can
sign as the witness and spend the genesis balance. That is fine for a
throwaway test network on a trusted host and unacceptable anywhere else.
Never reuse it on Mainnet, Nile, or any network carrying value.

## Tear-down: always clean up

```bash
Expand Down
23 changes: 14 additions & 9 deletions internal/output/exitcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ package output
// Exit codes per contracts/cli-contract.md.
// Stable across minor versions.
const (
ExitSuccess = 0 // Operation completed successfully or no changes needed
ExitGeneralError = 1 // Unclassified error
ExitValidationError = 2 // Intent file or config validation failed
ExitTargetUnreachable = 3 // SSH connection failed or Docker not available
ExitPreflightFailure = 4 // Target does not meet requirements
ExitPartialSuccess = 5 // Multi-node operation: some succeeded, some failed
ExitHumanRequired = 10 // Destructive change in non-interactive mode without --auto-approve
ExitSuccess = 0 // Operation completed successfully or no changes needed
ExitGeneralError = 1 // Unclassified error
ExitValidationError = 2 // Intent file or config validation failed
ExitTargetUnreachable = 3 // SSH connection failed or Docker not available
ExitPreflightFailure = 4 // Target does not meet requirements
// No code 5. A multi-node command that partially succeeds returns
// ExitGeneralError with `error_code: "PARTIAL_SUCCESS"` in the JSON
// (see cmd/network/destroy.go) — the machine-readable distinction lives
// in the payload, not the exit status. A `ExitPartialSuccess = 5`
// constant sat here unreferenced by any command; callers that branched
// on 5 would have waited forever for a code nothing emitted. 5 is left
// unassigned rather than reused, so it stays available if the split is
// ever wanted.
ExitHumanRequired = 10 // Destructive change in non-interactive mode without --auto-approve
)

// ExitCodeName returns a human-readable name for an exit code.
Expand All @@ -25,8 +32,6 @@ func ExitCodeName(code int) string {
return "TARGET_UNREACHABLE"
case ExitPreflightFailure:
return "PREFLIGHT_FAILURE"
case ExitPartialSuccess:
return "PARTIAL_SUCCESS"
case ExitHumanRequired:
return "HUMAN_REQUIRED"
default:
Expand Down
15 changes: 15 additions & 0 deletions internal/render/templates/private_net_config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,21 @@ genesis.block = {

//localWitnessAccountAddress =

// ---------------------------------------------------------------------------
// WARNING: the key below is PUBLIC. It is committed to this repository and to
// every copy of it, and it is printed in this project's documentation.
//
// It is here on purpose: the genesis block above is derived from the matching
// address, so a private network built from this template cannot produce blocks
// without it. That is the only thing it is for.
//
// NEVER use this key on Mainnet, Nile, or any network that carries value, and
// never leave it in place on a private network reachable from an untrusted
// host — anyone can sign as this witness and spend the genesis balance.
// For anything that is not a throwaway rig, generate your own keypair with a
// TRON keytool and set a matching genesis block in the `genesis.block` section
// above — the two must agree or the node will not produce.
// ---------------------------------------------------------------------------
localwitness = [
a31d54825aea2fc5127e3bd435fc2346021313005e5f304ab33372432784acae # you must enable this value and the witness address TM4yToQ1njkcFwi3ADY5x6dbdfNekU3rVi are matched.
]
Expand Down
6 changes: 6 additions & 0 deletions knowledge/test-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ the genesis address `TM4yToQ1njkcFwi3ADY5x6dbdfNekU3rVi` baked into the
`private_net_config.conf` template. Use that exact key unless you also
supply a fresh genesis block.

This key is **public** — it ships in the template and in this document, so
treat any rig using it as unauthenticated: anyone who can reach the node can
sign as the witness and spend the genesis balance. That is fine for a
throwaway test network on a trusted host and unacceptable anywhere else.
Never reuse it on Mainnet, Nile, or any network carrying value.

## Tear-down: always clean up

```bash
Expand Down
15 changes: 15 additions & 0 deletions private_net_config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,21 @@ genesis.block = {

//localWitnessAccountAddress =

// ---------------------------------------------------------------------------
// WARNING: the key below is PUBLIC. It is committed to this repository and to
// every copy of it, and it is printed in this project's documentation.
//
// It is here on purpose: the genesis block above is derived from the matching
// address, so a private network built from this template cannot produce blocks
// without it. That is the only thing it is for.
//
// NEVER use this key on Mainnet, Nile, or any network that carries value, and
// never leave it in place on a private network reachable from an untrusted
// host — anyone can sign as this witness and spend the genesis balance.
// For anything that is not a throwaway rig, generate your own keypair with a
// TRON keytool and set a matching genesis block in the `genesis.block` section
// above — the two must agree or the node will not produce.
// ---------------------------------------------------------------------------
localwitness = [
a31d54825aea2fc5127e3bd435fc2346021313005e5f304ab33372432784acae # you must enable this value and the witness address TM4yToQ1njkcFwi3ADY5x6dbdfNekU3rVi are matched.
]
Expand Down
Loading
Loading