Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d2b7a8f
docs: update README with new information
nadavis Mar 14, 2026
fee4ef7
style: fix formatting in README.md
nadavis Mar 14, 2026
28a5926
fix: resolve merge conflict in README (duplicate CLI Reference sections)
nadavis Mar 14, 2026
d3c99cf
feat: add Claude AI code review on PRs to main
nadavis Mar 15, 2026
7f018aa
fix: add env validation and diff size limit to AI review
nadavis Mar 15, 2026
7fe57ab
style: format ai-review.mjs with prettier
nadavis Mar 15, 2026
a2ea754
Merge branch 'main' into dev
nadav-node9 Mar 15, 2026
bac1a39
feat: context sniper UI, browser gating, Apache-2.0, Claude AI review
nadavis Mar 15, 2026
abe4b3a
fix: harden AI review script per security audit
nadavis Mar 16, 2026
f7ae32d
style: apply prettier formatting
nadavis Mar 16, 2026
d666294
fix: switch to GITHUB_TOKEN, add --ignore-scripts, auto-sync dev from…
nadavis Mar 16, 2026
96f9d9f
fix: pin transitive CI deps via package-lock.json
nadavis Mar 16, 2026
e4d2cdc
chore: sync dev from main [skip ci]
github-actions[bot] Mar 16, 2026
430db8d
feat: Context Sniper UI parity — native popup, browser daemon, cloud
nadavis Mar 16, 2026
748a5bc
feat: shadow mode — passive stderr warning when SaaS allows through
nadavis Mar 16, 2026
260eac1
fix: config validation, audit mode delivery, cloud race, double-brows…
nadavis Mar 16, 2026
9f62709
Merge branch 'main' into dev
nadav-node9 Mar 16, 2026
2230792
fix: invalid config fields now stripped before merge, not silently ap…
nadavis Mar 16, 2026
904c3cb
style: prettier format config-schema.ts
nadavis Mar 16, 2026
553b653
test: address code review — fix timing race, add wildcard ignored-too…
nadavis Mar 16, 2026
97890ce
test: fix double-resolve in runCheckAsync, add malformed payload + ig…
nadavis Mar 16, 2026
65302b8
test: fix malformed payload test to match fail-open design
nadavis Mar 16, 2026
4b5d643
chore: sync dev from main [skip ci]
github-actions[bot] Mar 16, 2026
52762da
fix: two policy bugs + refresh example config for v1 release
nadavis Mar 17, 2026
38dde45
chore: prettier format example config
nadavis Mar 17, 2026
aac0c3c
fix: address all PR review issues — environment merge safety, allow-r…
nadavis Mar 17, 2026
97b961d
fix: block $() substitution, global npm install, and multi-field secr…
nadavis Mar 17, 2026
bc28e8e
Merge branch 'main' into dev
nadav-node9 Mar 17, 2026
32e0dc9
chore: sync dev from main [skip ci]
github-actions[bot] Mar 17, 2026
44b477e
fix: address second code review — dangerousWords regression, semicolo…
nadavis Mar 17, 2026
e160871
Merge branch 'main' into dev
nadav-node9 Mar 17, 2026
353a745
chore: sync dev from main [skip ci]
github-actions[bot] Mar 17, 2026
4bf8d3d
feat: scope node9 undo to current directory by default
nadavis Mar 20, 2026
67e8dde
fix: address code review — image src, jq injection safe payload
nadavis Mar 20, 2026
5d58ed5
Merge remote-tracking branch 'origin/main' into dev
nadavis Mar 20, 2026
0b671fd
chore: sync dev from main [skip ci]
github-actions[bot] Mar 20, 2026
61d37ab
docs: add Hugging Face Space badge and Try it Live section
nadavis Mar 20, 2026
4b4cc9e
Merge branch 'main' into dev
nadav-node9 Mar 20, 2026
2a2c109
chore: sync dev from main [skip ci]
github-actions[bot] Mar 20, 2026
344cca1
feat: add shield templates — one-command security for postgres, githu…
nadavis Mar 20, 2026
1afab89
style: apply prettier formatting to cli.ts
nadavis Mar 20, 2026
5e30a9a
fix: address shield code review — security regex hardening and correc…
nadavis Mar 20, 2026
6080a74
fix: address second shield code review
nadavis Mar 20, 2026
feafc0f
fix: address third shield code review
nadavis Mar 20, 2026
2da4a63
fix: address fourth shield review — regex fix, /etc/ narrowing, tests
nadavis Mar 20, 2026
30dc5a6
fix: address fifth shield review — scoping bug, Array.isArray guards,…
nadavis Mar 20, 2026
896cd22
refactor: load shields dynamically in getConfig() — remove config.jso…
nadavis Mar 21, 2026
729f3f8
fix: remove shield rules that duplicate built-in protections
nadavis Mar 21, 2026
cdb1cfd
docs: restructure README with two-layer protection model
nadavis Mar 21, 2026
da639e1
feat: replace rules system with smartRules + add matchesGlob/notMatch…
nadavis Mar 21, 2026
4950297
style: fix Prettier formatting in README.md
nadavis Mar 21, 2026
41f0632
test: add Layer 1 security invariant tests + restore precedence docs
nadavis Mar 21, 2026
1b14a0f
feat: add DLP content scanner + fix Cursor hook + fix shields warning
nadavis Mar 21, 2026
cf0d7d9
fix: address PR review — rule name assertions, glob tests, README cla…
nadavis Mar 21, 2026
86920d3
style: fix Prettier formatting in README.md
nadavis Mar 21, 2026
298ed68
fix: address second PR review — README clarity, test isolation, Layer…
nadavis Mar 21, 2026
f2ff28c
Merge branch 'main' into dev
nadav-node9 Mar 21, 2026
01790c5
chore: sync dev from main [skip ci]
github-actions[bot] Mar 21, 2026
e6b11d4
feat: add Flight Recorder — real-time activity stream in browser and …
nadavis Mar 21, 2026
1c019e5
fix: resolve race engine clashes between browser and native popup cha…
nadavis Mar 21, 2026
a119f70
fix: address PR review — timer leak, MCP caller gap, and browser UX
nadavis Mar 21, 2026
80dab4c
fix: await socket flush in notifyActivity so all tool calls appear in…
nadavis Mar 21, 2026
7556512
fix: suppress false BLOCK entries and render history in node9 tail
nadavis Mar 21, 2026
638b52d
chore: fix prettier formatting in authorizeHeadless
nadavis Mar 21, 2026
5977e5c
fix: address PR review — socket ordering, byte cap, audit trail, dupl…
nadavis Mar 21, 2026
6243d05
fix: address second PR review — ID mismatch, shields auth, dedup, por…
nadavis Mar 21, 2026
b51483c
Merge branch 'main' into dev
nadav-node9 Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **Flight Recorder — Browser Dashboard:** The browser dashboard (`localhost:7391`) is now a true fixed-viewport 3-column layout. The left column streams every tool call in real-time — appearing immediately as `● PENDING` and resolving to `✓ ALLOW`, `✗ BLOCK`, or `🛡️ DLP` as decisions arrive. The feed scrolls internally and never causes the browser page to scroll. History from the current session is replayed to new browser tabs via an in-memory ring buffer (last 100 events).
- **`node9 tail` — Terminal Flight Recorder:** New command that streams live agent activity directly to the terminal. Uses a spec-compliant SSE parser (handles TCP fragmentation), filters history floods on connect, and shows a live `● …` pending indicator for slow operations (bash, SQL, agent calls). Auto-starts the daemon if it isn't running. Supports `--history` to replay recent events on connect. Output is pipeable (`node9 tail | grep DLP`).
- **Shields Panel in Browser Dashboard:** The right sidebar now shows all available shields (postgres, github, aws, filesystem) with live enable/disable toggles. Changes take effect immediately on the next tool call — no daemon restart required. Toggle state is broadcast via SSE to keep multiple open tabs in sync.
- **Improved Pending Approval Cards:** Approval cards now show an `⚠️ Action Required` header with a live countdown timer that turns red under 15 seconds. Allow/Deny buttons have clearer labels (`✅ Allow this Action` / `🚫 Block this Action`). The deny button uses a softer outlined style to reduce accidental clicks.
- **DLP Content Scanner:** Node9 now scans every tool call argument for secrets before policy evaluation. Seven built-in patterns cover AWS Access Key IDs, GitHub tokens (`ghp_`, `gho_`, `ghs_`), Slack bot tokens (`xoxb-`), OpenAI API keys, Stripe secret keys, PEM private keys, and Bearer tokens. `block`-severity patterns hard-deny the call immediately; `review`-severity patterns route through the normal race engine. Secrets are redacted to a prefix+suffix sample in all audit logs. Configurable via `policy.dlp.enabled` and `policy.dlp.scanIgnoredTools`.
- **Shield Templates:** `node9 shield enable <service>` installs a curated rule set for a specific infrastructure service. Available shields: `postgres` (blocks `DROP TABLE`, `TRUNCATE`, `DROP COLUMN`; reviews `GRANT`/`REVOKE`), `github` (blocks `gh repo delete`; reviews remote branch deletion), `aws` (blocks S3 bucket deletion, EC2 termination; reviews IAM and RDS changes), `filesystem` (reviews `chmod 777` and writes to `/etc/`). Manage with `node9 shield enable|disable|list|status`.
- **Shadow Git Snapshots (Phase 2):** (Coming Soon) Automatic lightweight git commits before AI edits, allowing `node9 undo`.

### Fixed

- **Cursor hook setup:** `node9 addto cursor` no longer attempts to write an unsupported `hooks.json` file. A clear warning is shown explaining that MCP proxy wrapping is the only supported protection mode for Cursor.
- **Empty shields file warning:** Suppressed a spurious parse warning that appeared on first run when `~/.node9/shields.json` existed but was empty.
- **`node9 tail` crash on daemon disconnect:** An unhandled `ECONNRESET` error on the readline interface no longer crashes the process — it exits cleanly with `❌ Daemon disconnected.`

---

## [0.3.0] - 2026-03-06
Expand Down
85 changes: 78 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@ Node9 initiates a **Concurrent Race** across all enabled channels. The first cha
- **Cloud (Slack):** Remote asynchronous approval for team governance.
- **Terminal:** Classic `[Y/n]` prompt for manual proxy usage and SSH sessions.

### 🛰️ Flight Recorder — See Everything, Instantly

Node9 records every tool call your AI agent makes in real-time — no polling, no log files, no refresh. Two ways to watch:

**Browser Dashboard** (`node9 daemon start` → `localhost:7391`)

A live 3-column dashboard. The left column streams every tool call as it happens, updating in-place from `● PENDING` to `✓ ALLOW` or `✗ BLOCK`. The center handles pending approvals. The right sidebar controls shields and persistent decisions — all without ever causing a browser scrollbar.

**Terminal** (`node9 tail`)

A split-pane friendly stream for terminal-first developers and SSH sessions:

```bash
node9 tail # live events only
node9 tail --history # replay recent history then go live
node9 tail | grep DLP # filter to DLP blocks only
```

```
🛰️ Node9 tail → localhost:7391
Showing live events. Press Ctrl+C to exit.

21:06:58 📖 Read {"file_path":"src/core.ts"} ✓ ALLOW
21:06:59 🔍 Grep {"pattern":"authorizeHeadless"} ✓ ALLOW
21:07:01 💻 Bash {"command":"npm run build"} ✓ ALLOW
21:07:04 💻 Bash {"command":"curl … Bearer sk-ant-…"} ✗ BLOCK 🛡️ DLP
```

`node9 tail` auto-starts the daemon if it isn't running — no setup step needed.

### 🧠 AI Negotiation Loop

Node9 doesn't just "cut the wire." When a command is blocked, it injects a **Structured Negotiation Prompt** back into the AI's context window. This teaches the AI why it was stopped and instructs it to pivot to a safer alternative.
Expand Down Expand Up @@ -99,12 +129,51 @@ Node9 has two layers of protection. You get Layer 1 automatically. Layer 2 is on

Built into the binary. Zero configuration required. Protects the tools every developer uses.

