Removed
- D9-001 — FE-13 §11.2 deprecation shims removed. The 13 hidden root-level
shims (list,describe,exec,init,validate,health,usage,
enable,disable,reload,config,completion,describe-pipeline)
installed by_register_deprecation_shimsand the__is_deprecation_shim__
collision-handling path inextra_commandswiring have been deleted along
with the_DEPRECATED_ROOT_COMMANDStable. Use the canonical
apcli <command>paths instead. Calls likeapcore-cli listnow exit
non-zero with Click's "No such command" message — the warning window
documented as "removed in v0.8" is closed.
Deprecated
CliModuleNotFoundErroralias — the symbol still resolves to
ModuleNotFoundError(see D1-002 in Changed) but is scheduled for
removal in v0.10.0. Update imports to
from apcore_cli import ModuleNotFoundError.
Security
- D10-001 —
Sandboxper-stream output cap (sandbox.py:155). The previous
implementation summedstdout + stderragainst a singlemax_output_bytes
budget — a runaway child writing only to stderr could starve the stdout
budget and vice versa, and the diagnostic on overflow did not name the
offending stream. Each stream now has an independent byte budget matching
Rust and TypeScript; the overflow error names the stream that tripped the
cap. - D11-W2 —
Sandboxswitched fromsubprocess.runtosubprocess.Popen
with threaded chunked reads (sandbox.py:155).capture_output=True
buffered the entire child stdio into parent memory before the cap was
checked, so a child producing GBs of output could OOM the parent before
the limit was enforced. The new implementation streams stdout/stderr
through reader threads with bounded buffers and kills the child as soon
as either stream exceeds its cap. Memory consumption is now bounded by
2 × max_output_bytesregardless of child output volume. - D11-003 —
ConfigEncryptorv1 decryption honours
APCORE_CLI_CONFIG_PASSPHRASE(config_encryptor.py:128)._aes_decrypt_v1
hard-coded the host:user material, so v1 ciphertext encrypted by the Rust
or TypeScript SDKs under a passphrase failed to decrypt on Python.
Decryption now tries the passphrase-derived key first when the env var is
set, falling back to host:user material — matching TypeScript
aesDecryptV1. Cross-SDK config bundles are now portable. - D11-008 —
AuditLogger._get_userfallback chain now includesLOGNAME
(audit.py:66). The canonical chain persecurity.md(D11-W1) is
getlogin → pwd.getpwuid → USER → LOGNAME → USERNAME → unknown. Python
previously skippedLOGNAME, so audit-loguserfields diverged from
Rust/TS on hosts where onlyLOGNAMEis set (some container runtimes,
cron jobs).
Added
builtin_group_name="apcli"kwarg oncreate_cli— downstream branded CLIs that embed apcore-cli can now expose the built-in commands under a custom namespace (e.g.mycorp-cli admin healthinstead ofmycorp-cli apcli health).ApcliGroupgains anameparameter (with property accessor) threaded throughfrom_cli_config/from_yaml/_build. Default"apcli"is unchanged. Validated against/^[a-z][a-z0-9_-]*$/; invalid values exit 2.RESERVED_GROUP_NAMEScollision check now consultsGroupedModuleGroup._reserved_group_names(instance attribute, defaults to the static frozenset; factory replaces with the resolved name). Env varAPCORE_CLI_APCLIand config keysapcli.*deliberately do NOT rename — they are apcore-cli-internal toggles, not user-facing. Cross-SDK parity with TypeScriptcreateCli({ builtinGroupName }). NewDEFAULT_BUILTIN_GROUP_NAMEconstant exported fromapcore_cli.builtin_group._exit_on_system_error(e)helper insystem_cmd.py— centralizes the canonical error→exit-code mapping for system-management subcommands, replacing 7 sites that previously used baresys.exit(1)(audit D11-B-002, see Fixed).- 5 new tests in
tests/test_builtin_group.py—TestBuiltinGroupRenameclass covers default name, custom name via both factories, validation of valid/invalid name shapes (5 valid + 6 invalid forms each). - D1-001 — 13
register_*_commandfactories +configure_man_help
re-exported fromapcore_clipackage root. Embedders that compose
their own root command tree no longer need to reach into private
submodules (apcore_cli.commands.list_cmd, etc.). All TS/Rust
register_*counterparts now have a Python public-API equivalent. - D1-003 —
apcore_cli.exit_codesmodule with 24EXIT_*integer
constants, anEXIT_CODESmapping dict, and anexit_code_for_error()
helper. Mirrors TSerrors.tsEXIT_CODES+exitCodeForErrorand
Rustsrc/lib.rsEXIT_*constants. Embedders can now map exceptions
to documented exit codes without re-implementing the table. - D1-007 —
format_module_list,format_module_detail,
resolve_formatre-exported from package root. The
output-formatter feature spec declares these as Contracts; previously
onlyformat_exec_resultwas public. - D1-W1 —
APCLI_SUBCOMMAND_NAMESre-exported fromapcore_cli.
Matches Rustlib.rsand is now in__all__for static-analysis
tooling. - D1-W2 —
ApcliConfigTypedDict added to the public surface,
mirroring the TypeScript type alias and Rust struct so embedders have
a static contract for theapcli.*config block. - D1-W3 —
register_config_namespace()helper + module-level
DEFAULTSconstant inconfig.py. The package still registers the
namespace at import time, but embedders can now invoke the helper
explicitly (parity withapcore-cli-typescript). - D1-W5 — Core dispatcher embedder API re-exported from package
root:build_module_command,collect_input,validate_module_id,
set_audit_logger,set_verbose_help,set_docs_url. Embedders no
longer have to import fromapcore_cli.clidirectly. Matches Rust
lib.rs:186-190and TSindex.ts:18. Newtests/test_public_api.py
pins the surface against future drift. - D1-info-1 — typed
ApcliGroupErrorexception
(builtin_group.py:107). Cross-SDK parity with RustApcliGroupError;
embedders previously had no stable error class to match on for
built-in-group config validation.ApcliGroupError(ValueError)
preserves backwards compat — existingexcept ValueErrorcallers
still catch it. The invalid-name regex check in__init__now raises
ApcliGroupError. Re-exported fromapcore_cli.
Fixed
- D11-B-006 —
discovery.py:208sort direction inverted.apcli list --sort calls|errors|latencynow defaults to DESCENDING (highest call count first) per spec T-LST-04, matching Rustdiscovery.rs:209and TypeScriptdiscovery.ts:186. Previously the user's raw--reverseflag (default False) was passed directly tosort_modules_by_usage(..., reverse=...), producing ASCENDING output by default — the inverse of the spec. Fix passesreverse=not reversefor the data path AND adds a re-sort at the call site for the audit-log-empty fallback so id-fallback continues to default ASCENDING per spec. - D11-B-002 —
system_cmd.pycollapsed every error to exit 1. The 7except Exception as e: sys.exit(1)sites bypassed Python's own_ERROR_CODE_MAP(canonical 44/46/47/77) — scripted operators could not distinguish "module not found" from "ACL denied" from generic failure. All 7 sites now route through the new_exit_on_system_error(e)helper which callsexit_code_for_error(e)fromapcore_cli.exit_codes. The 4 audit-log entries previously hardcodingexit_code=1now log the resolved code. - D11-NEW-005 — RESERVED_PROPERTY_NAMES no longer raises generic
ValueError.schema_to_click_optionspreviously raisedValueErrorwhen a schema property collided with a built-in CLI option — opaque to scripted callers and inconsistent with the neighbour flag-collision branch (which already exited 48). Now writes a user-facingError:line to stderr and callssys.exit(48)per spec, matching TSprocess.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF)and RustCliError::SchemaParserFailure → EXIT_SCHEMA_CIRCULAR_REF. Tests tightened frompytest.raises((ValueError, Exception))topytest.raises(SystemExit)withcode == 48assertion. - D9-NEW-002 —
ref_resolver.pyallOf requirednot deduplicated._resolve_node'sallOfbranch concatenated parentrequired+ each branch'srequiredwithout dedup, producing duplicate entries in the merged schema'srequiredarray. JSON Schema validators ignore duplicates so observable validation behaviour was unchanged, but cross-SDK byte-comparison tooling (and theanyOf/oneOfpaths, which already deduped) flagged the divergence. Fix: explicit seen-set dedup preserving first-seen order, matching TS[...new Set(...)]and Rustmerge_allof. - D10-003 —
build_module_commandleakedRefResolverError
tracebacks (cli.py:538). Theresolve_refscatch clause re-raised
unchanged, so callers saw a Python traceback instead of a clean
documented exit code. Now translatesCircularRefError/
MaxDepthExceededErrortosys.exit(48)andUnresolvableRefError
(plus genericRefResolverError) tosys.exit(45), mirroring
schema_parser.py:111and the Rust/TS contracts. - D11-NEW-003 —
ref_resolvermax_depthover-counted plain nested
properties(ref_resolver.py)._resolve_nodepreviously
incrementeddepth + 1when recursing into nestedproperties
values, so a schema with >32 levels of nested objects (no$refat
all) was rejected withMaxDepthExceededError. The spec wording is
"Maximum$refresolution recursion depth" —$refhops along a
single chain, not total stack depth.depthis now only incremented
on$reftraversal, aligning with Rustref_resolver.rs:297. Also
adds 4 regression tests foranyOf/oneOfsibling-required
preservation andanyOfoverlap dedup. - D10-info-1 —
APCORE_CLI_APCLIenv var not trimmed on read
(builtin_group.py:414). Spec invariant 2 requires the parser to be
case-insensitive AND trim-on-read. Surrounding whitespace previously
caused a silent Tier-3/Tier-4 fall-through. Now strips before
lowercasing, matching Rust/TS. - D11-010 —
AuditLoggerwrite-failure warnings deduplicated
(audit.py:55). Previously warned on every failed write, flooding
logs when an audit dir is unwritable. An instance flag now gates the
warning so it fires once per logger instance, matching the TS
writeFailureWarnedflag.
Changed
apcli system *andapcli strategy describe-pipeline--formatchoices
expanded from[table, json]to[table, json, csv, yaml, jsonl], matching
the existingapcli list/apcli execchoice set.markdownandskill
are deliberately excluded from these subcommands — their payloads are
health / strategy results, notScannedModuledata. Issue
#20.- Dependency bump: requires
apcore >= 0.21.0(was>= 0.19.0) and the
optional[toolkit]extra now requiresapcore-toolkit >= 0.6(was>= 0.5).
Aligns with upstreamapcore 0.21.0(Module.preview / PreflightResult.predicted_changes,
ephemeral.* namespace pilot) andapcore-toolkit 0.6.0(surface-aware formatters).
No CLI-visible behavioural breaks — apcore 0.20→0.21 deprecations
(TaskStore.put/save,TaskStatus.RETRYING,CircuitOpenError) keep
legacy aliases for one minor release; the cli does not call those surfaces directly. - D1-002 —
CliModuleNotFoundErrorrenamed toModuleNotFoundError
for cross-language port-ability with TS / RustModuleNotFoundError.
The class intentionally shadowsbuiltins.ModuleNotFoundErrorinside
theapcore_clinamespace. A deprecation alias
CliModuleNotFoundError = ModuleNotFoundErroris kept for backwards
compatibility and will be removed in v0.10.0. Reverses the D2-001
rename which predated the cross-SDK parity policy. - Issue #19 — drop "apcore" branding from embedded-mode
--help:
create_cli()now resolves the top-level CLI description from the new
description=parameter (defaults tof"{prog_name} CLI"), theapcli
subgroup advertises itself asBuilt-in commandsrather than
apcore-cli built-in commands, and the--verboseoption / footer drop
the trailingapcorefrom(including built-in apcore options). Standalone
bin entry (apcore_cli/__main__.py:main()) passes
description="<prog> — execute apcore modules from the command line"
explicitly so the standalone surface is unchanged.
Added
--format markdownand--format skillforapcli listandapcli describe
(issue #20). Both
delegate toapcore_toolkit.format_module(s)(≥0.6) so the output is
byte-identical to the same toolkit call in the TypeScript and Rust SDKs.
--format skillproduces vendor-neutral SKILL.md content directly loadable
by Claude Code (.claude/skills/<id>/SKILL.md) and Gemini CLI
(.gemini/skills/<id>/SKILL.md):
apcli describe users.create --format skill > .claude/skills/users.create/SKILL.mdA new internal adapter _descriptor_to_scanned() maps ModuleDescriptor
(apcore registry) to ScannedModule (apcore-toolkit). A ClickException with
a clear install hint is raised if the optional [toolkit] extra is missing.
- Issue #18 — host-app
--versionopt-in: newversion: str | None = None
parameter oncreate_cli(). When supplied, registers-V/--versionwith
the host's version string. When omitted, the--versionflag is no
longer registered — embedded CLIs that do not opt in stop leaking the
SDK's own version through-V/--version. The standalone bin entry
passesversion=apcore_cli.__version__explicitly so the
apcore-clibinary's behaviour is preserved. - Issue #19 —
description: str | None = Noneoncreate_cli(). - Issue #17 —
system.usageaggregator +list --sort calls|errors|latency:
new moduleapcore_cli.system_usagereads~/.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
buriedlogger.warning. 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 of
system.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:
apcore_cli/system_usage.py.