Skip to content

fix: canonicalize provider file dependencies - #987

Open
mldangelo-oai wants to merge 70 commits into
mainfrom
mdangelo/codex/sec-provider-reference-parsing
Open

fix: canonicalize provider file dependencies#987
mldangelo-oai wants to merge 70 commits into
mainfrom
mdangelo/codex/sec-provider-reference-parsing

Conversation

@mldangelo-oai

Copy link
Copy Markdown
Contributor

Summary

Fixes Codex Security finding csf_3ab060a90865e4376ff77143 (occ_5a7cef563208654a40c215b3).

Promptfoo supports Python provider references such as file://providers/provider.py:custom_call and absolute local provider URLs. The action retained the function selector in the dependency filename and re-rooted absolute paths, so provider-only changes could miss the relevance gate.

This patch:

  • removes a final selector only for supported .py:function references;
  • handles string and object-form provider IDs identically;
  • preserves provider-only absolute paths before repository containment;
  • handles Windows file-URL drive prefixes without confusing the drive colon;
  • leaves invalid JavaScript :callApi syntax un-reinterpreted;
  • adds a provider-only main gate regression;
  • regenerates the committed bundle.

Other file-bearing config fields retain their existing resolution semantics.

Verification

  • four supported/canonicalization regressions failed before the patch;
  • Promptfoo 0.121.17 accepts and executes both relative Python-selector and absolute-provider fixtures;
  • the patched bundled helper maps both to the exact underlying provider.py files;
  • Promptfoo rejects the JavaScript suffix with Could not identify provider;
  • absolute escape rejection and Windows drive-colon behavior are covered;
  • npm run all passes: 9 test files, 244 tests, 100% coverage;
  • post-commit npm run package leaves the tree clean.

Packaging note

Current main references a js-yaml 5.2.1 npm tarball that is no longer available, so npm ci cannot reproduce the merged dependency update. For bundle verification only, I built and packed the exact authoritative js-yaml Git tag (ac16b42c46c11c5c7f66062bfc78b168b5f07ecd) without changing repository metadata. The generated license artifact matched main before bundling this patch.

Copy link
Copy Markdown
Contributor Author

@codex review

Please review exact head 5b54d9d3b2513af90fa35c0b1e9796efd2d68b42, focusing on parity with Promptfoo's supported Python provider selector grammar, provider-only absolute path preservation plus repository containment, Windows drive-colon handling, the invalid JavaScript suffix negative control, the provider-only gate path, and whether the committed dist/ bundle matches source/current-main packaging.

Copilot AI 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.

Pull request overview

This pull request fixes dependency extraction for Promptfoo provider references so the GitHub Action correctly detects provider-only changes (including Python file://...provider.py:function selectors and absolute provider paths) and doesn’t miss runs behind the “relevance” gate.

Changes:

  • Canonicalizes provider file:// references by stripping only supported Python :function selectors and handling string/object provider IDs consistently.
  • Preserves absolute provider paths (when inside the repo) for correct containment checks; improves Windows drive-colon handling for file:///C:/... provider URLs.
  • Adds regression tests for provider canonicalization and updates the bundled dist/index.js.

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated no comments.

File Description
src/utils/config.ts Adds provider-specific file URL canonicalization and preserves absolute provider paths for correct dependency gating.
tests/utils/config.test.ts Adds targeted regression coverage for Python selector stripping, absolute provider containment, invalid JS suffix behavior, Windows drive-colon handling, and explicit null configs.
tests/main.test.ts Adds a main-gate regression to ensure provider file changes trigger the action run.
dist/index.js Regenerates the bundled action output to include the updated dependency extraction logic (and updated bundled deps).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mldangelo and others added 2 commits July 9, 2026 00:32
…ovider-reference-parsing

# Conflicts:
#	dist/index.js.map
Explain that only the Python `.py:function` selector is stripped (leaving an
invalid `.js:callApi` untouched) and why the Windows drive-letter slash is
dropped. Regenerate dist after merging main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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: 1210cfafbe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/utils/config.ts

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/utils/config.ts Outdated

@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: 80304634b2

ℹ️ 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 src/utils/config.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ 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".

@mldangelo-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@mldangelo-oai
mldangelo-oai requested a review from Copilot July 15, 2026 01:07

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 6 changed files in this pull request and generated no new comments.

@mldangelo-oai

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 10 changed files in this pull request and generated 2 comments.

Comment thread src/utils/config.ts Outdated
Comment thread src/main.ts Outdated

@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: be587921c7

ℹ️ 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 src/utils/config.ts Outdated

@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: 67aba3522b

ℹ️ 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 src/utils/config.ts Outdated
Comment on lines +1488 to +1489
} else if (mayRenderFileUrl(promptPath)) {
processPotentialFileUrl(promptPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply relative prompt-path detection after env rendering

When a prompt entry is an env template that renders to a normal relative prompt path, such as PROMPT_PATH=prompts/current/main.txt with prompts: ['{{ env.PROMPT_PATH }}'], this branch calls processPotentialFileUrl() and never falls through to the existing relative-path heuristic below. The extractor records no dependency, so a PR that only changes prompts/current/main.txt can be skipped even though Promptfoo evaluates that prompt file.

AGENTS.md reference: AGENTS.md:L7-L8

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +1686 to +1688
const fileUrl = testPath.startsWith('file://')
? testPath
: `file://${testPath}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid double-prefixing rendered test file URLs

When tests is an env template that renders to a file URL, for example TESTS=file://tests/cases.yaml with tests: '{{ env.TESTS }}', this check prefixes the raw template before rendering, so processFileUrl() sees file://file://tests/cases.yaml and records file:/tests/cases.yaml instead of tests/cases.yaml. A PR changing only the test file then misses the dependency gate; decide whether to add file:// from the rendered test path instead.

AGENTS.md reference: AGENTS.md:L7-L8

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
) {
return;
}
const sheetSeparator = value.indexOf('#');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Strip sheet suffix after rendering templated test paths

When a templated test path renders to an Excel sheet reference, such as TESTS=tests/cases.xlsx#Sheet1 with tests: '{{ env.TESTS }}', the sheet separator is searched on the raw template rather than the rendered path. The dependency is recorded as tests/cases.xlsx#Sheet1, while GitHub reports changes to tests/cases.xlsx, so a PR that only updates the spreadsheet can be skipped even though Promptfoo reads it.

AGENTS.md reference: AGENTS.md:L7-L8

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
current as { options?: { provider?: unknown } }
).options?.provider;
if (gradingProvider) {
processProviderValue(gradingProvider, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase external grading providers before extracting dependencies

When an imported test file defines a grading provider under options.provider, this path is still resolved from the main config directory, unlike the test-level provider just above. For example, tests/cases.yaml containing options: { provider: 'python:graders/rubric.py:call_api' } makes Promptfoo load tests/graders/rubric.py, but the action watches graders/rubric.py, so a PR changing only the actual grading provider can be skipped.

Useful? React with 👍 / 👎.

@mldangelo mldangelo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Canonicalizes provider file dependencies and conservatively resolves templated/mapped provider paths and env templates. This is the most defensively-bounded member of the family — explicit MAX_PROVIDER_VALUES (1024), MAX_PROVIDER_CONFIGS (128), MAX_GLOB_MATCHES (4096), and MAX_BRACE_EXPANSIONS caps, plus recursion guards (activeProviderObjects WeakSet, a visit counter) — which is good. But it overlaps almost entirely with #979 (scalar provider deps) and substantially with #974 (mapped providers): three separate PRs independently reworking provider dependency extraction on the same file. I did a structural pass here; the caps and guards look sound, and it keeps the fail-open return [] catch. The overriding recommendation is to pick one provider implementation and consolidate.

Cross-cutting (the seven config.ts PRs): #974, #977, #979, #981, #982, #983, and #987 are seven independent branches off main that each rewrite the same extractFileDependencies in src/utils/config.ts (this file is 251 lines on main; these PRs add ~130–1000 diff lines each). They mutually conflict — only one can merge cleanly and the rest need large manual reconciliation — and they don't even agree on error semantics: on an unexpected parse error #974 throws (fails the action), most return [] (silently disables dependency tracking, which can miss a changed dependency), and #979 returns ['./'] (conservatively watches the root — the safest of the three). Strongly recommend consolidating this family into a single stacked/reviewable change with one agreed error policy (prefer #979's conservative-watch) and shared helpers, rather than merging seven overlapping rewrites.

Merge/stacking note: One of seven competing config.ts rewrites; overlaps with #979 and #974 on provider dependency extraction specifically — those three should become one.

@mldangelo

Copy link
Copy Markdown
Member

Review (current head 67aba35) — no code change pushed

Re-reviewed against the current head; no change pushed because I found no simplification that's both safe and clearly worth it on this head (clean of the duplicated-constant / repeated-sentinel patterns I fixed elsewhere).

Assessment

This is the most defensively-bounded member of the family — explicit MAX_PROVIDER_VALUES, MAX_PROVIDER_CONFIGS, MAX_GLOB_MATCHES, and MAX_BRACE_EXPANSIONS caps, plus recursion guards (activeProviderObjects WeakSet + a visit counter). The caps and guards look sound, and it keeps the fail-open return [] catch.

The real issue is overlap, not this PR's internals

#987 (provider canonicalization), #979 (scalar provider deps), and #974 (mapped providers) all independently rework provider dependency extraction on the same function. Three parallel implementations of the same concern is the thing to fix — pick one provider implementation and consolidate the other two into it, rather than merging three overlapping rewrites. More broadly this is one of the seven competing config.ts PRs that should become a single stacked change with one error policy and shared helpers.

Original inline comments predate a restack and may show as "outdated".

isValidFunctionName restricted .go selectors to call_api/CallApi, so
file://p.go:OtherFunc recorded a dependency literally named 'p.go:OtherFunc'
that never matches the real p.go, missing a re-eval when p.go changes.
promptfoo's GolangProvider strips any :function selector for a .go file (like
.py/.rb), so align .go with the generic function-name check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mldangelo

Copy link
Copy Markdown
Member

Correction: a real bug was here — fix pushed (f9a80e9)

My earlier "no code change" comment was based on a pattern scan; a fresh, adversarially-verified correctness pass on the current head then found a genuine P3 bug, now fixed. npm run all passes (10 files, 576 tests, 100% coverage).

The bug

isValidFunctionName restricted .go provider selectors to exactly call_api/CallApi, while every other script type (.py, .rb, js) uses the generic /^[^\\/:\0]+$/u check. So providers: [file://p.go:OtherFunc] was not canonicalized — providerFilePath returned the raw p.go:OtherFunc, and the action recorded a dependency literally named p.go:OtherFunc that never matches a change to the real file p.go. Editing p.go therefore failed to trigger a re-eval. This is under-inclusive (a missed re-run), which is worse than over-running, though narrow (custom Go function names).

I verified against node_modules/promptfoo: GolangProvider's parsePathOrGlob strips any :function selector for a .go file (no name constraint), and executeGolangScript passes custom function names through — so OtherFunc is a valid, supported Go provider function and the real dependency is p.go. Notably the golang:...go:CustomCall path in this same PR already strips arbitrary names; only the file://…go:… path was inconsistent.

The fix

Align .go with the generic function-name check (drop the .go-specific call_api|CallApi branch), so file://p.go:OtherFunc canonicalizes to p.go. Updated the test that previously locked in the "preserve unsupported Go selector" behavior to assert the corrected canonicalization.

@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: f9a80e968f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/utils/config.ts Outdated
Comment on lines +1945 to +1947
if (config.extensions) {
for (const extension of config.extensions) {
processFileUrl(extension, false, false, configEnvironment, false, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve extension paths before stripping selectors

When an extension is configured as a plain file path with a hook selector, e.g. extensions: ['extensions/hooks.js:extensionHook'], this passes the raw string into the script-path parser, which blindly drops the first file://. Promptfoo documents extensions as file paths with a function name, so the action records ons/hooks.js instead of extensions/hooks.js; a PR changing only the actual hook file can miss the dependency gate and skip an evaluation that would run changed code.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +1485 to +1486
} else if (mayRenderFileUrl(promptPath)) {
processPotentialFileUrl(promptPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Inspect string-form structured prompt files

When a structured prompt is referenced in the common string/id form, such as prompts: ['file://prompts/chat.yaml'] or { id: 'file://prompts/chat.yaml' }, this branch only records the YAML/JSON file itself. Since Promptfoo's YAML/JSON prompt processors recursively resolve file:// references inside those files, a PR that changes only a nested fragment and no watched prompt glob/config file can leave dependencyChanged false and skip an evaluation that would load the changed fragment; inspectStructuredPrompt() currently only runs for object-form file: below.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +1474 to +1478
const executablePath = promptPath.slice('exec:'.length);
processFileUrl(
executablePath.startsWith('file://')
? executablePath
: `file://${executablePath}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse exec prompt command parts

When an executable prompt includes an interpreter or arguments, such as prompts: ['exec:python scripts/build_prompt.py --mode ci'], this prefixes the whole command as a single file URL and records python scripts/build_prompt.py --mode ci instead of the script that Promptfoo executes after splitting the command. In workflows that set prompts for gating, a PR changing only scripts/build_prompt.py can therefore miss dependencyChanged and skip an evaluation that would run changed prompt-generation code.

Useful? React with 👍 / 👎.

Comment thread src/utils/config.ts Outdated
Comment on lines +1696 to +1697
if (!/\.(?:ya?ml|jsonl?)$/i.test(externalTestPath)) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Inspect CSV test-case dependencies

When external tests are loaded from CSV, for example tests: file://cases.csv with a __expected value like file://validators/check.js, this extension gate records only the CSV file and skips inspecting the assertion dependencies inside it. Promptfoo supports CSV test files and parses file-backed JavaScript assertions from __expected, so a PR changing only the validator can leave dependencyChanged false and skip an evaluation that would load changed assertion code.

Useful? React with 👍 / 👎.

A directory dependency emitted without a trailing slash (e.g. 'tools') was
only prefix-matched when isDirectory(dep) was true, so a PR that deletes the
directory (isDirectory false) skipped the eval even though a watched child
file changed. Always test the directory-prefix form; a file dependency has no
children so it never false-matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mldangelo

Copy link
Copy Markdown
Member

Another correctness fix pushed (138f0d1) — deleted directory dependency

An extended adversarial pass over the main.ts matching logic found a real missed-re-eval bug (P3), now fixed. npm run all passes (576 tests, 100% coverage, CI green).

A directory dependency emitted without a trailing slash (e.g. tools) was only prefix-matched when isDirectory(dep) was true. In a PR that deletes the directory, isDirectory returns false, so the dep.endsWith('/') || isDirectory(dep) block was skipped and a changed child (tools/agent.js) failed to trigger the eval — a false all-clear for a deleted provider-dependency directory. Fixed by always testing the directory-prefix form (a file dep has no children, so it never false-matches). This same bug affected #974/#979/#982/#983 and is fixed in all of them.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-24T08:36:48.491760Z ac5679d New commits
🔒 Security Review Completed 2026-08-24T08:37:44.561242Z ac5679d New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 712af1971f

ℹ️ 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 src/utils/config.ts
for (const provider of config.providers) {
if (typeof provider === 'string' && provider.startsWith('file://')) {
processFileUrl(provider);
processFileUrl(provider, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebuild the distributed action bundle

This source change is not included in dist/, which is byte-for-byte identical to the parent commit. Because action.yml:5 executes dist/index.js, users invoking the action still run the old processFileUrl implementation and function-qualified Python provider changes remain undetected; the rebuild check in .github/workflows/check-dist.yml:41-51 will also fail. Regenerate and commit the bundle as required.

AGENTS.md reference: AGENTS.md:L96-L99

Useful? React with 👍 / 👎.

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.

4 participants