clawarmor 2.0.0-alpha.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -146
- package/cli.js +3 -2
- package/demo-preview.gif +0 -0
- package/demo.cast +680 -0
- package/demo.gif +0 -0
- package/lib/fix.js +76 -14
- package/lib/harden.js +114 -13
- package/lib/prescan.js +166 -70
- package/lib/protect.js +60 -4
- package/lib/status.js +38 -15
- package/package.json +2 -2
- package/scripts/record-demo.py +125 -0
package/demo.gif
ADDED
|
Binary file
|
package/lib/fix.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// clawarmor fix — auto-apply safe one-liner fixes
|
|
2
|
+
// Now with impact classification: safe / caution / breaking
|
|
2
3
|
import { readFileSync } from 'fs';
|
|
3
4
|
import { homedir } from 'os';
|
|
4
5
|
import { join } from 'path';
|
|
@@ -9,57 +10,90 @@ import { loadConfig, get } from './config.js';
|
|
|
9
10
|
const HISTORY_PATH = join(homedir(), '.clawarmor', 'history.json');
|
|
10
11
|
const SEP = paint.dim('─'.repeat(52));
|
|
11
12
|
|
|
13
|
+
// Impact levels
|
|
14
|
+
const IMPACT = { SAFE: 'safe', CAUTION: 'caution', BREAKING: 'breaking' };
|
|
15
|
+
|
|
16
|
+
const IMPACT_BADGE = {
|
|
17
|
+
[IMPACT.SAFE]: () => paint.green('🟢 Safe'),
|
|
18
|
+
[IMPACT.CAUTION]: () => paint.yellow('🟡 Caution'),
|
|
19
|
+
[IMPACT.BREAKING]: () => paint.red('🔴 Breaking'),
|
|
20
|
+
};
|
|
21
|
+
|
|
12
22
|
// Fixes that are safe to auto-apply (single config set command, no restart risk)
|
|
13
23
|
const AUTO_FIXABLE = {
|
|
14
24
|
'browser.ssrf': {
|
|
15
25
|
cmd: 'openclaw config set browser.ssrfPolicy.dangerouslyAllowPrivateNetwork false',
|
|
16
26
|
desc: 'Block browser SSRF to private networks',
|
|
17
27
|
needsRestart: true,
|
|
28
|
+
impact: IMPACT.CAUTION,
|
|
29
|
+
impactDetail: 'Browser tool will no longer be able to access local/private network URLs.\n' +
|
|
30
|
+
' If your agent browses internal dashboards or local services, those will be blocked.',
|
|
18
31
|
},
|
|
19
32
|
'discovery.mdns': {
|
|
20
33
|
cmd: 'openclaw config set discovery.mdns.mode minimal',
|
|
21
34
|
desc: 'Set mDNS to minimal mode',
|
|
22
35
|
needsRestart: true,
|
|
36
|
+
impact: IMPACT.SAFE,
|
|
37
|
+
impactDetail: 'Only reduces network advertisement. No functionality change.',
|
|
23
38
|
},
|
|
24
39
|
'logging.redact': {
|
|
25
40
|
cmd: 'openclaw config set logging.redactSensitive tools',
|
|
26
41
|
desc: 'Enable log redaction',
|
|
27
42
|
needsRestart: false,
|
|
43
|
+
impact: IMPACT.SAFE,
|
|
44
|
+
impactDetail: 'Redacts sensitive data from logs. No functionality change.',
|
|
28
45
|
},
|
|
29
46
|
'tools.fs.workspaceOnly': {
|
|
30
47
|
cmd: 'openclaw config set tools.fs.workspaceOnly true',
|
|
31
48
|
desc: 'Restrict filesystem to workspace',
|
|
32
49
|
needsRestart: true,
|
|
50
|
+
impact: IMPACT.BREAKING,
|
|
51
|
+
impactDetail: 'Agent will ONLY be able to read/write files inside the workspace directory.\n' +
|
|
52
|
+
' Access to home directory, system files, and other paths will be blocked.\n' +
|
|
53
|
+
' Skills or workflows that read files outside workspace will break.',
|
|
33
54
|
},
|
|
34
55
|
'tools.applyPatch.workspaceOnly': {
|
|
35
56
|
cmd: 'openclaw config set tools.exec.applyPatch.workspaceOnly true',
|
|
36
57
|
desc: 'Restrict apply_patch to workspace',
|
|
37
58
|
needsRestart: true,
|
|
59
|
+
impact: IMPACT.CAUTION,
|
|
60
|
+
impactDetail: 'Patches can only be applied to files in the workspace. Usually fine unless\n' +
|
|
61
|
+
' your agent patches system files or configs outside the workspace.',
|
|
38
62
|
},
|
|
39
63
|
'fs.config.perms': {
|
|
40
64
|
cmd: 'chmod 600 ~/.openclaw/openclaw.json',
|
|
41
65
|
desc: 'Lock down config file permissions',
|
|
42
66
|
needsRestart: false,
|
|
43
67
|
shell: true,
|
|
68
|
+
impact: IMPACT.SAFE,
|
|
69
|
+
impactDetail: 'Only restricts other system users. Your agent runs as you.',
|
|
44
70
|
},
|
|
45
71
|
'fs.accounts.perms': {
|
|
46
72
|
cmd: 'chmod 600 ~/.openclaw/agent-accounts.json',
|
|
47
73
|
desc: 'Lock down credentials file permissions',
|
|
48
74
|
needsRestart: false,
|
|
49
75
|
shell: true,
|
|
76
|
+
impact: IMPACT.SAFE,
|
|
77
|
+
impactDetail: 'Only restricts other system users. Your agent runs as you.',
|
|
50
78
|
},
|
|
51
79
|
'agents.sandbox': {
|
|
52
|
-
// Multi-step fix: enables sandbox WITH workspace access so Telegram sessions don't lose memory files
|
|
53
80
|
cmd: "openclaw config set agents.defaults.sandbox.mode non-main && openclaw config set agents.defaults.sandbox.workspaceAccess rw && openclaw config set agents.defaults.sandbox.scope session",
|
|
54
81
|
desc: 'Enable sandbox isolation (with workspace access preserved for Telegram/group sessions)',
|
|
55
82
|
needsRestart: true,
|
|
56
83
|
requiresDocker: true,
|
|
84
|
+
impact: IMPACT.BREAKING,
|
|
85
|
+
impactDetail: 'Non-main sessions will run inside Docker containers.\n' +
|
|
86
|
+
' Requires Docker Desktop to be installed and running.\n' +
|
|
87
|
+
' Telegram/group sessions will lose direct host access (shell commands,\n' +
|
|
88
|
+
' file reads outside workspace). Workspace files remain accessible.',
|
|
57
89
|
},
|
|
58
90
|
'fs.dir.perms': {
|
|
59
91
|
cmd: 'chmod 700 ~/.openclaw',
|
|
60
92
|
desc: 'Lock down ~/.openclaw directory',
|
|
61
93
|
needsRestart: false,
|
|
62
94
|
shell: true,
|
|
95
|
+
impact: IMPACT.SAFE,
|
|
96
|
+
impactDetail: 'Only restricts other system users from listing the directory.',
|
|
63
97
|
},
|
|
64
98
|
};
|
|
65
99
|
|
|
@@ -71,7 +105,7 @@ function box(title) {
|
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
export async function runFix(flags = {}) {
|
|
74
|
-
console.log(''); console.log(box('ClawArmor Fix
|
|
108
|
+
console.log(''); console.log(box('ClawArmor Fix v2.1')); console.log('');
|
|
75
109
|
|
|
76
110
|
// Load last audit failures
|
|
77
111
|
let lastFailed = [];
|
|
@@ -89,8 +123,13 @@ export async function runFix(flags = {}) {
|
|
|
89
123
|
const fixable = lastFailed.filter(id => AUTO_FIXABLE[id]);
|
|
90
124
|
const manual = lastFailed.filter(id => !AUTO_FIXABLE[id]);
|
|
91
125
|
|
|
126
|
+
const safeFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.SAFE);
|
|
127
|
+
const cautionFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.CAUTION);
|
|
128
|
+
const breakingFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.BREAKING);
|
|
129
|
+
|
|
92
130
|
console.log(` ${paint.dim('Last audit had')} ${paint.bold(String(lastFailed.length))} ${paint.dim('failing checks.')}`);
|
|
93
|
-
console.log(` ${paint.bold(String(fixable.length))} ${paint.dim('can be auto-fixed.')} ${paint.dim(String(
|
|
131
|
+
console.log(` ${paint.bold(String(fixable.length))} ${paint.dim('can be auto-fixed:')} ${paint.green(String(safeFixes.length) + ' safe')} ${paint.dim('·')} ${paint.yellow(String(cautionFixes.length) + ' caution')} ${paint.dim('·')} ${paint.red(String(breakingFixes.length) + ' breaking')}`);
|
|
132
|
+
console.log(` ${paint.dim(String(manual.length))} ${paint.dim('require manual steps.')}`);
|
|
94
133
|
|
|
95
134
|
if (!fixable.length) {
|
|
96
135
|
console.log('');
|
|
@@ -99,40 +138,60 @@ export async function runFix(flags = {}) {
|
|
|
99
138
|
console.log(''); return 0;
|
|
100
139
|
}
|
|
101
140
|
|
|
102
|
-
// Load config for mainKey check
|
|
141
|
+
// Load config for mainKey check
|
|
103
142
|
const { config: cfg } = loadConfig();
|
|
104
143
|
const mainKey = get(cfg, 'agents.mainKey', 'main');
|
|
105
144
|
|
|
106
145
|
console.log('');
|
|
107
146
|
if (flags.dryRun) {
|
|
108
147
|
console.log(` ${paint.cyan('Dry run — would apply:')}`);
|
|
148
|
+
console.log('');
|
|
109
149
|
for (const id of fixable) {
|
|
110
150
|
const f = AUTO_FIXABLE[id];
|
|
151
|
+
const badge = IMPACT_BADGE[f.impact]();
|
|
111
152
|
if (id === 'agents.sandbox' && mainKey !== 'main') {
|
|
112
153
|
console.log(` ${paint.yellow('⚠')} ${paint.bold('Custom mainKey detected:')} agents.mainKey="${mainKey}"`);
|
|
113
154
|
console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
|
|
114
155
|
}
|
|
115
|
-
console.log(` ${paint.dim('→')} ${f.desc}`);
|
|
116
|
-
console.log(`
|
|
117
|
-
|
|
118
|
-
if (f.
|
|
156
|
+
console.log(` ${badge} ${paint.dim('→')} ${f.desc}`);
|
|
157
|
+
console.log(` ${paint.dim('Impact:')} ${f.impactDetail}`);
|
|
158
|
+
console.log(` ${paint.dim(f.cmd)}`);
|
|
159
|
+
if (f.requiresDocker) console.log(` ${paint.yellow('⚠')} ${paint.dim('Requires Docker Desktop to be installed and running')}`);
|
|
160
|
+
if (f.needsRestart) console.log(` ${paint.dim('↻ gateway restart required after applying')}`);
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply safe + caution fixes.')}`);
|
|
164
|
+
if (breakingFixes.length) {
|
|
165
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
|
|
119
166
|
}
|
|
120
|
-
console.log('');
|
|
121
|
-
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply these fixes.')}`);
|
|
122
167
|
console.log(''); return 0;
|
|
123
168
|
}
|
|
124
169
|
|
|
125
170
|
if (!flags.apply) {
|
|
126
|
-
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --dry-run')} ${paint.dim('to preview fixes.')}`);
|
|
127
|
-
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply
|
|
171
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --dry-run')} ${paint.dim('to preview fixes with impact analysis.')}`);
|
|
172
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply safe + caution fixes.')}`);
|
|
173
|
+
if (breakingFixes.length) {
|
|
174
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
|
|
175
|
+
}
|
|
128
176
|
console.log(''); return 0;
|
|
129
177
|
}
|
|
130
178
|
|
|
131
179
|
// Apply fixes
|
|
132
|
-
let applied = 0, failed = 0, needsRestart = false;
|
|
180
|
+
let applied = 0, failed = 0, skippedBreaking = 0, needsRestart = false;
|
|
133
181
|
console.log(SEP);
|
|
134
182
|
for (const id of fixable) {
|
|
135
183
|
const f = AUTO_FIXABLE[id];
|
|
184
|
+
const badge = IMPACT_BADGE[f.impact]();
|
|
185
|
+
|
|
186
|
+
// Skip breaking unless --force
|
|
187
|
+
if (f.impact === IMPACT.BREAKING && !flags.force) {
|
|
188
|
+
console.log(` ${badge} ${f.desc}`);
|
|
189
|
+
console.log(` ${paint.dim('Impact:')} ${f.impactDetail}`);
|
|
190
|
+
console.log(` ${paint.red('⊘ Skipped')} ${paint.dim('(breaking — use --apply --force to include)')}`);
|
|
191
|
+
console.log('');
|
|
192
|
+
skippedBreaking++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
136
195
|
|
|
137
196
|
// Warn about custom mainKey before sandbox fix
|
|
138
197
|
if (id === 'agents.sandbox' && mainKey !== 'main') {
|
|
@@ -140,7 +199,7 @@ export async function runFix(flags = {}) {
|
|
|
140
199
|
console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
|
|
141
200
|
}
|
|
142
201
|
|
|
143
|
-
process.stdout.write(` ${
|
|
202
|
+
process.stdout.write(` ${badge} ${f.desc}...`);
|
|
144
203
|
// Check Docker requirement
|
|
145
204
|
if (f.requiresDocker) {
|
|
146
205
|
const dockerCheck = spawnSync('docker', ['info'], { stdio: 'ignore' });
|
|
@@ -162,6 +221,9 @@ export async function runFix(flags = {}) {
|
|
|
162
221
|
}
|
|
163
222
|
|
|
164
223
|
console.log('');
|
|
224
|
+
if (skippedBreaking > 0) {
|
|
225
|
+
console.log(` ${paint.dim(`${skippedBreaking} breaking fix${skippedBreaking !== 1 ? 'es' : ''} skipped — use --apply --force to include`)}`);
|
|
226
|
+
}
|
|
165
227
|
if (needsRestart) {
|
|
166
228
|
console.log(` ${paint.yellow('!')} ${paint.bold('Restart required:')} ${paint.dim('openclaw gateway restart')}`);
|
|
167
229
|
}
|
package/lib/harden.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Modes:
|
|
3
3
|
// default: show each fix, prompt y/N before applying
|
|
4
4
|
// --dry-run: show what WOULD be fixed, no writes
|
|
5
|
-
// --auto: apply all safe fixes without confirmation (
|
|
5
|
+
// --auto: apply all safe + caution fixes without confirmation (skips breaking)
|
|
6
|
+
// --auto --force: apply ALL fixes including breaking ones
|
|
6
7
|
|
|
7
8
|
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
|
|
8
9
|
import { join } from 'path';
|
|
@@ -20,6 +21,29 @@ const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
|
|
|
20
21
|
const CLI_PATH = new URL('../cli.js', import.meta.url).pathname;
|
|
21
22
|
const SEP = paint.dim('─'.repeat(52));
|
|
22
23
|
|
|
24
|
+
// ── Impact levels ─────────────────────────────────────────────────────────────
|
|
25
|
+
// SAFE: No functionality impact. Pure security improvement.
|
|
26
|
+
// CAUTION: May change agent behavior. User should be aware.
|
|
27
|
+
// BREAKING: Will disable or restrict features currently in use.
|
|
28
|
+
|
|
29
|
+
const IMPACT = {
|
|
30
|
+
SAFE: 'safe',
|
|
31
|
+
CAUTION: 'caution',
|
|
32
|
+
BREAKING: 'breaking',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const IMPACT_BADGE = {
|
|
36
|
+
[IMPACT.SAFE]: (s) => paint.green(`🟢 Safe`),
|
|
37
|
+
[IMPACT.CAUTION]: (s) => paint.yellow(`🟡 Caution`),
|
|
38
|
+
[IMPACT.BREAKING]: (s) => paint.red(`🔴 Breaking`),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const IMPACT_LABEL = {
|
|
42
|
+
[IMPACT.SAFE]: 'No functionality impact',
|
|
43
|
+
[IMPACT.CAUTION]: 'May change agent behavior',
|
|
44
|
+
[IMPACT.BREAKING]: 'Will disable or restrict features you\'re actively using',
|
|
45
|
+
};
|
|
46
|
+
|
|
23
47
|
function box(title) {
|
|
24
48
|
const W = 52, pad = W - 2 - title.length, l = Math.floor(pad / 2), r = pad - l;
|
|
25
49
|
return [
|
|
@@ -56,12 +80,23 @@ function buildFixes(config) {
|
|
|
56
80
|
// Fix 1: world/group-readable credential files — chmod 600
|
|
57
81
|
const badFiles = findWorldReadableCredFiles();
|
|
58
82
|
for (const f of badFiles) {
|
|
83
|
+
// Classify: credential files (tokens, keys) are safe to lock down.
|
|
84
|
+
// Config files that other tools might read need caution.
|
|
85
|
+
const isSensitive = /\.(env|json|key|pem|token|secret)$/i.test(f.name) ||
|
|
86
|
+
f.name === 'agent-accounts.json' ||
|
|
87
|
+
f.name === 'openclaw.json';
|
|
88
|
+
const isScript = /\.(py|sh|js|ts)$/i.test(f.name);
|
|
89
|
+
|
|
59
90
|
fixes.push({
|
|
60
91
|
id: `cred.perms.${f.name}`,
|
|
61
92
|
problem: `${f.name} is readable by other users (permissions: ${f.mode})`,
|
|
62
93
|
action: `chmod 600 ${f.path}`,
|
|
63
94
|
description: `Set permissions to 600 (owner-only) on ${f.path}`,
|
|
64
95
|
type: 'shell',
|
|
96
|
+
impact: IMPACT.SAFE,
|
|
97
|
+
impactDetail: isScript
|
|
98
|
+
? 'Only restricts other system users. Scripts will still run as you.'
|
|
99
|
+
: 'Only restricts other system users from reading this file. Your agent is unaffected.',
|
|
65
100
|
manualNote: null,
|
|
66
101
|
});
|
|
67
102
|
}
|
|
@@ -75,6 +110,11 @@ function buildFixes(config) {
|
|
|
75
110
|
action: 'openclaw config set gateway.host 127.0.0.1',
|
|
76
111
|
description: 'Change gateway.host to 127.0.0.1 (loopback only)',
|
|
77
112
|
type: 'openclaw',
|
|
113
|
+
impact: IMPACT.BREAKING,
|
|
114
|
+
impactDetail: 'Remote connections to the gateway will stop working.\n' +
|
|
115
|
+
' If you access OpenClaw from other devices on your network (e.g. phone,\n' +
|
|
116
|
+
' tablet, or another computer), those connections will be blocked.\n' +
|
|
117
|
+
' Only localhost access will work after this change.',
|
|
78
118
|
manualNote: 'Restart gateway after applying: openclaw gateway restart',
|
|
79
119
|
});
|
|
80
120
|
}
|
|
@@ -85,9 +125,14 @@ function buildFixes(config) {
|
|
|
85
125
|
fixes.push({
|
|
86
126
|
id: 'exec.ask.off',
|
|
87
127
|
problem: 'exec.ask is off — shell commands run without user confirmation',
|
|
88
|
-
action: 'openclaw config set exec.ask
|
|
89
|
-
description: 'Enable exec
|
|
128
|
+
action: 'openclaw config set tools.exec.ask on-miss',
|
|
129
|
+
description: 'Enable exec approval for unrecognized commands',
|
|
90
130
|
type: 'openclaw',
|
|
131
|
+
impact: IMPACT.BREAKING,
|
|
132
|
+
impactDetail: 'Your agent will need approval for shell commands not in the allowlist.\n' +
|
|
133
|
+
' Autonomous workflows (cron jobs, background tasks, sub-agents) that run\n' +
|
|
134
|
+
' shell commands may pause waiting for approval.\n' +
|
|
135
|
+
' You\'ll need to approve commands via the web UI or CLI.',
|
|
91
136
|
manualNote: 'Restart gateway after applying: openclaw gateway restart',
|
|
92
137
|
});
|
|
93
138
|
}
|
|
@@ -157,10 +202,24 @@ function getManualItems() {
|
|
|
157
202
|
];
|
|
158
203
|
}
|
|
159
204
|
|
|
205
|
+
// ── Print impact summary ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function printImpactSummary(fixes) {
|
|
208
|
+
const counts = { [IMPACT.SAFE]: 0, [IMPACT.CAUTION]: 0, [IMPACT.BREAKING]: 0 };
|
|
209
|
+
for (const fix of fixes) counts[fix.impact]++;
|
|
210
|
+
|
|
211
|
+
const parts = [];
|
|
212
|
+
if (counts[IMPACT.SAFE]) parts.push(paint.green(`${counts[IMPACT.SAFE]} safe`));
|
|
213
|
+
if (counts[IMPACT.CAUTION]) parts.push(paint.yellow(`${counts[IMPACT.CAUTION]} caution`));
|
|
214
|
+
if (counts[IMPACT.BREAKING]) parts.push(paint.red(`${counts[IMPACT.BREAKING]} breaking`));
|
|
215
|
+
|
|
216
|
+
return parts.join(paint.dim(' · '));
|
|
217
|
+
}
|
|
218
|
+
|
|
160
219
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
161
220
|
|
|
162
221
|
export async function runHarden(flags = {}) {
|
|
163
|
-
console.log(''); console.log(box('ClawArmor Harden v2.
|
|
222
|
+
console.log(''); console.log(box('ClawArmor Harden v2.1')); console.log('');
|
|
164
223
|
|
|
165
224
|
const { config } = loadConfig();
|
|
166
225
|
const fixes = buildFixes(config);
|
|
@@ -170,6 +229,11 @@ export async function runHarden(flags = {}) {
|
|
|
170
229
|
const beforeScore = before?.score ?? null;
|
|
171
230
|
const beforeGrade = before?.grade ?? null;
|
|
172
231
|
|
|
232
|
+
// Count by impact
|
|
233
|
+
const safeFixes = fixes.filter(f => f.impact === IMPACT.SAFE);
|
|
234
|
+
const cautionFixes = fixes.filter(f => f.impact === IMPACT.CAUTION);
|
|
235
|
+
const breakingFixes = fixes.filter(f => f.impact === IMPACT.BREAKING);
|
|
236
|
+
|
|
173
237
|
// ── DRY RUN ────────────────────────────────────────────────────────────────
|
|
174
238
|
if (flags.dryRun) {
|
|
175
239
|
console.log(` ${paint.cyan('Dry run — showing what would be fixed (no changes applied):')}`);
|
|
@@ -183,17 +247,23 @@ export async function runHarden(flags = {}) {
|
|
|
183
247
|
}
|
|
184
248
|
|
|
185
249
|
for (const fix of fixes) {
|
|
250
|
+
const badge = IMPACT_BADGE[fix.impact]();
|
|
186
251
|
console.log(` ${paint.yellow('!')} ${paint.bold(fix.problem)}`);
|
|
187
252
|
console.log(` ${paint.dim('Fix:')} ${fix.description}`);
|
|
188
253
|
console.log(` ${paint.dim('Cmd:')} ${fix.action}`);
|
|
254
|
+
console.log(` ${badge}${paint.dim(':')} ${fix.impactDetail}`);
|
|
189
255
|
if (fix.manualNote) console.log(` ${paint.dim('Note:')} ${fix.manualNote}`);
|
|
190
256
|
console.log('');
|
|
191
257
|
}
|
|
192
258
|
|
|
193
259
|
console.log(SEP);
|
|
194
|
-
console.log(` ${fixes.length} fix${fixes.length !== 1 ? 'es' : ''} available
|
|
260
|
+
console.log(` ${fixes.length} fix${fixes.length !== 1 ? 'es' : ''} available: ${printImpactSummary(fixes)}`);
|
|
261
|
+
console.log('');
|
|
195
262
|
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden')} ${paint.dim('to apply interactively.')}`);
|
|
196
|
-
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto')} ${paint.dim('to apply
|
|
263
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto')} ${paint.dim('to apply safe + caution fixes.')}`);
|
|
264
|
+
if (breakingFixes.length) {
|
|
265
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
|
|
266
|
+
}
|
|
197
267
|
console.log('');
|
|
198
268
|
|
|
199
269
|
const manualItems = getManualItems();
|
|
@@ -223,25 +293,52 @@ export async function runHarden(flags = {}) {
|
|
|
223
293
|
return 0;
|
|
224
294
|
}
|
|
225
295
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
296
|
+
if (flags.auto) {
|
|
297
|
+
// --auto mode: apply safe + caution, SKIP breaking unless --force
|
|
298
|
+
const autoLabel = flags.force
|
|
299
|
+
? paint.cyan('Auto mode (--force) — applying ALL fixes including breaking')
|
|
300
|
+
: paint.cyan('Auto mode — applying safe + caution fixes (skipping breaking)');
|
|
301
|
+
console.log(` ${autoLabel}`);
|
|
302
|
+
console.log(` ${paint.dim('Fixes found:')} ${printImpactSummary(fixes)}`);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(` ${paint.cyan('Interactive mode — review and apply fixes one by one')}`);
|
|
305
|
+
console.log(` ${paint.dim('Fixes found:')} ${printImpactSummary(fixes)}`);
|
|
306
|
+
}
|
|
230
307
|
console.log('');
|
|
231
308
|
|
|
232
|
-
let applied = 0, skipped = 0, failed = 0;
|
|
309
|
+
let applied = 0, skipped = 0, failed = 0, skippedBreaking = 0;
|
|
233
310
|
const restartNotes = [];
|
|
234
311
|
|
|
235
312
|
for (const fix of fixes) {
|
|
313
|
+
const badge = IMPACT_BADGE[fix.impact]();
|
|
314
|
+
|
|
236
315
|
console.log(SEP);
|
|
316
|
+
console.log(` ${badge}`);
|
|
237
317
|
console.log(` ${paint.bold('Problem:')} ${fix.problem}`);
|
|
238
318
|
console.log(` ${paint.dim('Fix:')} ${fix.description}`);
|
|
319
|
+
console.log(` ${paint.dim('Impact:')} ${fix.impactDetail}`);
|
|
239
320
|
console.log(` ${paint.dim('Command:')} ${paint.dim(fix.action)}`);
|
|
240
321
|
console.log('');
|
|
241
322
|
|
|
242
|
-
let doApply
|
|
323
|
+
let doApply;
|
|
243
324
|
|
|
244
|
-
if (
|
|
325
|
+
if (flags.auto) {
|
|
326
|
+
// In auto mode: apply safe + caution, skip breaking unless --force
|
|
327
|
+
if (fix.impact === IMPACT.BREAKING && !flags.force) {
|
|
328
|
+
console.log(` ${paint.red('⊘ Skipped')} ${paint.dim('(breaking — use --auto --force to include)')}`);
|
|
329
|
+
skippedBreaking++;
|
|
330
|
+
skipped++;
|
|
331
|
+
console.log('');
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
doApply = true;
|
|
335
|
+
} else {
|
|
336
|
+
// Interactive mode: always ask, but warn about breaking
|
|
337
|
+
if (fix.impact === IMPACT.BREAKING) {
|
|
338
|
+
console.log(` ${paint.red('⚠ This fix will change how your agent works.')}`);
|
|
339
|
+
console.log(` ${paint.red(' Read the impact above carefully before applying.')}`);
|
|
340
|
+
console.log('');
|
|
341
|
+
}
|
|
245
342
|
doApply = await askYN(` Apply this fix? [y/N] `);
|
|
246
343
|
}
|
|
247
344
|
|
|
@@ -268,6 +365,10 @@ export async function runHarden(flags = {}) {
|
|
|
268
365
|
console.log('');
|
|
269
366
|
console.log(` Applied: ${paint.green(String(applied))} Skipped: ${paint.dim(String(skipped))} Failed: ${failed > 0 ? paint.red(String(failed)) : paint.dim('0')}`);
|
|
270
367
|
|
|
368
|
+
if (skippedBreaking > 0) {
|
|
369
|
+
console.log(` ${paint.dim(`(${skippedBreaking} breaking fix${skippedBreaking !== 1 ? 'es' : ''} skipped — use --auto --force to include)`)}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
271
372
|
// Restart notes
|
|
272
373
|
if (restartNotes.length) {
|
|
273
374
|
console.log('');
|