Repository navigation
arch(reborn): futureproof RebornRuntime surface for epic #3036 (Configuration-as-Code) - #3703
serrrfirat wants to merge 4 commits into
Conversation
Splits the previously flat `runtime_input.rs` (api_version-less, with only `services + llm + runner` fields) into a versioned input contract that anticipates the layers epic [#3036](#3036) ("Configuration-as-Code for IronClaw Reborn: tenant blueprints and use-case harnesses") will write to the runtime. The shape change is **additive** — every previously-supported invocation of `RebornRuntimeInput::from_services(svc).with_llm(llm).with_runner_ settings(s)` still compiles unchanged — and **forward-compatible**: identity, policy, drivers, and harness slots are now first-class fields with safe CLI-shaped defaults today, so blueprint apply / harness activation can land in epic sub-issues without breaking the input contract. New DTOs (composition-owned, no escaping ironclaw_reborn / ironclaw_llm types): - RebornRuntimeApiVersion (V1) — schema stamp. Composition rejects different majors fail-closed. Mirrors the `api_version = "ironclaw.config/v1"` discipline epic #3036 puts on declarative blueprints. - RebornIdentityConfig { tenant, default_agent, default_owner, default_project } — replaces the hardcoded "reborn-cli" strings that lived inside build_reborn_runtime. `cli_default()` ships the safe single-tenant local-dev shape. Once epic #3036's tenant repo lands, this DTO will be sourced from RebornTenantRepo instead. - ConversationIdentity — per-conversation override consumed by `new_conversation_for` (added in a sibling commit). Any field `None` falls back to the runtime's default identity. - RebornPolicyConfig { deployment_mode, default_profile, default_approval_policy } — uses existing `ironclaw_host_api::runtime_policy::{DeploymentMode, RuntimeProfile, ApprovalPolicy}`. Values are recorded and logged at boot today (`build_reborn_runtime` emits a tracing::debug snapshot); per-request enforcement lands with RuntimePolicyRepo in epic #3036. - RebornDriverConfig { default, additional } over RebornDriverChoice::{TextOnly, Planned}. Today's composition wires only TextOnly end-to-end; selecting Planned as default returns a clear `InvalidArgument` from `build_reborn_runtime` rather than silently falling back. The DTO ships now so operator-facing config (and the future blueprint TOML) has a typed driver-selection surface and the `requested_run_profile` per-turn mechanism (added in a sibling commit) has a registry to look up against. - RebornHarnessSelection { harness_id: RebornHarnessId } — forward- compat slot for the harness substrate (HarnessRepo, HarnessActivationService, capability-surface filter, instruction- bundle overlay) defined by epic #3036's HarnessManifest. Composition today logs the harness id once at boot and proceeds without an overlay; the same field starts driving activation when the substrate lands. RebornHarnessId carries the same kebab-case rules as v1 PackageId so a future migration to a typed `ironclaw_harness::HarnessId` is a re-export, not a value transformation. Compatibility wiring inside build_reborn_runtime: - api_version is checked against the composition's compiled RebornRuntimeApiVersion::current(); mismatch returns RebornRuntimeError::IncompatibleApiVersion. - drivers.default == Planned returns InvalidArgument today (recognized name, not yet wired body). - harness selection logs at WARN and proceeds. - identity drives the default ThreadScope built inside the runtime, replacing the previous hardcoded "reborn-cli" tenant/agent strings. Tests (unit, in submodules): - api_version: V1 is current, major mismatch fails closed, wire format "ironclaw.runtime/vM.N" is stable. - identity: cli_default() produces the expected stable scope; ConversationIdentity builder chain composes. - policy: cli_default() returns local-single-user / LocalDev / AskDestructive; builders compose. - drivers: text_only_only() is the no-extras shape; with_additional dedupes against the default and itself. - harness: RebornHarnessId validation rejects empty, too long, non-lowercase-leading, and invalid characters; serde round-trips.
…cope for epic #3036 Epic [#3036](#3036) ("Configuration-as-Code") introduces operations that are fundamentally privileged effects: blueprint apply writes to typed settings / skill / mission / project repos; harness install / activate / deactivate manipulate the typed harness repo and bump capability-surface version. The epic explicitly calls out: > Apply must be admin-scoped for anything beyond `scope = { user = > self }`. Reuse AdminScope::new from src/tenant.rs; do not invent a > new privilege gate. This commit lands the *shape* of that admin gate in the standalone Reborn composition root so the substrate work can land incrementally without re-deciding the privilege model in each sub-issue. RebornAdminScope (sealed): - Carries a human-readable audit_reason recorded against every admin write under this scope. - Sealed via a private SealedMarker field. The only public constructor today is `for_cli_admin(audit_reason)`; remote / authenticated admin paths get their own constructor when the privilege/auth crate from epic #3036 lands (will add a `for_authenticated_admin(credential, audit_reason)` then). - Boundary test pins the sealed property (reborn_runtime_surface_is_futureproof_for_epic_3036). RebornAdminClient: - Constructed via `RebornRuntime::admin(scope)` (added in a sibling commit). Outside crates cannot construct one directly — `pub(crate) fn new(...)` keeps the constructor inside ironclaw_reborn_composition. - Exposes typed async methods for every privileged op epic #3036 lists: apply_blueprint, diff_blueprint, install_harness, activate_harness, deactivate_harness, list_harnesses. - Each method returns RebornAdminError::NotYetWired today with precise pointers — operation name, tracking issue "#3036", and the specific substrate it needs ("BlueprintParser + BlueprintApplyService + typed repos (Settings, Skill, Mission, Project)", etc.). Operators hitting the error get a precise next-step pointer instead of "feature unimplemented". Typed DTOs reserved for the operator surface (today opaque, take effect when the substrate lands): - ApplyBlueprintRequest / DiffBlueprintRequest with BlueprintSource::{Path, GitUrl}, ApplyMode::{DryRun, Apply}, optional lockfile path. - ApplyReport / ApplyChange / ApplyChangeAction matching the `Change { domain, key, action, before_hash, after_hash }` shape epic #3036 specifies. - InstallHarnessRequest, ActivateHarnessRequest, DeactivateHarnessRequest, HarnessActivationScope::{Thread, Project, Tenant} matching the activation-scope vocabulary from the epic. - HarnessDescriptor for `list_harnesses` (id, installed_revision, active_for_threads). RebornAdminError variants: - NotYetWired (today's surface; carries operation + tracking issue + required substrate). - Unauthorized (reserved — surfaces once the privilege/auth crate lands and admin scope construction grows a credential parameter). - Repo (reserved — surfaces once the typed repos exist and transactional writes can fail). Unit tests: - admin_scope_carries_audit_reason: round-trips the audit string. - every_admin_method_returns_not_yet_wired: ensures every published method surfaces the documented stub error so a regression that quietly turns one into a panic / silent success fails fast.
Epic [#3036](#3036) lists the operator CLI surface its substrate eventually drives: ironclaw config apply <path|git-url> [--dry-run] [--lockfile] ironclaw config diff <path|git-url> ironclaw config watch <git-url> [--interval-secs] [--require-signature] ironclaw harness install <path|git-url> ironclaw harness list [--json] ironclaw harness activate <id> --thread|--project|--tenant <scope> ironclaw harness deactivate --thread|--project|--tenant <scope> This commit lands every one of those as a compiling, discoverable, shell-completable subcommand in `ironclaw-reborn` today. Each is a "stub" — it parses its full argument shape, validates types, then fails with a structured error that names the operation, the tracking issue (#3036), and the specific substrate it needs. Why lock the surface now (not when the substrate lands): 1. Shell completion / operator muscle memory: scripted tooling that discovers commands via `--help` or completion scripts sees the final command tree from day one. No "the command appeared in v0.X but didn't exist in v0.(X-1)" surprises. 2. Argument shape is contractual: --dry-run, --lockfile, --git, --require-signature, --thread/--project/--tenant, --interval-secs are all decided here, on the PR that introduces the stubs, not six PRs later when implementation pressure tempts shortcuts. 3. Forces the runtime-side admin contract to be designed up front: RebornAdminClient methods exist with their final signatures (sibling commit) because the CLI calls them. The substrate epic sub-issues just fill in bodies. Structure: crates/ironclaw_reborn_cli/src/commands/ config/ # was flat config.rs mod.rs # dispatches to subcommands apply.rs # stub diff.rs # stub watch.rs # stub harness/ # new mod.rs install.rs # stub list.rs # stub activate.rs # stub deactivate.rs # stub stubs.rs # shared "not yet wired" output # helper -- one format across all # stubs so operators / scripts see # a uniform shape (operation name, # tracking issue, required substrate) Behavior on invocation today: $ ironclaw-reborn config apply --dry-run /tmp/x ironclaw-reborn: command `config.apply` is not yet wired tracking issue : #3036 requires : BlueprintParser + BlueprintApplyService + typed repos (Settings, Skill, Mission, Project) The command shape (arguments, output format) is locked from epic #3036; the substrate that makes it execute lands incrementally via the issue's sub-issues. Re-run after the relevant sub-issue ships. (exit code 1) Smoke tests added (7 new tests, all passing): - help_lists_harness_subcommand_for_epic_3036 - config_apply_is_a_discoverable_stub - config_diff_is_a_discoverable_stub - config_watch_is_a_discoverable_stub - harness_install_is_a_discoverable_stub - harness_list_is_a_discoverable_stub - harness_activate_is_a_discoverable_stub - harness_deactivate_is_a_discoverable_stub All existing 35 CLI smoke tests continue to pass.
Adds `reborn_runtime_surface_is_futureproof_for_epic_3036` to the
Reborn dependency-boundary suite. Locks three properties:
1. RebornRuntimeInput carries the explicit epic-3036 fields:
api_version, identity, policy, drivers, harness. A regression
that drops any of these (or removes the version stamp) fails
the test alongside a precise message naming the missing field
and the back-pointer to this rule.
2. RebornAdminClient + RebornAdminScope + RebornAdminError exist
on `ironclaw_reborn_composition`'s public surface, with every
admin method epic #3036 specifies present as a typed async fn:
apply_blueprint, diff_blueprint, install_harness,
activate_harness, deactivate_harness, list_harnesses. The test
reads the source file to assert presence (cheap and resilient;
a smarter macro reflection isn't needed for a contract this
stable).
3. RebornAdminScope remains *sealed*: it carries a private
SealedMarker field and outside crates cannot construct one
without going through the published constructors. A regression
that adds a third unconstrained `pub fn from_anything()` trips
this assertion. The only legitimate public constructor today
is `for_cli_admin(audit_reason)`; remote admin paths will
gain their own constructor when the privilege/auth crate from
epic #3036 lands.
4. The CLI command tree contains the operator-facing stub files
the epic enumerates:
config/{mod, apply, diff, watch}.rs
harness/{mod, install, list, activate, deactivate}.rs
stubs.rs
Removing any of these re-opens the operator-surface instability
problem the stubs exist to prevent.
There was a problem hiding this comment.
Code Review
This pull request implements the foundational infrastructure for epic #3036, introducing a versioned input contract for the Reborn runtime, a privileged admin surface for configuration and harness management, and stubbed CLI commands for blueprint application and harness lifecycle operations. The review feedback highlights several areas for improvement: the need to use Weak references in the conversation registry to prevent memory leaks, and the necessity of enforcing mutual exclusivity for CLI arguments using clap's conflict attributes in several command definitions.
| conversations: tokio::sync::RwLock< | ||
| std::collections::HashMap<ThreadId, ConversationContext>, | ||
| >, |
There was a problem hiding this comment.
The conversations map is an unbounded HashMap that grows with every new conversation. To prevent memory leaks in long-running processes, store Weak references to the resources and prune entries where the Weak reference can no longer be upgraded on access, as per the repository's registry management guidelines.
References
- When using a static HashMap as a registry for shared resources like locks, store Weak references to the resources to prevent memory leaks. On access, prune entries where the Weak reference can no longer be upgraded.
| #[arg(value_name = "PATH", required_unless_present = "git")] | ||
| pub path: Option<PathBuf>, | ||
|
|
||
| /// Git URL of a blueprint repository. Mutually exclusive with `PATH`. | ||
| #[arg(long = "git", value_name = "URL")] | ||
| pub git: Option<String>, |
There was a problem hiding this comment.
The path and git arguments are documented as mutually exclusive, but this is not enforced by clap. While required_unless_present ensures at least one is provided, it does not prevent both from being specified simultaneously. Use conflicts_with to enforce mutual exclusivity at the argument level.
| #[arg(value_name = "PATH", required_unless_present = "git")] | |
| pub path: Option<PathBuf>, | |
| /// Git URL of a blueprint repository. Mutually exclusive with `PATH`. | |
| #[arg(long = "git", value_name = "URL")] | |
| pub git: Option<String>, | |
| #[arg(value_name = "PATH", required_unless_present = "git", conflicts_with = "git")] | |
| pub path: Option<PathBuf>, | |
| /// Git URL of a blueprint repository. Mutually exclusive with PATH. | |
| #[arg(long = "git", value_name = "URL", conflicts_with = "path")] | |
| pub git: Option<String>, |
| #[arg(long = "thread", value_name = "THREAD_ID")] | ||
| pub thread: Option<String>, | ||
|
|
||
| /// Activate on a project. Mutually exclusive with `--thread` and | ||
| /// `--tenant`. | ||
| #[arg(long = "project", value_name = "PROJECT_ID")] | ||
| pub project: Option<String>, | ||
|
|
||
| /// Activate on a tenant. Mutually exclusive with `--thread` and | ||
| /// `--project`. Tenant-wide activation requires admin scope. | ||
| #[arg(long = "tenant", value_name = "TENANT_ID")] | ||
| pub tenant: Option<String>, |
There was a problem hiding this comment.
The --thread, --project, and --tenant arguments are documented as mutually exclusive, but this is not enforced by clap. Use conflicts_with_all to ensure only one scope is specified at a time, preventing ambiguous activation requests.
| #[arg(long = "thread", value_name = "THREAD_ID")] | |
| pub thread: Option<String>, | |
| /// Activate on a project. Mutually exclusive with `--thread` and | |
| /// `--tenant`. | |
| #[arg(long = "project", value_name = "PROJECT_ID")] | |
| pub project: Option<String>, | |
| /// Activate on a tenant. Mutually exclusive with `--thread` and | |
| /// `--project`. Tenant-wide activation requires admin scope. | |
| #[arg(long = "tenant", value_name = "TENANT_ID")] | |
| pub tenant: Option<String>, | |
| #[arg(long = "thread", value_name = "THREAD_ID", conflicts_with_all = ["project", "tenant"])] | |
| pub thread: Option<String>, | |
| /// Activate on a project. Mutually exclusive with --thread and | |
| /// --tenant. | |
| #[arg(long = "project", value_name = "PROJECT_ID", conflicts_with_all = ["thread", "tenant"])] | |
| pub project: Option<String>, | |
| /// Activate on a tenant. Mutually exclusive with --thread and | |
| /// --project. Tenant-wide activation requires admin scope. | |
| #[arg(long = "tenant", value_name = "TENANT_ID", conflicts_with_all = ["thread", "project"])] | |
| pub tenant: Option<String>, |
| pub(crate) struct HarnessDeactivateCommand { | ||
| /// Deactivate on a thread. | ||
| #[arg(long = "thread", value_name = "THREAD_ID")] | ||
| pub thread: Option<String>, | ||
|
|
||
| /// Deactivate on a project. | ||
| #[arg(long = "project", value_name = "PROJECT_ID")] | ||
| pub project: Option<String>, | ||
|
|
||
| /// Deactivate on a tenant. | ||
| #[arg(long = "tenant", value_name = "TENANT_ID")] | ||
| pub tenant: Option<String>, |
There was a problem hiding this comment.
The --thread, --project, and --tenant arguments should be mutually exclusive for deactivation as well to ensure the command intent is clear. Use conflicts_with_all to enforce this constraint in clap.
#[arg(long = "thread", value_name = "THREAD_ID", conflicts_with_all = ["project", "tenant"])]
pub thread: Option<String>,
/// Deactivate on a project.
#[arg(long = "project", value_name = "PROJECT_ID", conflicts_with_all = ["thread", "tenant"])]
pub project: Option<String>,
/// Deactivate on a tenant.
#[arg(long = "tenant", value_name = "TENANT_ID", conflicts_with_all = ["thread", "project"])]
pub tenant: Option<String>,
Code ReviewOverviewForward-architecture PR that reshapes +2124 / −182, 23 files. Stacked on #3695. Strengths
Issues / Suggestions1.
|
zmanian
left a comment
There was a problem hiding this comment.
Thanks for the detailed write-up. The narrative is clear and I appreciate the table mapping epic #3036 concepts to today's seams. That said, I want to push back hard on the framing. "Futureproofing" is exactly the kind of work I'm most skeptical of, and the PR-as-shipped bakes in a lot of shape we may regret.
What is load-bearing today and worth keeping
RebornIdentityConfig+ConversationIdentity+new_conversation_for(...)+ theconversationsHashMap inRebornRuntime. This is real, solves a real problem (the old code hardcodedtenant_id = "reborn-cli"and shared oneThreadScopeacross all conversations), and the refactor ofsend_user_messageto look up per-conversation context is a genuine fix, not speculation. Keep this.- The deletion of
actor_user_idandthread_scopefields onRebornRuntimein favor of identity-driven scope construction is also good — that was a latent multi-tenant bug.
Cut those out and ship them on their own. The rest concerns me.
Speculative surface I'd push back on
-
RebornHarnessSelection/RebornHarnessId/SendMessageOptions::harness_override. Zero consumers today.build_reborn_runtimelogs awarn!and ignores it.send_user_message_with_optionslogs awarn!and ignores it. We are committing to (a) the harness-id stringly-typed namespace, (b) per-message override being the activation granularity (vs. per-thread or per-tenant), and (c) the name "harness" — all before the harness substrate exists and can talk back about what shape it actually wants. When sub-issue 4 from #3036 lands, the harness service will have opinions about activation scope that may not match this DTO. Better to add this field when the substrate lands and we know the shape, in one PR. Removing a no-op field later is cheap; reshaping a public field with consumers is expensive. -
RebornDriverChoice::Planned. This is a variant that, if selected, returnsErr(InvalidArgument)with a "not yet wired" message. So the only valid value today isTextOnly. AVec<RebornDriverChoice>ofadditionaldrivers with one possible value, plus adefaultfield, is solving zero problems today. When planned driver lands (#3651 — a separate slice), it brings its own host wiring requirements that may or may not fit this DTO. DropRebornDriverChoice::PlannedandRebornDriverConfigentirely; add a driver-config DTO at the point planned-driver actually wires. -
RebornPolicyConfig. All three fields (deployment_mode,default_profile,default_approval_policy) hitlet _ = (&policy, &drivers);and atracing::debug!. Nothing reads them at request time. The comment honestly admits "field-level enforcement is downstream work." Same argument: add the input when there's a consumer. Today this is shape-bikeshedding without a tiebreaker — theRuntimePolicyRepolookup path doesn't exist, so we're guessing what input it wants. -
RebornAdminClient+ every DTO under it. Six async methods, all returningErr(NotYetWired { tracking_issue: "#3036" }).ApplyBlueprintRequest,DiffBlueprintRequest,InstallHarnessRequest,ActivateHarnessRequest,DeactivateHarnessRequest,HarnessActivationScope,HarnessDescriptor,ApplyMode,ApplyReport,ApplyChange,ApplyChangeAction,BlueprintSource— every one of these is exported, every one is a guess at a shape the substrate will dictate. The PR description acknowledges this: "the parsed-blueprint DTO will live in the futureironclaw_blueprintcrate ... For now this is a path/url placeholder so the admin surface already takes a typed argument and won't break when the parser lands." That's not stability, that's saving on one rename in a single PR while paying for it with ~400 lines of dead public API right now. -
RebornAdminScopesealing. I like the direction — privilege gating belongs here — but theSealedMarkermechanism does nothing today. Any caller with the type in scope can callRebornAdminScope::for_cli_admin("whatever"). The stringaudit_reasonhas no enforcement. The seal only prevents non-pubfield construction, and the onlypubconstructor accepts any string. This is the ergonomic shape of a privilege gate without the actual gating. When the auth crate lands, the constructor changes anyway, so this is essentially comment-driven design. Either ship it with real auth (credential parameter onfor_cli_admin, even if the only valid credential today is "I'm the CLI binary") or wait. -
reborn_runtime_surface_is_futureproof_for_epic_3036boundary test. This test reads source files as strings andassert!(contains(...))on struct field names and the existence of CLI stub files. I understand the intent — pin the surface so a contributor can't drift back to implicit shape — but it pins today's guess at the surface, and it pins it by string match on source. Any reshape (which sub-issue 2/3/4 may demand) trips this test and the contributor either modifies the test (rendering it ceremonial) or argues with it. A test like this only makes sense when the shape is settled by something external (an RFC, a wire format, an upstream API). Epic #3036 sub-issues haven't been written; the shape isn't settled. Drop this test or scope it tightly to the load-bearing identity layer. -
The CLI stub commands. I'm more sympathetic here —
harness install/list/activate/deactivate+config apply/diff/watchas discoverable stubs that point operators to the tracking issue is reasonable UX. But: shell completion as justification is thin (operator muscle memory has nothing to anchor on yet either), and locking the flag shape of these commands (--thread/--project/--tenant,--require-signature,--interval-secs) is the same kind of guess as #4. If you keep the stubs, drop the flags and accept positional args + free-form ones until the substrate constrains them.
On the "concentrate the reshape" argument
The PR description argues this is cheaper than five future breaking changes. I'd argue the opposite. Today's PR commits to specific DTO shapes for 5+ subsystems whose substrate doesn't exist. Each substrate sub-issue will discover real constraints — what the parser actually returns, what the harness service actually needs to filter on, what the policy repo's per-request resolver actually keys off — and at that point either (a) the DTO matches by luck, or (b) we change it. Option (b) is the same breaking change we were trying to avoid, just earlier and with less information.
The cheapest version of futureproofing is not doing it, and adding each input field at the same PR that adds its consumer. That gives us tiebreakers (the consumer's API) instead of vibes (what we think the consumer's API will probably look like).
What I'd ship instead
Split this into:
- Identity refactor (~300 LOC):
RebornIdentityConfig,ConversationIdentity,new_conversation_for,conversationsmap, removal of hardcodedreborn-clitenant. Ship this now, it's a real fix. - Api-version stamp (optional, ~50 LOC): If you want the version-stamp discipline, add it minimally — one variant, mandatory field, fail-closed on major mismatch. Skip the
RebornRuntimeApiVersionErrorenum elaboration until there's a second variant. - Everything else: defer to the sub-issue that adds the consumer.
Substrate/binary boundary
To answer the explicit question: no, this PR doesn't entangle binary concerns into substrate. Everything new lives in ironclaw_reborn_composition and the CLI crate. The substrate crates (ironclaw_reborn, ironclaw_turns, ironclaw_host_api) are not touched. That much is clean.
Bottom line
REQUEST CHANGES. The identity refactor is a clear win; please carve it out. The rest is too much shape committed too early on epic #3036 contracts that don't exist yet. I'd rather take five smaller breaking-change PRs that each follow their consumer than one large PR that pre-commits to all the shapes.
— Zaki
Summary
Reshapes
RebornRuntimeInput/RebornRuntime/ theironclaw-rebornCLI command tree so that epic #3036 — "Configuration-as-Code for IronClaw Reborn: tenant blueprints and use-case harnesses" can land its substrate (typed repos, blueprint parser, apply service, harness registry, instruction-bundle assembler) without breaking the standalone Reborn binary's input contract, runtime handle, or operator-facing CLI surface.This is a forward-architecture PR: nearly every line is additive seams + DTOs + stubs + boundary tests. Behavior of the running agent is unchanged.
Stacked on top of #3695 (composition root + narrow surface + live binary).
How epic #3036 shows up in this PR
api_version = "ironclaw.config/v1"on every blueprintRebornRuntimeApiVersion::V1mandatory field onRebornRuntimeInput; major mismatch isRebornRuntimeError::IncompatibleApiVersion(fail-closed)[scope] tenant = "acme"+ per-tenant identityRebornIdentityConfig { tenant, default_agent, default_owner, default_project }first-class field;ConversationIdentityper-conversation overrideRebornTenantRepo; runtime reads from repo[runtime]profile + approval policyRebornPolicyConfig { deployment_mode, default_profile, default_approval_policy }recorded in boot log viatracing::debugRuntimePolicyRepolookup; default written by blueprint apply[providers.<name>]LLM configRebornLlmConfigalready on the input (PR #3695); CLI env-resolves todayProviderRepopopulated by blueprint applyHarnessManifestactivationRebornHarnessSelection { harness_id }optional input field; logged + no-op todayHarnessActivationService→InstructionBundleAssembleroverlay + capability-surface filter; per-message override viaSendMessageOptions::with_harnessBlueprintApplyServiceas a privilegedCapabilityHostworkflowRebornAdminClienthandle withapply_blueprint / diff_blueprintreturningRebornAdminError::NotYetWiredHarnessRepolifecycleRebornAdminClient::{install_harness, activate_harness, deactivate_harness, list_harnesses}returningNotYetWiredAdminScopeprivilege gate (epic explicitly says "reuseAdminScope::newfromsrc/tenant.rs")RebornAdminScopesealed via privateSealedMarker;for_cli_admin(audit_reason)is the only public constructorfor_authenticated_admin(credential, audit_reason)when the privilege/auth crate landsironclaw config apply / diff / watchCLI commandstracking_issue: #3036errorsfail_not_yet_wired(...)toruntime.admin(scope).apply_blueprint(...).awaitironclaw harness install / list / activate / deactivateEvery row above is a future PR that swaps an
Err(NotYetWired)for a real body — not a future PR that changes the shape ofRebornRuntimeInput,RebornRuntime::admin(scope),RebornAdminClient, or the CLI subcommand tree.Stack of commits
arch(reborn-composition): futureproof RebornRuntimeInput for epic #3036runtime_input.rsinto a module; addsRebornRuntimeApiVersion,RebornIdentityConfig,ConversationIdentity,RebornPolicyConfig,RebornDriverConfig/RebornDriverChoice,RebornHarnessSelection/RebornHarnessId. Compatibility wired inbuild_reborn_runtime(api_version check, planned-driver = NotYetWired, harness selection = log+proceed). Unit tests per DTO.arch(reborn-composition): add RebornAdminClient + sealed RebornAdminScope for epic #3036runtime_admin.rs.RebornAdminScopesealed via privateSealedMarker.RebornAdminClientwith 6 typed async methods, each returningRebornAdminError::NotYetWired { operation, tracking_issue: "#3036", requires: "<substrate>" }.RebornRuntime::admin(scope)accessor +RebornRuntime::new_conversation_for(ConversationIdentity)+SendMessageOptions { requested_run_profile, harness_override }+send_user_message_with_options(...). Per-conversation identity now tracked in anArc<RwLock<HashMap<ThreadId, ConversationContext>>>so multi-tenantnew_conversation_foris real, not hardcoded.feat(reborn-cli): lock epic #3036 operator surface as discoverable stubscommands/config.rs→commands/config/{mod,apply,diff,watch}.rs; newcommands/harness/{mod,install,list,activate,deactivate}.rs; sharedcommands/stubs.rsfor uniform "not yet wired" output. 8 new CLI smoke tests pinning the operator-facing stub surface.arch(boundaries): pin RebornRuntime futureproof surface for epic #3036reborn_runtime_surface_is_futureproof_for_epic_3036asserts (1) all 5 epic-3036 fields onRebornRuntimeInput, (2) every admin method present, (3)RebornAdminScoperemains sealed (private_sealed: SealedMarkerfield), (4) every CLI stub file exists.What the runtime looks like to callers now
That is the entire surface a downstream consumer touches. No
TurnCoordinator, noSessionThreadService, noHostManagedModelGateway, noSubmitTurnRequest, no host-api substrate types — everything stays inside the composition root.Operator surface verified
Tests
ironclaw_llmironclaw_reborn(withroot-llm-provider)ironclaw_reborn_composition(both feature variants)runtime_input/submodules +runtime_adminironclaw_reborn_cliironclaw_architecturereborn_runtime_surface_is_futureproof_for_epic_3036)End-to-end smoke verified:
ironclaw-reborn run -m pingwithout LLM env vars still surfacesRecoveryRequiredin ~2s, same as PR #3695. The futureproof reshape did not regress the live-agent path.What this PR is NOT
NotYetWired. No typed repo, no blueprint parser, no apply service, no harness registry, no instruction-bundle assembler, no capability-surface filter, no reference harness, no GitOps watcher.ironclaw-reborn runtoday.ironclawroot crate) dependencies. The CLI'sBoundaryRule.forbiddenset inherited from PR arch(reborn): consolidate composition root, narrow public surface, ship live binary #3695 is unchanged and still enforced.How epic #3036 sub-issues consume this PR
Each sub-issue from the epic's "Suggested decomposition" reads as a body-swap on top of this shape:
ironclaw_blueprintcrate. No runtime touch. (Sub-issue 1.)BlueprintApplyService+ per-domain reconcilers + ApplyReport/lockfile. Implements the body ofRebornAdminClient::apply_blueprint+::diff_blueprint. Composition gains an optionalArc<dyn BlueprintApplyService>substrate field. (Sub-issue 2.)InstructionBundleAssembler. Newironclaw_settingscrate. Composition starts passingwith_identity_context_source(...)toRebornLoopDriverHostFactory.RebornPolicyConfigbecomes read-through toRuntimePolicyRepo. (Sub-issue 3.)HarnessManifesttyped repo + lifecycle + activation gating. Newironclaw_harnesscrate. Implements the body ofRebornAdminClient::install_harness / activate_harness / deactivate_harness / list_harnesses.RebornHarnessSelectionbecomes live: composition wires the active harness's capability-surface filter viawith_safety_context(...)and its prompt overlay via the instruction-bundle assembler from sub-issue 3. (Sub-issue 4.)red-team. Pure content. Lands underharnesses/red-team/. (Sub-issue 5.)chain-incident-response. Pure content. (Sub-issue 6.)apply/diff/watch) + admin web endpoint. CLI stub bodies replaced with real calls throughruntime.admin(scope).apply_blueprint(...). Admin web is an orthogonal frontend concern. (Sub-issue 7.)At each step,
RebornRuntimeInput,RebornRuntime,RebornAdminClient, and the CLI command tree do not change shape — only the bodies inside method definitions, and (orthogonally) the composition's wiring insidebuild_reborn_runtime. External tooling and operator muscle memory hold steady.Verification checklist (against epic acceptance criteria)
The epic enumerates many AC bullets; this PR doesn't satisfy them (none of the substrate is built), but several structural prerequisites the AC implicitly depends on are now in place:
RebornAdminClientis only obtainable viaRebornRuntime::admin(scope: RebornAdminScope); scope is sealed and pinned by a boundary test.RebornRuntimeApiVersion::V1mandatory; major mismatch fail-closed; pinned by unit test.apply_blueprint(blueprint, scope, mode = Apply | DryRun)returns a typedApplyReport" — signature exists; body isNotYetWired. Body lands in sub-issue 2.Change { domain, key, action: Create|Update|NoOp|Delete-deferred, before_hash, after_hash }" —ApplyChangecarries exactly those fields.visible_capabilities()before next model call" — (seam exists viaRebornLoopDriverHostFactory::with_safety_context; wired by sub-issue 4)harness.deactivatedthenharness.activatedaudit events" — (audit emission in sub-issue 4)harness install / list / activate / deactivateexist on the CLI" — stubs ship today with the exact argument shape; sub-issue 4 / sub-issue 7 swap bodies.ironclaw config apply <path>produces a lockfile" — argument exists; body in sub-issue 2.Why this PR is small and why it's now
~1k LOC. Every line is one of three things: (a) a typed DTO that didn't exist, (b) a stubbed method body whose signature is committed by the epic, or (c) a boundary test pinning the shape. There is no behavior change to bisect against, no agent-loop touch.
Shipping the reshape now, before any sub-issue starts, means epic #3036's sub-issues can land independently as body-swaps without each one re-litigating "what's the right input shape" or "where does the admin gate live." That conversation is settled here.
The alternative — extending
RebornRuntimeInputincrementally as each sub-issue lands — would force five separate breaking changes to the public input contract, force the CLI command tree to grow new top-level subcommands across releases (breaking shell completion expectations), and re-decide the privilege gate on each sub-issue. Concentrating the shape change here is the cheapest version of futureproofing.cc @ilblackdragon (this addresses the "too many things exposed; just those few structs/traits" feedback that originated this work — every type in the new public surface is one of [DTO, handle, error, ID]; nothing exposes underlying substrate traits).