Skip to content

settings: implement print/get/set/reset + wire the centrs.env config tier - #159

Merged
mobileskyfi merged 8 commits into
mainfrom
settings-command
Jul 3, 2026
Merged

settings: implement print/get/set/reset + wire the centrs.env config tier#159
mobileskyfi merged 8 commits into
mainfrom
settings-command

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #135.

  • Wires the missing config precedence tier (default < config < comment-kv < env < cli) into the shared resolver — src/resolver/config-file.ts loads centrs.env, and resolveStringSetting/resolveBooleanSetting/resolveOptionalIntegerSetting gain a config argument threaded through retrieve/execute/api/transfer/terminal and their fan-outs, resolveTarget/resolveAuth, resolveCdb/expandCdbSelection, and mcp/config.ts. This was the real prerequisite the issue's own follow-up comment called out — without it, settings print would have had nothing truthful to report.
  • Implements centrs settings print/get/set/reset (src/settings.ts + src/cli/settings.ts): a 13-key managed registry with full validation, a 5-key refused list (credential-shaped/self-referential keys), an atomic centrs.env writer that preserves hand-edited comments/foreign lines byte-for-byte, boolean canonicalization to 1/0 on disk, and a read-only __default__ CDB probe on print.
  • 4 new error/warning codes (settings/reserved-key, settings/unknown-key, settings/consequential-value, settings/skip-env-file-active) plus internal/settings-failed, each cataloged and documented per the error-pages contract; the 6 pre-existing settings/* stub pages hand-enriched.
  • docs/MATRIX.md and commands/settings/README.md flipped to CHR-passed.

Deliberately out of scope (see "New issue" below): the interactive TTY first-time-setup flow. Bare centrs settings behaves like print regardless of TTY for now.

Test plan

  • bun run lint && bun run test && bun run build — 1042 unit tests, 0 fail
  • bun run test:integration against real CHR 7.23.1 — 121 tests, 0 fail across all 25 integration files (confirms the resolver threading didn't regress any transport-backed command)
  • test/integration/settings.test.ts — all 32 examples in commands/settings/examples.md, 1:1
  • test/unit/settings-registry.test.ts / settings-file.test.ts — exhaustive per-key validation + atomic-write mechanics
  • Manual proof that centrs settings set format json changes a real retrieve() resolution's output, not just settings print's own report

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new settings command for viewing, updating, and resetting app settings.
    • Settings now follow clearer precedence rules, including values from the user config file.
  • Bug Fixes

    • Improved handling of reserved, unknown, and invalid setting values.
    • Added safer redaction for secret-like settings and clearer warnings for risky values.
  • Documentation

    • Expanded settings and error documentation with usage details, defaults, and examples.
    • Updated release/status notes to reflect the new settings behavior.

Reviewed spec baseline for issue #135 before any code lands, so later
implementation commits can be diffed against a stable, agreed spec:
merges the earlier managed/unmanaged key split into one fully-validated
tier, corrects config-vs-comment-kv precedence direction, adds the
missing config-layer provenance prerequisite, and adds 32 numbered
examples mirroring commands/devices/examples.md.
Implements the prerequisite issue #135 requires before `settings` can
have anything truthful to report: `default < config < comment-kv <
env < cli` was documented in the constitution and typed in
SettingSourceKind, but no code ever loaded centrs.env or produced a
"config" source.

- New src/resolver/config-file.ts: XDG-path resolver (injectable env,
  mirroring defaultCdbPath's pattern), a raw file reader for settings'
  own always-read-the-file needs, and loadEnvFileDefaults() which
  every other command calls (honoring CENTRS_SKIP_ENV_FILE).
- resolveStringSetting/resolveBooleanSetting/resolveOptionalIntegerSetting
  gain a config argument, checked between comment-kv and default.
- Threaded through retrieve/execute/api/transfer/terminal and their
  fan-out siblings, resolver/target.ts (host/port/ssh-key),
  resolver/mac.ts (MAC-detection host candidate, kept in sync with
  resolveTarget's own ladder), resolveCdb/expandCdbSelection
  (CENTRS_CDB_FILE), and mcp/config.ts (CENTRS_CDB_FILE,
  CENTRS_MCP_ALLOW_ADHOC).
- CENTRS_USERNAME/CENTRS_PASSWORD/CENTRS_CDB_PASSWORD deliberately
  never read the config tier, even if hand-added to the file — a
  stronger defense than read-time redaction alone.
- Fan-out via-pinning guards (retrieve/execute/api) deliberately
  exclude the config tier: a centrs.env default is the weakest
  precedence layer and must not override per-device comment-kv `via=`
  across a mixed fan-out.

Not yet built: the settings CLI command itself that reads/writes this
file (next commit).
Builds src/settings.ts (managed-key registry for the 13 real CENTRS_*
keys, refused-key list, atomic centrs.env writer, __default__ CDB
probe) and src/cli/settings.ts, wired into src/cli.ts + src/index.ts.

- print/get resolve env > config > default, never cli/comment-kv (no
  call context exists for a bare `settings` invocation).
- set validates per-key, canonicalizes booleans to 1/0 on disk
  (CENTRS_MCP_ALLOW_ADHOC's strict "1" reader), warns (not blocks) on
  consequential values (insecure=true, transfer-via=ftp), never
  partially writes on a validation failure.
- reset deletes the line rather than blanking it (a blank value would
  be "set to empty string" for a bash-sourced file); reset with no
  <attr> clears every managed line, leaving unrecognized/foreign lines
  and comments untouched.
- Four new error/warning codes (settings/reserved-key,
  settings/unknown-key, settings/consequential-value,
  settings/skip-env-file-active) plus internal/settings-failed, each
  cataloged and documented per the error-pages contract; the 6
  pre-existing settings/* stub pages hand-enriched to match.

Bare `centrs settings` behaves like `print` regardless of TTY —
interactive first-time setup is an explicitly deferred later slice.
test/integration/settings.test.ts exercises every numbered example in
commands/settings/examples.md 1:1 (print/get/set/reset, --all,
--skip-env-file, the __default__ CDB boundary), in-process via
runCli + console capture, mirroring devices.test.ts's pattern —
no CHR needed since settings does no network IO.

test/unit/settings-registry.test.ts and settings-file.test.ts add
function-level coverage beyond the 32 examples: exhaustive per-key
validation (all 13 managed keys' valid/invalid values), attr-name
normalization, refused-key read/write behavior, and atomic-write
mechanics (.bak creation, line preservation, reset-deletes-not-blanks,
no-trailing-newline round-tripping).

Fixed a real CLI parsing bug surfaced by testing example 20: a
negative-integer value like `settings set max-results -1` was
misparsed as an unrecognized flag rather than a positional value.

package.json: settings.test.ts joins the fixture-only fast tier
(test:integration:fast) alongside devices.test.ts; adds a dedicated
test:integration:settings script.
docs/MATRIX.md's settings pointer now matches devices' fixture-backed
CHR-passed language. commands/settings/README.md's Status line, the
config-layer/testability sections, and the error-codes list are
rewritten past-tense to describe what's actually built rather than
what needs to land — the interactive TTY slice remains explicitly
deferred (still not built). examples.md drops its now-resolved
"prerequisite not yet built" callout. docs/errors/README.md notes
src/settings.ts as a second settings/* code source.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 40261703-1e4b-4446-8e59-3cc8835a5eb9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a new centrs settings command (print/get/set/reset) with a managed/refused key registry, atomic file I/O, CLI surface, and documentation/tests. It also introduces a centrs.env config-file precedence tier, threading a loaded config map through devices, target, mac, cdb, and all command resolvers (api, execute, retrieve, transfer, terminal, mcp).

Changes

Settings Command Feature

Layer / File(s) Summary
Settings module: envelopes, registry, file I/O, rendering
src/settings.ts, src/core/error-catalog.ts, src/index.ts
New settings command implementation with managed/refused key registries, atomic centrs.env read/write, print/get/set/reset logic, text/json/yaml rendering, new error catalog codes, and public re-exports.
CLI command routing and argument parsing
src/cli/settings.ts, src/cli.ts
Adds the settings CLI subcommand with flag/arg parsing, help output, and dispatch/error rendering, wired into the top-level CLI router.
Settings documentation and error pages
commands/settings/README.md, commands/settings/examples.md, docs/errors/settings/*, docs/errors/internal/settings-failed.md, docs/errors/README.md, docs/MATRIX.md, README.md, commands/devices/README.md, package.json
Expands settings command spec and examples, populates settings error pages, updates precedence wording and status pointers, and adds new integration test scripts.
Settings unit and integration tests
test/integration/settings.test.ts, test/unit/settings-*.test.ts
Adds integration and unit tests for CLI behavior, file mechanics, and registry validation.

Estimated code review effort: 4 (Complex) | ~75 minutes

Config-file precedence tier wiring

Layer / File(s) Summary
Config-file loader and resolver precedence update
src/resolver/config-file.ts, src/resolver/index.ts, src/resolver/settings.ts, test/unit/resolver.test.ts, test/unit/settings-config-file.test.ts
Adds loadEnvFileDefaults and related helpers to read/parse centrs.env, and inserts a "config" precedence layer into string/boolean/integer setting resolvers.
CDB, devices, selection, target, mac, mcp resolver wiring
src/resolver/cdb.ts, src/devices.ts, src/resolver/selection.ts, src/resolver/target.ts, src/resolver/mac.ts, src/mcp/config.ts, src/cli/mcp.ts
Adds optional config parameters to CDB/device/target/mac/mcp resolvers so config values participate in CDB file, host, port, ssh-key, and MCP default resolution.
API command config threading
src/api.ts, src/api-fanout.ts
Loads env-file defaults once and threads them through API request resolution and fan-out expansion/per-target resolution.
Execute command config threading
src/execute.ts, src/execute-fanout.ts
Loads env-file defaults and threads them through execute request/global-context resolution and fan-out expansion/per-target resolution.
Retrieve command config threading
src/retrieve.ts, src/retrieve-fanout.ts
Loads env-file defaults and threads them through retrieve request/global-context resolution and fan-out expansion/per-target resolution.
Transfer, transfer fanout, and terminal config threading
src/transfer.ts, src/transfer-fanout.ts, src/terminal.ts
Loads env-file defaults and threads them through transfer/terminal request resolution and transfer fan-out expansion.

Estimated code review effort: 4 (Complex) | ~80 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as runSettingsCli
  participant Settings as settingsSet
  participant Registry as settingsManagedKeys
  participant File as centrs.env

  User->>CLI: centrs settings set <attr> <value>
  CLI->>Settings: settingsSet({attr, value, env})
  Settings->>Registry: lookupAttr / parse(value)
  Registry-->>Settings: validated value or error
  Settings->>File: read existing lines
  Settings->>File: write CENTRS_*=value (temp+fsync+rename)
  Settings-->>CLI: SettingsSuccessEnvelope
  CLI-->>User: rendered text/json/yaml output
Loading
sequenceDiagram
  participant Command as resolveXRequest
  participant ConfigLoader as loadEnvFileDefaults
  participant Resolver as resolveStringSetting
  participant Target as resolveTarget/resolveAuth

  Command->>ConfigLoader: loadEnvFileDefaults(env)
  ConfigLoader-->>Command: config map (centrs.env)
  Command->>Resolver: resolve setting(env, commentKv, config)
  Resolver-->>Command: value + source (cli/env/comment-kv/config/default)
  Command->>Target: resolveTarget(..., config)
  Target-->>Command: resolved host/port/auth
Loading

Possibly related issues

Possibly related PRs

  • tikoci/centrs#11: Both PRs modify resolveTarget/resolveAuth in src/resolver/target.ts, with this PR threading the config tier through the same mac-telnet target-resolution code.
  • tikoci/centrs#110: Both PRs modify resolveApiRequest and related resolver functions in src/api.ts, extending the same API request resolution path.
  • tikoci/centrs#117: Both PRs modify the transfer-fanout implementation (TransferFanoutInternals.expand, selectTransferMethod), directly extending the same fan-out resolution code.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has a good summary and test plan, but it omits the template’s Links, Change type, and Notes sections. Add the required Links, Change type, and Notes sections, including linked spec/work items, the change-type checkbox, validation run, and RouterOS/protocol assumptions.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding settings commands and wiring the centrs.env config tier.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch settings-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/cli/settings.ts Dismissed
Comment thread src/cli/settings.ts Dismissed
Comment thread src/cli/settings.ts Dismissed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements the previously designed centrs settings command surface and wires the missing centrs.env “config” precedence tier into the shared resolver so that settings print/get reports match real resolution behavior across commands.

Changes:

  • Add centrs settings print|get|set|reset with a managed/refused key registry, validation, and atomic centrs.env editing.
  • Implement src/resolver/config-file.ts and thread a new config tier through core resolution paths (retrieve/execute/api/transfer/terminal, selection/CDB, and MCP config).
  • Add integration + unit tests for settings behavior and update docs/error catalog to match.

Reviewed changes

Copilot reviewed 46 out of 46 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/unit/settings-registry.test.ts Unit coverage for managed/refused registry + per-key validation behavior.
test/unit/settings-file.test.ts Unit coverage for centrs.env write/reset mechanics and preservation rules.
test/unit/settings-config-file.test.ts Unit coverage for config-file path resolution and parsing behavior.
test/unit/resolver.test.ts Updates precedence tests to include the new config tier.
test/integration/settings.test.ts Fixture-backed integration tests for the full settings CLI surface.
src/settings.ts New settings command implementation: registry, read/write, renderers, envelopes.
src/cli/settings.ts New CLI command wiring for centrs settings and flag/subcommand parsing.
src/resolver/config-file.ts New centrs.env loader for the resolver config tier.
src/resolver/settings.ts Add config tier to string/boolean/optional-integer resolver helpers.
src/resolver/index.ts Export config-file tier helpers from resolver index.
src/retrieve.ts Load and thread config into retrieve resolution paths.
src/retrieve-fanout.ts Load config once per invocation and thread through fanout resolution.
src/execute.ts Load/thread config and optionally accept preloaded config from fanout.
src/execute-fanout.ts Load config once and pass through member resolution.
src/api.ts Load/thread config and accept preloaded config for fanout.
src/api-fanout.ts Load config once and pass through member resolution.
src/transfer.ts Load/thread config into transfer resolution paths.
src/transfer-fanout.ts Load config once and pass into selection expansion.
src/terminal.ts Load/thread config into terminal resolution and resolve policy selection.
src/resolver/target.ts Thread config into host/port/ssh-key resolution (credentials intentionally excluded).
src/resolver/mac.ts Include config in host candidate resolution and MAC targeting inputs.
src/resolver/cdb.ts Treat CENTRS_CDB_FILE in config as “explicit” for missing-file semantics; thread config into device settings resolution.
src/resolver/selection.ts Thread config into selection expansion and explicit-CDB detection.
src/mcp/config.ts Allow MCP config resolution to consume config tier (kept sync).
src/devices.ts Allow devices settings resolution to read CENTRS_CDB_FILE from config tier.
src/cli/mcp.ts Load config tier for MCP CLI before resolving MCP config.
src/cli.ts Wire settings command into top-level CLI routing and help listing.
src/index.ts Export settings APIs/types from public entrypoint.
src/core/error-catalog.ts Register new settings/internal error codes.
README.md Update settings precedence description to name centrs.env user config.
commands/settings/README.md Promote settings command spec to CHR-passed and document final surface/behavior.
commands/settings/examples.md Add executable examples for settings command behavior.
docs/MATRIX.md Mark settings row CHR-passed with evidence pointer.
docs/errors/README.md Expand settings/* family description to include settings command rules.
docs/errors/internal/settings-failed.md New error page for internal settings failure.
docs/errors/settings/unknown-key.md New error page for unknown settings key.
docs/errors/settings/reserved-key.md New error page for refused/reserved keys.
docs/errors/settings/consequential-value.md New warning page for consequential-but-valid values.
docs/errors/settings/skip-env-file-active.md New warning page for print under skip-env-file.
docs/errors/settings/invalid-format.md Enrich existing format error page.
docs/errors/settings/invalid-boolean.md Enrich existing boolean error page.
docs/errors/settings/invalid-integer.md Enrich existing integer error page.
docs/errors/settings/invalid-timeout.md Enrich existing timeout error page.
docs/errors/settings/invalid-via.md Enrich existing via error page.
commands/devices/README.md Align precedence prose with the centrs.env config tier wording.
package.json Add integration test scripts for settings and include it in fast integration set.

Comment thread src/settings.ts
Comment on lines +453 to +465
const handle = await open(tempPath, "w");
try {
await handle.write(content);
await handle.sync();
} finally {
await handle.close();
}
try {
await rename(tempPath, path);
} catch (error) {
await unlink(tempPath).catch(() => undefined);
throw error;
}
Comment thread src/settings.ts Outdated
Comment on lines +574 to +577
const knownKeys = new Set<string>([
...settingsManagedKeys.map((def) => def.envKey),
...settingsRefusedKeys.map((def) => def.envKey),
]);
Comment on lines +116 to +125
export async function loadEnvFileDefaults(
env: Record<string, string | undefined> = Bun.env,
): Promise<Record<string, string>> {
if (isSkippingEnvFile(env)) {
return {};
}
const path = defaultSettingsPath(env);
const { lines } = await readSettingsFileRaw(path);
return parseEnvFileDefaults(lines);
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/resolver/target.ts (1)

82-98: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pass config through resolveTarget and resolveAuth in src/retrieve.ts
buildResolvedRetrieve still calls both helpers without the new config argument, so centrs retrieve bypasses centrs.env values for host/port/ssh-key while the other command paths use them.

🤖 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 `@src/resolver/target.ts` around lines 82 - 98, Pass the new config object
through the retrieve flow so `buildResolvedRetrieve` does not bypass
`centrs.env` values. Update the `buildResolvedRetrieve` call sites to supply
`config` into both `resolveTarget` and `resolveAuth`, matching the existing
`resolveTarget` signature and the `resolveAuth` helper in `src/retrieve.ts`.
Ensure host, port, and ssh-key resolution use the same config-backed precedence
as the other command paths.
src/retrieve.ts (1)

650-665: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Thread config into resolveTarget and resolveAuth. src/retrieve.ts:650-665 still drops the config tier here, so CENTRS_HOST/CENTRS_PORT from centrs.env won’t apply during target resolution, and CENTRS_SSH_KEY won’t apply for SSH auth.

🤖 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 `@src/retrieve.ts` around lines 650 - 665, The target/auth resolution path in
retrieve.ts is missing the config tier, so CENTRS_HOST, CENTRS_PORT, and
CENTRS_SSH_KEY from centrs.env are not considered. Update the calls to
resolveTarget and resolveAuth to thread the config object through alongside env,
via.value, and cdbResolution, and make sure the resolver functions use that
config tier when choosing target host/port and SSH auth material.
🧹 Nitpick comments (5)
commands/settings/README.md (1)

222-333: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move shared precedence/error docs out of the command README.

This section duplicates the repo-level constitution and error-catalog contract. Keeping it here will drift from the canonical source and makes commands/settings/README.md harder to keep command-focused.

As per coding guidelines, commands/*/README.md: "Each commands// directory must contain a README.md file that documents intent, flags, and behavior (the 'designed' tier), and should not restate the constitution or link elsewhere for envelope, errors, settings precedence, target selection, and protocol selection documentation".

🤖 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 `@commands/settings/README.md` around lines 222 - 333, Remove the duplicated
precedence and error-catalog contract content from commands/settings/README.md
and keep this README focused on settings-specific intent, flags, and behavior.
Move or delete the sections that restate the shared constitution, config
precedence, source provenance, and error code listings, and leave only
command-local guidance tied to symbols like settings print/get/set/reset and
defaultSettingsPath/config-file resolution.

Source: Coding guidelines

src/cli/settings.ts (1)

40-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--all/--cdb-file/--cdb-password/--skip-env-file aren't validated against subcommand.

These flags are documented as "print only" but parseSettingsCliArgs accepts them unconditionally for any subcommand (e.g. centrs settings set foo bar --all silently ignores --all instead of erroring). Minor UX polish; not currently causing incorrect behavior since settingsGet/Set/Reset simply don't read these fields.

Also applies to: 86-140

🤖 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 `@src/cli/settings.ts` around lines 40 - 69, The `settings` CLI options for
`--all`, `--cdb-file`, `--cdb-password`, and `--skip-env-file` are being
accepted by `parseSettingsCliArgs` for every subcommand even though they are
intended for `print` only. Update the subcommand validation in
`parseSettingsCliArgs` (and any related settings command wiring in
`settingsGet`, `settingsSet`, and `settingsReset`) so these flags are rejected
with an error when used with non-`print` subcommands, while still remaining
valid for `print`.
src/settings.ts (2)

648-666: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

print <attr> omits comment-kv tips that get provides.

settingsGet calls viaCommentKvTip(lookup.def) (Line 783) to surface the "a device's own comment-kv override may take precedence" tip for keys like via/timeout/port. The single-attr branch of settingsPrint hardcodes tips: [], so the same information is silently missing when a user runs centrs settings print via instead of centrs settings get via.

♻️ Suggested fix
 		const data: SettingsPrintData = {};
+		const tips: Tip[] = [];
 		if (lookup.kind === "managed") {
 			data[lookup.def.attr] = resolvePrintEntry(lookup.def, args.env, config);
+			tips.push(...viaCommentKvTip(lookup.def));
 		} else {
 			data[lookup.def.attr] = resolveRefusedEntry(lookup.def, args.env, config);
 		}
 		return {
 			ok: true,
 			data,
 			warnings,
-			tips: [],
+			tips,
 			meta: settingsMeta("print", settingsFile),
 		};
🤖 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 `@src/settings.ts` around lines 648 - 666, The single-attr branch in
settingsPrint is dropping the same comment-kv guidance that settingsGet already
exposes. Update the args.attr path in settingsPrint to reuse
viaCommentKvTip(lookup.def) when building the response, so keys like via,
timeout, and port include the comment-kv precedence tip instead of always
returning tips: [].

439-466: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Atomic-write helper looks solid; note the read-modify-write race across concurrent invocations.

writeSettingsFileLines itself is atomic (temp file + fsync + rename), but settingsSet/settingsReset read the full file, mutate in memory, then write back — two concurrent centrs settings set invocations can still lose one writer's update (last-writer-wins on the whole file, not per-key). Acceptable for a single-user local CLI; flagging for awareness rather than as a blocker.

🤖 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 `@src/settings.ts` around lines 439 - 466, `writeSettingsFileLines` is atomic,
but `settingsSet`/`settingsReset` still have a read-modify-write race that can
drop concurrent updates. Fix this by serializing settings mutations or adding a
file-level lock around the full read/update/write flow so only one writer can
modify the settings file at a time. Use the existing `settingsSet`,
`settingsReset`, and `writeSettingsFileLines` paths as the place to coordinate
the protection and prevent last-writer-wins overwrites.
src/retrieve.ts (1)

754-786: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

resolveRetrieveRequest can't accept a pre-loaded config, causing redundant reloads in fanout.

Unlike resolveExecuteRequest/resolveApiRequest, which accept options.config so fan-out can supply an already-loaded config, resolveRetrieveRequest always calls loadEnvFileDefaults(env) itself. This forces retrieve-fanout.ts's ad-hoc-member path to re-read/parse centrs.env once per ad-hoc target instead of reusing the fanout's already-computed config.

♻️ Proposed fix
 export async function resolveRetrieveRequest(
 	request: RetrieveRequest,
 	env: Record<string, string | undefined>,
+	options: { config?: Record<string, string | undefined> } = {},
 ): Promise<ResolvedRetrieveRequest> {
 	const attributeSelections = validateRetrieveRequestShape(request);
-	const config = await loadEnvFileDefaults(env);
+	const config = options.config ?? (await loadEnvFileDefaults(env));

And in retrieve-fanout.ts's ad-hoc path:

 			return resolveRetrieveRequest(
 				{ ...request, targetInput: member.input },
 				env,
+				{ config },
 			);
🤖 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 `@src/retrieve.ts` around lines 754 - 786, resolveRetrieveRequest currently
always calls loadEnvFileDefaults(env), which prevents fanout from reusing an
already-loaded config and causes repeated centrs.env reloads. Update
resolveRetrieveRequest to accept an optional preloaded config in the same style
as resolveExecuteRequest and resolveApiRequest, then use that config when
provided and only fall back to loadEnvFileDefaults(env) otherwise. Also update
the retrieve-fanout.ts ad-hoc member path to pass its existing config into
resolveRetrieveRequest so the fanout path avoids redundant parsing.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/errors/internal/settings-failed.md`:
- Around line 7-9: The `internal/settings-failed` markdown still contains
placeholder text instead of real recovery guidance. Replace the stub in the
`docs/errors/internal/settings-failed.md` entry with concrete trigger and
remediation details that explain when this error is emitted and what the user
should do next, keeping the wording consistent with the error contract
referenced from `docs/CONSTITUTION.md`.

In `@docs/errors/settings/invalid-timeout.md`:
- Around line 12-14: The timeout wording in this doc is too broad because
parseDuration only accepts unsigned inputs, so this branch can reject 0 but not
a negative duration. Update the text in invalid-timeout.md to say that
settings.ts rejects a syntactically valid duration that parses to 0, and remove
the mention of negative numbers so the description matches parseDuration and the
timeout validation rule precisely.

In `@docs/errors/settings/unknown-key.md`:
- Line 3: The opening summary for the unknown-key settings error only mentions
settings get and set, so update the short description to also include reset.
Keep the detailed explanation unchanged, and make sure the summary sentence in
the unknown-key error doc matches the full set of supported commands by
referencing the existing settings get/set/reset wording.

In `@src/settings.ts`:
- Around line 807-826: The settingsSet flow is re-parsing the existing on-disk
value with def.parse(previousRaw, def.envKey), which throws before a valid
replacement can be written when the stored value is malformed. Update
settingsSet to tolerate an invalid previous entry by avoiding strict parse of
previousRaw and instead only use the raw existing line/value as the previous
comparison target, similar to the tolerant
resolvePrintEntry/buildPrintEntryTolerant path. Keep the main parsed new value
handling unchanged, and ensure the write path still replaces the old entry even
if the prior value is bad.

---

Outside diff comments:
In `@src/resolver/target.ts`:
- Around line 82-98: Pass the new config object through the retrieve flow so
`buildResolvedRetrieve` does not bypass `centrs.env` values. Update the
`buildResolvedRetrieve` call sites to supply `config` into both `resolveTarget`
and `resolveAuth`, matching the existing `resolveTarget` signature and the
`resolveAuth` helper in `src/retrieve.ts`. Ensure host, port, and ssh-key
resolution use the same config-backed precedence as the other command paths.

In `@src/retrieve.ts`:
- Around line 650-665: The target/auth resolution path in retrieve.ts is missing
the config tier, so CENTRS_HOST, CENTRS_PORT, and CENTRS_SSH_KEY from centrs.env
are not considered. Update the calls to resolveTarget and resolveAuth to thread
the config object through alongside env, via.value, and cdbResolution, and make
sure the resolver functions use that config tier when choosing target host/port
and SSH auth material.

---

Nitpick comments:
In `@commands/settings/README.md`:
- Around line 222-333: Remove the duplicated precedence and error-catalog
contract content from commands/settings/README.md and keep this README focused
on settings-specific intent, flags, and behavior. Move or delete the sections
that restate the shared constitution, config precedence, source provenance, and
error code listings, and leave only command-local guidance tied to symbols like
settings print/get/set/reset and defaultSettingsPath/config-file resolution.

In `@src/cli/settings.ts`:
- Around line 40-69: The `settings` CLI options for `--all`, `--cdb-file`,
`--cdb-password`, and `--skip-env-file` are being accepted by
`parseSettingsCliArgs` for every subcommand even though they are intended for
`print` only. Update the subcommand validation in `parseSettingsCliArgs` (and
any related settings command wiring in `settingsGet`, `settingsSet`, and
`settingsReset`) so these flags are rejected with an error when used with
non-`print` subcommands, while still remaining valid for `print`.

In `@src/retrieve.ts`:
- Around line 754-786: resolveRetrieveRequest currently always calls
loadEnvFileDefaults(env), which prevents fanout from reusing an already-loaded
config and causes repeated centrs.env reloads. Update resolveRetrieveRequest to
accept an optional preloaded config in the same style as resolveExecuteRequest
and resolveApiRequest, then use that config when provided and only fall back to
loadEnvFileDefaults(env) otherwise. Also update the retrieve-fanout.ts ad-hoc
member path to pass its existing config into resolveRetrieveRequest so the
fanout path avoids redundant parsing.

In `@src/settings.ts`:
- Around line 648-666: The single-attr branch in settingsPrint is dropping the
same comment-kv guidance that settingsGet already exposes. Update the args.attr
path in settingsPrint to reuse viaCommentKvTip(lookup.def) when building the
response, so keys like via, timeout, and port include the comment-kv precedence
tip instead of always returning tips: [].
- Around line 439-466: `writeSettingsFileLines` is atomic, but
`settingsSet`/`settingsReset` still have a read-modify-write race that can drop
concurrent updates. Fix this by serializing settings mutations or adding a
file-level lock around the full read/update/write flow so only one writer can
modify the settings file at a time. Use the existing `settingsSet`,
`settingsReset`, and `writeSettingsFileLines` paths as the place to coordinate
the protection and prevent last-writer-wins overwrites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 13079b0b-5c38-4a31-8446-c2ee01d807f4

📥 Commits

Reviewing files that changed from the base of the PR and between 123a1b9 and 40252a1.

📒 Files selected for processing (46)
  • README.md
  • commands/devices/README.md
  • commands/settings/README.md
  • commands/settings/examples.md
  • docs/MATRIX.md
  • docs/errors/README.md
  • docs/errors/internal/settings-failed.md
  • docs/errors/settings/consequential-value.md
  • docs/errors/settings/invalid-boolean.md
  • docs/errors/settings/invalid-format.md
  • docs/errors/settings/invalid-integer.md
  • docs/errors/settings/invalid-timeout.md
  • docs/errors/settings/invalid-via.md
  • docs/errors/settings/reserved-key.md
  • docs/errors/settings/skip-env-file-active.md
  • docs/errors/settings/unknown-key.md
  • package.json
  • src/api-fanout.ts
  • src/api.ts
  • src/cli.ts
  • src/cli/mcp.ts
  • src/cli/settings.ts
  • src/core/error-catalog.ts
  • src/devices.ts
  • src/execute-fanout.ts
  • src/execute.ts
  • src/index.ts
  • src/mcp/config.ts
  • src/resolver/cdb.ts
  • src/resolver/config-file.ts
  • src/resolver/index.ts
  • src/resolver/mac.ts
  • src/resolver/selection.ts
  • src/resolver/settings.ts
  • src/resolver/target.ts
  • src/retrieve-fanout.ts
  • src/retrieve.ts
  • src/settings.ts
  • src/terminal.ts
  • src/transfer-fanout.ts
  • src/transfer.ts
  • test/integration/settings.test.ts
  • test/unit/resolver.test.ts
  • test/unit/settings-config-file.test.ts
  • test/unit/settings-file.test.ts
  • test/unit/settings-registry.test.ts

Comment on lines +7 to +9
See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
contract. This stub will be expanded with the typical trigger and remediation
for `internal/settings-failed`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the stub with actual recovery guidance.

This still says it "will be expanded" instead of explaining when internal/settings-failed is emitted and what the user should do next. A shipped error page should include a concrete trigger and remediation path.

🧰 Tools
🪛 LanguageTool

[grammar] ~7-~7: Ensure spelling is correct
Context: ...ION.md`](../../CONSTITUTION.md) for the centrs error contract. This stub will be expan...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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 `@docs/errors/internal/settings-failed.md` around lines 7 - 9, The
`internal/settings-failed` markdown still contains placeholder text instead of
real recovery guidance. Replace the stub in the
`docs/errors/internal/settings-failed.md` entry with concrete trigger and
remediation details that explain when this error is emitted and what the user
should do next, keeping the wording consistent with the error contract
referenced from `docs/CONSTITUTION.md`.

Comment thread docs/errors/settings/invalid-timeout.md Outdated
Comment on lines +12 to +14
`settings.ts` layers one more rule on top for `timeout` specifically: even a
syntactically valid duration that parses to `0` or a negative number is
rejected — a zero or negative timeout isn't a meaningful value to set.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tighten the timeout wording.

parseDuration only accepts unsigned inputs, so this branch can reject 0 but not a negative duration. As written, the doc implies negatives are reachable here, which is misleading.

Fix
-`settings.ts` layers one more rule on top for `timeout` specifically: even a
-syntactically valid duration that parses to `0` or a negative number is
-rejected — a zero or negative timeout isn't a meaningful value to set.
+`settings.ts` layers one more rule on top for `timeout` specifically: even a
+syntactically valid duration that parses to `0` is rejected — a zero timeout
+isn't a meaningful value to set.
📝 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
`settings.ts` layers one more rule on top for `timeout` specifically: even a
syntactically valid duration that parses to `0` or a negative number is
rejected — a zero or negative timeout isn't a meaningful value to set.
`settings.ts` layers one more rule on top for `timeout` specifically: even a
syntactically valid duration that parses to `0` is rejected — a zero timeout
isn't a meaningful value to set.
🤖 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 `@docs/errors/settings/invalid-timeout.md` around lines 12 - 14, The timeout
wording in this doc is too broad because parseDuration only accepts unsigned
inputs, so this branch can reject 0 but not a negative duration. Update the text
in invalid-timeout.md to say that settings.ts rejects a syntactically valid
duration that parses to 0, and remove the mention of negative numbers so the
description matches parseDuration and the timeout validation rule precisely.

Comment thread docs/errors/settings/unknown-key.md Outdated
@@ -0,0 +1,25 @@
# `settings/unknown-key`

`settings get`/`set` received a token that is not a recognized CENTRS_*-shaped setting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include reset in the one-line summary.

The detailed section already covers reset, but the opening sentence only names get/set. That makes the short description incomplete for readers scanning the error list.

Fix
-`settings get`/`set` received a token that is not a recognized CENTRS_*-shaped setting.
+`settings get`/`set`/`reset` received a token that is not a recognized CENTRS_*-shaped setting.
📝 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
`settings get`/`set` received a token that is not a recognized CENTRS_*-shaped setting.
`settings get`/`set`/`reset` received a token that is not a recognized CENTRS_*-shaped setting.
🤖 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 `@docs/errors/settings/unknown-key.md` at line 3, The opening summary for the
unknown-key settings error only mentions settings get and set, so update the
short description to also include reset. Keep the detailed explanation
unchanged, and make sure the summary sentence in the unknown-key error doc
matches the full set of supported commands by referencing the existing settings
get/set/reset wording.

Comment thread src/settings.ts
Comment on lines +807 to +826
export async function settingsSet(
args: SettingsSetArgs,
): Promise<SettingsSuccessEnvelope<SettingsSetData>> {
const settingsFile = defaultSettingsPath(args.env);
const lookup = lookupAttr(args.attr);
if (lookup.kind === "unknown") {
throw unknownKeyError(args.attr);
}
if (lookup.kind === "refused") {
throw reservedKeyError(lookup.def);
}
const def = lookup.def;
const parsed = def.parse(args.value, def.envKey);

const lines = await readRawLines(settingsFile);
const index = findLineIndex(lines, def.envKey);
const previousRaw = index >= 0 ? rawValueAtLine(lines, index) : undefined;
const previous =
previousRaw !== undefined ? def.parse(previousRaw, def.envKey).value : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

settings set can't fix an already-malformed value.

previous is computed by strictly re-parsing the existing raw on-disk value via def.parse(previousRaw, def.envKey). If the file was hand-edited into an invalid state (e.g. CENTRS_PORT=abc), this throws before the new (valid) value is ever written — so centrs settings set port 8080 fails with settings/invalid-integer citing the old garbage value, even though the new value is fine. This defeats the exact recovery path the tolerant resolvePrintEntry/buildPrintEntryTolerant design (Lines 521-569) was built for.

🐛 Proposed fix: tolerate a malformed previous value
-	const previous =
-		previousRaw !== undefined ? def.parse(previousRaw, def.envKey).value : null;
+	const previous = (() => {
+		if (previousRaw === undefined) {
+			return null;
+		}
+		try {
+			return def.parse(previousRaw, def.envKey).value;
+		} catch {
+			// Malformed value in a hand-edited file: report it as-is so `set`
+			// can still overwrite it (mirrors buildPrintEntryTolerant).
+			return previousRaw;
+		}
+	})();
📝 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
export async function settingsSet(
args: SettingsSetArgs,
): Promise<SettingsSuccessEnvelope<SettingsSetData>> {
const settingsFile = defaultSettingsPath(args.env);
const lookup = lookupAttr(args.attr);
if (lookup.kind === "unknown") {
throw unknownKeyError(args.attr);
}
if (lookup.kind === "refused") {
throw reservedKeyError(lookup.def);
}
const def = lookup.def;
const parsed = def.parse(args.value, def.envKey);
const lines = await readRawLines(settingsFile);
const index = findLineIndex(lines, def.envKey);
const previousRaw = index >= 0 ? rawValueAtLine(lines, index) : undefined;
const previous =
previousRaw !== undefined ? def.parse(previousRaw, def.envKey).value : null;
export async function settingsSet(
args: SettingsSetArgs,
): Promise<SettingsSuccessEnvelope<SettingsSetData>> {
const settingsFile = defaultSettingsPath(args.env);
const lookup = lookupAttr(args.attr);
if (lookup.kind === "unknown") {
throw unknownKeyError(args.attr);
}
if (lookup.kind === "refused") {
throw reservedKeyError(lookup.def);
}
const def = lookup.def;
const parsed = def.parse(args.value, def.envKey);
const lines = await readRawLines(settingsFile);
const index = findLineIndex(lines, def.envKey);
const previousRaw = index >= 0 ? rawValueAtLine(lines, index) : undefined;
const previous = (() => {
if (previousRaw === undefined) {
return null;
}
try {
return def.parse(previousRaw, def.envKey).value;
} catch {
// Malformed value in a hand-edited file: report it as-is so `set`
// can still overwrite it (mirrors buildPrintEntryTolerant).
return previousRaw;
}
})();
🤖 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 `@src/settings.ts` around lines 807 - 826, The settingsSet flow is re-parsing
the existing on-disk value with def.parse(previousRaw, def.envKey), which throws
before a valid replacement can be written when the stored value is malformed.
Update settingsSet to tolerate an invalid previous entry by avoiding strict
parse of previousRaw and instead only use the raw existing line/value as the
previous comparison target, similar to the tolerant
resolvePrintEntry/buildPrintEntryTolerant path. Keep the main parsed new value
handling unchanged, and ensure the write path still replaces the old entry even
if the prior value is bad.

mobileskyfi and others added 2 commits July 3, 2026 06:57
….23.0

Folds dependabot PR #158's bump into this branch directly to avoid a
package.json merge conflict; #158 is being closed as superseded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot:
- writeSettingsFileLines: fsync the directory after rename, matching
  winbox-cdb-write.ts's durability pattern.
- print --all: refused/credential keys hand-added to centrs.env now
  surface in the unrecognized list (redacted if secret) instead of being
  silently treated as "known" and hidden.
- loadEnvFileDefaults: strip credential/self-referential keys from the
  config map itself (REFUSED_CONFIG_ENV_KEYS, resolver/settings.ts), so
  no call site can accidentally read one back out of `config`, even if a
  resolver's own guard is ever missed. parseEnvFileDefaults stays
  unfiltered so `settings` can still inspect/redact hand-added lines.

CodeRabbit:
- settingsSet no longer throws when the *previous* on-disk value is
  already malformed (e.g. a hand-edited CENTRS_PORT=abc) -- it reports
  the raw value tolerantly and still writes the new, valid one. Previously
  `set` couldn't repair a broken file.
- Fixed invalid-timeout.md's wording: parseDuration only accepts
  unsigned durations, so negative values were never reachable.
- unknown-key.md's summary now mentions `reset`, not just get/set.

Left internal/settings-failed.md as the generic stub: it deliberately
mirrors internal/devices-failed.md's identical catch-all shape, and
enriching only one would break that parity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Pushed two follow-up commits addressing review feedback:

Copilot

  • writeSettingsFileLines now fsyncs the directory after rename(), matching winbox-cdb-write.ts's durability pattern.
  • print --all now surfaces hand-added refused/credential CENTRS_* lines (redacted if secret) instead of silently treating them as "known" and hiding them.
  • loadEnvFileDefaults strips credential/self-referential keys from the returned config map itself (new REFUSED_CONFIG_ENV_KEYS in resolver/settings.ts, drift-guarded by a unit test against settingsRefusedKeys), so no call site can leak one back out even if a resolver guard is ever missed. parseEnvFileDefaults stays unfiltered so settings can still inspect/redact hand-added lines.

CodeRabbit

  • settingsSet no longer throws when the previous on-disk value is already malformed (e.g. a hand-edited CENTRS_PORT=abc) — it now tolerates it and still writes the new valid value. This was a real bug: set couldn't repair a broken file.
  • Fixed invalid-timeout.md wording (negative durations were never reachable — parseDuration only accepts unsigned input).
  • unknown-key.md's summary now mentions reset, not just get/set.

Left internal/settings-failed.md as the generic stub — it deliberately mirrors internal/devices-failed.md's identical catch-all shape, and enriching only one would break that parity.

CodeQL (js/clear-text-logging #88–90, src/cli/settings.ts): investigated — entry.password is only ever narrowed to .length > 0 (settings.ts) before reaching the envelope, so the raw secret never crosses into anything logged. Same false-positive shape as #80/#81/#83. Flagging for a maintainer call rather than dismissing unilaterally.

All new/updated tests pass (bun run lint && bun run test && bun run build, plus lint:ci), including added coverage for each fix above.

Root-caused both new js/insecure-temporary-file findings (src/devices.ts:215,
src/transfer.ts:1205) by downloading the PR's SARIF and reading codeFlows: the
sole taint source across every path is the literal "/tmp/does-not-exist.cdb"
in test/integration/settings.test.ts (example 32), which CodeQL's OSTempDir
source model treats as an os-temp-dir source purely because it matches
/tmp/% -- no real tmpdir() call is involved. The sink model (InsecureFileOpen)
fires whenever the path argument has no secure mode (no group/other bits),
regardless of wx/O_EXCL collision-safety.

Two real fixes, not just noise-chasing:
- test/integration/settings.test.ts: build the nonexistent-cdb path under the
  test's own sandbox dir (freshDir()) instead of a hardcoded "/tmp/..."
  literal, removing the false taint source and improving test portability.
- src/devices.ts createEmptyCdbNoClobber: pass 0o600 to the exclusive-create
  open() so a new CDB (device credentials) is owner-only instead of
  inheriting the process umask.
- src/transfer.ts writeLocalSink: pass { mode: 0o600 } to writeFileSync so a
  newly created download destination (may hold RouterOS backups/configs) is
  owner-only. Mode only applies on creation, so existing destinations are
  unaffected.

Also dismissed alerts #88-90 (js/clear-text-logging, src/cli/settings.ts) as
false positives after tracing their codeFlows against
CleartextLoggingCustomizations.qll: #88's source is passwordSet, a boolean
matched purely by a property-name heuristic (not value/type aware). #89/#90's
apparent taint is context-insensitive over-tainting of the shared
expectValue()/CentrsError argv-parsing plumbing reused by every CLI
subcommand -- the actual sink content is a hardcoded literal error string
("Missing value for --password."), never a real secret. Written
justifications are attached to the dismissals via the code-scanning API.

Documented both false-positive shapes, how to fetch a PR's alert-diff state
and SARIF codeFlows, and the "no inline suppression, dismiss with reason or
query-filter" reality in
.github/instructions/github-security-quality.instructions.md so future PRs
can self-diagnose instead of re-litigating this each time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

CodeQL alert-diff: root-caused and resolved

Downloaded the PR's SARIF (gh api -H "Accept: application/sarif+json" .../code-scanning/analyses/<id>) and traced every codeFlows entry against the actual query sources in github/codeql (not just the alert text). Findings:

js/insecure-temporary-file (#91 devices.ts:215, #92 transfer.ts:1205) -- fixed in caa9448

Both alerts trace back to a single source: the literal "/tmp/does-not-exist.cdb" added in test/integration/settings.test.ts (example 32, this PR). InsecureTemporaryFileCustomizations.qll's OSTempDir source matches any string literal starting with /tmp/, not just real os.tmpdir() calls -- no mkdtemp() involvement at all. The sink (InsecureFileOpen) fires whenever the write has no secure mode (no group/other permission bits); wx/O_EXCL collision-safety is irrelevant to this rule.

Fixed both ends: the test now sandboxes the path under its own freshDir() instead of a raw /tmp/... literal (kills the false source), and devices.ts/transfer.ts now pass explicit 0o600 to their file-creation calls (kills the sink permanently, and is genuine hardening -- these files can hold RouterOS device credentials).

js/clear-text-logging (#88-90, src/cli/settings.ts) -- dismissed as false positive

Dismissed all three via the code-scanning API with per-alert written justification (audit trail per SECURITY.md).

Follow-up

Documented both false-positive shapes plus how to pull a PR's alert-diff state and SARIF codeFlows in .github/instructions/github-security-quality.instructions.md, so this doesn't need re-litigating each time it recurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

settings: implement print/get/set/reset (designed + advertised in README, but not routed in the CLI)

3 participants