Skip to content

feat(resources): linkAuth validator wiring and per-host outcome codes (#113 slice 2)#125

Open
ejdutton wants to merge 2 commits into
mainfrom
feat/113-linkauth-validator-wiring
Open

feat(resources): linkAuth validator wiring and per-host outcome codes (#113 slice 2)#125
ejdutton wants to merge 2 commits into
mainfrom
feat/113-linkauth-validator-wiring

Conversation

@ejdutton

Copy link
Copy Markdown
Collaborator

Summary

Wires the slice-1 pure engine into ExternalLinkValidator and adds the five LINK_AUTH_* outcome codes. End-to-end: when an adopter sets resources.linkAuth in vibe-agent-toolkit.config.yaml, vat resources validate bypasses markdown-link-check for any URL whose host is claimed by a provider, issues an authenticated fetch() against the rewritten URL, and classifies the response per design §7 into one of:

Code Trigger Default severity
LINK_AUTH_DEAD 404/410 from honest-404 hosts (notFoundMeaning: dead) error
LINK_AUTH_DEAD_OR_UNAUTHORIZED 404 from ambiguous hosts (e.g. GitHub) warning
LINK_AUTH_FORBIDDEN 403 warning
LINK_AUTH_UNAUTHORIZED 401 warning
LINK_AUTH_UNVERIFIED no token resolved warning

LINK_AUTH_DEAD = error is the only error-severity code; design issue #113 §7 establishes that an authenticated 404 against an honest-404 host (e.g. SharePoint) is high-confidence link rot, satisfying the rule-design corpus-evidence bar.

What's in the PR

  • packages/resources/src/link-auth-fetch.tsfetchAuthenticated() with bounded redirect loop, cross-origin Authorization stripping (§8) that stays sticky across the rest of the chain (defeats token-laundering), 429/Retry-After honoring (§5.2) parsing delta-seconds and HTTP-date forms with a 60s DoS cap and a 250ms good-neighbor floor.
  • packages/resources/src/link-auth-classify.ts — pure (status, providerCheck) → outcome+code per §7.
  • packages/resources/src/link-auth-config-build.ts — adopter config → engine config bridge with post-expansion validation against InlineProviderSchema (catches typo'd macro overrides that pass through MacroProviderSchema's .passthrough()); compile-time _KeysAgree type assertion locks the schema's top-level field set to the engine's Provider interface.
  • ExternalLinkValidator wiring + a second ExternalLinkCache instance for auth-branch results, keyed by the rewritten URL (the original blob/ URL 404s — caching that would poison results) and scoped to a per-OS-user subdirectory cacheDir/auth-${sanitizedOsUser}/ (§6.3). One-shot console.warn when the OS-user fallback chain lands at 'default' (surfaces the cross-user-leak risk).
  • Cache entries gain version: 1; entries written under any other version produce a miss (forward-compat for slice 3).
  • Doc-anchor coverage iterator at packages/agent-schema/test/docs/validation-codes.test.ts asserts every CODE_REGISTRY entry has a matching ### \CODE`heading indocs/validation-codes.md` — 126 assertions covering all 63 codes.
  • 5 new doc sections under "Authenticated External Link Codes" in docs/validation-codes.md.

196 new tests across link-auth-classify (13), link-auth-fetch (19), external-link-validator-auth (25), link-auth-config-build (10), validation-codes (+126 doc-iterator, +2 LINK_AUTH_* registry), and external-link-cache (+1 version-gate). Security-load-bearing tests pin the cross-origin Authorization strip, path-traversal sanitizer (table-driven over 9 pathological osUser inputs), post-expansion validation, unverified-no-cache invariant, cache-hit re-classification, and Object.hasOwn prototype-pollution defense on the { use } discriminator.

Adopter-visible breaking changes

  • LinkAuthConfig (the Zod-inferred adopter type) renames to LinkAuthProjectConfig — eliminates IDE auto-import ambiguity with the engine's LinkAuthConfig from @vibe-agent-toolkit/utils. Engine type unchanged. The LinkAuthConfigSchema name is unchanged.
  • Cache layout adds auth-${osUser}/ subdirectory under cacheDir; pre-existing external-links.json entries lacking the new version field trigger a one-time re-fetch on first run after upgrade.

Fixed

  • ExternalLinkValidator.clearCache() and getCacheStats() now operate on both caches. Previously they touched only the anonymous cache, so an adopter rotating a token would see stale 401/403 entries until the auth cache TTL expired.

Deferred

  • Slice 3: content-fetch primitive + content cache (30-min TTL, force-refresh bypass).
  • Slice 4: cross-platform .cmd-shim system test for the token dispatcher, VAT_LINKAUTH_ALLOW_COMMAND=0 opt-out, contributor docs.

Test plan

  • CI green on Ubuntu + Windows matrix
  • Spot-check the new auth-cache directory creation by running vat resources validate against a project with a resources.linkAuth.providers: [{ use: github }] config
  • Verify LINK_AUTH_DEAD_OR_UNAUTHORIZED fires (warning) for a private GitHub URL the test identity can't see
  • Verify LINK_AUTH_UNAUTHORIZED fires when GITHUB_TOKEN is intentionally stale
  • Confirm anonymous external-URL checking still works against URLs no provider claims

🤖 Generated with Claude Code

…#113 slice 2)

Wires the slice-1 pure engine into ExternalLinkValidator and adds the
five LINK_AUTH_* outcome codes. End-to-end: when an adopter sets
resources.linkAuth in vibe-agent-toolkit.config.yaml, vat resources
validate bypasses markdown-link-check for any URL whose host is claimed
by a provider, issues an authenticated fetch against the rewritten URL,
and classifies the response per design §7 into one of:

  LINK_AUTH_DEAD                    (404/410 from honest-404 hosts)
  LINK_AUTH_DEAD_OR_UNAUTHORIZED    (404 from ambiguous hosts like GitHub)
  LINK_AUTH_FORBIDDEN               (403)
  LINK_AUTH_UNAUTHORIZED            (401)
  LINK_AUTH_UNVERIFIED              (no token resolved)

New surface: fetchAuthenticated() with cross-origin Authorization
stripping (§8, sticky across chains) and 429/Retry-After honoring
(§5.2, 60s DoS cap + 250ms good-neighbor floor); pure classifier per
§7; project-config → engine bridge with post-expansion validation
against InlineProviderSchema (catches typo'd macro overrides); auth
cache keyed by rewritten URL and scoped to a per-OS-user subdirectory
(§6.3) with an explicit version field forward-compat for slice 3.

196 new tests across 6 files. Doc-anchor coverage iterator
(packages/agent-schema/test/docs/validation-codes.test.ts) iterates
CODE_REGISTRY and asserts each code has a matching docs section.

Adopter-visible breaking changes: the LinkAuthConfig type exported
from @vibe-agent-toolkit/resources renames to LinkAuthProjectConfig
(resolves auto-import ambiguity with the engine type of the same
name); the external-link cache layout adds an auth-${osUser}/
subdirectory and a version field that invalidates pre-existing
entries on first read.

Fixed: ExternalLinkValidator.clearCache()/getCacheStats() now operate
on both anonymous and authenticated caches (previously the auth cache
survived a manual clear, surfacing stale 401/403 entries after token
rotation).

Deferred to later slices: content-fetch primitive and content cache
(slice 3); cross-platform .cmd-shim system test, VAT_LINKAUTH_ALLOW_COMMAND=0
opt-out, and contributor docs (slice 4).

See CHANGELOG.md [Unreleased] for full details.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.97050% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.95%. Comparing base (5d6f2ff) to head (e688e64).

Files with missing lines Patch % Lines
packages/resources/src/external-link-validator.ts 84.37% 20 Missing ⚠️
packages/resources/src/resource-registry.ts 12.50% 7 Missing ⚠️
packages/resources/src/link-auth-fetch.ts 94.44% 5 Missing ⚠️
packages/resources/src/link-auth-config-build.ts 95.12% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #125      +/-   ##
==========================================
+ Coverage   81.78%   81.95%   +0.16%     
==========================================
  Files         223      226       +3     
  Lines       17135    17458     +323     
  Branches     3326     3412      +86     
==========================================
+ Hits        14014    14307     +293     
- Misses       3121     3151      +30     
Files with missing lines Coverage Δ
packages/agent-schema/src/validation-codes.ts 100.00% <100.00%> (ø)
packages/resources/src/external-link-cache.ts 94.59% <100.00%> (+2.28%) ⬆️
packages/resources/src/link-auth-classify.ts 100.00% <100.00%> (ø)
packages/utils/src/link-auth/resolve-token.ts 82.92% <100.00%> (ø)
packages/utils/src/link-auth/resolve.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-config-build.ts 95.12% <95.12%> (ø)
packages/resources/src/link-auth-fetch.ts 94.44% <94.44%> (ø)
packages/resources/src/resource-registry.ts 59.39% <12.50%> (-0.44%) ⬇️
packages/resources/src/external-link-validator.ts 90.42% <84.37%> (-5.91%) ⬇️
Files with missing lines Coverage Δ
packages/agent-schema/src/validation-codes.ts 100.00% <100.00%> (ø)
packages/resources/src/external-link-cache.ts 94.59% <100.00%> (+2.28%) ⬆️
packages/resources/src/link-auth-classify.ts 100.00% <100.00%> (ø)
packages/utils/src/link-auth/resolve-token.ts 82.92% <100.00%> (ø)
packages/utils/src/link-auth/resolve.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-config-build.ts 95.12% <95.12%> (ø)
packages/resources/src/link-auth-fetch.ts 94.44% <94.44%> (ø)
packages/resources/src/resource-registry.ts 59.39% <12.50%> (-0.44%) ⬇️
packages/resources/src/external-link-validator.ts 90.42% <84.37%> (-5.91%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jdutton

jdutton commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Code audit — 3 findings to address before merge

Nice work on this slice — well-structured and well-tested, CI green. Three items from review:

1. 🔴 Postel's Law inverted: adopter config schema is .strict()

packages/resources/src/schemas/link-auth.ts:104 (InlineProviderSchema) and :141 (LinkAuthConfigSchema)

Both schemas parse the resources.linkAuth section of the adopter's vibe-agent-toolkit.config.yaml — external data we read but don't generate — yet they end in .strict(). Per the repo CLAUDE.md Postel's Law rule, "reading external data (files we don't control) → .passthrough(); writing data our agents generate → .strict()." These are on the wrong side.

The inline comment actually misreads the rule to justify strict:

// Strict ... per Postel's law ("writing our output → conservative")

This schema reads adopter input, it doesn't write our output. Impact is functional: config-parser.ts:47 throws on a strict-parse failure, so an adopter who adds a forward-compatible or slightly-misspelled field under linkAuth gets vat resources validate crashing instead of degrading.

Fix: .passthrough() on the config-facing schemas (matching how the rest of project-config treats adopter input), and correct the comment. If we want typo-catching DX, that belongs in a separate lint/warn pass, not a hard parse failure.

2. 🟠 Token resolution runs per-URL with no memoization

packages/resources/src/external-link-validator.ts:290packages/utils/src/link-auth/resolve-token.ts:85

resolveAuthenticatedUrl() is called inside validateLink(url), once per URL (urls.map(url => this.validateLink(url))), and resolveToken() has zero memoization. Treat all token resolvers as expensive (not just command sources — env/file/future sources should be presumed costly too): validating N links to the same host re-resolves the token N times, and a command source spawns a subprocess each time.

Fix: memoize resolved tokens per provider for the duration of a validate run.

3. 🟡 authCache get/set unguarded — IO error crashes the whole run

external-link-validator.ts:375 (authCache.get) and :416 (authCache.set)

external-link-cache.ts loadCache() swallows only ENOENT/SyntaxError and re-throws everything else (e.g. EACCES); saveCache() has no try/catch (ENOSPC / read-only dir throws). The two new call sites are unwrapped, so a permission/disk/corruption problem on the cache file aborts the entire vat resources validate instead of degrading to a live fetch.

Fix: wrap auth-cache access so IO failures fall through to a live fetch, or make ExternalLinkCache swallow non-ENOENT IO errors internally.


Verified NOT bugs (checked during review, no action needed): no-anonymous-fallback / LINK_AUTH_UNVERIFIED is intentional per design §6.3 and fetch exceptions are caught at :390–409; buildAuthResult status:'error' is only an internal "emit an issue?" flag (final severity comes from CODE_REGISTRY by code, so warning-severity codes still surface as warnings); cache version: 1 invalidation is an expected one-time re-fetch.

…nCommand, fail-soft cache IO

Addresses jdutton's three review findings on PR #125:

1. Postel's Law: the adopter-facing linkAuth schemas (InlineProviderSchema,
   LinkAuthConfigSchema, and the five nested object schemas) were `.strict()`
   but parse external input. Switched all seven to `.passthrough()` to match
   the repo CLAUDE.md rule for adopter configs — forward-compatible / typo'd
   fields now degrade rather than crash `vat resources validate`. Adjusted
   the file header comment that misread Postel's Law, and updated four
   schema tests to assert the new passthrough semantics. The compile-time
   _KeysAgree drift check moved from `keyof z.infer<...>` to
   `keyof Schema.shape` so passthrough's index signature doesn't defeat it.
   Post-expansion validation still catches missing required fields and
   wrong types on declared fields (e.g. invalid notFoundMeaning enum value);
   typo-catching DX belongs in a separate lint pass.

2. Token memoization: ExternalLinkValidator now wraps the linkAuth deps'
   runCommand with a Map<JSON-argv, RunResult>, so validating N URLs from
   the same host runs `gh auth token` (or any command-source token) at
   most once per validator instance. Treats *all* token sources as
   potentially expensive per the review. Engine's DEFAULT_RUN_COMMAND
   exported as `defaultRunCommand` so the wrapper calls through to a
   single source of truth (avoids a duplicate-implementation jscpd clone).
   Two new tests pin the memoization (single provider → 1 call across N
   URLs; distinct providers → separate calls).

3. Fail-soft cache IO: ExternalLinkCache.loadCache() previously rethrew
   anything other than ENOENT/SyntaxError; saveCache() had no try/catch.
   An EACCES/EROFS on the cache file aborted the whole validate run.
   Both paths now swallow IO errors — read returns empty cache, write
   no-ops while the in-memory cache stays authoritative for the rest of
   the run. Two POSIX-skipped tests using chmod simulate EACCES on read
   and write.

CHANGELOG updated to reflect the final post-review behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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