Skip to content

Commit 24c4c90

Browse files
committed
refactor: simplify environment variable prefix convention by removing double-underscore requirement for sub-packages
1 parent 1e11284 commit 24c4c90

4 files changed

Lines changed: 46 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [0.15.1] - 2026-03-31
11+
12+
### Changed
13+
14+
- **Env prefix convention simplified** — Removed the `^APCORE_[A-Z0-9]` reservation rule from namespace registration. Sub-packages now use single-underscore prefixes (`APCORE_MCP`, `APCORE_OBSERVABILITY`, `APCORE_SYS`) instead of the double-underscore form. The longest-prefix-match dispatch algorithm already disambiguates correctly; the previous restriction was unnecessary.
15+
- Built-in namespace env prefixes: `APCORE__OBSERVABILITY``APCORE_OBSERVABILITY`, `APCORE__SYS``APCORE_SYS`.
16+
17+
---
18+
1019
## [0.15.0] - 2026-03-29
1120

1221
### Added
@@ -16,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1625
- **Namespace Registration (§9.5)**`Config.register_namespace(name, schema, env_prefix, defaults)` API with cross-language examples (Python, TypeScript, Rust, Go, Java). Global (class-level) registry shared across Config instances. Late registration permitted with explicit `reload()` to apply. No `unregister_namespace` in this version
1726
- **Unified Configuration File (§9.6)** — Single YAML file with namespace-partitioned sections. Automatic mode detection: legacy mode (no `apcore:` key, fully backward compatible) vs namespace mode (`apcore:` key present). `_config` reserved namespace for meta-configuration (`strict`, `allow_unknown`)
1827
- **Mount Mechanism (§9.7)**`config.mount(namespace, from_file|from_dict)` for attaching external configuration sources without requiring a unified file. Primary integration path for third-party projects with existing config systems
19-
- **Per-Namespace Env Override (§9.8)** — Each namespace declares its own `env_prefix`. Longest-prefix-match dispatch algorithm resolves ambiguity. `APCORE__MCP` double-underscore convention for apcore sub-packages to avoid collision with `APCORE_` prefix. Compatibility note for apflow's simpler env convention
28+
- **Per-Namespace Env Override (§9.8)** — Each namespace declares its own `env_prefix`. Longest-prefix-match dispatch algorithm resolves ambiguity. `APCORE_MCP` double-underscore convention for apcore sub-packages to avoid collision with `APCORE_` prefix. Compatibility note for apflow's simpler env convention
2029
- **Namespace-Aware Access API (§9.9)**`config.get("namespace.key.path")` with dot-path namespace resolution algorithm. `config.namespace(name)` for full subtree retrieval. `config.bind(ns, type)` / `config.get_typed(path, type)` for typed access. `Config.registered_namespaces()` for introspection
2130
- **Validation Algorithm A12-NS (§9.10)** — Extended A12 for namespace mode: validates `apcore` namespace with original algorithm, validates registered namespaces against their JSON Schema, handles unknown namespaces per strict/allow_unknown settings
2231
- **Hot-Reload Namespace Support (§9.11)**`config.reload()` re-reads YAML, re-detects mode, re-applies namespace defaults and env overrides, re-validates, and re-reads mounted files
@@ -32,8 +41,8 @@ Three mechanisms addressing ecosystem consistency across apcore, apcore-mcp, apc
3241
- **Error Formatter Registry (§8.8)** — Shared `ErrorFormatter` protocol and registration point. apcore-mcp and apcore-a2a each independently implement protocol-specific error mappers (MCP camelCase/sanitization, A2A JSON-RPC code mapping); this registry makes the contract explicit and discoverable. Adoption is SHOULD-level for ecosystem adapters — apcore does not ship adapter-specific formatters. New error code: `ERROR_FORMATTER_DUPLICATE`
3342

