Skip to content

fix(ci): OPENAI_BASE_URL is optional, and an Actions variable not a secret - #351

Merged
Brian Krabach (bkrabach) merged 3 commits into
mainfrom
lane/baseurl-attractor
Sep 7, 2026
Merged

fix(ci): OPENAI_BASE_URL is optional, and an Actions variable not a secret#351
Brian Krabach (bkrabach) merged 3 commits into
mainfrom
lane/baseurl-attractor

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Mirror of microsoft/amplifier-bundle-dot-runner#86, for this repo's own
capsule pipelines. Two owner rulings, 2026-09-07:

  1. OPENAI_BASE_URL is OPTIONAL. Absent, the luna instance runs on the
    provider-openai module's own default endpoint -- "the default should have
    been fine"
    . The preflight must not refuse for its absence.
  2. It is NOT a secret. It is an endpoint URL, now set as a repository
    VARIABLE on both repos, so every workflow reads
    ${{ vars.OPENAI_BASE_URL || secrets.OPENAI_BASE_URL }} -- variable first,
    secret kept as a fallback so anyone who provisioned it as a secret is not
    broken. Character-identical to the expression in dot-runner#86.

What still refuses, unchanged

The issue-#155 / EXTENSIONS §36 guarantee is untouched: no run starts unable
to reach its second judge.
The preflight still refuses, before any of the
30-90+ minute budget is spent, on

  • a missing OPENAI_API_KEY (still a repo SECRET, still REQUIRED), and
  • an instance the graphs address that provider-instances.yaml does not define.

Nothing was weakened to make the endpoint optional. Two new refusals were
added, because the mechanism has its own failure modes: an optional-vars:
directive naming a placeholder the template never references, and an unset
optional var still referenced after the line deletion -- i.e. in a shape too
tangled to remove surgically. An unexpandable ${VAR} is left VERBATIM by the
engine, which in a base_url would aim the run at a literal host.

How "optional" is expressed

provider-instances.yaml declares it, in a machine-readable directive the
installer reads:

#   optional-vars: OPENAI_BASE_URL

install_provider_instances.sh already derived the instance ids from the
graphs and the credential names from the template, so that neither list is
written down twice; optionality now comes from the same place. Unset, the whole
base_url: ${OPENAI_BASE_URL} line is left out of the installed file and the
step log says which branch the run took, in the same words dot-runner's inline
preflight uses:

  credentials present : OPENAI_API_KEY (values not printed; the file on disk holds ${VAR} placeholders, not secrets)
  keys omitted        : OPENAI_BASE_URL
  OPENAI_BASE_URL is unset -- OPTIONAL by owner ruling 2026-09-07, so its key was left out of the installed instance and the provider module's own default endpoint applies. ...

"The operator chose the default" and "the endpoint silently went missing" must
not read the same in a job log.

The scrubber

OPENAI_BASE_URL stays on the literal watch list -- an endpoint's value
carries no debugging value in evidence. What matters now is the EMPTY case,
which this change makes normal: an empty literal matches at every position in
every file, so an unguarded watch list would not leak the evidence tree, it
would destroy it. MIN_LITERAL_LEN already prevented that; nothing pinned it
and nothing said why it mattered. Now both, with a positive control so the
empty-value test cannot pass vacuously.

Tests -- RED first

tests/test_provider_base_url_optional.py is new and was RED before the
change, 5 of 11 failing on the unmodified tree:

FAILED test_absent_base_url_installs_the_instance_without_the_key
FAILED test_the_omission_is_surgical
FAILED test_every_workflow_reads_base_url_from_vars_then_secrets[capsule-specify.yml]
FAILED test_every_workflow_reads_base_url_from_vars_then_secrets[feature-specify.yml]
FAILED test_every_workflow_reads_base_url_from_vars_then_secrets[capsule-implement.yml]
5 failed, 5 passed

It runs the SHIPPED installer in a throwaway HOME with an explicitly built
environment, so a developer who exports OPENAI_BASE_URL cannot colour the
result. It covers unset and set-but-empty -- the shape CI actually
produces, since Actions sets the env entry to "" rather than dropping it --
proves the omission is surgical (installed file == template minus exactly that
one line, byte for byte), proves neither the URL nor the key is ever expanded
onto disk, keeps the two surviving refusals locked, and pins the workflow
expression fail-closed.

Verification

  • python -m pytest tests/ -q --ignore=tests/e2e -> 241 passed, 2 skipped
    (both skips pre-existing).
  • actionlint (with shellcheck) on all three changed workflows: the only
    findings are two pre-existing SC2012 infos, present identically on main.
  • shellcheck clean on install_provider_instances.sh; PyYAML parses every
    workflow and the installed settings file in both branches.
  • dot -Tsvg renders task-runner.dot after its header-comment fix.
  • The scrubber's own suite: 33 passed, 103 subtests.

Records

A dated record keeps its words; a live claim gets corrected. The pipeline
README's Files rows and install_provider_instances.sh's refusal count are
corrected in place; the "New repo secrets required: OPENAI_API_KEY,
OPENAI_BASE_URL" lines in the README's re-sync log and in
docs/designs/REVIEW-pipelines-2026-09.md §4.4 are left verbatim and
superseded by an appended entry / a second addendum. task-runner.dot's header
bullet had been left by a same-day rename saying "OPENAI_API_KEY /
OPENAI_BASE_URL, not OPENAI_API_KEY" -- the same variable on both sides of a
contrast -- and now says what it meant to.

Generated with Amplifier

Co-Authored-By: Amplifier 240397093+microsoft-amplifier@users.noreply.github.com

…n a secret

Owner rulings, 2026-09-07, mirrored from the same change in
amplifier-bundle-dot-runner:

  1. OPENAI_BASE_URL is OPTIONAL. Absent, the `luna` instance runs on the
     OpenAI module's own default endpoint ("the default should have been
     fine"). The preflight must not refuse for its absence. It still refuses
     for a missing OPENAI_API_KEY and for an instance nothing can serve --
     the issue-#155 / EXTENSIONS Sec 36 guarantee that no run starts unable
     to reach its second judge is untouched.
  2. It is NOT a secret. It is an endpoint URL, now set as a repository
     VARIABLE on both repos, so every workflow reads
     `${{ vars.OPENAI_BASE_URL || secrets.OPENAI_BASE_URL }}` -- vars first,
     secrets kept as a fallback so an operator who stored it as a secret
     before the ruling is not broken.

.github/capsule-pipeline/provider-instances.yaml
  The header now separates REQUIRED (OPENAI_API_KEY, a secret) from OPTIONAL
  (OPENAI_BASE_URL, a variable), and carries the machine-readable half of
  that split: an `optional-vars:` directive the installer reads. The prose
  and the check can no longer disagree, which is the failure mode a
  documented-only "optional" always ends in.

.github/capsule-pipeline/install_provider_instances.sh
  Reads that directive. A required var missing is a refusal, as before. An
  optional var missing takes its whole `key: ${VAR}` line OUT of the
  installed file -- so the module default applies -- and the step log says
  which name was omitted and why, because "the operator chose the default"
  and "the endpoint silently went missing" must not look the same in a log.
  Two new refusals rather than guesses: a directive naming a placeholder the
  template does not reference (exit 2), and an unset optional var still
  referenced after the line deletion, i.e. in a shape too tangled to remove
  surgically (exit 2 -- the engine leaves an unexpandable ${VAR} verbatim,
  which in a base_url would send the run at a literal host).

.github/workflows/{capsule-specify,feature-specify,capsule-implement}.yml
  All 14 OPENAI_BASE_URL assignments (preflight, engine run, and every
  scan/scrub/gate step) take the vars-then-secrets expression. Each
  preflight step's comment states the split: which name is a secret and
  required, which is a variable and optional, and what the empty string --
  what the expression yields when neither exists -- makes the installer do.

tests/test_provider_base_url_optional.py
  New, and RED before this commit (5 of 11 failing). Runs the real installer
  in a throwaway HOME with an explicitly built environment, so a developer
  who exports OPENAI_BASE_URL cannot colour the result:
    - unset AND set-but-empty (the shape CI actually produces: Actions sets
      the env entry to "" rather than dropping it) install the instance with
      no base_url key, exit 0, and log the omission;
    - the omission is surgical -- the installed file is the template minus
      exactly its one base_url line, byte for byte;
    - set, the key is written as the ${VAR} PLACEHOLDER; neither the URL nor
      the api key is ever expanded onto disk;
    - a missing OPENAI_API_KEY still refuses, names that key, and does not
      mention OPENAI_BASE_URL -- a refusal that sends an operator to
      provision the wrong thing is worse than no message;
    - an instance the template does not define is still refused;
    - every workflow assignment matches the vars-then-secrets expression
      exactly (fail-closed if a file sets it nowhere);
    - the scrubber ignores an EMPTY watched value, with a positive control
      that a real value is still redacted -- an empty literal matches at
      every position in every file, so an unguarded watch list would shred
      the whole evidence tree the moment the endpoint stopped being set.

Verified: actionlint clean on all three workflows (the only two findings are
pre-existing SC2012 `ls` infos, present identically on HEAD); shellcheck
clean on the installer; PyYAML parses every workflow and the installed
settings file in both branches.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…claims, append the record

The rule this repo already applies (see the 2026-09-07 records commit): a dated
record keeps its words; a live claim gets corrected or marked past.

.github/capsule-pipeline/README.md
  - The `provider-instances.yaml` row said "Required repo secrets:
    OPENAI_API_KEY, OPENAI_BASE_URL". It now states the two different shapes:
    one required secret, one optional variable whose absence means the module's
    default endpoint, plus what the `optional-vars:` directive is for.
  - The `install_provider_instances.sh` row gains the optional half of the
    derivation (nothing is written down twice: instance ids from the graphs,
    credentials from the template, optionality from its directive) and the two
    new refusals, so its "three refusals" count is no longer wrong.
  - A new re-sync entry carries the two rulings, names the earlier entry's
    superseded line rather than editing it, and records the new guard.

docs/designs/REVIEW-pipelines-2026-09.md
  Sec 4.4's addendum ends with "New repo secrets required: OPENAI_API_KEY,
  OPENAI_BASE_URL". Half of that is now wrong, so it gets a second addendum
  rather than an edit -- the same treatment the first one got. Sec 4.4's core
  claim is untouched: a luna pin still cannot be smuggled in without its infra.

.github/capsule-pipeline/task-runner.dot
  Its 2026-09-07 header bullet read "the credential the run needs
  (OPENAI_API_KEY / OPENAI_BASE_URL, not OPENAI_API_KEY)" -- a same-day rename
  had left the same variable on both sides of a contrast, so the sentence said
  nothing. Now: one secret, OPENAI_API_KEY, and an optional variable. Header
  comment only; the graph body and its pinned sha are untouched, and the file
  still renders under `dot -Tsvg`.

.github/capsule-pipeline/scrub_secrets.py
  Comments only. The watch-list note records that OPENAI_BASE_URL is a
  variable, is optional, and that an UNSET one must be a no-op here rather than
  a scrub; MIN_LITERAL_LEN's note records that its floor case is what makes
  that true -- an empty literal matches at every position in every file, so the
  unguarded shape would not leak evidence, it would destroy it.

tests/test_no_live_gpt5_pins.py
  Its instance-provisioning guard demanded `secrets.<NAME>` for every ${VAR}
  the template references. That is now wrong for an Actions variable, so it
  accepts `vars.<NAME>` or `secrets.<NAME>` -- the exact expression stays
  pinned by tests/test_provider_base_url_optional.py, which is the file that
  should own it.

Verified: full root suite green (241 passed, 2 pre-existing skips);
`dot -Tsvg` renders task-runner.dot; the scrubber's own suite green
(33 passed, 103 subtests).

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ner uses

The sibling change in amplifier-bundle-dot-runner (PR #86) logs "<NAME> is set"
/ "<NAME> is unset" from its inline preflight. This repo's preflight is a
script over a template rather than a heredoc, so the two implementations differ
by construction -- but the job log a human greps should not. The installer now
prints one line per OPTIONAL var in those same words, and the guard pins the
phrase rather than a paraphrase of it.

Also verified against that PR: the workflow expression is character-identical
in both repos -- `${{ vars.OPENAI_BASE_URL || secrets.OPENAI_BASE_URL }}`.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach
Brian Krabach (bkrabach) merged commit 9f8ef57 into main Sep 7, 2026
6 checks passed
@bkrabach
Brian Krabach (bkrabach) deleted the lane/baseurl-attractor branch September 7, 2026 20:57
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