@fabioforest/openclaw 3.8.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/README.md +31 -0
- package/bin/openclaw.js +15 -18
- package/lib/cli/ide.js +19 -15
- package/lib/cli/init.js +13 -1
- package/lib/cli/setup.js +25 -0
- package/lib/cli/uninstall.js +5 -0
- package/lib/cli/update.js +68 -12
- package/lib/setup/config_wizard.js +228 -150
- package/lib/utils/scope_guard.js +112 -0
- package/package.json +2 -2
- package/templates/.agent/workflows/chat-first.md +16 -0
package/README.md
CHANGED
|
@@ -425,6 +425,37 @@ Verifica conectividade, proxy, versões e integridade do ambiente.
|
|
|
425
425
|
|
|
426
426
|
---
|
|
427
427
|
|
|
428
|
+
## 🤖 Agentes Especializados (Personas)
|
|
429
|
+
|
|
430
|
+
O OpenClaw vem com **agentes pré-configurados** que combinam skills específicas para realizar funções complexas:
|
|
431
|
+
|
|
432
|
+
| Persona | Foco | Skills Principais |
|
|
433
|
+
|---------|------|-------------------|
|
|
434
|
+
| **`sysadmin-proativo`** | Manutenção de servidores, logs, segurança | `vps-cloud-infra`, `openclaw-ops`, `openclaw-security` |
|
|
435
|
+
| **`workflow-automator`** | Criação e execução de automações | `ai-capture`, `mission-control`, `openclaw-dev` |
|
|
436
|
+
| **`setup-specialist`** | Onboarding e configuração inicial | `universal-setup`, `ai-provider-setup`, `openclaw-installation-debugger` |
|
|
437
|
+
|
|
438
|
+
**Como usar:**
|
|
439
|
+
Basta pedir no chat: *"Atue como sysadmin e verifique os logs do servidor"* ou *"Inicie o workflow-automator para criar uma automação de tickets"*. O `openclaw-router` ativará a persona correta.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## ⚡ Workflows Prontos
|
|
444
|
+
|
|
445
|
+
Além de skills isoladas, o OpenClaw traz **fluxos de trabalho completos** (runbooks executáveis):
|
|
446
|
+
|
|
447
|
+
| Workflow | Descrição | Comando Trigger |
|
|
448
|
+
|----------|-----------|-----------------|
|
|
449
|
+
| **`ai-capture`** | Captura inteligente de dados/tickets usando IA | *"Iniciar captura de dados"* |
|
|
450
|
+
| **`doctor`** | Diagnóstico e reparo automático do ambiente | `openclaw doctor` |
|
|
451
|
+
| **`healthcheck`** | Verificação rápida de saúde (API, DB, cache) | `openclaw healthcheck` |
|
|
452
|
+
| **`restart-openclaw`** | Reinício seguro e auditado do serviço | `openclaw restart` |
|
|
453
|
+
|
|
454
|
+
**Execução:**
|
|
455
|
+
Workflows são arquivos `.md` em `.agent/workflows/` que o agente lê e executa passo a passo, garantindo consistência e auditoria.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
428
459
|
## 🔒 Segurança
|
|
429
460
|
|
|
430
461
|
O OpenClaw segue 3 princípios fundamentais:
|
package/bin/openclaw.js
CHANGED
|
@@ -32,6 +32,7 @@ const COMMANDS = {
|
|
|
32
32
|
inspect: "../lib/cli/inspect",
|
|
33
33
|
assist: "../lib/cli/assist",
|
|
34
34
|
ide: "../lib/cli/ide",
|
|
35
|
+
setup: "../lib/cli/setup",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -42,7 +43,8 @@ const COMMANDS = {
|
|
|
42
43
|
function parseArgs(argv) {
|
|
43
44
|
const flags = {
|
|
44
45
|
plan: true, // Default: PLAN mode (read-only)
|
|
45
|
-
audit: true // Default: Generate audit logs
|
|
46
|
+
audit: true, // Default: Generate audit logs
|
|
47
|
+
_: [] // Captura subcomandos/args
|
|
46
48
|
};
|
|
47
49
|
let command = null;
|
|
48
50
|
|
|
@@ -61,7 +63,6 @@ function parseArgs(argv) {
|
|
|
61
63
|
flags.quiet = true;
|
|
62
64
|
} else if (arg === "--apply") {
|
|
63
65
|
flags.apply = true;
|
|
64
|
-
flags.plan = false; // Apply overrides Plan default
|
|
65
66
|
} else if (arg === "--plan") {
|
|
66
67
|
flags.plan = true;
|
|
67
68
|
} else if (arg === "--yes" || arg === "-y") {
|
|
@@ -70,13 +71,21 @@ function parseArgs(argv) {
|
|
|
70
71
|
flags.audit = false;
|
|
71
72
|
} else if (arg === "--merge") {
|
|
72
73
|
flags.merge = true;
|
|
73
|
-
} else if (!arg.startsWith("-")
|
|
74
|
-
command
|
|
74
|
+
} else if (!arg.startsWith("-")) {
|
|
75
|
+
if (!command) {
|
|
76
|
+
command = arg;
|
|
77
|
+
} else {
|
|
78
|
+
flags._.push(arg); // Captura subcomandos/args
|
|
79
|
+
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
//
|
|
79
|
-
|
|
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);
|
|
80
89
|
|
|
81
90
|
return { command, flags };
|
|
82
91
|
}
|
|
@@ -145,18 +154,6 @@ async function main() {
|
|
|
145
154
|
// Resolver caminho de destino
|
|
146
155
|
const targetPath = path.resolve(flags.path || ".");
|
|
147
156
|
|
|
148
|
-
// Comando setup — roda wizard diretamente
|
|
149
|
-
if (command === "setup") {
|
|
150
|
-
const wizardPath = path.join(__dirname, "..", "lib", "setup", "config_wizard.js");
|
|
151
|
-
try {
|
|
152
|
-
require(wizardPath);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
console.error(`❌ Erro ao rodar setup wizard: ${err.message}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
157
|
// Verificar se o comando existe
|
|
161
158
|
if (!COMMANDS[command]) {
|
|
162
159
|
console.error(`❌ Comando desconhecido: "${command}"`);
|
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");
|
|
@@ -173,7 +172,7 @@ async function runDoctor({ targetPath }) {
|
|
|
173
172
|
|
|
174
173
|
// Verificar rules
|
|
175
174
|
const rulesDir = path.join(agentDir, "rules");
|
|
176
|
-
const requiredRules = ["CONSENT_FIRST.md", "ROUTER_PROTOCOL.md"];
|
|
175
|
+
const requiredRules = ["CONSENT_FIRST.md", "ROUTER_PROTOCOL.md", "SECURITY.md", "WEB_AUTOMATION.md"];
|
|
177
176
|
for (const rule of requiredRules) {
|
|
178
177
|
checks.push({ name: `rules/${rule}`, ok: fs.existsSync(path.join(rulesDir, rule)) });
|
|
179
178
|
}
|
|
@@ -189,6 +188,11 @@ async function runDoctor({ targetPath }) {
|
|
|
189
188
|
const hooksDir = path.join(agentDir, "hooks");
|
|
190
189
|
checks.push({ name: "hooks/pre-tool-use.js", ok: fs.existsSync(path.join(hooksDir, "pre-tool-use.js")) });
|
|
191
190
|
|
|
191
|
+
// Verificar State Persistence
|
|
192
|
+
const stateDir = path.join(agentDir, "state");
|
|
193
|
+
checks.push({ name: "state/mission_control.json", ok: fs.existsSync(path.join(stateDir, "mission_control.json")) });
|
|
194
|
+
checks.push({ name: "state/MEMORY.md", ok: fs.existsSync(path.join(stateDir, "MEMORY.md")) });
|
|
195
|
+
|
|
192
196
|
// Exibir resultado
|
|
193
197
|
let allOk = true;
|
|
194
198
|
for (const c of checks) {
|
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/setup.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
async run({ targetPath, flags, templatesDir }) {
|
|
5
|
+
// Regras propostas pelo usuario:
|
|
6
|
+
// - default é PLAN (flags.plan true no bin/openclaw.js)
|
|
7
|
+
// - APPLY só com --apply
|
|
8
|
+
// - Não alterar nada fora de --apply
|
|
9
|
+
const wizard = require(path.join(__dirname, "..", "setup", "config_wizard.js"));
|
|
10
|
+
|
|
11
|
+
const base = targetPath || process.cwd();
|
|
12
|
+
|
|
13
|
+
// Mantendo consistência com os outros comandos que exportam .run() e recebem { targetPath, flags }
|
|
14
|
+
return wizard({
|
|
15
|
+
base,
|
|
16
|
+
flags: {
|
|
17
|
+
plan: flags.plan !== false, // default true
|
|
18
|
+
apply: !!flags.apply, // só executa com apply
|
|
19
|
+
yes: !!flags.yes, // skip prompts (ainda assim não faz nada se não apply)
|
|
20
|
+
force: !!flags.force, // para ações destrutivas (se você quiser usar)
|
|
21
|
+
},
|
|
22
|
+
templatesDir,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
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");
|
|
@@ -41,14 +42,36 @@ function fileHash(filePath) {
|
|
|
41
42
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Simples gerador de diff rústico mas efetivo para CLI
|
|
47
|
+
*/
|
|
48
|
+
function simpleDiff(oldStr, newStr) {
|
|
49
|
+
const oldLines = oldStr.split("\n");
|
|
50
|
+
const newLines = newStr.split("\n");
|
|
51
|
+
let diffStr = "";
|
|
52
|
+
|
|
53
|
+
const maxLength = Math.max(oldLines.length, newLines.length);
|
|
54
|
+
let diffCount = 0;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < maxLength && diffCount < 10; i++) {
|
|
57
|
+
if (oldLines[i] !== newLines[i]) {
|
|
58
|
+
if (oldLines[i] !== undefined) diffStr += `\x1b[31m- ${oldLines[i]}\x1b[0m\n`;
|
|
59
|
+
if (newLines[i] !== undefined) diffStr += `\x1b[32m+ ${newLines[i]}\x1b[0m\n`;
|
|
60
|
+
diffCount++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (diffCount >= 10) diffStr += `\x1b[90m... (diff truncado para 10 linhas)\x1b[0m\n`;
|
|
64
|
+
|
|
65
|
+
return diffStr || " (Nenhuma diferença detetada no payload comparável)";
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
/**
|
|
45
69
|
* Analisa atualizações necessárias.
|
|
46
70
|
* Retorna lista de ações planejadas.
|
|
47
71
|
*/
|
|
48
72
|
function planUpdates(src, dest, actions = { added: [], updated: [], skipped: [] }) {
|
|
49
73
|
if (!fs.existsSync(dest)) {
|
|
50
|
-
// Diretório não existe no destino, será
|
|
51
|
-
// Mas a lógica recursiva precisa entrar
|
|
74
|
+
// Diretório não existe no destino, será acompanhado na copia de arquivos
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
@@ -105,6 +128,12 @@ async function run({ targetPath, flags }) {
|
|
|
105
128
|
const actions = planUpdates(TEMPLATES_DIR, agentDir);
|
|
106
129
|
const audit = [getAuditHeader(ctx, "update", flags)];
|
|
107
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
|
+
|
|
108
137
|
// 2. Exibir Plano
|
|
109
138
|
console.log(`\n🧭 Plano de Atualização (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
|
|
110
139
|
console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
|
|
@@ -115,7 +144,9 @@ async function run({ targetPath, flags }) {
|
|
|
115
144
|
}
|
|
116
145
|
if (actions.updated.length > 0) {
|
|
117
146
|
console.log(`\n🔄 Modificados (${actions.updated.length}):`);
|
|
118
|
-
actions.updated.forEach(a =>
|
|
147
|
+
actions.updated.forEach(a => {
|
|
148
|
+
console.log(` ~ UPDATE ${safeRel(targetPath, a.dest)} (Exige confirmação interativa)`);
|
|
149
|
+
});
|
|
119
150
|
}
|
|
120
151
|
if (actions.skipped.length > 0) {
|
|
121
152
|
console.log(`\n⏭️ Ignorados (${actions.skipped.length} arquivos idênticos)`);
|
|
@@ -128,20 +159,21 @@ async function run({ targetPath, flags }) {
|
|
|
128
159
|
|
|
129
160
|
if (planMode) {
|
|
130
161
|
console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
|
|
131
|
-
console.log("
|
|
162
|
+
console.log(" Dica: a opção --merge foi desativada no modo interativo para forçar diff por arquivo.");
|
|
163
|
+
console.log(" Para aplicar e resolver conflitos: npx openclaw update --apply");
|
|
132
164
|
return;
|
|
133
165
|
}
|
|
134
166
|
|
|
135
|
-
// 3. Confirmação
|
|
136
|
-
if (!flags.yes) {
|
|
137
|
-
const ok = await ask("\nAplicar
|
|
167
|
+
// 3. Confirmação inicial
|
|
168
|
+
if (!flags.yes && actions.updated.length === 0) {
|
|
169
|
+
const ok = await ask("\nAplicar e copiar arquivos novos? (y/N): ");
|
|
138
170
|
if (ok.toLowerCase() !== "y") {
|
|
139
171
|
console.log("⏹️ Cancelado.");
|
|
140
172
|
return;
|
|
141
173
|
}
|
|
142
174
|
}
|
|
143
175
|
|
|
144
|
-
// 4. Execução
|
|
176
|
+
// 4. Execução Interativa Segura (Conflitos file-by-file)
|
|
145
177
|
try {
|
|
146
178
|
console.log("\n🚀 Executando atualizações...");
|
|
147
179
|
|
|
@@ -151,18 +183,42 @@ async function run({ targetPath, flags }) {
|
|
|
151
183
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
152
184
|
}
|
|
153
185
|
|
|
186
|
+
// Action: Added
|
|
154
187
|
for (const action of actions.added) {
|
|
155
188
|
ensureDir(action.dest);
|
|
156
189
|
fs.copyFileSync(action.src, action.dest);
|
|
157
190
|
audit.push(`- ACT: CREATED ${safeRel(targetPath, action.dest)}`);
|
|
158
191
|
}
|
|
159
192
|
|
|
193
|
+
// Action: Updated (Conflict Resolver)
|
|
160
194
|
for (const action of actions.updated) {
|
|
161
195
|
ensureDir(action.dest);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
const rPath = safeRel(targetPath, action.dest);
|
|
197
|
+
|
|
198
|
+
let overwrite = flags.yes || flags.force;
|
|
199
|
+
|
|
200
|
+
if (!overwrite) {
|
|
201
|
+
console.log(`\n⚠️ CONFLITO DETECTADO: ${rPath}`);
|
|
202
|
+
console.log("------------------------------------------------");
|
|
203
|
+
const oldContent = fs.readFileSync(action.dest, "utf-8");
|
|
204
|
+
const newContent = fs.readFileSync(action.src, "utf-8");
|
|
205
|
+
console.log(simpleDiff(oldContent, newContent));
|
|
206
|
+
console.log("------------------------------------------------");
|
|
207
|
+
|
|
208
|
+
const ans = await ask(`Substituir a sua versão de ${rPath} pelo código acima? [y/N]: `);
|
|
209
|
+
overwrite = ans.toLowerCase() === "y";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (overwrite) {
|
|
213
|
+
const backupPath = action.dest + ".bak";
|
|
214
|
+
fs.copyFileSync(action.dest, backupPath);
|
|
215
|
+
fs.copyFileSync(action.src, action.dest);
|
|
216
|
+
console.log(`✅ Sobrescrito: ${rPath} (Backup guardado localmente: .bak)`);
|
|
217
|
+
audit.push(`- ACT: UPDATED ${rPath} (Backup: ${path.basename(backupPath)})`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`⏭️ Ignorado (Mantido customização em ${rPath})`);
|
|
220
|
+
audit.push(`- ACT: SKIPPED UPDATE FOR CUSTOMIZED FILE ${rPath}`);
|
|
221
|
+
}
|
|
166
222
|
}
|
|
167
223
|
|
|
168
224
|
console.log("\n✨ Atualização concluída com sucesso!");
|
|
@@ -1,188 +1,266 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* OpenClaw OS - Universal Setup Wizard
|
|
4
|
-
*
|
|
5
|
-
* Orquestrador principal que delega para os módulos lib/:
|
|
6
|
-
* - detect.js → detecção de ambiente (Docker/WSL2/Mac/Linux/VPS)
|
|
7
|
-
* - config.js → leitura/escrita JSON atômica + defaults
|
|
8
|
-
* - security.js → tokens, masking e verificação de porta
|
|
9
|
-
* - channels.js → validação e configuração de canais
|
|
10
|
-
*
|
|
11
|
-
* Princípios:
|
|
12
|
-
* - Seguro por padrão: bind localhost + auth token
|
|
13
|
-
* - Cross-platform: detecção automática de ambiente
|
|
14
|
-
* - Não destrutivo: nunca sobrescreve sem confirmação
|
|
15
|
-
*
|
|
16
|
-
* @module config_wizard
|
|
17
|
-
* @version 2.0.0
|
|
18
|
-
* @author OpenClaw DevOps
|
|
19
|
-
*/
|
|
1
|
+
// lib/setup/config_wizard.js
|
|
20
2
|
const fs = require("fs");
|
|
21
3
|
const os = require("os");
|
|
22
4
|
const path = require("path");
|
|
23
|
-
const
|
|
5
|
+
const crypto = require("crypto");
|
|
24
6
|
|
|
25
|
-
// Módulos extraídos para lib/
|
|
7
|
+
// Módulos extraídos para lib/
|
|
26
8
|
const { detectEnvironment } = require("../detect");
|
|
27
|
-
const { readJsonSafe, writeJsonSafe,
|
|
9
|
+
const { readJsonSafe, writeJsonSafe, initConfigDefaults } = require("../config");
|
|
28
10
|
const { mask, generateToken, portInUse } = require("../security");
|
|
29
11
|
const { supportedChannels, configureChannel } = require("../channels");
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
13
|
+
// util simples
|
|
14
|
+
function exists(p) { try { return fs.existsSync(p); } catch { return false; } }
|
|
15
|
+
|
|
16
|
+
async function ask(prompt) {
|
|
17
|
+
const readline = require("readline");
|
|
18
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
return new Promise((res) => rl.question(prompt, (ans) => { rl.close(); res(ans.trim()); }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// CONSENT-FIRST: cria arquivo somente com apply + confirmação
|
|
23
|
+
async function ensureFileWithConsent({ filePath, content, flags }) {
|
|
24
|
+
if (exists(filePath)) {
|
|
25
|
+
console.log(`✅ KEEP ${path.basename(filePath)} (já existe)`);
|
|
26
|
+
return { changed: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`📝 PLAN criar ${path.basename(filePath)} (não existe)`);
|
|
30
|
+
|
|
31
|
+
if (!flags.apply) {
|
|
32
|
+
return { changed: false, planned: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!flags.yes) {
|
|
36
|
+
const ok = (await ask(`Criar ${path.basename(filePath)}? (y/N): `)).toLowerCase() === "y";
|
|
37
|
+
if (!ok) {
|
|
38
|
+
console.log(`⏹️ SKIP ${path.basename(filePath)}`);
|
|
39
|
+
return { changed: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
44
|
+
console.log(`✅ DONE ${path.basename(filePath)}`);
|
|
45
|
+
return { changed: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deepClone(obj) {
|
|
49
|
+
return JSON.parse(JSON.stringify(obj));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function diffKeys(before, after) {
|
|
53
|
+
// diff simples: mostra chaves alteradas
|
|
54
|
+
const changes = [];
|
|
55
|
+
const walk = (a, b, prefix = "") => {
|
|
56
|
+
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
const p = prefix ? `${prefix}.${k}` : k;
|
|
59
|
+
const va = a ? a[k] : undefined;
|
|
60
|
+
const vb = b ? b[k] : undefined;
|
|
61
|
+
const oba = va && typeof va === "object" && !Array.isArray(va);
|
|
62
|
+
const obb = vb && typeof vb === "object" && !Array.isArray(vb);
|
|
63
|
+
if (oba && obb) walk(va, vb, p);
|
|
64
|
+
else if (JSON.stringify(va) !== JSON.stringify(vb)) changes.push(p);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(before, after, "");
|
|
68
|
+
return changes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = async function configWizard({ base, flags, templatesDir } = {}) {
|
|
72
|
+
const targetBase = base || process.cwd();
|
|
73
|
+
const f = flags || { plan: true, apply: false, yes: false, force: false };
|
|
74
|
+
|
|
75
|
+
// Segurança: default é PLAN (se alguém chamar sem flags)
|
|
76
|
+
if (!("apply" in f) && !("plan" in f)) {
|
|
77
|
+
f.plan = true;
|
|
78
|
+
f.apply = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("\n🧙 OpenClaw Setup Wizard — CONSENT-FIRST");
|
|
82
|
+
console.log(`📍 Base: ${targetBase}`);
|
|
83
|
+
console.log(`🧪 Mode: ${f.apply ? "APPLY" : "PLAN (read-only)"}`);
|
|
84
|
+
|
|
59
85
|
const env = detectEnvironment();
|
|
60
|
-
console.log(
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
console.log(`🌍 Ambiente: ${env}`);
|
|
87
|
+
|
|
88
|
+
const configPath = path.join(targetBase, "openclaw.json");
|
|
89
|
+
const hasConfig = exists(configPath);
|
|
90
|
+
|
|
91
|
+
// 1) Inspecionar contexto
|
|
92
|
+
console.log("\n1) Contexto:");
|
|
93
|
+
console.log(`- openclaw.json: ${hasConfig ? "encontrado" : "não encontrado"}`);
|
|
94
|
+
|
|
95
|
+
// Se não tem config, em PLAN só sugere; em APPLY pergunta se quer criar.
|
|
96
|
+
let config = hasConfig ? readJsonSafe(configPath) : null;
|
|
97
|
+
if (!config) {
|
|
98
|
+
console.log("\n📝 PLAN criar openclaw.json (não existe ou inválido)");
|
|
99
|
+
if (!f.apply) {
|
|
100
|
+
console.log("ℹ️ Dica: rode `openclaw setup --apply` para criar/configurar.");
|
|
101
|
+
} else {
|
|
102
|
+
if (!f.yes) {
|
|
103
|
+
const ok = (await ask("Criar openclaw.json básico? (y/N): ")).toLowerCase() === "y";
|
|
104
|
+
if (!ok) {
|
|
105
|
+
console.log("❎ Cancelado. Nenhuma alteração feita.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
config = {};
|
|
72
110
|
}
|
|
73
|
-
config = parsed;
|
|
74
111
|
}
|
|
75
112
|
|
|
76
|
-
//
|
|
77
|
-
config =
|
|
113
|
+
// Se chegou aqui e config ainda é null (por PLAN, por exemplo), vamos simular o defaults para o diff
|
|
114
|
+
if (!config) config = {};
|
|
78
115
|
|
|
79
|
-
|
|
116
|
+
// 2) Propor ajustes seguros (NÃO aplicar ainda)
|
|
117
|
+
const before = deepClone(config);
|
|
80
118
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
config = initConfigDefaults(config);
|
|
120
|
+
|
|
121
|
+
// gateway.bind seguro
|
|
122
|
+
config.gateway = config.gateway || {};
|
|
123
|
+
config.gateway.bind = config.gateway.bind || "127.0.0.1";
|
|
124
|
+
if (config.gateway.bind !== "127.0.0.1") {
|
|
125
|
+
if (f.apply) {
|
|
126
|
+
const ans = f.yes ? "y" : await ask(`gateway.bind está "${config.gateway.bind}". Ajustar para "127.0.0.1"? (y/n): `);
|
|
127
|
+
if (ans.toLowerCase() === "y") config.gateway.bind = "127.0.0.1";
|
|
128
|
+
} else {
|
|
129
|
+
config.gateway.bind = "127.0.0.1"; // Simulando a mudança para o diff no modo PLAN
|
|
130
|
+
}
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
// auth mode seguro
|
|
134
|
+
config.auth = config.auth || {};
|
|
135
|
+
config.auth.mode = config.auth.mode || "token";
|
|
136
|
+
if (config.auth.mode !== "token") {
|
|
137
|
+
if (f.apply) {
|
|
138
|
+
const ans = f.yes ? "y" : await ask(`auth.mode está "${config.auth.mode}". Ajustar para "token"? (y/n): `);
|
|
139
|
+
if (ans.toLowerCase() === "y") config.auth.mode = "token";
|
|
140
|
+
} else {
|
|
141
|
+
config.auth.mode = "token"; // Simulação
|
|
142
|
+
}
|
|
97
143
|
}
|
|
98
144
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
config.auth.token = generateToken();
|
|
106
|
-
console.log(`✔ Token gerado: ${mask(config.auth.token)} (salvo no openclaw.json)`);
|
|
107
|
-
needWrite = true;
|
|
145
|
+
if (!config.auth.token) {
|
|
146
|
+
// gera token somente se APPLY e com consentimento
|
|
147
|
+
if (f.apply) {
|
|
148
|
+
if (!f.yes) {
|
|
149
|
+
const ok = (await ask("Gerar token de auth automaticamente? (y/N): ")).toLowerCase() === "y";
|
|
150
|
+
if (ok) config.auth.token = generateToken();
|
|
108
151
|
} else {
|
|
109
|
-
|
|
110
|
-
if (manual) { config.auth.token = manual; needWrite = true; }
|
|
152
|
+
config.auth.token = generateToken();
|
|
111
153
|
}
|
|
112
154
|
} else {
|
|
113
|
-
|
|
155
|
+
config.auth.token = "<TOKEN_GERADO_NO_APPLY>"; // Simulação para o diff
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3) Canais: apenas PLANO por padrão; em APPLY perguntar
|
|
160
|
+
console.log("\n2) Canais disponíveis:", supportedChannels().join(", "));
|
|
161
|
+
if (!f.apply) {
|
|
162
|
+
console.log("🧭 PLAN (não vou alterar canais sem --apply)");
|
|
163
|
+
} else {
|
|
164
|
+
const pick = f.yes ? "" : await ask(`Qual canal ativar? (${supportedChannels().join("/")}/nenhum): `);
|
|
165
|
+
const channelChoice = (pick || "").toLowerCase();
|
|
166
|
+
if (supportedChannels().includes(channelChoice)) {
|
|
167
|
+
if (!f.yes) console.log(`ℹ️ Vou configurar '${channelChoice}'.`);
|
|
168
|
+
await configureChannel(config, channelChoice, ask);
|
|
114
169
|
}
|
|
115
170
|
}
|
|
116
171
|
|
|
117
|
-
//
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
172
|
+
// Filesystem allowlist
|
|
173
|
+
if (f.apply) {
|
|
174
|
+
console.log("\n📁 Acesso a arquivos locais (mínimo necessário)");
|
|
175
|
+
console.log("Adicione apenas pastas que o OpenClaw realmente precisa acessar.");
|
|
176
|
+
const addPath = await ask("Adicionar uma pasta allowlist agora? (caminho ou ENTER para pular): ");
|
|
177
|
+
if (addPath) {
|
|
178
|
+
const resolved = addPath.replace(/^~\//, os.homedir() + path.sep);
|
|
179
|
+
config.filesystem.allowlist.push(resolved);
|
|
180
|
+
console.log(`✔ Allowlist adicionada: ${resolved}`);
|
|
122
181
|
}
|
|
123
182
|
}
|
|
124
183
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
184
|
+
// 4) Mostrar plano de mudanças no JSON
|
|
185
|
+
const changedPaths = diffKeys(before, config);
|
|
186
|
+
console.log("\n3) Plano de mudanças em openclaw.json:");
|
|
187
|
+
if (changedPaths.length === 0) console.log("- (nenhuma mudança necessária)");
|
|
188
|
+
else changedPaths.forEach(p => console.log(`- ${p}`));
|
|
130
189
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (configured) needWrite = true;
|
|
190
|
+
// 5) Aplicar mudanças somente com consentimento
|
|
191
|
+
if (!f.apply) {
|
|
192
|
+
console.log("\n✅ Setup finalizado em PLAN. Nenhuma alteração aplicada.");
|
|
135
193
|
} else {
|
|
136
|
-
|
|
194
|
+
let shouldWrite = true;
|
|
195
|
+
if (changedPaths.length > 0) {
|
|
196
|
+
if (!f.yes) {
|
|
197
|
+
const ok = (await ask("\nAplicar alterações em openclaw.json? (y/N): ")).toLowerCase() === "y";
|
|
198
|
+
if (!ok) {
|
|
199
|
+
console.log("❎ Cancelado. openclaw.json não será alterado.");
|
|
200
|
+
shouldWrite = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (shouldWrite) {
|
|
204
|
+
writeJsonSafe(configPath, config);
|
|
205
|
+
console.log(`✅ DONE openclaw.json atualizado em ${configPath}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (!hasConfig && shouldWrite) {
|
|
209
|
+
writeJsonSafe(configPath, config);
|
|
210
|
+
console.log(`✅ DONE openclaw.json criado em ${configPath}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
137
213
|
}
|
|
138
214
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
config.filesystem.allowlist.push(resolved);
|
|
146
|
-
needWrite = true;
|
|
147
|
-
console.log(`✔ Allowlist adicionada: ${resolved}`);
|
|
148
|
-
}
|
|
215
|
+
// 6) Persistência por projeto (IDE option B): somente com consentimento
|
|
216
|
+
const agentDir = path.join(targetBase, ".agent");
|
|
217
|
+
const stateDir = path.join(agentDir, "state");
|
|
218
|
+
|
|
219
|
+
console.log("\n4) Persistência por projeto (.agent/state):");
|
|
220
|
+
console.log("🧭 PLAN criar MEMORY.md / SOUL.md / AGENTS.md somente com consentimento");
|
|
149
221
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const inUse = await portInUse("127.0.0.1", port);
|
|
159
|
-
if (inUse) console.log(`ℹ Porta ${port} respondeu em 127.0.0.1 (ok se OpenClaw está rodando).`);
|
|
160
|
-
else console.log(`ℹ Porta ${port} não respondeu em 127.0.0.1 (ok se ainda não iniciou).`);
|
|
161
|
-
|
|
162
|
-
// --- 11. Hardening: recomendações para VPS ---
|
|
163
|
-
if (env === "linux-vps-root") {
|
|
164
|
-
console.log("\n🛡 Hardening (recomendado)");
|
|
165
|
-
console.log("- Crie um usuário não-root (ex: clawuser) e desative login por senha no SSH.");
|
|
166
|
-
console.log("- Ative firewall (UFW) e fail2ban.");
|
|
167
|
-
console.log("- Exponha publicamente apenas WireGuard (UDP) se usar VPN.");
|
|
222
|
+
// Só criamos state se já existir .agent ou se usuário quiser criar (em apply)
|
|
223
|
+
if (f.apply && !exists(agentDir)) {
|
|
224
|
+
if (!f.yes) {
|
|
225
|
+
const ok = (await ask("Criar pasta .agent/ (para estado por projeto)? (y/N): ")).toLowerCase() === "y";
|
|
226
|
+
if (ok) fs.mkdirSync(stateDir, { recursive: true });
|
|
227
|
+
} else {
|
|
228
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
229
|
+
}
|
|
168
230
|
}
|
|
169
231
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
232
|
+
if (exists(agentDir) || exists(stateDir)) {
|
|
233
|
+
if (!exists(stateDir) && f.apply) fs.mkdirSync(stateDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
await ensureFileWithConsent({
|
|
236
|
+
filePath: path.join(stateDir, "MEMORY.md"),
|
|
237
|
+
content: "# MEMORY.md (por projeto)\n\n- Resumos úteis, decisões e preferências persistentes do projeto.\n",
|
|
238
|
+
flags: f
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await ensureFileWithConsent({
|
|
242
|
+
filePath: path.join(stateDir, "SOUL.md"),
|
|
243
|
+
content: "# SOUL.md\n\n- Identidade e regras de comportamento do agente.\n",
|
|
244
|
+
flags: f
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await ensureFileWithConsent({
|
|
248
|
+
filePath: path.join(stateDir, "AGENTS.md"),
|
|
249
|
+
content: "# AGENTS.md\n\n- Agentes e personas disponíveis neste projeto.\n",
|
|
250
|
+
flags: f
|
|
251
|
+
});
|
|
174
252
|
} else {
|
|
175
|
-
console.log("
|
|
253
|
+
console.log("ℹ️ .agent não existe neste projeto. (ok) — nada será criado em PLAN.");
|
|
176
254
|
}
|
|
177
255
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
256
|
+
// Checagens adicionais informativas (Healthcheck da porta)
|
|
257
|
+
if (f.apply) {
|
|
258
|
+
const port = 18789;
|
|
259
|
+
console.log("\n🔎 Checagens rápidas");
|
|
260
|
+
const inUse = await portInUse("127.0.0.1", port);
|
|
261
|
+
if (inUse) console.log(`ℹ Porta ${port} respondeu em 127.0.0.1 (ok se OpenClaw está rodando).`);
|
|
262
|
+
else console.log(`ℹ Porta ${port} não respondeu em 127.0.0.1 (ok se ainda não iniciou).`);
|
|
263
|
+
}
|
|
184
264
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
process.exit(1);
|
|
188
|
-
});
|
|
265
|
+
console.log("\n✅ Wizard concluído.");
|
|
266
|
+
};
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabioforest/openclaw",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Agentes autônomos para engenharia de software",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Roteamento de Skill Padrão e Seguro (Chat-first Workflow)
|
|
3
|
+
---
|
|
4
|
+
# Chat-first Workflow (Roteamento Inteligente)
|
|
5
|
+
|
|
6
|
+
O OpenClaw adota um fluxo "chat-first" nas interações com o desenvolvedor, onde o Agent sempre entra como o Roteador central (descrito em `.agent/skills/openclaw-router/SKILL.md`) antes de selecionar qualquer action.
|
|
7
|
+
|
|
8
|
+
## Passos Operacionais (Agent Actions)
|
|
9
|
+
1. **INSPECT**: A partir do prompt do usuário, identifique a real necessidade invocando a análise do repositório/arquivos locais. Nunca tome decisões sem checar o ambiente `.agent/`.
|
|
10
|
+
2. **ROUTE**: Baseado na intenção identificada, ative mentalmente a Skill correspondente. Se o usuário pedir um script de deploy, ative `devops-toolkit`. Se pedir frontend component, ative `openclaw-dev`.
|
|
11
|
+
3. **PLAN**: Com a Skill ativada, escreva um `.md` no diretório temporário/contextual descrevendo sua tática baseada nas regras do repositório para resolver o problema, solicitando feedback com o comando correspondente ao final da sua mensagem (`/plan`, `/execute`).
|
|
12
|
+
4. **CONSENT**: Aguarde a confirmação de que o plano pode ser seguido (salvo explicitamente em `--yes` outputs passados pelo desenvolvedor).
|
|
13
|
+
5. **APPLY**: Execute os comandos na máquina, gere ou atualize código.
|
|
14
|
+
6. **AUDIT**: Notifique via CLI Audit (se usando CLI) ou inclua logs para os registros em `MEMORY.md`.
|
|
15
|
+
|
|
16
|
+
> **MANDATORY**: Nunca gere ou modifique configurações sem antes consultar e entender o projeto através do `inspect` e da lista de Skills aprovadas (em `openclaw.json` ou `AGENTS.md`).
|