3443
- **apcore Built-in Namespace Registrations (§9.15)** — The framework pre-registers two namespaces for its own subsystems, applying the Config Bus pattern to apcore's own internal configuration. Both promote existing flat keys already present in apcore-python's `config.py`; migration is 1:1 with no breaking changes:
35-
- **`observability`** (`APCORE__OBSERVABILITY`) — Extracts `apcore.observability.*` flat keys (tracing, metrics, logging, error_history, platform_notify) into a dedicated namespace. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) **should** read from this namespace rather than using independent logging defaults
36-
- **`sys_modules`** (`APCORE__SYS`) — Promotes `apcore.sys_modules.*` flat keys into a dedicated namespace. `register_sys_modules()` prefers `config.namespace("sys_modules")` in namespace mode with `config.get("sys_modules.*")` legacy fallback
44+
- **`observability`** (`APCORE_OBSERVABILITY`) — Extracts `apcore.observability.*` flat keys (tracing, metrics, logging, error_history, platform_notify) into a dedicated namespace. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) **should** read from this namespace rather than using independent logging defaults
45+
- **`sys_modules`** (`APCORE_SYS`) — Promotes `apcore.sys_modules.*` flat keys into a dedicated namespace. `register_sys_modules()` prefers `config.namespace("sys_modules")` in namespace mode with `config.get("sys_modules.*")` legacy fallback
3746

3847
- **Event Type Naming and Collision Fix (§9.16)** — Resolves two confirmed collisions in apcore-python's emitted event types:
3948
- `"module_health_changed"` was used for two distinct events (toggle on/off vs. error rate recovery); replaced by canonical names `apcore.module.toggled` and `apcore.health.recovered`

PROTOCOL_SPEC.md

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4491,7 +4491,7 @@ Config.register_namespace(
44914491
|-----------|----------|-------------|
44924492
| `name` | MUST | Namespace identifier. Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` (lowercase, hyphens allowed). Examples: `apcore`, `apflow`, `apcore-mcp`, `my-billing`. |
44934493
| `schema` | MAY | JSON Schema document (inline object or file path). When provided, the namespace section is validated against this schema during `Config.validate()`. When `nil`, the namespace is registered for isolation and env override only — no structural validation is performed. |
4494-
| `env_prefix` | MAY | Uppercase prefix for environment variable overrides (e.g., `APFLOW`). When `nil`, no environment variable overrides are applied for this namespace. Must match pattern: `^[A-Z][A-Z0-9]*(_[A-Z0-9]+|__[A-Z][A-Z0-9]*)*$`. The `__` (double-underscore) form is used to avoid collision with the `APCORE_` prefix (e.g., `APCORE__MCP`). |
4494+
| `env_prefix` | MAY | Uppercase prefix for environment variable overrides (e.g., `APFLOW`). When `nil`, no environment variable overrides are applied for this namespace. Must match pattern: `^[A-Z][A-Z0-9]*(_[A-Z0-9]+|__[A-Z][A-Z0-9]*)*$`. The `__` (double-underscore) form is used to avoid collision with the `APCORE_` prefix (e.g., `APCORE_MCP`). |
44954495
| `defaults` | MAY | Default configuration values for this namespace. Merged before file data (lowest priority). |
44964496

44974497
**Registration rules:**
@@ -4925,9 +4925,9 @@ Examples (namespace "apflow", env_prefix "APFLOW"):
49254925
APFLOW_API_TIMEOUT=60 → apflow.api.timeout
49264926
APFLOW_GOVERNANCE_DEFAULT__POLICY=x → apflow.governance.default_policy
49274927
4928-
Examples (namespace "apcore-mcp", env_prefix "APCORE__MCP"):
4929-
APCORE__MCP_TRANSPORT=stdio → apcore-mcp.transport
4930-
APCORE__MCP_PORT=9000 → apcore-mcp.port
4928+
Examples (namespace "apcore-mcp", env_prefix "APCORE_MCP"):
4929+
APCORE_MCP_TRANSPORT=stdio → apcore-mcp.transport
4930+
APCORE_MCP_PORT=9000 → apcore-mcp.port
49314931
49324932
Examples (namespace "apcore", env_prefix "APCORE" — unchanged from §9.2):
49334933
APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000 → apcore.executor.default_timeout
@@ -4942,31 +4942,31 @@ Examples (namespace "apcore", env_prefix "APCORE" — unchanged from §9.2):
49424942
Env prefix conflicts arise when one registered prefix is a string prefix of another, making it ambiguous which namespace owns a given env var. The following rules prevent this:
49434943

49444944
1. Each `env_prefix` **must** be unique across all registered namespaces. Attempting to register a duplicate `env_prefix` **must** raise `CONFIG_ENV_PREFIX_CONFLICT`.
4945-
2. Any `env_prefix` that starts with `APCORE_` (i.e., matches `^APCORE_[A-Z0-9]`) **must** raise `CONFIG_ENV_PREFIX_CONFLICT`. This prevents collision with the `apcore` namespace's `APCORE_` prefix. The double-underscore form (`^APCORE__[A-Z]`, e.g., `APCORE__MCP`) is explicitly permitted and dispatched via longest-prefix-match (see dispatch algorithm below).
4945+
2. Any `env_prefix` that starts with `APCORE_` (i.e., matches `^APCORE_[A-Z0-9]`) **must** raise `CONFIG_ENV_PREFIX_CONFLICT`. This prevents collision with the `apcore` namespace's `APCORE_` prefix. The double-underscore form (`^APCORE_[A-Z]`, e.g., `APCORE_MCP`) is explicitly permitted and dispatched via longest-prefix-match (see dispatch algorithm below).
49464946
3. The prefix `APCORE` is reserved for the `apcore` namespace. Attempting to register it for another namespace **must** raise `CONFIG_NAMESPACE_RESERVED`.
49474947

4948-
**Resolving the `APCORE` / `APCORE__MCP` ambiguity:**
4948+
**Resolving the `APCORE` / `APCORE_MCP` ambiguity:**
49494949

49504950
A naive prefix scheme would make `APCORE_MCP` (for apcore-mcp) collide with `APCORE_` (for apcore, key path `mcp.*`). The ecosystem convention resolves this by requiring apcore ecosystem packages to use a double-underscore separator between `APCORE` and the sub-package name in their env prefix:
49514951

49524952
| Package | Namespace | Env Prefix | Why safe |
49534953
|---------|-----------|------------|----------|
49544954
| apcore | `apcore` | `APCORE` | Base prefix |
4955-
| apcore-mcp | `apcore-mcp` | `APCORE__MCP` | `APCORE__MCP_` is not a valid `APCORE_` key (double `__` creates invalid path) |
4956-
| apcore-a2a | `apcore-a2a` | `APCORE__A2A` | Same reasoning |
4957-
| apcore-cli | `apcore-cli` | `APCORE__CLI` | Same reasoning |
4955+
| apcore-mcp | `apcore-mcp` | `APCORE_MCP` | `APCORE_MCP_` is not a valid `APCORE_` key (double `__` creates invalid path) |
4956+
| apcore-a2a | `apcore-a2a` | `APCORE_A2A` | Same reasoning |
4957+
| apcore-cli | `apcore-cli` | `APCORE_CLI` | Same reasoning |
49584958
| apflow | `apflow` | `APFLOW` | Completely disjoint prefix |
49594959
| django-apcore | `django-apcore` | `DJANGO_APCORE` | No `DJANGO` namespace registered — no prefix collision |
49604960

4961-
This works because the `APCORE_` prefix matcher stops at the first `_` boundary. An env var like `APCORE__MCP_TRANSPORT` starts with `APCORE__` (double underscore), which the `APCORE_` prefix handler would interpret as key path `apcore._mcp.transport` — not a valid apcore config path. The `APCORE__MCP_` prefix handler correctly claims it.
4961+
This works because the `APCORE_` prefix matcher stops at the first `_` boundary. An env var like `APCORE_MCP_TRANSPORT` starts with `APCORE_` (double underscore), which the `APCORE_` prefix handler would interpret as key path `apcore._mcp.transport` — not a valid apcore config path. The `APCORE_MCP_` prefix handler correctly claims it.
49624962

49634963
However, this convention introduces complexity. Implementations **must** use **longest-prefix-match** when dispatching env vars to namespaces:
49644964

49654965
```
49664966
Algorithm: dispatch_env_var(env_key, registered_prefixes)
49674967
49684968
Input:
4969-
env_key — Environment variable name (e.g., "APCORE__MCP_TRANSPORT")
4969+
env_key — Environment variable name (e.g., "APCORE_MCP_TRANSPORT")
49704970
registered_prefixes — List of (env_prefix + "_", namespace_name) tuples,
49714971
sorted by prefix length descending
49724972
@@ -5116,7 +5116,7 @@ Config.registered_namespaces()
51165116
→ [
51175117
{name: "apcore", env_prefix: "APCORE", has_schema: true},
51185118
{name: "apflow", env_prefix: "APFLOW", has_schema: true},
5119-
{name: "apcore-mcp", env_prefix: "APCORE__MCP", has_schema: true},
5119+
{name: "apcore-mcp", env_prefix: "APCORE_MCP", has_schema: true},
51205120
{name: "billing", env_prefix: "BILLING", has_schema: false},
51215121
]
51225122
```
@@ -5255,7 +5255,7 @@ from apcore import Config
52555255
Config.register_namespace(
52565256
"apcore-mcp",
52575257
schema=_resolve_schema_path("apcore-mcp.schema.json"),
5258-
env_prefix="APCORE__MCP", # double underscore to avoid APCORE_ prefix collision
5258+
env_prefix="APCORE_MCP", # double underscore to avoid APCORE_ prefix collision
52595259
defaults={"transport": "streamable-http", "port": 8000},
52605260
)
52615261
```
@@ -5265,17 +5265,17 @@ Config.register_namespace(
52655265
| Package | Namespace | Env Prefix | Conflict? | Schema |
52665266
|---------|-----------|------------|-----------|--------|
52675267
| apcore (core) | `apcore` | `APCORE` || `apcore-config.schema.json` |
5268-
| apcore-mcp | `apcore-mcp` | `APCORE__MCP` | Yes — `APCORE_` is a prefix of `APCORE_MCP_`; use `APCORE__MCP` to disambiguate | `apcore-mcp.schema.json` |
5269-
| apcore-a2a | `apcore-a2a` | `APCORE__A2A` | Same as above | `apcore-a2a.schema.json` |
5270-
| apcore-cli | `apcore-cli` | `APCORE__CLI` | Same as above | `apcore-cli.schema.json` |
5268+
| apcore-mcp | `apcore-mcp` | `APCORE_MCP` | Yes — `APCORE_` is a prefix of `APCORE_MCP_`; use `APCORE_MCP` to disambiguate | `apcore-mcp.schema.json` |
5269+
| apcore-a2a | `apcore-a2a` | `APCORE_A2A` | Same as above | `apcore-a2a.schema.json` |
5270+
| apcore-cli | `apcore-cli` | `APCORE_CLI` | Same as above | `apcore-cli.schema.json` |
52715271
| apflow | `apflow` | `APFLOW` | No — disjoint from `APCORE_` | `apflow.schema.json` |
52725272
| django-apcore | `django-apcore` | `DJANGO_APCORE` | No — no `DJANGO` namespace registered | `django-apcore.schema.json` |
52735273
| fastapi-apcore | `fastapi-apcore` | `FASTAPI_APCORE` | No — no `FASTAPI` namespace registered | `fastapi-apcore.schema.json` |
52745274
| flask-apcore | `flask-apcore` | `FLASK_APCORE` | No — no `FLASK` namespace registered | `flask-apcore.schema.json` |
52755275
| nestjs-apcore | `nestjs-apcore` | `NESTJS_APCORE` | No — no `NESTJS` namespace registered | `nestjs-apcore.schema.json` |
52765276
| axum-apcore | `axum-apcore` | `AXUM_APCORE` | No — no `AXUM` namespace registered | `axum-apcore.schema.json` |
52775277

5278-
> **Why `APCORE__MCP` and not `APCORE_MCP`?** The prefix `APCORE_` (for the `apcore` namespace) is a string prefix of `APCORE_MCP_`. An env var like `APCORE_MCP_TRANSPORT` is ambiguous: does it set `apcore → mcp.transport` or `apcore-mcp → transport`? The double-underscore convention (`APCORE__MCP_`) breaks the ambiguity because `APCORE_` never matches `APCORE__MCP_TRANSPORT` as an apcore key (the double underscore creates an invalid key path `_mcp.transport`). Framework integrations like `DJANGO_APCORE` do not have this problem because no `DJANGO` namespace is registered, so `DJANGO_APCORE_*` is unambiguous.
5278+
> **Why `APCORE_MCP` and not `APCORE_MCP`?** The prefix `APCORE_` (for the `apcore` namespace) is a string prefix of `APCORE_MCP_`. An env var like `APCORE_MCP_TRANSPORT` is ambiguous: does it set `apcore → mcp.transport` or `apcore-mcp → transport`? The double-underscore convention (`APCORE_MCP_`) breaks the ambiguity because `APCORE_` never matches `APCORE_MCP_TRANSPORT` as an apcore key (the double underscore creates an invalid key path `_mcp.transport`). Framework integrations like `DJANGO_APCORE` do not have this problem because no `DJANGO` namespace is registered, so `DJANGO_APCORE_*` is unambiguous.
52795279
52805280
#### 9.13.2 Third-Party Package Integration
52815281

@@ -5416,7 +5416,7 @@ Extracts the existing `observability.*` flat keys from the `apcore` namespace in
54165416
Config.register_namespace(
54175417
"observability",
54185418
schema="schemas/observability.schema.json",
5419-
env_prefix="APCORE__OBSERVABILITY",
5419+
env_prefix="APCORE_OBSERVABILITY",
54205420
defaults={
54215421
"tracing": {
54225422
"enabled": False,
@@ -5450,12 +5450,12 @@ Config.register_namespace(
54505450

54515451
**Migration:** Existing `apcore.observability.*` flat keys map 1:1 to `observability.*` namespace keys. No changes required to existing configuration files.
54525452

5453-
**Environment variable examples (`env_prefix = APCORE__OBSERVABILITY`):**
5453+
**Environment variable examples (`env_prefix = APCORE_OBSERVABILITY`):**
54545454

54555455
```
5456-
APCORE__OBSERVABILITY_TRACING_STRATEGY=error_first
5457-
APCORE__OBSERVABILITY_LOGGING_LEVEL=debug
5458-
APCORE__OBSERVABILITY_METRICS_EXPORTER=prometheus
5456+
APCORE_OBSERVABILITY_TRACING_STRATEGY=error_first
5457+
APCORE_OBSERVABILITY_LOGGING_LEVEL=debug
5458+
APCORE_OBSERVABILITY_METRICS_EXPORTER=prometheus
54595459
```
54605460

54615461
**Ecosystem adoption** — adapter packages **should** read from this namespace rather than maintaining their own observability defaults:
@@ -5476,7 +5476,7 @@ Promotes the existing `sys_modules.*` flat keys — currently read directly by `
54765476
Config.register_namespace(
54775477
"sys_modules",
54785478
schema="schemas/sys-modules.schema.json",
5479-
env_prefix="APCORE__SYS",
5479+
env_prefix="APCORE_SYS",
54805480
defaults={
54815481
"enabled": True,
54825482
"health": {"enabled": True},
@@ -5500,12 +5500,12 @@ Config.register_namespace(
55005500

55015501
**Migration:** `register_sys_modules()` **must** prefer `config.namespace("sys_modules")` in namespace mode, falling back to `config.get("sys_modules.*")` in legacy mode. No breaking change.
55025502

5503-
**Environment variable examples (`env_prefix = APCORE__SYS`):**
5503+
**Environment variable examples (`env_prefix = APCORE_SYS`):**
55045504

55055505
```
5506-
APCORE__SYS_ENABLED=true
5507-
APCORE__SYS_USAGE_RETENTION__HOURS=336
5508-
APCORE__SYS_EVENTS_ENABLED=false
5506+
APCORE_SYS_ENABLED=true
5507+
APCORE_SYS_USAGE_RETENTION__HOURS=336
5508+
APCORE_SYS_EVENTS_ENABLED=false
55095509
```
55105510

55115511
---

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ apcore-a2a:
880880

881881
Each package registers its own namespace with `Config.register_namespace()`. Third-party projects can also participate via `config.mount()` without modifying their existing configuration files. See the protocol spec for the full integration spectrum — from zero-coupling to full unification.
882882

883-
**Environment variable overrides** work per namespace: `APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000` for apcore, `APFLOW_API_TIMEOUT=60` for apflow, `APCORE__MCP_PORT=9000` for apcore-mcp.
883+
**Environment variable overrides** work per namespace: `APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000` for apcore, `APFLOW_API_TIMEOUT=60` for apflow, `APCORE_MCP_PORT=9000` for apcore-mcp.
884884

885885
---
886886

docs/features/config-bus.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Config::register_namespace(apcore::NamespaceRegistration {
6767
Env vars are dispatched to namespaces using **longest-prefix-match**. Sort all registered `envPrefix` values by length descending; the first match wins.
6868

6969
```
70-
APCORE__OBSERVABILITY_TRACING_ENABLED=true
70+
APCORE_OBSERVABILITY_TRACING_ENABLED=true
7171
→ namespace "observability", key "tracing.enabled"
7272
7373
MY_PLUGIN_TIMEOUT=10000
@@ -78,7 +78,7 @@ Separator rules:
7878
- Double `__` in the suffix → literal `_` in the key
7979
- Single `_` in the suffix → `.` separator in the key
8080

81-
**Reserved prefix:** Any env var matching `APCORE_[A-Z0-9]` is reserved for apcore's legacy flat-key override scheme and cannot be used as a namespace `envPrefix`. Double-underscore `APCORE__` prefixes are allowed for apcore sub-package namespaces.
81+
**Reserved prefix:** Any env var matching `APCORE_[A-Z0-9]` is reserved for apcore's legacy flat-key override scheme and cannot be used as a namespace `envPrefix`. Double-underscore `APCORE_` prefixes are allowed for apcore sub-package namespaces.
8282

8383
## Namespace Access
8484

@@ -198,8 +198,8 @@ apcore pre-registers two namespaces at startup:
198198

199199
| Namespace | Env prefix | Description |
200200
|-----------|-----------|-------------|
201-
| `observability` | `APCORE__OBSERVABILITY` | Tracing, metrics, logging, error history, platform notify config |
202-
| `sys_modules` | `APCORE__SYS` | System modules enable/disable and threshold config |
201+
| `observability` | `APCORE_OBSERVABILITY` | Tracing, metrics, logging, error history, platform notify config |
202+
| `sys_modules` | `APCORE_SYS` | System modules enable/disable and threshold config |
203203

204204
## Config Discovery (§9.14)
205205

@@ -257,13 +257,13 @@ The `_config` reserved namespace controls validation behavior. `strict: true` ca
257257
```python
258258
# Python
259259
namespaces = Config.registered_namespaces()
260-
# [{"name": "observability", "env_prefix": "APCORE__OBSERVABILITY", "has_schema": False}, ...]
260+
# [{"name": "observability", "env_prefix": "APCORE_OBSERVABILITY", "has_schema": False}, ...]
261261
```
262262

263263
```typescript
264264
// TypeScript
265265
const namespaces = Config.registeredNamespaces();
266-
// [{ name: 'observability', envPrefix: 'APCORE__OBSERVABILITY', hasSchema: false }, ...]
266+
// [{ name: 'observability', envPrefix: 'APCORE_OBSERVABILITY', hasSchema: false }, ...]
267267
```
268268

269269
## Key Files

0 commit comments

Comments
 (0)