Skip to content

fix: merge latest dev updates into main#26

Merged
nadav-node9 merged 72 commits intomainfrom
dev
Mar 22, 2026
Merged

fix: merge latest dev updates into main#26
nadav-node9 merged 72 commits intomainfrom
dev

Conversation

@nadav-node9
Copy link
Contributor

Auto-generated PR

Merge latest dev changes into main to trigger a release.

⚠️ Important: When you click Squash and Merge, ensure the commit message starts with:

  • fix: to publish a Patch release (0.0.X)
  • feat: to publish a Minor release (0.X.0)
    If it starts with chore:, no NPM package will be published!

nadavis and others added 30 commits March 14, 2026 16:41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- native.ts: add extractContext + formatArgs with matchedField/matchedWord
  tracing for "Context Sniper" popup — shows dangerous word in context
- core.ts: extend evaluatePolicy return with matchedField/matchedWord;
  per-field scan after dangerous word found; pass through authorizeHeadless
- daemon/index.ts: gate SSE broadcast and browser open on browser config flag
- LICENSE/package.json/README.md: MIT → Apache-2.0
- .github/workflows/ai-review.yml: add paths-ignore to prevent self-modification
- scripts/ai-review.mjs: upgrade to claude-sonnet-4-6, max_tokens 2048

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap diff in <diff>...</diff> markers with untrusted-content notice to mitigate prompt injection
- Surface truncation note in posted PR comment when diff exceeds MAX_DIFF_CHARS
- Downgrade API errors to warning comments + exit 0 so Anthropic outages don't block PRs
- Pin @anthropic-ai/sdk@0.78.0 and @octokit/rest@22.0.1 to prevent supply-chain drift
- Add explicit permissions block (contents: read, pull-requests: write)
- Exclude dependabot[bot] from triggering review
- Add fetch-depth: 0 to checkout step

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… main

- ai-review.yml: replace AUTO_PR_TOKEN with GITHUB_TOKEN (permissions block
  already scopes it correctly — no broad PAT needed)
- ai-review.yml: add --ignore-scripts to npm install to block malicious
  postinstall hooks from transitive dependencies
- sync-dev.yml: new workflow — after every push to main, merge main back into
  dev so release-bot version bumps don't cause recurring README conflicts on
  the next dev -> main PR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move @anthropic-ai/sdk and @octokit/rest into devDependencies and switch the
ai-review workflow from bare npm install to npm ci --ignore-scripts. This
locks all transitive dependencies to the committed lockfile, eliminating
supply-chain drift on every CI run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- context-sniper.ts (new): shared RiskMetadata type, smartTruncate,
  extractContext (returns {snippet, lineIndex}), computeRiskMetadata
- native.ts: import from context-sniper, use .snippet on extractContext calls
- core.ts: add tier to evaluatePolicy returns; compute riskMetadata once in
  authorizeHeadless; pass it to initNode9SaaS, askDaemon, notifyDaemonViewer
- daemon/index.ts: store and broadcast riskMetadata in PendingEntry
- daemon/ui.html: renderPayload() uses riskMetadata for intent badge, tier,
  file path, annotated snippet, matched-word highlight; falls back to raw args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the Node9 cloud responds with shadowMode:true (org is in shadow
mode), print a yellow warning to stderr instead of blocking the agent.
The developer sees exactly why it was flagged without being interrupted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er, and integration tests

- Add Zod v3 schema validation (config-schema.ts) with clear error messages for
  bad config.json — catches literal newlines, invalid regex, unknown keys, bad enums
- Fix silent JSON parse fallback in tryLoadConfig: bad config now warns to stderr
  instead of silently using DEFAULT_CONFIG (which had cloud:true causing unexpected
  browser/cloud popups when config was invalid)
- Fix auditLocalAllow fire-and-forget killed by process.exit: audit mode path now
  awaits the POST so SaaS receives the event before the process exits
- Gate all auditLocalAllow calls on approvers.cloud so cloud:false (privacy mode)
  never sends data to SaaS
