diff --git a/authors/thefinalmilkman.md b/authors/thefinalmilkman.md new file mode 100644 index 00000000..a4d9981f --- /dev/null +++ b/authors/thefinalmilkman.md @@ -0,0 +1,13 @@ +Author: The Final Milkman +Title: Software Developer +Description: The Final Milkman is a software developer focused on practical + automation, developer tooling, and verifiable AI-assisted workflows. He + enjoys turning ambiguous engineering tasks into small systems that can be + tested, reviewed, and improved. +Author Image: [https://avatars.githubusercontent.com/thefinalmilkman?size=512] +Author LinkedIn: +Author Twitter: +Company Name: +Company Description: +Company Logo Dark: +Company Logo White: diff --git a/definitions/20260603_definition_api_contract_migration.md b/definitions/20260603_definition_api_contract_migration.md new file mode 100644 index 00000000..627b0d9c --- /dev/null +++ b/definitions/20260603_definition_api_contract_migration.md @@ -0,0 +1,28 @@ +--- +title: 'API Contract Migration' +description: + 'An API contract migration changes a request or response shape while managing + compatibility for existing consumers.' +date: 2026-06-03 +author: 'The Final Milkman' +--- + +# API Contract Migration + +## Definition + +An API contract migration is a planned change to the structure or behavior of +an API request, response, error, or event. The migration defines the new +contract and the steps required to keep existing consumers working while they +adopt it. + +## Context and Usage + +API contract migrations are common when a service renames fields, nests data, +adds version metadata, changes validation rules, or retires legacy behavior. A +safe migration usually includes a compatibility window, automated contract +tests, consumer updates, and a separate removal step for deprecated fields. + +For example, an API can introduce `profile.display_name` while temporarily +preserving a legacy top-level `name` field. New consumers can adopt the nested +field without immediately breaking older clients. diff --git a/guides/20260603_guide_rehearse_api_contract_migrations_in_daytona.md b/guides/20260603_guide_rehearse_api_contract_migrations_in_daytona.md new file mode 100644 index 00000000..1f018c69 --- /dev/null +++ b/guides/20260603_guide_rehearse_api_contract_migrations_in_daytona.md @@ -0,0 +1,375 @@ +--- +title: 'Rehearse API Contract Migrations in Daytona' +description: + 'Use Omni Engineer and Claude Engineer in a Daytona sandbox to migrate an API + response, preserve compatibility, and verify tests.' +date: 2026-06-03 +author: 'The Final Milkman' +tags: ['AI', 'API', 'daytona', 'devcontainer'] +--- + +# Rehearse API Contract Migrations in Daytona + +# Introduction + +An API response can look small in a pull request and still break every consumer +that depends on it. Renaming `name` to `profile.display_name`, for example, may +be a sensible model improvement. It is also a contract change that needs a +migration plan, compatibility tests, and a careful review of downstream code. + +AI engineering tools can help with that work, but they are most useful when +their roles are explicit. In this guide, [Omni +Engineer](https://github.com/Doriandarko/omni-engineer) acts as the +implementation agent, [Claude +Engineer](https://github.com/Doriandarko/claude-engineer) acts as the +compatibility reviewer, and a disposable [Daytona +sandbox](https://www.daytona.io/docs/en/) keeps the exercise isolated from your +local machine. + +The result is a repeatable [API contract +migration](../definitions/20260603_definition_api_contract_migration.md) +rehearsal. You will add portable Dev Container definitions to both agent +repositories, run the agents inside Daytona, migrate a small Flask API, and +confirm that the new response shape does not break the legacy consumer. + +## TL;DR + +- Add secret-free `devcontainer.json` files to Omni Engineer and Claude + Engineer so their repository setup is portable. +- Create one Daytona sandbox with the required API keys supplied as + [environment + variables](../definitions/20241126_definition_environment_variables.md). +- Ask Omni Engineer to implement a version-two user response while preserving + the legacy `name` field. +- Ask Claude Engineer to review the patch for backward-compatibility risks. +- Run contract tests and inspect the final response before treating the + migration as ready. + +## Step 1: Prepare Portable Agent Repositories + +The companion Dev Container contributions for this guide are: + +| Repository | Purpose | Pull Request | +| --- | --- | --- | +| Omni Engineer | Python 3.11 environment and dependency installation | [Doriandarko/omni-engineer#44](https://github.com/Doriandarko/omni-engineer/pull/44) | +| Claude Engineer | Python 3.11 environment, dependency installation, and port `5000` forwarding | [Doriandarko/claude-engineer#269](https://github.com/Doriandarko/claude-engineer/pull/269) | + +Each `.devcontainer/devcontainer.json` uses the repository's existing +`requirements.txt` file. Neither file copies an `.env` file or embeds an API +key. That matters because a [development +container](../definitions/20240819_definition_development%20container.md) +should describe the runtime, not carry credentials. + +The Omni Engineer configuration is intentionally small: + +```json +{ + "name": "Omni Engineer", + "image": "mcr.microsoft.com/devcontainers/python:3.11", + "postCreateCommand": "python -m pip install --upgrade pip && python -m pip install -r requirements.txt", + "remoteUser": "vscode" +} +``` + +Claude Engineer uses the same base pattern and forwards its Flask web UI port: + +```json +{ + "name": "Claude Engineer", + "image": "mcr.microsoft.com/devcontainers/python:3.11", + "postCreateCommand": "python -m pip install --upgrade pip && python -m pip install -r requirements.txt", + "forwardPorts": [5000], + "remoteUser": "vscode" +} +``` + +Daytona's current CLI is sandbox-based. The walkthrough below uses the current +`daytona create`, `daytona exec`, and `daytona ssh` commands instead of +assuming that Daytona automatically interprets `devcontainer.json`. The Dev +Container files remain useful as portable setup definitions for compatible +editors and future repository users. + +You will also use a small starter project: + +- [Daytona API Contract Migration + Demo](https://github.com/thefinalmilkman/daytona-api-contract-migration-demo) + +Its version-one endpoint returns this payload: + +```json +{ + "id": 7, + "name": "Ada Lovelace" +} +``` + +The migration will add a version-two nested profile while keeping `name` +available for one compatibility window. + +## Step 2: Create the Shared Daytona Sandbox + +Install the Daytona CLI and authenticate with `daytona login` before starting. +The [Daytona CLI +reference](https://www.daytona.io/docs/tools/cli/) documents sandbox creation, +command execution, and SSH access. + +Keep your keys in shell variables so they do not appear directly in the command +you save to notes or documentation: + +```bash +export OPENROUTER_API_KEY="your-openrouter-key" +export ANTHROPIC_API_KEY="your-anthropic-key" +``` + +Create a sandbox and pass the keys as environment variables: + +```bash +daytona create \ + --name contract-migration-lab \ + --env OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \ + --env ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" +``` + +> **Note:** Do not commit API keys to either agent repository or to the demo +> project. Daytona supports environment variables at sandbox creation time, so +> the credentials can stay outside the codebase. + +Clone the two agents and the demo project: + +```bash +daytona exec contract-migration-lab \ + "git clone https://github.com/thefinalmilkman/omni-engineer.git /home/daytona/omni-engineer" + +daytona exec contract-migration-lab \ + "git clone https://github.com/thefinalmilkman/claude-engineer.git /home/daytona/claude-engineer" + +daytona exec contract-migration-lab \ + "git clone https://github.com/thefinalmilkman/daytona-api-contract-migration-demo.git /home/daytona/daytona-api-contract-migration-demo" +``` + +Create separate virtual environments. This keeps the two agents' dependency +sets from interfering with each other: + +```bash +daytona exec contract-migration-lab \ + --cwd /home/daytona/omni-engineer "python -m venv .venv" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/omni-engineer \ + ".venv/bin/python -m pip install -r requirements.txt" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/claude-engineer "python -m venv .venv" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/claude-engineer \ + ".venv/bin/python -m pip install -r requirements.txt" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + "python -m venv .venv" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + ".venv/bin/python -m pip install -r requirements.txt" +``` + +![API contract migration rehearsal flow](assets/20260603_guide_rehearse_api_contract_migrations_in_daytona_img1.svg) + +The single sandbox gives both agents access to the same demo repository. Omni +Engineer can change the files, Claude Engineer can inspect the exact diff, and +the test runner can verify the result without moving code between machines. + +## Step 3: Ask Omni Engineer to Make the Migration + +Open an interactive shell in the sandbox: + +```bash +daytona ssh contract-migration-lab +``` + +Start Omni Engineer: + +```bash +cd /home/daytona/omni-engineer +.venv/bin/python main.py +``` + +Add the demo's contract-sensitive files to Omni Engineer's context: + +```text +/add ../daytona-api-contract-migration-demo/app.py +/add ../daytona-api-contract-migration-demo/consumer.py +/add ../daytona-api-contract-migration-demo/test_contract.py +``` + +Then give it a bounded migration request: + +```text +Migrate the demo user response to contract version 2. + +Acceptance criteria: +1. Successful GET /users/ responses include contract_version: 2. +2. Add profile.display_name using the existing name value. +3. Keep the legacy top-level name field for one compatibility window. +4. Update consumer.display_name so it works with both version-one and + version-two payloads. +5. Add tests for the new shape, legacy compatibility, and missing users. +6. Do not change the 404 error contract. +7. Show the diff before treating the task as complete. +``` + +This prompt gives the agent a clear boundary. It does not merely ask for a +"better" API. It defines the migration target, the compatibility promise, and +the behavior that must remain stable. + +After reviewing the proposed edits, run the tests from another terminal: + +```bash +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + ".venv/bin/python -m pytest -q" +``` + +The important result is not the number of changed lines. It is that the tests +express both sides of the contract: + +| Contract Concern | Expected Test | +| --- | --- | +| New clients can read the version-two shape | Assert `contract_version` and `profile.display_name` | +| Existing clients keep working | Assert the legacy `name` field remains present | +| Consumer code handles both versions | Call `display_name` with version-one and version-two payloads | +| Error behavior stays stable | Assert missing users still return the original `404` payload | + +## Step 4: Use Claude Engineer as the Compatibility Reviewer + +Once the implementation tests pass, start Claude Engineer in the same sandbox: + +```bash +cd /home/daytona/claude-engineer +.venv/bin/python ce3.py +``` + +Ask it to review the actual demo repository rather than a pasted summary: + +```text +Review the API contract migration in +/home/daytona/daytona-api-contract-migration-demo. + +Focus on backward compatibility, response consistency, consumer fallback +behavior, and missing regression tests. Inspect app.py, consumer.py, +test_contract.py, and the git diff. Do not remove the legacy name field. +If you find a concrete gap, explain it first and then propose the smallest +patch that resolves it. +``` + +Separating implementation from review is useful even when both agents are +capable coders. The first agent is biased toward satisfying the requested +change. The second agent is explicitly biased toward finding what the first +agent missed. + +Use this review checklist: + +| Review Question | Why It Matters | +| --- | --- | +| Is the old field still present? | Existing consumers may deserialize it directly | +| Is the new field derived from the same source value? | Duplicate fields can drift if they are built independently | +| Can the consumer read both shapes? | A rolling migration often includes mixed payload versions | +| Did the `404` payload change accidentally? | Error contracts are API contracts too | +| Do tests describe the compatibility window? | Future cleanup should be intentional, not accidental | + +If Claude Engineer suggests a patch, rerun the same test command after applying +it. A reviewer recommendation is only useful when it is converted into a +verifiable change. + +## Step 5: Confirm the Result + +Run a final whitespace check, test suite, and response inspection: + +```bash +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + "git diff --check" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + ".venv/bin/python -m pytest -q" + +daytona exec contract-migration-lab \ + --cwd /home/daytona/daytona-api-contract-migration-demo \ + ".venv/bin/python -c \"from app import app; print(app.test_client().get('/users/7').get_json())\"" +``` + +A successful migration should produce a payload shaped like this: + +```json +{ + "contract_version": 2, + "id": 7, + "name": "Ada Lovelace", + "profile": { + "display_name": "Ada Lovelace" + } +} +``` + +The top-level `name` field is deliberately redundant. It is the bridge that +lets existing consumers continue working while new consumers adopt +`profile.display_name`. When the compatibility window ends, removing `name` +should be a separate, announced change with its own tests and release notes. + +## Common Issues and Troubleshooting + +**Problem: An agent reports that its API key is missing.** + +**Solution:** Confirm that the shell variables were populated before +`daytona create`. If the sandbox was created without the variables, delete it +and create a new one with the required `--env` flags. Do not solve the problem +by committing a real `.env` file. + +**Problem: Omni Engineer edits the wrong repository.** + +**Solution:** Use absolute paths in the prompt and repeat the allowed file +list. Keep the task bounded to `app.py`, `consumer.py`, and +`test_contract.py` until the first migration passes. + +**Problem: The new response passes tests, but the consumer still fails.** + +**Solution:** Add a test that calls `display_name` with both payload versions. +The consumer should prefer `profile.display_name` when present and fall back to +the legacy `name` field. + +**Problem: Dependency installation fails or packages conflict.** + +**Solution:** Keep one virtual environment per repository. Recreate only the +failing environment and install from that repository's own +`requirements.txt`. + +**Problem: The review produces broad refactoring suggestions.** + +**Solution:** Ask for the smallest patch that resolves a concrete +compatibility gap. Contract migrations are easier to verify when unrelated +refactors are left for another change. + +## Conclusion + +This workflow turns an API response change into a small engineering rehearsal +instead of a hopeful edit. Omni Engineer implements a bounded migration, Claude +Engineer reviews the exact patch, and Daytona provides a disposable place to +run both tools and the contract tests. + +The larger lesson is simple: AI agents become more dependable when their work +is shaped by explicit acceptance criteria, separate review roles, and +executable tests. A sandbox makes that process repeatable, while the companion +Dev Container definitions keep the agent repositories easier to open and use +across compatible development environments. + +## References + +- [Daytona Documentation](https://www.daytona.io/docs/en/) +- [Daytona CLI Reference](https://www.daytona.io/docs/tools/cli/) +- [Daytona Environment Variables](https://www.daytona.io/docs/en/sandboxes/environment-variables/) +- [Dev Container Specification](https://containers.dev/) +- [Omni Engineer Repository](https://github.com/Doriandarko/omni-engineer) +- [Claude Engineer Repository](https://github.com/Doriandarko/claude-engineer) +- [API Contract Migration Demo](https://github.com/thefinalmilkman/daytona-api-contract-migration-demo) diff --git a/guides/assets/20260603_guide_rehearse_api_contract_migrations_in_daytona_img1.svg b/guides/assets/20260603_guide_rehearse_api_contract_migrations_in_daytona_img1.svg new file mode 100644 index 00000000..53a35cee --- /dev/null +++ b/guides/assets/20260603_guide_rehearse_api_contract_migrations_in_daytona_img1.svg @@ -0,0 +1,35 @@ + + API contract migration rehearsal in a Daytona sandbox + Omni Engineer changes a demo API, Claude Engineer reviews the change, and contract tests verify the result inside one Daytona sandbox. + + + Daytona sandbox: contract-migration-lab + One isolated environment, two agent roles, one verifiable API migration + + + Omni Engineer + Implements the + version-two response + + + Demo API + Preserves legacy name + Adds profile.display_name + + + Claude Engineer + Reviews compatibility + and regression coverage + + + + + + + + Contract tests and response inspection + Verify the new shape, the legacy bridge, and stable errors + + + +