diff --git a/docs/plans/retire-oibot-agent.md b/docs/plans/retire-oibot-agent.md new file mode 100644 index 0000000..3bb4398 --- /dev/null +++ b/docs/plans/retire-oibot-agent.md @@ -0,0 +1,215 @@ +# Plan: retire `oibot-agent` from the Tobserver change workflow + +## Status + +Implementation is in progress. + +- The standalone catalog skill is implemented on `agent/tobserver-personal-agit` at commit `ce0f775` and published as [oibot/agent-skills PR #1](https://codeberg.org/oibot/agent-skills/pulls/1). +- AGit creation and a second linear push were verified against the same pull request without a write API token. +- Dotfiles are prepared to pin that catalog revision, load `create-skill` and `tobserver-change` in Pi, and package the bundled helper as `tobserver-agent`; Nix evaluation and the Home Manager build pass. +- The new configuration has not been activated, the Agent Skills PR has not been merged, and no bot credential has been revoked or deleted. +- Cutover is blocked until the user confirms Tobserver `main` protection in Codeberg and resolves or deliberately migrates the active legacy workspace at `/private/var/folders/5p/sj0yx8w92b51dzngrxn9gqz00000gn/T/tobserver-agent.d9wG9L` (`agent/add-bitcoin-pulse-module-wiring-20260723-130500-866d04`). + +## Goal + +Use the personal Codeberg account `oibot` for Tobserver change branches while preserving a human-only merge boundary: + +- `main` accepts changes only through pull requests; +- direct, force, and deletion pushes to `main` are denied, including for `oibot`; +- the agent can create and update pull requests without a write-capable Codeberg API token; +- a human merges through the Codeberg web UI; +- the agent never accesses or deploys to the live Tobserver machine. + +The trade-off is accepted: Codeberg will attribute both agent-created pull requests and human merges to `oibot`, so there is no account-level distinction or independently enforceable approval by a second user. + +## Repository and distribution decision + +The personal-only workflow will be implemented as a self-contained skill in the separate Agent Skills catalog rather than maintained as local skill and script copies in dotfiles. + +- The canonical skill directory will be `~/Code/agent-skills/tobserver-change/`. +- The directory will contain `SKILL.md`, the `tobserver-agent` helper under `scripts/`, routing and execution evals, focused helper tests, and every reference or asset required to install, operate, validate, and recover the workflow. +- The helper script will have one canonical source in the skill directory. Dotfiles may package or wrap that bundled script to expose `tobserver-agent` on `PATH`, but must not retain a second editable copy. +- The completed Agent Skills change will be pushed as an `oibot` branch and reviewed through a pull request in `oibot/agent-skills`; do not push directly to protected `main`. +- Dotfiles will add the Agent Skills repository as a pinned, non-flake input and configure Pi to load both `create-skill` and `tobserver-change` from that input. +- The local `config/pi/skills/tobserver-change` copy and the upstream `anthropic-skills`/`skill-creator` input will be removed after Pi successfully loads the catalog's `tobserver-change` and `create-skill` skills. +- `flake.lock` will pin the reviewed Agent Skills revision so a future catalog push cannot silently change Pi's active instructions or helper. +- Existing local helper and skill files remain available during migration and are removed only after the catalog-backed installation passes local and end-to-end validation. + +The intended order is: build and evaluate the standalone catalog skill, publish and review it, pin it in dotfiles, verify Pi and the packaged helper, complete the personal-only AGit cutover, and only then revoke or remove `oibot-agent` credentials. + +## Security invariants + +1. Protect `main` before changing credentials or helper behavior. +2. Do not place `oibot` on a direct-push or branch-protection bypass list. +3. Do not expose an `oibot` API token capable of merging pull requests or changing repository settings to the helper or agent. +4. Keep the existing workspace, branch-prefix, validation, secret, no-force-push, no-merge, and no-deployment checks. +5. Keep the current bot credentials available for rollback until the personal-only workflow completes an end-to-end test. +6. Do not delete keys, tokens, open pull requests, or active workspaces automatically. + +## Preconditions and decisions + +- [x] Confirm that independent Codeberg attribution and approval are no longer requirements. +- [x] Confirm that human merging will happen only in the Codeberg web UI. +- [x] Inspect existing open `oibot-agent` pull requests; the public API returned none. +- [x] Check `tobserver-agent status` and preserve the active legacy workspace recorded above. +- [x] Establish an isolated Agent Skills branch and preserve existing dotfiles commits and owner work. + +## Phase 1: enforce the server-side merge boundary + +In `oibot/Tobserver` repository settings, configure an exact protection rule for `main`: + +- [ ] deny direct pushes for everyone, including repository owners; +- [ ] deny force pushes; +- [ ] deny branch deletion; +- [ ] require changes to arrive through pull requests; +- [ ] retain applicable status-check and conversation-resolution requirements; +- [ ] ensure there is no `oibot` push or bypass exception. + +Verify the saved rule in the Codeberg UI. Do not probe Tobserver by attempting a push that could succeed if the rule is wrong. If a push-level test is desired, reproduce the rule in a disposable repository first. + +**Gate:** do not continue until the UI confirms that `oibot` cannot directly update `main`. + +## Phase 2: prove a tokenless pull-request path + +Use Forgejo AGit over SSH so pull requests can be created without an API write token: + +```sh +git push origin HEAD:refs/for/main/ \ + -o title="" \ + -o description="<description>" +``` + +Before refactoring the helper: + +- [ ] confirm `oibot` authenticates through `git@codeberg.org` with the personal SSH key; +- [ ] confirm the public Codeberg API exposes the pull-request metadata needed by `revise` without authentication; +- [ ] determine the exact AGit representation of the topic, head ref, and pull-request URL; +- [ ] determine a robust multiline Markdown description encoding supported by Codeberg; +- [ ] create a clearly labeled AGit test pull request from an empty commit so it has no tree changes; +- [ ] push a second linear empty commit with the same topic and verify that it updates the same pull request without force-pushing; +- [ ] close the test pull request manually in Codeberg. + +Do not merge the test pull request. + +**Gate:** if AGit creation, URL discovery, or linear revision cannot be made reliable without a write-capable personal API token, stop and reconsider the account-removal design. + +## Phase 3: refactor `tobserver-agent` + +Target file: `~/Code/agent-skills/tobserver-change/scripts/tobserver-agent` + +### Identity and credentials + +- [ ] change the push URL from `git@codeberg-tobserver-agent:oibot/Tobserver.git` to the normal personal Codeberg SSH endpoint; +- [ ] retain an informational agent-specific commit author name; +- [ ] replace the bot noreply email with an agreed `oibot`-compatible address; +- [ ] remove bot Keychain account/service constants; +- [ ] remove write-token loading and temporary authorization-header handling; +- [ ] use unauthenticated HTTPS only for public read operations. + +### Pull-request creation + +- [ ] replace API `POST /pulls` creation with one AGit push; +- [ ] generate a unique, shell-safe AGit topic tied to the existing `agent/*` branch/workspace identity; +- [ ] pass the validated title and Markdown body without unsafe quoting, truncation, or accidental shell interpretation; +- [ ] capture and validate the returned pull-request URL under `https://codeberg.org/oibot/Tobserver/pulls/`; +- [ ] preserve the workspace when AGit submission or URL discovery fails. + +### Pull-request revision + +- [ ] replace the `oibot-agent` author assertion with checks appropriate to the new model: canonical repository, open state, `main` base, expected AGit topic/head, and helper-compatible history; +- [ ] fetch revision metadata without a write-capable token; +- [ ] push additional commits using the same AGit topic; +- [ ] preserve normal fast-forward/linear updates and continue to forbid force-push; +- [ ] verify that updating a pull request cannot create a duplicate PR silently. + +### Existing safety checks + +Retain and regression-test: + +- [ ] helper-created temporary workspaces only; +- [ ] exact canonical fetch and push endpoints; +- [ ] `agent/*` local branch naming; +- [ ] refusal to work on `main`; +- [ ] no SOPS payload changes or plaintext secret access; +- [ ] Nix formatting, flake evaluation, derivation evaluation, Git whitespace, and Gitleaks checks; +- [ ] no merge, deployment, live-server access, force-push, remote branch deletion, or automatic failed-workspace cleanup. + +## Phase 4: update the Tobserver skill and evaluations + +Target files: + +- `~/Code/agent-skills/tobserver-change/SKILL.md` +- `~/Code/agent-skills/tobserver-change/evals/evals.json` + +Changes: + +- [ ] remove the dedicated-bot compatibility requirement; +- [ ] replace the bot SSH alias with the normal Codeberg SSH endpoint; +- [ ] describe protected `main` plus human web merge as the approval boundary; +- [ ] state that the helper must not use a write-capable Codeberg API token; +- [ ] replace bot-owned PR revision checks with canonical AGit topic/head checks; +- [ ] retain the `agent/*`, no-force-push, no-merge, no-deploy, and recoverability constraints; +- [ ] update eval expectations that currently require `oibot-agent` ownership; +- [ ] add or revise an eval covering rejection of direct `main` pushes and API-based merge attempts. + +## Phase 5: validate locally + +- [x] run `bash -n tobserver-change/scripts/tobserver-agent` from the Agent Skills repository; +- [x] run focused helper tests for branch/topic restrictions, AGit metadata, push-option encoding, URL validation, and revision matching; +- [ ] test failure paths with mocked Git/HTTP commands so failed submission preserves the workspace; +- [ ] run the repository's Nix checks without deploying or building a Tobserver system closure; +- [x] inspect the Agent Skills diff for credentials, token remnants, unrelated edits, and stale bot assumptions; +- [ ] rerun the `tobserver-change` routing and representative workflow evaluations. + +## Phase 6: end-to-end cutover test + +With bot credentials still intact for rollback: + +1. [ ] create a fresh helper workspace; +2. [ ] make an empty or harmless non-system test commit; +3. [ ] run all helper checks; +4. [ ] submit through AGit as `oibot`; +5. [ ] verify that one PR targets `main` and no direct `main` update occurred; +6. [ ] revise the same PR with one additional linear commit; +7. [ ] verify that no duplicate PR or force-push occurred; +8. [ ] close the test PR manually without merging; +9. [ ] verify failed and interrupted workspaces remain resumable. + +**Gate:** retire the bot only after the complete create-and-revise workflow passes. + +## Phase 7: retire bot credentials + +Perform these as explicit human-approved cleanup steps: + +- [ ] remove `codeberg-tobserver-agent` from `~/.ssh/config`; +- [ ] unload `~/.ssh/oibot-agent` from `ssh-agent`; +- [ ] revoke the bot API token in Codeberg; +- [ ] remove the bot token from macOS Keychain; +- [ ] remove the bot SSH key from Codeberg; +- [ ] remove `oibot-agent` repository access; +- [ ] archive or delete the bot account only after confirming it owns no required issues, PRs, repositories, or audit artifacts; +- [ ] delete local bot key files only with separate explicit confirmation and after any desired backup. + +Do not rewrite historical commits or pull requests merely to remove the bot name. + +## Rollback + +Until Phase 7: + +1. restore the previous helper push URL and bot token path; +2. restore the skill's bot-identity requirements; +3. use the preserved bot SSH alias and token; +4. resume preserved helper workspaces rather than recreating changes; +5. keep `main` protection in place throughout rollback. + +After bot credentials are revoked, rollback requires issuing a new constrained credential or restoring the bot account deliberately; do not weaken `main` protection as a shortcut. + +## Completion criteria + +- `oibot` cannot directly, forcibly, or destructively update `main`. +- The helper creates and linearly updates a single PR through SSH/AGit. +- The helper and agent have no write-capable Codeberg API token. +- Only a human web action merges a PR. +- All existing local checks and secret boundaries still pass. +- No live Tobserver access or deployment occurred. +- Bot credentials and access are removed only after successful cutover and explicit approval. diff --git a/flake.lock b/flake.lock index 804b905..fb6946c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,19 +1,20 @@ { "nodes": { - "anthropic-skills": { + "agent-skills": { "flake": false, "locked": { - "lastModified": 1784256457, - "narHash": "sha256-QZ+zJkyLd/42rxgtJEZSUOz9R75Tse6UXW7G0nOkFS8=", - "owner": "anthropics", - "repo": "skills", - "rev": "fa0fa64bdc967915dc8399e803be67759e1e62b8", - "type": "github" + "lastModified": 1784899453, + "narHash": "sha256-/tMA9LQkVClOOXREW4oeXhq9PzyEUK9RIo3GrfLl2SI=", + "ref": "agent/tobserver-personal-agit", + "rev": "ce0f775ccb125ea34b604e282496da18cea7c061", + "revCount": 3, + "type": "git", + "url": "https://codeberg.org/oibot/agent-skills.git" }, "original": { - "owner": "anthropics", - "repo": "skills", - "type": "github" + "ref": "agent/tobserver-personal-agit", + "type": "git", + "url": "https://codeberg.org/oibot/agent-skills.git" } }, "bun2nix": { @@ -309,7 +310,7 @@ }, "root": { "inputs": { - "anthropic-skills": "anthropic-skills", + "agent-skills": "agent-skills", "darwin": "darwin", "emacsConfig": "emacsConfig", "hermes-agent": "hermes-agent", diff --git a/flake.nix b/flake.nix index 90193ab..42404d6 100644 --- a/flake.nix +++ b/flake.nix @@ -24,8 +24,8 @@ hermes-agent.url = "github:NousResearch/hermes-agent"; - anthropic-skills = { - url = "github:anthropics/skills"; + agent-skills = { + url = "git+https://codeberg.org/oibot/agent-skills.git?ref=agent/tobserver-personal-agit"; flake = false; }; diff --git a/module/home-manager.nix b/module/home-manager.nix index 0da79f3..af05ece 100644 --- a/module/home-manager.nix +++ b/module/home-manager.nix @@ -2,6 +2,9 @@ let piFirecrawl = pkgs.callPackage ../pkgs/pi-firecrawl.nix { }; + tobserverAgent = pkgs.callPackage ../pkgs/tobserver-agent.nix { + script = inputs.agent-skills + "/tobserver-change/scripts/tobserver-agent"; + }; in { home.file.".clojure/deps.edn".source = ../config/clojure/deps.edn; @@ -59,6 +62,7 @@ in sops tree typescript + tobserverAgent wget ]; @@ -102,7 +106,8 @@ in pi.coding-agent = { enable = true; skills = [ - (inputs.anthropic-skills + "/skills/skill-creator") + (inputs.agent-skills + "/create-skill") + (inputs.agent-skills + "/tobserver-change") ]; environment = { FIRECRAWL_API_KEY.file = config.sops.secrets.firecrawl_api_key.path; diff --git a/pkgs/tobserver-agent.nix b/pkgs/tobserver-agent.nix new file mode 100644 index 0000000..bd0920f --- /dev/null +++ b/pkgs/tobserver-agent.nix @@ -0,0 +1,32 @@ +{ script +, coreutils +, curl +, findutils +, git +, gitleaks +, gnused +, jq +, nix +, nixpkgs-fmt +, openssh +, writeShellApplication +}: + +writeShellApplication { + name = "tobserver-agent"; + + runtimeInputs = [ + coreutils + curl + findutils + git + gitleaks + gnused + jq + nix + nixpkgs-fmt + openssh + ]; + + text = builtins.readFile script; +}