All notable changes to this project will be documented in this file. Format: Keep a Changelog.
Two features: FE-14 ACL Governance makes the CLI's access-control surface reachable for the first time, and FE-15a OpenAPI Import adds apcli openapi scan / generate. APCLI_SUBCOMMAND_NAMES grows from 13 to 15.
The ACL work turned up two silent access-control bypasses — see Security below. They were present in all three SDKs and are fixed in all three.
make check is green end to end: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings (0 warnings), apdev-rs check-chars (29 files, ASCII-only), and 996 tests across 33 binaries, up from 800.
-
FE-14: the CLI now attaches an ACL. apcore has enforced access control since PROTOCOL_SPEC §6, and this CLI has always carried the downstream half of it — exit code
77forACL_DENIED, anaclrow in--dry-runpreflight,acl_checkinapcli describe-pipeline. None of it was ever reachable, because no apcore-cli SDK had ever constructed or attached anACL: all three build anExecutordirectly rather than going through theAPCorebootstrap that performsACL::discover. The result was an executor whoseacl_checkstep consulted nothing, and agovernance_state()reportingunprotected_control_surface: truefor every project — including projects shipping anacl/global_acl.yamland reasonably assuming it was in force.New
src/acl_loader.rsresolves an ACL root through the FE-07 4-tier chain (--acl>APCORE_ACL_ROOT>acl.rootinapcore.yaml>./acl) and delegates the parse toACL::load. Rule-key closure,effect/approvalenum closure and pattern-array arity are apcore's contract and conformance-tested there; the CLI does not reimplement them.Enforcement is only-when-configured. A missing root attaches nothing and changes no behaviour, preserving apcore's missing-path invariant: synthesizing an empty ACL with
default_effect: denywould deny every call in every project that lacks anacl/directory. Every existing project behaves exactly as it did. -
apcli aclsubcommand group (src/acl_cmd.rs):list,check,validate,status.checkcallsACL::check_access, never the booleanACL::check— the latter fails closed on an approval requirement, returningfalsefor a call that is allowed but needs a human, which would report "denied" for a rule set that in fact permits the call. Both axes are reported separately, and an allow-with-approval outcome exits0.validaterenderssyncandasyncas separate columns rather than one boolean: a finding withsync=no, async=yesis an async-only handler, working underasync_check()and unevaluable undercheck(), and collapsing them loses exactly that.statusrenders all nineExecutor::governance_state()observations.acl_configuredalone is not the answer — the ACL and approval gates are pipeline steps, andinternal/testing/minimalremove them, so an executor can hold an ACL that no step ever consults. -
--identity-id/--identity-type/--roleglobal flags. These build aContextidentity so conditional rules keyed onrolesoridentity_typesare evaluable from the terminal. They are unauthenticated argv assertions, not authentication, and each flag's--helpsays so.apcli acl checkrestates the three with identical wording; clap resolves the two levels per argument, so a subcommand flag overrides only its own counterpart and a root flag not restated still applies.Context::caller_idis never fabricated — apcore deliberately makes it unsettable, so a top-level CLI call is always@external, and a flag that set it would let any user assume any module's identity. When only--roleor--identity-typeis given,Identity.idfalls back toDEFAULT_IDENTITY_ID(@cli), pinned in value and export name across all three SDKs; the@prefix follows apcore's synthetic-principal convention so it cannot collide with a real user id ofcli. -
FE-15a:
apcli openapi scanandapcli openapi generate(src/openapi_cmd.rs,src/openapi_source.rs).scanreads an OpenAPI 3.0/3.1 document through the toolkit'sOpenAPIScannerand renders the modules it would produce in every FE-08 format;generate -o DIRmaterializes them as<id>.binding.yamlthroughYAMLWriter. Neither registers a module, builds an executor, or issues a request to the described API —scanof a local file performs no network I/O at all.The CLI is an adapter, not a second implementation:
derive_module_idoutput is returned verbatim (it is the subject of a cross-SDK conformance corpus and must match byte-for-byte in three languages), schema extraction is the toolkit's, and the routing contract is exactly the two flat keyshttp_methodandurl_path. The scanner hooks are deliberately not exposed as flags — overriding derivation hands back the naming guarantee, which is not something a command-line flag should be able to do silently. -
Proxy-hazard detection.
HTTPProxyRegistryWriterdecides body-versus-query by HTTP method alone, so a query parameter declared on aPOST/PUT/PATCHoperation would be sent in the request body — silently. FE-15a cannot fix that (the fix is upstream, in apcore-toolkit 0.12.0), but it makes it visible: the CLI holds the raw document, which still carriesparameters[].in, so affected operations are named with their method and offending parameter names by bothscanandgenerate. Hazards are counted separately from scanner warnings, appear under a top-levelhazardskey in machine formats because they describe a future execution path, and never change the exit code. -
FE-14 §4.8: ACL decisions now reach the FE-05 audit log. apcore emits exactly one
AuditEntrypercheck_access()call, but only through anaudit_loggercallback, and nothing in apcore wires theacl.audit.*keys to one. The CLI now does: whenacl.audit.enabledis true, the sameAuditLoggerthe module-dispatch path uses is installed as the callback, so ACL decisions land in~/.apcore-cli/audit.jsonlbeside execution records.This SDK attaches with
ACL::set_audit_loggerrather than rebuilding. §4.8 describesACL::new(src.rules, src.default_effect, logger)because that is the only mechanism Python and TypeScript offer, and explicitly permits an SDK to use whichever its runtime has. Rust's setter is strictly less lossy on two counts: the rebuild must carrydefault_effectacross by hand — pass a literal"deny"and every file declaringdefault_effect: allowhas its governing default silently inverted for each unmatched call — and the rebuild drops theyaml_paththatreload()depends on. The setter can express neither mistake.acl.audit.enabled: falseattaches theACL::loadresult with no callback and no rebuild; an ACL an embedder supplied itself never reaches the loader and is attached unchanged.The wire record is 13 fields, in apcore's
AuditEntrydeclaration order, and nothing else. Key order is normative — the log is JSONL, so an unspecified order would make the same decision serialize to different bytes per SDK. It is pinned by a#[derive(Serialize)]struct rather than aserde_json::json!literal:serde_json::Mapis aBTreeMapunless thepreserve_orderfeature happens to be enabled somewhere in the dependency graph, so a map literal would emit alphabetical order on one build and insertion order on another, silently. Serialising apcore'sAuditEntrydirectly is wrong for a second reason —skip_serializing_if = "Option::is_none"on six optional fields would dropmatched_rule,matched_rule_index,identity_type,call_depth,trace_idandhandler_errorfrom any entry that did not populate them. Here an absent value isnulland every line carries the same key set. No CLI field is added either, notably not theuserfield FE-05 puts on execution records, so a consumer can read an ACL record against apcore'sAuditEntryrather than a CLI dialect of it.acl.audit.include_deniedgoverns denied decisions, matching apcore's ownschemas/acl-config.schema.json("Whether to log denied access attempts"):falsesuppresses deny entries and leaves allow entries alone. It is not an inverted "log denials only" switch.A logging fault never changes an access decision. The callback is infallible by construction — building the record cannot fail, and
AuditLoggerswallows its own IO errors behind a one-shot warning — so an unwritable audit log costs the entry and nothing else. -
Test coverage.
tests/test_acl_cmd.rs(70 cases, including the §4.8 rows T-ACL-26 / 27 / 27a / 27b / 27c) andtests/test_openapi_cmd.rs(40 cases), driven end-to-end through the real binary so the asserted exit codes are the ones a user's shell sees. The section 4.10 cases use a sentinel file the module's own script creates, because an exit code alone does not prove a subprocess was never started.The §4.8 rows run in-process instead, deliberately: the production audit path writes to
~/.apcore-cli/audit.jsonl, so a test spawning the real binary with auditing on would append to the developer's own log. They call the exact functionmain.rscalls, with anAuditLoggerpointed at a temp file. T-ACL-27a asserts that no logger was installed — read off apcore'sACLDebugrendering, the only introspection it offers — rather than merely that no entries were written; the two differ, and only the former rules out a callback that silently drops everything. T-ACL-26 asserts an equality on the ordered key list read off the raw JSONL text, which pins field set, order, casing and the absence of extras at once.
-
apcore = ">=0.30",apcore-toolkit = { version = ">=0.11.1", features = ["http-proxy"] }. The floors track the aligned apcore 0.30.0 / apcore-toolkit 0.11.1 release. Both bumps are confined to layers this CLI does not consume, so neither forced a behavioural change here — the only edit either required was to a test that had pinned the toolkit dependency line verbatim, version floor included, and therefore failed on any bump regardless of what changed. It now asserts thefeatures = ["http-proxy"]half, which is what it was written to guard; the floor is Cargo's business.The
http-proxyfeature is required, not optional:load_specsits behind it in the Rust toolkit, and an SDK that cannot reach the HTTP path must fail with an actionable message rather than a missing-symbol link error. Local-file scanning and the YAML writer need none of it. -
ConfigResolver::DEFAULTSgainsacl.root(./acl),acl.audit.enabled(true) andacl.audit.include_denied(true). All three are apcore-owned keys, so their environment variables areAPCORE_ACL_ROOT,APCORE_ACL_AUDIT_ENABLEDandAPCORE_ACL_AUDIT_INCLUDE_DENIED— the apcore convention, following theAPCORE_EXTENSIONS_ROOTprecedent rather than extending it. There is deliberately noacl.enabled: false: a key whose only effect is to silently disable access control is a foot-gun that reads as configuration. To disable enforcement, pointacl.rootat a path that does not exist.The two audit booleans accept
true/1/yes/onandfalse/0/no/off, case-insensitively after trimming — a table shared with Python and TypeScript, and deliberately notstr::parse::<bool>(): Rust'sFromStr for boolerrors on"0", so delegating to it would leaveAPCORE_ACL_AUDIT_ENABLED=0unable to switch auditing off while the same value worked in the other two SDKs. An unrecognised spelling falls back to the key's default (true) with a warning naming the key, rather than tofalse— reading an unparseable governance value as "off" would let a typo silently stop the audit trail. -
tests/acl_argument_scoped_approval.rsbuilds its rules throughACLRule::new. apcore 0.29.0 makesACLRule#[non_exhaustive], so struct-literal construction no longer compiles across the package boundary.
-
ACL_RULE_ERRORexited 1 instead of 47. It is a realapcore::errors::ErrorCodethat no SDK's exit map carried, so a malformed ACL file fell through tomap_apcore_error_to_exit_code's catch-all arm — the code that reads as "the module ran and failed", indistinguishable from a genuine execution failure. Now47(CONFIG_INVALID), added in all three SDKs together.47rather than77because the ACL could not be read, which is a configuration fault, not a denial.77stays reserved for an actual access decision, or a script branching on it would misreport a broken config as a permissions problem. -
The test suite appended to the developer's real
~/.apcore-cli/audit.jsonl.tests/test_e2e.rsspawns the binary withoutAPCORE_CLI_AUDIT_DISABLE=1, so threemath.addexecutions wrote 639 bytes of real audit records into the developer's own log on everycargo testrun — andsystem_usage's summary reader, which derived the same home path independently, read from it. Pre-existing in all three SDKs and fixed in all three; the FE-14 §4.8 work is what surfaced it. It never made a test fail, so a green suite was never evidence either way.Under the existing
test-supportfeature,AuditLogger::default_path()now resolves to a temp file instead. The fix is deliberately notAPCORE_CLI_AUDIT_DISABLE=1intest_e2e.rs: that would stop those tests exercising the audit path at all, trading a visible problem for an invisible one. Redirecting keeps the writes happening where they can be counted. The redirect path is derived rather than published through an environment variable, so the test process and every binary it spawns compute the same value independently —std::env::set_varis unsound once other threads are running, and test binaries are multi-threaded by default.A production build compiles the branch out entirely (verified: the string
apcore-cli-test-auditdoes not appear in thecargo build --releasebinary), so no environment or feature setting can relocate a released binary's audit trail.home_audit_path()is split out and pinned by its own test against the real home-derived value, because an assertion thatdefault_path()merely "contains.apcore-cli/audit.jsonl" would keep passing against the redirect and silently stop testing the shipped location.system_usagenow delegates to the same function rather than re-deriving the path, so reader and writer cannot drift apart.Verified by measurement, not by tests passing:
~/.apcore-cli/audit.jsonlheld 241911 bytes / md575d0c8f5e1cd2951436379cd776a0bc7both before and after a fullcargo test --all-features, while the redirect target captured the 3 records (642 bytes) that used to land there — confirming the writes moved rather than merely stopped. -
Tracing wrote diagnostics to stdout, corrupting every machine format. apcore emits one WARNING per unevaluable ACL rule at the default log level, and the
tracing_subscriberfmt layer's default writer is stdout — soapcli acl validate --format json | jqfailed on a rule set that had anything to report, which is precisely when a user runs it. The layer now writes to stderr, where every other diagnostic in this crate already goes.
-
--sandboxsilently disabled access control.sandbox_runnerconstructs a freshRegistry+ExecutorfromAPCORE_EXTENSIONS_ROOTwith no ACL attached, so a rule set that denied a module was enforced for a plain call and ignored for a sandboxed one. This inverts the user's intent outright:--sandboxis a security flag, so switching on stronger isolation switched off access control. Present in Python and TypeScript too, fixed in all three. -
Filesystem script modules were never gated.
FsDiscovererexecutables are spawned as subprocesses and never reachExecutor::call, so the pipeline'sacl_checkstep never saw them — meaning a configured ACL was silently ignored for exactly the modules this CLI discovers. An operator writingacl/global_acl.yamlwould believe a denial was in force when nothing was checking. Rust-specific (the other SDKs have no equivalent discovery path).Both are the same defect: attaching an ACL to the executor gates the calls that go through that executor, and gates nothing else. The decision is now reached in the parent, which already holds the ACL, and a denied call is refused with exit
77before the subprocess is spawned — one enforcement point rather than one per execution mechanism. The child re-loadingacl.rootis explicitly not the control: the sandbox forwards a narrow environment allowlist by design, so the child's view is neither guaranteed nor trustworthy as a gate.An ACL-sourced
approval: requiredcomposes with the module annotation before the CLI's approval gate on these paths too, exactly as apcore's gate does for a normal call — otherwise the same rule would demand a human on one path and wave the call through on another. Five tests go red when the gate is reverted, including the pair that proves the sentinel file is not created. -
The gate itself had the same bypass one level down: it passed no
Context. AContextis built from the identity flags and is legitimatelyNonewhen none were given — correct forapcli acl check, which simulates a call and is honestly context-free. It is wrong for a gate. PROTOCOL_SPEC §6.5 makes every conditional rule a non-match when a call supplies no context, while apcore's pipeline creates one at Step 1 for every real call. So adenyrule carryingconditionsfired in-process and went inert on the delegated path — the same silent bypass, one level down, and invisible to any test using an unconditional rule.Both gates now always present a context: the identity-bearing one when flags were given, otherwise a freshly built
@external/externalidentity reproducing exactly whatExecutor::callconstructs forctx: None, so anidentity_typesrule behaves identically on both paths. They also pass the call's arguments as the governance projection, without which anarguments-scoped rule is unevaluable — and per §6.1.1 an unevaluabledenyrule takes effect, so the omission silently denied calls it should have permitted as well as permitting ones it should have denied. Both directions are pinned by discriminating pairs. -
Credentials never reach disk. Headers supplied via
openapi generate --headerto fetch a protected document exist only for that fetch; neither they nor the document'ssecuritySchemesare copied into any generated artifact.
-
A retracted claim, recorded because it reached implementers. An earlier draft of this changelog — and of FE-14 §4.8 — said the audit wiring was blocked on a public
ACL.set_audit_loggerthat Python and TypeScript would have to gain, and that shipping it in Rust alone would put this SDK ahead on a cross-SDK surface. That was wrong on the premise: all three SDKs already accept the callback as a constructor argument (ACL(rules, default_effect, audit_logger=…),new ACL(rules, defaultEffect, auditLogger),ACL::new(rules, default_effect, audit_logger)), so §4.8's load-then-construct sequence needed nothing new anywhere. Rust'sset_audit_loggeris an extra convenience on top, not the prerequisite. The wiring and bothacl.audit.*keys land here in 0.12.0, alongside Python and TypeScript. -
All of FE-15b is excluded.
generateproduces binding artifacts; it does not make an API callable, and the commands'--helpsays so rather than implying otherwise. Passing the generated files to--bindingdoes not yet produce working commands, on two independent prerequisites:--bindingis a real registration path only in Python (TypeScript populates a display-overlay map; Rust constructs aDisplayResolverand discards it), andHTTPProxyRegistryWritercannot correctly encode a query parameter declared on a body method until apcore-toolkit 0.12.0 carries parameter locations. Neither is about OpenAPI; both are pre-existing debt. -
--writer nativewas specified and then withdrawn. Every toolkit source writer resolvesScannedModule.targetas amodule.path:callableimport path, while an OpenAPI-derivedtargetis always a route descriptor such as"GET /pets"— so the flag could never have succeeded for any inputgeneratecan produce. Same root cause as theRegistryWriterlimitation.generateis binding-YAML only, and no refusing stub is left behind. Emitting genuine host-language source for an OpenAPI operation means emitting an HTTP proxy implementation, which belongs with FE-15b. -
apcli aclis arequires_executorentry but is not inAPCLI_ALWAYS_REGISTERED— undermode: includeit registers only when explicitly listed.openapineeds neither registry nor executor. Neither is a system command, so neither gates onsystem.health.summaryavailability. -
The
apcli-visibilitygolden byte-match remains#[ignore]d pending the canonical help formatter port, as it has been since FE-13. All five behavioural scenarios pass. The four new root flags' help strings are normative across all three SDKs, and are pinned here by a direct unit test on the clapArgmetadata rather than only through the golden — a fixture that does not byte-match in every SDK would let a reword pass locally and break the others.
Bumps the required apcore floor to 0.28 and apcore-toolkit to 0.10.2 to track the aligned apcore 0.28.0 release (2026-08-31). Carries one display fix and a crate-root lint attribute (both under Fixed). make check is green end to end: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings (0 warnings), and 800 tests across 31 binaries including the conformance suite, all against apcore 0.28.0.
Why a minor rather than a patch. This SDK needed no correctness fix of its own, but the three CLI SDKs ship as one version line, and the apcli health summary output changes here too. Version-locking them is what keeps the compatibility table and the cross-SDK conformance fixtures meaningful; see apcore-cli-python 0.11.0 for the changes that set the bump. Note for downstream crates: a apcore-cli = "0.10" pin does not resolve 0.11 — widen it to ">=0.11" or "0.11".
tests/acl_argument_scoped_approval.rs(4 cases) pins the cross-SDK contract that an ACL-sourced approval requirement reachesCliApprovalHandler, replacing what had been a code-path argument with a measurement. The fourth case is the discriminating one — with auto-approve off and no TTY undercargo testthe handler refuses, so the ungated call must still succeed while theforce-carrying call must fail withErrorCode::ApprovalDenied. Without that pair a gate that never fired would pass the suite.
apcore = ">=0.28",apcore-toolkit = ">=0.10.2". apcore-toolkit 0.10.2 is a dependency-tracking release with no source change.
-
DEPENDENCY_NOT_FOUNDandDEPENDENCY_VERSION_MISMATCHexited 1 instead of 44. Both are realapcore::errors::ErrorCodevariants, and both reachedmap_apcore_error_to_exit_code's catch-all arm, which returnsEXIT_MODULE_EXECUTE_ERROR. apcore-cli-python and apcore-cli-typescript map both to 44, so the same dependency failure ended a script with a different code depending on which CLI ran it — and 1 is the code that reads as "the module ran and failed" rather than "the module could not be resolved".Found by a mechanical three-way diff of the exit-code maps, not by inspection: extracting all three and comparing key-for-key reported 2 divergent of 22 codes, and re-running it after the fix reports 0.
EXIT_DEPENDENCY_NOT_FOUND/EXIT_DEPENDENCY_VERSION_MISMATCHare named as their own constants to mirror the other two SDKs rather than reusingEXIT_MODULE_NOT_FOUNDat the call site. Pinned in all three SDKs so the maps cannot drift again. -
The
apcli healthsummary line reported "no data" for a project whose modules it had just listed. apcore classifies module health in four tiers —healthy/degraded/error/unknown— and the tally iterated only the first three.unknownmeans "no calls recorded yet", which is the state every module in a fresh project is in, so the common case rendered a populated table above a total that denied it:probe.echo unknown 0.0% -- Summary: no dataPre-existing, and not introduced by this upgrade — all three SDKs have emitted
unknownsince the tier set existed. apcore 0.28.0 is what brought it into focus:sys-health-summary.schema.jsonhad declared the enum as["healthy", "degraded", "unhealthy"], a value no SDK emits, and the release corrects it to the four tiers actually produced, splitting the summary'sunhealthycount field intoerrorandunknown. With the canonical shape finally naming four tiers, rendering three is a plain omission. Fixed in all three SDKs together, with the tally now coveringunknown; a genuinely empty tally still reads "no data". -
cargo clippy -- -D warningsfailed onclippy::result_large_err, which would have taken CI red. TwoSandboxmethods returningResult<Value, ModuleExecutionError>were flagged because the enum's passthrough variant carriesapcore::errors::ModuleErrorby value and that type is ≥184 bytes. Not a regression from this upgrade — reproduced identically with the previousapcore = ">=0.27"pin, so the trigger was the clippy version, not apcore; but CI runsdtolnay/rust-toolchain@stablewith-D warnings, so it was going to fail there regardless of when it started.Resolved with a crate-root
#![allow(clippy::result_large_err)], mirroring apcore-rust, which suppresses the same lint at its own crate root with the reason that applies verbatim here: "ModuleError is intentionally large (rich structured error for an SDK); boxing it everywhere would change the public API." The variant holds the error by value on purpose —cli::map_module_error_to_exit_codereads itsErrorCodeto keep the exit-code taxonomy identical across the--sandboxand direct paths — so boxing would both break a public enum in a patch release and diverge from the decision made by the crate that owns the type. -
The discriminating approval tests no longer depend on stdin not being a terminal. They originally used
CliApprovalHandlerwith auto-approve off and relied on its non-TTY refusal, which is not a property of the test — it is a property of how the suite happens to be launched.cargo testdoes not redirect stdin, so run from an interactive shell the Rust case printed its prompt, blocked for the full 60-second timeout and then failed onApprovalTimeoutinstead ofApprovalDenied;pytest -sand a main-thread vitest configuration reach the same trap. All three now register a small recording stub that always refuses, which removes the ambient dependency and lets each test assert the stronger property directly: that the gate consulted a handler at all, and for exactly which call. The realCliApprovalHandleris still exercised, on the auto-approve path, where its answer is deterministic.
-
Three of 0.28.0's BREAKING Rust changes land on types this crate names, and all three are source-compatible here.
ACLRulegains anapprovalfield andAuditEntrybecomes#[non_exhaustive]— neither is constructed by this crate, which never builds or loads anACL.CallbackApprovalHandler::newnow takes an async fallible callback —CliApprovalHandlerimplementsapcore::ApprovalHandlerdirectly and never uses the convenience constructor.ACL::evaluate_conditionsreturningConditionOutcomeandACL::checkfailing closed on allow-with-approval-required are both unreachable: the crate calls neither. -
The one 0.28.0 change that reaches this SDK works correctly and needed no code. Spec v1.28.0 §6.9 makes the approval gate fire on the union of three sources, so an ACL rule carrying
approval: required(§6.1.6) now routes calls to modules annotatedrequires_approval: falsethroughCliApprovalHandler. The trait adapter rebuilds itsmodule_defshape fromrequest.annotations.requires_approval, andbuiltin_steps.rs:816sets that totruebefore constructing the request for any source of the requirement — soget_requires_approvalpasses and the prompt runs.cli_to_apcore_resultalready returns a typedapcore::ApprovalResult; apcore-cli-python returned a bare mapping on the same path and had to be fixed in its 0.11.0. -
system.usage.*behaviour changes are upstream-side and pass through unread. 0.28.0 makes both modules honourperiod(statistics were previously computed over the full retained history) and changeshourly_distribution[].hourtoYYYY-MM-DDTHH.dispatch_usageforwards--periodverbatim, and the two TTY formatters read onlymodules,period,module_id,call_count,error_count,avg_latency_ms,trendandp99_latency_ms.hourly_distributionappears nowhere in this crate — nor in the Python or TypeScript CLIs — so no formatter or assertion depends on the retired key shape. -
What the delta does not touch.
Registry.list/get_definition,Executor::call/validate, the approval handler protocol and the toolkitformat_*surfaces are all unchanged across 0.27.0 → 0.28.0.
Patch release. Bumps the required apcore floor to 0.27 to track the aligned apcore 0.27.0 release (2026-08-14). No source changes — the full test suite (fmt + clippy + all tests) passes unchanged against apcore 0.27.0, including the 511-case conformance suite.
The apcore 0.26.0 → 0.27.0 delta is BREAKING at the spec level, but touches no surface the CLI consumes — verified against the release notes and the actual call sites:
- Middleware semantics —
before_stepfailure is now terminal/non-recoverable,after_stepfires after a recovered step body. The CLI never constructs or configures middleware or pipelines; it only constructsExecutor::new(Arc<Registry>, Config)and callsExecutor::call(module_id, input, None, None)/describe_pipeline(read-onlyStrategyInfo). No exposure. - ACL-failed
validate()introspection — a failedaclcheck now withholdsmodule_preflight/module_previewchecks andpredicted_changes. The CLI's validate path builds its own preflight checks locally from the registry descriptor (validate.rs), never consumingExecutor::validate()'s return shape. No exposure. Registry.register_versionedmetadatadependenciespersistence — the CLI never callsregister; module registration is viaRegistry::discover(&FsDiscoverer), which emitsdependencies: vec![]/ emptymetadata. No exposure.- Schema conversion (A23) — object detection, nullable
anyOfwrapping, sortedrequiredare SDK-conversion rules. The CLI runs its own schema→clap converter (schema_parser.rs/ref_resolver.rs) on the descriptor'sinput_schema;requiredis order-insensitive andtype-less nullable branches already fall to the string default. No exposure. pipeline.configure4-field set /requires/providesnon-configurable — the CLI never configures pipelines; a host config carrying other keys now fails at load (spec-mandated strictness, upstream concern).- No type coercion at the module boundary — the CLI's own clap-string→JSON coercion (
cli.rs) applies beforecall(), which receives already-typed JSON values. No exposure. - Removed/renamed API surface —
ErrorCode::ConfigurationError→PipelineConfigurationError, OtelTracing* removal +opentelemetryfeature,TracingMiddlewareConfigfield removal,SchemaValidator::new()no-coerce default — none used by the CLI (which uses only theModuleExecuteErrorvariant and no tracing middleware).
Patch release. Bumps the required apcore floor to 0.26 to align the ecosystem on the 0.26.0 governance layer (additive, no breaking changes). No code or API changes.
update package dependency version for apcore-toolkit (0.10.0) and increment project patch version
-
Required runtime bumped to apcore 0.25.0 and apcore-toolkit 0.9.1.
Cargo.tomldependencies raised fromapcore = "0.24"/apcore-toolkit = "=0.8.1"toapcore = "0.25"/apcore-toolkit = "=0.9.1", tracking the aligned apcore 0.25.0 and apcore-toolkit 0.9.1 releases (both resolve from crates.io). No source changes — the full test suite passes unchanged.Neither delta touches a surface the CLI consumes:
- apcore 0.24.0 → 0.25.0 adds config-driven ACL discovery (
acl.rootactivation +ACL.discover), auto-wired only by theAPCorebootstrap and skipped when the caller supplies its ownExecutor. The CLI builds its ownExecutor::new(Arc<Registry>, config)and never constructsAPCore, so discovery does not engage. The companion change — Rust'sacl.rootnow defaulting to./aclinstead of being hard-required — only relaxes config validation and is backward-compatible. - apcore-toolkit 0.8.1 → 0.9.1 is a bug-fix release; its only API-surface
change relaxes
RegistryWriter::write/HttpProxyWriter::writefrom&mut Registryto&Registry(source-compatible, and unused by the CLI). The toolkit surface the CLI uses (format_*,DisplayResolver,ScannedModule,ModuleStyle,FormatOutput) is unchanged.
- apcore 0.24.0 → 0.25.0 adds config-driven ACL discovery (
-
Required runtime bumped to apcore 0.24.0 and apcore-toolkit 0.8.1.
Cargo.tomldependencies raised fromapcore = "0.22"/apcore-toolkit = "=0.8.0"toapcore = "0.24"/apcore-toolkit = "=0.8.1", tracking the aligned apcore 0.24.0 and apcore-toolkit 0.8.1 releases (both resolve from crates.io). No source changes — the full test suite passes unchanged.The apcore 0.22.0 → 0.24.0 delta does not touch any surface the CLI consumes:
- Schema type coercion now default-on;
SchemaValidatorreturns the coerced value — the CLI does not use apcore'sSchemaValidator. It implements its own JSON-Schema → clap translator (schema_parser.rs) and validates via thejsonschemacrate directly (cli.rs), so the coercion change has no effect. - Per-instance
ToggleStateisolation (#71) — the CLI never constructsToggleState/APCorenor callsis_module_disabled(); toggling is delegated tosystem.*modules viaExecutor::call(). - Error
detailssnake_case alignment (A-D-019) — the CLI reads onlyerr.code(for exit-code mapping); it never serializes apcore errordetails. Registry::list()/get_definition(),Executor::call()/set_approval_handler(),Config::default(), theApprovalHandlertrait, andModuleAnnotationsare unchanged across the delta.- Out of scope and unused by the CLI: registry-event delivery/DLQ (A-D-013),
middleware
on_error(A-D-010/012/015),APCore.on()/events()bus (D1-011), array redaction (A-D-003),Configenv coercion (A-D-007/009),CircuitBreakerMiddleware,Context::create().
- Schema type coercion now default-on;
- Removed
toolkitCargo feature flag — apcore-toolkit is now unconditionally required (resolves 6.2; lands ADR-07).apcore-toolkit = "=0.7.0"was already declared as a hard runtime dependency inCargo.toml, but the code base wrapped every toolkit-delegating path in#[cfg(feature = "toolkit")]and provided silent-downgrade fallbacks under#[cfg(not(feature = "toolkit"))]. This created the same "fake optional" self-contradiction the PY / TS 0.10.0 release fixed: required at the manifest level, soft-degraded at the code level. The sweep landed in this release:- Deleted 10
#[cfg(feature = "toolkit")]gates acrosssrc/output.rs(descriptor adapter, markdown/skill arms informat_module_list/format_module_detail, and 5 test helpers / tests) andsrc/main.rs(toolkit-integration block). - Deleted 3
#[cfg(not(feature = "toolkit"))]fallback branches that silently degraded--format markdown/--format skillto JSON with atracing::warn!. - Removed the now-dead
TOOLKIT_MISSING_HINTconst. - Removed
toolkit = []anddefault = ["toolkit"]fromCargo.toml[features]. Onlytest-supportremains.
- Deleted 10
- Migration for downstream crates that depended on
apcore-cliwithdefault-features = false: explicitly opting out of thetoolkitfeature was previously a way to compile without the toolkit code paths (at the cost of silent format downgrade); that option is gone. apcore-toolkit will always be linked. If you genuinely cannot tolerate the toolkit dependency, pin toapcore-cli = "0.9"and stay there until you can adopt the unified surface.
tests/conformance_snake_case_kwargs.rs— runs the cross-language Algorithm C-SNAKE fixture (apcore-cli/conformance/fixtures/snake-case-kwargs/cases.json) againstschema_to_clap_args+reconcile_bool_pairs, mirroringextract_cli_kwargs's extraction path. Five cases verify that schema property names with underscores (has_solution,sort_by,sort_order) survive the round trip from clap parse to the kwargs dict. No source change required — clap'sArg::new(prop_name)keeps the snake_case id as the access key; the Rust SDK is a parity reference for the parallel TypeScript fix. Surfaced as part of the cross-SDK regression coverage gap audit.
- Sandbox output-cap raises wrong error class (D11-007) — byte-cap overflow now returns
ModuleExecutionError::OutputSizeExceeded { module_id, limit_bytes, overflow_stream }instead ofOutputParseFailed. Display message uses MiB units and names the overflowing stream (stdout/stderr/stdout+stderr), matching Python and TypeScript.src/security/sandbox.rs:374. exec --dry-runemits Rust-only "Pipeline preview" stderr block (D11-011) — the preview was not declared in the spec and had no Python/TS equivalent. Removed for cross-SDK parity;--tracenow uniformly routes throughexecutor.call_with_traceacross all three SDKs.- CLI brand string inconsistency in error messages (D11-006) —
src/security/config_encryptor.rs:56DecryptFailederror text changed fromapcore-cli config setto canonicalapcli config set, matchingsrc/security/auth.rswhich already usedapcli. - Unused
schemarsdev-dependency (D6 re-audit) —schemars = "0.8"in[dev-dependencies]had zero usage (use schemars,#[derive(JsonSchema)]). Removed. - Stale
CLAUDE.mdSandbox::executearity claim (D10 re-audit) — the "Current Conventions" bullet claimed Rust used a 2-parameter signature with executor bound at construction time; actual source has been 3-parameter since v0.7. Updated to reflect the real 3-parameter form.
set_all_options_helpcross-SDK parity note (D1-W1) —src/cli.rs:104doc-comment now documents that Rust intentionally ships without the deprecatedset_verbose_helpalias (post-rename, no pre-v0.9 callers). Python/TS keep the alias for one MINOR deprecation cycle.ConfigResolver::resolvelanguage-idiom note (D10-W1) —src/config.rs:113doc-comment documents that Rust narrows the return toOption<String>(serde_yaml_ng string-coercion) while Python returnsAnyand TypeScript returnsunknown. Embedders needing typed YAML access are pointed at a v0.10 typed-resolver follow-up.AuthProviderencryptor-fallback language-idiom note (D11-005) —src/security/auth.rsnow documents that Rust's two-tier encryptor chain (explicit arg → fresh instance) differs from Python/TS's three-tier chain (explicit arg →config.encryptorpeer attribute → fresh instance). The peer-attribute tier requires aConfigResolverfield addition tracked as a v0.10 follow-up.
- CSV
--format csvheterogeneous-keys data loss —format_exec_resultpreviously derived headers fromarr[0].as_object().keys()only, silently dropping fields that first appeared in later rows. Now delegates toapcore_toolkit::format_csv()which uses the union of keys across all rows in insertion-order.src/output.rs:537-566. - CSV line terminator — now
\r\nper RFC 4180 (was\n). Existing test expectations updated; old\n-based assertions replaced with CRLF assertions intests/test_output.rs.
- User-visible help/man/completion text no longer leaks the
apcoreframework name to end users of downstream CLIs built on apcore-cli. Affected strings:--extensions-diroption help (Path to apcore extensions directory.→Path to extensions directory.,src/main.rs:367),execsubcommand description (Execute an apcore module→Execute a module,src/shell.rs:62,src/cli.rs:344, plus theshell.rs:1102test fixture and thetests/test_shell.rs:13-14integration-test fixture), and man-pageENVIRONMENTtext insrc/shell.rs:640, 653, 658(Path to the apcore extensions directory→Path to the extensions directory,Global apcore logging verbosity→Global logging verbosity,API key for authenticating with the apcore registry→API key for authenticating with the registry). README's--verboserow updated to match. Thetest_generate_man_page_name_uses_descriptionassertion updated to the new "about" text. Logger fields, source comments, doc comments, and environment-variable identifiers (APCORE_*) are unchanged — only descriptive copy that appears in--help, shell completion, andmanoutput. Cross-SDK parity with Python 0.8.1 and TypeScript 0.8.2.
- Global
--verboseflag renamed to--all-options— The help-display flag is now--all-options; useapcore-cli module --help --all-optionsto reveal hidden built-in options.verboseis removed from the reserved schema property names set — module schemas may now freely defineverbose: booleanfor runtime output control. Public API:set_verbose_help/is_verbose_helprenamed toset_all_options_help/is_all_options_help; statics renamed accordingly. Tracked in apcore-cli#21.
apcore-toolkitpromoted from optional Cargo feature to REQUIRED runtime dependency (>=0.7.0). Thetoolkitfeature flag is retained indefaultfeatures for backward compatibility — existing#[cfg(feature = "toolkit")]gates continue to work — but consumers usingdefault-features = falsemust explicitly enablefeatures = ["toolkit"]to compile. Reqired because csv/yaml/jsonl now route through the toolkit's reference implementation.serde_json::Mapiteration order — transitively switched to insertion-order via the toolkit'spreserve_orderfeature. Test assertions that relied on alphabetical iteration (tests/test_output.rs::test_csv_plain_value_passthrough) updated to expect insertion-order.
csv_scalar_stringandcsv_fieldprivate helpers — replaced byapcore_toolkit::format_csv()and the toolkit's RFC 4180 internals.
See ADR-09 in apcore-cli/docs/tech-design.md for the byte-equivalent toolkit-delegated tier rationale.
- D10-001 (critical) —
AuthProvider::authenticate_requestrejects trailing CR/LF in the API key (src/security/auth.rs). Previous behaviour stripped trailing\r/\nbefore the malformed-key check, allowing a key ending in"\n"to silently produceAuthorization: Bearer <stripped>and exposing the SDK to header-injection vectors. Python and TypeScript both reject any\ror\nat any position; Rust now matches. The regression testtest_authenticate_request_strips_trailing_crlfwas asserting the wrong behaviour and has been renamed totest_authenticate_request_rejects_trailing_crlfwith the assertion inverted. - D10-truncated #1 —
ConfigEncryptor::retrievesurfaces a user-actionable decryption error (src/security/config_encryptor.rs). Allretrieve()-time decryption failures (b64 decode, v1 AES, v2 AES) now route through a newConfigEncryptorError::DecryptFailed { key }variant with the spec'd message"Failed to decrypt configuration value '{key}'. Re-configure with 'apcore-cli config set {key}'."Previously, the most common decryption failure modes leaked the internalAuthTagMismatchmessage and dropped both the originating config key and the remediation guidance.AuthTagMismatchis preserved as the internal-helper variant returned from_aes_decrypt_v1/_aes_decrypt_v2. Cross-SDK parity with Python (config_encryptor.py:62-64,70-72) and TypeScript (config-encryptor.ts:136-149). - D11-001 — Built-in-group rename surface (
src/builtin_group.rs,src/cli.rs,src/main.rs,src/lib.rs). Restores FE-13 P0 parity with PythonApcliGroup.nameand TypeScriptApcliGroup#name. Newpub fn ApcliGroup::name(&self) -> &straccessor backed by aname: Stringfield that defaults to"apcli"and is validated againstNAME_REGEX = ^[a-z][a-z0-9_-]*$. New factory variantsfrom_cli_config_with_name/from_yaml_with_name/try_from_yaml_with_nameacceptname: Option<String>; the original 2-arg factories delegate withNonefor backward compatibility. Newvalidate_builtin_group_namehelper andApcliGroupError::InvalidNamevariant. New module-leveleffective_reserved_group_names,is_reserved_group_name, andpub fn set_reserved_group_names(...)(mirrors TypeScriptsetReservedGroupNames);cli.rs::build_module_command_with_limitconsults the live set so a renamed built-in group is honoured at collision-check time. Binary entry-point seeds the live set fromapcli_cfg.name()and threads the resolved name through theclap::Command::new(...)builder. - D11-W3 — Sandbox canonicalises inherited
APCORE_EXTENSIONS_ROOT(src/security/sandbox.rs:248). The child env now carries an absolute, symlink-resolved path so sandboxed processes cannot escape via a relative or symlink-bait extensions root. - D11-002 —
sorted_jsonrecurses into nested objects and arrays for hash canonicalisation (src/security/audit.rs:20). Previously only top-level keys were sorted, so audit-log input hashes diverged for inputs with nested structures. Aligns Rust with the Python and TypeScript canonicalisation contract.
-
D11-001 —
pub fn set_reserved_group_names(names: &[String])module-level setter onbuiltin_group(mirrors TypeScriptsetReservedGroupNames) plus theApcliGroup::name()accessor andfrom_cli_config_with_name/from_yaml_with_name/try_from_yaml_with_namefactory variants. See Security entry above for the full surface. -
D1-004 —
Sandbox::with_extensions_root(...)andSandbox::with_max_output_bytes(...)builder methods (src/security/sandbox.rs). Cross-SDK parity with PythonSandbox.with_extensions_root/with_max_output_bytes(apcore-cli-python/src/apcore_cli/security/sandbox.py:85,95).extensions_rootoverrides any inheritedAPCORE_EXTENSIONS_ROOTenv var with a canonicalised absolute path;max_output_bytesreplaces theSANDBOX_OUTPUT_SIZE_LIMIT_BYTESconstant as the per-instance output cap. Both fields are wired through to_sandboxed_execute. 5 new unit tests + 4 new integration tests cover field defaults, single-setter behaviour, fluent chaining, and the disabled-path passthrough invariant. -
CliError::SchemaParserFailure { module_id, source }variant insrc/cli.rs— wrapsSchemaParserError::ReservedPropertyNameand::FlagCollisionso both route toEXIT_SCHEMA_CIRCULAR_REF(48) viaCliError::exit_code(). Previously these errors were re-wrapped asCliError::InvalidModuleIdand exited with code 2, breaking cross-SDK exit-code parity with Pythonsys.exit(48)and TypeScriptprocess.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF). Audit D11-NEW-005 (see Fixed). -
--format markdownand--format skillforapcli listandapcli describe(issue aiperceivable/apcore-cli#20), gated behind thetoolkitCargo feature. Both delegate toapcore_toolkit::format_module(s)(≥0.6) so the output is byte-identical to the same toolkit call in the Python and TypeScript SDKs.--format skillemits vendor-neutral SKILL.md content directly loadable by Claude Code (.claude/skills/<id>/SKILL.md) and Gemini CLI (.gemini/skills/<id>/SKILL.md):apcore-cli apcli describe users.create --format skill > .claude/skills/users.create/SKILL.mdA new internal
descriptor_to_scanned()helper adapts the registry's JSON module-descriptor shape to the toolkit'sScannedModuletype. When thetoolkitfeature is disabled, requestingmarkdownorskilllogs a warning and falls back tojson. -
Issue #17 —
system_usageaggregator +list --sort calls|errors|latency: new modulesrc/system_usage.rsreads~/.apcore-cli/audit.jsonl, filters by period (default 24h), and returns per-module aggregates (calls,errors,avg latency_ms).list --sort {calls,errors,latency}now consults the aggregator instead of falling back to id-sort with a buriedtracing::warn!. When the audit log has no entries in the period window the discovery layer prints a user-visible note to stderr (note: no usage data available for --sort <field>; sorted by id. ...) and falls back to id-sort. Module-protocol registration ofsystem.usage.summary/system.usage.moduleas registry-callable built-ins is tracked as a follow-up — today the readers are invoked directly by the discovery layer. -
New file:
src/system_usage.rs. -
Issue #18 + #19 — Rust parity: new
pub fn create_cli_with(extensions_dir, prog_name, host_version, host_description) -> clap::Commandlives in the binary entry point (src/main.rs) — embedding API is BIN-only in v0.8 pending the post-D9 redesign.host_version=Some(v)overrides-V/--version;host_description=Some(d)overrides the top-level--help"About" line. Issue #18 opt-in semantics: whenhost_versionisNone,--versionis NOT registered — embedded callers no longer leak the SDK's ownCARGO_PKG_VERSION. The standaloneapcore-clibinary explicitly passesSome(env!("CARGO_PKG_VERSION").to_string())so its--versionflag remains wired. Whenhost_descriptionis omitted, the surface defaults tof"{prog_name} CLI". Rationale: the embedding API was removed in v0.7.0 (D9-001/D9-002), but parameterizing the builder now means downstream Rust hosts experimenting withapcore-clias a library do not have to fork the crate, and the re-introduced embedding API can route through this seam without further signature churn. -
Issue #19 — debrand standalone help strings: the top-level
--helpdescription, theapclisubgroup description, the--verboseoption text, the rootafter_helpfooter, and the per-module verbose-hint footer incli.rsno longer hard-code "apcore" in their phrasing. The description defaults tof"{prog_name} CLI"(matches TS / Python), and the four(including built-in apcore options)strings drop the trailingapcore. Standalone bin still uses the SDK package name forprog_nameby default, so the publicapcore-cli --helpoutput is unchanged in spirit; downstream hosts now get a neutral surface out of the box. -
D5-002 — Dedicated unit tests for
builtin_groupanddisplay_helpers(tests/test_builtin_group.rs,tests/test_display_helpers.rs). 10 tests coverAPCLI_SUBCOMMAND_NAMES/RESERVED_GROUP_NAMESconstants, all fourfrom_cli_configmodes, both auto-detect branches, and bothtry_from_yamlvalidation errors. 6 tests cover display-block extraction, alias precedence chain, and tag fallback chain. -
D11-NEW-001 / D11-NEW-003 —
ref_resolverpreserves parent siblingrequiredinanyOf/oneOf(src/ref_resolver.rs).resolve_nodenow extractssibling_requiredfrom the parent before the branch loop and merges it sibling-first deduplicated with the branch intersection at the end;merged.propertiesis also seeded from the parent (parity with the existingallOfbranch handling). 3 new regression tests coveranyOf,oneOf, and dedup overlap. Matches Pythonref_resolver.py:100-118. -
Documented parity gap for the built-in-group rename featureinsrc/lib.rs(now superseded by D11-001 above — kept here for the comment block listing the implementation requirements that have since landed). -
D1-006 — Documented
allowed_prefixesparity gap insrc/lib.rs. TypeScriptcreateCligainedallowedPrefixes(commit0f2e08a); Rust cannot mirror it until the high-level embedding factory (removed in v0.7.0 D9-001/002) returns. The lib-level cross-SDK parity note now records that TypeScript is no longer missing it and Rust is the sole gap.
- D6-W1 —
serde_yamlreplaced withserde_yaml_ng = "0.10"(Cargo.toml:29). Upstreamserde_yamlwas deprecated;serde_yaml_ngis the maintained drop-in replacement. No API surface change. - D6-003 —
apcorepin policy relaxed from=0.21.0to0.21(minor floor), aligning withapcore-cli-python(>=0.21.0) andapcore-cli-typescript(>=0.21.0). - Dependency bumps —
nix 0.29 → 0.30.1,thiserror 1 → 2.0.18,comfy-table 6 → 7.2.2(transitive:crossterm 0.26 → 0.29,unicode-width 0.1 → 0.2). Makefilecoveragetarget now passes--fail-under-lines 85tocargo llvm-cov, matching the Pythonpyproject.toml[tool.coverage.report] fail_under = 85and the new TypeScriptvitest.config.tsthresholds.lines: 85. Cross-SDK CI parity (audit D5-004).apcli listandapcli describe--formatvalue-parsers expanded to[table, json, csv, yaml, jsonl, markdown, skill].describepreviously accepted only[table, json]. Unknown values exit with code 2 (clap rejection) as before. Issue aiperceivable/apcore-cli#20.- Dependency bump:
apcore = "0.21"(was=0.19.0) and the optionalapcore-toolkit = "=0.6.0"(was=0.5.0). Aligns with upstreamapcore 0.21.0(Module::preview/PreflightResult::predicted_changes) andapcore-toolkit 0.6.0(surface-aware formatters). No CLI-visible behavioural breaks. - D8-W1 —
Cargo.lockis now tracked in git. Per Cargo guidance, the lockfile must be committed for crates that ship a[[bin]]target to guarantee reproducible binary builds. The lockfile was previously gitignored. - D9-W5 —
register_completion_commandno longer takesprog_name(src/shell.rs:79). The parameter was unused; signature now matches the TypeScriptregisterCompletionCommandcontract.
- D11-NEW-005 —
schema_to_clap_argsErr(SchemaParserError::*)was mapped to exit code 2, not 48. The call site insrc/cli.rs:425previously wrapped bothReservedPropertyNameandFlagCollisionasCliError::InvalidModuleId, which exits 2. Both are spec-defined exit-48 schema-validity errors perapcore-cli/docs/features/schema-parser.mdContract:schema_to_click_optionsErrors (cross-SDK parity with Pythonsys.exit(48)and TSprocess.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF)). Fix routes through the newCliError::SchemaParserFailurevariant. - D9-NEW-002 —
merge_allofdid not deduplicaterequiredacross branches. The function concatenated each branch'srequiredarray via.extend(), producing duplicates when two branches independently required the same field name. Spec mandates first-seen-wins dedup (matching TypeScript[...new Set(...)]and Python's new explicit seen-set). Fix: replace.extend()with afor item in req { if !merged_required.contains(item) { merged_required.push(...) } }loop. Outerobj.requiredparent-vs-branches dedup at line 244-251 was already correct. - D10-002 —
resolve_refsexit-code split (src/cli.rs:69).RefResolverError::Unresolvablenow exits45(EXIT_SCHEMA_REF_UNRESOLVABLE) whileRefResolverError::CircularandRefResolverError::MaxDepthExceededexit48(EXIT_SCHEMA_CIRCULAR_REF). Previously all three collapsed onto a single exit code, breaking cross-SDK parity with Pythonsys.exit(45)/sys.exit(48)and the TypeScriptEXIT_CODES.SCHEMA_REF_UNRESOLVABLE/SCHEMA_CIRCULAR_REFsplit. - D10-W1 + D11-W5 —
schema_parserflag-collision check probesseen_flagsbefore inserting the synthetic--no-X(src/schema_parser.rs:280), and the collision message now references the original boolean property name instead of the negated form. Cross-SDK message parity. - D10-truncated #3 — Clarified
CliApprovalHandler::check_approvalshadow (src/approval.rs). Added a doc-comment disambiguation table covering bothcheck_approvaloverloads (the inherent method that takes&Valueand is an alias forrequest_approval, and theapcore::ApprovalHandlertrait impl that takes&strand implements the spec's Phase B polling protocol returning"rejected — CLI does not support async polling"). The previous comment claimed the inherent method "matches the Python/TypeScriptcheck_approvalmethod name", which was misleading. Doc-only change. - D11-W1 —
ConfigEncryptorusername fallback chain extended toUSER → LOGNAME → USERNAME(src/security/config_encryptor.rs:233) for Windows parity with the Python and TypeScript SDKs. - D9-W3 —
register_discovery_commandsdeleted;cmd_listdemoted topub(crate)(src/discovery.rs:313). The wrapper had no remaining callers and exposed an internal helper that was never part of the spec'd surface. - D10-info-1 —
APCORE_CLI_APCLIenv value is now trimmed before lowercase normalisation (src/builtin_group.rs:633). Spec invariant 2 (apcore-cli/docs/features/builtin-group.md) requires the env-var parser to be both case-insensitive and trim-on-read; values like" show "or"\thide\n"now resolve to"all"/"none"instead of hitting the warn-and-fallthrough branch. Pure-whitespace strings collapse to"unset"(parity with the empty-string short-circuit) rather than warning. - D11-010 —
AuditLoggerwrite-failure warnings are deduplicated. Repeated IO failures against the sameAuditLoggerinstance now emit"Could not write audit log"at most once; subsequent failures fall through totracelevel. The dedup flag lives inArc<AtomicBool>so clones share state, matching TypeScript_writeFailureWarnedand Python_write_failure_warned(src/security/audit.rs:227). - D11-011 —
ExposureFilteracceptsmode = "none"silently (src/exposure.rs:59). Python and TypeScript treat"none"as a legitimate user-supplied value (hides every module); Rust was warning"Unknown ExposureFilter mode 'none'"and clamping back to"none". The end-state was identical, but the spurious warning broke log-noise parity."none"is now in theVALID_MODESwhitelist; truly unknown modes still warn-and-clamp (fail-closed).
- D9-003 — FE-13 §11.2 root-level deprecation shims. The 13 hidden
root-level shim subcommands (
list,describe,exec,validate,init,health,usage,enable,disable,reload,config,completion,describe-pipeline) that forwarded toapcli <name>with a deprecation warning were removed per spec §11.3 ("Removed in v0.8"). Callers must now useapcli <name>. TheDEPRECATED_ROOT_COMMANDSconst,print_deprecation_warning,build_apcli_group_for_dispatch,forward_shim_args, andparse_shim_forhelpers insrc/main.rswere deleted along with the registration loop and 13 dispatch arms. - D6-002 —
tokio-test = "0.4"dev-dependency removed. The crate had zero references acrosssrc/,tests/, andexamples/;#[tokio::test]macros come from tokio's ownmacrosfeature. - D9-W3 —
register_discovery_commandswrapper removed fromsrc/discovery.rs. See Fixed entry above.
- Removed
run_with_configandCliConfigfrom the public surface — both were stubs and unwired (D9-001, D9-002).run_with_configreturned 1 with a "not yet implemented" message in every branch;CliConfigdeclaredcommands_dir,binding_path,group_depth,expose, andapclifields that no code path read. The embedding API will be reintroduced when actually implemented.CliConfigErrorwas removed alongside. - Removed
EXIT_CONFIG_NAMESPACE_DUPLICATEconstant alias (D9-003) — useEXIT_CONFIG_NAMESPACE_RESERVEDfor exit code 78.
- Cross-language conformance test (
tests/conformance_apcli_visibility.rs) consuming the shared apcli-visibility fixtures from theaiperceivable/apcore-clispec repo (conformance/fixtures/apcli-visibility/). One#[test]per canonical scenario (standalone-default,embedded-default,cli-override,env-override,yaml-include). Asserts apcli group visibility and subcommand registration against each fixture'screate_cli.json/env.json/input.yamlinputs. A process-globalMutexguards scenarios that touchAPCORE_CLI_APCLI/cwd. Byte-matching againstexpected_help.txtis gated behind#[ignore]until the canonical clap v4 / GNU-style help formatter is ported — tracked for parity withapcore-cli-typescript/src/canonical-help.ts. APCORE_CLI_SPEC_REPOenv var — overrides the spec-repo lookup path for conformance fixtures. Defaults to a sibling checkout (../apcore-cli/). The test is a no-op (prints a skip notice and returns) when the spec repo is absent.- New
[[test]]entry inCargo.tomlregistering the conformance test binary. - FE-12: Module Exposure Filtering — Declarative control over which discovered modules are exposed as CLI commands.
ExposureFilterstruct inexposure.rswithis_exposed(&self, module_id)andfilter_modules(&self, ids)methods.- Three modes:
All(default),Include(whitelist),Exclude(blacklist) with glob-pattern matching. ExposureFilter::from_config(value)constructor for loading fromapcore.yamlexposesection.list --exposure {exposed,hidden,all}filter flag in discovery commands.GroupedModuleGroupintegration: applies exposure filter during command registration.ConfigResolvergainsexpose.*config keys.- 3-tier config precedence:
--expose-modeCLI flag > env var >apcore.yaml. (The fourthCliConfig.exposetier was removed alongsideCliConfig— see the Removed section above.) - Hidden modules remain invocable via
exec <module_id>.
- New file:
exposure.rs.
- Correctly propagate executor errors by moving
map_errinside theblock_in_placescope.
- CI — spec-repo checkout:
.github/workflows/ci.ymlnow checks outaiperceivable/apcore-cliinto.apcore-cli-spec/and exposes it tocargo testviaAPCORE_CLI_SPEC_REPO. Mirrors the pattern inapcore-cli-python/apcore-cli-typescript. - Dependency bump: requires
apcore = 0.18.0(was0.17.1). MAX_MODULE_ID_LENGTHupdated to 192 (was 128) —cli.rsconstantMODULE_ID_MAX_LENandvalidate_module_idalready tracked the upstream spec change.describe-pipelinerendering updated to build aStrategyInfovalue (newapcore 0.18.0type) from preset step data and use itsname/step_count/step_namesfields for display. Header format:Pipeline: <name> (<n> steps).FsDiscoverer::discoversignature updated todiscover(&self, _roots: &[String])to match the newapcore::registry::Discoverertrait contract (discover(roots: &[String])).Registry::discover(&discoverer)now returnsusize(module count) instead ofVec<String>— updatedmain.rsandfs_discoverer.rstests accordingly.Registry::get_definitionnow returnsOption<ModuleDescriptor>(owned) instead ofOption<&ModuleDescriptor>— removed unnecessary.cloned()call indiscovery.rs.- Centralized CLI dispatch flags and builtin command definitions to improve maintainability.
- Dependency bump: requires
apcore = 0.17.1(was0.15.1). Adds Execution Pipeline Strategy, Config Bus enhancements, Pipeline v2 declarative step metadata,minimalstrategy preset. CliConfig::group_depthdefault changed from 0 to 1 (customDefaultimpl).- Error tuple in executor path changed to
(i32, String, Option<Value>)to carry structured error data for FE-11 enhanced error output.
- FE-11: Usability Enhancements — 11 new capabilities:
--dry-runpreflight mode. Standalonevalidatecommand invalidate.rswithformat_preflight_result()andfirst_failed_exit_code().- System management commands:
health,usage,enable,disable,reload,config get/config setinsystem_cmd.rs. Graceful no-op when system modules unavailable. - Enhanced error output:
emit_error_json()/emit_error_tty()with structured guidance fields fromOption<&Value>. --tracepipeline visualization with timing data.CliApprovalHandlerstruct inapproval.rs.--approval-timeout,--approval-tokenflags.--streamJSONL output.- Enhanced
listcommand:--search,--status,--annotation,--sort,--reverse,--deprecated,--deps,--flat.ListOptionsstruct. --strategyselection:standard,internal,testing,performance,minimal.describe-pipelinecommand instrategy.rswith Pure/Removable/Timeout columns.- Output format extensions:
--format csv|yaml|jsonl,--fieldsdot-path field selection.format_module_list_with_deps(). - Multi-level grouping:
CliConfig::group_depth. - Custom command extension:
CliConfig::extra_commands: Vec<clap::Command>.
- New error code constant:
EXIT_CONFIG_ENV_MAP_CONFLICT. - New files:
system_cmd.rs,strategy.rs,validate.rs. BUILTIN_COMMANDSexpanded to 14 entries.KNOWN_BUILTINSinshell.rsupdated to match.RESERVED_FLAG_NAMESexpanded with all FE-11 flag names.
- Pre-populated registry support —
CliConfigstruct with optionalregistry(pre-populatedRegistryProvider) andexecutor(pre-builtModuleExecutor) fields. When provided, downstream binaries can skip filesystem discovery entirely. This enables frameworks that register modules at runtime (e.g. apflow's bridge) to generate CLI commands from their existing registry. CliConfigexported from crate root withDefaultimpl.
- Verbose help mode — Built-in apcore options (
--input,--yes,--large-input,--format,--sandbox) are now hidden from--helpoutput by default. Pass--help --verboseto display the full option list including built-in options. - Universal man page generation —
build_program_man_page()generates a complete roff man page covering all registered commands.--help --manoutputs the man page, enabling downstream projects to get man pages for free. - Documentation URL support —
set_docs_url()sets a base URL for online docs. Per-command help showsDocs: {url}/commands/{name}, man page SEE ALSO includesFull documentation at {url}. No default — disabled when not set.
build_module_command_with_limit()andadd_dispatch_flags()respect the global verbose help flag to control built-in option visibility.--sandboxis now always hidden from help (not yet implemented). Only four built-in options (--input,--yes,--large-input,--format) toggle with--verbose.- Improved built-in option descriptions for clarity.
- Grouped CLI commands (FE-09) —
GroupedModuleGrouporganizes modules into nested subcommand groups by namespace prefix, enablingapcore-cli <group> <command>invocation. - Display overlay helpers —
get_display()andget_cli_display_fields()resolve alias, description, and tags frommetadata["display"]. - Init command (FE-10) —
apcore-cli init module <id>scaffolds new modules with--style(decorator/convention/binding),--dir, and--descriptionoptions. - Grouped shell completions — Bash, Zsh, and Fish completions now support two-level group/command completion via
_APCORE_GRP. - Optional apcore-toolkit integration —
DisplayResolverandRegistryWriterviatoolkitfeature flag with graceful fallback. - Path traversal validation —
--dirrejects paths containing..components.
BUILTIN_COMMANDSupdated to includeinit(6 items, sorted).APCORE_AUTH_API_KEYadded to man page ENVIRONMENT section.- Dependency bump:
apcore >= 0.14.
- Rebrand: aipartnerup → aiperceivable
- Help text truncation limit increased from 200 to 1000 characters (
HELP_TEXT_MAX_LENconstant) cli.help_text_max_lengthconfig key added toConfigResolver::DEFAULTS(default: 1000)logging.leveldefault changed from"INFO"to"WARNING"inConfigResolver::DEFAULTS— aligns with Python/TypeScript SDKs and updated spec
extract_help_with_limit— configurable-limit variant ofextract_help(schema_parser.rs)schema_to_clap_args_with_limit— configurable-limit variant ofschema_to_clap_args(schema_parser.rs)build_module_command_with_limit— acceptshelp_text_max_lengthparameter (cli.rs)HELP_TEXT_MAX_LENconstant exported from crate root (lib.rs)- Test:
test_extract_help_truncates_at_1000 - Test:
test_extract_help_no_truncation_within_limit - Test:
test_extract_help_custom_max_length - Test:
test_help_truncated_at_1000_chars(integration) - Test:
test_help_within_limit_not_truncated(integration) - 459 tests (up from 458)
Core Features (ported from apcore-cli-python 0.2.0)
- ConfigResolver — 4-tier configuration precedence (CLI flag > env var > YAML file > defaults)
- Core Dispatcher —
validate_module_id,collect_input(STDIN + CLI merge, 10MiB limit),LazyModuleGroup(lazy command cache),build_module_command(schema-to-clap),dispatch_module(full execution pipeline with SIGINT handling) - Schema Parser —
schema_to_clap_argsconverting JSON Schema to clapArginstances, boolean flag pairs (--flag/--no-flag), enum choices withPossibleValuesParser,reconvert_enum_valuesfor type coercion,extract_helpwith 200-char truncation - Ref Resolver —
resolve_refswith$refinlining,allOfmerge,anyOf/oneOfintersection, depth limit (32), circular detection - Output Formatter — TTY-adaptive rendering (
comfy-tablefor terminals, JSON for pipes),format_module_list,format_module_detail,format_exec_result,resolve_format,truncate - Discovery —
listcommand with AND tag filtering,describecommand with exit-44 on not found,RegistryProvidertrait,ApCoreRegistryProvideradapter - Approval Gate — TTY-aware HITL prompts,
--yesandAPCORE_CLI_AUTO_APPROVE=1bypass, 60stokio::select!timeout,NonInteractiveerror for non-TTY, all variants exit 46 - Shell Integration —
completioncommand (bash/zsh/fish/elvish/powershell viaclap_complete),mancommand (roff format with EXIT CODES and ENVIRONMENT sections) - Security —
AuthProvider(env/config/keyring with Bearer header),ConfigEncryptor(AES-256-GCM + PBKDF2, keyring fallback),AuditLogger(JSONL append, salted SHA-256 input hash),Sandbox(tokio subprocess, env whitelist, 300s timeout)
Dispatch & Execution
execsubcommand — first-class clap subcommand for module execution- External subcommand routing —
apcore-cli math.add --a 5routes throughdispatch_module - Schema-derived flags — external subcommands look up module descriptor to build
--a,--betc. frominput_schema FsDiscoverer— recursively scans extensions directory formodule.jsondescriptors- Script-based execution — modules with
run.shnext tomodule.jsonexecute as subprocesses (JSON stdin/stdout protocol) - Path-traversal validation — executable paths canonicalized and verified to stay within extensions root
Examples
- 8 example modules:
math.add,math.multiply,text.upper,text.reverse,text.wordcount,sysutil.info,sysutil.env,sysutil.disk - Each module has
module.json(descriptor) +run.sh(execution script) examples/run_examples.sh— runs all 15 demo scenariosexamples/README.md— module authoring guide
Developer Experience
Makefilewithsetup,build,check(fmt + clippy + tests),cleantargets.bin/local binary directory to avoid PATH conflict with Pythonapcore-cli- Pre-commit hook (fmt, clippy, check-chars)
- 458 tests across 17 test files, 0 failures
cargo clippy --all-targets --all-features -- -D warningsclean
Infrastructure
- 10 exit codes matching the apcore protocol (0, 1, 2, 44, 45, 46, 47, 48, 77, 130)
add_dispatch_flags()shared helper for exec and external subcommand flagstest-supportcargo feature for gating test utilities (MockRegistry,mock_module)- Unified
RegistryProvidertrait (consolidated from separateModuleRegistry+RegistryProvider)
apcore0.13.0clap4 (derive + env + string)tokio1 (rt-multi-thread, macros, time, process, io-util, io-std, signal)serde+serde_json+serde_yaml0.9comfy-table6aes-gcm0.10 +sha20.10 +pbkdf20.12keyring2clap_complete4thiserror1 +anyhow1tracing0.1 +tracing-subscriber0.3reqwest0.12async-trait0.1base640.22,gethostname0.4,chrono0.4,dirs5,tempfile3