feat(resources): linkAuth validator wiring and per-host outcome codes (#113 slice 2)#125
feat(resources): linkAuth validator wiring and per-host outcome codes (#113 slice 2)#125ejdutton wants to merge 2 commits into
Conversation
…#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 Report❌ Patch coverage is Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
Code audit — 3 findings to address before mergeNice work on this slice — well-structured and well-tested, CI green. Three items from review: 1. 🔴 Postel's Law inverted: adopter config schema is
|
…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>
|



Summary
Wires the slice-1 pure engine into
ExternalLinkValidatorand adds the fiveLINK_AUTH_*outcome codes. End-to-end: when an adopter setsresources.linkAuthinvibe-agent-toolkit.config.yaml,vat resources validatebypassesmarkdown-link-checkfor any URL whose host is claimed by a provider, issues an authenticatedfetch()against the rewritten URL, and classifies the response per design §7 into one of:LINK_AUTH_DEADnotFoundMeaning: dead)LINK_AUTH_DEAD_OR_UNAUTHORIZEDLINK_AUTH_FORBIDDENLINK_AUTH_UNAUTHORIZEDLINK_AUTH_UNVERIFIEDLINK_AUTH_DEAD = erroris 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.ts—fetchAuthenticated()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+codeper §7.packages/resources/src/link-auth-config-build.ts— adopter config → engine config bridge with post-expansion validation againstInlineProviderSchema(catches typo'd macro overrides that pass throughMacroProviderSchema's.passthrough()); compile-time_KeysAgreetype assertion locks the schema's top-level field set to the engine'sProviderinterface.ExternalLinkValidatorwiring + a secondExternalLinkCacheinstance for auth-branch results, keyed by the rewritten URL (the originalblob/URL 404s — caching that would poison results) and scoped to a per-OS-user subdirectorycacheDir/auth-${sanitizedOsUser}/(§6.3). One-shotconsole.warnwhen the OS-user fallback chain lands at'default'(surfaces the cross-user-leak risk).version: 1; entries written under any other version produce a miss (forward-compat for slice 3).packages/agent-schema/test/docs/validation-codes.test.tsasserts everyCODE_REGISTRYentry has a matching### \CODE`heading indocs/validation-codes.md` — 126 assertions covering all 63 codes.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), andexternal-link-cache(+1 version-gate). Security-load-bearing tests pin the cross-origin Authorization strip, path-traversal sanitizer (table-driven over 9 pathologicalosUserinputs), post-expansion validation, unverified-no-cache invariant, cache-hit re-classification, andObject.hasOwnprototype-pollution defense on the{ use }discriminator.Adopter-visible breaking changes
LinkAuthConfig(the Zod-inferred adopter type) renames toLinkAuthProjectConfig— eliminates IDE auto-import ambiguity with the engine'sLinkAuthConfigfrom@vibe-agent-toolkit/utils. Engine type unchanged. TheLinkAuthConfigSchemaname is unchanged.auth-${osUser}/subdirectory undercacheDir; pre-existingexternal-links.jsonentries lacking the newversionfield trigger a one-time re-fetch on first run after upgrade.Fixed
ExternalLinkValidator.clearCache()andgetCacheStats()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
.cmd-shim system test for the token dispatcher,VAT_LINKAUTH_ALLOW_COMMAND=0opt-out, contributor docs.Test plan
vat resources validateagainst a project with aresources.linkAuth.providers: [{ use: github }]configLINK_AUTH_DEAD_OR_UNAUTHORIZEDfires (warning) for a private GitHub URL the test identity can't seeLINK_AUTH_UNAUTHORIZEDfires whenGITHUB_TOKENis intentionally stale🤖 Generated with Claude Code