Release 0.9.0
Added
tests/conformance/test_snake_case_kwargs.py— runs the cross-language Algorithm C-SNAKE fixture (apcore-cli/conformance/fixtures/snake-case-kwargs/cases.json) againstbuild_module_commandviaclick.testing.CliRunner. Five cases verify that schema property names with underscores (has_solution,sort_by,sort_order) survive the round trip from CLI parse to the input dict received byexecutor.call. No source change required — click natively maps--has-solutiontohas_solution; the Python SDK is the parity reference for the parallel TypeScript fix. Surfaced as part of the cross-SDK regression coverage gap audit.
Fixed (2026-05-13 — cross-SDK audit D10/D11/D1)
ConfigEncryptorLOGNAME key-derivation chain (D10-001 / D11-003) — PBKDF2 username fallback wasUSER → USERNAME → "unknown"(3-tier); nowUSER → LOGNAME → USERNAME → "unknown"(4-tier) matching the spec and Rust. On hosts whereUSERis unset butLOGNAMEis set (cron,sudo -i, container init), ciphertext written by the Python SDK now round-trips correctly with the Rust SDK.src/apcore_cli/security/config_encryptor.py:96, 165.ConfigEncryptor.storekeyring write-failure not wrapped (D11-004) — rawkeyring.set_passwordexceptions now caught and re-raised asConfigDecryptionError, matching TypeScript and Rust.src/apcore_cli/security/config_encryptor.py:31.ref_resolveronly descended intoproperties(D11-001) — recursive schema walk now visits every dict-valued child (items, additionalProperties, patternProperties, if/then/else, not, contains, propertyNames), matching TypeScript and Rust.$refunder array schemas and conditional schemas is now resolved.src/apcore_cli/ref_resolver.py:142.ref_resolvercopy-on-write visited-set (D11-002) — now uses a single mutable set with remove-on-unwind, allowing diamond$refpatterns (two sibling schemas referencing the same$def) to resolve correctly.src/apcore_cli/ref_resolver.py:71.AuditLogger._get_useruses real UID instead of effective UID (D11-010) — switched fromos.getuid()toos.geteuid()so audit records reflect the privileges the process actually runs with undersudo/ setuid binaries. Matches Rust (geteuid) and TypeScript (os.userInfo).src/apcore_cli/security/audit.py:77.check_approvalignoresAPCORE_CLI_APPROVAL_TIMEOUTenv var (D11-012) —CliApprovalHandlerand the legacycheck_approval()wrapper now honor the env var when no explicit timeout is passed (precedence: constructor arg > env var > 60 s default). Matches TypeScript.src/apcore_cli/approval.py.exec --dry-runcrashes withAttributeErrorwhen executor lacksvalidate(D11-013) — guarded withhasattr(executor, "validate"); falls back to synthetic{"valid": True}matching TypeScript.src/apcore_cli/discovery.py:378.CliApprovalHandler.request_approvalmissingrequires_approval=Falseshort-circuit (D11-014) — now returnsapproved/not_requiredwhen the request explicitly carriesrequires_approval=False, matching Rust.src/apcore_cli/approval.py:66.- CLI brand string in auth error messages (D11-006) — remediation strings now say
apcli config set auth.api_key(canonical FE-13 name) instead ofapcore-cli config set auth.api_key.src/apcore_cli/security/auth.py. reconvert_enum_valuesmissing from public re-export (D1-W2) — added to__init__.pyimport block and__all__. Embedders can nowfrom apcore_cli import reconvert_enum_valuesfor parity with TypeScript and Rust.- Ref-resolver error hierarchy missing from public re-export (D1-W3) —
CircularRefError,MaxDepthExceededError,UnresolvableRefError,RefResolverErroradded to__init__.pyimport block and__all__. Parity with TypeScriptindex.ts:82-84. DEFAULT_BUILTIN_GROUP_NAMEmissing from public re-export (D1 re-audit) — added to__init__.py. Parity with Rustlib.rs:190.- Dead exit-code constants removed (D9-W1) —
EXIT_CONFIG_ENV_PREFIX_CONFLICTandEXIT_CONFIG_ENV_MAP_CONFLICT(both = 78, zero callers) deleted fromsrc/apcore_cli/exit_codes.py. - Unused
pytest-asynciodev dependency removed (D6) — the package was declared but never exercised; no async tests exist. Removed from[project.optional-dependencies].dev.
Fixed
- CSV
--format csvPython-repr bug —csv.DictWriterwas called with{k: str(v) for k, v in row.items()}which emitted Python repr{'k': 'v'}(single quotes) for nested dict/list values. The output was not valid JSON and any downstream JSON parser would fail. Now delegates toapcore_toolkit.format_csv(rows)which emits canonical compact JSON.src/apcore_cli/output.py:149, 378. - CSV heterogeneous-keys data loss — header is now the union of keys across all rows (was first-row only via
list(rows[0].keys())). - CSV line terminator — now
\r\nper RFC 4180. - JSONL canonical form — now compact (no spaces between separators), matching the cross-SDK contract. Tests updated.
Changed
- User-visible help/man/completion text no longer leaks the
apcoreframework name to end users of downstream CLIs built on apcore-cli. Affected strings:initgroup description (Scaffold new apcore modules→Scaffold new modules,init_cmd.py:45),--extensions-diroption help (Path to apcore extensions directory.→Path to extensions directory.,factory.py:460), zsh/fish completion descriptions forexec(Execute an apcore module→Execute a module,shell.py:130, 211), and man-pageENVIRONMENTsection text (shell.py:299, 314, 319, 458) — dropsapcorefrom the descriptive copy (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). Logger names, source comments, module docstrings, and environment-variable identifiers (APCORE_*) are unchanged — only descriptive copy that appears in--help, shell completion, andmanoutput. Cross-SDK parity with TypeScript 0.8.2 and Rust 0.8.1.
Changed (breaking CLI surface)
- 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. Internal API:set_verbose_help()renamed toset_all_options_help(); module-level global_verbose_helprenamed to_all_options_help. Tracked in apcore-cli#21.
Changed (breaking dependency semantics)
apcore-toolkitpromoted from optional extra to REQUIRED runtime dependency (>=0.7.0). The previouspip install 'apcore-cli[toolkit]'extras pattern is retained as a no-op for backward compat with install scripts, but the toolkit is now always installed alongside apcore-cli. All--formatoperations route through the toolkit's reference implementation for csv/jsonl/markdown/skill.
Why
See ADR-09 in apcore-cli/docs/tech-design.md for the byte-equivalent toolkit-delegated tier rationale.