apcore-cli 0.10.5__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/CHANGELOG.md +143 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/PKG-INFO +3 -3
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/pyproject.toml +3 -3
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/__main__.py +5 -0
- apcore_cli-0.12.0/src/apcore_cli/_sandbox_runner.py +38 -0
- apcore_cli-0.12.0/src/apcore_cli/acl_cmd.py +458 -0
- apcore_cli-0.12.0/src/apcore_cli/acl_loader.py +554 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/approval.py +54 -21
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/builtin_group.py +4 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/cli.py +81 -54
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/config.py +29 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/discovery.py +70 -18
- apcore_cli-0.12.0/src/apcore_cli/exit_codes.py +126 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/factory.py +166 -3
- apcore_cli-0.12.0/src/apcore_cli/openapi_cmd.py +499 -0
- apcore_cli-0.12.0/src/apcore_cli/openapi_source.py +275 -0
- apcore_cli-0.12.0/src/apcore_cli/security/audit.py +177 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/security/config_encryptor.py +1 -1
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/security/sandbox.py +46 -2
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/strategy.py +25 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/system_cmd.py +35 -13
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/conformance/test_snake_case_kwargs.py +4 -4
- apcore_cli-0.12.0/tests/conftest.py +64 -0
- apcore_cli-0.12.0/tests/test_acl_cmd.py +936 -0
- apcore_cli-0.12.0/tests/test_acl_loader.py +811 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_approval.py +219 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_bugfixes.py +7 -7
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_builtin_group.py +15 -1
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_config.py +30 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_discovery.py +79 -4
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_discovery_fe13.py +2 -2
- apcore_cli-0.12.0/tests/test_exit_codes.py +138 -0
- apcore_cli-0.12.0/tests/test_openapi_cmd.py +603 -0
- apcore_cli-0.12.0/tests/test_openapi_source.py +320 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_system_cmd.py +117 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_toolkit_integration.py +61 -0
- apcore_cli-0.10.5/src/apcore_cli/_sandbox_runner.py +0 -25
- apcore_cli-0.10.5/src/apcore_cli/exit_codes.py +0 -74
- apcore_cli-0.10.5/src/apcore_cli/security/audit.py +0 -86
- apcore_cli-0.10.5/tests/conftest.py +0 -28
- apcore_cli-0.10.5/tests/test_exit_codes.py +0 -58
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.github/copilot-ignore +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.gitignore +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.gitmessage +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/CLAUDE.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/LICENSE +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/Makefile +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/README.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/README.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/examples/run_examples.sh +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/approval-gate.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/config-resolver.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/discovery.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/exposure-filtering.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/output-formatter.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/overview.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/schema-parser.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/security-manager.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/shell-integration.md +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/planning/state.json +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/exposure.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/init_cmd.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/shell.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/system_usage.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/src/apcore_cli/validate.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/__init__.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/conformance/__init__.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/conformance/test_apcli_visibility.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/shell_test_utils.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_apcli_integration.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_cli.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_display_helpers.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_e2e.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_exposure.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_factory_fe13.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_integration.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_list_command_filters.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_output.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_output_format_exec.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_output_format_markdown_skill.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_public_api.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_sandbox_runner.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_security/test_sandbox.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_shell.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_strategy.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_system_usage.py +0 -0
- {apcore_cli-0.10.5 → apcore_cli-0.12.0}/tests/test_validate.py +0 -0
|
@@ -5,6 +5,149 @@ All notable changes to apcore-cli (Python SDK) will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.12.0] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
Two features: **FE-14 ACL Governance** and **FE-15a OpenAPI Import**. Bumps the required `apcore` floor to `0.30.0` and `apcore-toolkit` to `[http-proxy]>=0.11.1`. Full suite: 1004 passed, 5 xfailed (815 → 1004; 189 new tests). `APCLI_SUBCOMMAND_NAMES` grows from 13 to 15.
|
|
11
|
+
|
|
12
|
+
**Why a minor rather than a patch.** FE-14 makes a code path *live* that has been inert since the CLI existed. A project that already ships `acl/global_acl.yaml` and reasonably assumed it was in force now actually gets enforcement: the `acl_check` pipeline step begins returning real verdicts, calls can exit `77`, and the `acl` row in `--dry-run` starts reporting. Nothing changes for a project with no ACL root — that is the whole design (see the first bullet under **Added**) — but for a project with one, observable behaviour changes.
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- **`--sandbox` bypassed the ACL completely; it is now gated in the parent before the subprocess is spawned.** `_sandbox_runner.py` builds a fresh `Registry` + `Executor` inside the subprocess and calls it directly, and never calls `set_acl`. Attaching an ACL to the CLI's executor gates the calls that go **through that executor** and nothing else — so a rule set denying `system.control.*` was enforced for a plain call and silently ignored for a sandboxed one.
|
|
17
|
+
|
|
18
|
+
The failure inverted the user's intent in the worst available direction: `--sandbox` is a **security** flag, so switching on stronger process isolation switched off access control. Anyone reaching for the safer-looking flag got *less* governance, with no warning, no log line, and a successful exit.
|
|
19
|
+
|
|
20
|
+
Found by the Rust SDK's agent in its own subprocess path and confirmed present in all three SDKs — this was never a Python-only divergence. It shipped with FE-14 as first implemented, because §4.2 said "attach the ACL to the executor" and stopped there; nothing in the feature spec said what to do about execution paths that build their own. §4.10 is new and now states the requirement normatively.
|
|
21
|
+
|
|
22
|
+
The access decision is now reached in the **parent**, which already holds the ACL, and **before** the spawn — a denied call exits `77` and no process is ever created. The child is deliberately *not* trusted to re-load the ACL as the control: the sandbox forwards a narrow environment allowlist by design and runs in a temporary working directory, so a child's view of `acl.root` is neither guaranteed nor trustworthy as a gate. (Forwarding it for defence in depth is permitted; treating it as the control is not, and a relative `acl.root` could not resolve from the child's cwd anyway.)
|
|
23
|
+
|
|
24
|
+
An ACL-sourced `approval: required` now composes with the module's own annotation before the CLI's approval gate runs on this path too — `check_approval` gained an `acl_requires_approval` parameter for the union. Without it the same rule would demand a human on an in-process call and wave through the sandboxed one.
|
|
25
|
+
|
|
26
|
+
**The gate always evaluates against a real `Context`, carrying the call's argument projection.** This is the second, subtler half of the same bypass, found by the TypeScript SDK's agent. 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 a gate passing `None` would leave conditional `deny` rules inert on the delegated path while they fire in-process. `build_context`'s `None`-when-nothing-asserted return is correct for `apcli acl check`, which honestly simulates a context-free call, and wrong for the gate; `delegated_context` is the separate constructor that never returns `None` and always attaches the §6.1.8 governance projection, without which `arguments`-scoped rules go inert the same way.
|
|
27
|
+
|
|
28
|
+
The gate lives in `Sandbox.execute`, ahead of `_sandboxed_execute`, so a downstream embedder calling `Sandbox` directly is covered rather than only the CLI's own dispatch. `_sandbox_runner.py` is deliberately left ungated and carries a comment saying why. Regression tests T-ACL-31/32/34 assert on the **spawn itself** (spying `subprocess.Popen`), not merely on the exit code, and are discriminating in both directions: the same call without `--sandbox` must also exit `77`, and with the rule removed both paths must succeed. Each was confirmed to fail against the pre-fix behaviour.
|
|
29
|
+
|
|
30
|
+
**No action is required of anyone who has not configured an ACL** — with no rule set attached the sandbox behaves exactly as it did before, per the enforcement-only-when-configured rule.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **FE-14: the CLI attaches an ACL to its Executor.** apcore has enforced access control since PROTOCOL_SPEC §6, and apcore-cli has always carried the *downstream* half — exit code `77` for `ACL_DENIED`, an `acl` row in `--dry-run`, `acl_check` in `apcli describe-pipeline`. None of it was reachable, because **no apcore-cli SDK had ever constructed an `ACL`**: all three build an `Executor` directly rather than going through `APCore`, which is the bootstrap that performs `ACL.discover()`. The result was an executor whose `acl_check` step consulted nothing and a `governance_state()` reporting `unprotected_control_surface: true` for every project. New `acl_loader.py` resolves `acl.root` through the FE-07 4-tier chain (`--acl` / `create_cli(acl=…)` → `APCORE_ACL_ROOT` → `acl.root` in `apcore.yaml` → `./acl`) and calls `Executor.set_acl()`.
|
|
35
|
+
|
|
36
|
+
Enforcement is **only-when-configured**. A missing root attaches nothing and changes no behaviour, preserving apcore's `ACL.discover` invariant that a missing path MUST NOT synthesize an empty default-deny ACL — which would deny every call in every project that lacks an `acl/` directory. There is deliberately **no `acl.enabled: false` switch**: a key whose only effect is to silently disable access control is a foot-gun that reads as configuration, and apcore does not offer one either. To disable enforcement, point `acl.root` at a path that does not exist.
|
|
37
|
+
|
|
38
|
+
`acl.root` is an **apcore-owned** config key, so its environment variable follows the apcore convention `APCORE_ACL_ROOT` — not `APCORE_CLI_ACL_ROOT`. This is the second apcore-prefixed variable the CLI resolves, after `APCORE_EXTENSIONS_ROOT`; the existing precedent is followed, not extended.
|
|
39
|
+
|
|
40
|
+
Discovery is **skipped** when the caller supplied its own `executor` and passed no explicit `acl=`. An embedded host that constructed its own Executor owns its own governance, and attaching a CLI-discovered ACL over it would change enforcement behind the host's back. An explicit `acl=` is an instruction and is always honoured.
|
|
41
|
+
|
|
42
|
+
- **FE-14: `apcli acl list | check | validate | status`.** A nested group under `apcli`, mirroring the `apcli config get|set` precedent, so a rule set is authorable and inspectable from the terminal rather than only from a host application.
|
|
43
|
+
|
|
44
|
+
`check` calls `ACL.check_access()` and **never** `check()`. The boolean `check()` fails closed on approval — it returns `false` for a call that is allowed but needs a human — so using it would report "denied" for a rule set that in fact permits the call. Authorization and approval are independent axes (§6.1.6) and are reported separately: an allow-with-approval outcome exits `0`, because the call *is* permitted and conflating "needs a human" with "denied" would make the exit code unusable for the scripted policy checks this command exists for. A denial exits `77`.
|
|
45
|
+
|
|
46
|
+
`validate` runs `ACL.validate_rules()` and renders `Sync` and `Async` as **separate** columns, never collapsed into one boolean (§6.1.3 rule 3): a finding with `sync=no, async=yes` is an async-only handler, working under `async_check()` and unevaluable under `check()`. Any finding exits `47` — the strict, CI-friendly default; JSON output carries each finding's `effect` so a caller that wants to gate only on `deny` rules can.
|
|
47
|
+
|
|
48
|
+
`status` renders all nine `Executor.governance_state()` observations. `acl_configured` alone is not the answer: the ACL and approval gates are pipeline *steps*, and the `internal` / `testing` / `minimal` strategies remove them, so an executor can hold an ACL that no step ever consults. `--strict` exits `47` on an unprotected control surface so a deployment can fail its own startup check without parsing output.
|
|
49
|
+
|
|
50
|
+
`list` with no ACL attached prints `No ACL configured.` and exits `0` — listing nothing is not an error. `check` and `validate` with no ACL exit `47`.
|
|
51
|
+
|
|
52
|
+
- **FE-14: `--identity-id`, `--identity-type` and `--role` global flags.** apcore deliberately makes `Context.caller_id` unsettable by callers — it is managed exclusively by `Context.child()`, so a top-level CLI invocation is always the effective caller `@external`. **The CLI does not fabricate a `caller_id`**, and exposes no flag that could; doing so would let any user assume any module's identity by passing a flag. What *is* settable is the identity, via `Context.create(identity=…)`, and that is what the `roles` and `identity_types` conditions read. The CLI now builds a `Context` for `apcli exec`, `apcli validate` and business-module dispatch, so conditional rules are finally evaluable from a terminal.
|
|
53
|
+
|
|
54
|
+
These flags are **unauthenticated caller assertions**, exactly like `--caller` on `apcli acl check`. A rule set that grants on `roles: [admin]` is trivially satisfiable by anyone who can run the binary. That is useful for evaluating a rule set locally and unacceptable as a deployment's only control; each flag's `--help` says so. When none of the three is given, no `Identity` is constructed and `Context.create()` is called exactly as before.
|
|
55
|
+
|
|
56
|
+
- **FE-15a: `apcli openapi scan | generate`.** `scan` reads an OpenAPI 3.0/3.1 document and renders the modules it would produce, in every FE-08 format; `generate` materializes them as `<id>.binding.yaml`. Neither registers a module, builds an executor, or issues a request to the described API — `scan` of a local file performs no network I/O at all, and `scan` of an `http(s)://` source fetches exactly one document, the one named on the command line.
|
|
57
|
+
|
|
58
|
+
The CLI is an adapter, not a second implementation. `derive_module_id` is the primary subject of apcore-toolkit's cross-SDK conformance corpus and MUST match byte-for-byte in all three languages, so the CLI calls it and **never** re-derives, normalizes, kebab-cases or otherwise post-processes the IDs it returns. Scan options map one-to-one onto `OpenAPIScanner.scan` keyword arguments and are forwarded verbatim; the scanner's hooks (`transform_operation`, `derive_module_id`, `transform_module`) are deliberately **not** exposed, because overriding derivation hands back the cross-SDK naming guarantee and that is not something a command-line flag should be able to do silently.
|
|
59
|
+
|
|
60
|
+
Scanner warnings (unresolvable `$ref`, external `$ref`, no 2xx response) are rendered, not dropped: the scanner is a degrade-with-warning design, and the warning is the only signal that a module's resulting flags are incomplete. A partially-understood document is still a successful scan and exits `0`.
|
|
61
|
+
|
|
62
|
+
**`generate` does not make an API callable.** Passing generated artifacts to `--binding` does not yet produce working commands, because `OpenAPIScanner` sets `target` to a route descriptor (`"GET /pets"`) and `RegistryWriter._to_function_module` resolves `target` as a dotted import path. That is FE-15b. The commands' `--help` states this plainly.
|
|
63
|
+
|
|
64
|
+
- **FE-15a: proxy-hazard detection.** apcore-toolkit's proxy writer decides body-versus-query by HTTP method alone, so a query parameter declared on a `POST` / `PUT` / `PATCH` operation **would be sent in the request body** — silently, with the server ignoring the value or rejecting the request and nothing reporting a fault. FE-15a cannot fix that (the information is not carried by `ScannedModule`; the fix is an apcore-toolkit 0.12.0 change) but it can make it visible. `detect_proxy_hazards` reads `parameters[].in` from the *raw* document — a diagnostic, not a routing decision, so no toolkit logic is duplicated — and both commands report the affected operations. Hazards are counted separately from scanner warnings, appear under a top-level `hazards` key in machine formats rather than inside a module's `warnings` array (they are a statement about a *future* execution path, not about the scan that just ran), and never change the exit code.
|
|
65
|
+
|
|
66
|
+
- **FE-14 §4.8: ACL decisions are written to the FE-05 audit log.** apcore emits exactly one `AuditEntry` per `check_access()`, and only through an `audit_logger` callback — nothing in apcore wires the `acl.audit.*` config keys to one. The CLI now does: with `acl.audit.enabled` true (the default whenever an ACL is attached), ACL decisions land in `~/.apcore-cli/audit.jsonl` beside execution records. All 13 fields are written **verbatim** in their `snake_case` wire form — `handler_error` and `approval_required` included, which are the two most droppable-looking and the two carrying the §6.3.1 distinctions ("no answer was obtainable" vs "the handler said no", authorization vs approval) that nothing else records.
|
|
67
|
+
|
|
68
|
+
**Key order is normative** (§4.8, tightened after TypeScript shipped first and found the gap): the log is JSONL, so leaving the order unspecified would make the same decision serialize to different bytes in each SDK — the divergence class this feature already hit on flag help text and the identity sentinel. Records are emitted in apcore's own `AuditEntry` declaration order (`timestamp, caller_id, target_id, decision, reason, matched_rule, matched_rule_index, identity_type, roles, call_depth, trace_id, handler_error, approval_required`), which is what TypeScript emits after its camelCase-to-`snake_case` conversion. T-ACL-26 asserts `list(record.keys()) == [...]` against a literal sequence — an equality check rather than a containment one, so field set, order and casing are pinned in a single assertion, and the sequence is spelled out in the test rather than compared against the source constant so that reordering the constant turns the test red instead of being followed silently. Confirmed discriminating by swapping two fields.
|
|
69
|
+
|
|
70
|
+
**A logging fault never changes an access decision.** With no FE-05 logger installed the callback writes nothing and fails silently, matching `AuditLogger`'s own write-failure posture; if the sink raises, the callback swallows the error and the decision stands. Both are pinned by tests.
|
|
71
|
+
|
|
72
|
+
The callback is a **constructor** argument and `ACL.load` takes none, so an audited ACL is loaded and then reconstructed: `src = ACL.load(path)` then `ACL(src.rules, src.default_effect, audit_logger=…)`. **`default_effect` is carried from `src` and never hardcoded.** A file may legitimately declare `default_effect: allow`, and passing the constructor's own `"deny"` literal would silently invert the governing verdict for every call no rule matched — a rule set that grants by default would start denying, with nothing in any output saying so. T-ACL-27b is the discriminating test and uses an `allow`-defaulted file for exactly that reason: every `deny`-defaulted fixture passes against the defect. It was confirmed to fail against a hardcoded `"deny"` and to be the only test that does.
|
|
73
|
+
|
|
74
|
+
**The rebuilt ACL loses `reload()`**, which needs the `_yaml_path` only `ACL.load` sets. This is accepted, not worked around — no apcore-cli SDK calls `reload()` on any path — and it is pinned by a test rather than left to be discovered. Writing the private attribute to fake the provenance was rejected: it would make `reload()` claim a file the object was not in fact loaded from. With `acl.audit.enabled: false` the `ACL.load` result is attached **directly**, no rebuild and no callback, so the caveat applies only to the auditing path; T-ACL-27a asserts the attached object *is* the load result rather than merely that no entries were written. An ACL supplied by an embedder through `create_cli(acl=…)` is attached unchanged whatever the audit settings say (§4.2), so it keeps both its own audit sink and its `reload()`; T-ACL-27c asserts identity, not equivalence.
|
|
75
|
+
|
|
76
|
+
Failures in the audit sink are swallowed. apcore invokes the callback inline on the decision path with no guard of its own, so an exception raised there would propagate out of `check_access` — turning a failure to *record* a verdict into a failure to *reach* one. A governance decision must not depend on the audit file being writable.
|
|
77
|
+
|
|
78
|
+
- **New config keys `acl.audit.enabled` and `acl.audit.include_denied`** (`APCORE_ACL_AUDIT_ENABLED` / `APCORE_ACL_AUDIT_INCLUDE_DENIED`, both defaulting to `true`), registered in `ConfigResolver.DEFAULTS`. Both are apcore-owned names, so they follow the `APCORE_*` convention rather than `APCORE_CLI_*`, exactly as `acl.root` does. `include_denied` takes its meaning from apcore's own `schemas/acl-config.schema.json` ("Whether to log denied access attempts", default `true`): `false` suppresses **deny** entries only, and allow entries keep being written. The CLI adopts that meaning rather than forking a similarly-named key with inverted semantics. The environment tier goes through an explicit spelling table (`true/1/yes/on`, `false/0/no/off`) because `bool("false")` is `True` and `APCORE_ACL_AUDIT_ENABLED=false` would otherwise turn auditing **on**; an unrecognized spelling falls back to the key's default rather than to `false`, so a typo in a governance setting cannot be the thing that silently stops the audit trail.
|
|
79
|
+
|
|
80
|
+
- **`ACL_RULE_ERROR -> 47` in `APCORE_ERROR_CODE_MAP`.** A real apcore `ErrorCode` that no SDK's exit map carried, so a malformed ACL file fell through to the generic `1` — indistinguishable from "the module ran and failed". `47` (`CONFIG_INVALID`) is correct rather than `77`: the ACL could not be *read*, which is a configuration fault, not a denial. `77` stays reserved for an actual access decision, or scripts branching on it would misreport a broken config as a permissions problem. Pinned by an assertion here, as the v0.11.0 audit's `DEPENDENCY_NOT_FOUND` finding requires.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- **`apcore>=0.30.0`, `apcore-toolkit[http-proxy]>=0.11.1`.** apcore 0.29.0 was FE-14's floor for the ACL surface the CLI consumes (`ACL.load`, `check_access`, `validate_rules`, `Executor.set_acl`, `governance_state`); the floors are raised to the current releases so the declared minimum matches what the suite is actually verified against. **Neither bump required a single source change** — apcore 0.30.0 and apcore-toolkit 0.11.1 are confined to layers this CLI does not consume, and the suite was unchanged at 977 passed / 5 xfailed across the bump, before the §4.8 tests below were added. The toolkit's `http-proxy` extra is required rather than optional: `load_spec` imports `httpx` lazily, and `apcli openapi scan https://…` must fail with an actionable message naming the missing extra rather than a bare `ImportError`. Local-file scanning and the YAML writer need none of it.
|
|
85
|
+
|
|
86
|
+
- **`Sandbox.execute` gained an optional fourth parameter, `context`.** A **public-surface change**, called out explicitly: `Sandbox` is exported from the package root, and the signature is now `execute(module_id, input_data, executor, context=None)`. It is additive and backward compatible — every existing three-argument call keeps working — and it carries the FE-14 identity into the ACL's conditional rules. The context is forwarded to `executor.call` **only when non-None**, so the no-identity path makes exactly the two-argument call it always did rather than a three-argument one with a `None`. Sandboxed (subprocess) execution deliberately does not carry it: the subprocess builds its own context, and an unauthenticated identity assertion is not something to serialize across that boundary.
|
|
87
|
+
|
|
88
|
+
- **The strategy-bypass warning now names the *configured* ACL.** `--strategy internal | testing | minimal` removes the `acl_check` step. The historical banner covered `testing` only and said nothing about what was being skipped; it now fires for all three and reads `⚠ Using '{strategy}' strategy — the configured ACL is not enforced.` — but **only when an ACL is actually attached**, because bypassing a real rule set is a materially different event from running with no rules at all.
|
|
89
|
+
|
|
90
|
+
- **`apcli openapi generate` emits binding YAML only; there is no `--writer` flag.** An earlier draft of the spec offered `--writer native`, mapping to the toolkit's `PythonWriter` on the `apcli init` precedent that each SDK scaffolds in its own language. **It cannot work, for the same reason `RegistryWriter` cannot.** Every toolkit source writer resolves `target` as a `module.path:callable` import path and rejects anything else — `PythonWriter._generate_code` raises `ValueError: Invalid target format: 'GET /pets'. Expected 'module.path:callable'.` — while an OpenAPI-derived `target` is always a route descriptor, so the flag could never have succeeded for any input this command can produce. Discovered during implementation and confirmed upstream; the spec was amended and the flag withdrawn in all three SDKs rather than shipped as a permanent failure. Emitting genuine host-language source for an OpenAPI operation means emitting an **HTTP proxy implementation**, which is a different generator from the stub-importing one the toolkit ships, and belongs with FE-15b.
|
|
91
|
+
|
|
92
|
+
### Notes
|
|
93
|
+
|
|
94
|
+
- **The test suite no longer writes to the developer's real `~/.apcore-cli/audit.jsonl`.** `create_cli` constructs an `AuditLogger()` with no path, so every test that builds a CLI has always appended execution records there; §4.8 would have added ACL decision records to the same file. A session-scoped autouse fixture now redirects `AuditLogger.DEFAULT_PATH` into `tmp_path`. Transparent to tests that pass an explicit `path=`, and no production behaviour changes — the log stays where it belongs when the CLI actually runs.
|
|
95
|
+
|
|
96
|
+
- **The earlier "§4.8 is blocked upstream" note was wrong and is retracted.** A previous draft of this entry (and of the feature spec) said FE-14's audit wiring could not ship until Python gained a public `ACL.set_audit_logger`, and deferred the `acl.audit.*` keys and T-ACL-26/27/27a/27b with it. **No upstream change was needed.** All three SDKs already accept the callback as a constructor argument, so §4.8's load-then-construct sequence works today; the only cost is `reload()`, which no apcore-cli SDK calls. The claim reached implementers before it was checked against the SDK sources, and is recorded here rather than quietly deleted because a wrong blocking claim is the kind of thing that gets repeated. The wiring, the two config keys and T-ACL-26/27/27a/27b/27c are in this release (see **Added**).
|
|
97
|
+
|
|
98
|
+
- **All of FE-15b is deliberately not included.** No `--openapi` startup flag, no `--binding` proxy dispatch, no `HTTPProxyRegistryWriter`, no `openapi.*` config keys. Two independent prerequisites, neither about OpenAPI: `--binding` is a real registration path only in Python (TypeScript populates a display-overlay map, Rust constructs a `DisplayResolver` and discards it), and the parameter-location defect above needs an apcore-toolkit 0.12.0 change across three SDKs. Shipping it Python-first would leave the feature's central correctness claim — an end-to-end execution test — verified in one third of the ecosystem.
|
|
99
|
+
|
|
100
|
+
- **Three previously-inert code paths become live with an ACL attached**, none of them needing new code. `apcli validate` / `--dry-run`'s `acl` row begins reporting real verdicts and a denial exits `77` through the existing cascade. The approval gate fires on the union of the module annotation, an ACL rule carrying `approval: required`, and `gate_destructive` — so a module annotated `requires_approval: false` can now legitimately route to `CliApprovalHandler`, a path the existing `acl-argument-scoped-approval` regression test already covers. And an ACL denial suppresses module-level `preflight()` / `preview()` introspection, so `predicted_changes` is empty for a denied caller — correct, and already handled since the CLI reads only `valid` / `checks` / `requires_approval`.
|
|
101
|
+
|
|
102
|
+
- **The `apcli-visibility` conformance goldens are unaffected but now understate the root options.** Those fixtures capture the **root** help, which lists only the `apcli` command and not its subcommands, so `acl` and `openapi` do not appear in them and the behavioural assertions still pass. The root *Options* block did gain four rows — `--identity-id`, `--identity-type`, `--role` in every scenario and `--acl` in the standalone ones — which the shared fixtures will need when they are regenerated centrally. The byte-match test remains `xfail` pending the canonical clap-style help formatter, as it was before this release. (The goldens already omit `--allowed-prefix`.)
|
|
103
|
+
|
|
104
|
+
## [0.11.0] - 2026-09-02
|
|
105
|
+
|
|
106
|
+
Bumps the required `apcore` floor to `0.28.0` and `apcore-toolkit` to `0.10.2`, and fixes **three defects the 0.28.0 upgrade made reachable or visible**. Full suite: 815 passed, 5 xfailed (798 → 815; 17 new regression tests), verified in a throwaway venv built from the declared `dev` extra rather than the working interpreter. Each new test was confirmed to fail against the pre-fix behaviour rather than merely to pass against the new one.
|
|
107
|
+
|
|
108
|
+
**Why a minor rather than a patch.** Every fix below restores behaviour that was already specified, but two of them change what a working consumer observes, and this ecosystem's rule — stated in apcore's own 0.28.0 release note — is that such a change "must ship as a **minor** (or major) version bump, never a patch". A script branching on exit code `1` from an `apcli` system command now sees `45`; a caller doing `handler.request_approval(...)["status"]` now gets a `TypeError`. Neither was correct behaviour, and both were reachable.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **The `apcli health` summary 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. `unknown` means "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:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
probe.echo unknown 0.0% --
|
|
116
|
+
Summary: no data
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Pre-existing, and not introduced by this upgrade** — all three SDKs have emitted `unknown` since the tier set existed. apcore 0.28.0 is what brought it into focus: `sys-health-summary.schema.json` had 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's `unhealthy` count field into `error` and `unknown`. With the canonical shape finally naming four tiers, rendering three is a plain omission. Fixed in all three SDKs together, with the tally now covering `unknown`; a genuinely empty tally still reads "no data".
|
|
120
|
+
|
|
121
|
+
- **The approval gate crashed on every gate-routed approval — `CliApprovalHandler` returned a mapping where the protocol requires an `ApprovalResult`.** apcore's `BuiltinApprovalGate` reads the handler's answer by attribute (`result.status`, `result.approved_by` in `builtin_steps.py`), so the handler's `{"status": "approved", ...}` dict raised `AttributeError` *inside* the gate and reached the caller as `MODULE_EXECUTE_ERROR`. Every one of the eight return paths was affected, so a module annotated `requires_approval: true` could not execute through the wired handler at all — including the `--yes` and `APCORE_CLI_AUTO_APPROVE=1` bypasses, which return early and therefore failed the same way.
|
|
122
|
+
|
|
123
|
+
It survived because nothing exercised it: no test in this SDK called `request_approval`, and the class was only reachable through `executor.set_approval_handler` in `factory.py`. apcore-cli-rust converts through `cli_to_apcore_result` and apcore-cli-typescript's `ApprovalResult` is a structurally-typed interface, so neither had the defect — this was a Python-only divergence from a shape both other SDKs got right.
|
|
124
|
+
|
|
125
|
+
**Pre-existing, but apcore 0.28.0 widens the blast radius.** Before 0.28.0 the gate fired only for a module whose *annotation* said `requires_approval: true`. Since spec v1.28.0 §6.9 the gate fires on the union of three sources, so an ACL rule carrying `approval: required` (§6.1.6) now routes calls to modules annotated `requires_approval: false` through the same broken path — the exact shape argument-scoped approval was added for (`git push --force` needs a human, `git push` does not).
|
|
126
|
+
|
|
127
|
+
`_approval_result()` now constructs `apcore.approval.ApprovalResult`, falling back to the mapping shape when apcore is not importable so the handler stays usable in isolation.
|
|
128
|
+
|
|
129
|
+
- **Every apcore error from an `apcli` system command exited 1 instead of its canonical code.** `exit_code_for_error` matched only on the CLI's own exception classes, none of which an apcore-raised `ModuleError` subclass is an instance of, so the wire code it carries was never read. `system_cmd._exit_on_system_error` documents the canonical taxonomy (44 module-not-found, 45 schema-validation, 46 approval-denied, 47 config-invalid, 77 ACL-denied) and delivered `1` for all of them. TS `exitCodeForError` falls through to a `codeMap` and Rust `map_module_error_to_exit_code` reads `err.code`; Python had the map — in `cli.py` — and never consulted it from this path.
|
|
130
|
+
|
|
131
|
+
**apcore 0.28.0 makes it newly reachable on a routine command.** `system.usage.summary` / `system.usage.module` now declare `"pattern": "^[1-9][0-9]*[hd]$"` on `period`, and 0.28.0 also stops `_DictSchemaAdapter.model_validate` being a pass-through, so dict-declared schemas are finally enforced. `apcore-cli apcli usage --period 0h` passes the flag through verbatim and now raises `SCHEMA_VALIDATION_ERROR` where it previously returned an empty window with exit 0 — and was reported as exit 1 rather than 45.
|
|
132
|
+
|
|
133
|
+
`APCORE_ERROR_CODE_MAP` moves to `exit_codes.py` as the single source of truth (`cli._ERROR_CODE_MAP` is now an alias, so the two copies cannot drift), gains the `DEPENDENCY_NOT_FOUND` / `DEPENDENCY_VERSION_MISMATCH` entries TS already had, and `exit_code_for_error` falls back to it.
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- **`apcore>=0.28.0`, `apcore-toolkit>=0.10.2`.** apcore-toolkit 0.10.2 is a dependency-tracking release with no source change; its stable surface consumed by the CLI (`format_*`, `DisplayResolver`, `BindingLoader`, `RegistryWriter`) is unchanged.
|
|
138
|
+
|
|
139
|
+
- **Exit-code map parity pinned across the three SDKs.** A mechanical three-way diff of the maps found `DEPENDENCY_NOT_FOUND` and `DEPENDENCY_VERSION_MISMATCH` mapped to 44 here and in the other non-Rust SDK, but falling through to 1 in apcore-cli-rust (fixed in its 0.11.0). Both codes now carry an explicit assertion here too, so the three maps cannot drift again without a test going red.
|
|
140
|
+
|
|
141
|
+
- **The new handler tests drive their coroutines with `asyncio.run` instead of `@pytest.mark.asyncio`.** This suite declares no async plugin — `pytest-asyncio` is absent from the `dev` extra and there is not one other async test in it — so a marker-based test is collected happily and then fails at run time with *"async def functions are not natively supported"* on any machine that does not happen to have the plugin installed for other reasons. Caught by CI, not locally, which is the point: the local interpreter had it and the declared dependency set does not.
|
|
142
|
+
|
|
143
|
+
- **The discriminating approval tests no longer depend on stdin not being a terminal.** They originally used `CliApprovalHandler` with 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 test` does 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 on `ApprovalTimeout` instead of `ApprovalDenied`; `pytest -s` and 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 real `CliApprovalHandler` is still exercised, on the auto-approve path, where its answer is deterministic.
|
|
144
|
+
|
|
145
|
+
### Notes
|
|
146
|
+
|
|
147
|
+
- **What the 0.27.0 → 0.28.0 delta does *not* touch.** The CLI never constructs or loads an `ACL`, never calls `check()` / `check_access()` (so §6.8.1's fail-closed legacy boolean does not reach it), never reads an `AuditEntry`, and never builds an `ACLRule` (so §6.1.5's `effect` value closure and the new `approval` field are inert here). `ExecutionPolicy.resolve()`'s new keyword-only call-site parameters are additive and the CLI configures no policy. `p99_latency_ms` changing value is display-only in `_format_usage_summary_tty`.
|
|
148
|
+
- **`Executor.validate()` now reports the governance-effective requirement (§7.9.5), which is an improvement the CLI gets for free.** `apcli validate` and the `--dry-run` path forward `result.requires_approval` verbatim, so a call gated only by an ACL argument-scoped rule is now correctly reported as needing approval. Pinned by `test_preflight_reports_the_acl_sourced_requirement`.
|
|
149
|
+
- **The CLI's own pre-execution `check_approval(module_def, ...)` still reads the static annotation**, which since spec v1.29.0 no longer means "no consent needed". That is correct here rather than a defect: the pre-check is an ergonomic early prompt, and a call it skips still meets the executor's gate, which now composes all three sources and routes to the same `CliApprovalHandler`. Verified end-to-end by `TestApprovalGateEndToEnd`.
|
|
150
|
+
|
|
8
151
|
## [0.10.5] - 2026-08-17
|
|
9
152
|
|
|
10
153
|
Patch release. Bumps the required `apcore` floor to `0.27.0` to track the aligned apcore 0.27.0 release (2026-08-14). **No source changes** — the full test suite passes unchanged (798 passed, 5 xfailed) against apcore 0.27.0.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Terminal adapter for apcore — execute AI-Perceivable modules from the command line
|
|
5
5
|
Project-URL: Homepage, https://aiperceivable.com
|
|
6
6
|
Project-URL: Repository, https://github.com/aiperceivable/apcore-cli-python
|
|
@@ -21,8 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Requires-Python: >=3.11
|
|
24
|
-
Requires-Dist: apcore-toolkit>=0.
|
|
25
|
-
Requires-Dist: apcore>=0.
|
|
24
|
+
Requires-Dist: apcore-toolkit[http-proxy]>=0.11.1
|
|
25
|
+
Requires-Dist: apcore>=0.30.0
|
|
26
26
|
Requires-Dist: click>=8.1
|
|
27
27
|
Requires-Dist: cryptography>=41.0
|
|
28
28
|
Requires-Dist: jsonschema>=4.20
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.12.0"
|
|
8
8
|
description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -26,8 +26,8 @@ classifiers = [
|
|
|
26
26
|
"Environment :: Console",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"apcore>=0.
|
|
30
|
-
"apcore-toolkit>=0.
|
|
29
|
+
"apcore>=0.30.0",
|
|
30
|
+
"apcore-toolkit[http-proxy]>=0.11.1",
|
|
31
31
|
"click>=8.1",
|
|
32
32
|
"jsonschema>=4.20",
|
|
33
33
|
"rich>=13.0",
|
|
@@ -56,6 +56,10 @@ def main(prog_name: str | None = None) -> None:
|
|
|
56
56
|
cmd_dir = _extract_argv_option(None, "--commands-dir")
|
|
57
57
|
bind_path = _extract_argv_option(None, "--binding")
|
|
58
58
|
allowed_prefixes = _extract_argv_option_repeatable(None, "--allowed-prefix") or None
|
|
59
|
+
# FE-14 §4.1 tier 1: the ACL is resolved and attached during create_cli(),
|
|
60
|
+
# before Click parses anything, so --acl has to be scraped from argv the
|
|
61
|
+
# same way --extensions-dir / --binding are.
|
|
62
|
+
acl_path = _extract_argv_option(None, "--acl")
|
|
59
63
|
# Standalone bin entry — SDK version IS the app version here. Passing it
|
|
60
64
|
# explicitly is required because create_cli() (issue #18) intentionally
|
|
61
65
|
# skips --version for embedded callers that do not opt in.
|
|
@@ -65,6 +69,7 @@ def main(prog_name: str | None = None) -> None:
|
|
|
65
69
|
commands_dir=cmd_dir,
|
|
66
70
|
binding_path=bind_path,
|
|
67
71
|
allowed_prefixes=allowed_prefixes,
|
|
72
|
+
acl=acl_path,
|
|
68
73
|
version=_sdk_version,
|
|
69
74
|
description=f"{prog_name or 'apcore-cli'} — execute apcore modules from the command line",
|
|
70
75
|
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Entry point for sandboxed module execution (FE-05).
|
|
2
|
+
|
|
3
|
+
**This runner is deliberately not ACL-gated, and must not become the gate.**
|
|
4
|
+
|
|
5
|
+
It builds a bare ``Registry`` + ``Executor`` with no ACL attached. That is
|
|
6
|
+
safe only because the *parent* reaches the access decision before spawning it
|
|
7
|
+
— see ``Sandbox.execute`` and ``acl_loader.enforce_acl_for_unguarded_path``
|
|
8
|
+
(FE-14 §4.10). Re-loading the ACL here would be defence in depth at best and
|
|
9
|
+
a false control at worst: the sandbox forwards a narrow environment allowlist
|
|
10
|
+
by design and runs in a temporary working directory, so a relative
|
|
11
|
+
``acl.root`` cannot resolve from here and this process's view of the rule set
|
|
12
|
+
is neither guaranteed nor trustworthy. One enforcement point, in the parent,
|
|
13
|
+
where the ACL actually is.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
module_id = sys.argv[1]
|
|
25
|
+
input_data = json.loads(sys.stdin.read())
|
|
26
|
+
extensions_root = os.environ.get("APCORE_EXTENSIONS_ROOT", "./extensions")
|
|
27
|
+
|
|
28
|
+
from apcore import Executor, Registry
|
|
29
|
+
|
|
30
|
+
registry = Registry(extensions_dir=extensions_root)
|
|
31
|
+
registry.discover()
|
|
32
|
+
executor = Executor(registry)
|
|
33
|
+
result = executor.call(module_id, input_data)
|
|
34
|
+
json.dump(result, sys.stdout)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|