Skip to content

feat: managed variables in the state file#45

Open
vtkovapi wants to merge 1 commit into
mainfrom
feat/state-file-variables
Open

feat: managed variables in the state file#45
vtkovapi wants to merge 1 commit into
mainfrom
feat/state-file-variables

Conversation

@vtkovapi

@vtkovapi vtkovapi commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What

Adds support for managed variables in the gitops state file. Centralize values that repeat across resources — callback URLs, a default model name, shared prompt fragments, dynamically defined names — in one place, and reference them from any resource file with a {{name}} placeholder.

// .vapi-state.<env>.json
{
  "assistants": { "...": { "uuid": "..." } },
  "variables": {
    "callback_url": "https://example.com/vapi/webhook",
    "default_model": "gpt-4.1",
    "max_tokens": 260
  }
}
# any resource file
model:
  model: "{{default_model}}"
  maxTokens: "{{max_tokens}}"   # → the NUMBER 260, type preserved
server:
  url: "{{callback_url}}"

npm run push substitutes the values; npm run pull restores the placeholders; drift treats a templated file and its rendered platform resource as in sync.

Design

Whole-value only (per the agreed scope): a placeholder substitutes only when the entire value is a single {{name}}. This makes substitution a clean, type-preserving, losslessly-reversible operation, so variables slot into the same symmetric model the engine already uses for tool/assistant/credential references — rather than fighting drift detection.

  • Forward (push)resolveVariables runs inside resolveReferences before resourceId→UUID resolution, so a variable can even yield a reference (toolIds: ["{{tool_ref}}"]).
  • Drift stays cleanhashLocalResource renders variables before hashing; the platform side is already rendered, so both hash in one basis. No phantom drift, all value types, zero reverse-substitution false positives. canonicalizeForHash is intentionally unchanged.
  • Pull preserves templatesrestoreVariablePlaceholders re-inserts a placeholder only where the local file already had it and the value still matches (guided reverse). A field changed on the dashboard becomes a literal + the dashboard wins.
  • Reference-awarenessextractReferencedIds resolves variables too, so dependency auto-creation, orphan/delete protection, and ignored-reference validation all see the real resourceId behind a placeholder.
  • Validation — an undefined {{name}} is a blocking finding in push and validate.
  • State plumbingvariables is exempted from the migration guard and the npm run migrate slim-rewrite (otherwise it trips the legacy-format check / gets dropped). serializeState keeps hand-authored object-value key order byte-stable across saves.

Out of scope for v1: in-string interpolation, ${ENV_VAR} expansion, secrets in the committed state file (documented).

Process

Built via brainstorming → design → TDD. After implementation, ran a multi-agent adversarial review (16 agents, 4 dimensions, per-finding verification): 5 findings confirmed, 7 rejected. All 5 addressed in this PR — the one HIGH (extractReferencedIds was not variable-aware, breaking delete-protection and dependency auto-creation) fixed with a regression test; two LOW fixed in code; two niche LOW edges documented.

Testing

  • 39 new specs across tests/variables.test.ts, tests/state-variables.test.ts, tests/resolver-variables.test.ts (forward/reverse substitution, type preservation, round-trip identity, migration-guard exemption, migrate preservation, serializer key-order, reference composition, undefined-var validation, nullish-map tolerance).
  • tsc --noEmit clean, biome check clean.
  • Verified zero new failures vs the pre-existing baseline (the repo has flaky tsx-subprocess tests unrelated to this change).
  • Non-variable repos behave identically (empty variables → every code path is a no-op).

Docs

docs/learnings/variables.md (new) + the three index tables (README.md, AGENTS.md, CLAUDE.md) + an improvements.md entry (#27) recording the migration-seam footgun this surfaced.

🤖 Generated with Claude Code

@vtkovapi vtkovapi self-assigned this Jun 25, 2026
Add a `variables` section to `.vapi-state.<env>.json` for centralizing
values that repeat across resources — callback URLs, a default model name,
shared prompt fragments. Resource files reference them with whole-value
`{{name}}` placeholders.

- Forward (push): `resolveVariables` replaces `{{name}}` with the managed
  value, native type preserved, wired into `resolveReferences` BEFORE
  resourceId→UUID resolution so a variable can yield a reference.
- Drift stays clean: `hashLocalResource` renders variables before hashing;
  the platform side is already rendered, so both hash in one basis (no
  phantom drift). `canonicalizeForHash` is intentionally unchanged.
- Pull preserves templates: `restoreVariablePlaceholders` re-inserts a
  placeholder only where the local file already had it and the value still
  matches (guided reverse → zero false positives), on both the main and
  `--resolve=theirs` write paths.
- `extractReferencedIds` is variable-aware so dependency auto-creation,
  orphan/delete protection, and ignored-reference validation see the real
  resourceId behind a `{{placeholder}}`.
- Validation: undefined `{{name}}` is a blocking finding in push + validate.
- State plumbing: `variables` is exempted from the migration guard and the
  `npm run migrate` slim-rewrite (or it would trip the legacy-format check
  and be dropped); loaded via `normalizeVariables`. `serializeState`
  preserves hand-authored object-value key order on save.
- Whole-value only (no in-string interpolation), per design.

Docs: docs/learnings/variables.md + index tables + improvements.md (#27).
Tests: variables / state-variables / resolver-variables (39 specs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vtkovapi vtkovapi force-pushed the feat/state-file-variables branch from ce9ce18 to 8f6df7d Compare June 25, 2026 01:20
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.

1 participant