add security hardening features (#543)

* add security hardening features

- Add authentication vault (`auth save/login/list/show/delete`) so credentials are stored locally and never exposed to the LLM (fixes Snyk W007)
- Add `--content-boundaries` flag to wrap page-sourced output in structural markers, helping LLMs distinguish tool output from untrusted page content (fixes Snyk W011)
- Add `--allowed-domains` flag to restrict browser navigation to trusted domains
- Add `--action-policy` for static allow/deny gating of action categories, with opt-in `--confirm-actions`/`--confirm-interactive` for orchestrator or human-in-the-loop confirmation
- Add `--max-output` flag to truncate large page outputs, preventing context flooding
- New docs page at /security, updated README, SKILL.md, CLI help text, and templates

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* docs
This commit is contained in:
Chris Tate
2026-02-25 15:33:20 -06:00
committed by GitHub
parent c0e2b80f8c
commit bc1e917e87
28 changed files with 3444 additions and 476 deletions
+120
View File
@@ -0,0 +1,120 @@
/**
* Standalone CLI entry point for auth vault operations that don't need a browser.
* Invoked directly by the Rust CLI to avoid sending passwords through the daemon channel.
*
* Usage: node auth-cli.js <json-command>
* Prints a JSON response to stdout and exits.
*/
import {
saveAuthProfile,
getAuthProfileMeta,
listAuthProfiles,
deleteAuthProfile,
} from './auth-vault.js';
interface AuthCommand {
id: string;
action: string;
name?: string;
url?: string;
username?: string;
password?: string;
usernameSelector?: string;
passwordSelector?: string;
submitSelector?: string;
}
function success(id: string, data: Record<string, unknown>): string {
return JSON.stringify({ success: true, id, data });
}
function error(id: string, message: string): string {
return JSON.stringify({ success: false, id, error: message });
}
function run(): void {
const input = process.argv[2];
if (!input) {
process.stderr.write('Usage: node auth-cli.js <json-command>\n');
process.exit(1);
}
let cmd: AuthCommand;
try {
cmd = JSON.parse(input);
} catch {
console.log(error('', 'Invalid JSON input'));
process.exit(1);
return;
}
const id = cmd.id || '';
try {
switch (cmd.action) {
case 'auth_save': {
if (!cmd.name || !cmd.url || !cmd.username || !cmd.password) {
console.log(error(id, 'Missing required fields: name, url, username, password'));
return;
}
const meta = saveAuthProfile({
name: cmd.name,
url: cmd.url,
username: cmd.username,
password: cmd.password,
usernameSelector: cmd.usernameSelector,
passwordSelector: cmd.passwordSelector,
submitSelector: cmd.submitSelector,
});
console.log(
success(id, {
saved: !meta.updated,
updated: meta.updated,
name: meta.name,
url: meta.url,
username: meta.username,
})
);
return;
}
case 'auth_list': {
const profiles = listAuthProfiles();
console.log(success(id, { profiles }));
return;
}
case 'auth_show': {
if (!cmd.name) {
console.log(error(id, 'Missing required field: name'));
return;
}
const meta = getAuthProfileMeta(cmd.name);
if (!meta) {
console.log(error(id, `Auth profile '${cmd.name}' not found`));
return;
}
console.log(success(id, { profile: meta }));
return;
}
case 'auth_delete': {
if (!cmd.name) {
console.log(error(id, 'Missing required field: name'));
return;
}
const deleted = deleteAuthProfile(cmd.name);
if (!deleted) {
console.log(error(id, `Auth profile '${cmd.name}' not found`));
return;
}
console.log(success(id, { deleted: true, name: cmd.name }));
return;
}
default:
console.log(error(id, `Unknown auth action: ${cmd.action}`));
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Operation failed';
console.log(error(id, msg));
}
}
run();