# Minimal Viable Agent — Stage 1 Local-first agent with a deterministic planner. No LLM, no external dependencies. ## What Is Implemented - Goal → Plan → Step selection → Execute → Observe → Update state → Finish - Deterministic keyword-based planner (no LLM at Stage 1) - Four step kinds: `shell`, `write_file`, `read_file`, `finish` - Shell allowlist enforcement (`shell=False`, no raw strings) - File ops path traversal guard (resolves + prefix check) - State persistence per run (`workspace/runs//state.json`) - Append-only JSONL audit log (`workspace/runs//audit.jsonl`) ## Project Structure ``` agent/ main.py CLI entry point core/ state.py Dataclass models: AgentState, Plan, PlanStep, StepResult controller.py Builds state, inits AuditLogger/Executor, starts run agent_loop.py Main select → execute → update loop planner/ planner.py Deterministic plan builder step_selector.py Picks next PENDING step runtime/ executor.py Dispatches steps to tools, catches all exceptions tool_registry.py Maps kind strings to handler functions tools/ shell.py Safe shell execution (allowlist, shell=False) file_ops.py write_file, read_file, finish handlers telemetry/ audit_logger.py JSONL audit log writer workspace/ runs// state.json Full run state (updated after each step) audit.jsonl Append-only event log ``` ## How to Run ```bash cd agent/ python main.py --goal "inspect project" python main.py --goal "create file hello.txt with text hello world" python main.py --goal "проверь что тут есть" python main.py --goal "anything else" # default plan python main.py --goal "inspect project" --run-id my-run-01 ``` ## Allowed Shell Commands `pwd`, `ls`, `echo`, `cat`, `python --version`, `git status` Blocked: `bash`, `sh`, `rm`, `sudo`, `curl`, `wget`, `chmod`, `chown`, `docker`, `systemctl` ## Negative Test Scenarios ```bash # Shell security python main.py --goal "test deny bash" # blocked: bash is in BLOCKED_COMMANDS python main.py --goal "test deny rm" # blocked: rm is in BLOCKED_COMMANDS python main.py --goal "test deny curl" # blocked: curl is in BLOCKED_COMMANDS python main.py --goal "test forbidden command" # alias for test deny bash # File security python main.py --goal "test traversal" # blocked: ../escape.txt outside workspace python main.py --goal "test absolute path" # blocked: /tmp/test.txt outside workspace ``` All negative scenarios exit with code 1 and produce `status: failed` in state.json. No blocked command or file is ever executed or created. ## Final State Semantics | Scenario | `run.status` | `current_step_id` | `history` | |---|---|---|---| | All steps done | `done` | `null` | all steps | | Step failed | `failed` | `null` | steps up to failure | | MAX_STEPS exceeded | `failed` | `null` | all executed steps | `current_step_id`: - Set to the step being executed **during** a step - Reset to `null` when the run terminates (done or failed) ## Security Boundaries **Shell:** - `subprocess.run(..., shell=False)` — always, unconditionally - BLOCKED_COMMANDS checked before ALLOWED_COMMANDS — explicit deny wins - Commands not in ALLOWED_COMMANDS are denied even if not in BLOCKED_COMMANDS - `python` and `git` have restricted sub-commands: only `--version` and `status` respectively **File ops:** - All paths resolved with `Path.resolve()` before access - Prefix check: `str(target).startswith(str(WORKSPACE_ROOT))` - Blocks: `../` traversal, absolute paths, normalized mixed traversal - WORKSPACE_ROOT = `agent/workspace/` (confirmed via `parents[2]`) ## Audit Events (per run) Every run produces these events in order: ``` run_started → plan_created → state_saved → (per step: step_started → step_finished → state_saved) → run_finished ``` ## Run Directory Layout ``` workspace/runs// audit.jsonl one JSON record per line, append-only state.json full AgentState snapshot, overwritten after each step ``` ## Stage 2A — Inter-Step Data Flow (Templates) Steps can reference outputs of previous steps via template expressions in `payload` values. ### Supported expressions | Expression | Resolves to | |---|---| | `{{ last.output_text }}` | `output_text` of the most recently completed step | | `{{ steps.step-1.output_text }}` | `output_text` of the step with `step_id = "step-1"` | ### How it works 1. Before each step executes, `has_templates(payload)` checks for `{{ ... }}` patterns 2. If found, `resolve_payload(payload, state)` substitutes all expressions 3. Executor receives the fully-resolved payload — tools never see raw templates 4. If any expression fails (step not in history, no output), the step is marked `FAILED` immediately, run terminates ### Example plan ``` step-1: shell { command: "pwd" } step-2: write_file { path: "report.txt", content: "{{ last.output_text }}" } step-3: read_file { path: "report.txt" } step-4: finish ``` step-2 payload is resolved to `{ path: "report.txt", content: "/workspace/agent\n" }` before writing. ### Audit event ```json { "event_type": "resolution_performed", "step_id": "step-2", "status": "ok", "details": { "templates_found": true, "success": true } } ``` On failure: `"status": "failed"`, `"details": { "error": "..." }` ### Flow test goals ```bash python main.py --goal "test flow pwd to file" # {{ last.output_text }} python main.py --goal "test flow read to copy" # {{ last.output_text }} chained python main.py --goal "test flow ls to file" # {{ steps.step-1.output_text }} python main.py --goal "test flow bad template" # resolution fails → FAILED ``` ## Stage 2B — Failure-Aware Retry ### Failure classification | Class | Trigger | |---|---| | `security_denied` | "blocked by security policy" / "not in allowlist" | | `template_error` | Template resolution failure (`{{ }}`) | | `file_error` | File not found / path traversal / missing payload key | | `execution_error` | Shell non-zero exit / timeout / `execution_error:` prefix | | `unknown_error` | Everything else | ### Retry policy | Class | Max attempts | Retry? | |---|---|---| | `security_denied` | 1 | Never | | `template_error` | 1 | Never | | `file_error` | 2 | 1 retry | | `execution_error` | 2 | 1 retry | | `unknown_error` | 1 | Never | Hard cap: 2 attempts per step maximum, regardless of policy. ### Attempt tracking `PlanStep.attempts` — incremented before each execution attempt (0 = not yet started) `StepResult.attempt` — which attempt produced this result (1-based) `StepResult.failure_class` — set on FAILED results, null on DONE Both fields are persisted in `state.json`. ### History model Multiple StepResult records with the same `step_id` indicate retry: ```json {"step_id":"step-1","status":"failed","attempt":1,"failure_class":"execution_error"} {"step_id":"step-1","status":"done","attempt":2,"failure_class":null} ``` ### Audit events (retry path) ``` step_started → attempt 1 step_finished → attempt 1 (failed) step_failed → failure_class=execution_error, retry_scheduled=true retry_scheduled → next_attempt=2 retry_attempt_started → attempt 2 retry_attempt_finished → attempt 2 (done or failed) ``` For non-retriable failures: ``` step_started → step_finished(failed) → step_failed(retry_scheduled=false) → run terminates ``` ### Recovery test goals ```bash python main.py --goal "test retry execution" # flaky step: fails once → retries → DONE python main.py --goal "test deny bash" # security_denied → no retry → FAILED python main.py --goal "test flow bad template" # template_error → no retry → FAILED ``` ### New files | File | Purpose | |---|---| | `core/classifier.py` | Maps (kind, error) → FailureClass | | `core/retry_policy.py` | Max attempts per FailureClass, should_retry() | | `runtime/tools/flaky.py` | Deterministic fail-once tool for testing | ## Stage 1 Limitations - Planner is deterministic (keyword matching), not LLM-driven - Plans are always 3 steps (2 for file security tests) — no dynamic plan length - ~~No inter-step data passing~~ — resolved in Stage 2A via template system - Shell allowlist is hardcoded in `shell.py`, not loaded from config - No retry logic on step failure - No human-in-the-loop approval gate - Windows only: `ls`/`pwd` work via Git bash; fails on pure Windows without Git ## Stage 1.2 Changes (Consistency Fixes) - `current_step_id` → `null` after run terminates (done or failed) - Added security test keywords: `test deny rm`, `test deny curl`, `test traversal`, `test absolute path` - Fixed `WORKSPACE_ROOT` bug: was `parents[3]` (wrong), now `parents[2]` (correct) - audit events now include: `run_started`, `plan_created`, `state_saved`, `step_started`, `step_finished`, `run_finished`