Skip to content
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ wheels/
.installed.cfg
*.egg

# Dependency lockfiles
# CI installs from pyproject.toml with pip (see .github/workflows/ci.yml); a uv lockfile is a
# local convenience only and is not part of the reproducible build.
uv.lock

# Virtual environments
.venv/
venv/
Expand Down
3 changes: 2 additions & 1 deletion docs/AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Per the AgentKit specification and development rules:
## 4. Consolidated AUDIT-01…18 Remediation (2026-10-25)

A second whole-codebase audit filed 18 standalone findings as GitHub issues (`AUDIT-01` …
`AUDIT-18`). They were fixed as a linear stack of nine branches; this table is the index, and
`AUDIT-18`). They were fixed as a linear 12-layer stack, merged as 14 commits (12 layer
commits plus two progress-bookkeeping commits); this table is the index, and
`docs/PROGRESS.md` records the per-layer detail.

| Finding | Severity | Issue | Branch | Resolution |
Expand Down
9 changes: 6 additions & 3 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,12 @@ readable by anyone who can reach those ports.
`AGENTKIT_REAL_INFRA=1 pytest -m real_infra tests/real` works against this stack without a
`REDIS_TEST_URL` — pass `REDIS_PASSWORD` (or let it come from `.env`) with any override URL.
- **The guard fails closed.** The `migrations` service runs
`python scripts/check_deploy_credentials.py` before anything else. It rejects the image
defaults, the documented placeholders, short passwords, a missing read-only URL, a read-only
role equal to the application role, and a `REDIS_URL` without a password.
`python scripts/check_deploy_credentials.py` before anything else. It audits the credentials
actually used, whether they come from components or from an explicit URL: it rejects the
image defaults, the documented placeholders, short passwords, a URL with no password, a
malformed URL, a missing read-only URL, a read-only role equal to the application role, and
a `REDIS_URL` without a password. A URL-only deployment is not required to repeat the
`*_PASSWORD` components it does not use.
- **Read-only SQL role.** `python scripts/init_readonly_role.py` provisions
`POSTGRES_READONLY_USER` with `CONNECT`/`USAGE`/`SELECT` only and
`default_transaction_read_only = on`. The `sql_readonly` tool connects with it and refuses to
Expand Down
18 changes: 17 additions & 1 deletion docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This document tracks execution progress across all backlog tasks, including pull
| M4-T01 | M4 | Safe AST Calculator Tool | `feat/safe-calculator-tool` | #14 | 2 | Done | Strict AST arithmetic parser with zero eval/exec and DoS guards |
| M4-T02 | M4 | Sandboxed File Reader Tool | `feat/sandboxed-file-reader-tool` | #15 | 2 | Done | FILE_TOOL_BASE_DIR sandboxing, path traversal & symlink defense |
| M4-T03 | M4 | SSRF-Protected HTTP Fetch Tool | `feat/ssrf-http-fetch-tool` | #16 | 2 | Done | DNS & IP SSRF validation, private/metadata blocking, safe redirect |
| M4-T04 | M4 | Read-Only SQL Tool | `feat/readonly-sql-tool` | #17 | 2 | Done | Strict SELECT AST validation, DDL/DML rejection, Markdown formatting |
| M4-T04 | M4 | Read-Only SQL Tool | `feat/readonly-sql-tool` | #17 | 2 | Done | Single SELECT/WITH lexer validation (dedicated read-only role is the primary control), DDL/DML rejection, Markdown formatting |
| M4-T05 | M4 | Web Search Tool Adapter | `feat/web-search-tool` | #18 | 2 | Done | Tavily search integration, error handling for missing key, formatted snippets |
| M5-T01 | M5 | TraceSink Protocol & InMemoryTraceSink | `feat/trace-sink-base` | #19 | 2 | Done | StepTrace model, TraceSink ABC, InMemoryTraceSink, Agent.run() trace wiring |
| M5-T02 | M5 | Contextual Structured JSON Logger | `feat/structured-logging` | #20 | 2 | Done | JSONFormatter, contextvars run_id/step_no propagation, Agent logging |
Expand Down Expand Up @@ -71,6 +71,11 @@ This document tracks execution progress across all backlog tasks, including pull
| AUDIT-15/17 | AUDIT | Loop Detection and API Key Auth | `fix/agent-loop-and-auth` | #84 | 1 | Done | Sliding-window duplicate detector; hmac.compare_digest with normalised keys |
| AUDIT-16 | AUDIT | Documentation Claim Corrections | `docs/correct-audit-claims` | #84 | 1 | Done | README/ARCHITECTURE/AUDIT/ADR-006/PROGRESS/PLAN corrected and guarded by tests |
| AUDIT-18-FU | AUDIT | Real-Infra Redis Authentication | `fix/real-infra-redis-auth` | #84 | 1 | Done | `REDIS_PASSWORD` completes a credential-less `REDIS_URL` for the app and the live suite; 18 fixture errors fixed |
| FU-86 | AUDIT | Credential Guard URL Password Audit | `fix/deploy-credential-url-passwords` | #90 | 1 | Done | URL-embedded passwords audited with the component policy; malformed URLs reported, not raised |
| FU-87 | AUDIT | Credential Guard Logging | `refactor/deploy-credential-logging` | #90 | 1 | Done | Guard reports through module logging instead of `print()`; handlers only in `__main__` |
| FU-85 | AUDIT | Real-Infra Provisioning Failures | `test/real-infra-provisioning-failures` | #90 | 1 | Done | Provisioning errors fail the live SQL-isolation tier instead of skipping it |
| FU-88 | AUDIT | Audit History Consistency | `docs/audit-history-consistency` | #90 | 1 | Done | Canonical 12-layer/14-commit wording; stale "AST validation" row corrected and guarded |
| FU-89 | AUDIT | uv Lockfile Policy | `chore/repo-uv-lock` | #90 | 1 | Done | `uv.lock` ignored; pip-based CI does not consume a uv lock |

