Skip to content

Releases: peterb154/strands-pgsql-agent-framework

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 23:10

v0.9.0 (2026-07-26)

This release is published under the MIT License.

⚠️ Breaking change

PgMemoryStore.delete now requires a namespace keyword, and it must be a str — None is rejected.

-store.delete(memory_id)
+store.delete(memory_id, namespace=f"user:{email}")

To delete regardless of namespace, use the new delete_across_namespaces(memory_id) — a separate method rather than a namespace=None flag, because add/search/list all read None as "the default namespace", so overloading it here would turn a threaded str | None variable into a silent cross-tenant delete.

Only code calling delete directly is affected; memory_tools doesn't expose deletion, so most agents need no changes. Full migration notes: README — Migrating from v0.8.0 to v0.9.0. Background: #3.

This note was added by hand. The commit was marked feat(memory)!:, which drove the version bump, but python-semantic-release renders its Breaking Changes section from a BREAKING CHANGE: footer — absent here, so the change was filed under Features below.


Continuous Integration

  • Bump pinned actions to current majors (b27c83f)

Pinning by SHA in 787fc4b froze the versions I'd written from memory, and the run flagged setup-uv@v5 as targeting deprecated Node 20. It was four majors stale (latest v9.0.0); checkout was one behind (v7.0.1). Both now pinned to the current release, with the version in the trailing comment.

That's the cost of SHA pinning: it stops silent drift in both directions, so bumps have to be deliberate. Worth it for actions holding repo credentials, but it means the comment is the only thing telling a reader how stale a pin is — keep it accurate.

Refs #3

Features

  • memory: Add PgMemoryStore.update for edit-in-place (3613ab2)

The missing corner of the CRUD. Requested in mealie-agent#15 and landed here rather than in a later release because it touches the same class and the same migration note — splitting them means downstream eats the re-stamp churn twice.

Without update, "fix the wording of this note" degrades into delete-then-add, which mealie-agent#15 observed an agent actually doing. That substitute is wrong three ways: it isn't atomic (a failure between the two calls loses the note with no rollback, and the agent reports the delete as having succeeded), it restamps id and created_at so an April fact edited today claims it was written today — corrupting the exact signal an audit view exists to show — and it pays for an extra embedding round trip to express an UPDATE as an INSERT.

update(memory_id, text, *, namespace: str) preserves id, created_at and metadata, and replaces text and embedding together. They have to move together: rewriting text without re-embedding leaves search matching the old wording while returning the new text, which is the one genuinely easy thing to get wrong here.

Namespace scoping is identical to delete, and for a sharper reason — an unscoped update overwrites another tenant's memory instead of removing it, so the victim keeps a note that no longer says what they wrote. Mismatch changes nothing and returns False.

Embedding is computed before the connection is checked out so a slow embedder doesn't hold a pooled connection; the cost is one wasted embedding on a bad id, cheaper than a check-then-update that would race.

updated_at is deliberately not added — it needs a migration and mealie-agent#15 scopes it as a separate call.

Refs #3

  • memory: Require namespace on PgMemoryStore.delete (72dd65b)

delete() took an id alone while add/search/list are all namespace-partitioned, so any agent surface exposing deletion let one tenant remove another tenant's memories by guessing an integer. Ids are BIGSERIAL and the recall tool renders hits as "- [id] text", so callers routinely see live ids.

namespace is now a required keyword with no default. Cross-namespace deletion stays available as delete(id, namespace=None) for admin/prune scripts, but has to be stated rather than being what you get by forgetting an argument. A mismatch removes nothing and returns False.

Two related gaps from the same report, both additive: - list() takes offset, so a namespace larger than one call can be paged. Ordering gains an id tiebreaker so pages don't overlap when timestamps tie. - MemoryHit carries created_at, populated by both list() and search().

Tests run against real Postgres following the test_session_lock pattern: wrong-namespace delete leaves the row intact, correct-namespace removes it, unscoped delete() raises TypeError, offset pages cover rows exactly once.

uv.lock picks up an unrelated one-line version correction (0.6.0 -> 0.8.0) that any uv run regenerates.

