Appearance
Package config contract
A package's configuration is layered. There are defaults baked into the package, profile overlays, environment-variable overlays, file overlays, and system.config service overlays. resolveConfiguredPackageConfig walks them in order and returns the merged result. This page documents the contract.
The config block
json
"config": {
"schema": "./config/config.schema.json",
"defaults": "./config/config.defaults.json",
"envPrefix": "MATRIX_CHAT_",
"providers": [
{ "kind": "env", "prefix": "MATRIX_CHAT_" },
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true },
{ "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "env": "MATRIX_CHAT_CONFIG_SERVICE", "optional": true }
]
}| Field | Meaning |
|---|---|
schema | Path (package-relative) to a JSON Schema describing valid config. Used by tooling for validation, autocompletion, config explain. |
defaults | Path to a JSON file containing the package's default config. The starting point for layering. |
envPrefix | If providers is omitted, envPrefix implies a single env-provider with that prefix. Convenience for simple cases. |
providers[] | Ordered list of provider overlays applied on top of defaults. Later providers win for the same key. |
The interface lives at projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:20-25.
Provider kinds
env — environment variables
json
{ "kind": "env", "prefix": "MATRIX_CHAT_" }Walks process.env, picks up every key starting with prefix, and translates the suffix into a deep path:
| Env var | Path | Notes |
|---|---|---|
MATRIX_CHAT_LOG_LEVEL=debug | logLevel | Single segment. |
MATRIX_CHAT_AUTH__PROVIDER=google | auth.provider | __ is the separator between segments. |
MATRIX_CHAT_LIMITS__MAX_MESSAGES=100 | limits.maxMessages | Camel-cased automatically. |
MATRIX_CHAT_TIMEOUT_MS=5000 | timeoutMs | Number parsed if value matches /^-?\d+(\.\d+)?$/. |
MATRIX_CHAT_FEATURES={"x":1} | features | JSON parsed if the value starts with { or [. |
MATRIX_CHAT_DEBUG=true | debug | true/false/null parsed as booleans/null. |
The translation lives at PackageConfigResolver.ts:135-153 (resolveEnvOverlay) and :319-327 (envSegmentToPathSegment).
You can also point an env-provider at a single JSON object:
json
{ "kind": "env", "env": "MATRIX_CHAT_CONFIG" }MATRIX_CHAT_CONFIG='{"logLevel":"debug","auth":{"provider":"google"}}' becomes the overlay verbatim.
file — JSON file pointed to by an env var
json
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true }Reads the path from process.env[provider.env], calls options.loadFileConfig(path), and uses the parsed object as an overlay. optional: true means "no env var set ⇒ skip this provider"; optional: false and missing file is an error.
The caller (typically the runner) supplies loadFileConfig; the resolver itself does no file I/O.
You can also use a literal path:
json
{ "kind": "file", "path": "./config.local.json", "optional": true }service — fetched from system.config
json
{ "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "optional": true }Calls system.config.listForMount and system.config.getEffective for every key under the namespace. Used when configuration is centrally managed and pushed at runtime (per-user overrides, per-Space defaults, etc.).
Each getEffective call may include realm from options.realm — useful when configuration varies per authority root.
The custom resolver hook options.loadServiceConfig lets the caller intercept service lookups (testing, alternative providers).
Default order
resolveConfiguredPackageConfig (PackageConfigResolver.ts:88-105) layers in this exact order:
defaults(the JSON loaded fromconfig.defaults).profile(an explicit overlay passed by the caller — e.g., a "dev" profile baked into the runtime).- For each provider in
config.providers, in declaration order:- resolve overlay; deep-merge into the working result.
overrides(an explicit overlay the caller layers last — e.g., test overrides, CLI flags).
Later layers win on conflict; deep-merge means nested objects are merged recursively, but arrays and primitives are replaced wholesale.
If providers is omitted but envPrefix is set, a single { kind: 'env', prefix: envPrefix } is implied (PackageConfigResolver.ts:107-117).
Worked example
json
"config": {
"schema": "./config/config.schema.json",
"defaults": "./config/config.defaults.json",
"providers": [
{ "kind": "env", "prefix": "MATRIX_CHAT_" },
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true },
{ "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "optional": true }
]
}config.defaults.json:
json
{ "logLevel": "info", "auth": { "provider": "anthropic" }, "limits": { "maxMessages": 50 } }Environment:
MATRIX_CHAT_LOG_LEVEL=debug
MATRIX_CHAT_LIMITS__MAX_MESSAGES=200File (MATRIX_CHAT_CONFIG_FILE=/etc/matrix/chat.json):
json
{ "auth": { "provider": "google" } }Service (system.config.getEffective for @open-matrix/chat):
json
{ "config": [
{ "key": "limits.timeoutMs", "value": 30000 }
] }Resolution:
- Start:
{ logLevel: "info", auth: { provider: "anthropic" }, limits: { maxMessages: 50 } }. - Env overlay:
logLevel: "debug",limits.maxMessages: 200→{ logLevel: "debug", auth: { provider: "anthropic" }, limits: { maxMessages: 200 } }. - File overlay:
auth.provider: "google"→{ logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200 } }. - Service overlay:
limits.timeoutMs: 30000→{ logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200, timeoutMs: 30000 } }.
The resulting config is what your actor sees.
Calling the resolver
ts
import { resolveConfiguredPackageConfig } from '@open-matrix/core';
interface MyConfig {
logLevel: 'debug' | 'info' | 'warn';
auth: { provider: 'google' | 'anthropic' };
limits: { maxMessages: number; timeoutMs?: number };
}
const config = await resolveConfiguredPackageConfig<MyConfig>({
defaults,
contract: matrixJson.config,
packageName: '@open-matrix/chat',
options: {
context: matrixContext, // for service provider
env: process.env,
loadFileConfig: async (path) => JSON.parse(await fs.promises.readFile(path, 'utf8')),
timeoutMs: 5000,
},
overrides: cliFlagOverrides,
});The return type is your declared config type, fully populated. The resolver does not validate against config.schema — that is the caller's job (typically the runner or matrix config explain).
Schema and validation
The config.schema path points to a JSON Schema. The SDK does not validate at config-resolution time. Validation is applied by:
matrix config explain <package>— the CLI reads the schema and the resolved config, prints which key came from which layer, and validates.hivecast doctor— when run with--repair, checks each known package's resolved config against its schema.- Your actor — for safety, validate inside
onBootstrap(). The defaults file should already pass; only the provider overlays can introduce invalid values.
See also
- Package manifest — where the
configblock lives. - Configuration — the four-page deep dive on schema, defaults, overrides, secrets.
- Config schema, Defaults, Overrides.
Source:
projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:1-349(full resolver).projects/matrix-3/packages/chat/matrix.json:135-156(live config block).