Understanding the L1-L4 Layered Configuration System in AIOS Pro

AIOS Pro resolves runtime configuration through a deterministic five-level hierarchy where L1 (Framework) → L2 (Project) → Pro → L3 (App) → L4 (Local), with each successive layer overwriting the previous according to deep-merge rules defined in ADR-PRO-002.

The L1-L4 layered configuration system in AIOS Pro provides a structured approach to managing environment-specific settings while maintaining team-wide consistency. Implemented in the SynkraAI/aios-core repository, this architecture ensures that global defaults remain stable, shared policies are enforceable, and individual developers can safely customize their environments without polluting version control.

The Five-Level Configuration Hierarchy

AIOS Pro evaluates configuration through five distinct layers, though only the first four (L1-L4) are present in every project. Each layer serves a specific purpose and is merged in a deterministic order, with higher layers overwriting lower ones based on the merge rules in ADR-PRO-002.

Layer File (relative to project root) Purpose Typical contents
L1 – Framework .aios-core/framework-config.yaml Read-only defaults shipped with the AIOS package Global metadata, default performance limits, built-in resource locations
L2 – Project .aios-core/project-config.yaml Team-wide settings that live in source control Project name, shared resource quotas, CI/CD integration points
L3 – App (optional) aios-app.config.yaml (inside an app folder) Per-app customisation – only loaded when options.appDir is supplied UI themes, feature flags that differ between micro-apps
L4 – Local .aios-core/local-config.yaml (git-ignored) Developer-specific, machine-specific overrides (secrets, IDE preferences, local tooling) ${ENV_VAR} interpolation allowed, secret tokens, personal IDE selections

How Configuration Inheritance Works in AIOS Pro

The inheritance mechanism in AIOS Pro is implemented in .aios-core/core/config/config-resolver.js and follows a strict loading and merging protocol.

Loading and Resolution Order

The entry point resolveConfig(projectRoot, options) first checks whether the repository is in legacy mode (detecting core-config.yaml). If not, it delegates to loadLayeredConfig() to orchestrate the hierarchy.

The merge sequence follows this exact order:

// Pseudo-code from loadLayeredConfig() in config-resolver.js
config = deepMerge(L1, L2)          // L2 overrides L1
config = deepMerge(config, Pro)     // Pro layer overrides previous
config = deepMerge(config, L3)      // optional app overrides
config = deepMerge(config, L4)      // L4 overrides everything above it
config = deepMerge(config, L5)      // user-wide overrides (outside L1-L4 scope)

Deep Merge Strategy and Rules

The actual merging logic resides in .aios-core/core/config/merge-utils.js and applies different strategies based on data type:

  • Scalars (string, number, boolean) – Last-wins semantics apply. The value from the higher layer completely replaces the lower layer value.
  • Objects – Deep-merged recursively. Nested properties from higher layers override specific keys in lower layer objects without discarding sibling properties.
  • Arrays – Replaced entirely by the higher-level array, unless the configuration key ends with the special suffix +append, which triggers concatenation instead of replacement.
  • null values – When a higher layer explicitly sets a key to null, that key is deleted from the resulting configuration.

Validation and Environment Handling

After each layer loads, the resolver enforces strict validation rules:

  • Environment Pattern Linting – The lintEnvPatterns function (in env-interpolator.js) ensures that only L4 (Local) configuration files may contain ${VAR} placeholders. Attempting to use environment variable interpolation in L1, L2, or L3 triggers a validation warning.
  • JSON-Schema Validation – Each layer has its own schema file (framework-config.schema.json, project-config.schema.json, local-config.schema.json, user-config.schema.json). The validateConfig function checks layer-specific constraints and collects warnings in result.warnings, accessible via the CLI command aios config validate --level L4.

When options.debug === true, loadLayeredConfig() builds a sources map that records which specific layer (L1-L4) supplied each configuration key, enabling precise provenance tracking via aios config show --debug.

Practical Implementation Example

To resolve a project's effective configuration with full provenance tracking:

const { resolveConfig } = require('.aios-core/core/config/config-resolver');

// Resolve a project's final config (debug mode shows provenance)
const { config, sources, warnings } = resolveConfig('/path/to/my-project', {
  debug: true,
  appDir: '/path/to/my-project/apps/dashboard',
});

