v0.10 carl-studio parity — entitlements client + AXON + slime + ledger forwarder#5
Merged
Conversation
…error codes
The Python counterpart of carl.camp's signPlatformJwt + /api/platform/
entitlements route. Fetches signed Ed25519 JWT, verifies against JWKS
at /.well-known/carl-camp-jwks.json, caches with 15-min TTL + 24h
offline-grace + JWKS pin-on-first-use.
New module: src/carl_studio/entitlements.py
- EntitlementsClient: fetch_remote, verify_jwt, fetch_jwks,
cache_get/set, is_offline_grace_valid
- Pydantic models: Entitlements, EntitlementGrant
- Module singleton: default_client()
- Background fetcher: fetch_remote_async() — single-thread executor
with atexit cleanup; errors captured by the Future, never re-raised
into the fast-path caller
- Test seam: reset_default_client() (public to satisfy pyright strict
`reportPrivateUsage` when tests import it)
Verification details:
- pynacl ed25519 (from [constitutional] extra), lazily imported inside
verify_jwt() so `import carl_studio.entitlements` stays cheap
- 5-min clock-skew tolerance on exp/nbf
- JWKS pin-on-first-use: rejects silent kid swaps; accepts additive
rotation (old kid + new kid both present)
- Cache file mode 0600, atomic via O_CREAT|O_EXCL with mode set on
create + os.replace; corrupt cache files quarantined to
<name>.corrupt-<ts>.json on parse failure
New error codes (carl_core/errors.py + __all__):
carl.gate.tier_remote_mismatch RemoteEntitlementError
carl.entitlements.network_unavailable EntitlementsNetworkError
carl.entitlements.signature_invalid EntitlementsSignatureError
carl.entitlements.cache_corrupt EntitlementsCacheError
carl.entitlements.jwks_stale JWKSStaleError
New dev dep: respx>=0.21 (httpx mocking).
Tests: 11 in tests/test_entitlements.py covering happy path, signature
mismatch, offline-grace boundaries (within / past 24h), cache
corruption, JWKS staleness (silent swap rejected, additive rotation
accepted), clock-skew tolerance, file mode 0600, and an
import-cheapness sanity check.
Foundation for: S1b (tier_gate verify_remote=True extension),
S1c (carl entitlements show CLI), I-S5a (zero-rl gate.py refresh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the v0.10 remote-tier verify ladder to the existing tier_gate
decorator. Default behavior is unchanged (verify_remote=False keeps
the legacy local-only check).
verify_remote=True semantics (local-fast-path-then-async per AP-1):
1. Existing local BaseGate runs first.
2. If local ALLOWS: schedule fetch_remote_async(jwt) — fire-and-
forget background verify. Errors swallowed; mismatch surfaces
on a future call when the cache flips.
3. If local DENIES: consult EntitlementsClient.cache_get(). If a
matching grant exists AND cache is within 24h offline-grace,
ALLOW with a tier_remote_grace event step. Otherwise raise
TierGateError as before.
This shape preserves the doctrine that `carl train --dry-run` never
blocks on the network — remote verify is async; only the local-
deny-but-cache-says-grant path adds latency, and even that path
hits a local file, not the wire.
Module-private helpers added (so tests can patch the surface
contract without spinning up the full entitlements client):
- _schedule_async_verify(feature) — fire-and-forget bg fetch
- _try_cached_grant(feature) — cache-only matching-grant probe
- _emit_tier_remote_grace(feature, ent) — best-effort log
- _resolve_bearer_token_for_verify() — env→file→LocalDB chain
Tests (added to tests/test_tier_resolver.py, +8):
- default_false_does_not_call_helpers (back-compat guard)
- local_allow_fires_async (background-verify scheduled)
- local_deny_with_cached_grant_allows (24h grace admit)
- local_deny_without_cached_grant_raises (security guarantee)
- local_deny_grant_outside_grace_raises (helper-owned grace)
- async_failure_does_not_propagate (fast-path doctrine)
- metadata_attribute_set / default_false_metadata (introspection)
Foundation for: S1c (carl entitlements show), S5a (zero-rl gate
refresh), F-S3a (carl train --managed gating).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing CLI surface for the v0.10 entitlements cache:
carl entitlements show # human-readable
carl entitlements show --json # machine-readable
carl entitlements show --refresh # force fetch first
Status taxonomy:
fresh cached_at within 15min (canonical)
stale within 1h, network may help
offline_grace within 24h, network down → cache is truth
offline_grace_expired beyond 24h → cache rejected
missing no cache file
corrupt cache file present but unparseable
`carl doctor` payload gains an `entitlements: {status, age_s, key_id}`
block so the existing doctor flow surfaces remote-tier health
without an extra command. The verbose readiness table also gets a
new Entitlements row with the matching detail string.
Imports of carl_studio.entitlements are lazy: top-level
`carl --version` / `carl --help` paths still skip the entitlements
module load. The new top-level verb is registered with the
`REGISTERED_SUBCOMMANDS` frozenset in `cli/entry.py` so the bare-
carl router knows about it.
Foundation for: J-S7a (CLAUDE.md routing table update).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original spec was marked 'Status: spec only. No implementation yet.' and described a planned design. Now that v0.10.0 ships the implementation (Phase A1-A3 + B5a + B1a/b + S1a-d), replace the stub with a doc that: - Reflects shipped reality (file paths, function signatures, error codes, CLI commands, doctor payload shape) - Lists locked decisions in a table (algorithm, key storage, rotation cadence, JWT exp, cache TTL, tier shape, skew tolerance, etc.) - Documents the API contract (request/response shapes, error envelopes, JWT claim layout, JWKS shape) - Maps every line to its owning file in carl.camp + carl-studio - Spells out the local-fast-path-then-async ladder semantics + the security guarantee (cache override only softens local deny within the 24h grace window) - Includes operator runbooks for first-time bootstrap and the 180-day rotation dance - Cross-references the implementation plan at /Users/terminals/.claude/plans/put-together-a-plan-sleepy-crystal.md Frontmatter updated: applies_to=v0.10.0, last_updated=2026-05-07. Closes Phase C-S1d.
…TP forwarder
Two-step landing of the v0.10 telemetry path:
S2a (carl_core): set_global_forwarder(fn) + _emit_to_forwarder(step) at
the bottom of packages/carl-core/src/carl_core/interaction.py. The
seam is HTTP-free; carl_studio registers itself via the fn callback,
inverting the dep so carl_core stays light.
- Errors raised by the forwarder swallowed + logged at WARN
- Atomic pointer assignment: thread-safe under CPython without a lock
- Tests: invoked-per-step, swallows-errors, None-clears, no-fwd-noop
S2b (carl_studio): src/carl_studio/telemetry/axon.py — AxonForwarder
class with a thread-bound queue.Queue + daemon flush thread, NOT
asyncio. Default-OFF: requires consent.telemetry == True AND no
AXON_FORWARD_DISABLED env opt-out.
Step → signal mapping (matches carl-studio CLAUDE.md AXON taxonomy):
TRAINING_STEP → skill.training_started
CHECKPOINT → skill.crystallized
LLM_REPLY → interaction.created
REWARD → interaction.created
EXTERNAL → action.dispatched
Step.phi/kuramoto_r/channel_coherence populated → also emits
coherence.update (decoupled from primary mapping; fires on any
action that carries coherence, including TOOL_CALL)
Idempotency: sha256(step_id:signal_type) where step_id is the 12-hex
globally-unique id assigned at Step construction — replay-safe.
Privacy: every payload runs through carl_core.errors._redact before
ship; secret-shaped keys (key/token/secret/password/authorization/
bearer) are scrubbed.
install_default_forwarder() is the one-call wiring: instantiates,
registers via set_global_forwarder, starts the daemon, registers
atexit cleanup. Idempotent.
Tests: 15 new tests in tests/test_axon_forwarder.py covering threshold
flush, env opt-out, consent off, secret-redaction, full mapping table
(5 ActionTypes), unmapped-action no-op, idempotency-key stability +
per-signal differentiation, secondary coherence event with kuramoto_r,
phi-only coherence on unmapped action, no-bearer silent drop, on_step
end-to-end queue, URL+bearer header verification, http failure swallow,
install_default_forwarder idempotency.
5-signal taxonomy lives in src/carl_studio/telemetry/axon_signals.py
as module-level constants + AXON_SIGNAL_ALLOWLIST frozenset, mirroring
carl.camp's vendored src/lib/axon-signal-types.ts.
Adaptations from brief (Step API didn't match spec assumptions):
- Forwarder fn signature: fn(step: Step) — Step has no chain_id /
step_index attrs; chain context flows via Step.session_id /
Step.trace_id (caller-set) or Step.step_id (always set).
- carl_core.errors uses _redact (not _redact_dict) — adapted import.
- Coherence detection reads top-level Step.phi/kuramoto_r/
channel_coherence (not Step.metadata which doesn't exist).
- record() requires `name` arg; tests + brief examples adapted.
- Idempotency seed = (step_id, signal_type) instead of
(chain_id, step_index, signal_type).
Closes Phases E-S2a + E-S2b. Next: F (managed slime training).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three carl-studio surfaces for v0.10 managed training:
1. src/carl_studio/adapters/slime_submit.py
- SlimeSubmitClient.submit(slime_args, idempotency_key=) →
SlimeRunHandle
- SlimeSubmitClient.get_status(run_id) → SlimeRunStatus
- SlimeSubmitClient.poll_until_done(run_id, interval, timeout)
- assert_no_user_hf_token_leak(slime_args) — defence-in-depth
guard for the CARL_CAMP_HF_TOKEN-only invariant. carl.camp's
dispatchManagedTrainingRun also enforces this; double-check
here means a misconfigured payload fails fast, before the
network round-trip.
2. carl train --managed flag + --wait + --idempotency-key
Routes to SlimeSubmitClient when set; existing SlimeAdapter
path unchanged otherwise. check_tier(train.slime.managed)
gates the managed path; --dry-run prints the translated
SlimeArgs payload without sending.
3. carl slime-schema (top-level command)
Prints SlimeArgs.model_json_schema() so carl.camp can regenerate
src/lib/slime-args-schema.ts when SlimeArgs evolves. --pretty
for indented output. Registered in cli/entry.py routing table.
SlimeRolloutBridge.finalize_resonant(slime_run_id=None):
When set, recorded under metadata.slime_run_id AND emitted on
the CHECKPOINT step's output. The header X-Carl-Slime-Run-Id
ships via the CLI's `carl resonant publish --slime-run-id <uuid>`
flag — the resonants route accepts binary octet-stream bodies,
so the metadata channel is HTTP headers, not envelope JSON
(per the F-S3a contract correction). _is_uuid validates the
id locally so malformed values fail fast (exit 2) instead of
round-tripping for a 422.
Three new error codes (carl_core.errors):
carl.slime.hf_token_leak (SlimeHfTokenLeakError)
carl.slime.managed_submit_failed (SlimeManagedSubmitFailedError)
carl.slime.run_not_found (SlimeRunNotFoundError)
Tests: 22 cases in tests/test_slime_submit.py covering every
contract case — 4 token-leak guard paths (key pattern, exact-match,
hf-shape, clean payload), 7 submit cases (happy, 401, 402, 422,
500, no-bearer, malformed body), 4 status cases (happy, 404,
succeeded-with-resonant, unknown-status), 2 boundary (empty
run_id, network error wrapped), 2 fixture-passes (Pydantic round
trip, non-dict ignored), 1 nested-list recursion, 1 error-code
contract assertion. respx mocks the wire; 0.46s targeted run.
Closes Phase F-S3a. Phase F (managed slime training) complete.
Next: Phase G (Stripe meters).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ration src/carl_studio/fsm_ledger_forward.py: ConstitutionalForwarder class. Always persists signed LedgerBlocks to ~/.carl/constitutional_ledger.jsonl (10MB rotation, max 3 archives — total ceiling 40 MB worst case). HTTP forward to carl.camp's existing /api/ledger/append (Phase H-B6a hardened the receiving table) is gated on consent.telemetry == True AND a resolvable bearer token. Forward failures are non-fatal; replay_pending() walks the local file and retries unacked entries (idempotency = signature_hex match) with no double-ack risk. src/carl_studio/fsm_ledger.py:evaluate_action gains optional forward: ConstitutionalForwarder | None = None kwarg. When set, the caller's existing local-block-append flow is preserved unchanged; the forwarder is invoked AFTER the local append + state advance, with errors caught and logged — never aborting the FSM transition. New error code: carl.constitutional.forward_failed (ConstitutionalForwardFailedError, subclass of NetworkError). Added to carl_core.errors.__all__. Tests: 14 cases in tests/test_constitutional_forward.py covering consent-off + no-bearer short-circuit, always-local-append on every path (5 sub-cases), hash-chain integrity across multiple forwards, replay retry + double-ack protection (3 sub-cases), signature stability in transit + URL composition, JSONL rotation at 10MB + MAX_ROTATIONS cap, plus integration smoke covering evaluate_action forward dispatch + error swallow. Closes Phase H-S4a. Phase H complete (constitutional ledger forwarding end-to-end). Next: Phase I (zero-rl-pipeline gate.py). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/private/v10_resonance_boundary.md: new note confirming the resonance package required ZERO changes during the v0.10-parity arc. grep across src/resonance/ for carl.tier / carl.gating / carl_studio.entitlements / requests / httpx / supabase returned zero hits. The MIT/BUSL boundary holds; the four pillars (entitlements, AXON, slime, ledger) all live above the resonance math kernel. - CLAUDE.md: CLI routing table gains 3 rows for v0.10 commands (entitlements show, train --managed, slime-schema). Release history adds the v0.10-parity entry. Deferred-roadmap items v0.16 + v0.17 schema export + v0.10 remote-tier all marked SHIPPED with implementation pointers.
GitHub's secret scanner rejected the push because tests/test_slime_submit.py:67
contained 'hf_abcdefghijABCDEFGHIJabcdefghijABCD' which matches their HF token
regex. The string was a deliberate fake used to verify assert_no_user_hf_token_leak
catches token-shaped values, but the literal triggers the scanner regardless.
Constructing the string at runtime ('hf_' + 'x' * 36) keeps the test working
while leaving no token-shaped literal in the source. The other two literals
in this file (lines 90, 150) already use the all-x pattern and pass scanning.
A functional bug surfaced during the v10-parity security review. The
carl.camp /api/platform/entitlements route returns:
{ ok: true, token: '<jws>', expires_in, tier, tier_label, ... }
But EntitlementsClient.fetch_remote read body.get('jwt') / body.get(
'access_token'). With both keys absent in the actual response, the
client raised EntitlementsNetworkError on every successful fetch — the
remote-tier verify chain was broken end-to-end despite the route + JWKS
both serving correctly in production.
Not a security bug (the JWT bytes never reach disk on the failure path
since cache_set is skipped), but a functional one that would have
silently broken the v0.10 happy path on first install.
Fix: read body.get('token') first, fall back to 'jwt' / 'access_token'
for forward-compat with future renames or alt deployments. Test
fixtures (tests/test_entitlements.py) updated to use the canonical
{'token': ...} shape — exercises the prod contract directly while the
fallback paths remain reachable via the OR chain.
Suite: 80/80 carl-studio v10-surface tests green. ruff + pyright clean
on changed files.
Caught during security review of v10-parity branch.
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
carl-studio side of the v0.10 carl.camp parity arc. Ships
EntitlementsClient,tier_gate(verify_remote=True), AXON forwarder (with carl_core seam),SlimeSubmitClient+ token-leak invariant guard,ConstitutionalForwarder, and thecarl entitlements showCLI surface.New modules:
src/carl_studio/entitlements.py—EntitlementsClient(pynacl + JWKS PoFU + 24h offline-grace + 15-min cache)src/carl_studio/telemetry/{__init__,axon,axon_signals}.py—AxonForwarder(thread-bound queue + daemon flush; consent + AXON_FORWARD_DISABLED gating)src/carl_studio/adapters/slime_submit.py—SlimeSubmitClient+assert_no_user_hf_token_leakinvariant guardsrc/carl_studio/fsm_ledger_forward.py—ConstitutionalForwarder(always-local-jsonl + opt-in HTTP forward; 10MB rotation)src/carl_studio/cli/entitlements_cmd.py—carl entitlements show [--json] [--refresh]Modified modules:
packages/carl-core/src/carl_core/interaction.py—set_global_forwarder(fn)seam (HTTP-free; carl_studio registers itself)packages/carl-core/src/carl_core/errors.py— 9 new error subclasses (entitlements, slime, constitutional namespaces)src/carl_studio/tier.py—tier_gate(..., verify_remote=False)extension (local-fast-path-then-async ladder)src/carl_studio/cli/training.py—--managedflag oncarl train+carl slime-schemasubcommandsrc/carl_studio/cli/resonant.py—--slime-run-id <uuid>flag →X-Carl-Slime-Run-Idheader on publishsrc/carl_studio/training/slime_bridge.py—finalize_resonant(slime_run_id=None)kwargsrc/carl_studio/cli/startup.py—entitlementsblock incarl doctorpayloadsrc/carl_studio/fsm_ledger.py—evaluate_action(forward=...)kwargpyproject.toml— addsrespx>=0.21to[dev]CLAUDE.md— CLI routing table updated; release-history + deferred-roadmap items marked SHIPPEDDocs:
docs/v10_remote_entitlements_spec.md— replaces stub with implementation reference (file paths, error codes, runbooks)docs/private/v10_resonance_boundary.md— confirms resonance package required ZERO changesTest posture:
Cross-repo dependencies:
Test plan
pytest tests/test_entitlements.py tests/test_axon_forwarder.py tests/test_slime_submit.py tests/test_constitutional_forward.py tests/test_tier_resolver.py tests/test_cli.py packages/carl-core/tests/test_interaction_forwarder.py packages/carl-core/tests/test_errors.py -qruff checkon changed filescarl camp login && carl entitlements show --refreshresolves the new ed25519 key from prodcarl train --managed --slime ...(post-Stripe-meter-bootstrap) → submits run, polls to terminal stateassert_no_user_hf_token_leakrejects payloads with HF tokens🤖 Generated with Claude Code