From d221dec3661b215bbeb50e98070d5770a3065036 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sun, 19 Jul 2026 14:34:34 -0700 Subject: [PATCH] Prevent run history for unknown commands --- cli/bash/commands/basectl/basectl.sh | 25 ++++++++--- .../basectl/tests/runtime-dispatch.bats | 43 +++++++++++++++++++ docs/observability.md | 9 +++- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/cli/bash/commands/basectl/basectl.sh b/cli/bash/commands/basectl/basectl.sh index 51b97b1f..dea75221 100644 --- a/cli/bash/commands/basectl/basectl.sh +++ b/cli/bash/commands/basectl/basectl.sh @@ -509,6 +509,24 @@ basectl_history_recordable_command() { esac } +basectl_validate_command() { + local command="${1:-}" + + case "$command" in + ""|activate|check|test|export-context|devcontainer|devenv-report|build|demo|run|repo|ci|release|prompt|docs|clean|logs|history|config|trust|doctor|gh|onboard|setup|help|projects|workspace|update|update-profile|version) + return 0 + ;; + *) + if [[ -f "$command" ]]; then + basectl_bare_script_usage_error "$command" + else + basectl_usage_error "Unrecognized command: $command" + fi + return $? + ;; + esac +} + basectl_args_request_help() { local argument for argument in "$@"; do @@ -778,6 +796,7 @@ basectl_main() { basectl_history_recordable_command "$command" || run_bundle_enabled=0 basectl_get_base_home || return 1 + basectl_validate_command "$command" || return $? if ((keep_temp)); then export BASE_CLI_KEEP_TEMP=true fi @@ -830,11 +849,7 @@ basectl_main() { fi ;; *) - if [[ -f "$command" ]]; then - basectl_bare_script_usage_error "$command" - else - basectl_usage_error "Unrecognized command: $command" - fi + basectl_validate_command "$command" command_status=$? ;; esac diff --git a/cli/bash/commands/basectl/tests/runtime-dispatch.bats b/cli/bash/commands/basectl/tests/runtime-dispatch.bats index 9c526962..615f5af2 100644 --- a/cli/bash/commands/basectl/tests/runtime-dispatch.bats +++ b/cli/bash/commands/basectl/tests/runtime-dispatch.bats @@ -26,6 +26,49 @@ load ./basectl_helpers.bash } +@test "basectl rejects unknown commands before creating persistent runtime state" { + local cache_root="$TEST_TMPDIR/cache" + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_CACHE_DIR="$cache_root" \ + bash -c ' + source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh" + basectl_main invalid + ' + + [ "$status" -eq 2 ] + [[ "$output" == *"Unrecognized command: invalid"* ]] + [ ! -e "$cache_root/base/runs" ] + [ ! -e "$cache_root/base/history/runs.jsonl" ] +} + + +@test "basectl keeps a run bundle for a recognized command failure" { + local cache_root="$TEST_TMPDIR/cache" + local run_root + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_CACHE_DIR="$cache_root" \ + bash -c ' + source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh" + log_debug() { :; } + basectl_do_setup() { return 7; } + basectl_history_record() { :; } + basectl_main setup + ' + + [ "$status" -eq 7 ] + run_root="$(find "$cache_root/base/runs" -mindepth 1 -maxdepth 1 -type d -print -quit)" + [ -n "$run_root" ] + [ -f "$run_root/run.json" ] + [ -f "$run_root/logs/primary.log" ] +} + + @test "basectl labels run bundle directories without changing the run ID" { run env \ BASE_HOME="$BASE_REPO_ROOT" \ diff --git a/docs/observability.md b/docs/observability.md index a4bb1a5c..56f6eb3a 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -72,8 +72,13 @@ History writes are best-effort: - never fail the user command because the history file cannot be written - ignore malformed history lines while warning in debug output -`basectl history` shows one record per public invocation. Legacy internal rows -are ignored so old caches do not reintroduce duplicate command entries. +`basectl history` shows one record per accepted public command invocation. +Top-level command and wrapper-syntax errors are rejected before persistent +runtime state is initialized, so they do not create a run bundle or history +record. Once a recognized command is accepted, its run remains observable even +if leaf-level argument validation, environment checks, or execution later +fails. Legacy internal rows are ignored so old caches do not reintroduce +duplicate command entries. ## Record Shape