Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/dashmate/configs/defaults/getBaseConfigFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export default function getBaseConfigFactory() {
context: path.join(PACKAGE_ROOT_DIR, '..', '..'),
dockerFile: path.join(PACKAGE_ROOT_DIR, '..', '..', 'Dockerfile'),
target: 'rs-dapi',
buildArgs: {},
},
},
metrics: {
Expand All @@ -293,6 +294,9 @@ export default function getBaseConfigFactory() {
context: path.join(PACKAGE_ROOT_DIR, '..', '..'),
dockerFile: path.join(PACKAGE_ROOT_DIR, '..', '..', 'Dockerfile'),
target: 'drive-abci',
// Extra Docker build args — see the `buildArgs` field on
// `dockerBuild` in the config schema.
buildArgs: {},
},
},
logs: {
Expand Down
11 changes: 11 additions & 0 deletions packages/dashmate/configs/getConfigFileMigrationsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -1433,6 +1433,17 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs)
.get('platform.drive.tenderdash.docker.image');
}

// Backfill the new `buildArgs: {}` field on each build block —
// forwarded into `dynamic-compose.yml` as `build.args` entries.
if (options.platform?.drive?.abci?.docker?.build
&& typeof options.platform.drive.abci.docker.build.buildArgs === 'undefined') {
options.platform.drive.abci.docker.build.buildArgs = {};
}
if (options.platform?.dapi?.rsDapi?.docker?.build
&& typeof options.platform.dapi.rsDapi.docker.build.buildArgs === 'undefined') {
options.platform.dapi.rsDapi.docker.build.buildArgs = {};
}

if (options.platform?.drive?.tenderdash?.p2p
&& typeof options.platform.drive.tenderdash.p2p.allowlistOnly === 'undefined') {
options.platform.drive.tenderdash.p2p.allowlistOnly = defaultConfig
Expand Down
5 changes: 4 additions & 1 deletion packages/dashmate/docker-compose.build.drive_abci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ services:
SCCACHE_BUCKET: ${SCCACHE_BUCKET}
SCCACHE_REGION: ${SCCACHE_REGION}
SCCACHE_S3_KEY_PREFIX: ${SCCACHE_S3_KEY_PREFIX}
SDK_TEST_DATA: ${SDK_TEST_DATA}
# Additional build args (SDK_TEST_DATA, CARGO_BUILD_PROFILE, …) are
# injected per-config by dashmate from
# `platform.drive.abci.docker.build.buildArgs`, rendered into
# `dynamic-compose.yml`, and merged with this file at compose time.
secrets:
- GITHUB_TOKEN
cache_from:
Expand Down
2 changes: 2 additions & 0 deletions packages/dashmate/docker-compose.build.rs-dapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ services:
SCCACHE_BUCKET: ${SCCACHE_BUCKET}
SCCACHE_REGION: ${SCCACHE_REGION}
SCCACHE_S3_KEY_PREFIX: ${SCCACHE_S3_KEY_PREFIX}
# Additional build args are injected per-config by dashmate from
# `platform.dapi.rsDapi.docker.build.buildArgs` via dynamic-compose.
secrets:
- GITHUB_TOKEN
cache_from:
Expand Down
14 changes: 12 additions & 2 deletions packages/dashmate/src/commands/config/set.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Args } from '@oclif/core';
import ConfigBaseCommand from '../../oclif/command/ConfigBaseCommand.js';
import Config from '../../config/Config.js';
import InvalidOptionPathError from '../../config/errors/InvalidOptionPathError.js';

export default class ConfigSetCommand extends ConfigBaseCommand {
static description = `Set config option
Expand Down Expand Up @@ -38,14 +40,22 @@
flags,
config,
) {
// check for existence
config.get(optionPath);
// Validate the path against the schema, not against the currently-set
// value. `config.get(...)` would throw `InvalidOptionPathError` for any
// key inside a map-shaped property (e.g. `…buildArgs.SDK_TEST_DATA`)
// because the value doesn't exist yet — that gate is the wrong shape for
// schemas that use `additionalProperties: <schema>` to model open maps.
// `Config.isSchemaPathAllowed` walks the schema and permits descent into
// both typed `properties` and `additionalProperties` value schemas.
if (!Config.isSchemaPathAllowed(optionPath)) {
throw new InvalidOptionPathError(optionPath);
}

let value;

try {
value = JSON.parse(optionValue);
} catch (e) {

Check warning on line 58 in packages/dashmate/src/commands/config/set.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

'e' is defined but never used
value = optionValue;
}

Expand Down
68 changes: 68 additions & 0 deletions packages/dashmate/src/config/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,74 @@
return lodashGet(this.options, path) !== undefined;
}

/**
* Check whether a path is reachable per the config JSON schema (regardless
* of whether a value is currently set there).
*
* Use this when checking the legality of a `set` to a path that doesn't yet
* have a value — notably under map-shaped properties whose schema uses
* `additionalProperties: <schema>` (e.g. `…build.buildArgs.<KEY>`), where
* `config.has(...)` will return `false` even though `config.set(...)` is
* semantically legal.
*
* `configJsonSchema` IS the per-config schema — the top-level
* `properties: { description, group, docker, core, platform, … }` describes
* one config entry. Walks it descending through:
* - `properties[segment]` (typed field),
* - `additionalProperties` (variable-key map, only when the value is a
* schema object — `additionalProperties: false` blocks the descent),
* - `$ref` references into `#/definitions/...`.
*
* @param {string} path - dot-separated option path (e.g.
* `'platform.drive.abci.docker.build.buildArgs.SDK_TEST_DATA'`).
* @return {boolean} true when the path is allowed by the schema.
*/
static isSchemaPathAllowed(path) {
if (typeof path !== 'string' || path.length === 0) return false;

Check warning on line 71 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition

Comment on lines +70 to +72
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject dot-paths with empty segments.

At Line 90, path.split('.') allows empty segments (e.g., trailing dot), which can make invalid keys schema-allowed under additionalProperties. Add an explicit empty-segment check.

Suggested fix
   static isSchemaPathAllowed(path) {
     if (typeof path !== 'string' || path.length === 0) return false;
+    const segments = path.split('.');
+    if (segments.some((segment) => segment.length === 0)) return false;
 
@@
-    for (const segment of path.split('.')) {
+    for (const segment of segments) {

Also applies to: 90-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashmate/src/config/Config.js` around lines 70 - 72, In
isSchemaPathAllowed, add a validation after splitting the dot-path that rejects
any empty segments so paths like "foo." or "a..b" return false; locate the split
operation (path.split('.')) inside the isSchemaPathAllowed method and ensure you
check every segment for === '' (or length === 0) and return false if any are
empty before continuing the existing allowed-key checks.

const resolveRef = (node) => {
if (!node || typeof node !== 'object') return node;

Check warning on line 74 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition
if (typeof node.$ref !== 'string') return node;

Check warning on line 75 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition
const ref = node.$ref;
if (!ref.startsWith('#/')) return null;

Check warning on line 77 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition
const segments = ref.slice(2).split('/');
let resolved = configJsonSchema;
for (const seg of segments) {
if (!resolved || typeof resolved !== 'object') return null;

Check warning on line 81 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition
resolved = resolved[seg];
}
return resolveRef(resolved);
};

let node = resolveRef(configJsonSchema);
if (!node) return false;

Check warning on line 88 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition

for (const segment of path.split('.')) {
node = resolveRef(node);
if (!node || typeof node !== 'object') return false;

Check warning on line 92 in packages/dashmate/src/config/Config.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

Expected { after 'if' condition

// Typed property.
if (node.properties && Object.prototype.hasOwnProperty.call(node.properties, segment)) {
node = node.properties[segment];
continue;
}

// Map with a schema for extra keys — descend into the value schema.
if (
node.additionalProperties
&& typeof node.additionalProperties === 'object'
) {
node = node.additionalProperties;
continue;
}

// No match and no permissive additionalProperties — path not allowed.
return false;
}

return true;
}

/**
* Get config option
*
Expand Down
9 changes: 9 additions & 0 deletions packages/dashmate/src/config/configJsonSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export default {
target: {
type: ['string', 'null'],
},
// Extra build args forwarded to `docker compose build` for this
// image. Each key/value pair becomes a `build.args` entry rendered
// into the per-config `dynamic-compose.yml` and picked up by
// compose at build time. Open-ended — image-specific keys live
// here (`CARGO_BUILD_PROFILE`, `SDK_TEST_DATA`, …).
buildArgs: {
type: 'object',
additionalProperties: { type: 'string' },
},
Comment on lines +40 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain buildArgs keys to valid env-var identifiers.

At Line 49, buildArgs currently accepts any key, including empty/invalid names that won’t map reliably to ${NAME} substitutions. Please validate key names in schema to prevent silent misconfiguration.

Suggested schema hardening
         buildArgs: {
           type: 'object',
+          propertyNames: {
+            type: 'string',
+            pattern: '^[A-Z_][A-Z0-9_]*$',
+          },
           additionalProperties: { type: 'string' },
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
buildArgs: {
type: 'object',
additionalProperties: { type: 'string' },
},
buildArgs: {
type: 'object',
propertyNames: {
type: 'string',
pattern: '^[A-Z_][A-Z0-9_]*$',
},
additionalProperties: { type: 'string' },
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashmate/src/config/configJsonSchema.js` around lines 49 - 52, The
buildArgs object currently allows any property name (via additionalProperties)
so invalid or empty keys can slip through; update the schema for buildArgs to
restrict property names to valid environment-variable identifiers by replacing
additionalProperties with patternProperties using a regex like
^[A-Z_][A-Z0-9_]*$ for keys and keep the value type as string, and set
additionalProperties: false to reject any names that don't match; target the
buildArgs schema block to apply this change.

},
required: ['enabled', 'context', 'dockerFile', 'target'],
additionalProperties: false,
Expand Down
20 changes: 19 additions & 1 deletion packages/dashmate/templates/dynamic-compose.yml.dot
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ services:
{{?}}

{{ driveLogs = Object.entries(it.platform.drive.abci.logs).filter(([, settings]) => settings.destination !== 'stderr' && settings.destination !== 'stdout'); }}
{{? driveLogs.length > 0 }}
{{ driveBuildArgs = Object.entries((it.platform.drive.abci.docker.build && it.platform.drive.abci.docker.build.buildArgs) || {}); }}
{{? driveLogs.length > 0 || driveBuildArgs.length > 0 }}
drive_abci:
{{? driveLogs.length > 0 }}
volumes:
{{~ driveLogs :logger }}
{{ [name, settings] = logger; }}
- {{=settings.destination}}:/var/log/dash/drive/{{=name}}/{{=settings.destination.split('/').reverse()[0]}}
{{~}}
{{?}}
{{? driveBuildArgs.length > 0 }}
build:
args:
{{~ driveBuildArgs :pair }}
{{=pair[0]}}: "{{=pair[1]}}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: buildArgs values are interpolated into YAML without escaping

Lines 30 and 58 render each pair as {{=pair[0]}}: "{{=pair[1]}}". doT's {{=...}} is raw interpolation and the schema only constrains values to type: 'string' with no character restriction, so a value containing ", \, or a newline produces invalid or altered YAML that fails the next dashmate render with a confusing parse error rather than at config set time. The PR opens this surface as a public, open-ended Record<string, string> API, so worth either YAML-escaping (single-quoted form with doubled single quotes) or constraining values via a safe-character regex in the schema.

source: ['claude', 'codex']

{{~}}
{{?}}
{{?}}

{{? it.platform.drive.tenderdash.log.path !== null }}
Expand All @@ -29,6 +39,7 @@ services:
{{?}}

{{? it.platform.dapi && it.platform.dapi.rsDapi }}
{{ rsDapiBuildArgs = Object.entries((it.platform.dapi.rsDapi.docker.build && it.platform.dapi.rsDapi.docker.build.buildArgs) || {}); }}
rs_dapi:
expose:
- 3009
Expand All @@ -40,6 +51,13 @@ services:
ports:
- {{=it.platform.dapi.rsDapi.metrics.host}}:{{=it.platform.dapi.rsDapi.metrics.port}}:{{=it.platform.dapi.rsDapi.metrics.port}}
{{?}}
{{? rsDapiBuildArgs.length > 0 }}
build:
args:
{{~ rsDapiBuildArgs :pair }}
{{=pair[0]}}: "{{=pair[1]}}"
{{~}}
{{?}}
{{?}}

{{ gatewayLogs = it.platform.gateway.log.accessLogs.filter((l) => l.type === 'file'); }}
Expand Down
143 changes: 143 additions & 0 deletions packages/dashmate/test/unit/config/Config.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { expect } from 'chai';
import Config from '../../../src/config/Config.js';

describe('Config', () => {
describe('.isSchemaPathAllowed', () => {
// The bug that triggered this method: `dashmate config set
// platform.drive.abci.docker.build.buildArgs.SDK_TEST_DATA true`
// was failing because the old `config.get(path)` pre-check rejected
// any path whose value isn't already stored — including new keys
// inside map-shaped properties whose schema legally accepts them
// via `additionalProperties: <schema>`. Each test pins one slice
// of the schema-walk so a regression surfaces fast.

describe('typed properties', () => {
it('accepts a deeply-nested path that traverses only `properties`', () => {
expect(
Config.isSchemaPathAllowed('platform.drive.abci.docker.build.enabled'),
).to.be.true();
});

it('accepts a top-level property', () => {
expect(Config.isSchemaPathAllowed('network')).to.be.true();
});

it('rejects a top-level typo', () => {
// `platfom` (typo) is not in top-level `properties` and the schema
// has `additionalProperties: false`, so it must not be allowed.
expect(
Config.isSchemaPathAllowed('platfom.drive.abci.docker.build.enabled'),
).to.be.false();
});

it('rejects a typo in the middle of the path', () => {
expect(
Config.isSchemaPathAllowed('platform.drive.abc.docker.build.enabled'),
).to.be.false();
});
});

describe('map-shaped properties (additionalProperties: <schema>)', () => {
it('accepts a new key inside a value-keyed map', () => {
// The original failure. `buildArgs` is defined as
// `{ type: 'object', additionalProperties: { type: 'string' } }`
// so any key with a string value is schema-legal even if not yet set.
expect(
Config.isSchemaPathAllowed(
'platform.drive.abci.docker.build.buildArgs.SDK_TEST_DATA',
),
).to.be.true();
});

it('accepts an arbitrary key inside the map (the whole point)', () => {
expect(
Config.isSchemaPathAllowed(
'platform.drive.abci.docker.build.buildArgs.ANY_KEY_AT_ALL',
),
).to.be.true();
});

it('accepts the map property itself', () => {
expect(
Config.isSchemaPathAllowed('platform.drive.abci.docker.build.buildArgs'),
).to.be.true();
});
});

describe('$ref traversal', () => {
it('descends through a $ref to `#/definitions/dockerBuild`', () => {
// `platform.drive.abci.docker.build` resolves via $ref to the shared
// `dockerBuild` definition — buildArgs is defined there.
expect(
Config.isSchemaPathAllowed(
'platform.drive.abci.docker.build.buildArgs.X',
),
).to.be.true();
});

it('descends through a $ref for a sibling Rust build (rs-dapi)', () => {
expect(
Config.isSchemaPathAllowed(
'platform.dapi.rsDapi.docker.build.buildArgs.X',
),
).to.be.true();
});
});

describe('edge cases', () => {
it('rejects an empty path', () => {
expect(Config.isSchemaPathAllowed('')).to.be.false();
});

it('rejects a non-string path', () => {
expect(Config.isSchemaPathAllowed(null)).to.be.false();
expect(Config.isSchemaPathAllowed(undefined)).to.be.false();
expect(Config.isSchemaPathAllowed(42)).to.be.false();
});

it('rejects descending past a leaf primitive', () => {
// `network` is a string at top level; you cannot index further.
expect(Config.isSchemaPathAllowed('network.something')).to.be.false();
});
});
});

describe('regression: paths the buggy pre-check rejected are now permitted', () => {
// Before the fix, `dashmate config set` did a value-existence pre-check
// (`config.get(path)`) that threw `InvalidOptionPathError` for any path
// whose value wasn't already stored. That blocked legal sets under
// map-shaped properties (`additionalProperties: <schema>`). The fix
// replaced the pre-check with `isSchemaPathAllowed`. These tests pin
// each path the original failure surfaced through.

it('permits the original failing path (`…buildArgs.SDK_TEST_DATA`)', () => {
expect(
Config.isSchemaPathAllowed(
'platform.drive.abci.docker.build.buildArgs.SDK_TEST_DATA',
),
).to.be.true();
});

it('permits the same shape for rs-dapi (parallel Rust build)', () => {
expect(
Config.isSchemaPathAllowed(
'platform.dapi.rsDapi.docker.build.buildArgs.CARGO_BUILD_PROFILE',
),
).to.be.true();
});

it('still permits the canonical typed paths that the pre-check used to handle', () => {
// Sanity: paths whose values DO exist after `dashmate setup local` —
// the original pre-check used to gate these via `config.get`. The
// schema walker must accept them too, or the CLI breaks for everyone.
for (const path of [
'platform.drive.abci.docker.build.enabled',
'platform.drive.abci.docker.image',
'platform.dapi.rsDapi.docker.build.enabled',
'core.insight.enabled',
]) {
expect(Config.isSchemaPathAllowed(path), `path: ${path}`).to.be.true();
}
});
});
});
Loading
Loading