apcore-cli 0.7.0 → 0.8.1

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,152 @@ All notable changes to apcore-cli (TypeScript SDK) will be documented in this fi
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.8.1] - 2026-05-09
9
+
10
+ ### Fixed
11
+
12
+ - **Init-time deadlock under Bun (`src/security/sandbox.ts`).** The
13
+ sandbox runner's 5 `await import('node:child_process|os|path|fs')`
14
+ calls were hoisted to static `import` statements at the top of
15
+ `sandbox.ts`. The dynamic-import pattern was a holdover from when
16
+ apcore-cli targeted both Node and browser; the CLI is Node-only by
17
+ nature (`#!/usr/bin/env node`, `process.argv[1]` re-exec, child-
18
+ process spawning), so deferring the imports added no value and
19
+ contributed to the Bun deadlock chain when the CLI was loaded via
20
+ `bun run dist/bin/apcore-cli.js`. Verified end-to-end on Bun 1.3.13:
21
+ `--version` returns in 108 ms (was: indefinite hang on Bun 1.2.x).
22
+ No public API change.
23
+ - **C-SNAKE/1 — schema kwargs forwarded under commander's camelCase keys instead of the schema's snake_case property names** (`src/main.ts:985-998`). Commander stores parsed flag values under camelCased attribute names (`--has-solution` → `options.hasSolution`); the action handler previously passed `Object.entries(options)` straight into `schemaKwargs`, so modules reading `input["has_solution"]` always saw `undefined`. Single-word flags (`--module`, `--page`) coincidentally worked because their camelCase form matches the schema name. Multi-word fields (`has_solution`, `sort_by`, `sort_order`) were silently dropped. The fix iterates `schemaOptions` and writes each value back under its original `propName`, matching Python click's auto-derived parameter name and Rust clap's explicit `Arg::new(prop_name)` semantics. Cross-SDK parity restored.
24
+ - **C-SNAKE/2 — boolean `--flag/--no-flag` pair was registered as a single comma-combined commander option** (`src/main.ts:957-975`). The schema-parser produced `flags: "--<flag>, --no-<flag>"` and the registration loop forwarded that string to `cmd.option(...)`. Commander does not parse the comma form the way Python click's `--flag/--no-flag` does — it routes both forms to the negated attribute and stores `false` for both, so `--has-solution` did not flip the value to `true`. Boolean schema flags now register as two separate `Option`s (`--<flag>` carrying the schema default + help, plus a hidden `--no-<flag>` companion); commander's auto-negation routes both to the same camelCase attribute and applies the correct value.
25
+
26
+ ### Added
27
+
28
+ - **`tests/conformance/snake-case-kwargs.test.ts`** — runs the cross-language Algorithm C-SNAKE fixture (`apcore-cli/conformance/fixtures/snake-case-kwargs/cases.json`) against `buildModuleCommand`. Five cases cover positive flag, negation, default fallback, snake_case string flags, and a multi-flag combination. The same fixture is consumed verbatim by the Python and Rust SDK runners.
29
+
30
+
31
+ ## [0.8.0] - 2026-05-08
32
+
33
+ ### Security
34
+
35
+ - **D11-008 — `AuditLogger.getUser` now includes `LOGNAME` in the env-var fallback chain** (`src/security/audit.ts:138`). The canonical user-resolution chain per spec `security.md` is `getlogin → pwd.getpwuid(getuid).pw_name → USER → LOGNAME → USERNAME → unknown`. The TS port collapsed the first two POSIX steps via `os.userInfo()` (correct) but then jumped from `USER` straight to `USERNAME`, dropping `LOGNAME`. On systems where only `LOGNAME` is set (some CI runners), audit entries fell through to `USERNAME` or `"unknown"` while Python/Rust/Go correctly resolved to `LOGNAME` — a cross-SDK divergence affecting the audit trail's user attribution. Fix inserts `process.env.LOGNAME` between `USER` and `USERNAME`.
36
+
37
+ ### Removed (BREAKING)
38
+
39
+ - **D9-002 — root-level deprecation shims removed (FE-13 §11.3).** Pre-v0.8
40
+ `createCli()` registered 13 hidden root-level commands (`list`, `describe`,
41
+ `exec`, `init`, `validate`, `health`, `usage`, `enable`, `disable`,
42
+ `reload`, `config`, `completion`, `describe-pipeline`) that printed a
43
+ `WARNING: '<name>' as a root-level command is deprecated. ... Will be
44
+ removed in v0.8` line on stderr and forwarded to `apcli <name>`. Per
45
+ PROTOCOL_SPEC FE-13 §11.3 these shims are removed in v0.8 — the `apcli`
46
+ sub-group (or the renamed `builtinGroupName`) is now the only path to
47
+ built-in commands. Internal `_DEPRECATED_ROOT_COMMANDS`,
48
+ `_registerDeprecationShims`, `_collectShimForwardArgs`, and the
49
+ `__isDeprecationShim` collision-detection branch in `createCli`'s
50
+ `extraCommands` handler are deleted.
51
+ - **D9-W1 — raw mutable bindings `verboseHelp` / `docsUrl` no longer exported** from `src/main.ts:50,58`. The setter pair `setVerboseHelp` / `setDocsUrl` is the sanctioned API and remains exported. Mutable `let` exports are brittle as a public surface (live-binding semantics depend on the importer's bundler) and no embedder imports the raw bindings; the public surface now only includes the setters.
52
+ - **D9-W2 — `emitErrorJson` / `emitErrorTty` no longer exported** from `src/main.ts:158,180` and dropped from the `src/index.ts` public re-export list. Both are only consumed inside `main.ts` (`buildModuleCommand` action handler). Now annotated `@internal`.
53
+
54
+ ### Added
55
+
56
+ - **`builtinGroupName?: string` option on `createCli`** — downstream branded CLIs that embed apcore-cli can now expose the built-in commands under a custom namespace (e.g. `mycorp-cli admin health` instead of `mycorp-cli apcli health`). `ApcliGroup` gains a `name` getter and the constructor option is threaded through `fromCliConfig` / `fromYaml` / `tryFromYaml` / `_build`. Default `"apcli"` is unchanged. Validated against `/^[a-z][a-z0-9_-]*$/`; invalid values exit 2. Two new module-level accessors `getReservedGroupNames()` / `setReservedGroupNames()` expose the live reserved-set so `cli.ts`'s `assertNotReserved` and `listCommands` honour the renamed group. Env var `APCORE_CLI_APCLI` and config keys `apcli.*` deliberately do NOT rename — they are apcore-cli-internal toggles, not user-facing. Cross-SDK parity with Python `create_cli(builtin_group_name=...)`. New `DEFAULT_BUILTIN_GROUP_NAME` constant exported from `./builtin-group.js`.
57
+ - **Client-side approval gate for `apcli enable / disable / reload / config set`** — new `requireApprovalForSystemCommand(moduleId, autoApprove)` helper in `src/system-cmd.ts` synthesises a minimal `ModuleDescriptor` with `annotations.requires_approval = true` and invokes `checkApproval(...)` before dispatching the executor call. `ApprovalDeniedError` / `ApprovalTimeoutError` propagate to `emitErrorAndExit` which maps them to exit 46 via `exitCodeForError`. `apcli config set` gains a `-y, --yes` flag for parity with the other three. Mirrors Python `_check_system_approval` and Rust `require_approval_for_system_command`. Audit D11-B-001 (see Fixed).
58
+ - **14 new tests in `tests/builtin-group.test.ts`** — `ApcliGroup builtin-group rename` describe block covers default name, custom name via both factories, validation of 6 invalid + 5 valid name shapes.
59
+ - **5 new tests in `tests/system-cmd.test.ts`** — `client-side approval gate (D11-B-001)` describe block covers `enable / disable / reload / config set` deny path on non-TTY without `--yes` (exit 46 + executor never called) and the `--yes` bypass.
60
+ - **D1-info-1 — `ApcliGroupError` typed exception** (`src/builtin-group.ts:101`, re-exported from `src/index.ts`) for cross-SDK error-class parity with Rust (`apcore_cli::ApcliGroupError`, re-exported at `lib.rs:183`). The previous plain `Error` throw on invalid `builtinGroupName` gave consumers no programmatic way to discriminate apcli config errors from generic ones. Existing `catch (e) { if (e instanceof Error) ... }` blocks continue to work; new code can use `instanceof ApcliGroupError`. The neighbouring throw at `builtin-group.ts:464` (caller-bug guard for `isSubcommandIncluded` invoked under wrong mode) stays as plain `Error` — out of scope for apcli config validation.
61
+ - **D1-004 — `Sandbox` builder methods `withExtensionsRoot` and `withMaxOutputBytes`** (`src/security/sandbox.ts`). Cross-SDK parity with Python `with_extensions_root` / `with_max_output_bytes` and the new Rust builders. Both fields drive runtime behaviour: `extensions_root` takes precedence over inherited `APCORE_EXTENSIONS_ROOT`, `max_output_bytes` replaces the per-stream output cap. The "future builder (Python parity)" comment at `sandbox.ts:77` is removed.
62
+ - **D1-006 — `allowedPrefixes?: string[]` option on `CreateCliOptions`** plumbed through `createCli → applyToolkitIntegration → loadBindingDisplayOverlay` so non-allowlisted `target:` entries are dropped before they pollute the display map. Mirrors Python `factory.py:78 allowed_prefixes` safety knob. New `ApplyToolkitIntegrationOptions` type exported.
63
+ - **D1-007 — `formatModuleList`, `formatModuleDetail`, `resolveFormat` re-exported from `src/index.ts`** alongside `formatExecResult`. The output-formatter feature spec declares contracts for all four; embedders building custom output paths can now consume the canonical formatters from the package root.
64
+ - **D5-003 — dedicated `tests/system-usage.test.ts`** covering the new `src/system-usage.ts` aggregator (period filtering, per-module aggregates, missing-audit-log fallback) — previously coverage for the aggregator was only incidental via the discovery-layer integration tests.
65
+
66
+ ### Fixed
67
+
68
+ - **D11-B-001 — `system-cmd.ts` skipped the client-side approval gate entirely**. Each of the four mutating subcommands (`enable`, `disable`, `reload`, `config set`) declared `--yes` but never read it; no `checkApproval(...)` call existed in the file. Operators on Python or Rust SDKs got an interactive 60s confirmation prompt; TS users got nothing — server-side enforcement was the only gate, and the `--yes` flag was completely dead. Fix wires `requireApprovalForSystemCommand(moduleId, opts.yes)` into all four action handlers (see Added). Description text updated from "Signal explicit intent (forwarded to server-side approval gate)" to "Skip approval prompt" — the original copy was misleading because nothing was actually forwarded.
69
+ - **D11-NEW-005 — TS RESERVED_NAMES exit code was 2, not 48**. `schema-parser.ts:101` previously called `process.exit(EXIT_CODES.INVALID_CLI_INPUT)` (=2) when a schema property collided with a reserved CLI option name. Spec mandates exit 48 (cross-SDK parity with Python `sys.exit(48)` and Rust `CliError::SchemaParserFailure → EXIT_SCHEMA_CIRCULAR_REF`). Fix changes to `EXIT_CODES.SCHEMA_CIRCULAR_REF`. The neighbour flag-collision branch already exited 48; both schema-author errors are now consistent. 6 existing tests updated from "exits 2" assertions to "exits 48".
70
+ - **D11-NEW-001 — `resolveRefs` dropped parent `required` when resolving `anyOf` / `oneOf` branches**. A schema like `{required: ["x"], anyOf: [{required: ["a"]}, {required: ["a"]}]}` resolved to `required: ["a"]` in TS — silently losing `"x"`. Per JSON Schema semantics, `parent.required` applies in addition to the branch intersection. Branch handling now preserves the parent node's `required` field (deduplicated, sibling-first ordering). Aligned with Python `ref_resolver.py:100-118`; Rust got the same fix in a sibling commit. 4 new ref-resolver regression tests.
71
+ - **D11-NEW-003 — `max_depth` over-counted plain nested-object recursion**. Previously a schema with >32 levels of nested object `properties` (no `$refs`) was rejected; the depth budget should only count `$ref` hops + composition-branch descents, not pure structural recursion. Aligned with Rust's interpretation of the spec ("Maximum $ref resolution recursion depth").
72
+ - **D11-NEW-004 — `schema_to_cli_options` flag-collision sites exited 2, not 48**. The two flag-collision sites in `src/schema-parser.ts` (regular flag collision, no-flag collision) previously exited 2 (`INVALID_CLI_INPUT`) instead of 48 (`SCHEMA_CIRCULAR_REF`) — observable: a CI / embedder checking `$? == 48` to detect schema-related failures missed the TS path. Fix matches Python `sys.exit(48)`. 2 updated schema-parser collision tests.
73
+ - **D11-W4 — `schema_to_cli_options` did not warn on `required` entries missing from `properties`** (`src/schema-parser.ts:81`). Python (`schema_parser.py:93-98`) and Rust (`schema_parser.rs:220-228`) iterate the schema `required` list and warn for entries not present in `properties`. The TS SDK skipped that loop, so schemas like `{required: ["foo"]}` with no matching property parsed silently — schema authors lost the cross-SDK guard rail. Now emits the same warning text as Python: `Required property '%s' not found in properties, skipping.` 3 new regression tests cover missing-only, all-present, and multiple-missing cases.
74
+ - **D10-info-1 — `APCORE_CLI_APCLI` env-var parser did not trim** (`src/builtin-group.ts:488`). Per spec invariant 2 in `features/builtin-group.md`, the parser is case-insensitive AND trim-on-read. The TS port lowercased but skipped the trim, so values like `" show "` or `"\thide\n"` silently fell through to the unknown-value warning branch — diverging from Python and Rust. Fix adds `.trim()` before `.toLowerCase()` and short-circuits to `null` when the trimmed value is empty (all-whitespace env is now treated as unset without emitting a warning, matching empty-string handling).
75
+
76
+ ### Changed
77
+
78
+ - **`vitest.config.ts` `coverage.thresholds`** added — `lines / functions / statements: 85`, `branches: 75`. Cross-SDK CI parity with Python `pyproject.toml [tool.coverage.report] fail_under = 85` and Rust `make coverage --fail-under-lines 85`. Audit D5-004.
79
+
80
+ - **D8-W2 — `validateModuleId` extracted to dedicated `src/validate.ts`** for parallel-layout parity with Python (`apcore_cli/validate.py`) and Rust (`src/validate.rs`). Embedders reading multiple SDKs side-by-side previously had to grep for the helper inside `main.ts`. The existing import path is preserved via re-export from `main.ts`, so `discovery.ts` and tests need no changes. Adds an exported `MAX_MODULE_ID_LENGTH` constant for spec parity.
81
+
82
+ - **D1-W4 — `formatPreflightResult` / `firstFailedExitCode` annotated `@internal`** (`src/output.ts:406`). Both stay `export`ed because `main.ts` and `discovery.ts` import them across module boundaries, but they are not re-exported from `index.ts` and have no documented embedder use case. The `@internal` JSDoc tag now matches the import-graph reality so doc generators and human readers see they are not part of the package's stable API.
83
+
84
+ - **D6-TS-info — `@sinclair/typebox` ^0.32 → ^0.34** (`package.json:41`, resolves to 0.34.48). Cross-SDK dependency hygiene: Rust and Python pulled their matching schema-validation deps to current minors months ago; TS was the laggard. The only usage is the stable `Type.*` builder facade in `src/init-cmd.ts` (`Type.Object` / `Type.Array` / `Type.Optional` / `Type.String` / `Type.Number` / `Type.Boolean`), all of which are identical between 0.32 and 0.34 — no source changes required.
85
+
86
+ - **D6-001 — `apcore-toolkit` devDependency switched from local `link:` path to registry version `^0.6.0`**. The previous entry pointed at a developer's absolute local clone, which broke `pnpm install` for any other contributor. Clean clones now resolve `apcore-toolkit` identically to the peer-dependency declaration (`>=0.6.0`).
87
+
88
+ - **`apcli list` and `apcli describe` `--format` choices** are now validated
89
+ via Commander's `Option.choices(...)` against the canonical set
90
+ `[table, json, csv, yaml, jsonl, markdown, skill]`. Unknown values exit
91
+ with code 2 instead of silently no-op'ing. Issue
92
+ [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20).
93
+ - **Dependency bump**: peer-dep `apcore-js >= 0.21.0` (was `>= 0.19.0`) and the
94
+ optional `apcore-toolkit >= 0.6.0` (was `>= 0.5.0`). Aligns with upstream
95
+ `apcore 0.21.0` (Module.preview / PreflightResult.predicted_changes) and
96
+ `apcore-toolkit 0.6.0` (surface-aware formatters).
97
+ - **Issue #19 — drop "apcore" branding from embedded-mode `--help`**: top-level
98
+ CLI description now resolves from a new `description?: string` field on
99
+ `CreateCliOptions` (defaults to `${progName} CLI`); the `apcli` subgroup
100
+ description is now `Built-in commands` instead of `apcore-cli built-in
101
+ commands`; `--verbose` option text and the help footer drop the trailing
102
+ `apcore` from `(including built-in apcore options)`. Standalone bin entry
103
+ (`bin/apcore-cli.ts → main()`) passes `description="<prog> — execute apcore
104
+ modules from the command line"` explicitly so the standalone surface is
105
+ unchanged.
106
+ - **Conformance fixtures (`aiperceivable/apcore-cli/conformance/fixtures/apcli-visibility/`)**
107
+ refreshed to match the new debranded help output and to forward `version` /
108
+ `description` from the fixture inputs through `captureHelp()`.
109
+
110
+ ### Added
111
+
112
+ - **`--format markdown` and `--format skill`** for `apcli list` and `apcli describe`
113
+ (issue [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20)).
114
+ Both delegate to `apcore-toolkit` (`formatModule` / `formatModules`, peer dep
115
+ ≥0.6) so the output is byte-identical to the same toolkit call in the Python
116
+ and Rust SDKs. `--format skill` emits vendor-neutral SKILL.md content
117
+ directly loadable by Claude Code (`.claude/skills/<id>/SKILL.md`) and
118
+ Gemini CLI (`.gemini/skills/<id>/SKILL.md`):
119
+
120
+ ```bash
121
+ apcore-cli apcli describe users.create --format skill > .claude/skills/users.create/SKILL.md
122
+ ```
123
+
124
+ A new internal adapter `descriptorToScanned()` maps `ModuleDescriptor`
125
+ to the toolkit's `ScannedModule`. The `formatModuleList` and
126
+ `formatModuleDetail` functions are now `async` to support the dynamic
127
+ toolkit import (the existing five-format paths remain effectively
128
+ synchronous and complete before the returned promise resolves).
129
+ - **Issue #18 — host-app `--version` opt-in**: new `version?: string` field on
130
+ `CreateCliOptions`. When supplied, registers `-V/--version` with the host's
131
+ version string. **When omitted, the `--version` flag is no longer registered**
132
+ — embedded CLIs that do not opt in stop leaking the SDK's own version. The
133
+ standalone bin entry passes `version: VERSION` (the SDK package version)
134
+ explicitly so the `apcore-cli` binary's behaviour is preserved. The
135
+ `configureManHelp(...)` man-page generator falls back to the SDK version
136
+ when the host does not supply one, so manpages always carry a version stamp.
137
+ - **Issue #19 — `description?: string`** on `CreateCliOptions`.
138
+ - **Issue #17 — `system.usage` aggregator + `list --sort calls|errors|latency`**:
139
+ new module `src/system-usage.ts` reads `~/.apcore-cli/audit.jsonl`, filters
140
+ by period (default 24h), and returns per-module aggregates (`calls`,
141
+ `errors`, `avg latency_ms`). `list --sort {calls,errors,latency}` now
142
+ consults the aggregator instead of falling back to id-sort with a buried
143
+ `process.stderr.write("Warning: ...")`. When the audit log has no entries
144
+ in the period window the discovery layer prints a user-visible note to
145
+ stderr (`note: no usage data available for --sort <field>; sorted by id.
146
+ ...`) and falls back to id-sort. Module-protocol registration of
147
+ `system.usage.summary` / `system.usage.module` as registry-callable
148
+ built-ins is tracked as a follow-up — today the readers are invoked
149
+ directly by the discovery layer.
150
+ - New file: `src/system-usage.ts`.
151
+
152
+ ---
153
+
8
154
  ## [0.7.0] - 2026-04-25
9
155
 
10
156
  ### Added
package/README.md CHANGED
@@ -49,9 +49,9 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
49
49
  pnpm add apcore-cli apcore-js
50
50
  ```
51
51
 
52
- Requires Node.js 18+ and `apcore-js >= 0.19.0`.
52
+ Requires Node.js 18+ and `apcore-js >= 0.21.0`.
53
53
 
54
- **Optional:** install `apcore-toolkit` (>=0.5.0) to enable display overlay and registry writer integration via `applyToolkitIntegration`, `DisplayResolver`, and `RegistryWriter`.
54
+ **Optional:** install `apcore-toolkit` (>=0.6.0) to enable display overlay and registry writer integration via `applyToolkitIntegration`, `DisplayResolver`, and `RegistryWriter`.
55
55
 
56
56
  ```bash
57
57
  pnpm add apcore-cli apcore-js
@@ -118,6 +118,8 @@ async function main() {
118
118
  executor,
119
119
  progName: "myapp",
120
120
  // expose: { mode: "include", include: ["admin.*"] },
121
+ // apcli: { mode: "exclude", exclude: ["init"] }, // hide init from the apcli group
122
+ // apcli: false, // or hide the entire apcli group
121
123
  // extraCommands: [customCmd1, customCmd2],
122
124
  });
123
125
  cli.parse(process.argv);
@@ -198,10 +200,10 @@ apcore-cli apcli describe math.add
198
200
  apcore-cli apcli exec math.add --a 5 --b 10
199
201
  ```
200
202
 
201
- > **Migration note (v0.7):** Root-level invocations (`apcore-cli list`, `apcore-cli describe`, …) still work in **standalone mode** but emit a deprecation
202
- > `WARNING` and are removed in v0.8. Embedded integrations (host CLIs that
203
- > inject a `registry`) never had root-level built-ins in the first place and
204
- > see no warnings. See the full migration timeline in the spec repo:
203
+ > **Migration note (v0.8):** Root-level built-ins (`apcore-cli list`, `apcore-cli describe`, …) were removed in v0.8 use `<cli> apcli <subcommand>`.
204
+ > Only the `apcli` group is supported; legacy root-level shims have been retired.
205
+ > Embedded integrations (host CLIs that inject a `registry`) never had root-level
206
+ > built-ins in the first place. See the full migration timeline in the spec repo:
205
207
  > [`docs/features/builtin-group.md §11 Migration`](../apcore-cli/docs/features/builtin-group.md#11-migration).
206
208
 
207
209
  The canonical 13 `apcli` subcommands:
@@ -230,7 +232,7 @@ The canonical 13 `apcli` subcommands:
230
232
 
231
233
  | Command | Description |
232
234
  |---------|-------------|
233
- | `apcli init` | Scaffold a starter `apcore.yaml` / extensions layout (see `registerInitCommand` in `src/init-cmd.ts`) |
235
+ | `apcli init module <id>` | Scaffold a new module (TS/JS/YAML binding) into the extensions or commands directory (see `registerInitCommand` in `src/init-cmd.ts`) |
234
236
  | `apcli validate` | Validate modules and configuration against JSON Schema (see `registerValidateCommand` in `src/discovery.ts`) |
235
237
 
236
238
  **Shell integration**
@@ -244,8 +246,8 @@ The canonical 13 `apcli` subcommands:
244
246
 
245
247
  | Mode | Invocation | Notes |
246
248
  |------|------------|-------|
247
- | **Standalone** (`apcore-cli`) | `apcore-cli apcli <subcommand>` | Discovery flags (`--extensions-dir`, `--commands-dir`, `--binding`) are registered. Legacy root-level built-ins are kept as deprecation shims. |
248
- | **Embedded** (`createCli({ registry, … })`) | `<host-cli> apcli <subcommand>` | Discovery flags are gated off (injected registry already supplies modules). No legacy shims embedded hosts are new territory. |
249
+ | **Standalone** (`apcore-cli`) | `apcore-cli apcli <subcommand>` | Discovery flags (`--extensions-dir`, `--commands-dir`, `--binding`) are registered. Only the `apcli` group is supported (legacy root-level shims were removed in v0.8). |
250
+ | **Embedded** (`createCli({ registry, … })`) | `<host-cli> apcli <subcommand>` | Discovery flags are gated off (injected registry already supplies modules). Only the `apcli` group is supported. |
249
251
 
250
252
  ### Module Execution Options
251
253
 
@@ -257,7 +259,7 @@ When executing a module (e.g. `apcore-cli math.add`), these built-in options are
257
259
  | `--yes` / `-y` | Bypass approval prompts |
258
260
  | `--large-input` | Allow STDIN input larger than 10MB |
259
261
  | `--format <fmt>` | Output format: `json`, `table`, `csv`, `yaml`, or `jsonl` |
260
- | `--sandbox` | Run module in subprocess sandbox (not yet implemented always hidden) |
262
+ | `--sandbox` | Run module in a subprocess sandbox (re-exec with stripped env; 64MiB stdout/stderr cap; 300s default timeout). Hidden by default — set `APCORE_CLI_SANDBOX=1` to enable globally. |
261
263
  | `--dry-run` | Run preflight checks (schema, ACL, approval) without executing (FE-11) |
262
264
  | `--trace` | Emit execution pipeline trace (strategy, hooks, middleware timings) |
263
265
  | `--stream` | Stream results line-by-line for stream-capable modules |
@@ -284,18 +286,20 @@ The `list` command supports enhanced filtering and inspection flags:
284
286
 
285
287
  ### Exit Codes
286
288
 
287
- | Code | Meaning |
288
- |------|---------|
289
- | `0` | Success |
290
- | `1` | Module execution error |
291
- | `2` | Invalid CLI input |
292
- | `44` | Module not found / disabled / load error |
293
- | `45` | Schema validation error |
294
- | `46` | Approval denied or timed out |
295
- | `47` | Configuration error |
296
- | `48` | Schema circular reference |
297
- | `77` | ACL denied |
298
- | `130` | Execution cancelled (Ctrl+C) |
289
+ The exit codes below are also exported as the `EXIT_CODES` const-object from `apcore-cli` (canonical source: `src/errors.ts`) and are mapped from thrown errors via `exitCodeForError()`.
290
+
291
+ | Code | `EXIT_CODES` key | Meaning |
292
+ |------|------------------|---------|
293
+ | `0` | `SUCCESS` | Success |
294
+ | `1` | `MODULE_EXECUTE_ERROR` / `MODULE_TIMEOUT` | Module execution error |
295
+ | `2` | `INVALID_CLI_INPUT` | Invalid CLI input |
296
+ | `44` | `MODULE_NOT_FOUND` / `MODULE_LOAD_ERROR` / `MODULE_DISABLED` / `DEPENDENCY_NOT_FOUND` / `DEPENDENCY_VERSION_MISMATCH` | Module not found / disabled / load error |
297
+ | `45` | `SCHEMA_VALIDATION_ERROR` | Schema validation error |
298
+ | `46` | `APPROVAL_DENIED` / `APPROVAL_TIMEOUT` | Approval denied or timed out |
299
+ | `47` | `CONFIG_NOT_FOUND` / `CONFIG_INVALID` | Configuration error |
300
+ | `48` | `SCHEMA_CIRCULAR_REF` | Schema circular reference |
301
+ | `77` | `ACL_DENIED` | ACL denied |
302
+ | `130` | *(no key — set by signal handler)* | Execution cancelled (Ctrl+C) |
299
303
 
300
304
  ## Configuration
301
305
 
@@ -320,6 +324,7 @@ apcore-cli uses a 4-tier configuration precedence:
320
324
  | `APCORE_CLI_APPROVAL_TIMEOUT` | Default approval prompt timeout in seconds | `60` |
321
325
  | `APCORE_CLI_STRATEGY` | Default execution strategy (`standard`, `internal`, `testing`, `performance`, `minimal`) | `standard` |
322
326
  | `APCORE_CLI_GROUP_DEPTH` | Maximum nesting depth when rendering grouped module command trees | `2` |
327
+ | `APCORE_CLI_APCLI` | Override apcli group visibility (Tier 2). Accepts `show`/`1`/`true` → all, `hide`/`0`/`false` → none. Sealed by `apcli.disable_env: true`. | *(unset)* |
323
328
 
324
329
  ### Config File (`apcore.yaml`)
325
330
 
@@ -335,6 +340,11 @@ cli:
335
340
  approval_timeout: 60 # seconds
336
341
  strategy: standard # standard | internal | testing | performance | minimal
337
342
  group_depth: 2 # grouped-module command-tree nesting depth
343
+ apcli: # built-in command group visibility (FE-13)
344
+ mode: all # all | none | include | exclude
345
+ include: [] # subcommand allowlist when mode=include
346
+ exclude: [] # subcommand denylist when mode=exclude
347
+ disable_env: false # set true to seal Tier 2 (APCORE_CLI_APCLI env var)
338
348
  ```
339
349
 
340
350
  ## Features
@@ -347,7 +357,7 @@ cli:
347
357
  - **TTY-adaptive output** -- rich tables for terminals, JSON for pipes (configurable via `--format`)
348
358
  - **Approval gate** -- TTY-aware HITL prompts for modules with `requires_approval: true`, with `--yes` bypass and 60s timeout
349
359
  - **Schema validation** -- inputs validated against JSON Schema before execution, with `$ref`/`allOf`/`anyOf`/`oneOf` resolution
350
- - **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing (stub not yet runnable)
360
+ - **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing (re-exec model, env stripping, 64MiB output cap)
351
361
  - **Shell completions** -- `apcore-cli completion bash|zsh|fish` generates completion scripts with dynamic module ID completion
352
362
  - **Man pages** -- `apcore-cli man <command>` for single commands, or `--help --man` for a complete program man page. `configureManHelp()` provides one-line integration for downstream projects
353
363
  - **Documentation URL** -- `setDocsUrl()` adds doc links to help footers and man pages
@@ -380,7 +390,7 @@ apcore-cli (the adapter)
380
390
  +-- approval TTY-aware HITL approval
381
391
  +-- output TTY-adaptive JSON/table output
382
392
  +-- AuditLogger JSON Lines execution logging
383
- +-- Sandbox Subprocess isolation (stub not yet runnable)
393
+ +-- Sandbox Subprocess isolation (re-exec, env stripped, output capped)
384
394
  |
385
395
  v
386
396
  apcore Registry + Executor (your modules, unchanged)
@@ -388,11 +398,11 @@ apcore Registry + Executor (your modules, unchanged)
388
398
 
389
399
  ## API Overview
390
400
 
391
- **Classes:** `LazyModuleGroup`, `ConfigResolver`, `AuthProvider`, `ConfigEncryptor`, `AuditLogger`, `Sandbox`
401
+ **Classes:** `LazyModuleGroup`, `GroupedModuleGroup`, `ApcliGroup`, `ExposureFilter`, `CliApprovalHandler`, `ConfigResolver`, `AuthProvider`, `ConfigEncryptor`, `AuditLogger`, `Sandbox`
392
402
 
393
- **Interfaces:** `CreateCliOptions`, `Registry`, `Executor`, `ModuleDescriptor`
403
+ **Interfaces:** `CreateCliOptions`, `Registry`, `Executor`, `ModuleDescriptor`, `APCore`, `ApcliConfig`, `ApcliMode`, `StrategyInfo`, `StrategyStep`
394
404
 
395
- **Functions:** `createCli`, `main`, `buildModuleCommand`, `validateModuleId`, `collectInput`, `schemaToCliOptions`, `reconvertEnumValues`, `resolveRefs`, `checkApproval`, `resolveFormat`, `formatModuleList`, `formatModuleDetail`, `formatExecResult`, `registerDiscoveryCommands`, `registerShellCommands`, `setAuditLogger`, `getAuditLogger`, `setVerboseHelp`, `setDocsUrl`, `buildProgramManPage`, `configureManHelp`, `exitCodeForError`, `mapType`, `extractHelp`, `truncate`
405
+ **Functions:** `createCli`, `main`, `buildModuleCommand`, `validateModuleId`, `collectInput`, `schemaToCliOptions`, `reconvertEnumValues`, `resolveRefs`, `checkApproval`, `formatExecResult`, `registerListCommand`, `registerDescribeCommand`, `registerExecCommand`, `registerValidateCommand`, `registerHealthCommand`, `registerUsageCommand`, `registerEnableCommand`, `registerDisableCommand`, `registerReloadCommand`, `registerConfigCommand`, `registerCompletionCommand`, `registerPipelineCommand`, `registerInitCommand`, `setAuditLogger`, `getAuditLogger`, `setVerboseHelp`, `setDocsUrl`, `configureManHelp`, `exitCodeForError`
396
406
 
397
407
  **Errors:** `ApprovalTimeoutError`, `ApprovalDeniedError`, `AuthenticationError`, `ConfigDecryptionError`, `ModuleExecutionError`, `ModuleNotFoundError`, `SchemaValidationError`
398
408