With capacity=2 in the runner, two builds can run in parallel and share
the same buildkit cache mount (sharing=shared is the default). dotnet
restore writes NuGet temp files (*.ar5 etc.) that the OTHER build can
race-remove mid-extraction, yielding 'Could not find file' errors like:
error : Could not find file '/root/.nuget/packages/microsoft.identitymodel.abstractions/8.0.1/3wu4hkqc.ar5'
sharing=locked serializes cache access (one build at a time can touch
the mount). Still benefits from across-build caching, just no concurrency
on the mount itself.
JwtBearer 10.0.7 transitively brings in Microsoft.IdentityModel.JsonWebTokens
9.x, which calls Base64UrlEncoder.Decode(ReadOnlySpan<char>, Span<byte>) —
an overload only present in Microsoft.IdentityModel.Tokens 9.x. The
explicit pin to 8.9.0 forced NuGet to downgrade Tokens to 8.9, producing
a runtime MissingMethodException on every JWT validation. Symptom: every
incoming request gets 401, never visible at default log level.
Drop the explicit pin so JwtBearer's transitive dep wins. The code only
uses SymmetricSecurityKey etc., which are stable across versions.
Same fix as nas-auth: mode=max takes ~4min and times out on the NAS,
which marks the build as failure and skips deploy. mode=min is fast
and ignore-error keeps the build from failing on cache export.
Mirror the cache infra added to nas-auth (commit a1ecba3):
- Dockerfile: `RUN --mount=type=cache,target=/root/.nuget/packages`
for restore + publish, with syntax 1.6 directive.
- workflow: cache-from/cache-to registry cache mode=max, so buildkit
layer cache survives across CI runs (fresh buildkit per run otherwise).
First run after this still pays full cost; each subsequent run should
drop ~50% off dotnet restore.
Claude.ai's MCP OAuth client only sends the RFC 8707 `resource` param
when it has Protected Resource Metadata (PRM) to learn the resource
identifier from. Without it, nas-auth's /authorize rejected the request
with 'resource 必填'.
Adds /.well-known/oauth-protected-resource (RFC 9728) that returns the
resource URL + AS URL + supported scopes. ResourceUrl is configurable
via Mcp__OAuthDiscovery__ResourceUrl, falling back to request authority
when unset (works in local dev).
Deploy job now cd's into /volume1/docker/compose/obsidian-mcp (exposed
to runner via the new gitea-runner mount) instead of cloning nas-infra
to a temp dir. The clone approach couldn't deploy because .env.shared
is gitignored and only exists on NAS.