@fabioforest/openclaw 3.9.0 → 3.10.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/bin/openclaw.js +14 -6
- package/lib/cli/ide.js +13 -14
- package/lib/cli/init.js +13 -1
- package/lib/cli/uninstall.js +5 -0
- package/lib/cli/update.js +7 -0
- package/lib/utils/scope_guard.js +112 -0
- package/package.json +1 -1
package/bin/openclaw.js
CHANGED
|
@@ -43,7 +43,8 @@ const COMMANDS = {
|
|
|
43
43
|
function parseArgs(argv) {
|
|
44
44
|
const flags = {
|
|
45
45
|
plan: true, // Default: PLAN mode (read-only)
|
|
46
|
-
audit: true // Default: Generate audit logs
|
|
46
|
+
audit: true, // Default: Generate audit logs
|
|
47
|
+
_: [] // Captura subcomandos/args
|
|
47
48
|
};
|
|
48
49
|
let command = null;
|
|
49
50
|
|
|
@@ -62,7 +63,6 @@ function parseArgs(argv) {
|
|
|
62
63
|
flags.quiet = true;
|
|
63
64
|
} else if (arg === "--apply") {
|
|
64
65
|
flags.apply = true;
|
|
65
|
-
flags.plan = false; // Apply overrides Plan default
|
|
66
66
|
} else if (arg === "--plan") {
|
|
67
67
|
flags.plan = true;
|
|
68
68
|
} else if (arg === "--yes" || arg === "-y") {
|
|
@@ -71,13 +71,21 @@ function parseArgs(argv) {
|
|
|
71
71
|
flags.audit = false;
|
|
72
72
|
} else if (arg === "--merge") {
|
|
73
73
|
flags.merge = true;
|
|
74
|
-
} else if (!arg.startsWith("-")
|
|
75
|
-
command
|
|
74
|
+
} else if (!arg.startsWith("-")) {
|
|
75
|
+
if (!command) {
|
|
76
|
+
command = arg;
|
|
77
|
+
} else {
|
|
78
|
+
flags._.push(arg); // Captura subcomandos/args
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
//
|
|
80
|
-
|
|
83
|
+
// Regra de precedência: apply sempre vence plan
|
|
84
|
+
if (flags.apply) flags.plan = false;
|
|
85
|
+
|
|
86
|
+
// Constrói ajudantes semânticos a partir de _
|
|
87
|
+
flags.subcommand = flags._[0] || null;
|
|
88
|
+
flags.args = flags._.slice(1);
|
|
81
89
|
|
|
82
90
|
return { command, flags };
|
|
83
91
|
}
|
package/lib/cli/ide.js
CHANGED
|
@@ -14,6 +14,7 @@ const readline = require("readline");
|
|
|
14
14
|
const { detectContext, getAuditHeader } = require("../context");
|
|
15
15
|
const { copyDirRecursive } = require("./init");
|
|
16
16
|
const { writeCliAudit } = require("../utils/audit-writer");
|
|
17
|
+
const { guardPlan } = require("../utils/scope_guard");
|
|
17
18
|
|
|
18
19
|
// Caminho dos templates do pacote
|
|
19
20
|
const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates");
|
|
@@ -28,21 +29,9 @@ function writeAudit(targetPath, lines, flags) {
|
|
|
28
29
|
writeCliAudit(targetPath, lines, flags, "ide");
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* Executa o comando ide.
|
|
33
|
-
* Uso: openclaw ide install [--apply] [--force]
|
|
34
|
-
* openclaw ide doctor
|
|
35
|
-
*
|
|
36
|
-
* @param {object} options
|
|
37
|
-
* @param {string} options.targetPath — diretório alvo
|
|
38
|
-
* @param {object} options.flags — flags do CLI
|
|
39
|
-
*/
|
|
40
32
|
async function run({ targetPath, flags }) {
|
|
41
|
-
// Detectar sub-comando
|
|
42
|
-
|
|
43
|
-
// verificar os args para o sub-comando
|
|
44
|
-
const args = process.argv.slice(3);
|
|
45
|
-
const subCmd = args.find(a => !a.startsWith("-")) || "install";
|
|
33
|
+
// Detectar sub-comando alinhado com o parseArgs global
|
|
34
|
+
const subCmd = flags.subcommand || "install";
|
|
46
35
|
|
|
47
36
|
if (subCmd === "doctor") {
|
|
48
37
|
return runDoctor({ targetPath, flags });
|
|
@@ -84,6 +73,16 @@ async function runInstall({ targetPath, flags }) {
|
|
|
84
73
|
console.log(" ✅ KEEP .agent/state/ (já existe)");
|
|
85
74
|
}
|
|
86
75
|
|
|
76
|
+
// 2.5 Scope Guard
|
|
77
|
+
const intents = { writes: [agentDst, stateDir], deletes: [], overwrites: [] };
|
|
78
|
+
// Se for um force, significa que um overwrite/delete de diretório ocorrerá
|
|
79
|
+
if (fs.existsSync(agentDst) && flags.force) {
|
|
80
|
+
intents.deletes.push(agentDst);
|
|
81
|
+
intents.overwrites.push(agentDst);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await guardPlan(targetPath, intents, flags);
|
|
85
|
+
|
|
87
86
|
if (planMode) {
|
|
88
87
|
console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
|
|
89
88
|
console.log(" Para aplicar, rode: npx openclaw ide install --apply");
|
package/lib/cli/init.js
CHANGED
|
@@ -14,6 +14,7 @@ const readline = require("readline");
|
|
|
14
14
|
const { initConfigDefaults, writeJsonSafe } = require("../config");
|
|
15
15
|
const { detectContext, getAuditHeader } = require("../context");
|
|
16
16
|
const { writeCliAudit } = require("../utils/audit-writer");
|
|
17
|
+
const { guardPlan } = require("../utils/scope_guard");
|
|
17
18
|
|
|
18
19
|
// Caminho dos templates incluídos no pacote
|
|
19
20
|
const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
|
|
@@ -107,6 +108,17 @@ async function run({ targetPath, flags }) {
|
|
|
107
108
|
actions.push({ type: "NOOP", path: configPath, reason: "Config exists" });
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// 2.5. Acionar Scope Guard
|
|
112
|
+
const intents = { writes: [], deletes: [], overwrites: [] };
|
|
113
|
+
for (const a of actions) {
|
|
114
|
+
if (a.type === "DELETE_DIR") intents.deletes.push(a.path);
|
|
115
|
+
if (a.type === "CREATE_DIR") intents.writes.push(a.path);
|
|
116
|
+
if (a.type === "CREATE_FILE") intents.writes.push(a.path);
|
|
117
|
+
if (a.type === "COPY_DIR" || a.type === "MERGE_DIR") intents.writes.push(a.to);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await guardPlan(targetPath, intents, flags);
|
|
121
|
+
|
|
110
122
|
// 3. Exibir Plano
|
|
111
123
|
console.log(`\n🧭 Plano de Execução (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
|
|
112
124
|
console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
|
|
@@ -122,7 +134,7 @@ async function run({ targetPath, flags }) {
|
|
|
122
134
|
|
|
123
135
|
if (planMode) {
|
|
124
136
|
console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
|
|
125
|
-
console.log(" Para aplicar, rode
|
|
137
|
+
console.log(" Para aplicar, rode o comando com --apply");
|
|
126
138
|
return;
|
|
127
139
|
}
|
|
128
140
|
|
package/lib/cli/uninstall.js
CHANGED
|
@@ -15,6 +15,7 @@ const fs = require("fs");
|
|
|
15
15
|
const path = require("path");
|
|
16
16
|
const readline = require("readline");
|
|
17
17
|
const { detectContext, getAuditHeader } = require("../context");
|
|
18
|
+
const { guardPlan } = require("../utils/scope_guard");
|
|
18
19
|
|
|
19
20
|
function ask(q) {
|
|
20
21
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -103,6 +104,10 @@ async function run({ targetPath, flags }) {
|
|
|
103
104
|
console.log(` 🔴 REMOVER openclaw.json`);
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// Acionar Scope Guard para todos os deletes listados
|
|
108
|
+
const intents = { writes: [], deletes: toRemove.map(i => i.path), overwrites: [] };
|
|
109
|
+
await guardPlan(targetPath, intents, flags);
|
|
110
|
+
|
|
106
111
|
// Verificar audit logs que seriam perdidos
|
|
107
112
|
const auditDir = path.join(agentDir, "audit");
|
|
108
113
|
if (fs.existsSync(auditDir)) {
|
package/lib/cli/update.js
CHANGED
|
@@ -15,6 +15,7 @@ const crypto = require("crypto");
|
|
|
15
15
|
const readline = require("readline");
|
|
16
16
|
const { detectContext, getAuditHeader } = require("../context");
|
|
17
17
|
const { writeCliAudit } = require("../utils/audit-writer");
|
|
18
|
+
const { guardPlan } = require("../utils/scope_guard");
|
|
18
19
|
|
|
19
20
|
// Caminho dos templates incluídos no pacote
|
|
20
21
|
const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
|
|
@@ -127,6 +128,12 @@ async function run({ targetPath, flags }) {
|
|
|
127
128
|
const actions = planUpdates(TEMPLATES_DIR, agentDir);
|
|
128
129
|
const audit = [getAuditHeader(ctx, "update", flags)];
|
|
129
130
|
|
|
131
|
+
// 1.5 Acionar Scope Guard
|
|
132
|
+
const intents = { writes: [], deletes: [], overwrites: [] };
|
|
133
|
+
for (const a of actions.added) intents.writes.push(a.dest);
|
|
134
|
+
for (const a of actions.updated) intents.overwrites.push(a.dest);
|
|
135
|
+
await guardPlan(targetPath, intents, flags);
|
|
136
|
+
|
|
130
137
|
// 2. Exibir Plano
|
|
131
138
|
console.log(`\n🧭 Plano de Atualização (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
|
|
132
139
|
console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utilitário para prompt interativo.
|
|
9
|
+
*/
|
|
10
|
+
function askQuestion(query) {
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
return new Promise((resolve) => rl.question(query, (ans) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
resolve(ans.trim());
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Valida se um caminho pretendido está dentro do diretório seguro.
|
|
23
|
+
* @param {string} targetPath - Caminho do projeto destino.
|
|
24
|
+
* @param {string} fileToMutate - O arquivo/pasta a ser mexido.
|
|
25
|
+
* @returns {boolean} - True se .agent/** ou default safe.
|
|
26
|
+
*/
|
|
27
|
+
function isPathInSafeScope(targetPath, fileToMutate) {
|
|
28
|
+
// Escopo seguro default: pasta `.agent` na raiz do targetPath
|
|
29
|
+
const safeScope = path.join(targetPath, ".agent");
|
|
30
|
+
const absoluteMutate = path.resolve(targetPath, fileToMutate);
|
|
31
|
+
|
|
32
|
+
// Arquivos no diretório pai principal que não deveriam ser bloqueados:
|
|
33
|
+
// Padrão do projeto autoriza explicitamente "openclaw.json", ".env.example" no wizard?
|
|
34
|
+
// Para simplificar, focamos na "prisão" primária.
|
|
35
|
+
|
|
36
|
+
// allow openclaw.json default config
|
|
37
|
+
if (absoluteMutate === path.join(targetPath, "openclaw.json")) return true;
|
|
38
|
+
|
|
39
|
+
// Retorna true se começa com o caminho the escopo.
|
|
40
|
+
return absoluteMutate.startsWith(safeScope);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Escudo de Proteção "Scope Guard"
|
|
45
|
+
* Evita que o CLI apague ou modifique arquivos da aplicação nativa (ex: index.js, package.json do usuário).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} targetPath - Caminho de execução (process.cwd geralmente)
|
|
48
|
+
* @param {object} intents - Objeto contendo listas { writes: [], deletes: [], overwrites: [] } com paths relativos ou absolutos
|
|
49
|
+
* @param {object} flags - As CLI Flags (`force`, `yes`, `apply`)
|
|
50
|
+
*/
|
|
51
|
+
async function guardPlan(targetPath, intents = { writes: [], deletes: [], overwrites: [] }, flags = {}) {
|
|
52
|
+
const outOfScope = {
|
|
53
|
+
writes: intents.writes.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
54
|
+
deletes: intents.deletes.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
55
|
+
overwrites: intents.overwrites.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const totalRisks = outOfScope.writes.length + outOfScope.deletes.length + outOfScope.overwrites.length;
|
|
59
|
+
|
|
60
|
+
// Se tudo ok (nenhuma fuga da jail), permitimos silenciosamente sem drama
|
|
61
|
+
if (totalRisks === 0) return true;
|
|
62
|
+
|
|
63
|
+
console.log("\n🛡️ [SCOPE GUARD ALERTA DE SEGURANÇA]");
|
|
64
|
+
console.log("-----------------------------------------");
|
|
65
|
+
console.log(`Detectado tentativa de alterar dados FORA do sandbox (.agent/**).`);
|
|
66
|
+
|
|
67
|
+
if (outOfScope.writes.length > 0) {
|
|
68
|
+
console.log("⚠️ Arquivos a GERAR fora do escopo:", outOfScope.writes);
|
|
69
|
+
}
|
|
70
|
+
if (outOfScope.overwrites.length > 0) {
|
|
71
|
+
console.log("🧨 Arquivos a SOBRESCREVER fora do escopo:", outOfScope.overwrites);
|
|
72
|
+
}
|
|
73
|
+
if (outOfScope.deletes.length > 0) {
|
|
74
|
+
console.log("🔥 DELETE SOLICITADO fora do escopo:", outOfScope.deletes);
|
|
75
|
+
}
|
|
76
|
+
console.log("-----------------------------------------\n");
|
|
77
|
+
|
|
78
|
+
// Lógica para modo interativo. Se flag Force vier sem ask, permitimos por conta-risco.
|
|
79
|
+
if (flags.force) {
|
|
80
|
+
console.log("⚠️ Flag --force detectada. Destravando ação destrutiva fora da sandbox.");
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (flags.yes) {
|
|
85
|
+
console.log("❌ Bloqueado! Não permitimos escapes da sandbox via '--yes'. Você deve rodar o processo interativamente ou usar --force.");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Interactive confirmation com hard-stop (obriga escrever DESTRUCTIVE para deletes/overwrites)
|
|
90
|
+
if (outOfScope.deletes.length > 0 || outOfScope.overwrites.length > 0) {
|
|
91
|
+
const ans = await askQuestion("Perigo crítico. Para aprovar sobrescritas ou apagamentos fora do .agent, digite 'DESTRUCTIVE': ");
|
|
92
|
+
if (ans !== "DESTRUCTIVE") {
|
|
93
|
+
console.log("⏹️ Cancelado pelo Módulo Scope Guard.");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Para WRITES brandos: conf simples
|
|
100
|
+
const ans = await askQuestion("Aprovar criação de novas rotas fora do sandbox? [y/N]: ");
|
|
101
|
+
if (ans.toLowerCase() !== "y") {
|
|
102
|
+
console.log("⏹️ Cancelado pelo Módulo Scope Guard.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
guardPlan,
|
|
111
|
+
isPathInSafeScope
|
|
112
|
+
};
|