Skip to content

refactor(cli): migrate credentials, registry, and policies to TypeScript#1382

Closed
cv wants to merge 6 commits intomainfrom
cv/migrate-credentials-registry-policies-ts
Closed

refactor(cli): migrate credentials, registry, and policies to TypeScript#1382
cv wants to merge 6 commits intomainfrom
cv/migrate-credentials-registry-policies-ts

Conversation

@cv
Copy link
Copy Markdown
Contributor

@cv cv commented Apr 2, 2026

Summary

Convert the last 3 modules that were blocked by #782 to TypeScript:

  • credentials.js (327 lines) → src/lib/credentials.ts
  • registry.js (246 lines) → src/lib/registry.ts
  • policies.js (303 lines) → src/lib/policies.ts

Depends on #1370 (config-io module) — merge that first.

716 CLI tests pass. Coverage ratchet passes. Relates to #924.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added safe JSON configuration I/O with permission-aware errors and remediation guidance
    • Added interactive credential management with secure password prompting and token handling
    • Added policy preset management with validation and merge/apply helpers
    • Added multi-sandbox registry with concurrent-access coordination
  • Tests

    • New config I/O test suite covering permissions, atomic writes, and edge cases
    • Updated credential and registry tests to reflect source-location and error-message changes

cv and others added 3 commits April 2, 2026 14:17
…lidation

Add src/lib/config-io.ts with atomic JSON read/write (temp + rename),
EACCES error handling with user-facing remediation hints, and directory
permission enforcement.

- Refactor credentials.js to use readConfigFile/writeConfigFile
- Refactor registry.js to use readConfigFile/writeConfigFile
- Add validatePreset() to policies.js (warns on missing binaries section)
- ConfigPermissionError with actionable remediation (sudo chown / rm)
- Co-located tests for config-io module

Fixes #692, #606. Supersedes the config-io and preset validation parts
of #782 (without the runner.js redaction, which landed separately in
#1246).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert the last 3 blocked-by-#782 CJS modules to TypeScript:

- credentials.js → src/lib/credentials.ts
- registry.js → src/lib/registry.ts
- policies.js → src/lib/policies.ts

716 CLI tests pass. Coverage ratchet passes.
Depends on #1370 (config-io module). Relates to #924.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f01238fe-a751-4c31-aeac-8d72d7743a59

📥 Commits

Reviewing files that changed from the base of the PR and between c68d34f and 3b6dc31.

📒 Files selected for processing (1)
  • src/lib/policies.ts

📝 Walkthrough

Walkthrough

