feat(resources): linkAuth content-fetch primitive and content cache (#113 slice 3)#136
Merged
Merged
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>
…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>
…tent-fetch-slice3
…113 slice 3) Implements §6.2 dual-mode headers, §6.3 content cache with 30-min TTL, and the public fetchAuthenticated primitive that wires the engine, transport, and cache together for authenticated binary content retrieval. Key changes: - rename link-auth-fetch.ts → link-auth-transport.ts; rename fetchAuthenticated → authTransport to free the name for the public primitive - engine (resolve.ts): add Provider.fetch?, dual-expand fetch.headers alongside auth.headers against the same token/capture context (§6.2) - engine: add LinkAuthConfig.cache? so cache.ttlMinutes rides the same config object without a second source of truth - content-cache.ts: per-entry two-file layout (<sha256>.json + .bin), write .bin before .json (commit-marker discipline), pickMetadata() whitelist against token persistence (§8), fail-soft IO, schema versioning - link-auth-deps-memo.ts: extract wrapLinkAuthDepsWithMemo from validator so both slice 2 and slice 3 share the same per-argv Map memoization - link-auth-content-fetch.ts: public fetchAuthenticated(); Object.hasOwn discrimination, fetch.headers merged over auth.headers, unverified/unsupported never touch cache (§6.3), cache write-through after full body read - cross-slice integration test: one adopter config + one shared memo feeds both ExternalLinkValidator and fetchAuthenticated; asserts wire-level Accept headers, token memo runs exactly once across 3 calls Bumps version to v0.1.39-rc.7. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #136 +/- ##
==========================================
+ Coverage 81.94% 82.31% +0.36%
==========================================
Files 227 233 +6
Lines 17632 18097 +465
Branches 3468 3600 +132
==========================================
+ Hits 14449 14897 +448
- Misses 3183 3200 +17
🚀 New features to boost your workflow:
|
…ats, signal, defaultSleep
Covers Codecov-flagged patch lines (ported from slice 2):
- external-link-validator: catch block in validateAuthenticatedLink (Error,
null/falsy, plain object, empty {}) — exercises all safeSerializeError branches
- external-link-validator: clearCache() and getCacheStats() happy paths
- external-link-validator: resolveOsUser() default path (no osUser option)
- link-auth-transport: signal pass-through to fetchImpl RequestInit
- link-auth-transport: defaultSleep body via fake timers (no sleep injection)
- link-auth-config-build: TypeError when use value is not a string
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
jdutton
added a commit
that referenced
this pull request
Jun 26, 2026
Integrate #136 (linkAuth content-fetch primitive + content cache) into the skill-test branch ahead of PR #135. Conflict resolution: - package.json (×24): adopted main's version 0.1.39-rc.7 across all packages (the branch's rc.8–rc.13 were throwaway adopter-test RCs cut from this branch; no new version bump for this PR). - CHANGELOG.md: kept both sets of [Unreleased] entries — main's canonical set (incl. linkAuth slices) plus the branch's skill-test-unique entries (vat skill test run/configure, files: glob+integrity, NON_PORTABLE_ASSET_REFERENCE, NON_PORTABLE_COMMAND, skill-authoring guidance, resolveSkillSource resolver). - bun.lock: regenerated via bun install — branch's glob dep added, main's yaml dep retained. bun run validate passes (3 phases, 14 steps, all green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gx1K1J2YJMfcisAqyCzaf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Slice 3 of issue #113 — the authenticated content-fetch primitive and per-entry disk cache that sit on top of the slice-1 engine and slice-2 validator wiring.
fetchAuthenticated(url, config, options)— public primitive that wires the engine, transport, and cache. Returns{ bytes, metadata, cached }on success;{ outcome: 'unverified' }or{ outcome: 'unsupported' }on short-circuit (neither ever touches the cache, per §6.3).ContentCache— per-entry two-file layout (<sha256(url)>.json+.bin). Writes.binbefore.json(commit-marker discipline so a crash mid-write can't serve stale metadata against new bytes).pickMetadata()whitelist prevents any auth header from persisting to disk (§8). Fail-soft IO: ENOENT/EACCES/corrupted JSON all degrade to a cache miss. Schema versioning for forward-compat.auth.headers(health-check Accept) andfetch.headers(content-retrieval Accept) against the same token/capture context. Primitive merges them:{ ...auth.headers, ...fetch.headers }soAcceptis overridden whileAuthorizationsurvives.wrapLinkAuthDepsWithMemo— extracted fromExternalLinkValidatorto a shared module so both the validator and the primitive can share a single per-argv Map memo, amortizinggh auth tokensubprocess cost across all calls in a run.ExternalLinkValidator(slice 2) andfetchAuthenticated(slice 3) coherently: same rewritten URL, correct per-slice Accept headers, token resolved exactly once across 3 cross-slice calls.Depends on: PR #125 (slice 2 — the validator wiring). This branch builds on top of that branch's commits.
Test plan
bun run validate(includes unit, integration, system)content-cache.test.ts— round-trip, TTL boundary (at-exactly-TTL valid, TTL+1ms miss), version-gate (missing/wrong version → miss), fail-soft IO (EACCES → miss, corrupted JSON → miss),pickMetadata()whitelist (smuggled Authorization not written to disk)link-auth-content-fetch.test.ts— short-circuit outcomes (unsupported, unverified), fetch headers override (§6.2), cache integration (write-through, cache hit, forceRefresh, no-cache-unsupported, no-cache-unverified per §6.3), token never persisted (recursive readdir asserting token string absent), AbortSignal pass-throughlinkauth-cross-slice.integration.test.ts— one config, one memo, both slices; 2 HTTP requests total (validator + fetch, cache hit = 0 requests); wire-level Accept headers match per-slice headers; runCommand called exactly once🤖 Generated with Claude Code