Skip to content

feat(config): project-local config discovery for CLI and SDKs - #866

Open
kcoopermiller wants to merge 16 commits into
mainfrom
feature/local-config-discovery
Open

feat(config): project-local config discovery for CLI and SDKs#866
kcoopermiller wants to merge 16 commits into
mainfrom
feature/local-config-discovery

Conversation

@kcoopermiller

@kcoopermiller kcoopermiller commented Aug 26, 2026

Copy link
Copy Markdown
Member

Summary

Lets the CLI and SDKs use project-local credentials instead of only ~/.prime/config.json — e.g. on a shared box where all your code lives under prime/, put credentials at prime/.prime/config.json and everything run from there (or any subdirectory) uses them.

Config directory resolution, for the CLI and the prime-sandboxes, prime-evals, prime-tunnel, and prime-traces SDKs:

  1. PRIME_CONFIG_DIR (explicit)
  2. Nearest ancestor of the working directory containing a trusted .prime/config.json
  3. ~/.prime (unchanged default)

Per-field env vars (PRIME_API_KEY, PRIME_TEAM_ID, …) still take precedence over either file. All Config() call sites are no-arg, so every command and SDK client picks this up automatically.

cd ~/code/prime
prime login --local          # or: prime config set-api-key --local
prime config view            # "Config File" row shows the active file and why; lists ignored files

# a config.json you wrote by hand needs a one-time approval:
prime config trust           # or: prime config trust ~/code/prime

Details

  • Explicit trust, à la direnv allow. A discovered .prime/config.json is ignored (one warning line) until prime config trust records it in ~/.prime/trusted_configs.json as resolved path → sha256 of content. A file that changes under the user (e.g. git pull) must be re-trusted. This is what stops a config committed to a cloned repository from pointing an environment-provided PRIME_API_KEY at someone else's base_url. Files the CLI writes itself (--local flows, set-*) refresh the entry; PRIME_CONFIG_DIR and explicit Config(config_dir=…) are trusted as such. Untrusted candidates are skipped rather than ending the search, so a nested planted file can't mask the user's trusted one further up. prime config untrust withdraws approval.
  • No merging. A project-local config is a whole replacement for the global one. Merging would let a local file with only a base_url send the user's global API key elsewhere.
  • Shared-box hardening. The upward walk stops at $HOME, and files not owned by the current uid are never candidates. Skipped on Windows, which has no uid model.
  • Nothing written until success. login --local / set-api-key --local use Config(create=False): the local file only appears on the first set_* call, so an aborted login can't leave an empty file that shadows the global config. A new local config inherits the service URLs of the currently active config instead of resetting to production.
  • Gitignore warning walks up to the enclosing repo root and asks git check-ignore about the real path (with a .gitignore scan fallback), so --local from a subdirectory of a repo is covered.
  • Config and environment files are now written owner-only (0600) — they hold API keys, and the CLI previously used the umask default.
  • prime-tunnel's frpc bin_dir deliberately stays under ~/.prime.
  • The SDK Config copies are kept dependency-free, so the discovery/trust helpers are duplicated into each (same pattern as the rest of those files). SDKs only read the trust registry; the CLI maintains it.

Tests

  • test_config_discovery.py in all five packages: global fallback, cwd/ancestor discovery, nearest-wins, untrusted ignored with warning, nested-untrusted doesn't mask trusted, trust bound to content, no-merge, env-var precedence, PRIME_CONFIG_DIR override, stop-at-home, ownership check, writes go to the discovered file and refresh trust, create=False defers writes, adopt_urls, trust/untrust CLI, --local flows incl. URL inheritance, aborted login --local leaves nothing, gitignore warning from a nested directory of a real git init repo.
  • conftest.py in each package starts every test in a tmp working directory (and drops PRIME_CONFIG_DIR / PRIME_API_KEY). Without this, discovery run from a checkout under $HOME would reach the developer's real ~/.prime, and tests that patch HOME would then read and write it.
  • prime 1051 passed · prime-sandboxes 108 · prime-evals 20 · prime-tunnel 34 · prime-traces 155; ruff and ty clean.
  • Smoke-tested the real CLI in a temp $HOME: hand-written local config ignored → prime config trust → used → file edited → ignored again; aborted login --local leaves no .prime/.

🤖 Generated with Claude Code

https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw


Note

High Risk
Changes how API keys and base URLs are resolved across the CLI and all SDKs; mistakes could misroute credentials or break existing setups that rely only on ~/.prime.

Overview
Adds project-local credentials so the CLI and Python SDKs can use ./.prime/config.json (from prime login --local / prime config set-api-key --local) instead of only ~/.prime. Resolution is PRIME_CONFIG_DIR → nearest trusted ancestor .prime/config.json~/.prime; a local file fully replaces the global one (no merge). PRIME_* env vars still win over files.

Trust (direnv-style): discovered project configs are ignored until prime config trust records path → SHA-256 in ~/.prime/trusted_configs.json; content changes require re-trust. CLI adds config trust / untrust, richer config view, gitignore warnings for --local, and deferred writes (create=False) so failed login does not leave an empty local config. Hardening: uid ownership on candidates, no project-local writes through symlinks, same trust rules for .prime/environments/*.json, owner-only atomic 0600 JSON writes, and flock on the trust registry.

The same discovery helpers are applied in prime_cli and each SDK (prime-evals, prime-sandboxes, prime-traces, prime-tunnel); prime-tunnel keeps frpc bin_dir under ~/.prime. Docs and broad test_config_discovery + hermetic conftest fixtures cover the new behavior.

Reviewed by Cursor Bugbot for commit 826318d. Bugbot is set up for automated code reviews on this repo. Configure here.

Resolve the config directory as PRIME_CONFIG_DIR > nearest ancestor of the
working directory containing .prime/config.json > ~/.prime, so credentials
can live next to a project (e.g. on a shared machine) instead of only in the
home directory. Applies to the CLI and the sandboxes, evals, tunnel and
traces SDKs; every Config() call site is no-arg so all of them pick it up.

- A project-local config is a whole replacement for the global one, never
  merged with it, so a planted local file cannot pair its base_url with the
  user's global API key. The walk stops at $HOME and ignores files not owned
  by the current uid.
- `prime login --local` / `prime config set-api-key --local` create
  ./.prime/config.json (mode 0600) and warn when the repo does not gitignore
  .prime/. `prime config view` shows which file is active.
- Config and environment files are now written owner-only (0600).
- Test conftests start each test in a tmp working directory so discovery
  never reaches the developer's real ~/.prime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91ab831a36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Comment thread packages/prime/src/prime_cli/commands/config.py Outdated
Comment thread packages/prime/src/prime_cli/commands/login.py Outdated
CI exports a PRIME_API_KEY secret for the sandboxes job, and env vars
outrank the config file by design, so the discovery test that expected the
file's key saw the secret instead. Drop the var in the test isolation
fixtures (sandboxes, evals, tunnel) alongside PRIME_CONFIG_DIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Comment thread packages/prime/src/prime_cli/core/config.py
Comment thread packages/prime/src/prime_cli/commands/config.py
Address review findings on project-local config discovery:

- Ownership is not enough to trust a discovered .prime/config.json: one
  committed to a repository the user clones is owned by the user and could
  redirect an environment-provided PRIME_API_KEY to an attacker's base_url.
  Discovered local configs are now ignored (with a one-line warning) until
  approved via `prime config trust`, recorded in ~/.prime/trusted_configs.json
  as path + content digest so a changed file must be re-trusted. The CLI's
  own writes (`--local` flows, set-*) refresh the trust entry; PRIME_CONFIG_DIR
  and explicit Config(config_dir=...) remain trusted as such. Untrusted
  candidates are skipped rather than ending the search, so a nested planted
  file cannot mask the user's trusted one. `prime config untrust` withdraws
  approval; `prime config view` lists ignored files.
- `prime login --local` / `set-api-key --local` no longer write a default
  file before authentication succeeds (Config(create=False)); an aborted
  login leaves nothing behind that would shadow the global config. A new
  local config inherits the service URLs of the currently active config
  instead of resetting to production.
- The gitignore warning walks up to the enclosing repository root and asks
  `git check-ignore` (falling back to scanning .gitignore files), so running
  `--local` from a subdirectory of a repo is covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Comment thread packages/prime/src/prime_cli/commands/config.py
`prime login --local` / `set-api-key --local` loaded whatever
./.prime/config.json was already there and, on writing the key, recorded
trust for the whole file — so a config planted in a cloned repository would
have its base_url adopted (and the login challenge sent to it). Refuse an
existing untrusted file with a pointer to `prime config trust`; a trusted
one is still updated in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Comment thread packages/prime/src/prime_cli/commands/config.py
The untrusted-file guard treated ~/.prime/config.json as a project-local
candidate when run from $HOME; the global config is never in the trust
registry, so `--local` there was refused. Recognize the global dir, skip
the trust check and the project-local notice for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Comment thread packages/prime/src/prime_cli/commands/config.py
`Config.is_global` compared unresolved paths while the --local guard
resolved them. Path.cwd() is always physical and Path.home() may go
through a symlink, so with a symlinked home `--local` from $HOME printed
the project-local notice and entered ~/.prime/config.json in the trust
registry. Route both through one resolved-path helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1e298f769

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/commands/config.py
On a shared machine a trusted path could be swapped for someone else's
file carrying the same bytes; the digest check alone still accepted it and
`--local` would write the new key into it before `_record_trust` noticed.
`is_trusted_local_config` (CLI and SDKs) now also requires the file to be
owned by the current user, `--local` refuses a foreign-owned existing file
up front, and `_write_private_json` refuses to write into any file — or
through any symlink — not owned by the current user, so O_TRUNC can't be
turned into a write to wherever a planted symlink points. User-owned
dotfile-style symlinks keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf57d55d56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
With a trusted project config, PRIME_CONTEXT / `prime config use` loaded
.prime/environments/<name>.json unchecked, so a repository update could
rewrite an environment's base_url while config.json stayed trusted. Trust
is now recorded per file: `prime config trust` approves config.json and
its environment files together, CLI writes to environment files refresh
their entries, `untrust` drops them, and a discovered local config refuses
untrusted environment files (PRIME_CONTEXT warns and skips; `use` errors).
Global and explicit config dirs are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Comment thread packages/prime/src/prime_cli/core/config.py Outdated
`_environment_file_is_trusted` only applied to discovered ("local")
configs, but `Config.local()` used by `login --local` is "explicit", so
PRIME_CONTEXT could still load a planted ./.prime/environments/<name>.json
and send the auth challenge to its base_url. Gate every non-global config
dir except an explicit PRIME_CONFIG_DIR, mirroring `_record_trust`.

`set_traces_url_for_active_environment` also read-modified-wrote the
environment file and then re-trusted it, which would launder a rewritten
base_url into trust; it now refuses an untrusted environment file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
…nfig

`set_traces_url_for_active_environment` wrote config.json and only then
refused an untrusted environment file, leaving traces_url persisted (and
re-trusted) on the root despite the command failing. Check the environment
file first so a refusal leaves no half-applied state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be971300be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Comment thread packages/prime/src/prime_cli/core/config.py
…gout trusted

A cloned repository can ship `.prime/config.json -> ../leak.json` (dangling,
so the path looks absent) or a symlinked environments/<name>.json; the
credential write would follow it and create a key-bearing file outside the
ignored .prime/ directory, or clobber an existing target. Project-local
config dirs now never write through symlinks (dangling or not) and `--local`
refuses a symlinked config.json up front; `prime config trust` skips
symlinked environment files. The global ~/.prime still follows user-owned
symlinks to an existing target, for dotfile setups, but refuses dangling
ones.

`prime logout` rewrote the active environment file with a plain write_text,
so on a project-local config its trust digest went stale and the next
`prime config use` was refused. Route it through
Config.update_current_environment_file(from_env=False), which does the
owner-only write and refreshes the trust entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a2bef34. Configure here.

Comment thread packages/prime/src/prime_cli/core/config.py
Comment thread packages/prime/src/prime_cli/core/config.py
…inked files

The symlink guards only inspected the leaf, so a cloned repository could
ship `.prime` or `.prime/environments` as a directory symlink and still
redirect credential writes outside the ignored tree. And a symlinked file
whose target was registered still counted as trusted (the registry keys on
the resolved path), so discovery or set-traces-url accepted it and the
later write refused after the root file was already updated.

One rule now covers project-local files: if the file, its directory, or
its .prime directory is a symlink it is never trusted, never recorded by
`prime config trust`, never a candidate environment file, and never
written to — and `--local` refuses it up front. The SDK copies mirror the
"never trusted" half so CLI and SDKs agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2bef34533

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Comment thread packages/prime/src/prime_cli/core/config.py
With a trusted project config whose current_environment is a named one,
a checkout could rewrite the (possibly tracked) environments/<name>.json;
the load path refused it, but update_current_environment_file() — run as
a side effect of login / set-* / logout — overwrote it with the fresh API
key and recorded the new digest as trusted, and `prime config save` did
the same. Both now check trust first: the implicit path skips the file
with a one-line warning so the command still succeeds, and `save` fails
with an explicit error pointing at `prime config trust`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ab76c3842

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py
`prime config delete <name>` unlinked environments/<name>.json through
whatever the environments/ directory resolved to, so a checkout that turned
it into a symlink would delete an attacker-chosen external file. Apply the
same involves_symlink rule as the write path for project-local configs,
and drop the file's trust entry when it is deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 540d56027a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Comment thread packages/prime/src/prime_cli/core/config.py
…mp file

`_write_private_json` opened the target with O_CREAT|O_TRUNC and chmod'ed
afterwards, so a legacy 0644 config.json held the new API key between the
write and the chmod, and readers of trusted_configs.json (every SDK
Config() loads it) could observe an empty or partial file while the CLI
was refreshing an entry, silently falling back to the global config.

Write to a fresh 0600 temporary file in the same directory and os.replace
it over the target: a permissive file is replaced rather than rewritten,
and readers see either the old or the new content. User-owned symlinks in
the global config dir are honored by replacing their target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
@kcoopermiller

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49955611ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/prime/src/prime_cli/core/config.py Outdated
Registry writes were atomic but the load-modify-save around them was not,
so two CLI processes refreshing different projects could each load the
same registry and the later replace dropped the other's entry, making that
project silently fall back to the global config. Every mutation now runs
under an flock on a sibling lock file (no-op on Windows); the registry
itself can still be atomically replaced while the lock is held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
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