Skip to content

Migrate trace client to ironclaw_reborn and prune to contributor-only CLI - #3738

Merged
zmanian merged 9 commits into
reborn-integrationfrom
trace-commons-invite-code
May 23, 2026
Merged

zmanian merged 9 commits into
reborn-integrationfrom
trace-commons-invite-code

Conversation

@zmanian

@zmanian zmanian commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator

Context

Completes the migration of the Ironclaw-side trace client into the reborn system, lands the pilot invite-code wiring, and prunes Ironclaw's trace CLI to contributor-only. Operator/admin/worker/tenant commands move out of Ironclaw entirely — they re-emerge as separate binaries in the trace-commons-server repo (companion stack linked below).

Companion stack — operator binaries in trace-commons-server

The 39 operator commands pruned here re-appear in trace-commons-server:

This PR (Ironclaw) and the companion stack can land independently — Ironclaw's CLI surface narrows, trace-commons-server's CLI surface broadens, and no behavior breaks because the underlying HTTP API is unchanged.

Commits (7)

  1. 5e568fcb5 extract trace client into ironclaw_reborn_traces crate — moves src/trace_contribution.rs (12k LOC) + src/trace_client.rs + src/tools/redaction.rs into a new workspace crate crates/ironclaw_reborn_traces. ConversationMessage lives in the new crate and is re-exported from src/history/store.rs so monolith callers are unaffected. 3-line pub use shims at the old monolith paths.
  2. 23104c9fa add traces subcommand stub to ironclaw_reborn_cli — clap surface stub. Replaced by commit 4.
  3. 930e82d4f wire pilot invite_code through upload-claim refresh — the original Migrate trace client to ironclaw_reborn and prune to contributor-only CLI #3738 scope, on the extracted paths.
  4. 3b5ca1fde migrate ironclaw traces CLI surface into ironclaw_reborn_cli — wholesale port of the legacy src/cli/traces.rs (50 subcommands, 9,873 LOC). Monolith's ironclaw traces is gone; ironclaw-reborn traces ... is the only path.
  5. 620ce8fc5 split traces CLI by audience into module tree — commands/traces/{mod,contributor,reviewer,worker,admin,tenant,shared}.rs.
  6. 24ff0c73b extract trace CLI tests into sibling tests.rs — 2,800-line test module pulled out via #[path] (same pattern as tracedao-ingest.rs).
  7. 5494fa00f prune operator commands from Ironclaw trace CLI — deletes 39 operator variants (reviewer 8, worker 8, admin 12, tenant+ranker+audit 11) and everything supporting them. Keeps 12 contributor variants.

Architectural cut

After this PR, Ironclaw's ironclaw-reborn traces exposes only contributor-side concerns:

Command Purpose
opt-in Consent + endpoint config + invite code + NEAR credit account ref
opt-out Consent withdrawal
status Standing-policy view
preview Envelope preview + redaction
enqueue Add envelope to local queue
flush-queue Submit eligible queued envelopes
queue-status Queue diagnostics
credit Local credit view + notices
submit One-shot submit
list-submissions Local submission records
revoke Revoke own submission
ingest-health Probe ingestion /health

Operator workflows (reviewer triage, worker routes, admin/retention/export, tenant policy) live in the four trace-commons-server binaries linked above, talking to the same HTTP API.

Breaking CLI change

Users invoking ironclaw traces ... must switch to ironclaw-reborn traces .... Operator commands previously available via ironclaw traces (quarantine-list, review-decision, maintenance-run, tenant-policy-*, etc.) are no longer available from Ironclaw at all — they move to trace-commons-server operator binaries.

Invite-code wiring

Companion to TraceCommons/trace-commons-server#109. The issuer returns four typed refusal labels (PilotAllowlistNotMatched 403, PilotAllowlistInviteCodeMissing 400, PilotAllowlistStale 503, PilotAllowlistMalformed 503). This PR:

  1. Carries the invite code through the standing policy. StandingTraceContributionPolicy.upload_token_invite_code: Option<String> (serde-default + skip-if-none). TraceUploadClaimIssuerRequest body field. trace_upload_claim_cache_key keys on the invite code so rotation forces a fresh mint.
  2. Surfaces typed refusal labels. fetch_trace_upload_claim_from_issuer parses {"error": "<Label>"} and maps each PilotAllowlist* to a user-actionable diagnostic.
  3. CLI flag + status surface. ironclaw-reborn traces opt-in --upload-token-invite-code <CODE> (off by default). status and queue-status report "pilot invite code: configured / not configured" — never echo the raw code.

Tests

All 7 invite-code unit tests pass, plus the broader contributor test suite (58 tests in reborn-cli):

  • standing_policy_serde_back_compat_when_invite_code_missing
  • standing_policy_serde_round_trips_invite_code_when_set
  • standing_policy_serde_omits_invite_code_when_none
  • cache_key_distinguishes_different_invite_codes
  • parse_trace_upload_claim_error_label_handles_known_shapes (5 in ironclaw_reborn_traces)
  • opt_in_invite_code_flag_parses_through_cli
  • opt_in_invite_code_defaults_to_none_when_absent (2 in ironclaw_reborn_cli, parse-through against the reborn Cli)

Verification

RUSTFLAGS="-D warnings" cargo check --workspace --all-targets   # clean
RUSTFLAGS="-D warnings" cargo test --workspace --no-run         # clean
cargo clippy --workspace --all-targets -- -D warnings           # clean
cargo test -p ironclaw_reborn_cli                                # 58 passed

Net diff vs origin/reborn-integration

27 files changed, +15,564 / −22,443 — a net −6,879 LOC in Ironclaw thanks to the prune. commands/traces/mod.rs is 1,838 lines; contributor.rs is 112 lines; tests.rs is 619 lines.

Compatibility

  • All monolith callers of crate::trace_contribution::* and crate::trace_client::* keep building via re-export shims.
  • crate::history::ConversationMessage continues to resolve (re-exported from ironclaw_reborn_traces).
  • No behavior change for deployments that don't configure an invite code.
  • CLI command names changed — see "Breaking CLI change".

Out of scope

  • Operator binaries in trace-commons-server — landing in the companion stack (chore: release v0.5.0 #124 / fix: sentinel value collision in FailoverProvider cooldown #125 / feat: 10 infrastructure improvements from zeroclaw #126).
  • Operator-runbook updates renaming ironclaw traces ... → ironclaw-reborn traces ... for the contributor surface.
  • Cleanup of operator-side helpers inside the ironclaw_reborn_traces library crate that no Ironclaw consumer references anymore — leaving them in place so trace-commons-server operator binaries can consume them via path dep / vendoring (currently the operator stack ports the wire shapes rather than depending on the Ironclaw crate, but this leaves the door open).
  • Full Settings/profile resolution on the reborn-cli side (currently uses the trimmed IRONCLAW_OWNER_ID env var path).
  • Porting the local privacy-filter sidecar canary into the operator-client model (currently stubbed in trace-commons-tenant privacy-filter-canary; in-progress as a follow-up to trace-commons-server feat: 10 infrastructure improvements from zeroclaw #126).

@github-actions github-actions Bot added scope: channel/cli TUI / CLI channel scope: docs Documentation size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels May 18, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bf198a67e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/trace_contribution.rs Outdated
Comment on lines +4820 to +4822
"the workload token did not carry an invite_code claim. \
Re-run `ironclaw traces opt-in --invite-code <CODE> ...` with the operator-issued code, \
or have your operator reissue a workload token that includes it.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use correct opt-in flag in invite-code-missing diagnostic

When the issuer returns PilotAllowlistInviteCodeMissing, this error message tells users to rerun with --invite-code, but the CLI only defines upload_token_invite_code on traces opt-in (which maps to --upload-token-invite-code in src/cli/traces.rs). In the allowlist-gated pilot flow, this sends users to a non-existent flag and blocks self-remediation unless they manually discover the right option from help output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 81f3459 — diagnostic now names the actual flag --upload-token-invite-code.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for operator-issued pilot invite codes in trace contributions. It adds a new CLI flag --upload-token-invite-code, updates the standing policy and diagnostic surfaces to handle this field securely, and incorporates the code into the upload claim request and cache key. Feedback focuses on ensuring documentation accurately reflects deferred implementation tasks, correcting a comment about response size limits, and fixing a diagnostic message that referenced an incorrect CLI flag.

Comment thread src/trace_contribution.rs Outdated
Comment on lines +648 to +654
/// Operator-issued pilot invite code. When set, the trace-commons
/// upload-claim request includes it (server-side
/// `WorkloadClaims.invite_code` / request-body fallback). When the
/// configured workload-token JWT also carries an `invite_code` claim,
/// the client refuses to refresh if the two disagree — surfaces a
/// `PilotAllowlistInviteCodeMismatch` diagnostic before any HTTP call.
/// Off by default; only required when the issuer is allowlist-gated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation describes verifying the invite_code claim, which is currently deferred. Please update the documentation to mark this as a known future task, ensuring it accurately reflects the current implementation while preserving the intent for future work.

References
  1. When an implementation is deferred or simplified, document the deferred work as a known future task to ensure documentation matches the current implementation state.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 81f3459 — rewrote the docstring to describe the actual behavior (typed PilotAllowlist* refusals from the issuer, no client-side pre-flight that decodes the workload JWT) and called out the pre-flight as a known future task. The PR description already lists it under "Out of scope".

Comment thread src/trace_contribution.rs Outdated
Comment on lines +4812 to +4813
// TRACE_UPLOAD_CLAIM_MAX_RESPONSE_BYTES; we cap at 4 KiB here
// because error bodies are tiny.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment states that the response is capped at 4 KiB, but the code uses TRACE_UPLOAD_CLAIM_MAX_RESPONSE_BYTES, which is defined as 64 KiB (line 45). Please update the comment to reflect the actual limit used by read_bounded_trace_upload_claim_response.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 81f3459 — comment now references TRACE_UPLOAD_CLAIM_MAX_RESPONSE_BYTES instead of asserting a specific number that disagreed with the constant.

Comment thread src/trace_contribution.rs Outdated
Comment on lines +4819 to +4822
Some("PilotAllowlistInviteCodeMissing") => Some(
"the workload token did not carry an invite_code claim. \
Re-run `ironclaw traces opt-in --invite-code <CODE> ...` with the operator-issued code, \
or have your operator reissue a workload token that includes it.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The diagnostic message suggests re-running the command with --invite-code <CODE>, but the actual CLI flag added in src/cli/traces.rs is --upload-token-invite-code. This discrepancy will lead to user confusion when they attempt to follow the instructions in the error message.

Suggested change
Some("PilotAllowlistInviteCodeMissing") => Some(
"the workload token did not carry an invite_code claim. \
Re-run `ironclaw traces opt-in --invite-code <CODE> ...` with the operator-issued code, \
or have your operator reissue a workload token that includes it.",
Some("PilotAllowlistInviteCodeMissing") => Some(
"the workload token did not carry an invite_code claim. \
Re-run ironclaw traces opt-in --upload-token-invite-code <CODE> ... with the operator-issued code, \
or have your operator reissue a workload token that includes it.",
),
References
  1. Ensure preflight configuration checks use specific error variants and provide accurate diagnostic messages to avoid user confusion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 81f3459 — applied the suggested edit verbatim. Both auto-reviewers caught the same issue from different angles; good signal.

zmanian added a commit that referenced this pull request May 18, 2026
Three fixes, all from chatgpt-codex-connector + gemini-code-assist on
the initial commit:

1. PilotAllowlistInviteCodeMissing diagnostic suggested
   `--invite-code` but the CLI flag is actually
   `--upload-token-invite-code`. Re-running the wrong flag would have
   failed clap's argument parsing and blocked self-remediation. Fix
   the diagnostic to name the real flag.

2. Comment near the error-body read claimed the body was capped at
   "4 KiB" but the actual cap is TRACE_UPLOAD_CLAIM_MAX_RESPONSE_BYTES
   (64 KiB). Rewrite the comment to reference the constant and stop
   asserting a number that wasn't true.

3. Docstring on upload_token_invite_code described a local JWT
   pre-flight check ("the client refuses to refresh if the two
   disagree — surfaces a PilotAllowlistInviteCodeMismatch diagnostic
   before any HTTP call") that this slice does not implement. Rewrite
   to describe the actual behavior (typed PilotAllowlist* refusals
   from the issuer, no client-side pre-flight) and call out the
   follow-up as a known future task.

No behavior change; comment + docstring + one user-facing string only.
Existing tests still pass.

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Pass an invite_code from the client through the trace upload-claim refresh flow to satisfy a new server-side invite-code allowlist

Stats: 9 findings (from 9 raw, 9 after dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, design. Reviewers failed: none. Body-only: 0

Security

  1. Medium Pilot invite code stored in plaintext on disk with default file permissions (src/cli/traces.rs:68, confidence 75) — anchor: src/cli/traces.rs:6966
    The invite code is serialized as plaintext into policy.json without restrictive file permissions. On a multi-user system, any local user can read it and bypass the issuer's allowlist gate.
    Fix: Set restrictive file permissions (0o600) when writing the policy file, or store a hash and send the raw value only at request time.

  2. Low Invite code embedded in plaintext in cache key string (src/trace_contribution.rs:648, confidence 50) — anchor: src/trace_contribution.rs:4701
    The cache key includes the raw invite code. If any future code logs cache keys, the secret would be exposed.
    Fix: Hash the invite code with SHA256 before embedding it in the cache key.

Bugs

  1. Low show_policy_status reports empty invite code as 'configured' (src/cli/traces.rs:70, confidence 75) — anchor: src/cli/traces.rs:2943
    Uses .is_some() instead of filtering empty strings like the diagnostics function does.
    Fix: Change to as_deref().is_some_and(|c| !c.trim().is_empty()).

Tests

  1. High fetch_trace_upload_claim_from_issuer PilotAllowlist error branch has no caller-level test (src/trace_contribution.rs:652, confidence 100) — anchor: src/trace_contribution.rs:4810
    The ~50-line error-handling branch for typed refusals needs an integration test with a mock HTTP server.
    Fix: Add test covering mock issuer returning 400 with PilotAllowlistNotMatched body.

  2. High fetch_trace_upload_claim_from_issuer unknown-label fallback error path untested (src/trace_contribution.rs:4810, confidence 100) — anchor: src/trace_contribution.rs:4851
    The generic HTTP-status fallback path when label is None needs separate coverage.
    Fix: Add test covering mock issuer returning 500 with non-JSON body.

  3. Medium opt_in does not test invite_code persistence when a value is provided (src/cli/traces.rs:7445, confidence 75) — anchor: src/cli/traces.rs:2830
    No test verifies that invite_code is correctly written to and read back from policy.json.
    Fix: Add test with upload_token_invite_code=Some("INV-PILOT-001") and read back.

  4. Medium trace_queue_status_diagnostics upload_token_invite_code_configured field untested (src/cli/traces.rs:2811, confidence 75) — anchor: src/cli/traces.rs:3193
    The new boolean field has no test verifying true/false reporting.
    Fix: Add test covering policy with and without invite_code.

  5. Low parse_trace_upload_claim_error_label does not test non-string error field (src/trace_contribution.rs:4819, confidence 50) — anchor: src/trace_contribution.rs:4912
    Missing coverage for error field as number, object, array, bool, null.
    Fix: Add test cases for non-string error field types.

Design

  1. Medium String-matched error labels should be an enum, not string literals (src/trace_contribution.rs:4811, confidence 75) — anchor: .claude/rules/types.md:17-19
    The four PilotAllowlist* labels form a closed set but are matched as string literals. Per .claude/rules/types.md, enums should be used for fixed small sets.
    Fix: Parse the raw string into a typed enum at the boundary, then match on the enum.

Comment thread src/trace_contribution.rs Outdated
/// upload-claim request includes it (mirrored into the request body;
/// the server-side issuer reads `WorkloadClaims.invite_code` today,
/// the body field is forward-compat for a later server slice). The
/// client surfaces the issuer's typed `PilotAllowlist*` refusals

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — fetch_trace_upload_claim_from_issuer PilotAllowlist error branch has no caller-level test.

The new ~50-line error-handling branch for PilotAllowlist* typed refusals (lines 4810-4860) is exercised only indirectly via the unit test on parse_trace_upload_claim_error_label. Per repo testing rules, 'Test through the caller, not just the helper' — the match-on-label, diagnostic-selection, and anyhow::bail! composition path needs an integration test using a mock HTTP server returning 4xx with a typed error body.

Fix: tests::trace_contribution::fetch_trace_upload_claim_from_issuer_returns_typed_pilot_allowlist_error covering mock issuer returning 400 with PilotAllowlistNotMatched body

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Added fetch_trace_upload_claim_from_issuer_returns_typed_pilot_allowlist_error in crates/ironclaw_reborn_traces/src/contribution.rs — uses an axum mock returning 400 + {"error":"PilotAllowlistNotMatched"} and asserts the typed diagnostic. The reviewer-mentioned line numbers are pre-extraction; the file moved to the new crate in this PR.

Comment thread src/trace_contribution.rs Outdated
safe_trace_upload_claim_issuer_url_label(&parsed),
status.as_u16()
);
if !status.is_success() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — fetch_trace_upload_claim_from_issuer unknown-label fallback error path untested.

When the issuer returns a non-success status with an unrecognized error label (or no parseable label), the code falls through to the generic HTTP-status bail! at line 4851. This fallback path is distinct from the typed-diagnostic path and needs separate coverage to ensure the error message format is correct when label is None.

Fix: tests::trace_contribution::fetch_trace_upload_claim_from_issuer_generic_http_error_when_label_unknown covering mock issuer returning 500 with non-JSON body

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Added fetch_trace_upload_claim_from_issuer_generic_http_error_when_label_unknown — mock returns 500 with non-JSON body, assertion confirms the generic HTTP 500 fallback fires (no PilotAllowlist label). Implementation note: the issuer URL validator rejects http/loopback, so the helper that formats the error message is now a pure function (build_trace_upload_claim_http_error) tested directly against bodies the axum mock produces. Both new tests round-trip through the mock.

Comment thread src/cli/traces.rs Outdated
#[arg(long)]
upload_token_workload_token_env: Option<String>,

/// Operator-issued pilot invite code. When set, included in upload-claim

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Pilot invite code stored in plaintext on disk with default file permissions.

The upload_token_invite_code is serialized as plaintext into policy.json via std::fs::write without setting restrictive file permissions (e.g., 0o600). The code comments in TraceQueueStatusDiagnostics refer to this as 'operator-secret material.' On a multi-user or shared system, any local user can read the invite code from the policy file and use it to bypass the issuer's allowlist gate. Other secret references in this file (e.g., bearer_token_env, upload_token_workload_token_env) store only environment variable names, not actual secrets — the invite code is the actual credential value.

Fix: Either store a hash of the invite code (sha256) and send the raw value only at request time from a secure source (env var or encrypted store), or set restrictive file permissions (0o600) when writing the policy file via OpenOptions::new().create(true).write(true).mode(0o600).open().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Both write paths (write_policy in the CLI module and write_json_file in the contribution module) now use OpenOptions::new().create(true).write(true).truncate(true).mode(0o600).open() under cfg(unix), with a fallback to fs::write on non-Unix. Took the perms approach over hashing since the invite code is short-lived and operator-rotated; the issuer's allowlist remains the authoritative gate.

Comment thread src/cli/traces.rs Outdated
}

#[test]
fn opt_in_invite_code_flag_parses_through_cli() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — opt_in does not test invite_code persistence when a value is provided.

The existing test opt_in_writes_runtime_owner_policy_and_normalizes_selected_tools passes upload_token_invite_code: None. No test verifies that when an invite_code is set, it is correctly written to the persisted policy.json file and can be read back.

Fix: tests::cli::traces::opt_in_persists_invite_code_when_set covering opt_in with upload_token_invite_code=Some("INV-PILOT-001") and reading back the policy file

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Added opt_in_persists_invite_code_when_set covering the round-trip: opt_in with Some("INV-PILOT-001") → read policy.json → deserialize as StandingTraceContributionPolicy → assert the field matches. The test uses a scoped-policy path to avoid racing with the existing opt_in_writes_runtime_owner_policy_... test; both global-policy-touching tests now serialize through GLOBAL_POLICY_TEST_MUTEX.

Comment thread src/cli/traces.rs Outdated
/// Hash-only: true when the standing policy has an `upload_token_invite_code`
/// set, false otherwise. The raw code is never returned, surfaced only as
/// a present/absent boolean to match the rest of the diagnostics surface.
upload_token_invite_code_configured: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — trace_queue_status_diagnostics upload_token_invite_code_configured field untested.

The new upload_token_invite_code_configured boolean computed in trace_queue_status_diagnostics (line 3193-3196) has no test verifying it reports true when invite_code is set and false when absent or whitespace-only.

Fix: tests::cli::traces::queue_status_diagnostics_reports_invite_code_configured covering policy with and without invite_code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Added queue_status_diagnostics_reports_invite_code_configured with three branches: a configured non-empty code, a whitespace-only string, and None. Confirms upload_token_invite_code_configured reports true/false/false respectively — matching the is_some_and(|c| !c.trim().is_empty()) filter.

Comment thread src/trace_contribution.rs Outdated
status.as_u16()
);
if !status.is_success() {
// Try to extract the issuer's typed error label so PilotAllowlist*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — String-matched error labels should be an enum, not string literals.

The four PilotAllowlist* error labels (InviteCodeMissing, NotMatched, Stale, Malformed) form a closed, known set but are matched as string literals in fetch_trace_upload_claim_from_issuer. .claude/rules/types.md mandates enums for fixed small sets — this module already follows that convention extensively (TraceFailureMode, TraceChannel, TraceAllowedUse, TraceCreditEventKind, TraceQueueHoldKind, etc.). String matching is fragile: a typo in any literal silently falls through to the _ => None arm with no compiler diagnostic. Parse the raw string into a typed enum at the boundary, then match on the enum.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Replaced the string-literal cascade with a typed enum PilotAllowlistRefusal { NotMatched, InviteCodeMissing, Stale, Malformed } plus PilotAllowlistRefusal::from_label and diagnostic(). The four diagnostic strings are byte-identical to before — only the dispatch is typed now, so a typo in any label flips the exhaustiveness check at compile time.

Comment thread src/cli/traces.rs Outdated

/// Operator-issued pilot invite code. When set, included in upload-claim
/// refresh requests so the issuer's allowlist gate can match it. Required
/// only when the configured issuer runs with TRACE_COMMONS_ALLOWLIST_SOURCE

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — show_policy_status reports empty invite code as 'configured'.

show_policy_status uses policy.upload_token_invite_code.is_some() to decide 'configured' vs 'not configured', but opt_in pre-filters empty/whitespace-only strings before storing. A manually edited policy file with "upload_token_invite_code": "" would display 'configured' while the actual behavior treats it as absent (trace_queue_status_diagnostics correctly filters empties via is_some_and(|code| !code.trim().is_empty())).

Fix: Change is_some() to as_deref().is_some_and(|c| !c.trim().is_empty()) to match the diagnostics function and the opt_in storage filter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Changed policy.upload_token_invite_code.is_some() to policy.upload_token_invite_code.as_deref().is_some_and(|c| !c.trim().is_empty()), matching the filter trace_queue_status_diagnostics already uses. Manually-edited policy files with an empty string now correctly report as not configured.

Comment thread src/trace_contribution.rs Outdated
pub upload_token_tenant_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upload_token_workload_token_env: Option<String>,
/// Operator-issued pilot invite code. When set, the trace-commons

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Invite code embedded in plaintext in cache key string.

The trace_upload_claim_cache_key function includes the raw invite code value in the cache key format string. While the cache is currently in-memory only (a LazyLock<BTreeMap>), if any future debugging, telemetry, or error-handling code logs cache keys or serializes the cache, the secret invite code would be exposed. The existing pattern in this codebase hashes sensitive identifiers (e.g., sha256: prefixed hashes for scopes, tenants, and revocation handles).

Fix: Hash the invite code with SHA256 before embedding it in the cache key, consistent with how other sensitive identifiers are handled in this file (e.g., sha256: prefixed hashes at lines 1905-1906, 2972-2973).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. trace_upload_claim_cache_key now embeds sha256:{hex} of the invite code instead of the raw value, matching the existing sha256:-prefixed convention used elsewhere in the file. Added a regression test confirming the raw code is absent and the hash is present. cache_key_distinguishes_different_invite_codes continues to pass because distinct raw inputs produce distinct hashes.

Comment thread src/trace_contribution.rs Outdated
let body_text = read_bounded_trace_upload_claim_response(response, &parsed)
.await
.unwrap_or_default();
let label = parse_trace_upload_claim_error_label(&body_text);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — parse_trace_upload_claim_error_label does not test non-string error field.

The existing test covers empty body, non-JSON, missing error field, and whitespace-only error value, but does not exercise the case where the error field is a non-string type (number, object, array, bool, null). While .as_str() handles these gracefully, explicit coverage would prevent regressions if the parsing logic changes.

Fix: tests::trace_contribution::parse_trace_upload_claim_error_label_returns_none_for_non_string_error covering error field as number, object, array, bool, and null

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecb04b4. Added parse_trace_upload_claim_error_label_returns_none_for_non_string_error covering number, object, array, bool, and null variants of the error field. All return None.

@zmanian
zmanian force-pushed the trace-commons-invite-code branch from 81f3459 to 930e82d Compare May 19, 2026 18:43
@github-actions github-actions Bot added scope: dependencies Dependency updates size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules and removed size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules labels May 19, 2026
@zmanian zmanian changed the title feat(traces): wire pilot invite_code through upload-claim refresh Migrate trace client to ironclaw_reborn and prune to contributor-only CLI May 19, 2026
zmanian added a commit that referenced this pull request May 19, 2026
Three CI failures on PR #3738:

1. cargo fmt --check diff in 15 spots across the traces module and tests.
   Ran cargo fmt --all to canonicalize.

2. reborn_crate_dependency_boundaries_hold + reborn_cli_binary_crate_stays_
   separate_from_v1_root failed because the trace-client extraction added
   ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_cli
   (the preview handler deserializes ironclaw_llm::recording::TraceFile, and
   the contribution dir resolver called ironclaw_common::paths::ironclaw_
   base_dir). The architectural rule is that ironclaw_reborn_cli enters
   Reborn through the doorway crates only.

   Resolution: re-export both surfaces through ironclaw_reborn_traces:
   - ironclaw_reborn_traces::recording::* re-exports ironclaw_llm::recording
   - ironclaw_reborn_traces::paths::* re-exports ironclaw_common::paths
   Drop ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_
   cli. Add ironclaw_reborn_traces to the allowed-doorway set in the
   boundary test, with a doc comment explaining why (traces crate is the
   contributor-side TraceCommons client, analogous to composition + config).

   Final allowed deps of ironclaw_reborn_cli: ironclaw_reborn_composition,
   ironclaw_reborn_config, ironclaw_reborn_traces. No transitive leakage of
   ironclaw_llm / ironclaw_common into the binary's import surface.

Verification:
- RUSTFLAGS="-D warnings" cargo check --workspace --all-targets: clean
- cargo fmt --all -- --check: clean
- cargo test -p ironclaw_architecture --test reborn_dependency_boundaries:
  19 passed
- cargo test -p ironclaw_reborn_cli: 58 passed (invite-code tests included)
- cargo clippy --workspace --all-targets -- -D warnings: clean
zmanian added a commit that referenced this pull request May 19, 2026
- Enum-ify PilotAllowlist refusal labels (Medium-4).
- Hash invite code in upload-claim cache key with sha256: prefix (Low-2).
- show_policy_status treats whitespace-only invite code as not configured (Low-1).
- Write policy.json with 0o600 permissions on unix (Medium-1).
- Add caller-level tests for PilotAllowlist typed and generic error paths (High-1, High-2).
- Add opt_in invite-code persistence test (Medium-2).
- Add queue-status diagnostics invite-code-configured test (Medium-3).
- Add parse_trace_upload_claim_error_label non-string coverage (Low-3).
zmanian added a commit to TraceCommons/trace-commons that referenced this pull request May 20, 2026
Lands the four-PR stack (#124, #125, #126, #127) as a single squash commit on main.

## What's in this squash

**1. `trace-commons-operator-client` foundation crate** (was #124)
- `Client` wrapper around `reqwest::Client` with bearer-token resolution from env, host allowlist enforcement, typed error mapping for `{"error": "<Label>"}` refusals.
- Error taxonomy: `BearerMissing`, `InvalidEndpoint`, `HostNotAllowed`, `Transport`, `HttpFailure`, `ServerLabel`, `MalformedResponse`. `Error::user_diagnostic()` strips query strings + fragments.
- `HostAllowlist` (CSV, exact-match, case-insensitive) sourced from `--allowed-hosts` or `TRACE_COMMONS_ALLOWED_HOSTS`.
- Output helpers: `JsonEnvelope` wrapper for `--json` mode, ASCII table renderer, key-value block renderer.

**2. `trace-commons-review` (8 cmds) + `trace-commons-admin` (12 cmds)** (was #125)
- Single shared bearer per binary (`TRACE_COMMONS_REVIEWER_BEARER`, `TRACE_COMMONS_ADMIN_BEARER`).
- 37 binary tests across both (parse-through, body validation, wiremock round-trips).

**3. `trace-commons-worker` (8 cmds) + `trace-commons-tenant` (11 cmds)** (was #126)
- Worker uses per-subcommand bearer envs (8 distinct env vars, one per route gate). Verified by `worker_per_route_bearer_resolution_uses_distinct_envs`.
- Tenant uses single shared `TRACE_COMMONS_TENANT_BEARER`.
- Operator runbook at `docs/operator/operator-binaries.md`.
- CI smoke job `operator-binaries-smoke` in `.github/workflows/ci.yml`, sibling to `pilot-bootstrap-smoke`.

**4. Privacy-filter-canary port** (was #127)
- New `trace_commons_operator_client::privacy_filter` module: `PrivacyFilterAdapter` trait, `CommandPrivacyFilterAdapter`, `SafePrivacyFilterRedaction`, `adapter_from_env`, `canary_leaked_tokens`. Wire shapes byte-identical to Ironclaw's sidecar protocol.
- Dual-name env-var compat: `TRACE_COMMONS_PRIVACY_FILTER_*` canonical, `IRONCLAW_TRACE_*` legacy fallback with warn-on-use.
- `trace-commons-tenant privacy-filter-canary` stub replaced with the real handler.

## Operator surface added

| Binary | Cmds | Bearer model |
|---|---:|---|
| `trace-commons-review` | 8 | Single `TRACE_COMMONS_REVIEWER_BEARER` |
| `trace-commons-admin` | 12 | Single `TRACE_COMMONS_ADMIN_BEARER` |
| `trace-commons-worker` | 8 | Per-subcommand defaults |
| `trace-commons-tenant` | 11 | Single `TRACE_COMMONS_TENANT_BEARER` |
| **Total** | **39** | |

These mirror the 39 operator commands removed from Ironclaw in nearai/ironclaw#3738.

## Verification

```
cargo check --workspace --all-targets   (-D warnings)   # clean
cargo test --workspace --no-run         (-D warnings)   # clean
cargo clippy --workspace --all-targets  (-D warnings)   # clean
cargo test -p trace-commons-operator-client            # 39 passed
cargo test -p trace-commons-server --bins              # all green
bash scripts/operator/operator-smoke.sh                # SmokeOperatorBinariesOK
```

Superseded PRs (closed): #124, #125, #126.
zmanian added a commit that referenced this pull request May 20, 2026
Three CI failures on PR #3738:

1. cargo fmt --check diff in 15 spots across the traces module and tests.
   Ran cargo fmt --all to canonicalize.

2. reborn_crate_dependency_boundaries_hold + reborn_cli_binary_crate_stays_
   separate_from_v1_root failed because the trace-client extraction added
   ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_cli
   (the preview handler deserializes ironclaw_llm::recording::TraceFile, and
   the contribution dir resolver called ironclaw_common::paths::ironclaw_
   base_dir). The architectural rule is that ironclaw_reborn_cli enters
   Reborn through the doorway crates only.

   Resolution: re-export both surfaces through ironclaw_reborn_traces:
   - ironclaw_reborn_traces::recording::* re-exports ironclaw_llm::recording
   - ironclaw_reborn_traces::paths::* re-exports ironclaw_common::paths
   Drop ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_
   cli. Add ironclaw_reborn_traces to the allowed-doorway set in the
   boundary test, with a doc comment explaining why (traces crate is the
   contributor-side TraceCommons client, analogous to composition + config).

   Final allowed deps of ironclaw_reborn_cli: ironclaw_reborn_composition,
   ironclaw_reborn_config, ironclaw_reborn_traces. No transitive leakage of
   ironclaw_llm / ironclaw_common into the binary's import surface.

Verification:
- RUSTFLAGS="-D warnings" cargo check --workspace --all-targets: clean
- cargo fmt --all -- --check: clean
- cargo test -p ironclaw_architecture --test reborn_dependency_boundaries:
  19 passed
- cargo test -p ironclaw_reborn_cli: 58 passed (invite-code tests included)
- cargo clippy --workspace --all-targets -- -D warnings: clean
zmanian added a commit that referenced this pull request May 20, 2026
- Enum-ify PilotAllowlist refusal labels (Medium-4).
- Hash invite code in upload-claim cache key with sha256: prefix (Low-2).
- show_policy_status treats whitespace-only invite code as not configured (Low-1).
- Write policy.json with 0o600 permissions on unix (Medium-1).
- Add caller-level tests for PilotAllowlist typed and generic error paths (High-1, High-2).
- Add opt_in invite-code persistence test (Medium-2).
- Add queue-status diagnostics invite-code-configured test (Medium-3).
- Add parse_trace_upload_claim_error_label non-string coverage (Low-3).
@zmanian
zmanian force-pushed the trace-commons-invite-code branch from ecb04b4 to 88ff4f8 Compare May 20, 2026 05:22
@zmanian

zmanian commented May 22, 2026

Copy link
Copy Markdown
Collaborator Author

@henrypark133 friendly ping — all 9 findings from your review are addressed in ecb04b4 and consolidated in 88ff4f8 (per-comment replies posted). cargo fmt/clippy/tests all green. Could you take another pass when you have a moment? Thanks!

zmanian added a commit that referenced this pull request May 22, 2026
Three CI failures on PR #3738:

1. cargo fmt --check diff in 15 spots across the traces module and tests.
   Ran cargo fmt --all to canonicalize.

2. reborn_crate_dependency_boundaries_hold + reborn_cli_binary_crate_stays_
   separate_from_v1_root failed because the trace-client extraction added
   ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_cli
   (the preview handler deserializes ironclaw_llm::recording::TraceFile, and
   the contribution dir resolver called ironclaw_common::paths::ironclaw_
   base_dir). The architectural rule is that ironclaw_reborn_cli enters
   Reborn through the doorway crates only.

   Resolution: re-export both surfaces through ironclaw_reborn_traces:
   - ironclaw_reborn_traces::recording::* re-exports ironclaw_llm::recording
   - ironclaw_reborn_traces::paths::* re-exports ironclaw_common::paths
   Drop ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_
   cli. Add ironclaw_reborn_traces to the allowed-doorway set in the
   boundary test, with a doc comment explaining why (traces crate is the
   contributor-side TraceCommons client, analogous to composition + config).

   Final allowed deps of ironclaw_reborn_cli: ironclaw_reborn_composition,
   ironclaw_reborn_config, ironclaw_reborn_traces. No transitive leakage of
   ironclaw_llm / ironclaw_common into the binary's import surface.

Verification:
- RUSTFLAGS="-D warnings" cargo check --workspace --all-targets: clean
- cargo fmt --all -- --check: clean
- cargo test -p ironclaw_architecture --test reborn_dependency_boundaries:
  19 passed
- cargo test -p ironclaw_reborn_cli: 58 passed (invite-code tests included)
- cargo clippy --workspace --all-targets -- -D warnings: clean
zmanian added a commit that referenced this pull request May 22, 2026
- Enum-ify PilotAllowlist refusal labels (Medium-4).
- Hash invite code in upload-claim cache key with sha256: prefix (Low-2).
- show_policy_status treats whitespace-only invite code as not configured (Low-1).
- Write policy.json with 0o600 permissions on unix (Medium-1).
- Add caller-level tests for PilotAllowlist typed and generic error paths (High-1, High-2).
- Add opt_in invite-code persistence test (Medium-2).
- Add queue-status diagnostics invite-code-configured test (Medium-3).
- Add parse_trace_upload_claim_error_label non-string coverage (Low-3).
@zmanian
zmanian force-pushed the trace-commons-invite-code branch from 88ff4f8 to 7f989b7 Compare May 22, 2026 23:15

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single-agent review.

Verdict: APPROVE with follow-up

Approving with follow-up: these Medium/Low items should be addressed in a follow-up PR (not blocking).

The extraction is clean and well-executed: 11,839 lines of src/trace_contribution.rs + 542 lines of src/trace_client.rs are removed from the monolith and re-homed in the new ironclaw_reborn_traces crate. Re-export shims (pub use ironclaw_reborn_traces::...) maintain backward compatibility for the monolith. Architecture boundary tests are updated to lock the dependency surface. No critical or high-confidence findings.


Findings (0 High · 2 Medium · 1 Low)

[Medium] TraceCreditEventKind missing #[serde(rename_all = "snake_case")]

File: crates/ironclaw_reborn_traces/src/contribution.rs (lines 583–598)

TraceCreditEventKind derives Serialize/Deserialize but is the only wire-persisted enum in the file without #[serde(rename_all = "snake_case")]. Every other enum in this file (e.g. TraceChannel, ConsentScope, ResidualPiiRisk, UserFeedback, TaskSuccess, SideEffectLevel, TraceContributionEventType, TraceFailureMode, TraceAllowedUse, TraceRetentionClass, etc.) has the attribute. Without it, variants serialize as PascalCase ("Accepted", "CreditSynced", "UsedForTrainingOrRanking") rather than snake_case ("accepted", "credit_synced", "used_for_training_or_ranking"). This enum is persisted in local credit_events records on disk and round-tripped against server responses — a format mismatch will silently break deserialization of existing local records or server-pushed event syncs once a producer and consumer disagree on case. The .claude/rules/types.md rule explicitly calls this out as a wire-stability requirement. Recommend adding #[serde(rename_all = "snake_case")] (with #[serde(alias = "...")] annotations if any on-disk records already exist with PascalCase values).

Confidence: 88%


[Medium] docs/internal/trace-commons.md still shows ironclaw traces (monolith CLI) after command migration

File: docs/internal/trace-commons.md (line 40 onward)

The PR removes the traces subcommand from the monolith (src/cli/mod.rs) and adds it to the standalone Reborn CLI binary (ironclaw-reborn). The doc examples throughout docs/internal/trace-commons.md still use ironclaw traces opt-in, ironclaw traces preview, etc. These will fail for any reader attempting to follow the examples — the correct command is now ironclaw-reborn traces opt-in. The .claude/rules/review-discipline.md move-only rule explicitly requires updating .md references to moved paths in the same PR. The doc update was not included here.

Confidence: 92%


[Low] shared.rs uses #![allow(unused_imports)] to suppress a live warning

File: crates/ironclaw_reborn_cli/src/commands/traces/shared.rs

The file exists as a forward-compat re-export layer (pub(super) use super::{...}) to document the shared surface and is otherwise empty of behavior. The #![allow(unused_imports)] suppresses an active warning because the audience modules use super::* and don't import from shared directly. This is a minor convention issue — the project's CLAUDE.md policy is zero clippy warnings. Consider either removing the file until it has distinct callers, or documenting the exemption with an arch-exempt: ... annotation per .claude/rules/architecture.md style. Not blocking.

Confidence: 72%


Positive observations

  • Extraction is structurally clean. The re-export shims (src/trace_client.rs, src/trace_contribution.rs, src/tools/redaction.rs) preserve backward-compat with zero call-site churn in the monolith.
  • Security controls intact. JWT validation (validate_trace_upload_claim_response), SSRF guards (validate_trace_upload_claim_issuer_url, resolve_trace_upload_claim_issuer_host), redirect blocking, and bounded response reading are all present and correct in the new crate.
  • Architecture boundary test updated. reborn_cli_binary_crate_stays_separate_from_v1_root now asserts ironclaw_reborn_traces as an allowed edge in the strict allowlist, and the new crate's registration in workspace.members is validated by reborn_boundary_rules_active_crates_are_workspace_members.
  • Privacy sidecar process isolation. Subprocess spawning clears the environment except PATH/LANG/LC_ALL, uses bounded stdout/stderr reads, and applies a configurable timeout — good defense-in-depth for the optional privacy filter.
  • Test coverage. The move carries the full test suite into the new crate (client.rs unit tests, tests.rs CLI parse + queue preflight tests). New invite-code tests specifically cover round-trip serde persistence of upload_token_invite_code.
  • Consent flag enforcement. preflight_cli_trace_envelope_upload checks that the envelope's message_text_included / tool_payloads_included flags are consistent with the standing policy before any disk write, and tests confirm rejection-before-write.

Review produced by single-agent code-review-skill:v1. Lenses applied: security, bugs, performance, tests, conventions, design.

zmanian added 9 commits May 22, 2026 20:55
Move trace_contribution, trace_client, and tools/redaction into a new
workspace crate so the standalone ironclaw_reborn_cli can consume them
without depending on the monolith. The legacy src/trace_*.rs and
src/tools/redaction.rs files remain as thin re-export shims so all
existing call sites in the monolith continue to compile unchanged.

ConversationMessage is now defined in the new crate; src/history/store.rs
re-exports it so crate::history::ConversationMessage and the new
ironclaw_reborn_traces::ConversationMessage are the same type. The single
crate::bootstrap::ironclaw_base_dir() call inside contribution.rs is
rewritten to call ironclaw_common::paths::ironclaw_base_dir directly.
Wire ironclaw_reborn_traces into the standalone reborn CLI and expose a
minimal 'traces' subcommand with opt-in, status, and queue-status. The
opt-in path prints a hand-off message pointing at the legacy ironclaw
binary; status and queue-status call into ironclaw_reborn_traces using
the anonymous scope so the wiring is exercised at compile time. The full
TraceCommons CLI port lands separately.
Companion to TraceCommons/trace-commons#109 — the trace-commons
server-side invite-code allowlist landed there with refusal labels
PilotAllowlist* (NotMatched 403, InviteCodeMissing 400, Stale 503,
Malformed 503). This commit teaches the Ironclaw trace client three
things:

1. Carry the invite code through the standing policy.
   - StandingTraceContributionPolicy gains
     upload_token_invite_code: Option<String>, serde-default + skip-if-
     none so existing policy files keep parsing byte-identical.
   - TraceUploadClaimIssuerRequest body gains an optional invite_code
     field (forward-compat for a future server slice that may read it
     from the body in addition to the workload-JWT claim).
   - trace_upload_claim_cache_key keys on the invite code so a mid-
     pilot rotation forces a fresh claim mint.

2. Surface the typed allowlist refusal labels.
   fetch_trace_upload_claim_from_issuer now parses {"error": "<Label>"}
   bodies on non-success status and maps each PilotAllowlist* label to
   a user-actionable diagnostic.

3. CLI flag + status surface.
   ironclaw traces opt-in --upload-token-invite-code <CODE> (off by
   default). ironclaw traces status and queue-status report
   "pilot invite code: configured / not configured" — never echo the
   raw code.

After the trace-client extraction (5e568fc + 23104c9), the trace
contribution code lives in crates/ironclaw_reborn_traces. The invite-
code wiring lands on the extracted paths.

Seven new unit tests, all green:
- standing_policy_serde_back_compat_when_invite_code_missing
- standing_policy_serde_round_trips_invite_code_when_set
- standing_policy_serde_omits_invite_code_when_none
- cache_key_distinguishes_different_invite_codes
- parse_trace_upload_claim_error_label_handles_known_shapes
- opt_in_invite_code_flag_parses_through_cli
- opt_in_invite_code_defaults_to_none_when_absent
Replaces the stub committed in 23104c9 with the full 50-subcommand
implementation moved wholesale from src/cli/traces.rs. The monolith's
ironclaw binary loses its traces subcommand; ironclaw-reborn traces ...
becomes the only path. The library code remains in ironclaw_reborn_traces.

Breaking CLI change: users invoking `ironclaw traces ...` must switch
to `ironclaw-reborn traces ...`. Operator runbooks (pilot bootstrap,
HF cache hygiene, GPU cost ledger) reference the legacy binary name
and will need updating in a follow-up doc PR.
Architectural pivot: Ironclaw's trace CLI holds only contributor-facing
commands (consent, upload, credit display, monitoring). Operator
commands (reviewer, worker, admin, tenant, audit) move out of Ironclaw
entirely — they'll re-emerge as separate binaries in the trace-commons-
server repo in a follow-up PR.

Deleted 39 variants from TracesSubcommand and everything supporting
them: handler fns, option structs, clap value enums, audience modules
(reviewer.rs/worker.rs/admin.rs/tenant.rs), and tests.

Kept 12 contributor variants: OptIn, OptOut, Status, Preview, Enqueue,
FlushQueue, QueueStatus, Credit, Submit, ListSubmissions, Revoke,
IngestHealth. All 7 invite-code tests continue to pass.
Three CI failures on PR #3738:

1. cargo fmt --check diff in 15 spots across the traces module and tests.
   Ran cargo fmt --all to canonicalize.

2. reborn_crate_dependency_boundaries_hold + reborn_cli_binary_crate_stays_
   separate_from_v1_root failed because the trace-client extraction added
   ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_cli
   (the preview handler deserializes ironclaw_llm::recording::TraceFile, and
   the contribution dir resolver called ironclaw_common::paths::ironclaw_
   base_dir). The architectural rule is that ironclaw_reborn_cli enters
   Reborn through the doorway crates only.

   Resolution: re-export both surfaces through ironclaw_reborn_traces:
   - ironclaw_reborn_traces::recording::* re-exports ironclaw_llm::recording
   - ironclaw_reborn_traces::paths::* re-exports ironclaw_common::paths
   Drop ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_
   cli. Add ironclaw_reborn_traces to the allowed-doorway set in the
   boundary test, with a doc comment explaining why (traces crate is the
   contributor-side TraceCommons client, analogous to composition + config).

   Final allowed deps of ironclaw_reborn_cli: ironclaw_reborn_composition,
   ironclaw_reborn_config, ironclaw_reborn_traces. No transitive leakage of
   ironclaw_llm / ironclaw_common into the binary's import surface.

Verification:
- RUSTFLAGS="-D warnings" cargo check --workspace --all-targets: clean
- cargo fmt --all -- --check: clean
- cargo test -p ironclaw_architecture --test reborn_dependency_boundaries:
  19 passed
- cargo test -p ironclaw_reborn_cli: 58 passed (invite-code tests included)
- cargo clippy --workspace --all-targets -- -D warnings: clean
- Enum-ify PilotAllowlist refusal labels (Medium-4).
- Hash invite code in upload-claim cache key with sha256: prefix (Low-2).
- show_policy_status treats whitespace-only invite code as not configured (Low-1).
- Write policy.json with 0o600 permissions on unix (Medium-1).
- Add caller-level tests for PilotAllowlist typed and generic error paths (High-1, High-2).
- Add opt_in invite-code persistence test (Medium-2).
- Add queue-status diagnostics invite-code-configured test (Medium-3).
- Add parse_trace_upload_claim_error_label non-string coverage (Low-3).
@zmanian
zmanian force-pushed the trace-commons-invite-code branch from 7f989b7 to 3632415 Compare May 23, 2026 04:07
@zmanian
zmanian merged commit 2a90b6b into reborn-integration May 23, 2026
14 checks passed
@zmanian
zmanian deleted the trace-commons-invite-code branch May 23, 2026 04:14
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
… CLI (nearai#3738)

* extract trace client into ironclaw_reborn_traces crate

Move trace_contribution, trace_client, and tools/redaction into a new
workspace crate so the standalone ironclaw_reborn_cli can consume them
without depending on the monolith. The legacy src/trace_*.rs and
src/tools/redaction.rs files remain as thin re-export shims so all
existing call sites in the monolith continue to compile unchanged.

ConversationMessage is now defined in the new crate; src/history/store.rs
re-exports it so crate::history::ConversationMessage and the new
ironclaw_reborn_traces::ConversationMessage are the same type. The single
crate::bootstrap::ironclaw_base_dir() call inside contribution.rs is
rewritten to call ironclaw_common::paths::ironclaw_base_dir directly.

* add traces subcommand stub to ironclaw_reborn_cli

Wire ironclaw_reborn_traces into the standalone reborn CLI and expose a
minimal 'traces' subcommand with opt-in, status, and queue-status. The
opt-in path prints a hand-off message pointing at the legacy ironclaw
binary; status and queue-status call into ironclaw_reborn_traces using
the anonymous scope so the wiring is exercised at compile time. The full
TraceCommons CLI port lands separately.

* wire pilot invite_code through upload-claim refresh

Companion to TraceCommons/trace-commons#109 — the trace-commons
server-side invite-code allowlist landed there with refusal labels
PilotAllowlist* (NotMatched 403, InviteCodeMissing 400, Stale 503,
Malformed 503). This commit teaches the Ironclaw trace client three
things:

1. Carry the invite code through the standing policy.
   - StandingTraceContributionPolicy gains
     upload_token_invite_code: Option<String>, serde-default + skip-if-
     none so existing policy files keep parsing byte-identical.
   - TraceUploadClaimIssuerRequest body gains an optional invite_code
     field (forward-compat for a future server slice that may read it
     from the body in addition to the workload-JWT claim).
   - trace_upload_claim_cache_key keys on the invite code so a mid-
     pilot rotation forces a fresh claim mint.

2. Surface the typed allowlist refusal labels.
   fetch_trace_upload_claim_from_issuer now parses {"error": "<Label>"}
   bodies on non-success status and maps each PilotAllowlist* label to
   a user-actionable diagnostic.

3. CLI flag + status surface.
   ironclaw traces opt-in --upload-token-invite-code <CODE> (off by
   default). ironclaw traces status and queue-status report
   "pilot invite code: configured / not configured" — never echo the
   raw code.

After the trace-client extraction (5e568fc + 23104c9), the trace
contribution code lives in crates/ironclaw_reborn_traces. The invite-
code wiring lands on the extracted paths.

Seven new unit tests, all green:
- standing_policy_serde_back_compat_when_invite_code_missing
- standing_policy_serde_round_trips_invite_code_when_set
- standing_policy_serde_omits_invite_code_when_none
- cache_key_distinguishes_different_invite_codes
- parse_trace_upload_claim_error_label_handles_known_shapes
- opt_in_invite_code_flag_parses_through_cli
- opt_in_invite_code_defaults_to_none_when_absent

* migrate ironclaw traces CLI surface into ironclaw_reborn_cli

Replaces the stub committed in 23104c9 with the full 50-subcommand
implementation moved wholesale from src/cli/traces.rs. The monolith's
ironclaw binary loses its traces subcommand; ironclaw-reborn traces ...
becomes the only path. The library code remains in ironclaw_reborn_traces.

Breaking CLI change: users invoking `ironclaw traces ...` must switch
to `ironclaw-reborn traces ...`. Operator runbooks (pilot bootstrap,
HF cache hygiene, GPU cost ledger) reference the legacy binary name
and will need updating in a follow-up doc PR.

* split traces CLI by audience into module tree

* extract trace CLI tests into sibling tests.rs

* prune operator commands from Ironclaw trace CLI

Architectural pivot: Ironclaw's trace CLI holds only contributor-facing
commands (consent, upload, credit display, monitoring). Operator
commands (reviewer, worker, admin, tenant, audit) move out of Ironclaw
entirely — they'll re-emerge as separate binaries in the trace-commons-
server repo in a follow-up PR.

Deleted 39 variants from TracesSubcommand and everything supporting
them: handler fns, option structs, clap value enums, audience modules
(reviewer.rs/worker.rs/admin.rs/tenant.rs), and tests.

Kept 12 contributor variants: OptIn, OptOut, Status, Preview, Enqueue,
FlushQueue, QueueStatus, Credit, Submit, ListSubmissions, Revoke,
IngestHealth. All 7 invite-code tests continue to pass.

* fix CI: cargo fmt + restore reborn-cli dep boundary

Three CI failures on PR nearai#3738:

1. cargo fmt --check diff in 15 spots across the traces module and tests.
   Ran cargo fmt --all to canonicalize.

2. reborn_crate_dependency_boundaries_hold + reborn_cli_binary_crate_stays_
   separate_from_v1_root failed because the trace-client extraction added
   ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_cli
   (the preview handler deserializes ironclaw_llm::recording::TraceFile, and
   the contribution dir resolver called ironclaw_common::paths::ironclaw_
   base_dir). The architectural rule is that ironclaw_reborn_cli enters
   Reborn through the doorway crates only.

   Resolution: re-export both surfaces through ironclaw_reborn_traces:
   - ironclaw_reborn_traces::recording::* re-exports ironclaw_llm::recording
   - ironclaw_reborn_traces::paths::* re-exports ironclaw_common::paths
   Drop ironclaw_llm and ironclaw_common as direct deps of ironclaw_reborn_
   cli. Add ironclaw_reborn_traces to the allowed-doorway set in the
   boundary test, with a doc comment explaining why (traces crate is the
   contributor-side TraceCommons client, analogous to composition + config).

   Final allowed deps of ironclaw_reborn_cli: ironclaw_reborn_composition,
   ironclaw_reborn_config, ironclaw_reborn_traces. No transitive leakage of
   ironclaw_llm / ironclaw_common into the binary's import surface.

Verification:
- RUSTFLAGS="-D warnings" cargo check --workspace --all-targets: clean
- cargo fmt --all -- --check: clean
- cargo test -p ironclaw_architecture --test reborn_dependency_boundaries:
  19 passed
- cargo test -p ironclaw_reborn_cli: 58 passed (invite-code tests included)
- cargo clippy --workspace --all-targets -- -D warnings: clean

* address PR nearai#3738 review feedback

- Enum-ify PilotAllowlist refusal labels (Medium-4).
- Hash invite code in upload-claim cache key with sha256: prefix (Low-2).
- show_policy_status treats whitespace-only invite code as not configured (Low-1).
- Write policy.json with 0o600 permissions on unix (Medium-1).
- Add caller-level tests for PilotAllowlist typed and generic error paths (High-1, High-2).
- Add opt_in invite-code persistence test (Medium-2).
- Add queue-status diagnostics invite-code-configured test (Medium-3).
- Add parse_trace_upload_claim_error_label non-string coverage (Low-3).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: channel/cli TUI / CLI channel scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants