fix(licensing): unblock CI + v2 aidn2 hardening (P1) — capability gating, CRL, scope/machine binding - #1891
Conversation
- docs/licensing/v2-licensing-design.md: full v2 design for review — hybrid offline/online (short exp + attestation + grace), CRL deny-list revocation, server-side-only Ed25519 signing, scope/machine binding, capability-based paid-tier gating (persistence + encrypted-model IP + air-gapped/seats; GPU stays free), and the CI unblock (short-lived scoped offline aidn2 + online fallback + public-key embed in the build). - tools/license-issuer/aidn2_issuer.py: reference issuer that mints aidn2 Ed25519 tokens + emits the public JWK (production signing moves to a server-side edge function; private key never ships). No key material committed (generated artifacts go to a scratch dir). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- LicenseClaims: add jti (revocation target), caps (authoritative offline capability grants), mach (node-lock), scope (audience binding). - AsymmetricLicenseVerifier: after signature verify, enforce CRL revocation (kid/jti), machine binding (mach == local machine id hash), and scope binding (scope == AIDOTNET_LICENSE_SCOPE); carry signed caps into the Active result. - LicenseRevocationProvider (new): JWS-style signed CRL (embedded + online-fetched), Ed25519-verified against the embedded public key, expiry-checked, fail-open. - csproj: embed optional BuildKey/LicenseRevocation.json as AiDotNet.LicenseRevocation. src builds clean. Client-side P1 per docs/licensing/v2-licensing-design.md §13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t hardening - LicenseCapabilities: canonical capability ids (model:save/load, tensors:save/load, model:encrypt, offline) shared by server caps[], token caps claim, and guards. - ModelPersistenceGuard: EnforceCore now takes a required capability. An Active license lifts the cap for an op only when it grants that capability; a token with NO caps is legacy grant-all (migration bridge); an Active license lacking the cap falls through to trial (community keeps free-trial saves). Save/serialize require model:save (paid gate); load/deserialize stay lenient (any Active). - ModuleInitializer: install the synthetic OFFLINE test license unless the env key is itself offline-verifiable (aidn2/aidn.). A server-only AIDN-* (community) env key — which forces the whole suite online and won't lift the save cap under capability gating — is replaced with the offline test license so the persistence-heavy suite is deterministically licensed. The real online/community-key path stays in LicenseE2ETests. Validated: with a simulated AIDN-PROD-COMMUNITY-* env key (the exact CI scenario), the previously-failing LicensingIntegrationTests pass 7/7 (incl. the encrypt round-trips); no new regressions in the license suite (10 unrelated pre-existing failures: 9 causal- discovery name-coincidence, 1 stale stripe-format test from #1807). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-{TIER}-{32hex}
The webhook emits server-validated AIDN-* keys (WEBHOOK_PREFIX=AIDN-PROD), not the
old aidn.{id}.{sig} form the test asserted — which #1807's 32-byte-signature rule
now rejects. Assert the actual format the webhook produces (pre-existing red on master).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (25)
WalkthroughThe PR defines the aidn2 licensing design and rollout, adds signed claim and CRL verification, enforces capabilities for persistence, introduces Supabase issuance and revocation services, adds Ed25519 tooling, and injects verification resources into CI builds. Changesaidn2 licensing
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant issue-license
participant Supabase
participant aidn2Signer
Client->>issue-license: Submit license key and machine hash
issue-license->>Supabase: Validate license through RPC
Supabase-->>issue-license: Return tier, license id, and capabilities
issue-license->>aidn2Signer: Sign aidn2 claims
aidn2Signer-->>Client: Return offline token
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Commit messages auto-fixedOne or more commit messages did not follow Conventional Commits, so they were rewritten to comply (subject case, header length ≤ 100, valid type). Each commit and its diff were preserved — no squashing. The branch was force-pushed with the corrected messages. If you have local work on this branch, run |
83bbb61 to
cf81a74
Compare
Adds a step to the Build job that writes src/BuildKey/LicensePublicKey.json and LicenseRevocation.json from repo vars/secrets (AIDOTNET_LICENSE_PUBLIC_KEY_JSON / AIDOTNET_LICENSE_REVOCATION_JSON) so the CI-built AiDotNet.dll can verify offline aidn2 tokens and honour the CRL. Both are PUBLIC/signed and safe to expose. No-op when unset (embed is Condition="Exists(...)"). Forward-looking P1 wiring for the CI aidn2 key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
…aps flow-through New AsymmetricLicenseBindingTests (10, all green): caps carried into the result; scope binding rejected when host scope unset/differs and accepted when it matches (no-scope tokens unaffected); machine binding rejected on a different machine and accepted on the matching hash; signed-CRL revocation rejects a revoked jti, passes a non-revoked one, and ignores an expired (stale) CRL. Extends LicenseTestSupport with jti/caps/mach/scope params on SignedKeyV2 and a SignedCrlV2 helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…evocations, revocations) Adds the server side of the v2 aidn2 offline-licensing model: - _shared/aidn2.ts: Ed25519 aidn2 token + CRL signer (private key stays a server secret; caps map kept byte-identical to the validate_license_key RPC so online and offline validation grant the same capabilities). - issue-license: online->offline bridge. Re-validates through validate_license_key (seat-checked, authoritative tier/license_id), then mints a short-exp, machine-bound aidn2 token the SDK caches + verifies offline. - get-revocations: serves the signed CRL the SDK enforces offline (jti = license id). - revocations table + revoke_license(uuid,text) helper that revokes on BOTH the online (status=revoked) and offline (CRL) paths atomically. Additive; applied to prod (yfkqwpgjahoamlgckjib) and locked to service_role (security-advisor clean). - tools/license-issuer/keygen.py: one-shot keypair bootstrap emitting the exact Supabase secret + CI variable + JWK (private key never leaves the operator's box). - docs/licensing/v2-deploy-runbook.md: ordered, reversible rollout. Edge functions 503 until AIDOTNET_LICENSE_SIGNING_KEY_PKCS8 is set, so deploying them + embedding the public key + the SDK auto-fetch glue are the runbook's remaining human-gated steps. validate_license_key already returns license_id/caps, so the live hot-path function is untouched and paid customers are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Helpers/ModelPersistenceGuard.cs (1)
262-291: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winBLOCKING: Apply capability gating to
ValidationPendinglicenses too.The new capability contract only covers
Active. A server key with a valid timestamp attestation entersValidationPendingduring an outage and returns successfully regardless ofrequiredCapability, bypassingmodel:save. Persist attested capabilities, or fail closed for capability-gated operations.Safe fail-closed fix
case LicenseKeyStatus.ValidationPending: - if (OnlineValidationAttestation.HasValidWithin( + if (string.IsNullOrEmpty(requiredCapability) && + OnlineValidationAttestation.HasValidWithin( licenseKey, OnlineValidationAttestation.AttestationValidity)) { LicensingTelemetryCollector.Instance.RecordLicensedOperation("persistence"); return; }As per path instructions, half-implemented patterns where some paths omit enforcement are blocking.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Helpers/ModelPersistenceGuard.cs` around lines 262 - 291, Update EnforceCore and the ValidationPending handling in ValidateLicenseKey so a ValidationPending license cannot bypass requiredCapability checks. Preserve and use attested capabilities when available; otherwise fail closed for capability-gated operations such as model:save, while retaining only the explicitly permitted behavior for ungated operations.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/sonarcloud.yml:
- Around line 268-275: Update the LicensePublicKey.json and
LicenseRevocation.json injection blocks in the workflow to receive the GitHub
variable/secret values through environment variables, then have Bash read those
variables when checking presence and writing JSON. Do not interpolate the JSON
expressions directly inside shell commands; preserve the existing skip messages
and file-size reporting.
In `@docs/licensing/v2-deploy-runbook.md`:
- Around line 95-100: Remove the `drop table public.revocations` operation from
the Rollback section while retaining the endpoint/function rollback guidance.
State that revocation and audit data must be preserved, and direct any required
schema changes through a forward migration rather than destructive rollback.
In `@docs/licensing/v2-licensing-design.md`:
- Line 84: Update the revocation-list description to document the envelope
emitted by get-revocations: an outer object containing kid, payload, and sig,
with the signed payload using rkids and rjti fields. Remove the incompatible
inline revoked_kids/revoked_jti schema while preserving the signing and
verification details.
In `@src/Helpers/LicenseRevocationProvider.cs`:
- Around line 68-72: Update the synchronization logic in
LicenseRevocationProvider around _fetched so same-second additive CRL updates
are accepted instead of rejected by the candidate.Iat <= _fetched.Iat check.
Preserve rejection of older replayed CRLs, and either track a monotonic sequence
for ordering or merge the deny-lists when timestamps are equal so newly revoked
tokens remain blocked.
- Around line 166-173: Update the CRL parsing and installation flow around
TryInstallFetched to validate positive exp values numerically, reject
out-of-range timestamps without calling FromUnixTimeSeconds, and preserve the
expiry on the constructed Crl instead of discarding it. Extend Crl and update
Effective() to ignore cached CRLs whose positive expiry is before
DateTimeOffset.UtcNow, while retaining exp == 0 as non-expiring.
In `@tests/AiDotNet.Tests/ModuleInitializer.cs`:
- Around line 79-92: Update the _envKeyOfflineUsable calculation in
ModuleInitializer to perform actual offline validation of _envLicenseKey rather
than relying on IsAsymmetricKeyFormat or IsSignedKeyFormat. Require validation
to return an Active result with model:save capability, or accept legacy keys
with empty capabilities; otherwise retain the synthetic offline-license setup
for missing, invalid, expired, mismatched, or capability-limited keys.
In `@tools/license-issuer/aidn2_issuer.py`:
- Around line 39-52: Update build_claims and its callers to always emit a unique
jti, constrained caps, and CI-bound scope claims, preventing absent fields from
triggering legacy grant-all or unbound behavior. Change the issuer’s token
lifetime default and validation to enforce a maximum of 30 days, including the
argument/configuration handling referenced by the other affected locations.
- Around line 96-103: Update the private-key creation flow in the block guarded
by args.private_key_pem so private_key.pem is created with owner-only
permissions explicitly, rather than relying on the process umask; preserve the
existing PEM serialization and writing behavior.
In `@tools/license-issuer/keygen.py`:
- Around line 61-63: Update keygen.py’s JWK output flow to support append/merge
mode: load existing keys when requested, preserve them, and reject duplicate kid
values before writing the bundle. In docs/licensing/v2-licensing-design.md lines
105-108, identify the merged keys[] artifact as the release input. In
docs/licensing/v2-deploy-runbook.md lines 62-67, instruct operators to merge
both generated public keys before embedding the bundle or setting the CI
variable.
- Around line 53-63: Ensure the CI token uses the key generated by keygen rather
than the issuer’s fresh default: in tools/license-issuer/keygen.py lines 53-63,
emit a usable private-key handoff artifact or command; in
tools/license-issuer/aidn2_issuer.py lines 69-76, accept keygen’s base64 DER
PKCS#8 output or standardize both tools on PEM; and in
docs/licensing/v2-deploy-runbook.md lines 78-87, pass the existing CI private
key explicitly to aidn2_issuer.py.
In `@website/supabase/functions/_shared/aidn2.ts`:
- Line 61: Update the signing-key identifier initialization near kid and its
duplicate occurrence to require AIDOTNET_LICENSE_KID explicitly instead of
falling back to "prod-2026a"; fail immediately with a clear configuration error
when the environment variable is missing or empty, ensuring generated tokens and
CRLs use only the configured key identifier.
- Around line 60-68: Update signAidn2Token to validate c.expDays before
constructing claims or signing: reject NaN, non-positive, non-integer, and
excessive values, using the applicable maximum lifetime for tokens and CRLs.
Ensure invalid lifetimes fail closed with an error, while valid values continue
to calculate exp from now and preserve the existing signing flow.
In `@website/supabase/functions/issue-license/index.ts`:
- Around line 113-118: Update the claims construction in issue-license to stop
assigning body.license_key to sub; use an opaque, non-reusable identifier for
the subject instead, while preserving licenseId as the revocation target in jti
and leaving the tier, seats, and caps claims unchanged.
- Around line 54-65: Update the request parsing and validation in the
issue-license handler’s try block to perform runtime checks instead of relying
on the erased IssueRequest type: require a non-null, non-array object with
string license_key and machine_id_hash values that are non-empty after trimming.
Return the existing 400 missing_fields response for invalid shapes or values,
and handle malformed JSON as a client error rather than allowing it to become a
500.
- Around line 1-2: Update the Supabase client import version in
website/supabase/functions/issue-license/index.ts and
website/supabase/functions/get-revocations/index.ts from 2.39.3 to the website’s
aligned 2.98.0 version, preserving the existing import structure.
In `@website/supabase/migrations/20260718000000_revocations.sql`:
- Around line 14-19: Change the foreign-key behavior on license_key_id in the
revocation table definition so deleting a license preserves the revocation row
and its jti while clearing only the optional license relationship. Replace the
cascade-delete action with the appropriate nullifying behavior, ensuring
license_key_id remains nullable and kid-only revocations continue to work.
- Around line 59-66: Make the offline revocation insert concurrency-safe by
adding a partial unique index on public.revocations for the license-key/JTI
combination used when jti equals the license key ID, then update the insert in
the offline path to use ON CONFLICT DO NOTHING. Remove the NOT EXISTS check once
the unique constraint handles idempotency.
---
Outside diff comments:
In `@src/Helpers/ModelPersistenceGuard.cs`:
- Around line 262-291: Update EnforceCore and the ValidationPending handling in
ValidateLicenseKey so a ValidationPending license cannot bypass
requiredCapability checks. Preserve and use attested capabilities when
available; otherwise fail closed for capability-gated operations such as
model:save, while retaining only the explicitly permitted behavior for ungated
operations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6aa5cbd0-9d5e-4d81-a94c-2dd5b2b9a9ba
📒 Files selected for processing (20)
.github/workflows/sonarcloud.ymldocs/licensing/v2-deploy-runbook.mddocs/licensing/v2-licensing-design.mdsrc/AiDotNet.csprojsrc/Helpers/AsymmetricLicenseVerifier.cssrc/Helpers/LicenseCapabilities.cssrc/Helpers/LicenseClaims.cssrc/Helpers/LicenseRevocationProvider.cssrc/Helpers/ModelPersistenceGuard.cstests/AiDotNet.Tests/Helpers/AsymmetricLicenseBindingTests.cstests/AiDotNet.Tests/Helpers/LicenseServerEndpointTests.cstests/AiDotNet.Tests/Helpers/LicenseTestSupport.cstests/AiDotNet.Tests/ModuleInitializer.cstools/license-issuer/aidn2_issuer.pytools/license-issuer/keygen.pywebsite/supabase/functions/_shared/aidn2.tswebsite/supabase/functions/get-revocations/index.tswebsite/supabase/functions/issue-license/index.tswebsite/supabase/migrations/20260718000000_revocations.sqlwebsite/supabase/migrations/20260718000100_lock_revoke_license_execute.sql
…verification The public half (JWK OKP, kid prod-2026a) of the production signing keypair whose private key lives only as the Supabase AIDOTNET_LICENSE_SIGNING_KEY_PKCS8 function secret. Verified end-to-end: the live get-revocations CRL signature verifies against this key, confirming it matches the server's private key. Embedded as AiDotNet.LicensePublicKey so the SDK verifies aidn2 tokens offline. Public by nature (verify-only) — safe to commit; the CI inject step overwrites it with the same value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parate RSA key Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit |
… auto-refresh Makes an AIDN-* server key keep working offline WITH correct capabilities and while staying revocable, closing the online→offline gap the v2 server side (issue-license, get-revocations) enables: - OnlineLicenseServices: after a successful ONLINE validation, a throttled BACKGROUND task (off the hot path, never blocks the result) fetches + installs the signed CRL and mints + caches a short-lived, machine-bound aidn2 token via issue-license. When the server is later unreachable, Validate()/ValidateAsync() fall back to that cached token — verified locally by AsymmetricLicenseVerifier (signature + exp + machine-lock + CRL), so it only ever GRANTS when genuinely valid, never weakening security. Fail-open/best-effort throughout; sibling function URLs derived from the validate-license URL. - LicenseRevocationProvider: lazily loads the last-fetched CRL from disk so revocation is enforced even on a fully-offline start; a live refresh supersedes it (newer iat wins). - 8 OnlineLicenseServicesTests (URL derivation, machine-bound/expired/missing token, end-to-end server-unreachable→offline-token fallback, CRL round-trip). 18/18 green with the existing aidn2 binding tests; full project builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires a dedicated CI-only Ed25519 key so CI validates a REAL signed aidn2 token E2E
instead of the synthetic ModuleInitializer license:
- Embed the ci-2026a public key alongside prod-2026a in BuildKey/LicensePublicKey.json
(and the AIDOTNET_LICENSE_PUBLIC_KEY_JSON CI variable) — both keys, kid-routed.
- sonarcloud.yml + heavy-timeout-nightly.yml test jobs now use secrets.AIDOTNET_CI_LICENSE_KEY
+ AIDOTNET_LICENSE_SCOPE=ci. Kept in its OWN secret so the global AIDOTNET_LICENSE_KEY
(other branches) is untouched — zero cross-branch impact. The token is scope-fenced ("ci"),
not machine-bound (ephemeral runners), full-caps, 1y exp; its private half is stored as a
secret for rotation. Proven locally: LicensingIntegration + aidn2 binding tests 17/17 green
with this exact key+scope+embed. Runbook step 6 documents rotation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uer, edge fns, migration
Resolves the 14 unresolved review findings on this PR:
Client SDK (C#):
- LicenseRevocationProvider: MERGE same-second CRLs (union deny-lists, keep later exp) instead
of discarding them; bound exp before FromUnixTimeSeconds and ENFORCE it at access time in
Effective() so a CRL valid-at-load doesn't stay authoritative past its exp. + regression test.
- ModuleInitializer: keep the env key only when it VERIFIES offline as Active AND grants model:save
(or legacy empty caps) — matching ModelPersistenceGuard — instead of trusting its shape. A forged/
expired/scope-mismatched/community key now correctly falls back to the synthetic license.
Tooling (Python):
- aidn2_issuer.py: emit jti/caps/optional scope; default validity 30d (was 5y); create the private
key with 0o600 perms.
- keygen.py: MERGE into an existing JWK bundle (preserve keys[], reject duplicate kid) so per-key
runs produce one multi-kid bundle, not overwrites.
Server (edge functions, redeployed + smoke-tested):
- _shared/aidn2.ts: require an explicit AIDOTNET_LICENSE_KID (no defaulted kid); validate token/CRL
lifetimes ([1,30] integer days) before signing.
- issue-license: validate the request at runtime (invalid JSON -> 400 not 500; non-blank string
fields); use the OPAQUE license_id as sub, never the reusable license_key.
Database (migration + forward-fix applied to prod):
- revocations FK ON DELETE SET NULL (not cascade) so deleting a license keeps its deny-list entry;
partial unique index + ON CONFLICT DO NOTHING makes revoke_license idempotency concurrency-safe.
Docs: design §6 documents the actual { kid, payload, sig } CRL envelope; §7 the merged multi-kid bundle.
Validated: license suites 52/52 (synthetic), 17/17 (ci-token kept), 11/11 binding (incl new merge test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tation (design §9/P3) - release-please.yml: inject src/BuildKey/LicensePublicKey.json (+ LicenseRevocation.json) from the repo var/secret before the release build, so a key ROTATION ships in the released NuGet without a code change (a set var overrides the committed file; unset leaves the committed one). - rotate-ci-license.yml (new): monthly + manual workflow that re-signs the scope:ci ci-2026a aidn2 token with the CI-only private key (AIDOTNET_CI_LICENSE_SIGNING_KEY_PKCS8) and updates the AIDOTNET_CI_LICENSE_KEY secret before expiry (needs a CI_LICENSE_ROTATION_PAT with secrets:write, since GITHUB_TOKEN can't). The embedded public key is unchanged, so nothing else rebuilds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ap token (design §P2) Wires register-community-license into the aidn2 issuer (per design §7/§10/§13-P2), safely: after issuing the AIDN-* community key it also mints a short-exp (7d) aidn2 "bootstrap" token via the shared signer and returns it as `bootstrap_token`, so a new community user can LOAD models offline immediately (before the SDK's first online validation derives a machine-bound token). Safe precisely because it's community-only: the token carries community caps (`tensors:load` — NO save/persist), so a non-machine-bound token grants nothing beyond the offline read community already allows. Paid tiers deliberately get NO non-bound token — their SDK derives a machine-bound one via issue-license. Minting is best-effort (never throws / never fails an otherwise-successful registration); `sub`/`jti` are the opaque license id, never the reusable key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bootstrap, P4 status Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…00 on every new activation) Found by end-to-end testing against the live endpoints: validate-license returned 500 server_error for any first-time activation on a new machine. Root cause: 20260610000000_log_validations_to_api_usage rebuilt validate_license_key from the pre-fix version and reintroduced pg_advisory_xact_lock(v_license.id) — that overload takes bigint, not uuid, so it threw `pg_advisory_xact_lock(uuid) does not exist` on the new-activation path (existing activations short-circuit before the lock, hiding it). Live since 2026-06-10; every new CI runner / new customer machine hit it — a direct contributor to the "invalid license in CI" symptom this whole effort started from. Restores the 20260504 hashtextextended(uuid::text)->bigint fix at both lock sites, keeping the api_usage logging. Applied to prod (yfkqwpgjahoamlgckjib) via forward migration 20260718000300; also corrected the regressing 20260610 file so fresh environments get the right function directly. Verified E2E after the fix: validate-license (community -> tensors:load; professional -> save+load), issue-license -> a machine-bound aidn2 token that verifies against the embedded prod-2026a key with opaque sub=license_id, and revoke_license -> the jti appears in the signed get-revocations CRL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…icing success UI register-community-license now returns a short-exp, load-only aidn2 `bootstrap_token`; the pricing page's "Community license created!" panel now shows it in a collapsed, clearly-optional "Advanced" details block (most users just use the AIDN-* key above). Rendered only when the field is present, so older/paid flows are unaffected. Astro build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… offline token) Points #1891's test jobs at the global AIDOTNET_LICENSE_KEY secret (now a dedicated enterprise CI key: save-capable, ~unlimited activations under a persistent CI user) instead of the scope-fenced ci-2026a aidn2 token. One consistent, save-capable key across all branches + external pipelines — and unlike the ci token, it validates online regardless of which public key the SDK embeds (the ci token only worked in builds that embed ci-2026a, so it never fixed external consumers). - sonarcloud.yml + heavy-timeout-nightly.yml: use secrets.AIDOTNET_LICENSE_KEY (no scope). ModuleInitializer swaps this online AIDN-* key for the deterministic synthetic OFFLINE license, so the persistence-heavy suite stays licensed with no network round-trip (verified: 45/45 with the enterprise key set). - Removed rotate-ci-license.yml (rotated the now-unused ci token; it also needed a secrets:write PAT). - Reverted src/BuildKey/LicensePublicKey.json + the AIDOTNET_LICENSE_PUBLIC_KEY_JSON var to prod-only (prod-2026a stays — it signs real users' offline tokens via issue-license). Deleted the unused AIDOTNET_CI_LICENSE_KEY / signing-key secrets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… offline token) Points #1891's test jobs at the global AIDOTNET_LICENSE_KEY secret (now a dedicated enterprise CI key: save-capable, ~unlimited activations under a persistent CI user) instead of the scope-fenced ci-2026a aidn2 token. One consistent, save-capable key across all branches + external pipelines — and unlike the ci token, it validates online regardless of which public key the SDK embeds (the ci token only worked in builds that embed ci-2026a, so it never fixed external consumers). - sonarcloud.yml + heavy-timeout-nightly.yml: use secrets.AIDOTNET_LICENSE_KEY (no scope). ModuleInitializer swaps this online AIDN-* key for the deterministic synthetic OFFLINE license, so the persistence-heavy suite stays licensed with no network round-trip (verified: 45/45 with the enterprise key set). - Removed rotate-ci-license.yml (rotated the now-unused ci token; it also needed a secrets:write PAT). - Reverted src/BuildKey/LicensePublicKey.json + the AIDOTNET_LICENSE_PUBLIC_KEY_JSON var to prod-only (prod-2026a stays — it signs real users' offline tokens via issue-license). Deleted the unused AIDOTNET_CI_LICENSE_KEY / signing-key secrets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ess (no PAT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit messages auto-fixedOne or more commit messages did not follow Conventional Commits, so they were rewritten to comply (subject case, header length ≤ 100, valid type). Each commit and its diff were preserved — no squashing. The branch was force-pushed with the corrected messages. If you have local work on this branch, run |
06b0a23 to
e3ce888
Compare
Why
CI's license-gated persistence tests (
LicensingIntegrationTests.*,ExpressionTree_Serialize*,InferenceSession…Serialize*) went red after #1883 started passingAIDOTNET_LICENSE_KEYinto the test jobs. Root cause: the secret is a communityAIDN-PROD-COMMUNITY-*online-only server key that does not lift the free-trial operation cap, so the shared trial budget drains to 0 and everySaveEncrypted/Loadthat expects success throwsLicenseRequiredException. Before #1883,ModuleInitializerinjected a synthetic offline key and CI was green.Deeper: v2
aidn2(asymmetric Ed25519) was code-complete but never operationalized — release CI injectsBuildKey.binbut notLicensePublicKey.json, there's no issuer, and the site still mintsAIDN-*. v1's symmetric HMAC build key ships in every DLL (reverse-engineerable → forgeable), which is why v2 exists.What (P1 — client SDK; unblocks CI)
ModuleInitializerhardening (the CI unblock): install a deterministic synthetic offline test license unless the env key is itself offline-verifiable (aidn2/aidn.). A server-only communityAIDN-*env key — which forces the whole suite online and won't lift the save cap under capability gating — is replaced, so the persistence-heavy suite is reliably licensed. The real online/community-key path stays covered byLicenseE2ETests.ModelPersistenceGuard+LicenseCapabilities): anActivelicense lifts the cap for an op only when it grants the required capability (model:savefor save/serialize; load stays lenient). No-caps tokens = legacy grant-all (migration bridge);Active-but-missing-cap falls through to the trial (community keeps its free-trial saves). This is the paid-conversion gate (save/persist/encrypt), GPU stays free.LicenseClaimsgainsjti(revocation id),caps(authoritative offline grants),mach(node-lock),scope(audience).AsymmetricLicenseVerifierenforces CRL revocation + machine + scope binding after signature verify, and carries signed caps into the result. NewLicenseRevocationProviderverifies a signed CRL (embedded + online-fetched) and denies revokedkid/jti.tools/license-issuer) + full design doc (docs/licensing/v2-licensing-design.md).StripeWebhook_LicenseKeyFormat_IsValid) to assert the actualAIDN-PROD-{TIER}-{32hex}webhook format (was asserting the retiredaidn.{12}.{16}form that fix(license): require a 32-byte signature to classify aidn. keys as offline-HMAC #1807 rejects).Validation
With
AIDOTNET_LICENSE_KEY=AIDN-PROD-COMMUNITY-*(the exact CI scenario), the previously-failingLicensingIntegrationTestspass 7/7 (incl. the encrypt round-trips); broader license suite green apart from unrelated pre-existing failures.srcbuilds clean.Status of the design §13 follow-ups
aidn2scope/machine/CRL, the "Both" offline model (per-tenant RSA entitlement activated + per-machineaidn2parity), and standalone client glue.issue-license+get-revocationsedge functions deployed (server signsaidn2/ the signed CRL),revocationstable +revoke_license().validate_license_keyalready returnslicense_id(thejti) and per-tiercapabilities, so no hot-path change was needed.ci-2026akeypair, its public key embedded alongsideprod-2026a(multi-kid bundle), a scope-fenced short-expaidn2token in its ownAIDOTNET_CI_LICENSE_KEYsecret withAIDOTNET_LICENSE_SCOPE=ciwired into the test workflows (isolated from the global secret); rotation documented.docs/licensing/v2-deploy-runbook.md(re-issue paid keys with caps, publish CRL, deprecateAIDN-*after 90 days).(Review follow-ups from CodeRabbit — CRL merge/expiry, ModuleInitializer offline-verify, issuer/keygen hardening, edge-fn validation + opaque
sub, migration FK/idempotency — all addressed ine9ec13fc7.)Summary by CodeRabbit
New Features
Bug Fixes
Documentation