Skip to content

feat(engine): pin each org folder to its Vapi org - #84

Merged
scott-lowe-vapi merged 1 commit into
mainfrom
feat/org-pinning
Oct 28, 2026
Merged

scott-lowe-vapi merged 1 commit into
mainfrom
feat/org-pinning

Conversation

@scott-lowe-vapi

@scott-lowe-vapi scott-lowe-vapi commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Value

V.A.L.U.E. tier: small. It adds a check before every command's first request, a new committed file per org folder, and a new way for commands to stop. Stacked on #83.

  • Problem: nothing ties resources/<org>/ to a Vapi org. The key alone decides where a command reads and writes. feat(setup): per-org API key variables; stop one org's key reaching another #83 removes one way to get the wrong key (an exported generic variable), but not the others: a mistyped per-org variable, a .env file copied between orgs, a CI secret holding the wrong org's key. With any of these, apply pushes the folder into the wrong org, cleanup deletes from it, and pull fills the folder with another org's resources. Each one runs without an error.
  • Who it affects: teams with more than one org, especially dev and prod, where one wrong key sends a prod deploy to dev or dev's configuration to prod.
  • What changes:
    • Pinning: the first command that can see the key's org records it in resources/<org>/.vapi-org.json, which is committed with the folder. Promotion skips dotfiles, so the pin never travels to another org.
    • Checking: every command then checks the key before its first request: pull, push, apply, promote (through its child commands), cleanup and rollback. It stops when the key belongs to another org, naming both org IDs and where the key came from, never the key. It also stops when the key's org has no resources but the folder is pinned to an org that had some, since pushing would fill a fresh org with this folder.
    • How it finds the key's org: a private key can't ask which org it belongs to (/org and /org/me return 401), so the org ID comes from one-item list calls: /assistant?limit=1, then other types until one lists something. A matching key costs one extra request per command. If the probes fail (5xx, network), the command warns that the key wasn't checked and continues. A folder with no pin and no files makes no extra request.
    • Escape hatch: delete .vapi-org.json in a reviewed commit. There's no flag or environment variable to skip the check. The test preload sets an internal switch so tests that count requests stay deterministic, and org-pin.test.ts turns it back on.
    • Docs: README, AGENTS.md and the configuration guide describe the file. AGENTS.md tells agents to stop and ask, and never to edit or delete the pin to get past a stop.
  • Not covered yet: call, sim and check don't check the pin. They don't change configuration, but they could run against the wrong org.
  • Known gap: an org with no resources can't be identified, so it can't be pinned until something exists in it. A backend endpoint that tells a private key its org would close this, and would let setup show the org's name.

Evidence of value

resources/acme/ pinned to org A Before After
Command with org B's key Reads and writes org B Stops after one probe request: "belongs to Vapi org B, but resources/acme/ is pinned to org A"
Command with an empty org's key Pushes the folder into it Stops: "belongs to an org with no resources"
Command with org A's key Runs Runs (one extra GET)
Probes fail (5xx, network) n/a Warns the key wasn't checked, then runs
  • Tests (13 new, 575 total): probe parsing (array and { results } pages, empty, failed, 401), every check outcome, a corrupt pin, and no requests for an unpinned org with no folder. An end-to-end test runs pull against a stub API returning org B for a folder pinned to org A: exit 1, and only /assistant?limit=1 reached the API.
  • Live run against a production org (read-only pull --bootstrap --bindings-only): the first run pinned the folder to the right org. With a fake org ID planted in the pin, the next run stopped at its first request, before listing anything. With the pin restored, it ran with no extra output.

Testing plan

  • npm run build and npm test (575) pass.
  • After merge: in the dogfood repo, run npm run pull -- <org> once per org and commit the new .vapi-org.json files. Then run apply --dry-run with another org's key exported as that org's per-org variable, and check that it stops.

🤖 Generated with Claude Code

scott-lowe-vapi commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

@scott-lowe-vapi
scott-lowe-vapi marked this pull request as ready for review October 28, 2026 06:09

scott-lowe-vapi commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Oct 28, 5:53 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Oct 28, 5:55 PM UTC: Graphite rebased this pull request as part of a merge.
  • Oct 28, 5:55 PM UTC: @scott-lowe-vapi merged this pull request with Graphite.

@scott-lowe-vapi
scott-lowe-vapi changed the base branch from feat/per-org-api-keys to graphite-base/84 October 28, 2026 17:53
@scott-lowe-vapi
scott-lowe-vapi changed the base branch from graphite-base/84 to main October 28, 2026 17:53
Nothing tied resources/<org>/ to an org: the key alone decided where a
command read and wrote. A key for the wrong org, from an exported
variable, a copied .env file or a mislabelled CI secret, pulled another
org's resources into the folder or pushed the folder into another org.

- The first command that can see the key's org records it in
  resources/<org>/.vapi-org.json, committed with the folder. Promotion
  skips dotfiles, so the pin never travels to another org.
- Every command then checks the key before its first request: pull,
  push, apply, promote (through its child commands), cleanup and
  rollback. A key for another org stops the command, naming both org
  IDs and where the key came from, never the key. So does a key whose
  org lists nothing while the folder is pinned.
- A private key can't ask which org it belongs to (/org and /org/me
  reject private keys), so the org ID comes from one-item list calls.
  If those fail, the command warns that the key wasn't checked and goes
  on. A folder with no pin and no files makes no extra request.
- README, AGENTS.md and the configuration guide describe the file, and
  AGENTS.md tells agents never to edit or delete it to get past a stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@scott-lowe-vapi
scott-lowe-vapi merged commit 62fe579 into main Oct 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants