diff --git a/AGENTS.md b/AGENTS.md index 0dd1a519..c53b5cb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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":{...}}, ...]} diff --git a/CHANGELOG.md b/CHANGELOG.md index 57336efb..f64232bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/README.md b/README.md index 38a1997c..d06ea0a8 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,35 @@ These commands exist so external test tools can drive trond programmatically. | `trond events [--follow] [--since ]` | 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 ` | 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 | @@ -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) | @@ -478,6 +509,7 @@ name: # 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 @@ -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) diff --git a/SECURITY.md b/SECURITY.md index 92dc5d9c..8bbeadf4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 @@ -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 `/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). diff --git a/cmd/network/network.go b/cmd/network/network.go index 6833d85b..54183ab3 100644 --- a/cmd/network/network.go +++ b/cmd/network/network.go @@ -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 diff --git a/cmd/output_format_test.go b/cmd/output_format_test.go new file mode 100644 index 00000000..0725faed --- /dev/null +++ b/cmd/output_format_test.go @@ -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()) + } + } +} diff --git a/cmd/root.go b/cmd/root.go index 12f30917..ba7c0a3f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,6 @@ var ( quiet bool verbose bool noColor bool - configFile string stateDirFlag string ) @@ -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 }, } @@ -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 @@ -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)) + } +} diff --git a/internal/knowledge/files/test-harness.md b/internal/knowledge/files/test-harness.md index b67c5f79..9a63ab5e 100644 --- a/internal/knowledge/files/test-harness.md +++ b/internal/knowledge/files/test-harness.md @@ -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 diff --git a/internal/output/exitcode.go b/internal/output/exitcode.go index d8b79429..77e98e1d 100644 --- a/internal/output/exitcode.go +++ b/internal/output/exitcode.go @@ -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. @@ -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: diff --git a/internal/render/templates/private_net_config.conf b/internal/render/templates/private_net_config.conf index 8c58f192..0573c9df 100644 --- a/internal/render/templates/private_net_config.conf +++ b/internal/render/templates/private_net_config.conf @@ -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. ] diff --git a/knowledge/test-harness.md b/knowledge/test-harness.md index b67c5f79..9a63ab5e 100644 --- a/knowledge/test-harness.md +++ b/knowledge/test-harness.md @@ -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 diff --git a/private_net_config.conf b/private_net_config.conf index 8c58f192..0573c9df 100644 --- a/private_net_config.conf +++ b/private_net_config.conf @@ -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. ] diff --git a/specs/001-trond-cli-platform/contracts/cli-contract.md b/specs/001-trond-cli-platform/contracts/cli-contract.md index 0c1fa484..1c4e7b87 100644 --- a/specs/001-trond-cli-platform/contracts/cli-contract.md +++ b/specs/001-trond-cli-platform/contracts/cli-contract.md @@ -9,12 +9,13 @@ Every command accepts: | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| -| --output | -o | enum | text | Output format: text, json, yaml | -| --log-format | | enum | text | Log format: text, json | +| --output | -o | enum | text | Output format: text, json. Any other value is refused with exit 2 | +| --log-format | | enum | text | Log format: text, json. Any other value is refused with exit 2 | | --quiet | -q | bool | false | Suppress non-essential output | | --verbose | -v | bool | false | Increase log verbosity | | --no-color | | bool | false | Disable ANSI colors | -| --config | | string | ~/.trond/config.yaml | trond config file path | +| --state-dir | | string | ~/.trond | Directory for state.json, audit.log, deployments (env: TROND_STATE_DIR) | +| --require-private | | bool | false | Refuse to mutate any non-private node; one-way (env: TROND_REQUIRE_PRIVATE) | ## Exit Codes @@ -25,7 +26,7 @@ Every command accepts: | 2 | VALIDATION_ERROR | Intent file or config validation failed | | 3 | TARGET_UNREACHABLE | SSH connection failed or Docker not available | | 4 | PREFLIGHT_FAILURE | Target does not meet requirements | -| 5 | PARTIAL_SUCCESS | Multi-node operation: some succeeded, some failed | +| _(5 unassigned)_ | — | Reserved. Partial multi-node results exit 1 with `error_code: "PARTIAL_SUCCESS"`; the distinction is in the JSON payload, not the exit status | | 10 | HUMAN_REQUIRED | Destructive change in non-interactive mode without --auto-approve | Exit codes are stable across minor versions. New codes may be added in minor releases;