| What it protects | Example blocked action |
| :--------------- | :------------------------------------------------------ |
| **Git** | `git push --force`, `git reset --hard`, `git clean -fd` |
| **Shell** | `curl ... \| bash`, `sudo` commands |
| **SQL** | `DELETE` / `UPDATE` without a `WHERE` clause |
| **Filesystem** | `rm -rf` targeting home directory |
| What it protects | Example blocked action |
| :---------------- | :------------------------------------------------------ |
| **Git** | `git push --force`, `git reset --hard`, `git clean -fd` |
| **Shell** | `curl ... \| bash`, `sudo` commands |
| **SQL** | `DELETE` / `UPDATE` without a `WHERE` clause |
| **Filesystem** | `rm -rf` targeting home directory |
| **Secrets (DLP)** | AWS keys, GitHub tokens, Stripe keys, PEM private keys |

### 🔍 DLP — Content Scanner (Always On)

Node9 scans **every tool call argument** for secrets before the command reaches your agent. If a credential is detected, Node9 hard-blocks the action, redacts the secret in the audit log, and injects a negotiation prompt telling the AI what went wrong.

**Built-in patterns:**

| Pattern | Severity | Prefix format |
| :---------------- | :------- | :-------------------------- |
| AWS Access Key ID | `block` | `AKIA` + 16 chars |
| GitHub Token | `block` | `ghp_`, `gho_`, `ghs_` |
| Slack Bot Token | `block` | `xoxb-` |
| OpenAI API Key | `block` | `sk-` + 20+ chars |
| Stripe Secret Key | `block` | `sk_live_` / `sk_test_` |
| PEM Private Key | `block` | `-----BEGIN PRIVATE KEY---` |
| Bearer Token | `review` | `Authorization: Bearer ...` |

`block` = hard deny, no approval prompt. `review` = routed through the normal race engine for human approval.

Secrets are **never logged in full** — the audit trail stores only a redacted sample (`AKIA****MPLE`).

**Config knobs** (in `node9.config.json` or `~/.node9/config.json`):

```json
{
"policy": {
"dlp": {
"enabled": true,
"scanIgnoredTools": true
}
}
}
```

| Key | Default | Description |
| :--------------------- | :------ | :----------------------------------------------------------------- |
| `dlp.enabled` | `true` | Master switch — disable to turn off all DLP scanning |
| `dlp.scanIgnoredTools` | `true` | Also scan tools in `ignoredTools` (e.g. `web_search`, `read_file`) |

### Layer 2 — Shields (Opt-in, Per Service)

