Releases: aiperceivable/apcore-cli-python
Releases · aiperceivable/apcore-cli-python
Release 0.8.0
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...
Release 0.7.0
Changed
- Dependency bump: requires
apcore >= 0.18.0(was>= 0.17.1). Aligns with upstreamapcore 0.18.0andapcore-toolkit 0.4.2breaking changes. MAX_MODULE_ID_LENGTH128 → 192:validate_module_id()and all references updated to the new 192-character limit introduced inapcore 0.18.0(apcore.registry.registry.MAX_MODULE_ID_LENGTH).describe-pipelinerendersStrategyInfo:executor.describe_pipeline(strategy)now returns aStrategyInfodataclass (name,step_count,step_names,description).strategy.pyupdated to useStrategyInfofields; header line isPipeline: {info.name} ({info.step_count} steps). Falls back gracefully to the legacy_resolve_strategy_namepath whendescribe_pipelineis unavailable.- CI — spec-repo checkout:
.github/workflows/ci.ymlnow checks outaiperceivable/apcore-cliinto.apcore-cli-spec/and exposes it topytestviaAPCORE_CLI_SPEC_REPO. Mirrors the pattern established inapcore-python/apcore-cli-typescript.
Added
create_cli(app=...)parameter:create_cli()accepts an optionalapp: APCoreunified client (introduced inapcore 0.18.0).appis mutually exclusive withregistry/executor(raisesValueError). Whenappis provided,registryandexecutorare extracted fromapp.registryandapp.executor. Filesystem discovery is skipped ifapp.registryalready contains registered modules; otherwise normal discovery proceeds intoapp.registry.- Cross-language conformance test harness (
tests/conformance/) consuming the shared apcli-visibility fixtures from theaiperceivable/apcore-clispec repo (conformance/fixtures/apcli-visibility/). Behavioral assertions (apcli group visibility, registered subcommand set forinclude/excludemodes, always-registeredexec) run today across all five canonical scenarios (standalone-default,embedded-default,cli-override,env-override,yaml-include). Byte-matching againstexpected_help.txtis markedxfailuntil Click'sHelpFormatteris replaced with a canonical clap v4 / GNU-style emitter, 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/). Tests are skipped (not failed) when the spec repo is absent.- FE-12: Module Exposure Filtering — Declarative control over which discovered modules are exposed as CLI commands.
ExposureFilterclass inexposure.pywithis_exposed(module_id)andfilter_modules(ids)methods.- Three modes:
all(default),include(whitelist),exclude(blacklist) with glob-pattern matching. ExposureFilter.from_config(dict)classmethod for loading fromapcore.yamlexposesection.create_cli(expose=...)parameter acceptingdictorExposureFilterinstance.list --exposure {exposed,hidden,all}filter flag in discovery commands.GroupedModuleGroup._build_group_map()integration: callsExposureFilter.is_exposed()to filter command registration.ConfigResolvergainsexpose.*config keys.- 4-tier config precedence:
CliConfig.expose>--expose-modeCLI flag > env var >apcore.yaml. - Hidden modules remain invocable via
exec <module_id>. - New file:
exposure.py.
Release 0.6.0
Changed
- Dependency bump: requires
apcore >= 0.17.1(was>= 0.15.1). Adds Execution Pipeline Strategy, Config Bus enhancements, Pipeline v2 declarative step metadata,minimalstrategy preset. - Schema parser: Required schema properties now correctly enforced at CLI option level (was silently optional).
- Approval gate: Fixed inverted logic in annotation type guard;
check_approval()now acceptstimeoutparameter.
Added
- FE-11: Usability Enhancements — 11 new capabilities:
--dry-runpreflight mode viaExecutor.validate(). Standalonevalidatecommand.- System management commands:
health,usage,enable,disable,reload,config get/config set. Graceful no-op when system modules unavailable. - Enhanced error output: structured JSON with
ai_guidance,suggestion,retryable,user_fixable,details. TTY hides machine-only fields. --tracepipeline visualization viacall_with_trace().CliApprovalHandlerclass implementing apcoreApprovalHandlerprotocol, wired toExecutor.set_approval_handler().--approval-timeout,--approval-tokenflags.--streamJSONL output viaExecutor.stream().- Enhanced
listcommand:--search,--status,--annotation,--sort,--reverse,--deprecated,--deps. --strategyselection:standard,internal,testing,performance,minimal.describe-pipelinecommand.- Output format extensions:
--format csv|yaml|jsonl,--fieldsdot-path field selection. - Multi-level grouping:
cli.group_depthconfig key. - Custom command extension:
create_cli(extra_commands=[...])with collision detection. - New error code:
CONFIG_ENV_MAP_CONFLICT. - New config keys:
cli.approval_timeout(60),cli.strategy("standard"),cli.group_depth(1). - New environment variables:
APCORE_CLI_APPROVAL_TIMEOUT,APCORE_CLI_STRATEGY,APCORE_CLI_GROUP_DEPTH. - New files:
system_cmd.py,strategy.py.
Release 0.5.0
Release version 0.5.0
See CHANGELOG.md for details.
Release 0.4.1
Fixed
- prevent click parameter mismatch by setting expose_value=False for the --man option
Release 0.4.0
Added
- 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.configure_man_help()adds--help --mansupport to any Click CLI, 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.
Changed
build_module_command()respects 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.
Release 0.3.1
Added
- DisplayResolver integration —
__main__.pyintegratesDisplayResolverfromapcore-toolkit(optional) when--bindingoption is provided; gracefully skipped when not installed. inittoBUILTIN_COMMANDS—initsubcommand is now registered in the builtin commands set.APCORE_AUTH_API_KEYto man page — environment variable documented in generated roff man page.- Grouped shell completion with
_APCORE_GRP— bash/zsh/fish completion scripts now support two-level group/command completion via the_APCORE_GRPenvironment variable (shell.py). - Path traversal validation for
--dirininitcommand — rejects paths containing..segments to prevent directory escape (init_cmd.py).
Fixed
RegistryWriterAPI call — constructor now called without parameters; fixesTypeErrorintroduced by upstream API change.
Changed
apcoredependency bumped to>=0.14.0.
Release 0.3.0
Added
- Display overlay routing (§5.13) —
LazyModuleGroupnow readsmetadata["display"]["cli"]for alias and description when building the command list and routingget_command(). Commands are exposed under their CLI alias instead of raw module_id. _alias_map: built frommetadata["display"]["cli"]["alias"](with module_id fallback), enablingapcore-cli alias-nameinvocation._descriptor_cache: populated during alias map build to avoid doubleregistry.get_definition()calls inget_command()._alias_map_builtflag only set on successful build, allowing retry after transient registry errors.- Display overlay in JSON output —
format_module_list(..., "json")now readsmetadata["display"]["cli"]forid,description, andtags, consistent with the table output branch.
Changed
_ERROR_CODE_MAP.get(error_code, 1): guarded withisinstance(error_code, str)to preventNone-key lookup.- Runtime companion:
apcore-toolkit >= 0.4.0enablesDisplayResolverandConventionScanner(graceful fallback when not installed).
Tests
TestDisplayOverlayAliasRouting(6 tests):list_commandsuses CLI alias,get_commandby alias, cache hit path, module_id fallback,build_module_commandalias and description.test_format_list_json_uses_display_overlay: JSON output uses display overlay alias/description/tags.test_format_list_json_falls_back_to_scanner_when_no_overlay: JSON output falls back to scanner values.
Added (Grouped Commands — FE-09)
GroupedModuleGroup(LazyModuleGroup)— organizes modules into nestedclick.Groupsubcommands based on namespace prefixes. Auto-groups by first.segment, withdisplay.cli.groupoverride from binding.yaml._resolve_group()— 3-tier group resolution: explicitdisplay.cli.group> first.segment of CLI alias > top-level._build_group_map()— lazy, idempotent group map builder with builtin collision detection and shell-safe group name validation.format_help()— collapsed root help with Commands, Modules, and Groups sections (with command counts)._LazyGroup(click.Group)— nested group that lazily builds subcommands from module descriptors.list --flatflag — opt-in flat display mode forlistcommand; default is now grouped display.format_grouped_module_list()— Rich table output grouped by namespace.- Updated shell completions — bash/zsh/fish completion scripts handle two-level group/command structure.
Changed (Grouped Commands)
create_cli()now usesGroupedModuleGroupinstead ofLazyModuleGroup.
Tests (Grouped Commands)
- 48 new tests:
TestResolveGroup(8+),TestBuildGroupMap(5+),TestGroupedModuleGroupRouting(7),TestLazyGroupInner(4),TestGroupedHelpDisplay(5),TestCreateCliGrouped(1),TestGroupedE2E(5),TestGroupedDiscovery(7+),TestGroupedCompletion(6).
Added (Convention Module Discovery — §5.14)
apcore-cli init module <id>— scaffolding command with--style(decorator, convention, binding) and--descriptionoptions. Generates module templates in the appropriate directory.--commands-dirCLI option — path to a convention commands directory. When set,ConventionScannerfromapcore-toolkitscans for plain functions and registers them as modules.
Tests (Convention Module Discovery)
- 6 new tests in
tests/test_init_cmd.pycovering all three styles and options.
Release 0.2.2
Changed
- Rebrand: aipartnerup → aiperceivable
Release 0.2.1
Changed
- Help text truncation limit increased from 200 to 1000 characters (configurable via
cli.help_text_max_lengthconfig key) _extract_help: addedmax_length: int = 1000parameter (schema_parser.py)schema_to_click_options: addedmax_help_length: int = 1000parameter (schema_parser.py)build_module_command: addedhelp_text_max_length: int = 1000parameter, threaded through to schema parser (cli.py)LazyModuleGroup: constructor acceptshelp_text_max_length: int = 1000, passes tobuild_module_command(cli.py)create_cli: resolvescli.help_text_max_lengthfromConfigResolverand passes toLazyModuleGroup(__main__.py)format_exec_result: nested dict/list values in table mode now rendered withjson.dumpsinstead ofstr()(output.py)
Added
cli.help_text_max_lengthconfig key (default: 1000) inConfigResolver.DEFAULTS(config.py)APCORE_CLI_HELP_TEXT_MAX_LENGTHenvironment variable support for configuring help text max lengthtest_help_truncation_default: tests default 1000-char truncationtest_help_no_truncation_within_limit: tests no truncation at 999 charstest_help_truncation_custom_max: tests custom max_length parameter- 263 tests (up from 261)