Migrates core bin/lib implementations to new TypeScript sources under src/lib/, adds new modules for config I/O, credentials, policies, and registry, replaces corresponding bin/lib/* files with thin re-export shims to dist/lib/*, and updates tests to reference new sources.

Changes

Cohort / File(s) Summary
Configuration I/O
src/lib/config-io.ts, src/lib/config-io.test.ts, bin/lib/config-io.js
Adds safe JSON config I/O: ConfigPermissionError, ensureConfigDir, writeConfigFile, readConfigFile with atomic writes and permission checks. Includes Vitest tests for permissions, corruption, and cleanup. Bin shim re-exports dist build.
Credential Management
src/lib/credentials.ts, bin/lib/credentials.js, test/credentials.test.js
New HOME-aware credential storage and interactive prompting (masked input, SIGINT handling), API/GitHub token bootstrapping, env precedence, and exported helpers; bin entrypoint replaced with a shim; tests updated to read source.
Policy Presets
src/lib/policies.ts, bin/lib/policies.js
New YAML preset management: listing, loading, endpoint extraction, validation, YAML/text merge strategies, applyPreset workflow invoking CLI, writing temp policy file, and updating registry. Bin file replaced by re-export shim.
Sandbox Registry
src/lib/registry.ts, bin/lib/registry.js, test/registry.test.js
Adds filesystem-backed sandbox registry with CRUD APIs, sandboxes.json persistence, directory-based advisory lock with stale-lock detection and retry, and withLock helper. Bin shim now re-exports dist implementation; test expectation adjusted for wrapped error message.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Policies
  participant Shell as "openshell/gh CLI"
  participant FS as "Filesystem (temp file)"
  participant Registry

  User->>Policies: request applyPreset(sandbox, preset)
  Policies->>Policies: validate sandbox & preset
  Policies->>Shell: run policy get <sandbox> (may fail)
  Shell-->>Policies: currentPolicy (or fallback)
  Policies->>Policies: merge preset into currentPolicy (YAML/text)
  Policies->>FS: write merged policy to temp file (0600)
  FS-->>Policies: temp file path
  Policies->>Shell: run policy set <sandbox> <temp file>
  Shell-->>Policies: command result
  Policies->>FS: remove temp file
  Policies->>Registry: withLock -> update sandbox.policies (append)
  Registry-->>Policies: updated sandbox entry
  Policies-->>User: success/failure
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement: testing

Suggested reviewers

  • kjw3
  • ericksoa

Poem

🐰 Files hop from bin to src with careful paws,

Shims point to dist while new TypeScript laws.
Credentials snug, policies stitched in line,
Registries lock, config files safe and fine.
A little rabbit cheers: compile and shine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: migrating three CLI modules (credentials, registry, policies) from JavaScript to TypeScript.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cv/migrate-credentials-registry-policies-ts

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/lib/registry.ts (1)

143-150: getDefault() fallback behavior may be unexpected.

When defaultSandbox is set but refers to a non-existent sandbox, this function falls back to the first sandbox in the list. This could mask data inconsistencies where defaultSandbox points to a deleted sandbox. Consider whether silently returning a different sandbox is the intended behavior, or if returning null would be more appropriate to signal the inconsistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/registry.ts` around lines 143 - 150, The current getDefault() in
registry.ts silently falls back to the first sandbox when data.defaultSandbox is
set but missing; update getDefault() to treat a dangling defaultSandbox as an
inconsistency and return null instead of returning names[0]. Specifically, after
calling load(), check if data.defaultSandbox exists and is present in
data.sandboxes; if it is return it, otherwise do not fall back to the first key
— return null (or optionally log/validate the inconsistency elsewhere). Keep
references to load(), data.defaultSandbox and data.sandboxes when making the
change.
src/lib/credentials.ts (1)

47-68: Minor optimization opportunity in getCredsFile.

getCredsDir() is called twice when _credsFile is null: once at line 65 and again at line 66. This is a minor inefficiency.

💡 Optional: Avoid double call
 function getCredsFile(): string {
   // Ensure dir cache is up to date with current HOME
-  getCredsDir();
-  if (!_credsFile) _credsFile = path.join(getCredsDir(), "credentials.json");
+  const dir = getCredsDir();
+  if (!_credsFile) _credsFile = path.join(dir, "credentials.json");
   return _credsFile;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/credentials.ts` around lines 47 - 68, getCredsFile calls
getCredsDir() twice when _credsFile is null; avoid the duplicate call by calling
getCredsDir() once, storing the result in a local variable (e.g., dir =
getCredsDir()), and then building _credsFile = path.join(dir,
"credentials.json"); keep the existing cache-refresh behavior tied to
getCredsDir() and reference the symbols getCredsFile, getCredsDir, and
_credsFile when making the change.
src/lib/config-io.test.ts (1)

75-93: Consider skipping this test on Windows.

The chmod-based write failure test relies on Unix filesystem permissions and will behave differently on Windows where chmod has limited effect. If Windows CI support is needed, consider conditionally skipping this test.

💡 Optional: Skip on Windows
-    it("cleans up temp file on write failure", () => {
+    it("cleans up temp file on write failure", { skip: process.platform === "win32" }, () => {

Or use it.skipIf:

it.skipIf(process.platform === "win32")("cleans up temp file on write failure", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/config-io.test.ts` around lines 75 - 93, The failing chmod-based test
"cleans up temp file on write failure" relies on Unix permissions and should be
skipped on Windows; update the test to conditionally skip when process.platform
=== "win32" (e.g., wrap or gate the it(...) with a platform check or use a skip
helper) so the cleanup assertion for writeConfigFile still runs on Unix CI, and
keep the existing fs.chmodSync(tmpDir/readonly) and tmp file assertions
unchanged for non-Windows runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/credentials.ts`:
- Around line 319-323: The CREDS_DIR and CREDS_FILE const exports are evaluated
once at import and become stale if HOME changes; change them to be dynamic by
exporting accessor functions or property getters that call getCredsDir() and
getCredsFile() at access time (e.g., replace the const exports CREDS_DIR and
CREDS_FILE with exported functions like _getCredsDir/_getCredsFile or define
exported getters that invoke getCredsDir/getCredsFile), and update any
consumers/tests to call the accessors instead of relying on the frozen const
values.

In `@src/lib/policies.ts`:
- Around line 253-260: The read-modify-write on sandbox policies is vulnerable
to a race: instead of calling registry.getSandbox(...) then mutating and calling
registry.updateSandbox(...), wrap the entire sequence in
registry.withLock(sandboxName, ...) or add/use an atomic
registry.appendPolicy(sandboxName, presetName) that checks presence and appends
under the lock; update the code paths that currently call registry.getSandbox
and registry.updateSandbox to use the locking closure (or the new appendPolicy)
so the include-check and push are performed atomically (referencing
registry.getSandbox, registry.updateSandbox, registry.withLock, and a proposed
registry.appendPolicy).

---

Nitpick comments:
In `@src/lib/config-io.test.ts`:
- Around line 75-93: The failing chmod-based test "cleans up temp file on write
failure" relies on Unix permissions and should be skipped on Windows; update the
test to conditionally skip when process.platform === "win32" (e.g., wrap or gate
the it(...) with a platform check or use a skip helper) so the cleanup assertion
for writeConfigFile still runs on Unix CI, and keep the existing
fs.chmodSync(tmpDir/readonly) and tmp file assertions unchanged for non-Windows
runs.

In `@src/lib/credentials.ts`:
- Around line 47-68: getCredsFile calls getCredsDir() twice when _credsFile is
null; avoid the duplicate call by calling getCredsDir() once, storing the result
in a local variable (e.g., dir = getCredsDir()), and then building _credsFile =
path.join(dir, "credentials.json"); keep the existing cache-refresh behavior
tied to getCredsDir() and reference the symbols getCredsFile, getCredsDir, and
_credsFile when making the change.

In `@src/lib/registry.ts`:
- Around line 143-150: The current getDefault() in registry.ts silently falls
back to the first sandbox when data.defaultSandbox is set but missing; update
getDefault() to treat a dangling defaultSandbox as an inconsistency and return
null instead of returning names[0]. Specifically, after calling load(), check if
data.defaultSandbox exists and is present in data.sandboxes; if it is return it,
otherwise do not fall back to the first key — return null (or optionally
log/validate the inconsistency elsewhere). Keep references to load(),
data.defaultSandbox and data.sandboxes when making the change.
🪄 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

Run ID: f27d896b-dcb9-487a-92c1-e06e72e90388

📥 Commits

Reviewing files that changed from the base of the PR and between 5454419 and c68d34f.

📒 Files selected for processing (11)
  • bin/lib/config-io.js
  • bin/lib/credentials.js
  • bin/lib/policies.js
  • bin/lib/registry.js
  • src/lib/config-io.test.ts
  • src/lib/config-io.ts
  • src/lib/credentials.ts
  • src/lib/policies.ts
  • src/lib/registry.ts
  • test/credentials.test.js
  • test/registry.test.js

Comment on lines +319 to +323
// Direct exports for TS consumers and jsconfig type checking.
// These are evaluated at import time but the HOME-aware cache
// ensures they stay correct across HOME changes in tests.
export const CREDS_DIR: string = getCredsDir();
export const CREDS_FILE: string = getCredsFile();
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

CREDS_DIR and CREDS_FILE exports will be stale if HOME changes after import.

The comment claims these stay correct across HOME changes, but they're evaluated once at import time as const assignments. Tests that stub HOME after importing will see stale values from these exports.

Consider using getters or documenting that consumers should use _getCredsDir()/_getCredsFile() for dynamic resolution.

💡 Suggested fix: Use getters for dynamic resolution
-// Direct exports for TS consumers and jsconfig type checking.
-// These are evaluated at import time but the HOME-aware cache
-// ensures they stay correct across HOME changes in tests.
-export const CREDS_DIR: string = getCredsDir();
-export const CREDS_FILE: string = getCredsFile();
+// Direct exports for TS consumers and jsconfig type checking.
+// Note: These are evaluated at import time. Use _getCredsDir()/_getCredsFile()
+// for dynamic resolution when HOME may change (e.g., in tests).
+export const CREDS_DIR: string = getCredsDir();
+export const CREDS_FILE: string = getCredsFile();

Or, if dynamic behavior is required:

// Use Object.defineProperty for lazy evaluation
let _exportedCredsDir: string | undefined;
let _exportedCredsFile: string | undefined;

export const CREDS_DIR = {
  get value() { return getCredsDir(); }
}.value; // This won't work for const exports

// Alternative: just export the functions and document usage
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/credentials.ts` around lines 319 - 323, The CREDS_DIR and CREDS_FILE
const exports are evaluated once at import and become stale if HOME changes;
change them to be dynamic by exporting accessor functions or property getters
that call getCredsDir() and getCredsFile() at access time (e.g., replace the
const exports CREDS_DIR and CREDS_FILE with exported functions like
_getCredsDir/_getCredsFile or define exported getters that invoke
getCredsDir/getCredsFile), and update any consumers/tests to call the accessors
instead of relying on the frozen const values.

Comment on lines +253 to +260
const sandbox = registry.getSandbox(sandboxName);
if (sandbox) {
const pols = sandbox.policies || [];
if (!pols.includes(presetName)) {
pols.push(presetName);
}
registry.updateSandbox(sandboxName, { policies: pols });
}
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

Race condition between getSandbox() and updateSandbox().

The sequence getSandbox → modify policies → updateSandbox is not atomic. Between reading the sandbox state (line 253) and updating it (line 259), another concurrent process could modify the policies array, causing those changes to be overwritten.

Consider using withLock from the registry module to wrap this entire read-modify-write sequence, or add an atomic appendPolicy function to the registry that handles the add-if-not-present logic internally.

🔒 Proposed fix using withLock
+import { withLock } from "./registry";

-  const sandbox = registry.getSandbox(sandboxName);
-  if (sandbox) {
-    const pols = sandbox.policies || [];
-    if (!pols.includes(presetName)) {
-      pols.push(presetName);
-    }
-    registry.updateSandbox(sandboxName, { policies: pols });
-  }
+  withLock(() => {
+    const sandbox = registry.getSandbox(sandboxName);
+    if (sandbox) {
+      const pols = sandbox.policies || [];
+      if (!pols.includes(presetName)) {
+        pols.push(presetName);
+      }
+      registry.updateSandbox(sandboxName, { policies: pols });
+    }
+  });

Note: Since updateSandbox also acquires the lock internally, you may need to implement a non-locking variant or use a reentrant lock pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/policies.ts` around lines 253 - 260, The read-modify-write on sandbox
policies is vulnerable to a race: instead of calling registry.getSandbox(...)
then mutating and calling registry.updateSandbox(...), wrap the entire sequence
in registry.withLock(sandboxName, ...) or add/use an atomic
registry.appendPolicy(sandboxName, presetName) that checks presence and appends
under the lock; update the code paths that currently call registry.getSandbox
and registry.updateSandbox to use the locking closure (or the new appendPolicy)
so the include-check and push are performed atomically (referencing
registry.getSandbox, registry.updateSandbox, registry.withLock, and a proposed
registry.appendPolicy).

@wscurran wscurran added enhancement: refactoring NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). labels Apr 3, 2026
The selectFromList function was added to policies.js on main (via #1370)
after our TS migration branched. Add the typed implementation to keep
the TS module in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cv
Copy link
Copy Markdown
Contributor Author

cv commented Apr 9, 2026

Closing this PR as stale/superseded.

I re-checked it against current main, and it no longer makes sense to merge as originally scoped:

  • the config-io / credentials / registry portions have already landed independently on main or are being handled in fix(cli): harden config file permission handling #1370
  • this branch now conflicts heavily with current main
  • the only remaining idea that still looks worth landing is a focused policies.js -> policies.ts migration, which should be done separately as a small fresh PR if we still want it

Leaving #1370 open for maintainer review.

@cv cv closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement: refactoring NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants