Relationships
#684 sensitive-arg guard rejects an all-vault.get() record/map as a "literal", blocking definition migration
Opened by anthony · 6/18/2026· Shipped 6/19/2026
Summary
A sensitive global argument declared as a record/map —
z.record(z.string(), z.string()).meta({ sensitive: true }) — whose every value
is a ${{ vault.get(...) }} expression is rejected by the sensitive-literal
guard as if it were a cleartext literal. The same single-expression value is
accepted on a z.string().meta({ sensitive: true }) field, so the rejection is
purely about the value being map-shaped.
Because the guard runs at the definition save chokepoint, this blocks any
definition write — and in particular it blocks swamp model method run whenever
the run triggers a type-version migration re-save (i.e. the instance's
typeVersion is behind the live type version and the model defines an upgrades
chain). The migration re-reads the on-disk definition (raw vault.get() map
intact), applies the upgrade, and saves it — and that save throws. The aborted
method need not consume the secret at all.
Root cause
src/domain/models/sensitive_field_extractor.ts:
extractSensitiveFields()flags the record field as sensitive (path = the field name) but does not recurse into arecordshape (it only walksobjectshapes), so the per-value expressions are never inspected.findLiteralSensitiveGlobalArgs()then callsisLiteralSecret()on the whole map value.isLiteralSecret()returnstruefor any non-string value (a documented, deliberate fail-closed choice), so an all-vault.get()map is classified as a cleartext literal even though it carries no cleartext.
Save chokepoint: src/infrastructure/persistence/yaml_definition_repository.ts
(findLiteralSensitiveGlobalArgs → throw). Migration re-save on a method run:
src/domain/models/method_execution_service.ts (the DefinitionUpgradeService
block that persists the upgraded definition).
Reproduction
- A model type whose
globalArgumentsincludesecrets: z.record(z.string(), z.string()).meta({ sensitive: true }), and which defines anupgradeschain. - A persisted instance carrying
secrets: { MY_KEY: "${{ vault.get('my-vault', 'my-key') }}" }, pinned to atypeVersionbehind the type's currentversion. swamp model method run <instance> <any-method>→ the migration re-save hits the guard and aborts with: "Global argument 'secrets' is marked sensitive and cannot be set to a literal value — it would be stored in cleartext in the definition YAML…"
With the instance's typeVersion matched to the live version (no migration, no
re-save) the same method runs fine with the same secrets block in place —
confirming the map value itself is acceptable and only the save-time guard
rejects it.
Expected
A sensitive container (record/array) whose leaves are all expression-only
(e.g. vault.get(...)) should be accepted, exactly as a single
${{ vault.get(...) }} string is — it carries no cleartext.
Proposed fix (behaviour-preserving)
Make isLiteralSecret() recurse into containers; keep bare non-string scalars
fail-closed (so the existing number/boolean literal tests stay green):
function isLiteralSecret(value: unknown): boolean {
if (value === undefined || value === null) return false;
if (typeof value === "string") {
if (value.trim() === "") return false;
return !isExpressionOnly(value);
}
// An array/plain-object is a literal secret iff ANY contained value is one.
// An all-`vault.get(...)` map carries no cleartext; an empty container carries
// no secret. Bare non-string scalars (number/boolean) stay fail-closed.
if (Array.isArray(value)) return value.some(isLiteralSecret);
if (typeof value === "object" &&
(Object.getPrototypeOf(value) === Object.prototype ||
Object.getPrototypeOf(value) === null)) {
return Object.values(value as Record<string, unknown>).some(isLiteralSecret);
}
return true;
}Suggested tests: an all-vault.get() record (and array) returns [] from
findLiteralSensitiveGlobalArgs; a record containing one literal string — or a
number — is still flagged.
Related residual (separate, lower priority)
extractSensitiveFieldValues() skips object values, so the resolved secret
values inside a sensitive map are never registered with the log redactor.
Independent of this guard bug, but worth handling in the same area (recurse into
record/array values there too).
Environment
swamp 20260616.195738.0-sha.2c8bba58; guard confirmed unchanged on main
(source fetched 2026-06-18). Reproduced on Linux.
Shipped
Click a lifecycle step above to view its details.