---

Expand Down Expand Up @@ -489,3 +494,14 @@ This document tracks execution progress across all backlog tasks, including pull
- **PR**: stacked on `fix/pg-url-credentials` — PR #84
- **Commits**: 1
- **Notes**: `scripts/init_readonly_role.py` changed only the password of a role that already existed, so a pre-existing `SUPERUSER`, `CREATEDB`, `CREATEROLE`, `REPLICATION` or `BYPASSRLS` role — or one that was a member of `pg_read_server_files` — kept everything the documented `SELECT`-only contract says it does not have. Provisioning now restricts first: `restriction_statements()` revokes those attributes and all privileges on the database, the `public` schema, tables, sequences and functions, `_revoke_memberships()` drops every direct membership, and only then are `CONNECT`/`USAGE`/`SELECT` granted back. It refuses a role that owns the target database (an owner can grant itself anything) and `_assert_restricted()` verifies the outcome, raising `ReadonlyRoleConfigError` rather than reporting a privileged role as provisioned. The tool checks the same properties per session in one round trip — `rolsuper`, `rolbypassrls` and membership of `pg_read_server_files`, `pg_write_server_files`, `pg_execute_server_program`, `pg_signal_backend`, `pg_monitor` or `pg_maintain`, resolved through `pg_has_role` so indirect membership counts — and refuses before the query runs. The statement lexer also re-exposes plain quoted identifiers to the function denylist, because PostgreSQL resolves `SELECT "pg_read_file"(…)` to the same built-in; keyword scans keep masking identifiers so `SELECT "delete" FROM t` stays valid. Covered by `tests/unit/test_readonly_role_provisioning.py`, new payloads in `tests/unit/test_sql_isolation.py`, and `tests/real/test_sql_isolation_real.py::test_reused_role_has_its_privileges_stripped`, which grants `CREATEDB` plus `pg_read_server_files` to the role and asserts provisioning strips both. Recorded in ADR-020.

### PR #84 Follow-ups (#85–#89)
- **Status**: Done
- **PR**: stacked on `main` — PR #90 (five layer commits plus one progress-bookkeeping commit)
- **Commits**: 5
- **Notes**:
- **#86 (guard correctness)** — `scripts/check_deploy_credentials.py` applied the weak-password policy only to the `POSTGRES_*` and `REDIS_PASSWORD` components. An explicit URL that carried its own userinfo bypassed it: `DATABASE_URL` was checked only against the exact known-password set, the read-only URL was never policy-checked, and `REDIS_URL` was checked for presence alone, so `redis://:short@host` passed the 12-character rule. `audit_credentials()` now resolves each URL exactly as `Settings` does and applies `_is_weak_password()` to URL-supplied secrets, requires a password in an explicit URL, validates a component whenever it is set but requires it only when it is the resolution source, and parses each URL once through a helper that reports "is not a valid connection URL" rather than leaking a SQLAlchemy traceback or the secret. `docs/DEPLOY.md` describes both credential forms.
- **#87 (logging)** — `main()` emits the header and one `ERROR` record per problem through `logging.getLogger(__name__)`; `basicConfig()` is confined to the `__main__` block so importing the module has no side effects. Exit code and message text are unchanged, and a test pins the no-`print()` rule from `AGENTS.md`.
- **#85 (test reliability)** — `tests/real/test_sql_isolation_real.py::readonly_engine` no longer converts `provision_readonly_role()` failures into `pytest.skip`. The upstream `real_database_url` fixture already applies migrations and fails loudly when PostgreSQL is unreachable, so a provisioning error is a defect and now turns CI red instead of leaving the isolation tier green-by-skip.
- **#88 (docs)** — `docs/AUDIT.md` records the canonical "linear 12-layer stack, merged as 14 commits (12 layer commits plus two progress-bookkeeping commits)" wording instead of "nine branches", and the historical M4-T04 row describes the lexer plus dedicated read-only role instead of "Strict SELECT AST validation". `tests/unit/test_docs_accuracy.py` guards both claims.
- **#89 (repo hygiene)** — `.gitignore` ignores `uv.lock` with a comment explaining that all three CI jobs install from `pyproject.toml` with pip, so a lockfile is not part of the reproducible build. Adopting uv properly (commit the lock, switch CI to `uv sync --locked`) remains a separate decision.
Loading
Loading