- Fix double browser windows when cloud+browser both enabled: RACER 3 (browser)
  now skips when cloudEnforced, preventing duplicate daemon /check entries
- Fix calledFromDaemon guard on terminal status messages to prevent duplicate output
- Add check.integration.test.ts: 20 end-to-end tests spawning real node9 check
  subprocess with isolated HOME dirs and in-process mock SaaS server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plied

Previously tryLoadConfig warned about invalid fields (e.g. mode:"bad-mode")
but still returned the raw object, letting them override valid values from
higher-priority config layers. A project-level node9.config.json with
mode:"bad-mode" would override the global mode:"audit", bypassing audit mode
and triggering the full cloud approval race unexpectedly.

sanitizeConfig() now drops top-level keys that fail Zod validation so invalid
project configs cannot corrupt the effective merged config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l test, improve comments

- Remove unreliable 200ms sleep from audit+cloud test: auditLocalAllow is
  awaited before process.exit so the POST is done by the time the subprocess
  closes; if it ever races here it would be a production bug too
- Add task* wildcard test: task_drop_all_tables must be fast-pathed to allow
  (documents the intentional security trade-off of user-configured wildcards)
- Expand runCheck docstring explaining why cwd=tmpHome is needed alongside
  HOME=tmpHome (avoids inheriting the repo's own node9.config.json)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…noredTools precedence tests

- Add `let resolved = false` guard in runCheckAsync to prevent double-resolve
  when child.kill() is called on timeout (close event fires after kill)
- Fix mockServer.close() in afterEach to return a Promise (was fire-and-forget)
- Document NODE9_TESTING=1 behavior in file header comment
- Add runCheck/runCheckAsync raw string support for malformed payload testing
- Add section 10: malformed JSON payload tests (non-JSON, empty, partial JSON)
- Add ignoredTools precedence test: task* wildcard + dangerous word in input
  documents that ignoredTools fast-path bypasses dangerousWords (by design)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CLI intentionally exits 0 on unparseable JSON (fail-open policy):
a transient serialization error must not block the AI session mid-flight.
The test was asserting the opposite. Updated all three malformed-payload
tests to verify graceful failure (no crash/stack trace) rather than an
error exit code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
core.ts:
- fix review-git-push regex: literal space → \s+ so "git  push" can't bypass
- fix getConfig(): environments block was always hardcoded {} and never merged
  from global/project config files; now applyLayer() accumulates environments
  correctly so strict-mode env overrides actually work

examples/node9.config.json.example:
- remove dangerousWords that caused false positives; keep only mkfs + shred
  (catastrophic, unambiguous — everything else handled by smartRules)
- add enterplanmode/enterworktree/exitworktree to ignoredTools
- add execute_query, query, mcp__postgres__*, mcp__github__* to toolInspection
- fix allow-readonly-bash regex: "npm run(build|test)" → "npm run (build|test)"
  (was matching "runbuild"/"runtest" instead of "run build"/"run test")
- remove smartRules already covered by built-in defaults:
  review-delete-without-where, block-force-push, block-drop-database, review-sudo
- remove "push"/"git" rules entries (match tool *names*, never fire for bash)
- remove non-functional environments block (was silently ignored until above fix)
- add approvalTimeoutMs:30000, version:"1.0", expanded snapshot.tools + ignorePaths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ule bypass, tests

core.ts:
- move mergedEnvironments declaration before applyLayer closure so the captured
  variable is clearly in scope (was hoisted at runtime but misleading to read)
- replace unsafe Partial<EnvironmentConfig> spread with field-level validation:
  only copies requireApproval when it is a boolean, ignoring any other input
- add version guard in tryLoadConfig: warns to stderr when a config file declares
  a version other than "1.0" so future schema changes can be caught early

examples/node9.config.json.example:
- add notMatches (&&|\|\||;\s*\S) condition to allow-readonly-bash rule so
  chained commands like "cat /etc/passwd && rm -rf /" are NOT fast-allowed
- tighten review-secrets-write file_path regex: add (^|[/\\]) path separator
  anchor so "notmy.env" no longer matches — only actual dotenv files do

tests (advanced_policy.test.ts):
- 6 new tests for allow-readonly-bash: verifies safe commands are allowed,
  &&/||/; chaining is rejected, pipe-only chains remain allowed
- 4 new tests for environments merge: project overrides global, type-unsafe
  values are dropped, multiple envs merge independently

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ets detection

- allow-readonly-bash: add notMatches for $() and backtick to prevent
  command substitution bypass
- allow-install-devtools: add notMatches guard for -g/--global flags
- review-secrets-write: change to conditionMode any and add path/filename
  field checks alongside file_path
- Add review-command-substitution rule (catches $() and backtick)
- Add review-global-install rule (catches npm/yarn/pnpm -g/--global)
- Add tests: $() bypass, backtick bypass, npm install -g, --global,
  review-secrets-write multi-field (path, filename), version mismatch
  warning/rejection paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n guard, secrets rule rename

- Restore rm and drop to dangerousWords (security regression from previous cleanup)
- Fix allow-readonly-bash notMatches: change ;\s*\S to bare ; so a
  trailing semicolon with no following content cannot bypass the guard
- Rename review-secrets-write → flag-secrets-access to reflect that it
  covers reads as well as writes; update reason string to match
- Add .env.bak test: dotfile backup IS flagged, notmy.env.bak is NOT
- Update all test fixtures to use ; instead of ;\s*\S to stay consistent
  with the corrected example config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nadavis and others added 19 commits March 21, 2026 12:13
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add tests proving built-in block rules (block-rm-rf-home, block-force-push)
  cannot be bypassed by a user-defined allow rule
- Restore Configuration Precedence section to README with 5-tier waterfall
  and note that built-in blocks always fire before user rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DLP Engine (src/dlp.ts):
- 7 built-in patterns: AWS key, GitHub token, Slack, OpenAI, Stripe, PEM, Bearer
- Recursive scanner with depth limit (5) and string length cap (100 KB)
- JSON-in-string detection for agents that stringify nested objects
- maskSecret() — only redacted sample stored, full secret never leaves dlp.ts
- severity: 'block' for known high-confidence patterns, 'review' for Bearer

Core integration (src/core.ts):
- DLP check runs before ignoredTools fast-path and audit mode
- Hard block for 'block' patterns; 'review' falls through to race engine
- DLP step 0 in explainPolicy waterfall
- dlp config merged per-layer in getConfig() with enabled/scanIgnoredTools

CLI (src/cli.ts):
- DLP-specific negotiation message (rotate the key, use env vars, don't retry)
- chalk.bgRed.white.bold alarm banner when blockedByLabel includes 'DLP'

Cursor fix (src/setup.ts):
- Remove hooks.json writing — Cursor does not support this format
- Print clear warning that native hook mode is pending Cursor support
- Only MCP proxy wrapping is configured

Shields fix (src/shields.ts):
- Treat empty shields.json as missing (suppress spurious parse warnings in tests)

Tests: 353 passing (22 new DLP tests, fake secrets split via concatenation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rity

advanced_policy.test.ts:
- Add ruleName assertion to allow-rm-safe-paths test (reviewer: vacuous test)
- Split compound test into two focused its (block vs allow scenarios)
- Add name field to project smartRule fixture so ruleName is assertable

core.ts:
- Return ruleName on allow verdict (was only returned for block/review)

core.test.ts:
- Add matchesGlob tests: path matching, boundary patterns, notMatchesGlob
- Add notMatches-no-flags tests: no throw + correct evaluation
- Fix notMatchesGlob test: add explicit block rule so fallthrough is observable

README.md:
- Add clarifying note: settings override order (Tier 1 wins) and smart rules
  evaluation order (defaults first) run in opposite directions — plus note that
  project block fires before shield block by design

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 1 invariant

README.md:
- Rewrite configuration precedence note as two distinct scannable paragraphs:
  settings (table/tier order) vs smart rules (concatenated list, first-match-wins)
- Add code block showing the exact evaluation order for smart rules
- Remove the dense single-paragraph explanation that reviewers found confusing

advanced_policy.test.ts:
- Add explicit comment that config-free tests rely on beforeEach existsSpy=false
- Add ruleName assertions to "reviews rm" tests (not just decision)
- Add Layer 1 bypass invariant test: user allow-all rule must not override
  built-in block-force-push — the most security-critical coverage gap identified

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…terminal

- Add `node9 tail` command: spec-compliant SSE stream of live agent activity,
  auto-starts daemon, supports --history replay and pipe-friendly output
- Browser dashboard redesigned as fixed-viewport 3-column layout: Flight
  Recorder on the left (scrolls internally, never causes page scroll), approvals
  in center, settings/shields/decisions on right
- Add Shields panel to browser dashboard with live enable/disable toggles,
  broadcast via SSE to keep multiple tabs in sync
- Add in-memory ring buffer (100 events) replayed to new SSE clients on connect
- Improve pending approval cards: action required header, live countdown timer,
  clearer Allow/Block button labels
- Add /shields GET+POST endpoints to daemon
- Fix tail crash on ECONNRESET (unhandled readline error event)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nnels

- Remove duplicate 'activity' broadcast from POST /check — the Unix socket
  (notifyActivity) already covers all tool calls; broadcasting from POST /check
  caused duplicate entries in node9 tail and the browser flight recorder

- Fix pending.delete race in POST /decision/:id: when the browser clicks
  Allow/Deny before GET /wait/:id has connected, keep the entry alive with
  earlyDecision set instead of deleting it immediately. Previously the long-poll
  would receive a 404 and askDaemon() returned 'deny' even when the user
  clicked Allow, preventing abortController.abort() from firing and leaving
  the native popup visible

- Make sendDecision/sendTrust async with proper error handling: if the POST
  /decision request fails (e.g. stale CSRF token after daemon restart), the
  card is restored with a red outline so the user can retry instead of
  silently disappearing while the native popup stays up forever

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Clear the earlyDecision cleanup timer in GET /wait/:id before consuming
  the entry, preventing the 30s setTimeout from firing on an already-deleted
  pending entry

- Re-add activity broadcast in POST /check for non-CLI callers: CLI sends
  fromCLI=true so the daemon skips the broadcast (socket already handled it);
  external callers (MCP integrations, scripts) get fromCLI=false and receive
  the broadcast as before, keeping the flight recorder complete for all paths

- Replace red-outline-only error state with: button disabled state during
  fetch (prevents double-click), inline error message on the card showing
  the HTTP status, and re-enabled buttons so the user can retry without
  refreshing the page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… node9 tail

Previously notifyActivity() fired a Unix socket write and returned void.
For fast-passing tools (Read, Glob, Grep, ignored tools in general),
process.exit(0) was called before the socket had time to connect, so the
activity event was never delivered — only slow tools like Bash that waited
for approval had enough time for the socket to flush.

Fix: notifyActivity() now returns a Promise that resolves on socket finish
or error. authorizeHeadless() awaits both the pending and result writes,
ensuring they complete before returning to the check handler's process.exit.
The added latency is negligible (sub-ms loopback socket) but now every
tool call — Read, Edit, Glob, Grep, Bash, Write — appears in node9 tail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Skip notifyActivity result notification when noApprovalMechanism is true
  so the CLI auto-start retry doesn't produce duplicate ALLOW+BLOCK entries
  in the flight recorder ring buffer
- In tail --history mode, render ring-buffer-replayed activity events directly
  when they already carry a resolved status (allow/block/dlp) instead of
  waiting for a matching activity-result that never arrives

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…icate broadcast

- notifyActivity: attach 'close' listener before sock.end() to avoid missing
  the event if the loopback socket flushes synchronously; drop redundant
  'finish' listener ('close' always fires after it)
- Unix socket server: add 1 MB byte cap and destroy socket on overflow to
  prevent unbounded memory growth from a runaway or malicious sender
- GET /wait/:id earlyDecision: remove duplicate broadcast('remove') — POST
  /decision already sent it, second one is spurious for reconnected clients
- DLP review path: write audit entry when severity='review' so the DLP flag
  is traceable in the audit log even if the race engine later approves the call
- tail.ts: tighten history guard from `data.ts && ...` to `data.ts > 0 && ...`
  to prevent falsy-ts events from bypassing the filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t constant

- Pass activityId in POST /check body so daemon reuses the CLI's flight-recorder
  UUID instead of generating its own; without this, tail.ts pending map never
  matched activity-result events for daemon-routed calls (functional bug)
- Add validToken check to GET /shields — shield status is operationally
  sensitive and should require auth like POST /shields
- Deduplicate dangerousWords when applying shield layer, consistent with the
  existing smartRules deduplication logic
- Replace hardcoded port 7391 in tail.ts ensureDaemon() with imported
  DAEMON_PORT constant so it stays in sync if the port ever changes
- Add comment explaining intentional in-place mutation of ring-buffer entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…covery

Default config changes:
- mode: "standard" → "audit" (observe before enforcing)
- approvalTimeoutMs: 0 → 30000 (auto-deny after 30s)
- approvers.cloud: true → false (opt-in after node9 login)
- enableHookLogDebug: false → true
- flightRecorder: true (new field)
- version: "1.0" added to config schema

Orphaned daemon recovery (no PID file):
- isDaemonRunning() falls back to ss port check when PID file missing
- ensureDaemon() in tail.ts HTTP health probes before spawning new daemon
- EADDRINUSE handler recovers orphaned PID via ss and writes PID file
- daemonStatus() detects and reports orphaned daemons

Test + e2e fixes for new defaults:
- All tests that expect blocking behaviour now explicitly set
  mode:"standard" and approvalTimeoutMs:0
- e2e.sh configs disable all approvers so race engine stays empty
  and noApprovalMechanism fires immediately regardless of any
  daemon running on port 7391

Docs: README settings table and CHANGELOG updated for all changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🤖 Claude Code Review

⚠️ AI review could not complete: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CZJY71UArZskVcPBTSqrK"}

Please review this PR manually.


Automated review by Claude Sonnet

Two new tests verify the orphaned-daemon detection branch in
isDaemonRunning() that runs when no PID file exists:
  - ss output containing the daemon port → returns true
  - ss output empty / port absent → returns false

Also add spawnSync to the child_process module mock so the default
mock returns status:1 (no match) and per-test overrides work cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🤖 Claude Code Review

⚠️ AI review could not complete: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CZJYfj7qAH1X6my5n4fuT"}

Please review this PR manually.


Automated review by Claude Sonnet

@github-actions
Copy link
Contributor

🤖 Claude Code Review

⚠️ AI review could not complete: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CZJZ1wPLMtD44rUEnNNjL"}

Please review this PR manually.


Automated review by Claude Sonnet

…d DLP wiring tests

- core.ts: notMatchesGlob with missing value now returns false (fail-closed)
  instead of true — a misconfigured condition no longer silently passes
- config-schema.ts: add .refine() so matchesGlob/notMatchesGlob conditions
  require a value field; produces a clear validation error instead of
  silently mis-evaluating at runtime
- core.test.ts: add DLP wiring tests that verify authorizeHeadless returns
  approved:false (with reason) when DLP detects an AWS key, and that
  scanIgnoredTools:false correctly bypasses DLP for ignored tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🤖 Claude Code Review

⚠️ AI review could not complete: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CZJZWBzwNv6yzFucbSFZd"}

Please review this PR manually.


Automated review by Claude Sonnet

@nadav-node9 nadav-node9 merged commit d18423f into main Mar 22, 2026
8 checks passed
nadav-node9 pushed a commit that referenced this pull request Mar 22, 2026
## [1.0.16](v1.0.15...v1.0.16) (2026-03-22)

### Bug Fixes

* merge latest dev updates into main ([#26](#26)) ([d18423f](d18423f)), closes [hi#confidence](https://github.com/hi/issues/confidence)
@nadav-node9
Copy link
Contributor Author

🎉 This PR is included in version 1.0.16 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants