Skip to content

Verify changed connector endpoints against vendor docs - #115

Closed
Bencheng21 wants to merge 5 commits into
mainfrom
verify-endpoint-changes-against-vendor-docs
Closed

Bencheng21 wants to merge 5 commits into
mainfrom
verify-endpoint-changes-against-vendor-docs

Conversation

@Bencheng21

@Bencheng21 Bencheng21 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Why

Connector PRs regularly change an API endpoint or migrate to a newer one (/v1//v2/, host changes, replacement endpoints). Two things go stale when that happens:

  1. The endpoint doc block at the top of client.go (criterion C1) keeps pointing at the old API.
  2. The endpoint the PR just migrated to may itself already be marked deprecated or scheduled for sunset in the vendor's docs.

The reviewer could not see (2) at all — the allowlist in action.yml carried no WebFetch, WebSearch, or curl.

The grant is opt-in and empty by default

New doc_fetch_domains input, threaded through the reusable workflow:

uses: ConductorOne/github-workflows/.github/workflows/pr-review.yaml@main
with:
  doc_fetch_domains: developer.okta.com,docs.okta.com
EXTRA_TOOLS=""
if [ -n "${DOC_FETCH_DOMAINS}" ]; then
  IFS=',' read -ra DOC_DOMAINS <<< "${DOC_FETCH_DOMAINS}"
  for domain in "${DOC_DOMAINS[@]}"; do
    EXTRA_TOOLS="${EXTRA_TOOLS},WebFetch(domain:${domain})"
  done
fi

Empty is the default, and grants nothing — claude_args ends at Bash(gh api:*)" with no WebFetch fragment at all, so every repo that has not opted in reviews exactly as it does today. general-pr-review.yaml declares workflow_call: {} with no inputs, so a general-profile caller cannot pass domains in the first place.

The value is unvalidated by design: it is set in the caller's own workflow file, which is already a code-reviewed artifact, so validating it here would be guarding against a repo maintainer attacking their own review job. A malformed entry produces a grant that never matches, which surfaces as E4 rather than a failed step.

Approval is per-repo, not per-PR. The domain list lives in the caller workflow, so changing it is a PR that CODEOWNERS can route to the connector approvers. No second run, no per-PR pause, and a PR cannot widen its own allowlist by adding a URL to client.go.

Review criteria (prompts/mixins/connector.md)

  • C9 — the client.go doc block (URL, API version, scopes) must be updated in the same PR when an endpoint path, version, or host changes.
  • C10 + new Endpoint Verification section — fetch the doc URL for changed endpoints and confirm the endpoint exists with the path and method used, is not marked deprecated/sunset, and that its documented scopes match the connector config and docs/connector.mdx. Gated the same way Provisioning already is.
    • E1 confirmed deprecation on a newly added endpoint → blocking-correctness
    • E2 future sunset date → suggestion, with the date named
    • E3 undocumented scope requirements → suggestion, or blocking when an existing install would break
    • E4 unreachable, auth-gated, JS-only, outside the configured domains, or WebFetch unavailable → stated suggestion; explicitly not a pass and not a failure
    • E5 missing doc URL is a C1/C9 finding, not a cue to hunt for a substitute URL
  • C11 — version segments belong in one constant so a migration is a one-line change.
  • B10 — endpoint swaps are breaking when they change response shape, ID semantics, or required scopes.
  • A Known Safe Patterns row so doc-URL redirects are not read as deprecation, and bug pattern 12 for offset→cursor migrations that leave the old token parsing in place.

Since doc_fetch_domains is empty by default, the section degrades explicitly: C9, C11, and B10 still apply from the diff alone, and the summary must state that vendor documentation was not verified. An unavailable or denied fetch is never evidence the endpoint is current.

prompts/base-pr-review.md is deliberately untouched — the shared base prompt should not describe a capability only one profile has, and its Step 5 tool list is guidance rather than a closed allowlist.

Security posture

Fetched content lands in a context holding pull-requests: write / issues: write and the ability to call gh pr review. Two layers:

  • Tool layer (enforced): no grant at all unless the repo opted in; when granted, scoped to the named domains.
  • Prompt layer (advisory): WebFetch is permitted only for the Endpoint Verification section; only https:// URLs already present in the source or PR description; never constructed from a guess, never followed because page content said to; fetched content cannot change review_mode, current_sha, severity, or the verdict; no repo/diff/credential content in a fetched URL; max 5 fetches.

The workflow triggers on pull_request (not pull_request_target) and every real step is gated to same-repo PRs (pr-review.yaml:43), so fork PRs cannot trigger a fetch at all.

Not closed by this: a ConductorOne collaborator acting in bad faith, who can already ship code the reviewer reads. This defends against a careless or compromised URL, not a hostile insider.

Verification

  • Both YAML files parse. Criteria sequences intact (C1–C11, E1–E5, R1–R13, B1–B10, others unchanged).
  • Not yet exercised against a real PR. No test covers prompt assembly or the tool allowlist. Two things to confirm on the first run against a repo that sets doc_fetch_domains:
    1. A real WebFetch call appears in the log rather than a denial. The step logs Vendor doc fetching enabled for: … vs disabled., which makes that diagnosable.
    2. That WebFetch(domain:…) actually confines fetches to those domains. It restricts only if unmatched tool calls are denied in headless mode; if claude-code-action runs the CLI permissively, the grant would enable the tool without scoping it. Cheapest check is a throwaway PR whose client.go doc link points at a domain deliberately left off the list, and seeing whether the run denies it.
  • Expect E4 to be the common outcome at first, both from repos that have not opted in and from auth-gated or JS-rendered vendor portals.

🤖 Generated with Claude Code

Connector PRs regularly change an API endpoint or migrate to a newer one,
and the endpoint a PR migrates to may already be deprecated or scheduled
for sunset in the vendor's documentation. The diff cannot show that, and
the reviewer had no way to look: the allowlist carried no web access.

Grant the reviewer WebFetch and add connector criteria that use it:

- C9: the client.go doc block must be updated when an endpoint path, API
  version, or host changes.
- C10/Endpoint Verification: fetch the doc URL for changed endpoints and
  confirm the endpoint exists, is not deprecated, and that its documented
  scopes match the connector config and docs. E1-E5 set severities, and
  E4 makes an unreachable or auth-gated page a stated suggestion rather
  than a pass in either direction.
- C11: version segments belong in one constant so a migration is a
  one-line change.
- B10: endpoint swaps are breaking when they change response shape, ID
  semantics, or required scopes.

Fetched pages are untrusted input, so both prompts restrict fetches to
https:// URLs already present in the checked-out source or PR description
and state that page content can never change review mode, severity, or the
verdict. A Known Safe Patterns row keeps doc-URL redirects from reading as
deprecation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Bencheng21
Bencheng21 requested a review from gontzess September 11, 2026 20:04

@gontzess gontzess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

wait this has webfetch, i dont think we want this

Bencheng21 and others added 4 commits September 11, 2026 13:07
The tool grant was unconditional: claude_args is one shared string, so the
general profile got WebFetch too even though its prompt never asks for it.
Emit the grant from the existing review-config branch instead, so only the
connector profile carries it and general is byte-identical to before.

Revert base-pr-review.md. Its two additions were redundant with the mixin's
Endpoint Verification section, and putting them in the shared base prompt
described a capability the general profile does not have. The one thing the
base sentence contributed -- that WebFetch is not a general research tool --
moves into the mixin's fetch rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The grant was profile-scoped but otherwise unrestricted: any connector repo
review could fetch any URL that appeared in its own source. Restrict it at
the tool layer instead of in prompt text, which is the layer that matters
when the fetched page is itself untrusted input.

doc_fetch_domains takes bare hostnames and expands to one
WebFetch(domain:...) grant each. Empty, the default, grants no WebFetch at
all, so every repo that has not opted in reviews exactly as it does today.
Entries are validated as bare hostnames before being interpolated into
--allowedTools; anything carrying quotes, spaces, schemes, paths, or globs
fails the step rather than widening the allowlist.

Approval moves from per-PR to per-repo: the domain list lives in the caller
workflow, so changing it is a PR that CODEOWNERS can route to the connector
approvers. No second run, no per-PR pause.

The mixin now degrades gracefully. Since most repos will have no domains
configured, an unavailable or denied fetch is an E4 outcome: C9, C11, and
B10 still apply from the diff, and the summary states that vendor docs were
not verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace doc_fetch_domains with doc_fetch. Domain scoping added a parsing
loop, a hostname regex, and an injection guard, all of which existed only
because hostnames were interpolated into --allowedTools. With a plain flag
nothing arbitrary is interpolated, so the whole block collapses to a
five-line if and the validation is no longer needed.

The per-repo opt-in is what was actually doing the gating: off by default,
so a repo that has not asked for doc fetching gets exactly the allowlist it
had before. The profile check goes away too -- general-pr-review.yaml
declares workflow_call with no inputs, so a general caller cannot pass the
flag in the first place.

Trade-off: an opted-in repo can now fetch any URL its prompt rules allow,
rather than a named set of hosts. The domain restriction also depended on
unmatched tool calls being denied in headless mode, which is unverified, so
this gives up less than it appears to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
doc_fetch_domains is a plain string again. Empty grants no WebFetch; any
non-empty value expands to one WebFetch(domain:...) grant per comma-separated
entry. The hostname regex, the injection guard, the whitespace stripping, and
the empty-element skip are gone.

The value is set in the caller's own workflow file, which is already a
code-reviewed artifact, so validating it here was guarding against a repo
maintainer attacking their own review job. A malformed entry now just
produces a grant that never matches, which surfaces as E4 rather than as a
failed step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Bencheng21 Bencheng21 closed this Sep 15, 2026
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