console.log('Effective config →', config);
console.log('Where each key came from →', sources);
if (warnings.length) console.warn('Config warnings:', warnings);

Step-by-step override example:

  1. L1 (Framework) supplies default metadata.framework_name = "AIOS‑FullStack" and performance_defaults.max_concurrent_operations = 4.

  2. L2 (Project) overrides the performance setting: performance_defaults.max_concurrent_operations = 8.

  3. L4 (Local) provides developer-specific overrides:

    performance_defaults:
      max_concurrent_operations: 16   # L4 overrides L2
    
    ide:
      selected:
        - vscode
        - claude-code
  4. Final merged result: max_concurrent_operations = 16 (from L4), metadata.framework_name = "AIOS‑FullStack" (from L1), and the IDE selection from L4, with all other L1/L2 values preserved.

Key Source Files for the L1-L4 System

File Role Link
config-resolver.js Detects legacy vs layered mode, orchestrates loading, validation, and source tracking 🔗
merge-utils.js Implements the deep-merge algorithm used for inheritance 🔗
config-cache.js TTL-based cache for already-resolved configs (optimises repeated calls) 🔗
env-interpolator.js Handles ${VAR} interpolation and linting of env patterns 🔗
schemas/framework-config.schema.json JSON-Schema for L1 🔗
schemas/project-config.schema.json JSON-Schema for L2 🔗
schemas/local-config.schema.json JSON-Schema for L4 (local) 🔗
tests/config/fixtures/local-config.yaml Example L4 file used in the test suite 🔗

Summary

  • The L1-L4 layered configuration system in AIOS Pro provides a deterministic five-level hierarchy (L1 Framework → L2 Project → Pro → L3 App → L4 Local) where each layer overwrites the previous according to deep-merge rules.
  • L1 contains read-only framework defaults, L2 stores team-wide project settings, L3 handles optional per-app customizations, and L4 holds git-ignored, developer-specific overrides including secrets.
  • The resolveConfig() function in config-resolver.js orchestrates loading, while merge-utils.js implements the inheritance logic: scalars use last-wins, objects deep-merge, arrays replace (unless using +append), and null deletes keys.
  • Environment variable interpolation is restricted to L4 only, enforced by env-interpolator.js, and each layer undergoes JSON-Schema validation to ensure configuration integrity.

Frequently Asked Questions

What happens if L4 local-config.yaml conflicts with L2 project-config.yaml?

When the same key exists in both files, L4 always wins. According to the merge logic in merge-utils.js, the deterministic loading order (L1 → L2 → Pro → L3 → L4) means that L4's values overwrite any previous layer's settings for scalar values, while nested objects are deep-merged with L4's properties taking precedence. This ensures that developer-specific overrides in .aios-core/local-config.yaml (which is git-ignored) can safely supersede team-wide policies without affecting other team members.

Can I use environment variables in AIOS Pro configuration files?

Only in L4 (Local) configuration. The env-interpolator.js module enforces a strict linting rule via lintEnvPatterns that restricts ${VAR} interpolation to .aios-core/local-config.yaml. If you attempt to use environment variable placeholders in L1 (Framework), L2 (Project), or L3 (App) configs, the validator will emit warnings. This security measure ensures that secrets and machine-specific values stay out of source control while allowing developers to reference local environment variables for personal tooling and API tokens.

How do I debug which configuration layer is providing a specific value?

Enable debug mode in the resolveConfig() options. When you call resolveConfig(projectRoot, { debug: true }), the loadLayeredConfig() function in config-resolver.js builds a sources map that records the provenance of every configuration key. This map indicates whether each value originated from L1, L2, L3, or L4. You can also use the CLI command aios config show --debug to inspect the effective configuration alongside its source layers, making it easy to trace why a particular setting has its current value.

Is the L3 App layer required for all AIOS Pro projects?

No, L3 is optional. The App layer (aios-app.config.yaml) is only loaded when the options.appDir parameter is explicitly supplied to resolveConfig(). This design supports micro-frontend or micro-service architectures where individual applications within a monorepo need distinct UI themes or feature flags. If you are building a single-application project or do not specify an app directory, the configuration resolver skips L3 entirely and proceeds directly from the Pro layer to L4 (Local), simplifying the inheritance chain.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →