feat(config): coerce whole-value ${VAR} JSON arrays/objects in env substitution#95603
feat(config): coerce whole-value ${VAR} JSON arrays/objects in env substitution#95603hcnode wants to merge 6 commits into
Conversation
|
Good catch! This looks like a valid bug report. |
|
Codex review: needs maintainer review before merge. Reviewed July 11, 2026, 2:07 PM ET / 18:07 UTC. Summary PR surface: Source +110, Tests +122, Docs +62. Total +294 across 7 files. Reproducibility: not applicable. current main's ordinary Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Keep the implementation design and proof available, but adopt it only after a maintainer explicitly sponsors the public config-language contract; a sponsored version should retain this shared parser/preservation path, focused tests, and public documentation. Do we have a high-confidence way to reproduce the issue? Not applicable: current main's ordinary Is this the best way to solve the issue? Yes in implementation shape: explicit whole-value array/object coercion is narrower and safer than implicit schema-blind parsing, but adopting the syntax requires maintainer product approval. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d4adddbe9c7a. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +110, Tests +122, Docs +62. Total +294 across 7 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
63f878e to
bc80063
Compare
resolveConfigEnvVars only ever returned strings, so a config value of
"${VAR}" resolved to the env var's raw text. Array/object config keys
(allowlists, id lists, structured limits) could not be injected via env
vars: the schema expected an array/object but received a string.
Add an explicit opt-in modifier ${VAR:json}: when a value is exactly one
${VAR:json} reference and the resolved text is a JSON array or object
literal, parse it into a real array/object. Plain ${VAR} always stays a
string, so existing string-typed fields (e.g. env.vars) are unaffected.
Embedded refs, multi-var strings, scalars, JSON primitives, malformed
JSON, and unknown modifiers all keep their string/literal result.
The token grammar preserves the verbatim braces content (`inner`) so
escaped $${VAR:json} stays literal and unresolved ${VAR:json}
placeholders keep the modifier in onMissing mode. The same `:json`
handling is mirrored in env-preserve's resolver.
Preserve authored ${VAR:json} references across config write-back:
restoreEnvVarRefs restores a sole ${VAR:json} template when the incoming
value deep-equals the env-resolved array/object, instead of inlining the
resolved structure into openclaw.json. Substitution and preservation
share the same token semantics to avoid grammar drift.
Adds tests for opt-in coercion, plain-${VAR} passthrough, escaped
$${VAR:json}, onMissing modifier preservation, and write-back round-trip.
bc80063 to
c344775
Compare
…fier
Adds guide + reference docs for the opt-in ${VAR:json} coercion: whole-value
opt-in, array/object-only parsing, string-field safety, escaping/missing/unknown
modifier behavior, and write-back preservation. Closes the ClawSweeper docs
blocker for the new config syntax.
|
Updated: added the public docs the review asked for, in both
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
This pull request has been automatically marked as stale due to inactivity. |
Closes #
What Problem This Solves
Resolves a problem where array or object config cannot be supplied through an environment variable. Config env substitution (
resolveConfigEnvVars) only ever returns strings, so a value written as"${VAR}"resolves to the env var's raw text. For config keys that expect an array or object (allowlists, id lists, structured limits), operators who want to keep that value out of the committed config file and inject it from the environment end up with a string where the schema expects an array/object, which fails validation.Why This Change Was Made
This adds an explicit opt-in modifier:
${VAR:json}. Only this form coerces — when a value is exactly one${VAR:json}reference and the resolved text is a JSON array or object literal, it is parsed into a real array/object. Plain${VAR}always stays a string (so existing string fields are never affected), and embedded references ("prefix-${VAR:json}"), multi-var strings, scalars, JSON primitives, and malformed JSON keep their string result. Unknown modifiers (${VAR:foo}) are left as literal placeholders.The earlier revision of this PR coerced any whole-value
${VAR}globally; review correctly flagged two compatibility problems, both now addressed:${VAR:json}, so string-typed fields such asenv.vars(z.record(z.string(), z.string())) keep their string value even when the env value looks like JSON.restoreEnvVarRefs(env-preserve.ts) now restores a sole${VAR:json}template when the incoming value is the env-resolved array/object, so the authored env reference survives config round-trips instead of being inlined intoopenclaw.json. The substitution and preservation paths share the same token helpers to avoid grammar drift.User Impact
Operators can inject array/object config through environment variables by opting in with
${VAR:json}. For example, withALLOW_FROM='["alice","bob"]', a config value of"${ALLOW_FROM:json}"resolves to the array["alice","bob"]. Plain${VAR}behavior, escaping ($${VAR}), and missing-var handling are unchanged, and authored${VAR:json}references are preserved across config writes. No migration is required.Evidence
Live terminal output from running the config loader (
resolveConfigEnvVars) and the write-back path (restoreEnvVarRefs) from source vianode, against a config file withallowFrom: "${ALLOW_FROM:json}"(opt-in) and a plain string-onlyenv.vars.TAGS: "${TAGS}", withALLOW_FROM='["alice","bob"]'andTAGS='["x","y"]'. Console output:This demonstrates all three guarantees: (1)
${ALLOW_FROM:json}coerces to a real array; (2) plain${TAGS}stays a string even though its value is JSON-looking (string fields are safe); (3) on write-back the authored${ALLOW_FROM:json}reference is restored rather than inlined.Focused tests:
src/config/env-substitution.test.tsandsrc/config/env-preserve.test.ts(92 passed total), covering opt-in coercion, plain-${VAR}passthrough, embedded/scalar/malformed/primitive passthrough, unknown-modifier literal passthrough, and${VAR:json}write-back round-trip (restore + caller-changed-value cases).pnpm tsgois clean.