Refs #3

  • memory: Split unscoped delete into its own method; add CI (359c308)

Review feedback on #4, from @peterb154 and a self-review that converged on the same two blockers.

delete(namespace=None) meant "every namespace" while add/search/list all read None as "the default namespace" — same class, same parameter name, same sentinel, inverted blast radius, invisible to a type checker. A str | None variable threaded into what looked like the same argument turned a scoped delete into a cross-tenant one: the bug #3 is about, wearing the fix's clothes. The README made it worse by saying "pass the same namespace you pass to search", which is a cross-tenant delete when that namespace is legitimately None.

delete(memory_id, *, namespace: str) now rejects None; the unscoped path moved to delete_across_namespaces(memory_id). Chose a second method over an ALL_NAMESPACES sentinel because the sentinel forces the annotation to str | object, giving up the static check that makes the required-kwarg fix bite. Runtime fails closed too: None yields WHERE namespace = NULL, which matches nothing, so an untyped caller deletes zero rows rather than someone else's. Both properties are now tested.

Nothing ran these tests. .github/workflows/ held only release.yml, so pytest and ruff never ran in CI, and test_memory_scoping self-skipped when Postgres was unreachable — a skipped security test and a passing one look identical in a CI summary. Added test.yml (builds images/db/Dockerfile for pgvector+postgis, migrates, lints, tests) and made the skip raise under CI=true. The signature assertions moved to test_import.py so they survive without a database — that's what stops someone restoring a default later.

Marked breaking, with major_on_zero=false so it still cuts 0.9.0 rather than claiming 1.0.0 stability. Without the marker the changelog would file a required-argument change under Features and anyone pinned >=0.8,<1.0 would upgrade into a TypeError.

Also: test_delete_explicit_none_is_cross_namespace asserted a postcondition a correctly-scoped delete satisfies identically, so it would have stayed green through the regression it was named for — rewritten to assert the contrast. Fixed the list() docstring's claim that paging stability comes from stable ids rather than from the total ordering.

Refs #3

Testing

  • memory: Close gaps found reviewing the review fixes (787fc4b)

Six findings from a delta review of 359c308 and 3613ab2. Three of them weakened tests that exist specifically to be trustworthy.

update() documented "metadata is preserved" in its docstring and the README, and nothing tested it. The SQL was already right, but the existing test couldn't have caught a regression anyway — it added a row with no metadata, so losing it was invisible. The fixture now carries metadata and asserts it survives; verified the assertion bites by clobbering the column out of band.

The fake embedder used sum(ord(c)), which is permutation-invariant: 'note 12' and 'note 21' embedded identically. Nothing collides today, but test_list_offset_pages writes note 0..4 and is one loop-count from it, and the update re-embed test asserts "the old wording no longer matches" — pick two anagrams and it passes while asserting nothing. Now crc32.

The signature tests compared Parameter.annotation to the literal "str", which only holds while memory.py carries from __future__ import annotations. Deleting that line — an unremarkable refactor — failed a test whose message claims the namespace contract broke. Now uses inspect.get_annotations(eval_str=True), which gives the same answer either way. Both tests assert the identical contract, so it's one shared helper.

Docstrings had grown to 24 lines against 14 of code on update, and 21 against 8 on delete, in a file where add and search get one line each — with the same prose duplicated in the README and the PR body. Trimmed to the contract plus the load-bearing why; the README keeps the long form.

Workflow: added permissions: contents: read (the job only reads code and runs pytest), pinned both actions by commit SHA with the version in a trailing comment (a tag is mutable, so @v6 runs whatever that owner points at today with repo credentials), and moved the env block above steps where readers expect it. release.yml stays tag-pinned — separate change.

Refs #3


Detailed Changes: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 27 Apr 22:04

v0.8.0 (2026-04-27)

This release is published under the MIT License.

Bug Fixes

  • Address PR review feedback (55e9dcf)

  • agentmail_operator_notify: read AGENTMAIL_API_KEY fresh on each call when api_key is None, so rotated secrets pick up without a restart. Docstring clarified. - agentmail_operator_notify: percent-encode the from_inbox path segment in the REST URL so a misconfigured env var can't path-traverse. AgentMail addresses contain @ legitimately, so that character stays unescaped. - agentmail.py: lift httpx and contextlib.nullcontext to module-level imports — both are hard deps and the deferred-import pattern was unwarranted overhead per call. - _process: document the build_agent-returns-fresh-Agent assumption near the MessageAddedEvent registration. Strands' HookRegistry has no remove_callback, so cache_agents=True would accumulate _capture closures across turns and produce a leak + incorrect trace data. Email-webhook consumers must use cache_agents=False. - test_email_webhook._wait_for_thread: drop the misleading deadline = threading.current_thread() dead-code line. - Add two
    operator_notify tests: env key read freshly per call, and path-traversal characters in from_inbox get percent-encoded.

Refs #1 #2

Documentation

  • Readme email-webhook section + v0.7.0 → v0.8.0 migration note (85f47ab)

Updates the Email agents quickstart to use the new build_agent contract (user_email kwarg) and the new optional kwargs (session_id_for, lock_session, on_failure). Adds:

  • A "build_agent signature" subsection explaining session_id vs
    user_email decoupling.
  • A "Failure callback contract" subsection with a Slack example
    alongside the agentmail_operator_notify convenience factory.
  • A scale note on session_lock's connection-hold behavior.
  • A "Migrating from v0.7.0 to v0.8.0" subsection with concrete
    diffs for the two breaking changes consumers will hit:
    build_agent signature and notify_email -> on_failure.

Refs #1

Features

  • Add agentmail_operator_notify convenience factory (f8de1af)

Builds a Callable[[FailureEvent], None] suitable for the new attach_email_webhook(..., on_failure=...) callback. Sends a failure-notification email via AgentMail's REST API (POST /v0/inboxes/{from_inbox}/messages/send).

Bypasses the MCP send tool intentionally — if the agent failure was in the MCP path, retrying through it would just fail again.

Sets Reply-To: noreply@<from-domain> by default to break the operator-replies-to-failure-email feedback loop. Custom reply_to accepted as a kwarg.

This is opt-in: the factory lives in strands_pg.agentmail and is exported from the package, but attach_email_webhook does not default to it. Chat-fronted agents that surface failures in their UI typically pass nothing for on_failure; agentmail-only / no-UI consumers wire this factory.

Refs #1

  • Add session_lock advisory-lock helper (ac23ed1)

Same-session concurrent agent runs race on Strands' message_id arithmetic and crash with a unique-constraint violation, silently dropping whatever tool call was in progress (incl. reply MCPs). session_lock(session_id) wraps an agent run in a Postgres advisory lock keyed on hashtext(session_id), serializing same-session writes across processes and replicas.

Best-effort unlock on exit: Postgres releases session-level advisory locks automatically when the connection closes, so a failed unlock is logged and swallowed rather than masking the user's real exception.

Tests hit a real Postgres and skip when unreachable.

Refs #1

  • Add walk_tool_trace + FailureEvent observability helpers (2fd7848)

Strands' default per-cycle logger emits Tool #N: name markers but drops args/results, so a tool that returns an error result (rather than raising) leaves no diagnostic trail. walk_tool_trace walks a list of Strands messages and rebuilds (toolUse, toolResult) pairs into a human-readable trace, including orphan toolUses surfaced as status=(no result) for cycles that exited mid-tool.

Pure: returns data, emits no log records — caller decides what to do with the lines. Replaces the camping-db _log_tool_trace which mixed return-value + logger.info side effects.

FailureEvent is a frozen dataclass carrying the inbound message, sender, failure reason, and trace lines. Will be passed to the new on_failure callback in the email-webhook refactor (next commit).

Refs #1

  • Pluggable on_failure + session/lock kwargs for email webhook (1b488a1)

Replaces the camping-db-shaped agent observability path with a clean upstream API. Three behavioral changes, one breaking signature change.

Behavioral changes:

  • Per-turn message capture uses a MessageAddedEvent hook callback instead of slicing agent.messages after the run. Slicing was unreliable: Strands' default conversation manager and the _fix_broken_tool_use path can replace agent.messages wholesale during a cycle, invalidating the length snapshot. Hook capture is robust against in-place pruning, list replacement, and partial-failure unwinds.

  • On failure, dump the full tool trace in one logger.warning record. Python's lastResort handler emits WARNING+ even with no handler configured, so the diagnostic trace shows up in docker logs regardless of whether the consumer set up logging.basicConfig. Per-line INFO would be silently filtered by default.

  • Three new optional kwargs on attach_email_webhook:

  • session_id_for: Callable[[AgentMailMessage], str] — consumer picks the keying. Defaults to lowercased sender (historic). Recommended: lambda m: m.thread_id or m.message_id to eliminate cross-thread races at the data layer. - lock_session: Callable[[str], ContextManager] — wraps the agent run in a session-scoped lock. Use strands_pg.session_lock. - on_failure: Callable[[FailureEvent], None] — fired when the agent raises or fails to call reply_to_message successfully. Framework provides only the mechanism; consumers wire their own delivery channel (email, Slack, PagerDuty). Replaces the empty-string-as-sentinel notify_email kwarg.

Migration note: build_agent factory must now accept the kwarg-only contract (session_id, *, user_email, extra_prompt=""). The inspect.signature introspection that supported the older (session_id, extra_prompt="") shape is gone. Migration is mechanical: add user_email to the factory signature and optionally use it for identity / memory namespacing. Old factories fail loudly via on_failure with a TypeError that names user_email.

Refs #1


Detailed Changes: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 21 Apr 16:36

v0.7.0 (2026-04-21)

This release is published under the MIT License.

Features

  • Safer re-stamp, mtime-aware prompt seeding, deployment gotchas (d03f2c6)

Three improvements that came out of actually stamping strands-pg into two agents (camping-db, mealie-agent) and hitting the rough edges:

  1. install.sh --refresh A framework-only re-stamp that touches ONLY strands_pg/ and the framework-numbered migrations (0*.sql). app.py, tools/, prompts/, Dockerfile, docker-compose.yml, bootstrap-lxc.sh, systemd/, etc. are left alone. Use this when bumping the framework on an existing agent. --force still exists for "I want a full re-stamp and I know what I'm overwriting"; --refresh is now the recommended upgrade path and won't make you restore from git if you forget the flag.

  2. PgPromptStore.seed_from_dir respects file mtime Old behavior: only wrote a prompt if the row didn't exist. That meant editing prompts/rules.md on disk + redeploying had NO effect unless you also PUT the new content to /prompts/ manually.

New default: for each .md file, insert if missing, OR update if the file's mtime is newer than the row's updated_at. Live tweaks made via the /prompts API still survive restarts (their updated_at leapfrogs the baked-in file mtime). Pass overwrite=True to force a full reseed regardless.

  1. README: Deployment gotchas section Writes down the nginx sub_filter + location-scope-replacement trap that ate an hour of mealie-agent's first shim deploy, plus notes on docker-build-vs-restart for GIT_SHA and where SSE streaming tends to stall in proxy chains.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.6.0...v0.7.0

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Apr 04:23

v0.6.0 (2026-04-21)

This release is published under the MIT License.

Features

  • Multi-scope memory_tools, per-request auth_verifier, commit_sha helper (3ca218c)

Three additive capabilities for agents that serve real users over HTTP:

  1. memory_tools(namespaces={suffix: ns, ...}) — builds parallel remember_ / recall_ tool pairs, each closed over its own namespace. Enables per-user + per-household (or per-org) memory scopes from a single agent build. Single-namespace form (namespace=...) is unchanged and still returns [remember, recall].

  2. make_app(auth_verifier=...) — opt-in per-request auth for /chat and /chat/stream. Callable (token) -> {session_id, ...} | None; a 401 is returned on missing/invalid tokens. When configured, session_id is derived from the verifier dict (not the request body) so clients can't spoof another user's session. The full context dict is passed to agent_factory(session_id, context=ctx) IF the factory accepts a context kwarg (detected via inspect) — keeps existing camping-db factories working unchanged.

  3. commit_sha() + make_app(health_info=...) — /health can advertise the deployed revision without shelling out. Reads .git/HEAD (supports worktrees + packed-refs). Pair with health_info=lambda: {"commit": commit_sha()} so deploy pipelines (n8n, GHA) can verify a push actually landed.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Apr 02:21

v0.5.0 (2026-04-21)

This release is published under the MIT License.

Features

  • Host-side deploy + optional AgentMail webhook helper (435c932)

Two optional framework additions earned from the camping-db migration. Both off by default — agents opt in.

  1. Host-side deploy orchestration (make_app(deploy=True))

POST /api/deploy now ships as an opt-in feature of make_app. When enabled, the endpoint: - Auths a bearer token from DEPLOY_TOKEN - Writes a timestamp to $DEPLOY_TRIGGER - Returns {"status": "ok", "action": "triggered", ...} — shape that matches typical IF-node success checks in workflow tools like n8n.

The orchestration itself runs on the LXC HOST via systemd, not inside the agent container. templates/agent/systemd/.in holds two unit templates (.path + *.service) with @agent@ and @dir@ placeholders; bootstrap-lxc.sh substitutes at install time so each agent gets distinct unit names (camping-db-deploy.path vs mealie-deploy.path can coexist). deploy.sh on the host does git pull + docker compose up --build and survives the rebuild (in-container orchestrators don't).

This graduates the pattern from camping-db, replacing the earlier in-container deploy that fought SIGTERM races, docker.sock security concerns, and docker-compose-plugin packaging issues.

  1. AgentMail webhook helper (strands_pg.agentmail)

New module with attach_email_webhook() + make_agentmail_mcp() for agents that want to serve email.

attach_email_webhook(app, build_agent, known_emails, ...) registers POST /api/webhook/email with all the gates that would otherwise be copy-pasted into every email agent: - Accepts message.received, .spam, .blocked variants (startswith) - Sender allowlist via dynamic known_emails() callable - Echo-loop prevention (skip messages from our own address) - message_id dedup - Background-thread processing so the webhook returns fast - System-prompt injection telling the model to call reply_to_message (without this, agents generate replies that never get sent)

make_agentmail_mcp() is a tiny factory that opens the MCPClient with the x-api-key auth header AgentMail actually wants (not Bearer — empirically verified, 401 without).

Intentionally not exported from init.py — keeps the opt-in discipline, so agents that don't email don't pay the MCP import cost.

README: - New "Deploy architecture" section with the host-side diagram. - New "Email agents" section with the wiring recipe + gotchas (x-api-key not Bearer, SPF/DKIM/DMARC prerequisite, must call reply_to_message). - Anti-pattern noted: don't mount /var/run/docker.sock into the agent container.

Next: update camping-db to use the new helpers (removes ~70 lines of email-webhook boilerplate + ~40 lines of deploy wiring from its app.py).

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 20 Apr 21:13

v0.4.0 (2026-04-20)

This release is published under the MIT License.

Features

  • install: --force flag to stamp into existing directories (7bf0a91)

install.sh now accepts --force (or -f) to overlay the framework stamp into an existing directory. Without the flag, the existing safety check still refuses (so nobody nukes an unrelated dir by accident).

Use cases: - Re-stamp an existing agent to pick up framework updates, then diff against working tree to pick which changes to keep (the "shadcn update" flow the README already describes). - Migrate an existing repo onto strands-pg — stamp files overlay the repo's working tree without touching .git, existing docs, or unrelated files. How camping-db's rewrite lands on its existing git history.

Overlay semantics: cp -R with target already existing. Same-name files get overwritten (soul.md, app.py, Dockerfile, etc.); files that aren't part of the stamp (README.md, CHANGELOG.md, .git, existing data/ dirs) are left alone. No rm -rf anywhere.

Verified: bash install.sh /tmp/force-test --force --ref main with a pre-existing keep-me.txt file left keep-me.txt intact and stamped all expected files.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 20 Apr 20:51

v0.3.0 (2026-04-20)

This release is published under the MIT License.

Features

  • Stamped bootstrap-lxc.sh + chromium sidecar stanza (6c4a7bd)

Two additions to the stamped agent template:

templates/agent/bootstrap-lxc.sh: Idempotent host prep for a fresh Debian/Ubuntu LXC. Installs Docker engine + compose plugin via get.docker.com, writes daemon.json with log rotation, installs baseline tools (git, curl, jq), and adds a systemd unit that auto-starts any /opt/*/docker-compose.yml stack on reboot. Includes a preflight check that warns if nesting=1 / keyctl=1 features are missing on a Proxmox LXC (Docker won't work without them).

The script is stamped INTO the agent repo (not hosted at strands-pg's main) so each agent owns it. When a specific agent needs unusual host setup (kernel module, sysctl, extra packages), edit this file in place and commit — it's a Dockerfile-equivalent for the LXC.

templates/agent/docker-compose.yml: Adds a commented-out chromium sidecar stanza (browserless/chrome) with usage notes. Agents that need a browser tool for scraping/automation (camping-db's GIS workflow, for example) uncomment and go. Comes with shm_size 2gb — non-negotiable for headless Chrome.

templates/agent/README.md: New "First time on a fresh host?" section pointing at bootstrap-lxc.sh.

templates/agent/requirements.txt: (already updated in the SSE commit) — sse-starlette pulled in.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 20 Apr 20:48

v0.2.0 (2026-04-20)

This release is published under the MIT License.

Features

  • Sse streaming /chat/stream endpoint in make_app (40dc9f4)

Adds a second chat endpoint alongside /chat that streams Strands agent events as Server-Sent Events. Wraps agent.stream_async into normalized event shapes so SSE consumers stay stable across SDK upgrades:

event: text text delta chunks

event: thinking reasoningText deltas (when model emits them)

event: tool_use tool name, emitted once per toolUseId

event: done terminal (empty data)

event: error any exception, data = message

Dependencies: - sse-starlette>=2.1 added to framework pyproject + template requirements.txt

Also restores pyproject.toml authors to "Brian Peterson" — was incorrectly scrubbed to "Alice Rider" in the earlier PII cleanup pass. The alice/bob personas were intended only for the camping-db/ walkthrough identities, not the package metadata (git commit authorship is elsewhere and was deliberately left alone per user call).

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.1.3...v0.2.0

v0.1.3

Choose a tag to compare

@github-actions github-actions released this 20 Apr 14:47

v0.1.3 (2026-04-20)

This release is published under the MIT License.

Bug Fixes

  • Restore over-scrubbed search example + correct User-Agent URL (29adbca)

Two small cleanups after the PII scrub rewrite:

  • camping-db/tools/geo.py: the geocode docstring example read
    "Mountain West KS" after the scrub (string replacement accidentally
    rewrote a city name inside a search example). Fixed to "Topeka KS".
  • camping-db/tools/{geo,parcels}.py: User-Agent string pointed at
    github.com/brianpeterson/strands-pgsql-agent-framework (a placeholder
    that was wrong from day one — the real repo is peterb154/...). Fixed
    to the actual URL.

Neither of these is PII. Just cleanup from the rewrite pass.


Detailed Changes: v0.1.2...v0.1.3

v0.1.2

Choose a tag to compare

@github-actions github-actions released this 19 Apr 15:47

v0.1.2 (2026-04-19)

This release is published under the MIT License.

Bug Fixes

  • templates: Deploy-to-production guide with IAM least-privilege policy (992b49d)

The stamped agent's README had no guidance on what real deployment looks like. This adds a "Deploying to production" section covering:

  1. IAM user + least-privilege policy JSON — scoped to bedrock:InvokeModel on just the Claude inference profile, the underlying foundation model, and the Titan embed model. Calls out the inference-profile-vs-FM-ARN gotcha explicitly (you need both, not just one). 2. The .env entries for static IAM user keys. 3. The ~/.aws mount that needs to be deleted from docker-compose.yml (the bind fails on hosts without ~/.aws, which is every fresh LXC). 4. A note on rotating the access key.

Complements the existing .env.example Option A (laptop dev with SSO-backed profile) vs Option B (production with static keys) distinction.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Detailed Changes: v0.1.1...v0.1.2