Expand Down Expand Up @@ -251,6 +320,7 @@ Use `node9 explain <tool> <args>` to dry-run any tool call and see exactly which
| `node9 status` | Show current protection status and active rules |
| `node9 doctor` | Health check — verifies binaries, config, credentials, and all agent hooks |
| `node9 shield <cmd>` | Manage shields (`enable`, `disable`, `list`, `status`) |
| `node9 tail [--history]` | Stream live agent activity to the terminal (auto-starts daemon if needed) |
| `node9 explain <tool> [args]` | Trace the policy waterfall for a given tool call (dry-run, no approval prompt) |
| `node9 undo [--steps N]` | Revert the last N AI file edits using shadow Git snapshots |
| `node9 check` | Called by agent hooks; evaluates a pending tool call and exits 0 (allow) or 1 (block) |
Expand Down Expand Up @@ -318,7 +388,8 @@ A corporate policy has locked this action. You must click the "Approve" button i
- [x] **Shadow Git Snapshots** (1-click Undo for AI hallucinations)
- [x] **Identity-Aware Execution** (Differentiates between Human vs. AI risk levels)
- [x] **Shield Templates** (`node9 shield enable <service>` — one-click protection for Postgres, GitHub, AWS)
- [ ] **Content Scanner / DLP** (Detect and block secrets like AWS keys and Bearer tokens in-flight)
- [x] **Content Scanner / DLP** (Detect and block secrets like AWS keys and Bearer tokens in-flight)
- [x] **Flight Recorder** (Real-time activity stream in browser dashboard and `node9 tail` terminal view)
- [ ] **Universal MCP Gateway** (Standalone security tunnel for LangChain, CrewAI, and any agent without native hooks)
- [ ] **Cursor & Windsurf Hook** (Native hook support for AI-first IDEs)
- [ ] **VS Code Extension** (Approval requests in a native sidebar — no more OS popups)
Expand Down
32 changes: 30 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,8 +899,15 @@ program
.argument('[action]', 'start | stop | status (default: start)')
.option('-b, --background', 'Start the daemon in the background (detached)')
.option('-o, --openui', 'Start in background and open browser')
.option(
'-w, --watch',
'Start daemon + open browser, stay alive permanently (Flight Recorder mode)'
)
.action(
async (action: string | undefined, options: { background?: boolean; openui?: boolean }) => {
async (
action: string | undefined,
options: { background?: boolean; openui?: boolean; watch?: boolean }
) => {
const cmd = (action ?? 'start').toLowerCase();
if (cmd === 'stop') return stopDaemon();
if (cmd === 'status') return daemonStatus();
Expand All @@ -909,6 +916,17 @@ program
process.exit(1);
}

if (options.watch) {
process.env.NODE9_WATCH_MODE = '1';
// Open browser shortly after daemon binds to its port
setTimeout(() => {
openBrowserLocal();
console.log(chalk.cyan(`🛰️ Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
}, 600);
startDaemon(); // foreground — keeps process alive
return;
}

if (options.openui) {
if (isDaemonRunning()) {
openBrowserLocal();
Expand Down Expand Up @@ -937,7 +955,17 @@ program
}
);

// 6. CHECK (Internal Hook - Upgraded with AI Negotiation Loop)
// 6. TAIL
program
.command('tail')
.description('Stream live agent activity to the terminal')
.option('--history', 'Include recent history on connect', false)
.action(async (options: { history?: boolean }) => {
const { startTail } = await import('./tui/tail.js');
await startTail(options);
});

// 7. CHECK (Internal Hook - Upgraded with AI Negotiation Loop)
program
.command('check')
.description('Hook handler — evaluates a tool call before execution')
Expand Down
101 changes: 97 additions & 4 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { confirm } from '@inquirer/prompts';
import fs from 'fs';
import path from 'path';
import os from 'os';
import net from 'net';
import { randomUUID } from 'crypto';
import pm from 'picomatch';
import { parse } from 'sh-syntax';
import { askNativePopup, sendDesktopNotification } from './ui/native';
Expand Down Expand Up @@ -1275,7 +1277,8 @@ async function askDaemon(
args: unknown,
meta?: { agent?: string; mcpServer?: string },
signal?: AbortSignal,
riskMetadata?: RiskMetadata
riskMetadata?: RiskMetadata,
activityId?: string
): Promise<'allow' | 'deny' | 'abandoned'> {
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;

Expand All @@ -1294,6 +1297,12 @@ async function askDaemon(
args,
agent: meta?.agent,
mcpServer: meta?.mcpServer,
fromCLI: true,
// Pass the flight-recorder ID so the daemon uses the same UUID for
// activity-result as the CLI used for the pending activity event.
// Without this, the two UUIDs never match and tail.ts never resolves
// the pending item.
activityId,
...(riskMetadata && { riskMetadata }),
}),
signal: checkCtrl.signal,
Expand Down Expand Up @@ -1404,12 +1413,83 @@ export interface AuthResult {
| 'audit';
}

// ── Flight Recorder — fire-and-forget socket notify ──────────────────────────
const ACTIVITY_SOCKET_PATH =
process.platform === 'win32'
? '\\\\.\\pipe\\node9-activity'
: path.join(os.tmpdir(), 'node9-activity.sock');

// Returns a Promise so callers can await socket flush before process.exit().
// Without await, process.exit(0) kills the socket mid-connect for fast-passing
// tools (Read, Glob, Grep, etc.), making them invisible in node9 tail.
function notifyActivity(data: {
id: string;
ts: number;
tool: string;
args?: unknown;
status: string;
label?: string;
}): Promise<void> {
return new Promise<void>((resolve) => {
try {
const payload = JSON.stringify(data);
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
sock.on('connect', () => {
// Attach listeners before calling end() so events fired synchronously
// on the loopback socket are not missed.
sock.on('close', resolve);
sock.end(payload);
});
sock.on('error', resolve); // daemon not running — resolve immediately
} catch {
resolve();
}
});
}

export async function authorizeHeadless(
toolName: string,
args: unknown,
allowTerminalFallback = false,
meta?: { agent?: string; mcpServer?: string },
options?: { calledFromDaemon?: boolean }
): Promise<AuthResult> {
// Skip socket notification when called from daemon — daemon already broadcasts via SSE
if (!options?.calledFromDaemon) {
const actId = randomUUID();
const actTs = Date.now();
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: 'pending' });
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
...options,
activityId: actId,
});
// noApprovalMechanism means no channels were available — the CLI will retry
// after auto-starting the daemon. Don't log a false 'block' to the flight
// recorder; the retry call will produce the real result notification.
if (!result.noApprovalMechanism) {
await notifyActivity({
id: actId,
tool: toolName,
ts: actTs,
status: result.approved
? 'allow'
: result.blockedByLabel?.includes('DLP')
? 'dlp'
: 'block',
label: result.blockedByLabel,
});
}
return result;
}
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
}

async function _authorizeHeadlessCore(
toolName: string,
args: unknown,
allowTerminalFallback = false,
meta?: { agent?: string; mcpServer?: string },
options?: { calledFromDaemon?: boolean; activityId?: string }
): Promise<AuthResult> {
if (process.env.NODE9_PAUSED === '1') return { approved: true, checkedBy: 'paused' };
const pauseState = checkPause();
Expand Down Expand Up @@ -1472,7 +1552,10 @@ export async function authorizeHeadless(
blockedByLabel: '🚨 Node9 DLP (Secret Detected)',
};
}
// severity === 'review': fall through to the race engine with a DLP label
// severity === 'review': fall through to the race engine with a DLP label.
// Write an audit entry now so the DLP flag is traceable even if the race
// engine later approves the call without recording why it was intercepted.
if (!isManual) appendLocalAudit(toolName, args, 'allow', 'dlp-review-flagged', meta);
explainableLabel = '🚨 Node9 DLP (Credential Review)';
}
}
Expand Down Expand Up @@ -1750,7 +1833,14 @@ export async function authorizeHeadless(
console.error(chalk.cyan(` URL → http://${DAEMON_HOST}:${DAEMON_PORT}/\n`));
}

const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
const daemonDecision = await askDaemon(
toolName,
args,
meta,
signal,
riskMetadata,
options?.activityId
);
if (daemonDecision === 'abandoned') throw new Error('Abandoned');

const isApproved = daemonDecision === 'allow';
Expand Down Expand Up @@ -2027,7 +2117,10 @@ export function getConfig(): Config {
for (const rule of shield.smartRules) {
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
}
for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
const existingWords = new Set(mergedPolicy.dangerousWords);
for (const word of shield.dangerousWords) {
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
}
}

// Advisory rm rules are always appended last so user-defined rules (project/global/shield)
Expand Down
Loading
Loading