Files
Anton 052591281d Initial clean version - Agent Maxim
- Complete agent system with planning and execution
- Bridge API with FastAPI server
- WhatsApp-style mobile dashboard
- Enhanced web search with query improvement
- Security guards and policy checks
- Comprehensive documentation
2026-04-09 18:07:43 +03:00

253 lines
8.8 KiB
Markdown

# 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/<run_id>/state.json`)
- Append-only JSONL audit log (`workspace/runs/<run_id>/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/<run_id>/
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/<run_id>/
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`