Skip to content

v0.10 carl-studio parity — entitlements client + AXON + slime + ledger forwarder#5

Merged
wheattoast11 merged 10 commits into
mainfrom
feat/v10-parity
May 8, 2026
Merged

v0.10 carl-studio parity — entitlements client + AXON + slime + ledger forwarder#5
wheattoast11 merged 10 commits into
mainfrom
feat/v10-parity

Conversation

@wheattoast11
Copy link
Copy Markdown
Owner

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 the carl entitlements show CLI surface.

New modules:

  • src/carl_studio/entitlements.pyEntitlementsClient (pynacl + JWKS PoFU + 24h offline-grace + 15-min cache)
  • src/carl_studio/telemetry/{__init__,axon,axon_signals}.pyAxonForwarder (thread-bound queue + daemon flush; consent + AXON_FORWARD_DISABLED gating)
  • src/carl_studio/adapters/slime_submit.pySlimeSubmitClient + assert_no_user_hf_token_leak invariant guard
  • src/carl_studio/fsm_ledger_forward.pyConstitutionalForwarder (always-local-jsonl + opt-in HTTP forward; 10MB rotation)
  • src/carl_studio/cli/entitlements_cmd.pycarl entitlements show [--json] [--refresh]

Modified modules:

  • packages/carl-core/src/carl_core/interaction.pyset_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.pytier_gate(..., verify_remote=False) extension (local-fast-path-then-async ladder)
  • src/carl_studio/cli/training.py--managed flag on carl train + carl slime-schema subcommand
  • src/carl_studio/cli/resonant.py--slime-run-id <uuid> flag → X-Carl-Slime-Run-Id header on publish
  • src/carl_studio/training/slime_bridge.pyfinalize_resonant(slime_run_id=None) kwarg
  • src/carl_studio/cli/startup.pyentitlements block in carl doctor payload
  • src/carl_studio/fsm_ledger.pyevaluate_action(forward=...) kwarg
  • pyproject.toml — adds respx>=0.21 to [dev]
  • CLAUDE.md — CLI routing table updated; release-history + deferred-roadmap items marked SHIPPED

Docs:

  • 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 changes

Test posture:

  • 135/135 v10-surface tests green (entitlements / axon / slime / constitutional / tier_resolver / cli)
  • ruff clean on changed files
  • pyright direct CLI clean (the harness shows stale "could not resolve" diagnostics post-Edit; per CLAUDE.md trust the CLI run)

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 -q
  • ruff check on changed files
  • Live: carl camp login && carl entitlements show --refresh resolves the new ed25519 key from prod
  • Live: carl train --managed --slime ... (post-Stripe-meter-bootstrap) → submits run, polls to terminal state
  • Verify assert_no_user_hf_token_leak rejects payloads with HF tokens

🤖 Generated with Claude Code

wheattoast11 and others added 10 commits May 7, 2026 20:28
…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.
@wheattoast11 wheattoast11 merged commit c31c668 into main May 8, 2026
1 check passed
@wheattoast11 wheattoast11 deleted the feat/v10-parity branch May 8, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant