apcore-cli 0.9.0 → 0.10.0

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.
@@ -116,6 +116,7 @@ var init_errors = __esm({
116
116
  }
117
117
  };
118
118
  SchemaValidationError = class extends Error {
119
+ code = "SCHEMA_VALIDATION_ERROR";
119
120
  constructor(message = "Schema validation failed") {
120
121
  super(message);
121
122
  this.name = "SchemaValidationError";
@@ -287,7 +288,7 @@ var init_sandbox = __esm({
287
288
  */
288
289
  async execute(moduleId, inputData, executor) {
289
290
  if (!this.enabled) {
290
- return executor.execute(moduleId, inputData);
291
+ return executor.call(moduleId, inputData);
291
292
  }
292
293
  return this._sandboxedExecute(moduleId, inputData);
293
294
  }
@@ -582,9 +583,17 @@ function mapType(propName, propSchema) {
582
583
  array: "string"
583
584
  };
584
585
  if (!schemaType) {
586
+ warn(`No type specified for property '${propName}', defaulting to string.`);
585
587
  return "string";
586
588
  }
587
- return typeMap[schemaType] ?? "string";
589
+ const mapped = typeMap[schemaType];
590
+ if (mapped === void 0) {
591
+ warn(
592
+ `Unknown schema type '${schemaType}' for property '${propName}', defaulting to string.`
593
+ );
594
+ return "string";
595
+ }
596
+ return mapped;
588
597
  }
589
598
  function extractHelp(propSchema, maxLength = 1e3) {
590
599
  let text = propSchema["x-llm-description"];
@@ -765,7 +774,7 @@ async function checkApproval(moduleDef, autoApprove, timeout) {
765
774
  if (!requiresApproval) {
766
775
  return;
767
776
  }
768
- const moduleId = moduleDef.id;
777
+ const moduleId = moduleDef.moduleId;
769
778
  if (autoApprove) {
770
779
  return;
771
780
  }
@@ -789,7 +798,7 @@ async function checkApproval(moduleDef, autoApprove, timeout) {
789
798
  }
790
799
  async function promptWithTimeout(moduleDef, timeout) {
791
800
  timeout = Math.max(1, Math.min(timeout, 3600));
792
- const moduleId = moduleDef.id;
801
+ const moduleId = moduleDef.moduleId;
793
802
  const annotations = moduleDef.annotations;
794
803
  const message = (annotations ? getAnnotation(annotations, "approval_message") : void 0) ?? `Module '${moduleId}' requires approval to execute.`;
795
804
  process.stderr.write(message + "\n");
@@ -869,7 +878,7 @@ var init_approval = __esm({
869
878
  const message = extra.approval_message ?? `Module '${moduleId}' requires approval to execute.`;
870
879
  process.stderr.write(message + "\n");
871
880
  try {
872
- await promptWithTimeout({ id: moduleId }, this.timeout);
881
+ await promptWithTimeout({ moduleId }, this.timeout);
873
882
  return { status: "approved", approved_by: "tty_user" };
874
883
  } catch {
875
884
  return { status: "rejected", reason: "User rejected or timed out" };
@@ -889,7 +898,7 @@ function descriptorToScanned(m) {
889
898
  const metadata = m.metadata ?? {};
890
899
  const display = metadata["display"] ?? null;
891
900
  return {
892
- moduleId: m.id,
901
+ moduleId: m.moduleId,
893
902
  description: m.description ?? "",
894
903
  inputSchema: m.inputSchema ?? {},
895
904
  outputSchema: m.outputSchema ?? {},
@@ -945,13 +954,13 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
945
954
  if (showDeps) headers.push("Deps");
946
955
  if (exposureFilter) headers.push("Exposure");
947
956
  const rows = modules.map((m) => {
948
- const base = [m.id, truncate(m.description, 80), (m.tags ?? []).join(", ")];
957
+ const base = [m.moduleId, truncate(m.description, 80), (m.tags ?? []).join(", ")];
949
958
  if (showDeps) {
950
959
  const deps = m.dependencies;
951
960
  base.push(String(Array.isArray(deps) ? deps.length : 0));
952
961
  }
953
962
  if (exposureFilter) {
954
- base.push(exposureFilter.isExposed(m.id ?? "") ? "\u2713" : "\u2014");
963
+ base.push(exposureFilter.isExposed(m.moduleId ?? "") ? "\u2713" : "\u2014");
955
964
  }
956
965
  return base;
957
966
  });
@@ -959,7 +968,7 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
959
968
  } else if (format === "json") {
960
969
  const result = modules.map((m) => {
961
970
  const entry = {
962
- id: m.id,
971
+ id: m.moduleId,
963
972
  description: m.description,
964
973
  tags: m.tags ?? []
965
974
  };
@@ -968,7 +977,7 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
968
977
  entry.dependency_count = Array.isArray(deps) ? deps.length : 0;
969
978
  }
970
979
  if (exposureFilter) {
971
- entry.exposed = exposureFilter.isExposed(m.id ?? "");
980
+ entry.exposed = exposureFilter.isExposed(m.moduleId ?? "");
972
981
  }
973
982
  return entry;
974
983
  });
@@ -1000,7 +1009,7 @@ function annotationsToDict(annotations) {
1000
1009
  async function formatModuleDetail(moduleDef, format) {
1001
1010
  if (format === "table") {
1002
1011
  process.stdout.write(`
1003
- Module: ${moduleDef.id}
1012
+ Module: ${moduleDef.moduleId}
1004
1013
  `);
1005
1014
  process.stdout.write(`
1006
1015
  Description:
@@ -1048,7 +1057,7 @@ Tags: ${tags.join(", ")}
1048
1057
  }
1049
1058
  } else if (format === "json") {
1050
1059
  const result = {
1051
- id: moduleDef.id,
1060
+ id: moduleDef.moduleId,
1052
1061
  description: moduleDef.description
1053
1062
  };
1054
1063
  if (moduleDef.inputSchema) result.input_schema = moduleDef.inputSchema;
@@ -1408,7 +1417,7 @@ function getDisplay(descriptor) {
1408
1417
  if (display && typeof display === "object" && !Array.isArray(display)) {
1409
1418
  return display;
1410
1419
  }
1411
- const overlay = lookupBindingDisplay(descriptor.id);
1420
+ const overlay = lookupBindingDisplay(descriptor.moduleId);
1412
1421
  return overlay ?? {};
1413
1422
  }
1414
1423
  var init_display_helpers = __esm({
@@ -1426,9 +1435,9 @@ import yaml2 from "js-yaml";
1426
1435
  function registerConfigNamespace() {
1427
1436
  try {
1428
1437
  const nodeRequire = createRequire(import.meta.url);
1429
- const { Config } = nodeRequire("apcore-js");
1430
- if (typeof Config?.registerNamespace === "function") {
1431
- Config.registerNamespace({
1438
+ const { Config: Config2 } = nodeRequire("apcore-js");
1439
+ if (typeof Config2?.registerNamespace === "function") {
1440
+ Config2.registerNamespace({
1432
1441
  name: "apcore-cli",
1433
1442
  envPrefix: "APCORE_CLI",
1434
1443
  defaults: NAMESPACE_DEFAULTS
@@ -1942,1672 +1951,1694 @@ var init_shell = __esm({
1942
1951
  }
1943
1952
  });
1944
1953
 
1945
- // src/security/audit.ts
1946
- var audit_exports = {};
1947
- __export(audit_exports, {
1948
- AuditLogger: () => AuditLogger,
1949
- canonicalizeForHash: () => canonicalizeForHash,
1950
- getAuditLogger: () => getAuditLogger,
1951
- setAuditLogger: () => setAuditLogger
1952
- });
1953
- import * as crypto from "crypto";
1954
- import * as fs3 from "fs";
1955
- import * as os from "os";
1956
- import * as path3 from "path";
1957
- function setAuditLogger(auditLogger) {
1958
- _auditLogger = auditLogger;
1959
- }
1960
- function getAuditLogger() {
1961
- return _auditLogger;
1954
+ // src/exposure.ts
1955
+ function escapeRegex(str) {
1956
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1962
1957
  }
1963
- function canonicalizeForHash(value) {
1964
- if (value === null || typeof value !== "object") return value;
1965
- if (Array.isArray(value)) return value.map(canonicalizeForHash);
1966
- const src = value;
1967
- const sorted = {};
1968
- for (const key of Object.keys(src).sort()) {
1969
- sorted[key] = canonicalizeForHash(src[key]);
1970
- }
1971
- return sorted;
1958
+ function compilePattern(pattern) {
1959
+ const sentinel = "\0GLOB\0";
1960
+ const escaped = pattern.replaceAll("**", sentinel);
1961
+ const parts = escaped.split("*");
1962
+ const regexParts = parts.map((p) => {
1963
+ const restored = p.replaceAll(sentinel, "**");
1964
+ return escapeRegex(restored);
1965
+ });
1966
+ let regex = regexParts.join("[^.]*");
1967
+ regex = regex.replaceAll("\\*\\*", ".+");
1968
+ return new RegExp(`^${regex}$`);
1972
1969
  }
1973
- var _auditLogger, AuditLogger;
1974
- var init_audit = __esm({
1975
- "src/security/audit.ts"() {
1970
+ var ExposureFilter;
1971
+ var init_exposure = __esm({
1972
+ "src/exposure.ts"() {
1976
1973
  "use strict";
1977
1974
  init_esm_shims();
1978
1975
  init_logger();
1979
- _auditLogger = null;
1980
- AuditLogger = class _AuditLogger {
1981
- static DEFAULT_PATH = path3.join(
1982
- os.homedir(),
1983
- ".apcore-cli",
1984
- "audit.jsonl"
1985
- );
1986
- logPath;
1987
- writeFailureWarned = false;
1988
- constructor(path6) {
1989
- this.logPath = path6 ?? _AuditLogger.DEFAULT_PATH;
1990
- this.ensureDirectory();
1991
- }
1992
- ensureDirectory() {
1993
- const dir = path3.dirname(this.logPath);
1994
- try {
1995
- fs3.mkdirSync(dir, { recursive: true });
1996
- try {
1997
- fs3.chmodSync(dir, 448);
1998
- } catch {
1999
- }
2000
- } catch {
1976
+ ExposureFilter = class _ExposureFilter {
1977
+ static VALID_MODES = ["all", "include", "exclude", "none"];
1978
+ _mode;
1979
+ _compiledInclude;
1980
+ _compiledExclude;
1981
+ constructor(mode = "all", include, exclude) {
1982
+ if (!_ExposureFilter.VALID_MODES.includes(mode)) {
1983
+ process.stderr.write(
1984
+ `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
1985
+ `
1986
+ );
1987
+ mode = "none";
2001
1988
  }
1989
+ this._mode = mode;
1990
+ const dedup = (arr) => [...new Set(arr)];
1991
+ this._compiledInclude = dedup(include ?? []).map(compilePattern);
1992
+ this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
2002
1993
  }
2003
- logExecution(moduleId, inputData, status, exitCode, durationMs) {
2004
- const entry = {
2005
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2006
- user: this.getUser(),
2007
- module_id: moduleId,
2008
- input_hash: this.hashInput(inputData),
2009
- status,
2010
- exit_code: exitCode,
2011
- duration_ms: durationMs
2012
- };
2013
- try {
2014
- fs3.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
2015
- try {
2016
- fs3.chmodSync(this.logPath, 384);
2017
- } catch {
2018
- }
2019
- } catch (err) {
2020
- if (!this.writeFailureWarned) {
2021
- this.writeFailureWarned = true;
2022
- warn(`Could not write audit log: ${err}`);
2023
- }
1994
+ /** Return true if the module should be exposed as a CLI command. */
1995
+ isExposed(moduleId) {
1996
+ if (this._mode === "all") return true;
1997
+ if (this._mode === "include") {
1998
+ return this._compiledInclude.some((rx) => rx.test(moduleId));
1999
+ }
2000
+ if (this._mode === "exclude") {
2001
+ return !this._compiledExclude.some((rx) => rx.test(moduleId));
2024
2002
  }
2003
+ return false;
2025
2004
  }
2026
- hashInput(inputData) {
2027
- const salt = crypto.randomBytes(16);
2028
- const payload = JSON.stringify(canonicalizeForHash(inputData));
2029
- return crypto.createHash("sha256").update(Buffer.concat([salt, Buffer.from(payload, "utf-8")])).digest("hex");
2005
+ /** Partition moduleIds into [exposed, hidden] lists. */
2006
+ filterModules(moduleIds) {
2007
+ const exposed = [];
2008
+ const hidden = [];
2009
+ for (const mid of moduleIds) {
2010
+ (this.isExposed(mid) ? exposed : hidden).push(mid);
2011
+ }
2012
+ return [exposed, hidden];
2030
2013
  }
2031
- getUser() {
2032
- try {
2033
- return os.userInfo().username;
2034
- } catch {
2035
- return process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2014
+ /**
2015
+ * Create an ExposureFilter from a parsed config dict.
2016
+ *
2017
+ * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
2018
+ */
2019
+ static fromConfig(config) {
2020
+ const expose = config.expose ?? {};
2021
+ if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
2022
+ warn("Invalid 'expose' config (expected dict), using mode: all.");
2023
+ return new _ExposureFilter();
2024
+ }
2025
+ const exposeObj = expose;
2026
+ const mode = exposeObj.mode ?? "all";
2027
+ if (!["all", "include", "exclude"].includes(mode)) {
2028
+ throw new Error(
2029
+ `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
2030
+ );
2031
+ }
2032
+ let include = exposeObj.include ?? [];
2033
+ if (!Array.isArray(include)) {
2034
+ warn("Invalid 'expose.include' (expected list), ignoring.");
2035
+ include = [];
2036
+ }
2037
+ let exclude = exposeObj.exclude ?? [];
2038
+ if (!Array.isArray(exclude)) {
2039
+ warn("Invalid 'expose.exclude' (expected list), ignoring.");
2040
+ exclude = [];
2036
2041
  }
2042
+ const filterList = (arr, label) => {
2043
+ const result = [];
2044
+ for (const p of arr) {
2045
+ if (!p) {
2046
+ warn(`Empty pattern in expose.${label}, skipping.`);
2047
+ } else {
2048
+ result.push(String(p));
2049
+ }
2050
+ }
2051
+ return result;
2052
+ };
2053
+ return new _ExposureFilter(
2054
+ mode,
2055
+ filterList(include, "include"),
2056
+ filterList(exclude, "exclude")
2057
+ );
2037
2058
  }
2038
2059
  };
2039
2060
  }
2040
2061
  });
2041
2062
 
2042
- // src/system-usage.ts
2043
- import * as fs4 from "fs";
2044
- import * as os2 from "os";
2045
- import * as path4 from "path";
2046
- function computeSummary(options = {}) {
2047
- const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
2048
- const period = options.period ?? "24h";
2049
- const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - PERIOD_TO_MS[period];
2050
- if (!fs4.existsSync(auditPath)) {
2051
- return /* @__PURE__ */ new Map();
2052
- }
2053
- let raw;
2054
- try {
2055
- raw = fs4.readFileSync(auditPath, "utf-8");
2056
- } catch {
2057
- return /* @__PURE__ */ new Map();
2058
- }
2059
- const counts = /* @__PURE__ */ new Map();
2060
- const errors = /* @__PURE__ */ new Map();
2061
- const latencySum = /* @__PURE__ */ new Map();
2062
- for (const line of raw.split("\n")) {
2063
- const trimmed = line.trim();
2064
- if (!trimmed) continue;
2065
- let entry;
2066
- try {
2067
- entry = JSON.parse(trimmed);
2068
- } catch {
2069
- continue;
2070
- }
2071
- const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
2072
- if (Number.isNaN(ts) || ts < cutoff) continue;
2073
- const moduleId = typeof entry.module_id === "string" ? entry.module_id : null;
2074
- if (!moduleId) continue;
2075
- counts.set(moduleId, (counts.get(moduleId) ?? 0) + 1);
2076
- if (entry.status === "error") {
2077
- errors.set(moduleId, (errors.get(moduleId) ?? 0) + 1);
2078
- }
2079
- const duration = entry.duration_ms;
2080
- if (typeof duration === "number") {
2081
- latencySum.set(moduleId, (latencySum.get(moduleId) ?? 0) + duration);
2082
- }
2083
- }
2084
- const out = /* @__PURE__ */ new Map();
2085
- for (const [id, calls] of counts) {
2086
- out.set(id, {
2087
- module_id: id,
2088
- calls,
2089
- errors: errors.get(id) ?? 0,
2090
- latency_ms: calls > 0 ? (latencySum.get(id) ?? 0) / calls : 0
2091
- });
2092
- }
2093
- return out;
2063
+ // src/builtin-group.ts
2064
+ function setReservedGroupNames(names) {
2065
+ _effectiveReservedNames = names;
2094
2066
  }
2095
- function sortModulesByUsage(modules, field, options = {}) {
2096
- const reverse = options.reverse ?? true;
2097
- const summary = computeSummary({
2098
- auditPath: options.auditPath,
2099
- period: options.period
2100
- });
2101
- if (summary.size === 0) {
2102
- modules.sort((a, b) => (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? ""));
2103
- if (reverse) modules.reverse();
2104
- return { used: false };
2067
+ function _validateBuiltinGroupName(name) {
2068
+ if (!name || !_NAME_REGEX.test(name)) {
2069
+ throw new ApcliGroupError(
2070
+ `builtinGroupName ${JSON.stringify(name)} must match /^[a-z][a-z0-9_-]*$/ (non-empty, lowercase, alphanumeric + '_' / '-', leading letter).`
2071
+ );
2105
2072
  }
2106
- const key = (m) => {
2107
- const id = m.id ?? m.module_id ?? "";
2108
- const s = summary.get(id);
2109
- if (!s) return 0;
2110
- return field === "latency" ? s.latency_ms : field === "calls" ? s.calls : s.errors;
2111
- };
2112
- modules.sort((a, b) => {
2113
- const diff = key(a) - key(b);
2114
- if (diff !== 0) return reverse ? -diff : diff;
2115
- return (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? "");
2116
- });
2117
- return { used: true };
2118
2073
  }
2119
- var PERIOD_TO_MS, DEFAULT_AUDIT_PATH;
2120
- var init_system_usage = __esm({
2121
- "src/system-usage.ts"() {
2122
- "use strict";
2123
- init_esm_shims();
2124
- PERIOD_TO_MS = {
2125
- "1h": 60 * 60 * 1e3,
2126
- "24h": 24 * 60 * 60 * 1e3,
2127
- "7d": 7 * 24 * 60 * 60 * 1e3,
2128
- "30d": 30 * 24 * 60 * 60 * 1e3
2129
- };
2130
- DEFAULT_AUDIT_PATH = path4.join(
2131
- os2.homedir(),
2132
- ".apcore-cli",
2133
- "audit.jsonl"
2134
- );
2135
- }
2136
- });
2137
-
2138
- // src/security/config-encryptor.ts
2139
- import * as crypto2 from "crypto";
2140
- import * as os3 from "os";
2141
- async function getKeytar() {
2142
- if (keytarModule) return keytarModule;
2143
- try {
2144
- keytarModule = await import("keytar");
2145
- return keytarModule;
2146
- } catch {
2147
- return null;
2148
- }
2149
- }
2150
- var PBKDF2_ITERATIONS, V1_STATIC_SALT, keytarModule, ConfigEncryptor;
2151
- var init_config_encryptor = __esm({
2152
- "src/security/config-encryptor.ts"() {
2074
+ var ApcliGroupError, DEFAULT_BUILTIN_GROUP_NAME, RESERVED_GROUP_NAMES, _effectiveReservedNames, _NAME_REGEX, VALID_USER_MODES, APCLI_SUBCOMMAND_NAMES, ApcliGroup;
2075
+ var init_builtin_group = __esm({
2076
+ "src/builtin-group.ts"() {
2153
2077
  "use strict";
2154
2078
  init_esm_shims();
2155
2079
  init_errors();
2156
2080
  init_logger();
2157
- PBKDF2_ITERATIONS = 6e5;
2158
- V1_STATIC_SALT = Buffer.from("apcore-cli-config-v1");
2159
- keytarModule = null;
2160
- ConfigEncryptor = class _ConfigEncryptor {
2161
- static SERVICE_NAME = "apcore-cli";
2162
- // One-shot flag so the "obfuscation only" warning fires exactly once
2163
- // per process instead of once per encrypt/decrypt call.
2164
- static weakFallbackWarned = false;
2081
+ ApcliGroupError = class extends Error {
2082
+ constructor(message) {
2083
+ super(message);
2084
+ this.name = "ApcliGroupError";
2085
+ }
2086
+ };
2087
+ DEFAULT_BUILTIN_GROUP_NAME = "apcli";
2088
+ RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set([DEFAULT_BUILTIN_GROUP_NAME]);
2089
+ _effectiveReservedNames = RESERVED_GROUP_NAMES;
2090
+ _NAME_REGEX = /^[a-z][a-z0-9_-]*$/;
2091
+ VALID_USER_MODES = /* @__PURE__ */ new Set([
2092
+ "all",
2093
+ "none",
2094
+ "include",
2095
+ "exclude"
2096
+ ]);
2097
+ APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
2098
+ "list",
2099
+ "describe",
2100
+ "exec",
2101
+ "validate",
2102
+ "init",
2103
+ "health",
2104
+ "usage",
2105
+ "enable",
2106
+ "disable",
2107
+ "reload",
2108
+ "config",
2109
+ "completion",
2110
+ "describe-pipeline"
2111
+ ]);
2112
+ ApcliGroup = class _ApcliGroup {
2113
+ _mode;
2114
+ _include;
2115
+ _exclude;
2116
+ _disableEnv;
2117
+ _registryInjected;
2118
+ _fromCliConfig;
2119
+ _name;
2120
+ constructor(init) {
2121
+ this._mode = init.mode;
2122
+ this._include = init.include;
2123
+ this._exclude = init.exclude;
2124
+ this._disableEnv = init.disableEnv;
2125
+ this._registryInjected = init.registryInjected;
2126
+ this._fromCliConfig = init.fromCliConfig;
2127
+ this._name = init.name;
2128
+ }
2165
2129
  /**
2166
- * Encrypt and store a configuration value.
2130
+ * Resolved name for the built-in command group (default `"apcli"`).
2131
+ * Overridable via createCli's `builtinGroupName` option for downstream
2132
+ * branded CLIs that want a custom namespace. Cross-SDK parity with
2133
+ * Python `ApcliGroup.name` (2026-05-08).
2134
+ */
2135
+ get name() {
2136
+ return this._name;
2137
+ }
2138
+ /**
2139
+ * Tier 1 constructor — config came from `createCli({ apcli })`.
2167
2140
  *
2168
- * Cross-SDK contract (D10-003, 2026-04-26): when the OS keyring is
2169
- * detected as available but `setPassword` then throws (locked keyring,
2170
- * transient backend failure, permission revoked, etc.), the error is
2171
- * propagated wrapped in a `ConfigDecryptionError`. Previously TS
2172
- * caught the exception and silently fell through to AES file encryption
2173
- * — a quiet downgrade that surprised users who expected a hard failure.
2174
- * Python lets the keyring exception propagate raw; Rust returns
2175
- * `ConfigDecryptionError::KeyringError`. The fall-through to AES is
2176
- * still reached when `getKeytar()` returns `null` (keyring
2177
- * genuinely unavailable on this platform / install).
2141
+ * A non-auto mode from this tier wins over env var and yaml.
2178
2142
  */
2179
- async store(key, value) {
2180
- const keytar = await getKeytar();
2181
- if (keytar) {
2182
- try {
2183
- await keytar.setPassword(_ConfigEncryptor.SERVICE_NAME, key, value);
2184
- return `keyring:${key}`;
2185
- } catch (err) {
2186
- const detail = err instanceof Error ? err.message : String(err);
2187
- throw new ConfigDecryptionError(
2188
- `Failed to store '${key}' in OS keyring: ${detail}. Unset APCORE_CLI_CONFIG_PASSPHRASE-aware backends or unlock the keyring before retrying.`
2189
- );
2190
- }
2191
- }
2192
- warn("OS keyring unavailable. Using file-based encryption.");
2193
- const ciphertext = this.aesEncrypt(value);
2194
- return `enc:v2:${ciphertext.toString("base64")}`;
2143
+ static fromCliConfig(config, opts) {
2144
+ return _ApcliGroup._build(
2145
+ config,
2146
+ opts,
2147
+ /*fromCliConfig*/
2148
+ true
2149
+ );
2195
2150
  }
2196
2151
  /**
2197
- * Retrieve and decrypt a configuration value.
2152
+ * Tier 3 constructor config came from `apcore.yaml`.
2153
+ *
2154
+ * Env var (Tier 2) may override the yaml-supplied mode.
2198
2155
  */
2199
- async retrieve(configValue, key) {
2200
- if (configValue.startsWith("keyring:")) {
2201
- const keytar = await getKeytar();
2202
- if (!keytar) {
2203
- throw new ConfigDecryptionError(
2204
- `Keyring module not available to retrieve '${key}'.`
2205
- );
2206
- }
2207
- try {
2208
- const refKey = configValue.slice("keyring:".length);
2209
- const result = await keytar.getPassword(
2210
- _ConfigEncryptor.SERVICE_NAME,
2211
- refKey
2212
- );
2213
- if (result === null || result === void 0) {
2214
- throw new ConfigDecryptionError(
2215
- `Keyring entry not found for '${refKey}'.`
2216
- );
2217
- }
2218
- return result;
2219
- } catch (err) {
2220
- if (err instanceof ConfigDecryptionError) throw err;
2221
- throw new ConfigDecryptionError(
2222
- `Failed to retrieve from keyring: ${err}`
2223
- );
2224
- }
2156
+ static fromYaml(config, opts) {
2157
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
2158
+ const got = Array.isArray(config) ? "array" : typeof config;
2159
+ warn(
2160
+ `apcore.yaml apcli has unexpected type ${got}; using auto-detect.`
2161
+ );
2162
+ return _ApcliGroup._build(
2163
+ void 0,
2164
+ opts,
2165
+ /*fromCliConfig*/
2166
+ false
2167
+ );
2225
2168
  }
2226
- if (configValue.startsWith("enc:v2:")) {
2227
- const data = Buffer.from(configValue.slice("enc:v2:".length), "base64");
2228
- try {
2229
- return this.aesDecrypt(data);
2230
- } catch {
2231
- throw new ConfigDecryptionError(
2232
- `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2233
- );
2234
- }
2169
+ return _ApcliGroup._build(
2170
+ config,
2171
+ opts,
2172
+ /*fromCliConfig*/
2173
+ false
2174
+ );
2175
+ }
2176
+ /**
2177
+ * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
2178
+ * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
2179
+ * Use this in programmatic contexts where throwing/exiting is unwanted.
2180
+ */
2181
+ static tryFromYaml(config, opts) {
2182
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
2183
+ const got = Array.isArray(config) ? "array" : typeof config;
2184
+ return [
2185
+ null,
2186
+ `apcore.yaml 'apcli:' must be a bool, object, or null; got ${got}`
2187
+ ];
2235
2188
  }
2236
- if (configValue.startsWith("enc:")) {
2237
- const data = Buffer.from(configValue.slice("enc:".length), "base64");
2238
- try {
2239
- return this.aesDecryptV1(data);
2240
- } catch {
2241
- throw new ConfigDecryptionError(
2242
- `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2243
- );
2189
+ if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
2190
+ const mode = config["mode"];
2191
+ if (mode !== void 0 && mode !== null) {
2192
+ const validModes = ["all", "none", "include", "exclude"];
2193
+ if (typeof mode !== "string" || !validModes.includes(mode)) {
2194
+ return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
2195
+ }
2244
2196
  }
2245
2197
  }
2246
- return configValue;
2198
+ return [_ApcliGroup.fromYaml(config, opts), null];
2247
2199
  }
2248
- // Derive an AES-256 key with a provided salt (v2 format).
2249
- //
2250
- // Order of preference:
2251
- // 1. APCORE_CLI_CONFIG_PASSPHRASE env var — a real secret supplied by
2252
- // the user; produces a key an attacker with filesystem read cannot
2253
- // reconstruct without also knowing the passphrase.
2254
- // 2. hostname + username — obfuscation-only derivation for backward
2255
- // compatibility. Emits a loud stderr warning on first use so
2256
- // operators know the stored value is NOT protected against a
2257
- // filesystem-read attacker.
2258
- deriveKey(salt) {
2259
- const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2260
- if (passphrase && passphrase.length > 0) {
2261
- return crypto2.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256");
2200
+ // -------------------------------------------------------------------------
2201
+ // Internal builder — shared by both factories
2202
+ // -------------------------------------------------------------------------
2203
+ static _build(config, opts, fromCliConfig) {
2204
+ const name = opts.name ?? DEFAULT_BUILTIN_GROUP_NAME;
2205
+ _validateBuiltinGroupName(name);
2206
+ if (config === true) {
2207
+ return new _ApcliGroup({
2208
+ mode: "all",
2209
+ include: [],
2210
+ exclude: [],
2211
+ disableEnv: false,
2212
+ registryInjected: opts.registryInjected,
2213
+ fromCliConfig,
2214
+ name
2215
+ });
2262
2216
  }
2263
- if (!_ConfigEncryptor.weakFallbackWarned) {
2264
- warn(
2265
- "APCORE_CLI_CONFIG_PASSPHRASE is not set. The `enc:v2:` fallback uses a key derived from hostname+username (non-secret inputs) and is OBFUSCATION ONLY \u2014 an attacker with filesystem read access can reconstruct the key. Set APCORE_CLI_CONFIG_PASSPHRASE or ensure the OS keyring is available for real encryption."
2217
+ if (config === false) {
2218
+ return new _ApcliGroup({
2219
+ mode: "none",
2220
+ include: [],
2221
+ exclude: [],
2222
+ disableEnv: false,
2223
+ registryInjected: opts.registryInjected,
2224
+ fromCliConfig,
2225
+ name
2226
+ });
2227
+ }
2228
+ if (config === void 0 || config === null) {
2229
+ return new _ApcliGroup({
2230
+ mode: "auto",
2231
+ include: [],
2232
+ exclude: [],
2233
+ disableEnv: false,
2234
+ registryInjected: opts.registryInjected,
2235
+ fromCliConfig,
2236
+ name
2237
+ });
2238
+ }
2239
+ if (typeof config !== "object" || Array.isArray(config)) {
2240
+ process.stderr.write(
2241
+ `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
2242
+ `
2266
2243
  );
2267
- _ConfigEncryptor.weakFallbackWarned = true;
2244
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2268
2245
  }
2269
- const hostname2 = os3.hostname();
2270
- const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2271
- const material = `${hostname2}:${username}`;
2272
- return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
2273
- }
2274
- aesEncrypt(plaintext) {
2275
- const salt = crypto2.randomBytes(16);
2276
- const key = this.deriveKey(salt);
2277
- const nonce = crypto2.randomBytes(12);
2278
- const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
2279
- const ct = Buffer.concat([
2280
- cipher.update(plaintext, "utf-8"),
2281
- cipher.final()
2282
- ]);
2283
- const tag = cipher.getAuthTag();
2284
- return Buffer.concat([salt, nonce, tag, ct]);
2285
- }
2286
- aesDecrypt(data) {
2287
- const salt = data.subarray(0, 16);
2288
- const nonce = data.subarray(16, 28);
2289
- const tag = data.subarray(28, 44);
2290
- const ct = data.subarray(44);
2291
- const key = this.deriveKey(salt);
2292
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2293
- decipher.setAuthTag(tag);
2294
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2295
- }
2296
- /** Decrypt legacy v1-format values: nonce(12)+tag(16)+ct, static salt. */
2297
- aesDecryptV1(data) {
2298
- const nonce = data.subarray(0, 12);
2299
- const tag = data.subarray(12, 28);
2300
- const ct = data.subarray(28);
2301
- const hostname2 = os3.hostname();
2302
- const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2303
- const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2304
- const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
2305
- for (const material of materials) {
2306
- for (const iterations of [6e5, 1e5]) {
2307
- try {
2308
- const key = crypto2.pbkdf2Sync(material, V1_STATIC_SALT, iterations, 32, "sha256");
2309
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2310
- decipher.setAuthTag(tag);
2311
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2312
- } catch {
2313
- }
2246
+ const cfg = config;
2247
+ let mode;
2248
+ if (cfg.mode === void 0 || cfg.mode === null) {
2249
+ mode = "auto";
2250
+ } else if (typeof cfg.mode !== "string") {
2251
+ process.stderr.write(
2252
+ `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
2253
+ `
2254
+ );
2255
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2256
+ } else if (!VALID_USER_MODES.has(cfg.mode)) {
2257
+ process.stderr.write(
2258
+ `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
2259
+ `
2260
+ );
2261
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2262
+ } else {
2263
+ mode = cfg.mode;
2264
+ }
2265
+ const include = _ApcliGroup._normalizeList(cfg.include, "include");
2266
+ const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
2267
+ const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
2268
+ let disableEnv = false;
2269
+ if (rawDisableEnv !== void 0) {
2270
+ if (typeof rawDisableEnv === "boolean") {
2271
+ disableEnv = rawDisableEnv;
2272
+ } else {
2273
+ warn(
2274
+ `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
2275
+ );
2314
2276
  }
2315
2277
  }
2316
- throw new Error("v1 decryption failed with all material+iteration combinations");
2317
- }
2318
- };
2319
- }
2320
- });
2321
-
2322
- // src/security/auth.ts
2323
- var AuthProvider;
2324
- var init_auth = __esm({
2325
- "src/security/auth.ts"() {
2326
- "use strict";
2327
- init_esm_shims();
2328
- init_errors();
2329
- init_config_encryptor();
2330
- AuthProvider = class {
2331
- config;
2332
- _encryptor;
2333
- constructor(config, encryptor) {
2334
- this.config = config;
2335
- this._encryptor = encryptor;
2278
+ return new _ApcliGroup({
2279
+ mode,
2280
+ include,
2281
+ exclude,
2282
+ disableEnv,
2283
+ registryInjected: opts.registryInjected,
2284
+ fromCliConfig,
2285
+ name
2286
+ });
2336
2287
  }
2337
2288
  /**
2338
- * Resolve the active ConfigEncryptor instance.
2289
+ * Normalize an include/exclude list. Non-array → warn and return [].
2339
2290
  *
2340
- * D11-005 (2026-05-12): three-tier fallback chain matching Python's
2341
- * `_get_encryptor` (auth.py:33): explicit constructor arg > peer attribute
2342
- * `config.encryptor` (set by embedders injecting forced-AES test fixtures
2343
- * or shared instances) > fresh `new ConfigEncryptor()`. Previously TS
2344
- * skipped the peer-attribute tier, silently giving embedders a different
2345
- * encryptor than the one they wired on the config.
2346
- */
2347
- getEncryptor() {
2348
- if (this._encryptor) return this._encryptor;
2349
- const fromConfig = this.config.encryptor;
2350
- if (fromConfig) return fromConfig;
2351
- return new ConfigEncryptor();
2352
- }
2353
- /**
2354
- * Retrieve the API key from the configured sources.
2355
- * Handles keyring: and enc: prefixes via ConfigEncryptor.
2291
+ * Unknown but well-formed entries emit a WARNING (spec §7 error table,
2292
+ * T-APCLI-25) but are retained in the returned list for forward-compat —
2293
+ * if apcore-cli later adds a subcommand named `foo`, existing configs
2294
+ * continue to work without a config change. At runtime, unknown names
2295
+ * simply never match any registered subcommand.
2356
2296
  */
2357
- async getApiKey() {
2358
- const result = this.config.resolve(
2359
- "auth.api_key",
2360
- "--api-key",
2361
- "APCORE_AUTH_API_KEY"
2362
- );
2363
- if (result === null || result === void 0) {
2364
- return null;
2297
+ static _normalizeList(raw, label) {
2298
+ if (raw === void 0 || raw === null) return [];
2299
+ if (!Array.isArray(raw)) {
2300
+ warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
2301
+ return [];
2365
2302
  }
2366
- const strResult = String(result);
2367
- if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
2368
- try {
2369
- return await this.getEncryptor().retrieve(strResult, "auth.api_key");
2370
- } catch (err) {
2371
- if (err instanceof ConfigDecryptionError) {
2372
- throw new AuthenticationError(
2373
- "Failed to decrypt stored API key. Re-store with 'apcli config set auth.api_key'."
2303
+ const out = [];
2304
+ for (const entry of raw) {
2305
+ if (typeof entry === "string" && entry.length > 0) {
2306
+ if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
2307
+ warn(
2308
+ `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
2374
2309
  );
2375
2310
  }
2376
- throw err;
2311
+ out.push(entry);
2312
+ } else {
2313
+ warn(`apcli.${label} contains non-string entry; skipping.`);
2377
2314
  }
2378
2315
  }
2379
- return strResult;
2316
+ return out;
2380
2317
  }
2318
+ // -------------------------------------------------------------------------
2319
+ // Public API
2320
+ // -------------------------------------------------------------------------
2381
2321
  /**
2382
- * Add authentication headers to an outgoing request.
2322
+ * Resolve effective visibility mode after applying tier precedence.
2383
2323
  *
2384
- * Cross-SDK contract (D10-002, 2026-04-26): the input `headers` object
2385
- * is mutated **in place** and the same reference is returned. Callers
2386
- * that share the headers reference (the documented pattern in
2387
- * apcore-cli/docs/features/security.md §AuthProvider) can read
2388
- * `headers.Authorization` after the call without re-binding the
2389
- * return value. Python and Rust both mutate-and-return; TS previously
2390
- * spread into a new object, which silently broke shared-reference
2391
- * callers.
2324
+ * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
2325
+ *
2326
+ * Tier order (spec §4.4):
2327
+ * 1. CliConfig non-auto wins outright.
2328
+ * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
2329
+ * 3. yaml non-auto.
2330
+ * 4. Auto-detect from registryInjected.
2392
2331
  */
2393
- async authenticateRequest(headers) {
2394
- const key = await this.getApiKey();
2395
- if (!key) {
2396
- throw new AuthenticationError(
2397
- "Remote registry requires authentication. Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
2398
- );
2332
+ resolveVisibility() {
2333
+ if (this._fromCliConfig && this._mode !== "auto") {
2334
+ return this._mode;
2399
2335
  }
2400
- if (/[\r\n]/.test(key)) {
2401
- throw new AuthenticationError(
2402
- "Malformed API key: contains invalid characters (CR/LF). Re-store with 'apcli config set auth.api_key'."
2403
- );
2336
+ if (!this._disableEnv) {
2337
+ const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
2338
+ if (envMode !== null) {
2339
+ return envMode;
2340
+ }
2404
2341
  }
2405
- headers.Authorization = `Bearer ${key.trim()}`;
2406
- return headers;
2342
+ if (this._mode !== "auto") {
2343
+ return this._mode;
2344
+ }
2345
+ return this._registryInjected ? "none" : "all";
2407
2346
  }
2408
2347
  /**
2409
- * Handle an HTTP response status code for auth-related errors.
2348
+ * True iff `subcommand` passes the include/exclude filter.
2349
+ *
2350
+ * Callers MUST first check {@link resolveVisibility} — this method throws
2351
+ * under modes `"all"` or `"none"` (caller bug per spec §4.6).
2410
2352
  */
2411
- handleResponse(statusCode) {
2412
- if (statusCode === 401 || statusCode === 403) {
2413
- throw new AuthenticationError(
2414
- "Authentication failed. Verify your API key."
2415
- );
2416
- }
2353
+ isSubcommandIncluded(subcommand) {
2354
+ const mode = this.resolveVisibility();
2355
+ if (mode === "include") return this._include.includes(subcommand);
2356
+ if (mode === "exclude") return !this._exclude.includes(subcommand);
2357
+ throw new Error(
2358
+ `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
2359
+ );
2417
2360
  }
2418
- };
2419
- }
2420
- });
2421
-
2422
- // src/security/index.ts
2423
- var security_exports = {};
2424
- __export(security_exports, {
2425
- AuditLogger: () => AuditLogger,
2426
- AuthProvider: () => AuthProvider,
2427
- ConfigEncryptor: () => ConfigEncryptor,
2428
- Sandbox: () => Sandbox,
2429
- getAuditLogger: () => getAuditLogger,
2430
- setAuditLogger: () => setAuditLogger
2361
+ /** True iff the `apcli` group itself should appear in root `--help`. */
2362
+ isGroupVisible() {
2363
+ return this.resolveVisibility() !== "none";
2364
+ }
2365
+ // -------------------------------------------------------------------------
2366
+ // Env parser (Tier 2) — co-located per spec §4.4
2367
+ // -------------------------------------------------------------------------
2368
+ /**
2369
+ * Parse APCORE_CLI_APCLI. Case-insensitive.
2370
+ *
2371
+ * - `show` / `1` / `true` → `"all"`
2372
+ * - `hide` / `0` / `false` → `"none"`
2373
+ * - Empty / unset → `null`
2374
+ * - Anything else → warn and return `null`
2375
+ */
2376
+ _parseEnv(raw) {
2377
+ if (raw === void 0 || raw === "") return null;
2378
+ const normalized = raw.trim().toLowerCase();
2379
+ if (normalized === "") return null;
2380
+ if (normalized === "show" || normalized === "1" || normalized === "true") {
2381
+ return "all";
2382
+ }
2383
+ if (normalized === "hide" || normalized === "0" || normalized === "false") {
2384
+ return "none";
2385
+ }
2386
+ warn(
2387
+ `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
2388
+ );
2389
+ return null;
2390
+ }
2391
+ };
2392
+ }
2431
2393
  });
2432
- var init_security = __esm({
2433
- "src/security/index.ts"() {
2394
+
2395
+ // src/cli.ts
2396
+ import { Command as Command2 } from "commander";
2397
+ function listAllDefinitions(registry) {
2398
+ const defs = [];
2399
+ for (const id of registry.list()) {
2400
+ const def = registry.getDefinition(id);
2401
+ if (def) defs.push(def);
2402
+ }
2403
+ return defs;
2404
+ }
2405
+ var init_cli = __esm({
2406
+ "src/cli.ts"() {
2434
2407
  "use strict";
2435
2408
  init_esm_shims();
2436
- init_audit();
2437
- init_auth();
2438
- init_config_encryptor();
2439
- init_sandbox();
2409
+ init_main();
2410
+ init_display_helpers();
2411
+ init_exposure();
2412
+ init_logger();
2413
+ init_builtin_group();
2414
+ init_errors();
2440
2415
  }
2441
2416
  });
2442
2417
 
2443
- // src/discovery.ts
2444
- import { Command as Command2, Option as Option2 } from "commander";
2445
- function validateTag(tag) {
2446
- if (!TAG_PATTERN.test(tag)) {
2447
- process.stderr.write(
2448
- `Error: Invalid tag format: '${tag}'. Tags must match [a-z][a-z0-9_-]*.
2449
- `
2450
- );
2451
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2452
- }
2453
- }
2454
- function collectTag(value, previous) {
2455
- return previous.concat([value]);
2418
+ // src/security/audit.ts
2419
+ var audit_exports = {};
2420
+ __export(audit_exports, {
2421
+ AuditLogger: () => AuditLogger,
2422
+ canonicalizeForHash: () => canonicalizeForHash,
2423
+ getAuditLogger: () => getAuditLogger,
2424
+ setAuditLogger: () => setAuditLogger
2425
+ });
2426
+ import * as crypto from "crypto";
2427
+ import * as fs3 from "fs";
2428
+ import * as os from "os";
2429
+ import * as path3 from "path";
2430
+ function setAuditLogger(auditLogger) {
2431
+ _auditLogger = auditLogger;
2456
2432
  }
2457
- function collectAnnotation(value, previous) {
2458
- return previous.concat([value]);
2433
+ function getAuditLogger() {
2434
+ return _auditLogger;
2459
2435
  }
2460
- function getAnnotationFlag(moduleDef, flag) {
2461
- const annotations = moduleDef.annotations;
2462
- if (!annotations || typeof annotations !== "object") return false;
2463
- const ann = annotations;
2464
- const map = {
2465
- "destructive": "destructive",
2466
- "requires-approval": "requires_approval",
2467
- "readonly": "readonly",
2468
- "streaming": "streaming",
2469
- "cacheable": "cacheable",
2470
- "idempotent": "idempotent",
2471
- "paginated": "paginated"
2472
- };
2473
- const attr = map[flag] ?? flag;
2474
- return ann[attr] === true;
2436
+ function canonicalizeForHash(value) {
2437
+ if (value === null || typeof value !== "object") return value;
2438
+ if (Array.isArray(value)) return value.map(canonicalizeForHash);
2439
+ const src = value;
2440
+ const sorted = {};
2441
+ for (const key of Object.keys(src).sort()) {
2442
+ sorted[key] = canonicalizeForHash(src[key]);
2443
+ }
2444
+ return sorted;
2475
2445
  }
2476
- function registerListCommand(apcliGroup, registry, exposureFilter) {
2477
- const listCmd = new Command2("list").description("List available modules in the registry.").option("--tag <tag>", "Filter modules by tag (AND logic). Repeatable.", collectTag, []).option("--flat", "Show flat list (no grouping).", false).addOption(
2478
- new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2479
- ).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2480
- new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2481
- ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2482
- new Option2("--sort <field>", "Sort order.").choices(["id", "calls", "errors", "latency"]).default("id")
2483
- ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2484
- new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2485
- ).action((opts) => {
2486
- for (const t of opts.tag) {
2487
- validateTag(t);
2488
- }
2489
- let modules = [];
2490
- for (const m of registry.listModules()) {
2491
- modules.push(m);
2492
- }
2493
- if (opts.tag.length > 0) {
2494
- const filterTags = new Set(opts.tag);
2495
- modules = modules.filter((m) => {
2496
- const mTags = m.tags ?? [];
2497
- return [...filterTags].every((t) => mTags.includes(t));
2498
- });
2499
- }
2500
- if (opts.search) {
2501
- const query = opts.search.toLowerCase();
2502
- modules = modules.filter(
2503
- (m) => (m.id ?? "").toLowerCase().includes(query) || (m.description ?? "").toLowerCase().includes(query)
2446
+ var _auditLogger, AuditLogger;
2447
+ var init_audit = __esm({
2448
+ "src/security/audit.ts"() {
2449
+ "use strict";
2450
+ init_esm_shims();
2451
+ init_logger();
2452
+ _auditLogger = null;
2453
+ AuditLogger = class _AuditLogger {
2454
+ static DEFAULT_PATH = path3.join(
2455
+ os.homedir(),
2456
+ ".apcore-cli",
2457
+ "audit.jsonl"
2504
2458
  );
2505
- }
2506
- if (opts.status === "enabled") {
2507
- modules = modules.filter((m) => {
2508
- const enabled = m.enabled;
2509
- return enabled !== false;
2510
- });
2511
- } else if (opts.status === "disabled") {
2512
- modules = modules.filter((m) => {
2513
- const enabled = m.enabled;
2514
- return enabled === false;
2515
- });
2516
- }
2517
- if (!opts.deprecated) {
2518
- modules = modules.filter((m) => {
2519
- const deprecated = m.deprecated;
2520
- return deprecated !== true;
2521
- });
2522
- }
2523
- if (opts.annotation.length > 0) {
2524
- for (const annFlag of opts.annotation) {
2525
- modules = modules.filter((m) => getAnnotationFlag(m, annFlag));
2459
+ logPath;
2460
+ writeFailureWarned = false;
2461
+ constructor(path6) {
2462
+ this.logPath = path6 ?? _AuditLogger.DEFAULT_PATH;
2463
+ this.ensureDirectory();
2526
2464
  }
2527
- }
2528
- if (opts.sort === "calls" || opts.sort === "errors" || opts.sort === "latency") {
2529
- const { used } = sortModulesByUsage(modules, opts.sort, { reverse: !opts.reverse });
2530
- if (!used) {
2531
- process.stderr.write(
2532
- `note: no usage data available for --sort ${opts.sort}; sorted by id. Run some modules first to populate ~/.apcore-cli/audit.jsonl.
2533
- `
2534
- );
2465
+ ensureDirectory() {
2466
+ const dir = path3.dirname(this.logPath);
2467
+ try {
2468
+ fs3.mkdirSync(dir, { recursive: true });
2469
+ try {
2470
+ fs3.chmodSync(dir, 448);
2471
+ } catch {
2472
+ }
2473
+ } catch {
2474
+ }
2535
2475
  }
2536
- } else {
2537
- modules.sort((a, b) => (a.id ?? "").localeCompare(b.id ?? ""));
2538
- if (opts.reverse) {
2539
- modules.reverse();
2476
+ logExecution(moduleId, inputData, status, exitCode, durationMs) {
2477
+ const entry = {
2478
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2479
+ user: this.getUser(),
2480
+ module_id: moduleId,
2481
+ input_hash: this.hashInput(inputData),
2482
+ status,
2483
+ exit_code: exitCode,
2484
+ duration_ms: durationMs
2485
+ };
2486
+ try {
2487
+ fs3.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
2488
+ try {
2489
+ fs3.chmodSync(this.logPath, 384);
2490
+ } catch {
2491
+ }
2492
+ } catch (err) {
2493
+ if (!this.writeFailureWarned) {
2494
+ this.writeFailureWarned = true;
2495
+ warn(`Could not write audit log: ${err}`);
2496
+ }
2497
+ }
2540
2498
  }
2541
- }
2542
- let showExposureCol = false;
2543
- if (exposureFilter && opts.exposure !== "all") {
2544
- if (opts.exposure === "exposed") {
2545
- modules = modules.filter((m) => exposureFilter.isExposed(m.id ?? ""));
2546
- } else if (opts.exposure === "hidden") {
2547
- modules = modules.filter((m) => !exposureFilter.isExposed(m.id ?? ""));
2499
+ hashInput(inputData) {
2500
+ const salt = crypto.randomBytes(16);
2501
+ const payload = JSON.stringify(canonicalizeForHash(inputData));
2502
+ return crypto.createHash("sha256").update(Buffer.concat([salt, Buffer.from(payload, "utf-8")])).digest("hex");
2548
2503
  }
2549
- }
2550
- if (opts.exposure === "all" && exposureFilter) {
2551
- showExposureCol = true;
2552
- }
2553
- const fmt = resolveFormat(opts.format);
2554
- const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
2555
- void formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
2556
- });
2557
- apcliGroup.addCommand(listCmd);
2558
- }
2559
- function registerDescribeCommand(apcliGroup, registry) {
2560
- const describeCmd = new Command2("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").addOption(
2561
- new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2562
- ).action((moduleId, opts) => {
2563
- validateModuleId(moduleId);
2564
- const moduleDef = registry.getModule(moduleId);
2565
- if (!moduleDef) {
2566
- process.stderr.write(
2567
- `Error: Module '${moduleId}' not found.
2568
- `
2569
- );
2570
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2571
- }
2572
- const fmt = resolveFormat(opts.format);
2573
- void formatModuleDetail(moduleDef, fmt);
2574
- });
2575
- apcliGroup.addCommand(describeCmd);
2576
- }
2577
- function registerExecCommand(apcliGroup, registry, executor) {
2578
- const execCmd = new Command2("exec").description("Execute a module by ID with JSON input.").argument("<module-id>", "Module ID to execute").option("--format <format>", "Output format (json, table, csv, yaml, jsonl).").option("--fields <fields>", "Comma-separated dot-paths to select from the result.").option(
2579
- "--input <json>",
2580
- "JSON object passed as input to the module. Use '-' to read JSON from stdin."
2581
- ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
2582
- "--approval-timeout <seconds>",
2583
- "Seconds to wait for interactive approval.",
2584
- parseInt
2585
- ).option("--sandbox", "Run module in an isolated subprocess with restricted env.", false).option("--strategy <name>", "Execution strategy (standard, parallel, sequential, etc.).").option("--trace", "Enable pipeline trace output.", false).option("--dry-run", "Validate inputs without executing the module.", false).option("--stream", "Stream output as JSONL instead of buffering.", false).action(async (moduleId, opts) => {
2586
- validateModuleId(moduleId);
2587
- const moduleDef = registry.getModule(moduleId);
2588
- if (!moduleDef) {
2589
- process.stderr.write(`Error: Module '${moduleId}' not found.
2590
- `);
2591
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2592
- }
2593
- let merged = {};
2594
- if (opts.input === "-") {
2595
- merged = await collectInput("-", {}, false);
2596
- } else if (opts.input !== void 0) {
2597
- try {
2598
- const parsed = JSON.parse(opts.input);
2599
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2600
- process.stderr.write("Error: --input JSON must be an object.\n");
2601
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2504
+ getUser() {
2505
+ try {
2506
+ return os.userInfo().username;
2507
+ } catch {
2508
+ return process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2602
2509
  }
2603
- merged = parsed;
2604
- } catch (err) {
2605
- const msg = err instanceof Error ? err.message : String(err);
2606
- process.stderr.write(`Error: --input is not valid JSON: ${msg}
2607
- `);
2608
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2609
2510
  }
2610
- }
2611
- const startTime = performance.now();
2511
+ };
2512
+ }
2513
+ });
2514
+
2515
+ // src/system-usage.ts
2516
+ import * as fs4 from "fs";
2517
+ import * as os2 from "os";
2518
+ import * as path4 from "path";
2519
+ function computeSummary(options = {}) {
2520
+ const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
2521
+ const period = options.period ?? "24h";
2522
+ const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - PERIOD_TO_MS[period];
2523
+ if (!fs4.existsSync(auditPath)) {
2524
+ return /* @__PURE__ */ new Map();
2525
+ }
2526
+ let raw;
2527
+ try {
2528
+ raw = fs4.readFileSync(auditPath, "utf-8");
2529
+ } catch {
2530
+ return /* @__PURE__ */ new Map();
2531
+ }
2532
+ const counts = /* @__PURE__ */ new Map();
2533
+ const errors = /* @__PURE__ */ new Map();
2534
+ const latencySum = /* @__PURE__ */ new Map();
2535
+ for (const line of raw.split("\n")) {
2536
+ const trimmed = line.trim();
2537
+ if (!trimmed) continue;
2538
+ let entry;
2612
2539
  try {
2613
- await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
2614
- if (opts.dryRun) {
2615
- if (executor.validate) {
2616
- const preflight = await executor.validate(moduleId, merged);
2617
- formatPreflightResult(preflight, opts.format);
2618
- } else {
2619
- process.stdout.write(JSON.stringify({ valid: true }) + "\n");
2620
- }
2621
- return;
2622
- }
2623
- let result;
2624
- if ((opts.trace || opts.strategy) && executor.callWithTrace) {
2625
- const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
2626
- result = res;
2627
- } else {
2628
- const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
2629
- const sandbox = new Sandbox2(opts.sandbox);
2630
- result = await sandbox.execute(moduleId, merged, executor);
2631
- }
2632
- const durationMs = Math.round(performance.now() - startTime);
2633
- const fmt = resolveFormat(opts.format);
2634
- formatExecResult(result, fmt, opts.fields);
2635
- const auditLogger = getAuditLogger();
2636
- if (auditLogger) {
2637
- auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
2638
- }
2639
- } catch (err) {
2640
- const exitCode = exitCodeForError(err);
2641
- const durationMs = Math.round(performance.now() - startTime);
2642
- try {
2643
- const auditLogger = getAuditLogger();
2644
- if (auditLogger) {
2645
- auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
2646
- }
2647
- } catch {
2648
- }
2649
- process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2650
- `);
2651
- process.exit(exitCode);
2652
- }
2653
- });
2654
- apcliGroup.addCommand(execCmd);
2655
- }
2656
- function registerValidateCommand(cli, registry, executor) {
2657
- const validateCmd = new Command2("validate").description("Run preflight checks without executing a module.").argument("<module-id>", "Module ID to validate").option("--input <source>", "JSON input file or '-' for stdin.").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2658
- validateModuleId(moduleId);
2659
- const moduleDef = registry.getModule(moduleId);
2660
- if (!moduleDef) {
2661
- process.stderr.write(`Error: Module '${moduleId}' not found.
2662
- `);
2663
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2540
+ entry = JSON.parse(trimmed);
2541
+ } catch {
2542
+ continue;
2664
2543
  }
2665
- const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
2666
- if (!executor.validate) {
2667
- process.stderr.write("Error: Executor does not support validate.\n");
2668
- process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
2544
+ const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
2545
+ if (Number.isNaN(ts) || ts < cutoff) continue;
2546
+ const moduleId = typeof entry.module_id === "string" ? entry.module_id : null;
2547
+ if (!moduleId) continue;
2548
+ counts.set(moduleId, (counts.get(moduleId) ?? 0) + 1);
2549
+ if (entry.status === "error") {
2550
+ errors.set(moduleId, (errors.get(moduleId) ?? 0) + 1);
2669
2551
  }
2670
- try {
2671
- const preflight = await executor.validate(moduleId, merged);
2672
- formatPreflightResult(preflight, opts.format);
2673
- process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2674
- } catch (err) {
2675
- const exitCode = exitCodeForError(err);
2676
- try {
2677
- const auditLogger = getAuditLogger();
2678
- if (auditLogger) {
2679
- auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
2680
- }
2681
- } catch {
2682
- }
2683
- process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2684
- `);
2685
- process.exit(exitCode);
2552
+ const duration = entry.duration_ms;
2553
+ if (typeof duration === "number") {
2554
+ latencySum.set(moduleId, (latencySum.get(moduleId) ?? 0) + duration);
2686
2555
  }
2687
- });
2688
- cli.addCommand(validateCmd);
2689
- }
2690
- var TAG_PATTERN;
2691
- var init_discovery = __esm({
2692
- "src/discovery.ts"() {
2693
- "use strict";
2694
- init_esm_shims();
2695
- init_approval();
2696
- init_errors();
2697
- init_main();
2698
- init_output();
2699
- init_audit();
2700
- init_system_usage();
2701
- TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2702
2556
  }
2703
- });
2704
-
2705
- // src/system-cmd.ts
2706
- import { Command as Command3 } from "commander";
2707
- async function callSystemModule(executor, moduleId, inputs) {
2708
- if (executor.call) {
2709
- return executor.call(moduleId, inputs);
2557
+ const out = /* @__PURE__ */ new Map();
2558
+ for (const [id, calls] of counts) {
2559
+ out.set(id, {
2560
+ module_id: id,
2561
+ calls,
2562
+ errors: errors.get(id) ?? 0,
2563
+ latency_ms: calls > 0 ? (latencySum.get(id) ?? 0) / calls : 0
2564
+ });
2710
2565
  }
2711
- return executor.execute(moduleId, inputs);
2566
+ return out;
2712
2567
  }
2713
- function emitResult(jsonPayload, fmt, ttyRender) {
2714
- if (fmt === "json" || !process.stdout.isTTY) {
2715
- process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
2716
- } else {
2717
- ttyRender();
2568
+ function sortModulesByUsage(modules, field, options = {}) {
2569
+ const reverse = options.reverse ?? true;
2570
+ const summary = computeSummary({
2571
+ auditPath: options.auditPath,
2572
+ period: options.period
2573
+ });
2574
+ const idOf = (m) => m.moduleId ?? m.id ?? m.module_id ?? "";
2575
+ if (summary.size === 0) {
2576
+ modules.sort((a, b) => idOf(a).localeCompare(idOf(b)));
2577
+ if (reverse) modules.reverse();
2578
+ return { used: false };
2718
2579
  }
2719
- }
2720
- function emitErrorAndExit(e) {
2721
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2722
- `);
2723
- process.exit(exitCodeForError(e));
2724
- }
2725
- async function requireApprovalForSystemCommand(moduleId, autoApprove) {
2726
- const syntheticModuleDef = {
2727
- id: moduleId,
2728
- name: moduleId,
2729
- description: `system command: ${moduleId}`,
2730
- annotations: { requires_approval: true }
2580
+ const key = (m) => {
2581
+ const id = idOf(m);
2582
+ const s = summary.get(id);
2583
+ if (!s) return 0;
2584
+ return field === "latency" ? s.latency_ms : field === "calls" ? s.calls : s.errors;
2731
2585
  };
2732
- await checkApproval(syntheticModuleDef, autoApprove, void 0);
2586
+ modules.sort((a, b) => {
2587
+ const diff = key(a) - key(b);
2588
+ if (diff !== 0) return reverse ? -diff : diff;
2589
+ return idOf(a).localeCompare(idOf(b));
2590
+ });
2591
+ return { used: true };
2733
2592
  }
2734
- function formatHealthSummaryTty(result) {
2735
- const summary = result.summary ?? {};
2736
- const modules = result.modules ?? [];
2737
- if (modules.length === 0) {
2738
- process.stdout.write("No modules found.\n");
2739
- return;
2740
- }
2741
- const total = summary.total_modules ?? modules.length;
2742
- process.stdout.write(`Health Overview (${total} modules)
2743
-
2744
- `);
2745
- process.stdout.write(` ${"Module".padEnd(28)} ${"Status".padEnd(12)} ${"Error Rate".padEnd(12)} Top Error
2746
- `);
2747
- process.stdout.write(` ${"-".repeat(28)} ${"-".repeat(12)} ${"-".repeat(12)} ${"-".repeat(20)}
2748
- `);
2749
- for (const m of modules) {
2750
- const top = m.top_error;
2751
- const topStr = top ? `${top.code} (${top.count ?? "?"})` : "\u2014";
2752
- const rate = `${((m.error_rate ?? 0) * 100).toFixed(1)}%`;
2753
- process.stdout.write(
2754
- ` ${String(m.module_id).padEnd(28)} ${String(m.status).padEnd(12)} ${rate.padEnd(12)} ${topStr}
2755
- `
2593
+ var PERIOD_TO_MS, DEFAULT_AUDIT_PATH;
2594
+ var init_system_usage = __esm({
2595
+ "src/system-usage.ts"() {
2596
+ "use strict";
2597
+ init_esm_shims();
2598
+ PERIOD_TO_MS = {
2599
+ "1h": 60 * 60 * 1e3,
2600
+ "24h": 24 * 60 * 60 * 1e3,
2601
+ "7d": 7 * 24 * 60 * 60 * 1e3,
2602
+ "30d": 30 * 24 * 60 * 60 * 1e3
2603
+ };
2604
+ DEFAULT_AUDIT_PATH = path4.join(
2605
+ os2.homedir(),
2606
+ ".apcore-cli",
2607
+ "audit.jsonl"
2756
2608
  );
2757
2609
  }
2758
- const parts = [];
2759
- for (const key of ["healthy", "degraded", "error"]) {
2760
- const count = summary[key];
2761
- if (count) parts.push(`${count} ${key}`);
2610
+ });
2611
+
2612
+ // src/security/config-encryptor.ts
2613
+ import * as crypto2 from "crypto";
2614
+ import * as os3 from "os";
2615
+ async function getKeytar() {
2616
+ if (keytarModule) return keytarModule;
2617
+ try {
2618
+ keytarModule = await import("keytar");
2619
+ return keytarModule;
2620
+ } catch {
2621
+ return null;
2762
2622
  }
2763
- process.stdout.write(`
2764
- Summary: ${parts.join(", ") || "no data"}
2765
- `);
2766
- }
2767
- function formatHealthModuleTty(result) {
2768
- process.stdout.write(`Module: ${result.module_id ?? "?"}
2769
- `);
2770
- process.stdout.write(`Status: ${result.status ?? "unknown"}
2771
- `);
2772
- const total = result.total_calls ?? 0;
2773
- const errors = result.error_count ?? 0;
2774
- const rate = result.error_rate ?? 0;
2775
- const avg = result.avg_latency_ms ?? 0;
2776
- const p99 = result.p99_latency_ms ?? 0;
2777
- process.stdout.write(`Calls: ${total.toLocaleString()} total | ${errors.toLocaleString()} errors | ${(rate * 100).toFixed(1)}% error rate
2778
- `);
2779
- process.stdout.write(`Latency: ${avg.toFixed(0)}ms avg | ${p99.toFixed(0)}ms p99
2780
- `);
2781
- const recent = result.recent_errors ?? [];
2782
- if (recent.length > 0) {
2783
- process.stdout.write(`
2784
- Recent Errors (top ${recent.length}):
2785
- `);
2786
- for (const e of recent) {
2787
- const count = e.count ?? "?";
2788
- const last = e.last_occurred ?? "?";
2789
- process.stdout.write(` ${String(e.code ?? "?").padEnd(24)} x${count} (last: ${last})
2790
- `);
2791
- }
2792
- }
2793
- }
2794
- function formatUsageSummaryTty(result) {
2795
- const modules = result.modules ?? [];
2796
- const period = result.period ?? "?";
2797
- if (modules.length === 0) {
2798
- process.stdout.write(`No usage data for period ${period}.
2799
- `);
2800
- return;
2801
- }
2802
- process.stdout.write(`Usage Summary (last ${period})
2803
-
2804
- `);
2805
- process.stdout.write(` ${"Module".padEnd(24)} ${"Calls".padStart(8)} ${"Errors".padStart(8)} ${"Avg Latency".padStart(12)} ${"Trend".padStart(10)}
2806
- `);
2807
- process.stdout.write(` ${"-".repeat(24)} ${"-".repeat(8)} ${"-".repeat(8)} ${"-".repeat(12)} ${"-".repeat(10)}
2808
- `);
2809
- for (const m of modules) {
2810
- const avg = `${(m.avg_latency_ms ?? 0).toFixed(0)}ms`;
2811
- process.stdout.write(
2812
- ` ${String(m.module_id).padEnd(24)} ${String(m.call_count ?? 0).padStart(8)} ${String(m.error_count ?? 0).padStart(8)} ${avg.padStart(12)} ${String(m.trend ?? "").padStart(10)}
2813
- `
2814
- );
2815
- }
2816
- const totalCalls = result.total_calls ?? modules.reduce((s, m) => s + (m.call_count ?? 0), 0);
2817
- const totalErrors = result.total_errors ?? modules.reduce((s, m) => s + (m.error_count ?? 0), 0);
2818
- process.stdout.write(`
2819
- Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
2820
- `);
2821
- }
2822
- function registerHealthCommand(apcliGroup, executor) {
2823
- const healthCmd = new Command3("health").description("Show module health status. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed health").option("--threshold <number>", "Error rate threshold (default: 0.01).", parseFloat, 0.01).option("--all", "Include healthy modules.", false).option("--errors <count>", "Max recent errors (module detail only).", parseInt, 10).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2824
- const fmt = resolveFormat(opts.format);
2825
- try {
2826
- if (moduleId) {
2827
- const result = await callSystemModule(executor, "system.health.module", {
2828
- module_id: moduleId,
2829
- error_limit: opts.errors
2830
- });
2831
- emitResult(result, fmt, () => formatHealthModuleTty(result));
2832
- } else {
2833
- const result = await callSystemModule(executor, "system.health.summary", {
2834
- error_rate_threshold: opts.threshold,
2835
- include_healthy: opts.all
2836
- });
2837
- emitResult(result, fmt, () => formatHealthSummaryTty(result));
2838
- }
2839
- } catch (e) {
2840
- emitErrorAndExit(e);
2841
- }
2842
- });
2843
- apcliGroup.addCommand(healthCmd);
2844
- }
2845
- function registerUsageCommand(apcliGroup, executor) {
2846
- const usageCmd = new Command3("usage").description("Show module usage statistics. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed usage").option("--period <period>", "Time window: 1h, 24h, 7d, 30d.", "24h").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2847
- const fmt = resolveFormat(opts.format);
2848
- try {
2849
- let result;
2850
- if (moduleId) {
2851
- result = await callSystemModule(executor, "system.usage.module", {
2852
- module_id: moduleId,
2853
- period: opts.period
2854
- });
2855
- } else {
2856
- result = await callSystemModule(executor, "system.usage.summary", {
2857
- period: opts.period
2858
- });
2859
- }
2860
- emitResult(result, fmt, () => {
2861
- if (moduleId) {
2862
- formatExecResult(result, fmt);
2863
- } else {
2864
- formatUsageSummaryTty(result);
2865
- }
2866
- });
2867
- } catch (e) {
2868
- emitErrorAndExit(e);
2869
- }
2870
- });
2871
- apcliGroup.addCommand(usageCmd);
2872
- }
2873
- function registerEnableCommand(apcliGroup, executor) {
2874
- const enableCmd = new Command3("enable").description("Enable a disabled module at runtime.").argument("<module-id>", "Module ID to enable").requiredOption("--reason <reason>", "Reason for enabling (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2875
- const fmt = resolveFormat(opts.format);
2876
- try {
2877
- await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2878
- const result = await callSystemModule(executor, "system.control.toggle_feature", {
2879
- module_id: moduleId,
2880
- enabled: true,
2881
- reason: opts.reason
2882
- });
2883
- emitResult(result, fmt, () => {
2884
- process.stdout.write(`Module '${moduleId}' enabled.
2885
- Reason: ${opts.reason}
2886
- `);
2887
- });
2888
- } catch (e) {
2889
- emitErrorAndExit(e);
2890
- }
2891
- });
2892
- apcliGroup.addCommand(enableCmd);
2893
- }
2894
- function registerDisableCommand(apcliGroup, executor) {
2895
- const disableCmd = new Command3("disable").description("Disable a module at runtime (calls are rejected until re-enabled).").argument("<module-id>", "Module ID to disable").requiredOption("--reason <reason>", "Reason for disabling (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2896
- const fmt = resolveFormat(opts.format);
2897
- try {
2898
- await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2899
- const result = await callSystemModule(executor, "system.control.toggle_feature", {
2900
- module_id: moduleId,
2901
- enabled: false,
2902
- reason: opts.reason
2903
- });
2904
- emitResult(result, fmt, () => {
2905
- process.stdout.write(`Module '${moduleId}' disabled.
2906
- Reason: ${opts.reason}
2907
- `);
2908
- });
2909
- } catch (e) {
2910
- emitErrorAndExit(e);
2911
- }
2912
- });
2913
- apcliGroup.addCommand(disableCmd);
2914
- }
2915
- function registerReloadCommand(apcliGroup, executor) {
2916
- const reloadCmd = new Command3("reload").description("Hot-reload a module from disk.").argument("<module-id>", "Module ID to reload").requiredOption("--reason <reason>", "Reason for reload (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2917
- const fmt = resolveFormat(opts.format);
2918
- try {
2919
- await requireApprovalForSystemCommand("system.control.reload_module", opts.yes);
2920
- const result = await callSystemModule(executor, "system.control.reload_module", {
2921
- module_id: moduleId,
2922
- reason: opts.reason
2923
- });
2924
- emitResult(result, fmt, () => {
2925
- const prev = result.previous_version ?? "?";
2926
- const newVer = result.new_version ?? "?";
2927
- const dur = result.reload_duration_ms ?? "?";
2928
- process.stdout.write(`Module '${moduleId}' reloaded.
2929
- `);
2930
- process.stdout.write(` Version: ${prev} -> ${newVer}
2931
- `);
2932
- process.stdout.write(` Duration: ${dur}ms
2933
- `);
2934
- });
2935
- } catch (e) {
2936
- emitErrorAndExit(e);
2937
- }
2938
- });
2939
- apcliGroup.addCommand(reloadCmd);
2940
- }
2941
- function registerConfigCommand(apcliGroup, executor) {
2942
- const configGroup = new Command3("config").description("Read or update runtime configuration.");
2943
- const configGetCmd = new Command3("get").description("Read a configuration value by dot-path key.").argument("<key>", "Configuration key (dot-path)").option("--format <format>", "Output format.", "table").action(async (key, opts) => {
2944
- const fmt = resolveFormat(opts.format);
2945
- try {
2946
- const result = await callSystemModule(executor, "system.config.get", { key });
2947
- const value = result?.value ?? result;
2948
- emitResult({ key, value }, fmt, () => {
2949
- process.stdout.write(`${key} = ${JSON.stringify(value)}
2950
- `);
2951
- });
2952
- } catch (e) {
2953
- emitErrorAndExit(e);
2954
- }
2955
- });
2956
- configGroup.addCommand(configGetCmd);
2957
- const configSetCmd = new Command3("set").description("Update a runtime configuration value (audit-logged; client-side approval gate applies).").argument("<key>", "Configuration key (dot-path)").argument("<value>", "New value").requiredOption("--reason <reason>", "Reason for config change (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (key, value, opts) => {
2958
- const fmt = resolveFormat(opts.format);
2959
- let parsedValue;
2960
- try {
2961
- parsedValue = JSON.parse(value);
2962
- } catch {
2963
- parsedValue = value;
2964
- }
2965
- try {
2966
- await requireApprovalForSystemCommand("system.control.update_config", opts.yes);
2967
- const result = await callSystemModule(executor, "system.control.update_config", {
2968
- key,
2969
- value: parsedValue,
2970
- reason: opts.reason
2971
- });
2972
- emitResult(result, fmt, () => {
2973
- const old = result.old_value ?? "?";
2974
- const newVal = result.new_value ?? "?";
2975
- process.stdout.write(`Config updated: ${key}
2976
- `);
2977
- process.stdout.write(` ${JSON.stringify(old)} -> ${JSON.stringify(newVal)}
2978
- `);
2979
- process.stdout.write(` Reason: ${opts.reason}
2980
- `);
2981
- });
2982
- } catch (e) {
2983
- emitErrorAndExit(e);
2984
- }
2985
- });
2986
- configGroup.addCommand(configSetCmd);
2987
- apcliGroup.addCommand(configGroup);
2988
2623
  }
2989
- var init_system_cmd = __esm({
2990
- "src/system-cmd.ts"() {
2991
- "use strict";
2992
- init_esm_shims();
2993
- init_approval();
2994
- init_errors();
2995
- init_output();
2996
- }
2997
- });
2998
-
2999
- // src/strategy.ts
3000
- import { Command as Command4, Option as Option3 } from "commander";
3001
- function lookupStrategyInfo(executor, strategyName) {
3002
- if (typeof executor.describePipeline === "function") {
3003
- try {
3004
- const current = executor.describePipeline();
3005
- if (current && current.name === strategyName) {
3006
- return { info: current, isCurrent: true };
3007
- }
3008
- } catch {
3009
- }
3010
- }
3011
- const ctor = executor.constructor;
3012
- if (ctor && typeof ctor.listStrategies === "function") {
3013
- try {
3014
- const all = ctor.listStrategies();
3015
- const info = all.find((s) => s.name === strategyName) ?? null;
3016
- return { info, isCurrent: false };
3017
- } catch {
3018
- return { info: null, isCurrent: false };
3019
- }
3020
- }
3021
- return { info: null, isCurrent: false };
3022
- }
3023
- function registerPipelineCommand(cli, executor) {
3024
- const pipelineCmd = new Command4("describe-pipeline").description("Show the execution pipeline steps for a strategy.").addOption(
3025
- new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
3026
- ).option("--format <format>", "Output format.").action((opts) => {
3027
- const fmt = resolveFormat(opts.format);
3028
- const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
3029
- if (info) {
3030
- const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
3031
- const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
3032
- if (fmt === "json" || !process.stdout.isTTY) {
3033
- const payload = {
3034
- strategy: info.name,
3035
- step_count: info.stepCount,
3036
- description: info.description,
3037
- steps: info.stepNames.map((name, i) => {
3038
- const stepMeta = strategySteps[i];
3039
- return {
3040
- index: i + 1,
3041
- name,
3042
- pure: stepMeta?.pure ?? false,
3043
- removable: stepMeta?.removable ?? true,
3044
- timeout_ms: stepMeta?.timeoutMs ?? null
3045
- };
3046
- })
3047
- };
3048
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3049
- } else {
3050
- process.stdout.write(`${header}
3051
-
3052
- `);
3053
- process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3054
- `);
3055
- process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3056
- `);
3057
- for (let i = 0; i < info.stepNames.length; i++) {
3058
- const stepMeta = strategySteps[i];
3059
- const pure = stepMeta?.pure ? "yes" : "no";
3060
- const removable = stepMeta?.removable !== false ? "yes" : "no";
3061
- const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
3062
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
3063
- `);
3064
- }
3065
- }
3066
- return;
3067
- }
3068
- const steps = PRESET_STEPS[opts.strategy] ?? [];
3069
- const pureSteps = /* @__PURE__ */ new Set([
3070
- "context_creation",
3071
- "call_chain_guard",
3072
- "module_lookup",
3073
- "acl_check",
3074
- "input_validation"
3075
- ]);
3076
- const nonRemovable = /* @__PURE__ */ new Set([
3077
- "context_creation",
3078
- "module_lookup",
3079
- "execute",
3080
- "return_result"
3081
- ]);
3082
- if (fmt === "json" || !process.stdout.isTTY) {
3083
- const payload = {
3084
- strategy: opts.strategy,
3085
- step_count: steps.length,
3086
- steps: steps.map((s, i) => ({
3087
- index: i + 1,
3088
- name: s,
3089
- pure: pureSteps.has(s),
3090
- removable: !nonRemovable.has(s)
3091
- }))
3092
- };
3093
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3094
- } else {
3095
- process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
3096
-
3097
- `);
3098
- process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3099
- `);
3100
- process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3101
- `);
3102
- for (let i = 0; i < steps.length; i++) {
3103
- const pure = pureSteps.has(steps[i]) ? "yes" : "no";
3104
- const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
3105
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
3106
- `);
3107
- }
3108
- }
3109
- });
3110
- cli.addCommand(pipelineCmd);
3111
- }
3112
- var PRESET_STEPS;
3113
- var init_strategy = __esm({
3114
- "src/strategy.ts"() {
3115
- "use strict";
3116
- init_esm_shims();
3117
- init_output();
3118
- PRESET_STEPS = {
3119
- standard: [
3120
- "context_creation",
3121
- "call_chain_guard",
3122
- "module_lookup",
3123
- "acl_check",
3124
- "approval_gate",
3125
- "middleware_before",
3126
- "input_validation",
3127
- "execute",
3128
- "output_validation",
3129
- "middleware_after",
3130
- "return_result"
3131
- ],
3132
- internal: [
3133
- "context_creation",
3134
- "call_chain_guard",
3135
- "module_lookup",
3136
- "middleware_before",
3137
- "input_validation",
3138
- "execute",
3139
- "output_validation",
3140
- "middleware_after",
3141
- "return_result"
3142
- ],
3143
- testing: [
3144
- "context_creation",
3145
- "module_lookup",
3146
- "middleware_before",
3147
- "input_validation",
3148
- "execute",
3149
- "output_validation",
3150
- "middleware_after",
3151
- "return_result"
3152
- ],
3153
- performance: [
3154
- "context_creation",
3155
- "call_chain_guard",
3156
- "module_lookup",
3157
- "acl_check",
3158
- "approval_gate",
3159
- "input_validation",
3160
- "execute",
3161
- "output_validation",
3162
- "return_result"
3163
- ],
3164
- minimal: [
3165
- "context_creation",
3166
- "module_lookup",
3167
- "execute",
3168
- "return_result"
3169
- ]
3170
- };
3171
- }
3172
- });
3173
-
3174
- // src/builtin-group.ts
3175
- function setReservedGroupNames(names) {
3176
- _effectiveReservedNames = names;
3177
- }
3178
- function _validateBuiltinGroupName(name) {
3179
- if (!name || !_NAME_REGEX.test(name)) {
3180
- throw new ApcliGroupError(
3181
- `builtinGroupName ${JSON.stringify(name)} must match /^[a-z][a-z0-9_-]*$/ (non-empty, lowercase, alphanumeric + '_' / '-', leading letter).`
3182
- );
3183
- }
3184
- }
3185
- var ApcliGroupError, DEFAULT_BUILTIN_GROUP_NAME, RESERVED_GROUP_NAMES, _effectiveReservedNames, _NAME_REGEX, VALID_USER_MODES, APCLI_SUBCOMMAND_NAMES, ApcliGroup;
3186
- var init_builtin_group = __esm({
3187
- "src/builtin-group.ts"() {
2624
+ var PBKDF2_ITERATIONS, V1_STATIC_SALT, keytarModule, ConfigEncryptor;
2625
+ var init_config_encryptor = __esm({
2626
+ "src/security/config-encryptor.ts"() {
3188
2627
  "use strict";
3189
2628
  init_esm_shims();
3190
2629
  init_errors();
3191
2630
  init_logger();
3192
- ApcliGroupError = class extends Error {
3193
- constructor(message) {
3194
- super(message);
3195
- this.name = "ApcliGroupError";
3196
- }
3197
- };
3198
- DEFAULT_BUILTIN_GROUP_NAME = "apcli";
3199
- RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set([DEFAULT_BUILTIN_GROUP_NAME]);
3200
- _effectiveReservedNames = RESERVED_GROUP_NAMES;
3201
- _NAME_REGEX = /^[a-z][a-z0-9_-]*$/;
3202
- VALID_USER_MODES = /* @__PURE__ */ new Set([
3203
- "all",
3204
- "none",
3205
- "include",
3206
- "exclude"
3207
- ]);
3208
- APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
3209
- "list",
3210
- "describe",
3211
- "exec",
3212
- "validate",
3213
- "init",
3214
- "health",
3215
- "usage",
3216
- "enable",
3217
- "disable",
3218
- "reload",
3219
- "config",
3220
- "completion",
3221
- "describe-pipeline"
3222
- ]);
3223
- ApcliGroup = class _ApcliGroup {
3224
- _mode;
3225
- _include;
3226
- _exclude;
3227
- _disableEnv;
3228
- _registryInjected;
3229
- _fromCliConfig;
3230
- _name;
3231
- constructor(init) {
3232
- this._mode = init.mode;
3233
- this._include = init.include;
3234
- this._exclude = init.exclude;
3235
- this._disableEnv = init.disableEnv;
3236
- this._registryInjected = init.registryInjected;
3237
- this._fromCliConfig = init.fromCliConfig;
3238
- this._name = init.name;
3239
- }
2631
+ PBKDF2_ITERATIONS = 6e5;
2632
+ V1_STATIC_SALT = Buffer.from("apcore-cli-config-v1");
2633
+ keytarModule = null;
2634
+ ConfigEncryptor = class _ConfigEncryptor {
2635
+ static SERVICE_NAME = "apcore-cli";
2636
+ // One-shot flag so the "obfuscation only" warning fires exactly once
2637
+ // per process instead of once per encrypt/decrypt call.
2638
+ static weakFallbackWarned = false;
3240
2639
  /**
3241
- * Resolved name for the built-in command group (default `"apcli"`).
3242
- * Overridable via createCli's `builtinGroupName` option for downstream
3243
- * branded CLIs that want a custom namespace. Cross-SDK parity with
3244
- * Python `ApcliGroup.name` (2026-05-08).
2640
+ * Encrypt and store a configuration value.
2641
+ *
2642
+ * Cross-SDK contract (D10-003, 2026-04-26): when the OS keyring is
2643
+ * detected as available but `setPassword` then throws (locked keyring,
2644
+ * transient backend failure, permission revoked, etc.), the error is
2645
+ * propagated wrapped in a `ConfigDecryptionError`. Previously TS
2646
+ * caught the exception and silently fell through to AES file encryption
2647
+ * — a quiet downgrade that surprised users who expected a hard failure.
2648
+ * Python lets the keyring exception propagate raw; Rust returns
2649
+ * `ConfigDecryptionError::KeyringError`. The fall-through to AES is
2650
+ * still reached when `getKeytar()` returns `null` (keyring
2651
+ * genuinely unavailable on this platform / install).
3245
2652
  */
3246
- get name() {
3247
- return this._name;
2653
+ async store(key, value) {
2654
+ const keytar = await getKeytar();
2655
+ if (keytar) {
2656
+ try {
2657
+ await keytar.setPassword(_ConfigEncryptor.SERVICE_NAME, key, value);
2658
+ return `keyring:${key}`;
2659
+ } catch (err) {
2660
+ const detail = err instanceof Error ? err.message : String(err);
2661
+ throw new ConfigDecryptionError(
2662
+ `Failed to store '${key}' in OS keyring: ${detail}. Unset APCORE_CLI_CONFIG_PASSPHRASE-aware backends or unlock the keyring before retrying.`
2663
+ );
2664
+ }
2665
+ }
2666
+ warn("OS keyring unavailable. Using file-based encryption.");
2667
+ const ciphertext = this.aesEncrypt(value);
2668
+ return `enc:v2:${ciphertext.toString("base64")}`;
3248
2669
  }
3249
2670
  /**
3250
- * Tier 1 constructor config came from `createCli({ apcli })`.
3251
- *
3252
- * A non-auto mode from this tier wins over env var and yaml.
2671
+ * Retrieve and decrypt a configuration value.
3253
2672
  */
3254
- static fromCliConfig(config, opts) {
3255
- return _ApcliGroup._build(
3256
- config,
3257
- opts,
3258
- /*fromCliConfig*/
3259
- true
3260
- );
2673
+ async retrieve(configValue, key) {
2674
+ if (configValue.startsWith("keyring:")) {
2675
+ const keytar = await getKeytar();
2676
+ if (!keytar) {
2677
+ throw new ConfigDecryptionError(
2678
+ `Keyring module not available to retrieve '${key}'.`
2679
+ );
2680
+ }
2681
+ try {
2682
+ const refKey = configValue.slice("keyring:".length);
2683
+ const result = await keytar.getPassword(
2684
+ _ConfigEncryptor.SERVICE_NAME,
2685
+ refKey
2686
+ );
2687
+ if (result === null || result === void 0) {
2688
+ throw new ConfigDecryptionError(
2689
+ `Keyring entry not found for '${refKey}'.`
2690
+ );
2691
+ }
2692
+ return result;
2693
+ } catch (err) {
2694
+ if (err instanceof ConfigDecryptionError) throw err;
2695
+ throw new ConfigDecryptionError(
2696
+ `Failed to retrieve from keyring: ${err}`
2697
+ );
2698
+ }
2699
+ }
2700
+ if (configValue.startsWith("enc:v2:")) {
2701
+ const data = Buffer.from(configValue.slice("enc:v2:".length), "base64");
2702
+ try {
2703
+ return this.aesDecrypt(data);
2704
+ } catch {
2705
+ throw new ConfigDecryptionError(
2706
+ `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2707
+ );
2708
+ }
2709
+ }
2710
+ if (configValue.startsWith("enc:")) {
2711
+ const data = Buffer.from(configValue.slice("enc:".length), "base64");
2712
+ try {
2713
+ return this.aesDecryptV1(data);
2714
+ } catch {
2715
+ throw new ConfigDecryptionError(
2716
+ `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2717
+ );
2718
+ }
2719
+ }
2720
+ return configValue;
3261
2721
  }
3262
- /**
3263
- * Tier 3 constructor — config came from `apcore.yaml`.
3264
- *
3265
- * Env var (Tier 2) may override the yaml-supplied mode.
3266
- */
3267
- static fromYaml(config, opts) {
3268
- if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3269
- const got = Array.isArray(config) ? "array" : typeof config;
2722
+ // Derive an AES-256 key with a provided salt (v2 format).
2723
+ //
2724
+ // Order of preference:
2725
+ // 1. APCORE_CLI_CONFIG_PASSPHRASE env var a real secret supplied by
2726
+ // the user; produces a key an attacker with filesystem read cannot
2727
+ // reconstruct without also knowing the passphrase.
2728
+ // 2. hostname + username obfuscation-only derivation for backward
2729
+ // compatibility. Emits a loud stderr warning on first use so
2730
+ // operators know the stored value is NOT protected against a
2731
+ // filesystem-read attacker.
2732
+ deriveKey(salt) {
2733
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2734
+ if (passphrase && passphrase.length > 0) {
2735
+ return crypto2.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256");
2736
+ }
2737
+ if (!_ConfigEncryptor.weakFallbackWarned) {
3270
2738
  warn(
3271
- `apcore.yaml apcli has unexpected type ${got}; using auto-detect.`
3272
- );
3273
- return _ApcliGroup._build(
3274
- void 0,
3275
- opts,
3276
- /*fromCliConfig*/
3277
- false
2739
+ "APCORE_CLI_CONFIG_PASSPHRASE is not set. The `enc:v2:` fallback uses a key derived from hostname+username (non-secret inputs) and is OBFUSCATION ONLY \u2014 an attacker with filesystem read access can reconstruct the key. Set APCORE_CLI_CONFIG_PASSPHRASE or ensure the OS keyring is available for real encryption."
3278
2740
  );
2741
+ _ConfigEncryptor.weakFallbackWarned = true;
3279
2742
  }
3280
- return _ApcliGroup._build(
3281
- config,
3282
- opts,
3283
- /*fromCliConfig*/
3284
- false
3285
- );
2743
+ const hostname2 = os3.hostname();
2744
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2745
+ const material = `${hostname2}:${username}`;
2746
+ return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
2747
+ }
2748
+ aesEncrypt(plaintext) {
2749
+ const salt = crypto2.randomBytes(16);
2750
+ const key = this.deriveKey(salt);
2751
+ const nonce = crypto2.randomBytes(12);
2752
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
2753
+ const ct = Buffer.concat([
2754
+ cipher.update(plaintext, "utf-8"),
2755
+ cipher.final()
2756
+ ]);
2757
+ const tag = cipher.getAuthTag();
2758
+ return Buffer.concat([salt, nonce, tag, ct]);
2759
+ }
2760
+ aesDecrypt(data) {
2761
+ const salt = data.subarray(0, 16);
2762
+ const nonce = data.subarray(16, 28);
2763
+ const tag = data.subarray(28, 44);
2764
+ const ct = data.subarray(44);
2765
+ const key = this.deriveKey(salt);
2766
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2767
+ decipher.setAuthTag(tag);
2768
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2769
+ }
2770
+ /** Decrypt legacy v1-format values: nonce(12)+tag(16)+ct, static salt. */
2771
+ aesDecryptV1(data) {
2772
+ const nonce = data.subarray(0, 12);
2773
+ const tag = data.subarray(12, 28);
2774
+ const ct = data.subarray(28);
2775
+ const hostname2 = os3.hostname();
2776
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2777
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2778
+ const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
2779
+ for (const material of materials) {
2780
+ for (const iterations of [6e5, 1e5]) {
2781
+ try {
2782
+ const key = crypto2.pbkdf2Sync(material, V1_STATIC_SALT, iterations, 32, "sha256");
2783
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2784
+ decipher.setAuthTag(tag);
2785
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2786
+ } catch {
2787
+ }
2788
+ }
2789
+ }
2790
+ throw new Error("v1 decryption failed with all material+iteration combinations");
2791
+ }
2792
+ };
2793
+ }
2794
+ });
2795
+
2796
+ // src/security/auth.ts
2797
+ var AuthProvider;
2798
+ var init_auth = __esm({
2799
+ "src/security/auth.ts"() {
2800
+ "use strict";
2801
+ init_esm_shims();
2802
+ init_errors();
2803
+ init_config_encryptor();
2804
+ AuthProvider = class {
2805
+ config;
2806
+ _encryptor;
2807
+ constructor(config, encryptor) {
2808
+ this.config = config;
2809
+ this._encryptor = encryptor;
3286
2810
  }
3287
2811
  /**
3288
- * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
3289
- * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
3290
- * Use this in programmatic contexts where throwing/exiting is unwanted.
2812
+ * Resolve the active ConfigEncryptor instance.
2813
+ *
2814
+ * D11-005 (2026-05-12): three-tier fallback chain matching Python's
2815
+ * `_get_encryptor` (auth.py:33): explicit constructor arg > peer attribute
2816
+ * `config.encryptor` (set by embedders injecting forced-AES test fixtures
2817
+ * or shared instances) > fresh `new ConfigEncryptor()`. Previously TS
2818
+ * skipped the peer-attribute tier, silently giving embedders a different
2819
+ * encryptor than the one they wired on the config.
3291
2820
  */
3292
- static tryFromYaml(config, opts) {
3293
- if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3294
- const got = Array.isArray(config) ? "array" : typeof config;
3295
- return [
3296
- null,
3297
- `apcore.yaml 'apcli:' must be a bool, object, or null; got ${got}`
3298
- ];
2821
+ getEncryptor() {
2822
+ if (this._encryptor) return this._encryptor;
2823
+ const fromConfig = this.config.encryptor;
2824
+ if (fromConfig) return fromConfig;
2825
+ return new ConfigEncryptor();
2826
+ }
2827
+ /**
2828
+ * Retrieve the API key from the configured sources.
2829
+ * Handles keyring: and enc: prefixes via ConfigEncryptor.
2830
+ */
2831
+ async getApiKey() {
2832
+ const result = this.config.resolve(
2833
+ "auth.api_key",
2834
+ "--api-key",
2835
+ "APCORE_AUTH_API_KEY"
2836
+ );
2837
+ if (result === null || result === void 0) {
2838
+ return null;
3299
2839
  }
3300
- if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
3301
- const mode = config["mode"];
3302
- if (mode !== void 0 && mode !== null) {
3303
- const validModes = ["all", "none", "include", "exclude"];
3304
- if (typeof mode !== "string" || !validModes.includes(mode)) {
3305
- return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
2840
+ const strResult = String(result);
2841
+ if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
2842
+ try {
2843
+ return await this.getEncryptor().retrieve(strResult, "auth.api_key");
2844
+ } catch (err) {
2845
+ if (err instanceof ConfigDecryptionError) {
2846
+ throw new AuthenticationError(
2847
+ "Failed to decrypt stored API key. Re-store with 'apcli config set auth.api_key'."
2848
+ );
3306
2849
  }
2850
+ throw err;
3307
2851
  }
3308
2852
  }
3309
- return [_ApcliGroup.fromYaml(config, opts), null];
2853
+ return strResult;
3310
2854
  }
3311
- // -------------------------------------------------------------------------
3312
- // Internal builder shared by both factories
3313
- // -------------------------------------------------------------------------
3314
- static _build(config, opts, fromCliConfig) {
3315
- const name = opts.name ?? DEFAULT_BUILTIN_GROUP_NAME;
3316
- _validateBuiltinGroupName(name);
3317
- if (config === true) {
3318
- return new _ApcliGroup({
3319
- mode: "all",
3320
- include: [],
3321
- exclude: [],
3322
- disableEnv: false,
3323
- registryInjected: opts.registryInjected,
3324
- fromCliConfig,
3325
- name
3326
- });
2855
+ /**
2856
+ * Add authentication headers to an outgoing request.
2857
+ *
2858
+ * Cross-SDK contract (D10-002, 2026-04-26): the input `headers` object
2859
+ * is mutated **in place** and the same reference is returned. Callers
2860
+ * that share the headers reference (the documented pattern in
2861
+ * apcore-cli/docs/features/security.md §AuthProvider) can read
2862
+ * `headers.Authorization` after the call without re-binding the
2863
+ * return value. Python and Rust both mutate-and-return; TS previously
2864
+ * spread into a new object, which silently broke shared-reference
2865
+ * callers.
2866
+ */
2867
+ async authenticateRequest(headers) {
2868
+ const key = await this.getApiKey();
2869
+ if (!key) {
2870
+ throw new AuthenticationError(
2871
+ "Remote registry requires authentication. Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
2872
+ );
3327
2873
  }
3328
- if (config === false) {
3329
- return new _ApcliGroup({
3330
- mode: "none",
3331
- include: [],
3332
- exclude: [],
3333
- disableEnv: false,
3334
- registryInjected: opts.registryInjected,
3335
- fromCliConfig,
3336
- name
3337
- });
3338
- }
3339
- if (config === void 0 || config === null) {
3340
- return new _ApcliGroup({
3341
- mode: "auto",
3342
- include: [],
3343
- exclude: [],
3344
- disableEnv: false,
3345
- registryInjected: opts.registryInjected,
3346
- fromCliConfig,
3347
- name
3348
- });
3349
- }
3350
- if (typeof config !== "object" || Array.isArray(config)) {
3351
- process.stderr.write(
3352
- `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
3353
- `
3354
- );
3355
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3356
- }
3357
- const cfg = config;
3358
- let mode;
3359
- if (cfg.mode === void 0 || cfg.mode === null) {
3360
- mode = "auto";
3361
- } else if (typeof cfg.mode !== "string") {
3362
- process.stderr.write(
3363
- `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
3364
- `
3365
- );
3366
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3367
- } else if (!VALID_USER_MODES.has(cfg.mode)) {
3368
- process.stderr.write(
3369
- `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
3370
- `
2874
+ if (/[\r\n]/.test(key)) {
2875
+ throw new AuthenticationError(
2876
+ "Malformed API key: contains invalid characters (CR/LF). Re-store with 'apcli config set auth.api_key'."
3371
2877
  );
3372
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3373
- } else {
3374
- mode = cfg.mode;
3375
- }
3376
- const include = _ApcliGroup._normalizeList(cfg.include, "include");
3377
- const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
3378
- const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
3379
- let disableEnv = false;
3380
- if (rawDisableEnv !== void 0) {
3381
- if (typeof rawDisableEnv === "boolean") {
3382
- disableEnv = rawDisableEnv;
3383
- } else {
3384
- warn(
3385
- `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
3386
- );
3387
- }
3388
2878
  }
3389
- return new _ApcliGroup({
3390
- mode,
3391
- include,
3392
- exclude,
3393
- disableEnv,
3394
- registryInjected: opts.registryInjected,
3395
- fromCliConfig,
3396
- name
3397
- });
2879
+ headers.Authorization = `Bearer ${key.trim()}`;
2880
+ return headers;
3398
2881
  }
3399
2882
  /**
3400
- * Normalize an include/exclude list. Non-array warn and return [].
3401
- *
3402
- * Unknown but well-formed entries emit a WARNING (spec §7 error table,
3403
- * T-APCLI-25) but are retained in the returned list for forward-compat —
3404
- * if apcore-cli later adds a subcommand named `foo`, existing configs
3405
- * continue to work without a config change. At runtime, unknown names
3406
- * simply never match any registered subcommand.
2883
+ * Handle an HTTP response status code for auth-related errors.
3407
2884
  */
3408
- static _normalizeList(raw, label) {
3409
- if (raw === void 0 || raw === null) return [];
3410
- if (!Array.isArray(raw)) {
3411
- warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
3412
- return [];
3413
- }
3414
- const out = [];
3415
- for (const entry of raw) {
3416
- if (typeof entry === "string" && entry.length > 0) {
3417
- if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
3418
- warn(
3419
- `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
3420
- );
3421
- }
3422
- out.push(entry);
3423
- } else {
3424
- warn(`apcli.${label} contains non-string entry; skipping.`);
3425
- }
2885
+ handleResponse(statusCode) {
2886
+ if (statusCode === 401 || statusCode === 403) {
2887
+ throw new AuthenticationError(
2888
+ "Authentication failed. Verify your API key."
2889
+ );
3426
2890
  }
3427
- return out;
3428
2891
  }
3429
- // -------------------------------------------------------------------------
3430
- // Public API
3431
- // -------------------------------------------------------------------------
3432
- /**
3433
- * Resolve effective visibility mode after applying tier precedence.
3434
- *
3435
- * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
3436
- *
3437
- * Tier order (spec §4.4):
3438
- * 1. CliConfig non-auto wins outright.
3439
- * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
3440
- * 3. yaml non-auto.
3441
- * 4. Auto-detect from registryInjected.
3442
- */
3443
- resolveVisibility() {
3444
- if (this._fromCliConfig && this._mode !== "auto") {
3445
- return this._mode;
3446
- }
3447
- if (!this._disableEnv) {
3448
- const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
3449
- if (envMode !== null) {
3450
- return envMode;
3451
- }
3452
- }
3453
- if (this._mode !== "auto") {
3454
- return this._mode;
3455
- }
3456
- return this._registryInjected ? "none" : "all";
2892
+ };
2893
+ }
2894
+ });
2895
+
2896
+ // src/security/index.ts
2897
+ var security_exports = {};
2898
+ __export(security_exports, {
2899
+ AuditLogger: () => AuditLogger,
2900
+ AuthProvider: () => AuthProvider,
2901
+ ConfigEncryptor: () => ConfigEncryptor,
2902
+ Sandbox: () => Sandbox,
2903
+ getAuditLogger: () => getAuditLogger,
2904
+ setAuditLogger: () => setAuditLogger
2905
+ });
2906
+ var init_security = __esm({
2907
+ "src/security/index.ts"() {
2908
+ "use strict";
2909
+ init_esm_shims();
2910
+ init_audit();
2911
+ init_auth();
2912
+ init_config_encryptor();
2913
+ init_sandbox();
2914
+ }
2915
+ });
2916
+
2917
+ // src/discovery.ts
2918
+ import { Command as Command3, Option as Option2 } from "commander";
2919
+ function validateTag(tag) {
2920
+ if (!TAG_PATTERN.test(tag)) {
2921
+ process.stderr.write(
2922
+ `Error: Invalid tag format: '${tag}'. Tags must match [a-z][a-z0-9_-]*.
2923
+ `
2924
+ );
2925
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2926
+ }
2927
+ }
2928
+ function collectTag(value, previous) {
2929
+ return previous.concat([value]);
2930
+ }
2931
+ function collectAnnotation(value, previous) {
2932
+ return previous.concat([value]);
2933
+ }
2934
+ function getAnnotationFlag(moduleDef, flag) {
2935
+ const annotations = moduleDef.annotations;
2936
+ if (!annotations || typeof annotations !== "object") return false;
2937
+ const ann = annotations;
2938
+ const map = {
2939
+ "destructive": "destructive",
2940
+ "requires-approval": "requires_approval",
2941
+ "readonly": "readonly",
2942
+ "streaming": "streaming",
2943
+ "cacheable": "cacheable",
2944
+ "idempotent": "idempotent",
2945
+ "paginated": "paginated"
2946
+ };
2947
+ const attr = map[flag] ?? flag;
2948
+ return ann[attr] === true;
2949
+ }
2950
+ function registerListCommand(apcliGroup, registry, exposureFilter) {
2951
+ const listCmd = new Command3("list").description("List available modules in the registry.").option("--tag <tag>", "Filter modules by tag (AND logic). Repeatable.", collectTag, []).option("--flat", "Show flat list (no grouping).", false).addOption(
2952
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2953
+ ).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2954
+ new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2955
+ ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2956
+ new Option2("--sort <field>", "Sort order.").choices(["id", "calls", "errors", "latency"]).default("id")
2957
+ ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2958
+ new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2959
+ ).action((opts) => {
2960
+ for (const t of opts.tag) {
2961
+ validateTag(t);
2962
+ }
2963
+ let modules = [];
2964
+ for (const m of listAllDefinitions(registry)) {
2965
+ modules.push(m);
2966
+ }
2967
+ if (opts.tag.length > 0) {
2968
+ const filterTags = new Set(opts.tag);
2969
+ modules = modules.filter((m) => {
2970
+ const mTags = m.tags ?? [];
2971
+ return [...filterTags].every((t) => mTags.includes(t));
2972
+ });
2973
+ }
2974
+ if (opts.search) {
2975
+ const query = opts.search.toLowerCase();
2976
+ modules = modules.filter(
2977
+ (m) => (m.moduleId ?? "").toLowerCase().includes(query) || (m.description ?? "").toLowerCase().includes(query)
2978
+ );
2979
+ }
2980
+ if (opts.status === "enabled") {
2981
+ modules = modules.filter((m) => {
2982
+ const enabled = m.enabled;
2983
+ return enabled !== false;
2984
+ });
2985
+ } else if (opts.status === "disabled") {
2986
+ modules = modules.filter((m) => {
2987
+ const enabled = m.enabled;
2988
+ return enabled === false;
2989
+ });
2990
+ }
2991
+ if (!opts.deprecated) {
2992
+ modules = modules.filter((m) => {
2993
+ const deprecated = m.deprecated;
2994
+ return deprecated !== true;
2995
+ });
2996
+ }
2997
+ if (opts.annotation.length > 0) {
2998
+ for (const annFlag of opts.annotation) {
2999
+ modules = modules.filter((m) => getAnnotationFlag(m, annFlag));
3457
3000
  }
3458
- /**
3459
- * True iff `subcommand` passes the include/exclude filter.
3460
- *
3461
- * Callers MUST first check {@link resolveVisibility} — this method throws
3462
- * under modes `"all"` or `"none"` (caller bug per spec §4.6).
3463
- */
3464
- isSubcommandIncluded(subcommand) {
3465
- const mode = this.resolveVisibility();
3466
- if (mode === "include") return this._include.includes(subcommand);
3467
- if (mode === "exclude") return !this._exclude.includes(subcommand);
3468
- throw new Error(
3469
- `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
3001
+ }
3002
+ if (opts.sort === "calls" || opts.sort === "errors" || opts.sort === "latency") {
3003
+ const { used } = sortModulesByUsage(modules, opts.sort, { reverse: !opts.reverse });
3004
+ if (!used) {
3005
+ process.stderr.write(
3006
+ `note: no usage data available for --sort ${opts.sort}; sorted by id. Run some modules first to populate ~/.apcore-cli/audit.jsonl.
3007
+ `
3470
3008
  );
3471
3009
  }
3472
- /** True iff the `apcli` group itself should appear in root `--help`. */
3473
- isGroupVisible() {
3474
- return this.resolveVisibility() !== "none";
3010
+ } else {
3011
+ modules.sort((a, b) => (a.moduleId ?? "").localeCompare(b.moduleId ?? ""));
3012
+ if (opts.reverse) {
3013
+ modules.reverse();
3475
3014
  }
3476
- // -------------------------------------------------------------------------
3477
- // Env parser (Tier 2) — co-located per spec §4.4
3478
- // -------------------------------------------------------------------------
3479
- /**
3480
- * Parse APCORE_CLI_APCLI. Case-insensitive.
3481
- *
3482
- * - `show` / `1` / `true` → `"all"`
3483
- * - `hide` / `0` / `false` → `"none"`
3484
- * - Empty / unset → `null`
3485
- * - Anything else warn and return `null`
3486
- */
3487
- _parseEnv(raw) {
3488
- if (raw === void 0 || raw === "") return null;
3489
- const normalized = raw.trim().toLowerCase();
3490
- if (normalized === "") return null;
3491
- if (normalized === "show" || normalized === "1" || normalized === "true") {
3492
- return "all";
3015
+ }
3016
+ let showExposureCol = false;
3017
+ if (exposureFilter && opts.exposure !== "all") {
3018
+ if (opts.exposure === "exposed") {
3019
+ modules = modules.filter((m) => exposureFilter.isExposed(m.moduleId ?? ""));
3020
+ } else if (opts.exposure === "hidden") {
3021
+ modules = modules.filter((m) => !exposureFilter.isExposed(m.moduleId ?? ""));
3022
+ }
3023
+ }
3024
+ if (opts.exposure === "all" && exposureFilter) {
3025
+ showExposureCol = true;
3026
+ }
3027
+ const fmt = resolveFormat(opts.format);
3028
+ const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
3029
+ void formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
3030
+ });
3031
+ apcliGroup.addCommand(listCmd);
3032
+ }
3033
+ function registerDescribeCommand(apcliGroup, registry) {
3034
+ const describeCmd = new Command3("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").addOption(
3035
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
3036
+ ).action((moduleId, opts) => {
3037
+ validateModuleId(moduleId);
3038
+ const moduleDef = registry.getDefinition(moduleId);
3039
+ if (!moduleDef) {
3040
+ process.stderr.write(
3041
+ `Error: Module '${moduleId}' not found.
3042
+ `
3043
+ );
3044
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3045
+ }
3046
+ const fmt = resolveFormat(opts.format);
3047
+ void formatModuleDetail(moduleDef, fmt);
3048
+ });
3049
+ apcliGroup.addCommand(describeCmd);
3050
+ }
3051
+ function registerExecCommand(apcliGroup, registry, executor) {
3052
+ const execCmd = new Command3("exec").description("Execute a module by ID with JSON input.").argument("<module-id>", "Module ID to execute").option("--format <format>", "Output format (json, table, csv, yaml, jsonl).").option("--fields <fields>", "Comma-separated dot-paths to select from the result.").option(
3053
+ "--input <json>",
3054
+ "JSON object passed as input to the module. Use '-' to read JSON from stdin."
3055
+ ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
3056
+ "--approval-timeout <seconds>",
3057
+ "Seconds to wait for interactive approval.",
3058
+ parseInt
3059
+ ).option("--sandbox", "Run module in an isolated subprocess with restricted env.", false).option("--strategy <name>", "Execution strategy (standard, parallel, sequential, etc.).").option("--trace", "Enable pipeline trace output.", false).option("--dry-run", "Validate inputs without executing the module.", false).option("--stream", "Stream output as JSONL instead of buffering.", false).action(async (moduleId, opts) => {
3060
+ validateModuleId(moduleId);
3061
+ const moduleDef = registry.getDefinition(moduleId);
3062
+ if (!moduleDef) {
3063
+ process.stderr.write(`Error: Module '${moduleId}' not found.
3064
+ `);
3065
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3066
+ }
3067
+ let merged = {};
3068
+ if (opts.input === "-") {
3069
+ merged = await collectInput("-", {}, false);
3070
+ } else if (opts.input !== void 0) {
3071
+ try {
3072
+ const parsed = JSON.parse(opts.input);
3073
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3074
+ process.stderr.write("Error: --input JSON must be an object.\n");
3075
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3493
3076
  }
3494
- if (normalized === "hide" || normalized === "0" || normalized === "false") {
3495
- return "none";
3077
+ merged = parsed;
3078
+ } catch (err) {
3079
+ const msg = err instanceof Error ? err.message : String(err);
3080
+ process.stderr.write(`Error: --input is not valid JSON: ${msg}
3081
+ `);
3082
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3083
+ }
3084
+ }
3085
+ const startTime = performance.now();
3086
+ try {
3087
+ await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
3088
+ if (opts.dryRun) {
3089
+ if (executor.validate) {
3090
+ const preflight = await executor.validate(moduleId, merged);
3091
+ formatPreflightResult(preflight, opts.format);
3092
+ } else {
3093
+ process.stdout.write(JSON.stringify({ valid: true }) + "\n");
3094
+ }
3095
+ return;
3096
+ }
3097
+ let result;
3098
+ if ((opts.trace || opts.strategy) && executor.callWithTrace) {
3099
+ const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
3100
+ result = res;
3101
+ } else {
3102
+ const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
3103
+ const sandbox = new Sandbox2(opts.sandbox);
3104
+ result = await sandbox.execute(moduleId, merged, executor);
3105
+ }
3106
+ const durationMs = Math.round(performance.now() - startTime);
3107
+ const fmt = resolveFormat(opts.format);
3108
+ formatExecResult(result, fmt, opts.fields);
3109
+ const auditLogger = getAuditLogger();
3110
+ if (auditLogger) {
3111
+ auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
3112
+ }
3113
+ } catch (err) {
3114
+ const exitCode = exitCodeForError(err);
3115
+ const durationMs = Math.round(performance.now() - startTime);
3116
+ try {
3117
+ const auditLogger = getAuditLogger();
3118
+ if (auditLogger) {
3119
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
3120
+ }
3121
+ } catch {
3122
+ }
3123
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
3124
+ `);
3125
+ process.exit(exitCode);
3126
+ }
3127
+ });
3128
+ apcliGroup.addCommand(execCmd);
3129
+ }
3130
+ function registerValidateCommand(cli, registry, executor) {
3131
+ const validateCmd = new Command3("validate").description("Run preflight checks without executing a module.").argument("<module-id>", "Module ID to validate").option("--input <source>", "JSON input file or '-' for stdin.").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3132
+ validateModuleId(moduleId);
3133
+ const moduleDef = registry.getDefinition(moduleId);
3134
+ if (!moduleDef) {
3135
+ process.stderr.write(`Error: Module '${moduleId}' not found.
3136
+ `);
3137
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3138
+ }
3139
+ const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
3140
+ if (!executor.validate) {
3141
+ process.stderr.write("Error: Executor does not support validate.\n");
3142
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
3143
+ }
3144
+ try {
3145
+ const preflight = await executor.validate(moduleId, merged);
3146
+ formatPreflightResult(preflight, opts.format);
3147
+ process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
3148
+ } catch (err) {
3149
+ const exitCode = exitCodeForError(err);
3150
+ try {
3151
+ const auditLogger = getAuditLogger();
3152
+ if (auditLogger) {
3153
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
3154
+ }
3155
+ } catch {
3156
+ }
3157
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
3158
+ `);
3159
+ process.exit(exitCode);
3160
+ }
3161
+ });
3162
+ cli.addCommand(validateCmd);
3163
+ }
3164
+ var TAG_PATTERN;
3165
+ var init_discovery = __esm({
3166
+ "src/discovery.ts"() {
3167
+ "use strict";
3168
+ init_esm_shims();
3169
+ init_approval();
3170
+ init_cli();
3171
+ init_errors();
3172
+ init_main();
3173
+ init_output();
3174
+ init_audit();
3175
+ init_system_usage();
3176
+ TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
3177
+ }
3178
+ });
3179
+
3180
+ // src/system-cmd.ts
3181
+ import { Command as Command4 } from "commander";
3182
+ import { Config } from "apcore-js";
3183
+ async function callSystemModule(executor, moduleId, inputs) {
3184
+ return executor.call(moduleId, inputs);
3185
+ }
3186
+ function emitResult(jsonPayload, fmt, ttyRender) {
3187
+ if (fmt === "json" || !process.stdout.isTTY) {
3188
+ process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
3189
+ } else {
3190
+ ttyRender();
3191
+ }
3192
+ }
3193
+ function emitErrorAndExit(e) {
3194
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
3195
+ `);
3196
+ process.exit(exitCodeForError(e));
3197
+ }
3198
+ async function requireApprovalForSystemCommand(moduleId, autoApprove) {
3199
+ const syntheticModuleDef = {
3200
+ moduleId,
3201
+ name: moduleId,
3202
+ description: `system command: ${moduleId}`,
3203
+ annotations: { requires_approval: true }
3204
+ };
3205
+ await checkApproval(syntheticModuleDef, autoApprove, void 0);
3206
+ }
3207
+ function formatHealthSummaryTty(result) {
3208
+ const summary = result.summary ?? {};
3209
+ const modules = result.modules ?? [];
3210
+ if (modules.length === 0) {
3211
+ process.stdout.write("No modules found.\n");
3212
+ return;
3213
+ }
3214
+ const total = summary.total_modules ?? modules.length;
3215
+ process.stdout.write(`Health Overview (${total} modules)
3216
+
3217
+ `);
3218
+ process.stdout.write(` ${"Module".padEnd(28)} ${"Status".padEnd(12)} ${"Error Rate".padEnd(12)} Top Error
3219
+ `);
3220
+ process.stdout.write(` ${"-".repeat(28)} ${"-".repeat(12)} ${"-".repeat(12)} ${"-".repeat(20)}
3221
+ `);
3222
+ for (const m of modules) {
3223
+ const top = m.top_error;
3224
+ const topStr = top ? `${top.code} (${top.count ?? "?"})` : "\u2014";
3225
+ const rate = `${((m.error_rate ?? 0) * 100).toFixed(1)}%`;
3226
+ process.stdout.write(
3227
+ ` ${String(m.module_id).padEnd(28)} ${String(m.status).padEnd(12)} ${rate.padEnd(12)} ${topStr}
3228
+ `
3229
+ );
3230
+ }
3231
+ const parts = [];
3232
+ for (const key of ["healthy", "degraded", "error"]) {
3233
+ const count = summary[key];
3234
+ if (count) parts.push(`${count} ${key}`);
3235
+ }
3236
+ process.stdout.write(`
3237
+ Summary: ${parts.join(", ") || "no data"}
3238
+ `);
3239
+ }
3240
+ function formatHealthModuleTty(result) {
3241
+ process.stdout.write(`Module: ${result.module_id ?? "?"}
3242
+ `);
3243
+ process.stdout.write(`Status: ${result.status ?? "unknown"}
3244
+ `);
3245
+ const total = result.total_calls ?? 0;
3246
+ const errors = result.error_count ?? 0;
3247
+ const rate = result.error_rate ?? 0;
3248
+ const avg = result.avg_latency_ms ?? 0;
3249
+ const p99 = result.p99_latency_ms ?? 0;
3250
+ process.stdout.write(`Calls: ${total.toLocaleString()} total | ${errors.toLocaleString()} errors | ${(rate * 100).toFixed(1)}% error rate
3251
+ `);
3252
+ process.stdout.write(`Latency: ${avg.toFixed(0)}ms avg | ${p99.toFixed(0)}ms p99
3253
+ `);
3254
+ const recent = result.recent_errors ?? [];
3255
+ if (recent.length > 0) {
3256
+ process.stdout.write(`
3257
+ Recent Errors (top ${recent.length}):
3258
+ `);
3259
+ for (const e of recent) {
3260
+ const count = e.count ?? "?";
3261
+ const last = e.last_occurred ?? "?";
3262
+ process.stdout.write(` ${String(e.code ?? "?").padEnd(24)} x${count} (last: ${last})
3263
+ `);
3264
+ }
3265
+ }
3266
+ }
3267
+ function formatUsageSummaryTty(result) {
3268
+ const modules = result.modules ?? [];
3269
+ const period = result.period ?? "?";
3270
+ if (modules.length === 0) {
3271
+ process.stdout.write(`No usage data for period ${period}.
3272
+ `);
3273
+ return;
3274
+ }
3275
+ process.stdout.write(`Usage Summary (last ${period})
3276
+
3277
+ `);
3278
+ process.stdout.write(` ${"Module".padEnd(24)} ${"Calls".padStart(8)} ${"Errors".padStart(8)} ${"Avg Latency".padStart(12)} ${"Trend".padStart(10)}
3279
+ `);
3280
+ process.stdout.write(` ${"-".repeat(24)} ${"-".repeat(8)} ${"-".repeat(8)} ${"-".repeat(12)} ${"-".repeat(10)}
3281
+ `);
3282
+ for (const m of modules) {
3283
+ const avg = `${(m.avg_latency_ms ?? 0).toFixed(0)}ms`;
3284
+ process.stdout.write(
3285
+ ` ${String(m.module_id).padEnd(24)} ${String(m.call_count ?? 0).padStart(8)} ${String(m.error_count ?? 0).padStart(8)} ${avg.padStart(12)} ${String(m.trend ?? "").padStart(10)}
3286
+ `
3287
+ );
3288
+ }
3289
+ const totalCalls = result.total_calls ?? modules.reduce((s, m) => s + (m.call_count ?? 0), 0);
3290
+ const totalErrors = result.total_errors ?? modules.reduce((s, m) => s + (m.error_count ?? 0), 0);
3291
+ process.stdout.write(`
3292
+ Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
3293
+ `);
3294
+ }
3295
+ function registerHealthCommand(apcliGroup, executor) {
3296
+ const healthCmd = new Command4("health").description("Show module health status. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed health").option("--threshold <number>", "Error rate threshold (default: 0.01).", parseFloat, 0.01).option("--all", "Include healthy modules.", false).option("--errors <count>", "Max recent errors (module detail only).", parseInt, 10).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3297
+ const fmt = resolveFormat(opts.format);
3298
+ try {
3299
+ if (moduleId) {
3300
+ const result = await callSystemModule(executor, "system.health.module", {
3301
+ module_id: moduleId,
3302
+ error_limit: opts.errors
3303
+ });
3304
+ emitResult(result, fmt, () => formatHealthModuleTty(result));
3305
+ } else {
3306
+ const result = await callSystemModule(executor, "system.health.summary", {
3307
+ error_rate_threshold: opts.threshold,
3308
+ include_healthy: opts.all
3309
+ });
3310
+ emitResult(result, fmt, () => formatHealthSummaryTty(result));
3311
+ }
3312
+ } catch (e) {
3313
+ emitErrorAndExit(e);
3314
+ }
3315
+ });
3316
+ apcliGroup.addCommand(healthCmd);
3317
+ }
3318
+ function registerUsageCommand(apcliGroup, executor) {
3319
+ const usageCmd = new Command4("usage").description("Show module usage statistics. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed usage").option("--period <period>", "Time window: 1h, 24h, 7d, 30d.", "24h").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3320
+ const fmt = resolveFormat(opts.format);
3321
+ try {
3322
+ let result;
3323
+ if (moduleId) {
3324
+ result = await callSystemModule(executor, "system.usage.module", {
3325
+ module_id: moduleId,
3326
+ period: opts.period
3327
+ });
3328
+ } else {
3329
+ result = await callSystemModule(executor, "system.usage.summary", {
3330
+ period: opts.period
3331
+ });
3332
+ }
3333
+ emitResult(result, fmt, () => {
3334
+ if (moduleId) {
3335
+ formatExecResult(result, fmt);
3336
+ } else {
3337
+ formatUsageSummaryTty(result);
3338
+ }
3339
+ });
3340
+ } catch (e) {
3341
+ emitErrorAndExit(e);
3342
+ }
3343
+ });
3344
+ apcliGroup.addCommand(usageCmd);
3345
+ }
3346
+ function registerEnableCommand(apcliGroup, executor) {
3347
+ const enableCmd = new Command4("enable").description("Enable a disabled module at runtime.").argument("<module-id>", "Module ID to enable").requiredOption("--reason <reason>", "Reason for enabling (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3348
+ const fmt = resolveFormat(opts.format);
3349
+ try {
3350
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
3351
+ const result = await callSystemModule(executor, "system.control.toggle_feature", {
3352
+ module_id: moduleId,
3353
+ enabled: true,
3354
+ reason: opts.reason
3355
+ });
3356
+ emitResult(result, fmt, () => {
3357
+ process.stdout.write(`Module '${moduleId}' enabled.
3358
+ Reason: ${opts.reason}
3359
+ `);
3360
+ });
3361
+ } catch (e) {
3362
+ emitErrorAndExit(e);
3363
+ }
3364
+ });
3365
+ apcliGroup.addCommand(enableCmd);
3366
+ }
3367
+ function registerDisableCommand(apcliGroup, executor) {
3368
+ const disableCmd = new Command4("disable").description("Disable a module at runtime (calls are rejected until re-enabled).").argument("<module-id>", "Module ID to disable").requiredOption("--reason <reason>", "Reason for disabling (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3369
+ const fmt = resolveFormat(opts.format);
3370
+ try {
3371
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
3372
+ const result = await callSystemModule(executor, "system.control.toggle_feature", {
3373
+ module_id: moduleId,
3374
+ enabled: false,
3375
+ reason: opts.reason
3376
+ });
3377
+ emitResult(result, fmt, () => {
3378
+ process.stdout.write(`Module '${moduleId}' disabled.
3379
+ Reason: ${opts.reason}
3380
+ `);
3381
+ });
3382
+ } catch (e) {
3383
+ emitErrorAndExit(e);
3384
+ }
3385
+ });
3386
+ apcliGroup.addCommand(disableCmd);
3387
+ }
3388
+ function registerReloadCommand(apcliGroup, executor) {
3389
+ const reloadCmd = new Command4("reload").description("Hot-reload a module from disk.").argument("<module-id>", "Module ID to reload").requiredOption("--reason <reason>", "Reason for reload (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
3390
+ const fmt = resolveFormat(opts.format);
3391
+ try {
3392
+ await requireApprovalForSystemCommand("system.control.reload_module", opts.yes);
3393
+ const result = await callSystemModule(executor, "system.control.reload_module", {
3394
+ module_id: moduleId,
3395
+ reason: opts.reason
3396
+ });
3397
+ emitResult(result, fmt, () => {
3398
+ const prev = result.previous_version ?? "?";
3399
+ const newVer = result.new_version ?? "?";
3400
+ const dur = result.reload_duration_ms ?? "?";
3401
+ process.stdout.write(`Module '${moduleId}' reloaded.
3402
+ `);
3403
+ process.stdout.write(` Version: ${prev} -> ${newVer}
3404
+ `);
3405
+ process.stdout.write(` Duration: ${dur}ms
3406
+ `);
3407
+ });
3408
+ } catch (e) {
3409
+ emitErrorAndExit(e);
3410
+ }
3411
+ });
3412
+ apcliGroup.addCommand(reloadCmd);
3413
+ }
3414
+ function registerConfigCommand(apcliGroup, executor) {
3415
+ const configGroup = new Command4("config").description("Read or update runtime configuration.");
3416
+ const configGetCmd = new Command4("get").description("Read a configuration value by dot-path key.").argument("<key>", "Configuration key (dot-path)").option("--format <format>", "Output format.", "table").action(async (key, opts) => {
3417
+ const fmt = resolveFormat(opts.format);
3418
+ try {
3419
+ const value = new Config().get(key);
3420
+ emitResult({ key, value }, fmt, () => {
3421
+ process.stdout.write(`${key} = ${JSON.stringify(value)}
3422
+ `);
3423
+ });
3424
+ } catch (e) {
3425
+ emitErrorAndExit(e);
3426
+ }
3427
+ });
3428
+ configGroup.addCommand(configGetCmd);
3429
+ const configSetCmd = new Command4("set").description("Update a runtime configuration value (audit-logged; client-side approval gate applies).").argument("<key>", "Configuration key (dot-path)").argument("<value>", "New value").requiredOption("--reason <reason>", "Reason for config change (required for audit).").option("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (key, value, opts) => {
3430
+ const fmt = resolveFormat(opts.format);
3431
+ let parsedValue;
3432
+ try {
3433
+ parsedValue = JSON.parse(value);
3434
+ } catch {
3435
+ parsedValue = value;
3436
+ }
3437
+ try {
3438
+ await requireApprovalForSystemCommand("system.control.update_config", opts.yes);
3439
+ const result = await callSystemModule(executor, "system.control.update_config", {
3440
+ key,
3441
+ value: parsedValue,
3442
+ reason: opts.reason
3443
+ });
3444
+ emitResult(result, fmt, () => {
3445
+ const old = result.old_value ?? "?";
3446
+ const newVal = result.new_value ?? "?";
3447
+ process.stdout.write(`Config updated: ${key}
3448
+ `);
3449
+ process.stdout.write(` ${JSON.stringify(old)} -> ${JSON.stringify(newVal)}
3450
+ `);
3451
+ process.stdout.write(` Reason: ${opts.reason}
3452
+ `);
3453
+ });
3454
+ } catch (e) {
3455
+ emitErrorAndExit(e);
3456
+ }
3457
+ });
3458
+ configGroup.addCommand(configSetCmd);
3459
+ apcliGroup.addCommand(configGroup);
3460
+ }
3461
+ var init_system_cmd = __esm({
3462
+ "src/system-cmd.ts"() {
3463
+ "use strict";
3464
+ init_esm_shims();
3465
+ init_approval();
3466
+ init_errors();
3467
+ init_output();
3468
+ }
3469
+ });
3470
+
3471
+ // src/strategy.ts
3472
+ import { Command as Command5, Option as Option3 } from "commander";
3473
+ function lookupStrategyInfo(executor, strategyName) {
3474
+ if (typeof executor.describePipeline === "function") {
3475
+ try {
3476
+ const current = executor.describePipeline();
3477
+ if (current && current.name === strategyName) {
3478
+ return { info: current, isCurrent: true };
3479
+ }
3480
+ } catch {
3481
+ }
3482
+ }
3483
+ const ctor = executor.constructor;
3484
+ if (ctor && typeof ctor.listStrategies === "function") {
3485
+ try {
3486
+ const all = ctor.listStrategies();
3487
+ const info = all.find((s) => s.name === strategyName) ?? null;
3488
+ return { info, isCurrent: false };
3489
+ } catch {
3490
+ return { info: null, isCurrent: false };
3491
+ }
3492
+ }
3493
+ return { info: null, isCurrent: false };
3494
+ }
3495
+ function registerPipelineCommand(cli, executor) {
3496
+ const pipelineCmd = new Command5("describe-pipeline").description("Show the execution pipeline steps for a strategy.").addOption(
3497
+ new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
3498
+ ).option("--format <format>", "Output format.").action((opts) => {
3499
+ const fmt = resolveFormat(opts.format);
3500
+ const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
3501
+ if (info) {
3502
+ const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
3503
+ const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
3504
+ if (fmt === "json" || !process.stdout.isTTY) {
3505
+ const payload = {
3506
+ strategy: info.name,
3507
+ step_count: info.stepCount,
3508
+ description: info.description,
3509
+ steps: info.stepNames.map((name, i) => {
3510
+ const stepMeta = strategySteps[i];
3511
+ return {
3512
+ index: i + 1,
3513
+ name,
3514
+ pure: stepMeta?.pure ?? false,
3515
+ removable: stepMeta?.removable ?? true,
3516
+ timeout_ms: stepMeta?.timeoutMs ?? null
3517
+ };
3518
+ })
3519
+ };
3520
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3521
+ } else {
3522
+ process.stdout.write(`${header}
3523
+
3524
+ `);
3525
+ process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3526
+ `);
3527
+ process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3528
+ `);
3529
+ for (let i = 0; i < info.stepNames.length; i++) {
3530
+ const stepMeta = strategySteps[i];
3531
+ const pure = stepMeta?.pure ? "yes" : "no";
3532
+ const removable = stepMeta?.removable !== false ? "yes" : "no";
3533
+ const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
3534
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
3535
+ `);
3496
3536
  }
3497
- warn(
3498
- `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
3499
- );
3500
- return null;
3501
3537
  }
3502
- };
3503
- }
3504
- });
3538
+ return;
3539
+ }
3540
+ const steps = PRESET_STEPS[opts.strategy] ?? [];
3541
+ const pureSteps = /* @__PURE__ */ new Set([
3542
+ "context_creation",
3543
+ "call_chain_guard",
3544
+ "module_lookup",
3545
+ "acl_check",
3546
+ "input_validation"
3547
+ ]);
3548
+ const nonRemovable = /* @__PURE__ */ new Set([
3549
+ "context_creation",
3550
+ "module_lookup",
3551
+ "execute",
3552
+ "return_result"
3553
+ ]);
3554
+ if (fmt === "json" || !process.stdout.isTTY) {
3555
+ const payload = {
3556
+ strategy: opts.strategy,
3557
+ step_count: steps.length,
3558
+ steps: steps.map((s, i) => ({
3559
+ index: i + 1,
3560
+ name: s,
3561
+ pure: pureSteps.has(s),
3562
+ removable: !nonRemovable.has(s)
3563
+ }))
3564
+ };
3565
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3566
+ } else {
3567
+ process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
3505
3568
 
3506
- // src/exposure.ts
3507
- function escapeRegex(str) {
3508
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3509
- }
3510
- function compilePattern(pattern) {
3511
- const sentinel = "\0GLOB\0";
3512
- const escaped = pattern.replaceAll("**", sentinel);
3513
- const parts = escaped.split("*");
3514
- const regexParts = parts.map((p) => {
3515
- const restored = p.replaceAll(sentinel, "**");
3516
- return escapeRegex(restored);
3569
+ `);
3570
+ process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3571
+ `);
3572
+ process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3573
+ `);
3574
+ for (let i = 0; i < steps.length; i++) {
3575
+ const pure = pureSteps.has(steps[i]) ? "yes" : "no";
3576
+ const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
3577
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
3578
+ `);
3579
+ }
3580
+ }
3517
3581
  });
3518
- let regex = regexParts.join("[^.]*");
3519
- regex = regex.replaceAll("\\*\\*", ".+");
3520
- return new RegExp(`^${regex}$`);
3582
+ cli.addCommand(pipelineCmd);
3521
3583
  }
3522
- var ExposureFilter;
3523
- var init_exposure = __esm({
3524
- "src/exposure.ts"() {
3584
+ var PRESET_STEPS;
3585
+ var init_strategy = __esm({
3586
+ "src/strategy.ts"() {
3525
3587
  "use strict";
3526
3588
  init_esm_shims();
3527
- init_logger();
3528
- ExposureFilter = class _ExposureFilter {
3529
- static VALID_MODES = ["all", "include", "exclude", "none"];
3530
- _mode;
3531
- _compiledInclude;
3532
- _compiledExclude;
3533
- constructor(mode = "all", include, exclude) {
3534
- if (!_ExposureFilter.VALID_MODES.includes(mode)) {
3535
- process.stderr.write(
3536
- `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
3537
- `
3538
- );
3539
- mode = "none";
3540
- }
3541
- this._mode = mode;
3542
- const dedup = (arr) => [...new Set(arr)];
3543
- this._compiledInclude = dedup(include ?? []).map(compilePattern);
3544
- this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
3545
- }
3546
- /** Return true if the module should be exposed as a CLI command. */
3547
- isExposed(moduleId) {
3548
- if (this._mode === "all") return true;
3549
- if (this._mode === "include") {
3550
- return this._compiledInclude.some((rx) => rx.test(moduleId));
3551
- }
3552
- if (this._mode === "exclude") {
3553
- return !this._compiledExclude.some((rx) => rx.test(moduleId));
3554
- }
3555
- return false;
3556
- }
3557
- /** Partition moduleIds into [exposed, hidden] lists. */
3558
- filterModules(moduleIds) {
3559
- const exposed = [];
3560
- const hidden = [];
3561
- for (const mid of moduleIds) {
3562
- (this.isExposed(mid) ? exposed : hidden).push(mid);
3563
- }
3564
- return [exposed, hidden];
3565
- }
3566
- /**
3567
- * Create an ExposureFilter from a parsed config dict.
3568
- *
3569
- * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
3570
- */
3571
- static fromConfig(config) {
3572
- const expose = config.expose ?? {};
3573
- if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
3574
- warn("Invalid 'expose' config (expected dict), using mode: all.");
3575
- return new _ExposureFilter();
3576
- }
3577
- const exposeObj = expose;
3578
- const mode = exposeObj.mode ?? "all";
3579
- if (!["all", "include", "exclude"].includes(mode)) {
3580
- throw new Error(
3581
- `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
3582
- );
3583
- }
3584
- let include = exposeObj.include ?? [];
3585
- if (!Array.isArray(include)) {
3586
- warn("Invalid 'expose.include' (expected list), ignoring.");
3587
- include = [];
3588
- }
3589
- let exclude = exposeObj.exclude ?? [];
3590
- if (!Array.isArray(exclude)) {
3591
- warn("Invalid 'expose.exclude' (expected list), ignoring.");
3592
- exclude = [];
3593
- }
3594
- const filterList = (arr, label) => {
3595
- const result = [];
3596
- for (const p of arr) {
3597
- if (!p) {
3598
- warn(`Empty pattern in expose.${label}, skipping.`);
3599
- } else {
3600
- result.push(String(p));
3601
- }
3602
- }
3603
- return result;
3604
- };
3605
- return new _ExposureFilter(
3606
- mode,
3607
- filterList(include, "include"),
3608
- filterList(exclude, "exclude")
3609
- );
3610
- }
3589
+ init_output();
3590
+ PRESET_STEPS = {
3591
+ standard: [
3592
+ "context_creation",
3593
+ "call_chain_guard",
3594
+ "module_lookup",
3595
+ "acl_check",
3596
+ "approval_gate",
3597
+ "middleware_before",
3598
+ "input_validation",
3599
+ "execute",
3600
+ "output_validation",
3601
+ "middleware_after",
3602
+ "return_result"
3603
+ ],
3604
+ internal: [
3605
+ "context_creation",
3606
+ "call_chain_guard",
3607
+ "module_lookup",
3608
+ "middleware_before",
3609
+ "input_validation",
3610
+ "execute",
3611
+ "output_validation",
3612
+ "middleware_after",
3613
+ "return_result"
3614
+ ],
3615
+ testing: [
3616
+ "context_creation",
3617
+ "module_lookup",
3618
+ "middleware_before",
3619
+ "input_validation",
3620
+ "execute",
3621
+ "output_validation",
3622
+ "middleware_after",
3623
+ "return_result"
3624
+ ],
3625
+ performance: [
3626
+ "context_creation",
3627
+ "call_chain_guard",
3628
+ "module_lookup",
3629
+ "acl_check",
3630
+ "approval_gate",
3631
+ "input_validation",
3632
+ "execute",
3633
+ "output_validation",
3634
+ "return_result"
3635
+ ],
3636
+ minimal: [
3637
+ "context_creation",
3638
+ "module_lookup",
3639
+ "execute",
3640
+ "return_result"
3641
+ ]
3611
3642
  };
3612
3643
  }
3613
3644
  });
@@ -3745,7 +3776,8 @@ __export(main_exports, {
3745
3776
  import { readFileSync as readFileSync3 } from "fs";
3746
3777
  import { fileURLToPath as fileURLToPath2 } from "url";
3747
3778
  import * as path5 from "path";
3748
- import { Command as Command5, CommanderError, Option as Option4 } from "commander";
3779
+ import { Command as Command6, CommanderError, Option as Option4 } from "commander";
3780
+ import { BindingLoader, DisplayResolver } from "apcore-toolkit";
3749
3781
  function setAllOptionsHelp(allOptions) {
3750
3782
  verboseHelp = allOptions;
3751
3783
  }
@@ -3783,6 +3815,37 @@ function resolveStringOption(cliValue, envValue) {
3783
3815
  }
3784
3816
  return void 0;
3785
3817
  }
3818
+ function validateInputSchema(schema, input) {
3819
+ const required = schema.required;
3820
+ if (required && Array.isArray(required)) {
3821
+ for (const field of required) {
3822
+ const val = input[field];
3823
+ if (val === null || val === void 0) {
3824
+ return `'${field}' is required`;
3825
+ }
3826
+ }
3827
+ }
3828
+ const properties = schema.properties;
3829
+ if (properties) {
3830
+ for (const [field, propSchema] of Object.entries(properties)) {
3831
+ const val = input[field];
3832
+ if (val === null || val === void 0) continue;
3833
+ const expectedType = propSchema.type;
3834
+ if (!expectedType) continue;
3835
+ const actualType = typeof val;
3836
+ if (expectedType === "string" && actualType !== "string") {
3837
+ return `'${field}' must be a string, got ${actualType}`;
3838
+ }
3839
+ if ((expectedType === "integer" || expectedType === "number") && actualType !== "number") {
3840
+ return `'${field}' must be a number, got ${actualType}`;
3841
+ }
3842
+ if (expectedType === "boolean" && actualType !== "boolean") {
3843
+ return `'${field}' must be a boolean, got ${actualType}`;
3844
+ }
3845
+ }
3846
+ }
3847
+ return null;
3848
+ }
3786
3849
  function emitErrorJson(e, exitCode) {
3787
3850
  const err = e instanceof Error ? e : new Error(String(e));
3788
3851
  const errRecord = err;
@@ -3893,7 +3956,7 @@ function createCli(extensionsDirOrOpts, progName, allOptions = false) {
3893
3956
  }
3894
3957
  }
3895
3958
  const registryInjected = registry !== void 0;
3896
- const program = new Command5(resolvedProgName).exitOverride().helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description(appDescription ?? `${resolvedProgName} CLI`).option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--all-options", "Show all options in help output (including built-in options)");
3959
+ const program = new Command6(resolvedProgName).exitOverride().helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description(appDescription ?? `${resolvedProgName} CLI`).option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--all-options", "Show all options in help output (including built-in options)");
3897
3960
  if (appVersion) {
3898
3961
  program.version(appVersion, "-V, --version", "Print version");
3899
3962
  }
@@ -4006,8 +4069,8 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4006
4069
  process.exit(EXIT_CODES.CONFIG_INVALID);
4007
4070
  };
4008
4071
  const effectiveRegistry = registry ?? {
4009
- listModules: () => emitUnwiredError(),
4010
- getModule: () => emitUnwiredError()
4072
+ list: () => emitUnwiredError(),
4073
+ getDefinition: () => emitUnwiredError()
4011
4074
  };
4012
4075
  const TABLE = [
4013
4076
  { name: "list", requiresExecutor: false, register: (g) => registerListCommand(g, effectiveRegistry, exposureFilter) },
@@ -4024,6 +4087,17 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4024
4087
  { name: "completion", requiresExecutor: false, register: (g) => registerCompletionCommand(g) },
4025
4088
  { name: "describe-pipeline", requiresExecutor: true, register: (g, _r, ex) => registerPipelineCommand(g, ex) }
4026
4089
  ];
4090
+ const _SYSTEM_COMMANDS = /* @__PURE__ */ new Set(["health", "usage", "enable", "disable", "reload", "config"]);
4091
+ const systemModulesAvailable = (() => {
4092
+ if (!executor) return false;
4093
+ const reg = executor.registry ?? registry;
4094
+ if (!reg) return false;
4095
+ try {
4096
+ return reg.getDefinition("system.health.summary") != null;
4097
+ } catch {
4098
+ return false;
4099
+ }
4100
+ })();
4027
4101
  const mode = apcliCfg.resolveVisibility();
4028
4102
  for (const entry of TABLE) {
4029
4103
  let shouldRegister;
@@ -4033,6 +4107,7 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4033
4107
  shouldRegister = _ALWAYS_REGISTERED.has(entry.name) || apcliCfg.isSubcommandIncluded(entry.name);
4034
4108
  }
4035
4109
  if (!shouldRegister) continue;
4110
+ if (_SYSTEM_COMMANDS.has(entry.name) && !systemModulesAvailable) continue;
4036
4111
  if (entry.requiresExecutor && !executor) {
4037
4112
  if (_ALWAYS_REGISTERED.has(entry.name)) {
4038
4113
  warn(
@@ -4054,38 +4129,22 @@ async function applyToolkitIntegration(commandsDir, bindingPath, options = {}) {
4054
4129
  if (!commandsDir && !bindingPath) {
4055
4130
  return;
4056
4131
  }
4057
- let toolkit;
4058
- try {
4059
- const toolkitModule = "apcore-toolkit";
4060
- toolkit = await import(
4061
- /* @vite-ignore */
4062
- toolkitModule
4063
- );
4064
- } catch {
4065
- warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
4066
- return;
4067
- }
4068
4132
  if (commandsDir) {
4069
4133
  warn("Convention scanning not available in the TypeScript toolkit");
4070
4134
  }
4071
4135
  if (bindingPath) {
4072
4136
  try {
4073
- await loadBindingDisplayOverlay(toolkit, bindingPath, options.allowedPrefixes);
4137
+ await loadBindingDisplayOverlay(bindingPath, options.allowedPrefixes);
4074
4138
  } catch (err) {
4075
4139
  const msg = err instanceof Error ? err.message : String(err);
4076
4140
  warn(`apcore-toolkit: failed to load binding '${bindingPath}': ${msg}`);
4077
4141
  }
4078
4142
  }
4079
4143
  }
4080
- async function loadBindingDisplayOverlay(toolkit, bindingPath, allowedPrefixes) {
4081
- const BindingLoaderCtor = toolkit.BindingLoader;
4082
- const DisplayResolverCtor = toolkit.DisplayResolver;
4083
- if (!BindingLoaderCtor || !DisplayResolverCtor) {
4084
- return;
4085
- }
4086
- const loader = new BindingLoaderCtor();
4144
+ async function loadBindingDisplayOverlay(bindingPath, allowedPrefixes) {
4145
+ const loader = new BindingLoader();
4087
4146
  const scanned = loader.load(bindingPath);
4088
- const resolver = new DisplayResolverCtor();
4147
+ const resolver = new DisplayResolver();
4089
4148
  const resolved = resolver.resolve(scanned, { bindingPath });
4090
4149
  const prefixes = allowedPrefixes && allowedPrefixes.length > 0 ? allowedPrefixes : null;
4091
4150
  const isTargetAllowed = (target) => {
@@ -4134,7 +4193,7 @@ function main(progName) {
4134
4193
  }
4135
4194
  }
4136
4195
  function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdName, verbose = verboseHelp) {
4137
- const moduleId = moduleDef.id;
4196
+ const moduleId = moduleDef.moduleId;
4138
4197
  let resolvedSchema = {};
4139
4198
  let schemaOptions = [];
4140
4199
  const display = getDisplay(moduleDef);
@@ -4160,7 +4219,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
4160
4219
  }
4161
4220
  schemaOptions = schemaToCliOptions(resolvedSchema, helpTextMaxLength);
4162
4221
  }
4163
- const cmd = new Command5(effectiveCmdName).description(cmdHelp);
4222
+ const cmd = new Command6(effectiveCmdName).description(cmdHelp);
4164
4223
  const inputOpt = new Option4("--input <source>", "Read JSON input from a file path, or use '-' to read from stdin pipe");
4165
4224
  const yesOpt = new Option4("-y, --yes", "Skip interactive approval prompts (for scripts and CI)").default(false);
4166
4225
  const largeInputOpt = new Option4("--large-input", "Allow stdin input larger than 10MB (default limit protects against accidental pipes)").default(false);
@@ -4296,6 +4355,12 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
4296
4355
  }
4297
4356
  process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
4298
4357
  }
4358
+ if (resolvedSchema.properties) {
4359
+ const validationErr = validateInputSchema(resolvedSchema, merged);
4360
+ if (validationErr) {
4361
+ throw new SchemaValidationError(`Validation failed: ${validationErr}`);
4362
+ }
4363
+ }
4299
4364
  if (approvalToken) {
4300
4365
  merged._approval_token = approvalToken;
4301
4366
  }