@fabioforest/openclaw 3.9.0 → 3.10.2
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 +9 -3
- package/bin/openclaw.js +14 -6
- package/lib/cli/ide.js +62 -83
- package/lib/cli/init.js +52 -69
- package/lib/cli/uninstall.js +69 -78
- package/lib/cli/update.js +93 -89
- package/lib/context/context-engine.js +89 -0
- package/lib/core/orchestrator.js +113 -0
- package/lib/utils/scope_guard.js +123 -0
- package/package.json +1 -1
- package/templates/.agent/skills/workspace-snapshot/SKILL.md +20 -0
- package/templates/.agent/workflows/chat-router.md +19 -0
- package/templates/ide/.cursor/rules/openclaw.mdc +11 -0
- package/templates/ide/.cursorrules +7 -0
- package/templates/ide/.github/copilot-instructions.md +11 -0
package/README.md
CHANGED
|
@@ -280,6 +280,7 @@ npx @fabioforest/openclaw ide install --apply --force
|
|
|
280
280
|
│ ├── mission_control.json # Empresa de Agentes
|
|
281
281
|
│ └── MEMORY.md # Memória do workspace
|
|
282
282
|
└── workflows/ # Slash commands e runbooks
|
|
283
|
+
└── chat-first.md # Workflow default de roteamento inteligente
|
|
283
284
|
```
|
|
284
285
|
|
|
285
286
|
---
|
|
@@ -446,6 +447,7 @@ Além de skills isoladas, o OpenClaw traz **fluxos de trabalho completos** (runb
|
|
|
446
447
|
|
|
447
448
|
| Workflow | Descrição | Comando Trigger |
|
|
448
449
|
|----------|-----------|-----------------|
|
|
450
|
+
| **`chat-first`** | **Roteamento Seguro** via INSPECT e PLAN antes de agira | Workflow Padrão (Core) |
|
|
449
451
|
| **`ai-capture`** | Captura inteligente de dados/tickets usando IA | *"Iniciar captura de dados"* |
|
|
450
452
|
| **`doctor`** | Diagnóstico e reparo automático do ambiente | `openclaw doctor` |
|
|
451
453
|
| **`healthcheck`** | Verificação rápida de saúde (API, DB, cache) | `openclaw healthcheck` |
|
|
@@ -458,15 +460,18 @@ Workflows são arquivos `.md` em `.agent/workflows/` que o agente lê e executa
|
|
|
458
460
|
|
|
459
461
|
## 🔒 Segurança
|
|
460
462
|
|
|
461
|
-
O OpenClaw segue
|
|
463
|
+
O OpenClaw segue 4 princípios fundamentais:
|
|
462
464
|
|
|
463
465
|
### 1. Read-only por padrão
|
|
464
|
-
Todo comando opera em **modo PLAN** (simulação). Nada é alterado sem `--apply`.
|
|
466
|
+
Todo comando opera em **modo PLAN** (simulação). Nada é alterado sem `--apply`. A flag `--apply` tem precedência absoluta, substituindo comportamento visual de planejamento.
|
|
465
467
|
|
|
466
468
|
### 2. Consent-first
|
|
467
469
|
Antes de qualquer alteração, o sistema mostra exatamente o que vai fazer e pede confirmação. Ações destrutivas exigem **confirmação forte** (digitar frase específica).
|
|
468
470
|
|
|
469
|
-
### 3.
|
|
471
|
+
### 3. Scope Guard (Proteção Anti-Destruição)
|
|
472
|
+
Qualquer tentativa do CLI de criar, deletar ou sobrescrever arquivos que estejam **fora** da bolha de segurança `.agent/` falha por padrão com o Módulo Scope Guard em ação total para isolar agentes. Pode ser ignorado interativamente no terminal digitando senhas de perigo.
|
|
473
|
+
|
|
474
|
+
### 4. Audit-first
|
|
470
475
|
Toda ação gera log detalhado em `.agent/audit/` com timestamp, comando, modo, contexto e resultado.
|
|
471
476
|
|
|
472
477
|
### Proteções ativas
|
|
@@ -474,6 +479,7 @@ Toda ação gera log detalhado em `.agent/audit/` com timestamp, comando, modo,
|
|
|
474
479
|
| Proteção | Como funciona |
|
|
475
480
|
|----------|---------------|
|
|
476
481
|
| **Hook pre-tool-use** | Bloqueia 12+ padrões destrutivos (`rm -rf`, `mkfs`, `dd`, `shutdown`) |
|
|
482
|
+
| **Scope Guard** | Protege arquivos/framework nativos do projeto/usuário host de reescritas inadvertidas CLI |
|
|
477
483
|
| **VPN-first** | Sem VPN, sem acesso remoto |
|
|
478
484
|
| **Bind localhost** | Serviços só acessíveis localmente por padrão |
|
|
479
485
|
| **Auth token** | Token obrigatório para acesso |
|
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 { executeAction } = require("../core/orchestrator");
|
|
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 });
|
|
@@ -76,6 +65,9 @@ async function runInstall({ targetPath, flags }) {
|
|
|
76
65
|
console.log(" 📦 COPY templates/.agent -> .agent/");
|
|
77
66
|
}
|
|
78
67
|
|
|
68
|
+
const ideSrc = path.join(TEMPLATES_DIR, "ide");
|
|
69
|
+
console.log(" 🌟 ADDON Adaptadores de IDE (.cursorrules, copilot-instructions, .mdc)");
|
|
70
|
+
|
|
79
71
|
// State templates (mission_control.json, MEMORY.md)
|
|
80
72
|
const stateDir = path.join(agentDst, "state");
|
|
81
73
|
if (!fs.existsSync(stateDir)) {
|
|
@@ -84,78 +76,65 @@ async function runInstall({ targetPath, flags }) {
|
|
|
84
76
|
console.log(" ✅ KEEP .agent/state/ (já existe)");
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
// 2.5 Scope Guard
|
|
80
|
+
const intents = {
|
|
81
|
+
writes: [
|
|
82
|
+
agentDst,
|
|
83
|
+
stateDir,
|
|
84
|
+
path.join(targetPath, ".cursorrules"),
|
|
85
|
+
path.join(targetPath, ".github"),
|
|
86
|
+
path.join(targetPath, ".cursor")
|
|
87
|
+
],
|
|
88
|
+
deletes: [],
|
|
89
|
+
overwrites: []
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const confirmationWord = (flags.force && fs.existsSync(agentDst)) ? "DELETE .agent" : null;
|
|
93
|
+
|
|
94
|
+
await executeAction({
|
|
95
|
+
actionName: "ide install",
|
|
96
|
+
context: ctx,
|
|
97
|
+
flags,
|
|
98
|
+
intents,
|
|
99
|
+
targetPath,
|
|
100
|
+
confirmationWord,
|
|
101
|
+
planFn: async () => { }, // O console.log inicial já fez o plan visual acima
|
|
102
|
+
executeFn: async () => {
|
|
103
|
+
if (fs.existsSync(agentDst) && flags.force) {
|
|
104
|
+
fs.rmSync(agentDst, { recursive: true, force: true });
|
|
100
105
|
}
|
|
101
|
-
} else {
|
|
102
|
-
const ok = await ask("\nAplicar instalação IDE? (y/N): ");
|
|
103
|
-
if (ok.toLowerCase() !== "y") {
|
|
104
|
-
console.log("⏹️ Cancelado.");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Execução
|
|
111
|
-
try {
|
|
112
|
-
console.log("\n🚀 Executando...");
|
|
113
|
-
|
|
114
|
-
if (fs.existsSync(agentDst) && flags.force) {
|
|
115
|
-
fs.rmSync(agentDst, { recursive: true, force: true });
|
|
116
|
-
audit.push("- ACT: DELETED .agent/");
|
|
117
|
-
}
|
|
118
106
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
107
|
+
const isMerge = fs.existsSync(agentDst);
|
|
108
|
+
copyDirRecursive(agentSrc, agentDst, undefined, isMerge);
|
|
109
|
+
copyDirRecursive(ideSrc, targetPath, undefined, true); // Copiar/Merge ide adaptors
|
|
110
|
+
|
|
111
|
+
// Criar state se necessário
|
|
112
|
+
const stateTarget = path.join(agentDst, "state");
|
|
113
|
+
if (!fs.existsSync(stateTarget)) {
|
|
114
|
+
fs.mkdirSync(stateTarget, { recursive: true });
|
|
115
|
+
// Criar mission_control.json default
|
|
116
|
+
const mcDefault = {
|
|
117
|
+
project_status: "active",
|
|
118
|
+
project_name: path.basename(targetPath),
|
|
119
|
+
sprint_goal: "",
|
|
120
|
+
agents: [
|
|
121
|
+
{ id: "orchestrator", role: "orchestrator", active: true },
|
|
122
|
+
{ id: "researcher", role: "researcher", active: true },
|
|
123
|
+
{ id: "writer", role: "writer", active: true },
|
|
124
|
+
],
|
|
125
|
+
task_queue: [],
|
|
126
|
+
history: [],
|
|
127
|
+
settings: {
|
|
128
|
+
work_dir: "mission_control",
|
|
129
|
+
max_tasks_per_tick: 2,
|
|
130
|
+
default_priority: "medium",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
fs.writeFileSync(path.join(stateTarget, "mission_control.json"), JSON.stringify(mcDefault, null, 2));
|
|
134
|
+
fs.writeFileSync(path.join(stateTarget, "MEMORY.md"), "# Memória Persistente\n\n(Adicione aqui resumos e decisões importantes)\n");
|
|
135
|
+
}
|
|
148
136
|
}
|
|
149
|
-
|
|
150
|
-
console.log("\n✨ IDE install concluído com sucesso!");
|
|
151
|
-
writeAudit(targetPath, audit, flags);
|
|
152
|
-
|
|
153
|
-
} catch (err) {
|
|
154
|
-
console.error(`\n❌ Falha: ${err.message}`);
|
|
155
|
-
audit.push(`\n## ERROR: ${err.message}`);
|
|
156
|
-
writeAudit(targetPath, audit, flags);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
137
|
+
});
|
|
159
138
|
}
|
|
160
139
|
|
|
161
140
|
/**
|
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 { executeAction } = require("../core/orchestrator");
|
|
17
18
|
|
|
18
19
|
// Caminho dos templates incluídos no pacote
|
|
19
20
|
const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
|
|
@@ -107,85 +108,67 @@ async function run({ targetPath, flags }) {
|
|
|
107
108
|
actions.push({ type: "NOOP", path: configPath, reason: "Config exists" });
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
|
|
113
|
-
|
|
111
|
+
// 2.5. Acionar Scope Guard
|
|
112
|
+
const intents = { writes: [], deletes: [], overwrites: [] };
|
|
114
113
|
for (const a of actions) {
|
|
115
|
-
if (a.type === "DELETE_DIR")
|
|
116
|
-
if (a.type === "CREATE_DIR")
|
|
117
|
-
if (a.type === "
|
|
118
|
-
if (a.type === "
|
|
119
|
-
if (a.type === "CREATE_FILE") console.log(` 📝 CREATE ${safeRel(targetPath, a.path)}`);
|
|
120
|
-
if (a.type === "NOOP") console.log(` ✅ KEEP ${safeRel(targetPath, a.path)}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (planMode) {
|
|
124
|
-
console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
|
|
125
|
-
console.log(" Para aplicar, rode: npx openclaw init --apply [--merge|--force]");
|
|
126
|
-
return;
|
|
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);
|
|
127
118
|
}
|
|
128
119
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
// Definir Palavra de Confirmação Forte se houver Force Delete
|
|
121
|
+
const hasDelete = actions.some(a => a.type === "DELETE_DIR");
|
|
122
|
+
const confirmationWord = hasDelete ? "DELETE .agent" : null;
|
|
123
|
+
|
|
124
|
+
// Delegar todo o fluxo final (Guard, Confirm, Execute, Audit) para o Orchestrator
|
|
125
|
+
await executeAction({
|
|
126
|
+
actionName: "init",
|
|
127
|
+
context: ctx,
|
|
128
|
+
flags,
|
|
129
|
+
intents,
|
|
130
|
+
targetPath,
|
|
131
|
+
confirmationWord,
|
|
132
|
+
planFn: async () => {
|
|
133
|
+
console.log(`\n🧭 Plano de Execução:\n`);
|
|
134
|
+
console.log(` Contexto: ${ctx.env.platform} | IDE: ${ctx.ide}\n`);
|
|
135
|
+
for (const a of actions) {
|
|
136
|
+
if (a.type === "DELETE_DIR") console.log(` 🔥 DELETE ${safeRel(targetPath, a.path)} (${a.reason})`);
|
|
137
|
+
if (a.type === "CREATE_DIR") console.log(` 📁 CREATE ${safeRel(targetPath, a.path)}`);
|
|
138
|
+
if (a.type === "COPY_DIR") console.log(` 📦 COPY templates -> ${safeRel(targetPath, a.to)}`);
|
|
139
|
+
if (a.type === "MERGE_DIR") console.log(` 🔄 MERGE templates -> ${safeRel(targetPath, a.to)} (Preservando existentes)`);
|
|
140
|
+
if (a.type === "CREATE_FILE") console.log(` 📝 CREATE ${safeRel(targetPath, a.path)}`);
|
|
141
|
+
if (a.type === "NOOP") console.log(` ✅ KEEP ${safeRel(targetPath, a.path)}`);
|
|
137
142
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
},
|
|
144
|
+
executeFn: async () => {
|
|
145
|
+
for (const a of actions) {
|
|
146
|
+
if (a.type === "DELETE_DIR") {
|
|
147
|
+
fs.rmSync(a.path, { recursive: true, force: true });
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
fs.rmSync(a.path, { recursive: true, force: true });
|
|
154
|
-
audit.push(`- ACT: DELETED ${a.path}`);
|
|
151
|
+
// Executar cópia/merge se necessário
|
|
152
|
+
const copyAction = actions.find(a => a.type === "COPY_DIR" || a.type === "MERGE_DIR");
|
|
153
|
+
if (copyAction) {
|
|
154
|
+
const isMerge = copyAction.type === "MERGE_DIR";
|
|
155
|
+
copyDirRecursive(TEMPLATES_DIR, agentDir, undefined, isMerge);
|
|
156
|
+
console.log(` ✅ Templates processados.`);
|
|
155
157
|
}
|
|
156
|
-
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(` ✅ Templates processados.`);
|
|
165
|
-
}
|
|
159
|
+
// Criar config se necessário
|
|
160
|
+
if (actions.some(a => a.type === "CREATE_FILE" && a.path === configPath)) {
|
|
161
|
+
const defaults = initConfigDefaults({});
|
|
162
|
+
writeJsonSafe(configPath, defaults);
|
|
163
|
+
console.log(` ✅ Config criada.`);
|
|
164
|
+
}
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
audit.push(`- ACT: CREATED openclaw.json`);
|
|
172
|
-
console.log(` ✅ Config criada.`);
|
|
166
|
+
// Gravar contexto se ainda não existe
|
|
167
|
+
const contextDir = path.join(agentDir, "context");
|
|
168
|
+
if (!fs.existsSync(contextDir)) fs.mkdirSync(contextDir, { recursive: true });
|
|
169
|
+
fs.writeFileSync(path.join(contextDir, "context.json"), JSON.stringify(ctx, null, 2));
|
|
173
170
|
}
|
|
174
|
-
|
|
175
|
-
// Gravar contexto se ainda não existe
|
|
176
|
-
const contextDir = path.join(agentDir, "context");
|
|
177
|
-
if (!fs.existsSync(contextDir)) fs.mkdirSync(contextDir, { recursive: true });
|
|
178
|
-
fs.writeFileSync(path.join(contextDir, "context.json"), JSON.stringify(ctx, null, 2));
|
|
179
|
-
|
|
180
|
-
console.log("\n✨ Concluído com sucesso!");
|
|
181
|
-
writeAudit(targetPath, audit, flags);
|
|
182
|
-
|
|
183
|
-
} catch (err) {
|
|
184
|
-
console.error(`\n❌ Falha na execução: ${err.message}`);
|
|
185
|
-
audit.push(`\n## ERROR: ${err.message}`);
|
|
186
|
-
writeAudit(targetPath, audit, flags);
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
171
|
+
});
|
|
189
172
|
}
|
|
190
173
|
|
|
191
174
|
module.exports = { run, copyDirRecursive };
|
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 { executeAction } = require("../core/orchestrator");
|
|
18
19
|
|
|
19
20
|
function ask(q) {
|
|
20
21
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -103,92 +104,82 @@ async function run({ targetPath, flags }) {
|
|
|
103
104
|
console.log(` 🔴 REMOVER openclaw.json`);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
|
|
127
|
-
console.log(" Para desinstalar, rode: npx @fabioforest/openclaw uninstall --apply");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Modo APPLY: pedir confirmação forte
|
|
132
|
-
console.log("");
|
|
133
|
-
if (!flags.yes) {
|
|
134
|
-
const confirm = await ask("⚠️ Digite 'UNINSTALL' para confirmar a remoção: ");
|
|
135
|
-
if (confirm !== "UNINSTALL") {
|
|
136
|
-
console.log("⏹️ Cancelado. Nada foi removido.");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
107
|
+
// Acionar Orquestrador
|
|
108
|
+
const intents = { writes: [], deletes: toRemove.map(i => i.path), overwrites: [] };
|
|
109
|
+
|
|
110
|
+
await executeAction({
|
|
111
|
+
actionName: "uninstall",
|
|
112
|
+
context: ctx,
|
|
113
|
+
flags,
|
|
114
|
+
intents,
|
|
115
|
+
targetPath,
|
|
116
|
+
skipAudit: true, // Audit manual pós-remoção
|
|
117
|
+
confirmationWord: "UNINSTALL",
|
|
118
|
+
planFn: async () => {
|
|
119
|
+
// Verificar audit logs que seriam perdidos
|
|
120
|
+
const auditDir = path.join(agentDir, "audit");
|
|
121
|
+
if (fs.existsSync(auditDir)) {
|
|
122
|
+
const auditCount = countFiles(auditDir);
|
|
123
|
+
if (auditCount > 0) {
|
|
124
|
+
console.log(`\n ⚠️ ${auditCount} log(s) de auditoria serão perdidos!`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
140
127
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
128
|
+
// Verificar state que seria perdido
|
|
129
|
+
const stateDir = path.join(agentDir, "state");
|
|
130
|
+
if (fs.existsSync(stateDir)) {
|
|
131
|
+
const stateCount = countFiles(stateDir);
|
|
132
|
+
if (stateCount > 0) {
|
|
133
|
+
console.log(` ⚠️ ${stateCount} arquivo(s) de estado serão perdidos (mission_control, MEMORY)!`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
executeFn: async () => {
|
|
138
|
+
// Backup opcional
|
|
139
|
+
if (!flags.force) {
|
|
140
|
+
const doBackup = flags.yes ? "s" : await ask("\n💾 Fazer backup antes de remover? (S/n): ");
|
|
141
|
+
if (doBackup.toLowerCase() !== "n") {
|
|
142
|
+
const backupName = `.agent.backup-${Date.now()}`;
|
|
143
|
+
const backupPath = path.join(targetPath, backupName);
|
|
144
|
+
try {
|
|
145
|
+
fs.cpSync(agentDir, backupPath, { recursive: true });
|
|
146
|
+
console.log(` ✅ Backup criado: ${backupName}/`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(` ⚠️ Falha no backup: ${err.message}`);
|
|
149
|
+
const cont = await ask(" Continuar sem backup? (y/N): ");
|
|
150
|
+
if (cont.toLowerCase() !== "y") {
|
|
151
|
+
throw new Error("Usuário abortou por falha no backup.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
const audit = [getAuditHeader(ctx, "uninstall", flags)];
|
|
157
|
+
const audit = [getAuditHeader(ctx, "uninstall", flags)];
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
for (const item of toRemove) {
|
|
160
|
+
if (item.isDir) {
|
|
161
|
+
fs.rmSync(item.path, { recursive: true, force: true });
|
|
162
|
+
} else {
|
|
163
|
+
fs.unlinkSync(item.path);
|
|
164
|
+
}
|
|
165
|
+
console.log(` ✅ Removido: ${item.label}`);
|
|
166
|
+
audit.push(`- ACT: REMOVED ${item.label}`);
|
|
170
167
|
}
|
|
171
|
-
console.log(` ✅ Removido: ${item.label}`);
|
|
172
|
-
audit.push(`- ACT: REMOVED ${item.label}`);
|
|
173
|
-
}
|
|
174
168
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
169
|
+
console.log("\n✨ OpenClaw desinstalado com sucesso!");
|
|
170
|
+
console.log(" Para reinstalar: npx @fabioforest/openclaw init --apply\n");
|
|
171
|
+
|
|
172
|
+
// Gravar audit no diretório pai (já que .agent/ foi removido)
|
|
173
|
+
if (flags.audit !== false) {
|
|
174
|
+
const filename = `openclaw-uninstall-${new Date().toISOString().replace(/[:.]/g, "-")}.md`;
|
|
175
|
+
const auditPath = path.join(targetPath, filename);
|
|
176
|
+
try {
|
|
177
|
+
fs.writeFileSync(auditPath, audit.join("\n") + "\n", "utf8");
|
|
178
|
+
console.log(` 📝 Log de auditoria: ${filename}`);
|
|
179
|
+
} catch (e) { /* silencioso */ }
|
|
180
|
+
}
|
|
186
181
|
}
|
|
187
|
-
|
|
188
|
-
} catch (err) {
|
|
189
|
-
console.error(`\n❌ Falha: ${err.message}`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
182
|
+
});
|
|
192
183
|
}
|
|
193
184
|
|
|
194
185
|
module.exports = { run };
|
package/lib/cli/update.js
CHANGED
|
@@ -14,7 +14,7 @@ const path = require("path");
|
|
|
14
14
|
const crypto = require("crypto");
|
|
15
15
|
const readline = require("readline");
|
|
16
16
|
const { detectContext, getAuditHeader } = require("../context");
|
|
17
|
-
const {
|
|
17
|
+
const { executeAction } = require("../core/orchestrator");
|
|
18
18
|
|
|
19
19
|
// Caminho dos templates incluídos no pacote
|
|
20
20
|
const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
|
|
@@ -28,10 +28,7 @@ function safeRel(targetPath, p) {
|
|
|
28
28
|
return path.relative(targetPath, p);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// writeAudit
|
|
32
|
-
function writeAudit(targetPath, lines, flags) {
|
|
33
|
-
writeCliAudit(targetPath, lines, flags, "update");
|
|
34
|
-
}
|
|
31
|
+
// Removida prop writeAudit (o orquestrador ou o executeFn fará isso manualmente ou deixaremos rolar)
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
34
|
* Calcula o SHA-256 de um arquivo (Utilitário mantido)
|
|
@@ -123,106 +120,113 @@ async function run({ targetPath, flags }) {
|
|
|
123
120
|
process.exit(1);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
// 1. Planejar
|
|
123
|
+
// 1. Planejar arrays
|
|
127
124
|
const actions = planUpdates(TEMPLATES_DIR, agentDir);
|
|
128
|
-
const audit = [getAuditHeader(ctx, "update", flags)];
|
|
129
|
-
|
|
130
|
-
// 2. Exibir Plano
|
|
131
|
-
console.log(`\n🧭 Plano de Atualização (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
|
|
132
|
-
console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
|
|
133
125
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
// Mapear intencionalidades
|
|
127
|
+
const intents = { writes: [], deletes: [], overwrites: [] };
|
|
128
|
+
for (const a of actions.added) intents.writes.push(a.dest);
|
|
129
|
+
for (const a of actions.updated) intents.overwrites.push(a.dest);
|
|
130
|
+
|
|
131
|
+
await executeAction({
|
|
132
|
+
actionName: "update",
|
|
133
|
+
context: ctx,
|
|
134
|
+
flags,
|
|
135
|
+
intents,
|
|
136
|
+
targetPath,
|
|
137
|
+
skipConfirm: true, // Manter o loop de override manual
|
|
138
|
+
skipAudit: true, // Escrevemos manual no final por causa dos arquivos "skipped" interativos
|
|
139
|
+
planFn: async () => {
|
|
140
|
+
console.log(`\n🧭 Plano de Atualização:\n`);
|
|
141
|
+
console.log(` Contexto: ${ctx.env.platform} | IDE: ${ctx.ide}\n`);
|
|
142
|
+
|
|
143
|
+
if (actions.added.length > 0) {
|
|
144
|
+
console.log(`📄 Novos (${actions.added.length}):`);
|
|
145
|
+
actions.added.forEach(a => console.log(` + CREATE ${safeRel(targetPath, a.dest)}`));
|
|
146
|
+
}
|
|
147
|
+
if (actions.updated.length > 0) {
|
|
148
|
+
console.log(`\n🔄 Modificados (${actions.updated.length}):`);
|
|
149
|
+
actions.updated.forEach(a => {
|
|
150
|
+
console.log(` ~ UPDATE ${safeRel(targetPath, a.dest)} (Exige confirmação interativa)`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (actions.skipped.length > 0) {
|
|
154
|
+
console.log(`\n⏭️ Ignorados (${actions.skipped.length} arquivos idênticos)`);
|
|
155
|
+
}
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
if (actions.added.length === 0 && actions.updated.length === 0) {
|
|
158
|
+
console.log("\n✅ Tudo atualizado. Nenhuma alteração necessária.");
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
executeFn: async () => {
|
|
162
|
+
if (actions.added.length === 0 && actions.updated.length === 0) {
|
|
163
|
+
return; // Nothing to apply
|
|
164
|
+
}
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
// Confirmação inicial (se customizado)
|
|
167
|
+
if (!flags.yes && actions.updated.length === 0) {
|
|
168
|
+
const ok = await ask("\nAplicar e copiar arquivos novos? (y/N): ");
|
|
169
|
+
if (ok.toLowerCase() !== "y") {
|
|
170
|
+
console.log("⏹️ Cancelado.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
175
|
+
// Criar diretórios necessários
|
|
176
|
+
function ensureDir(p) {
|
|
177
|
+
const dir = path.dirname(p);
|
|
178
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
179
|
+
}
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
console.log("\n🚀 Executando atualizações...");
|
|
181
|
+
const audit = [getAuditHeader(ctx, "update", flags)];
|
|
172
182
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
// Action: Added
|
|
184
|
+
for (const action of actions.added) {
|
|
185
|
+
ensureDir(action.dest);
|
|
186
|
+
fs.copyFileSync(action.src, action.dest);
|
|
187
|
+
audit.push(`- ACT: CREATED ${safeRel(targetPath, action.dest)}`);
|
|
188
|
+
}
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
audit.push(`- ACT: CREATED ${safeRel(targetPath, action.dest)}`);
|
|
184
|
-
}
|
|
190
|
+
// Action: Updated (Conflict Resolver)
|
|
191
|
+
for (const action of actions.updated) {
|
|
192
|
+
ensureDir(action.dest);
|
|
193
|
+
const rPath = safeRel(targetPath, action.dest);
|
|
185
194
|
|
|
186
|
-
|
|
187
|
-
for (const action of actions.updated) {
|
|
188
|
-
ensureDir(action.dest);
|
|
189
|
-
const rPath = safeRel(targetPath, action.dest);
|
|
195
|
+
let overwrite = flags.yes || flags.force;
|
|
190
196
|
|
|
191
|
-
|
|
197
|
+
if (!overwrite) {
|
|
198
|
+
console.log(`\n⚠️ CONFLITO DETECTADO: ${rPath}`);
|
|
199
|
+
console.log("------------------------------------------------");
|
|
200
|
+
const oldContent = fs.readFileSync(action.dest, "utf-8");
|
|
201
|
+
const newContent = fs.readFileSync(action.src, "utf-8");
|
|
202
|
+
console.log(simpleDiff(oldContent, newContent));
|
|
203
|
+
console.log("------------------------------------------------");
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const oldContent = fs.readFileSync(action.dest, "utf-8");
|
|
197
|
-
const newContent = fs.readFileSync(action.src, "utf-8");
|
|
198
|
-
console.log(simpleDiff(oldContent, newContent));
|
|
199
|
-
console.log("------------------------------------------------");
|
|
205
|
+
const ans = await ask(`Substituir a sua versão de ${rPath} pelo código acima? [y/N]: `);
|
|
206
|
+
overwrite = ans.toLowerCase() === "y";
|
|
207
|
+
}
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
209
|
+
if (overwrite) {
|
|
210
|
+
const backupPath = action.dest + ".bak";
|
|
211
|
+
fs.copyFileSync(action.dest, backupPath);
|
|
212
|
+
fs.copyFileSync(action.src, action.dest);
|
|
213
|
+
console.log(`✅ Sobrescrito: ${rPath} (Backup guardado localmente: .bak)`);
|
|
214
|
+
audit.push(`- ACT: UPDATED ${rPath} (Backup: ${path.basename(backupPath)})`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`⏭️ Ignorado (Mantido customização em ${rPath})`);
|
|
217
|
+
audit.push(`- ACT: SKIPPED UPDATE FOR CUSTOMIZED FILE ${rPath}`);
|
|
218
|
+
}
|
|
203
219
|
}
|
|
204
220
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} else {
|
|
212
|
-
console.log(`⏭️ Ignorado (Mantido customização em ${rPath})`);
|
|
213
|
-
audit.push(`- ACT: SKIPPED UPDATE FOR CUSTOMIZED FILE ${rPath}`);
|
|
221
|
+
console.log("\n✨ Atualização concluída com sucesso!");
|
|
222
|
+
|
|
223
|
+
// Auditing manual
|
|
224
|
+
if (flags.audit !== false) {
|
|
225
|
+
const { writeCliAudit } = require("../utils/audit-writer");
|
|
226
|
+
writeCliAudit(targetPath, audit, flags, "update");
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
|
-
|
|
217
|
-
console.log("\n✨ Atualização concluída com sucesso!");
|
|
218
|
-
writeAudit(targetPath, audit, flags);
|
|
219
|
-
|
|
220
|
-
} catch (err) {
|
|
221
|
-
console.error(`\n❌ Falha na execução: ${err.message}`);
|
|
222
|
-
audit.push(`\n## ERROR: ${err.message}`);
|
|
223
|
-
writeAudit(targetPath, audit, flags);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
229
|
+
});
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
module.exports = { run, fileHash };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context Engine do OpenClaw AI OS
|
|
5
|
+
*
|
|
6
|
+
* Responsável por gerenciar, carregar e persistir o conhecimento e estado de operação:
|
|
7
|
+
* 1. system.json - Variáveis de ambiente, config, OS, Docker
|
|
8
|
+
* 2. workspace.json - Mapeamento de linguagens, dependências, arquivos principais, histórico de testes
|
|
9
|
+
* 3. user.json - Preferências, IDE, token settings, perfil de agressividade do AI
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
class ContextEngine {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} projectRoot - Raiz do projeto (onde fica .agent/)
|
|
18
|
+
*/
|
|
19
|
+
constructor(projectRoot) {
|
|
20
|
+
this.projectRoot = projectRoot;
|
|
21
|
+
this.contextDir = path.join(projectRoot, ".agent", "context");
|
|
22
|
+
|
|
23
|
+
// Cria o diretório de contexto caso não exista (independente do init)
|
|
24
|
+
if (!fs.existsSync(this.contextDir)) {
|
|
25
|
+
try {
|
|
26
|
+
fs.mkdirSync(this.contextDir, { recursive: true });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Ignorar em diretórios hostis onde openclaw não está armado
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getFilePath(type) {
|
|
34
|
+
return path.join(this.contextDir, `${type}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Carrega um fragmento do contexto ou retorna defaults
|
|
39
|
+
* @param {string} type - "system", "workspace" ou "user"
|
|
40
|
+
*/
|
|
41
|
+
load(type) {
|
|
42
|
+
const file = this._getFilePath(type);
|
|
43
|
+
if (fs.existsSync(file)) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Falha de parse, retorna default seguro
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Salva ou sobrepõe as flags no contexto
|
|
56
|
+
* @param {string} type - "system", "workspace" ou "user"
|
|
57
|
+
* @param {Object} data - Objeto de dados (será feito um object merge shallow com key overwrites)
|
|
58
|
+
*/
|
|
59
|
+
save(type, data) {
|
|
60
|
+
// Se .agent não existe ou não foi inicializado, silenciosamente bypass (para CLI cmds sujos antes do IDE_INSTALL)
|
|
61
|
+
if (!fs.existsSync(this.contextDir)) return false;
|
|
62
|
+
|
|
63
|
+
const current = this.load(type);
|
|
64
|
+
const merged = { ...current, ...data };
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(this._getFilePath(type), JSON.stringify(merged, null, 2), "utf-8");
|
|
68
|
+
return true;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Agrega todos os arquivos de contexto para compor a mentalidade da query.
|
|
76
|
+
*/
|
|
77
|
+
getFullContext() {
|
|
78
|
+
return {
|
|
79
|
+
system: this.load("system"),
|
|
80
|
+
workspace: this.load("workspace"),
|
|
81
|
+
user: this.load("user"),
|
|
82
|
+
meta: {
|
|
83
|
+
timestamp: new Date().toISOString()
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = ContextEngine;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core Orchestrator do OpenClaw AI OS
|
|
5
|
+
*
|
|
6
|
+
* Atua como o "Kernel" do sistema, centralizando o ciclo de vida:
|
|
7
|
+
* INSPECT -> PLAN -> CONSENT -> APPLY -> AUDIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const readline = require("readline");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const { guardPlan } = require("../utils/scope_guard");
|
|
14
|
+
const { writeCliAudit } = require("../utils/audit-writer");
|
|
15
|
+
|
|
16
|
+
function ask(q) {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
return new Promise((res) => rl.question(q, (ans) => { rl.close(); res(ans.trim()); }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executa uma ação unificada no OpenClaw AI OS.
|
|
23
|
+
* Garante que regras de segurança, escopo e auditoria sejam aplicadas.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string} options.actionName - Nome da ação (ex: "ide install", "uninstall")
|
|
27
|
+
* @param {Object} options.context - Contexto do workspace/módulo coletado
|
|
28
|
+
* @param {Object} options.flags - Flags da CLI (--apply, --force, --yes)
|
|
29
|
+
* @param {Object} options.intents - { writes: [], deletes: [], overwrites: [] }
|
|
30
|
+
* @param {Function} options.executeFn - Função assíncrona que aplica as mudanças de fato
|
|
31
|
+
* @param {Function} options.planFn - Função de exibição do plano (read-only)
|
|
32
|
+
* @param {string} options.targetPath - Root path do projeto
|
|
33
|
+
* @param {boolean} options.skipAudit - Se true, não grava o audit log no .agent/audit (útil para uninstall)
|
|
34
|
+
* @param {boolean} options.skipConfirm - Se true, pula o prompt padrão de apply (útil para per-file diff loops)
|
|
35
|
+
* @returns {boolean} true se executado, false se cancelado ou em modo plan
|
|
36
|
+
*/
|
|
37
|
+
async function executeAction({
|
|
38
|
+
actionName,
|
|
39
|
+
context,
|
|
40
|
+
flags,
|
|
41
|
+
intents = { writes: [], deletes: [], overwrites: [] },
|
|
42
|
+
executeFn,
|
|
43
|
+
planFn,
|
|
44
|
+
confirmationWord = null,
|
|
45
|
+
targetPath,
|
|
46
|
+
skipAudit = false,
|
|
47
|
+
skipConfirm = false
|
|
48
|
+
}) {
|
|
49
|
+
const planMode = !flags.apply;
|
|
50
|
+
|
|
51
|
+
// 1. INSPECT / PLAN - Scope Guard
|
|
52
|
+
await guardPlan(targetPath, intents, flags);
|
|
53
|
+
|
|
54
|
+
// Exibir o plano real
|
|
55
|
+
if (planFn && typeof planFn === "function") {
|
|
56
|
+
await planFn();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (planMode) {
|
|
60
|
+
console.log(`\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.`);
|
|
61
|
+
console.log(` Para aplicar, rode com a flag --apply`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. CONSENT
|
|
66
|
+
if (!flags.yes && !skipConfirm) {
|
|
67
|
+
if (confirmationWord) {
|
|
68
|
+
const confirm = await ask(`\n⚠️ Ação destrutiva requer confirmação forte. Digite '${confirmationWord}' para confirmar: `);
|
|
69
|
+
if (confirm !== confirmationWord) {
|
|
70
|
+
console.log("⏹️ Cancelado. Nenhuma alteração feita.");
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const confirm = await ask(`\n⚠️ Deseja APLICAR as alterações acima? (s/N): `);
|
|
75
|
+
if (confirm.toLowerCase() !== "s") {
|
|
76
|
+
console.log("⏹️ Cancelado. Nenhuma alteração feita.");
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. APPLY
|
|
83
|
+
console.log(`\n⚙️ Executando [${actionName}]...`);
|
|
84
|
+
try {
|
|
85
|
+
await executeFn();
|
|
86
|
+
console.log(`✅ Ação [${actionName}] concluída com sucesso.`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`❌ Erro ao executar [${actionName}]:`, err.message);
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. AUDIT
|
|
93
|
+
if (!skipAudit && flags.audit !== false) {
|
|
94
|
+
try {
|
|
95
|
+
const auditPayload = [
|
|
96
|
+
`--- AUDIT LOG: ${actionName} ---`,
|
|
97
|
+
`Date: ${new Date().toISOString()}`,
|
|
98
|
+
`User Flags: ${JSON.stringify(flags)}`,
|
|
99
|
+
`Intents: writes=${intents.writes.length} overwrites=${intents.overwrites.length} deletes=${intents.deletes.length}`,
|
|
100
|
+
`Status: SUCCESS`
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Reutiliza a função de escrita de log da CLI
|
|
104
|
+
writeCliAudit(targetPath, auditPayload, flags, actionName);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log("⚠️ Não foi possível escrever o audit log.");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { executeAction };
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
// permitemos as configs nativas da IDE copiadas no install
|
|
40
|
+
const safeIdioms = [
|
|
41
|
+
path.join(targetPath, ".cursorrules"),
|
|
42
|
+
path.join(targetPath, ".github"),
|
|
43
|
+
path.join(targetPath, ".cursor")
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
if (safeIdioms.some(idiom => absoluteMutate.startsWith(idiom))) return true;
|
|
47
|
+
|
|
48
|
+
// Retorna true se começa com o caminho the escopo.
|
|
49
|
+
if (absoluteMutate.startsWith(safeScope)) return true;
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Escudo de Proteção "Scope Guard"
|
|
56
|
+
* Evita que o CLI apague ou modifique arquivos da aplicação nativa (ex: index.js, package.json do usuário).
|
|
57
|
+
*
|
|
58
|
+
* @param {string} targetPath - Caminho de execução (process.cwd geralmente)
|
|
59
|
+
* @param {object} intents - Objeto contendo listas { writes: [], deletes: [], overwrites: [] } com paths relativos ou absolutos
|
|
60
|
+
* @param {object} flags - As CLI Flags (`force`, `yes`, `apply`)
|
|
61
|
+
*/
|
|
62
|
+
async function guardPlan(targetPath, intents = { writes: [], deletes: [], overwrites: [] }, flags = {}) {
|
|
63
|
+
const outOfScope = {
|
|
64
|
+
writes: intents.writes.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
65
|
+
deletes: intents.deletes.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
66
|
+
overwrites: intents.overwrites.filter(p => !isPathInSafeScope(targetPath, p)),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const totalRisks = outOfScope.writes.length + outOfScope.deletes.length + outOfScope.overwrites.length;
|
|
70
|
+
|
|
71
|
+
// Se tudo ok (nenhuma fuga da jail), permitimos silenciosamente sem drama
|
|
72
|
+
if (totalRisks === 0) return true;
|
|
73
|
+
|
|
74
|
+
console.log("\n🛡️ [SCOPE GUARD ALERTA DE SEGURANÇA]");
|
|
75
|
+
console.log("-----------------------------------------");
|
|
76
|
+
console.log(`Detectado tentativa de alterar dados FORA do sandbox (.agent/**).`);
|
|
77
|
+
|
|
78
|
+
if (outOfScope.writes.length > 0) {
|
|
79
|
+
console.log("⚠️ Arquivos a GERAR fora do escopo:", outOfScope.writes);
|
|
80
|
+
}
|
|
81
|
+
if (outOfScope.overwrites.length > 0) {
|
|
82
|
+
console.log("🧨 Arquivos a SOBRESCREVER fora do escopo:", outOfScope.overwrites);
|
|
83
|
+
}
|
|
84
|
+
if (outOfScope.deletes.length > 0) {
|
|
85
|
+
console.log("🔥 DELETE SOLICITADO fora do escopo:", outOfScope.deletes);
|
|
86
|
+
}
|
|
87
|
+
console.log("-----------------------------------------\n");
|
|
88
|
+
|
|
89
|
+
// Lógica para modo interativo. Se flag Force vier sem ask, permitimos por conta-risco.
|
|
90
|
+
if (flags.force) {
|
|
91
|
+
console.log("⚠️ Flag --force detectada. Destravando ação destrutiva fora da sandbox.");
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (flags.yes) {
|
|
96
|
+
console.log("❌ Bloqueado! Não permitimos escapes da sandbox via '--yes'. Você deve rodar o processo interativamente ou usar --force.");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Interactive confirmation com hard-stop (obriga escrever DESTRUCTIVE para deletes/overwrites)
|
|
101
|
+
if (outOfScope.deletes.length > 0 || outOfScope.overwrites.length > 0) {
|
|
102
|
+
const ans = await askQuestion("Perigo crítico. Para aprovar sobrescritas ou apagamentos fora do .agent, digite 'DESTRUCTIVE': ");
|
|
103
|
+
if (ans !== "DESTRUCTIVE") {
|
|
104
|
+
console.log("⏹️ Cancelado pelo Módulo Scope Guard.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Para WRITES brandos: conf simples
|
|
111
|
+
const ans = await askQuestion("Aprovar criação de novas rotas fora do sandbox? [y/N]: ");
|
|
112
|
+
if (ans.toLowerCase() !== "y") {
|
|
113
|
+
console.log("⏹️ Cancelado pelo Módulo Scope Guard.");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
guardPlan,
|
|
122
|
+
isPathInSafeScope
|
|
123
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Criação de um snapshot do contexto do workspace atual para o router
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Workspace Snapshot
|
|
6
|
+
|
|
7
|
+
## Objetivo
|
|
8
|
+
Analisar e capturar o estado global do projeto (stack, dependências, rotas, testes e pontos cegos) gerando um artefato `context.json` otimizado. Isso alimenta os roteadores de agentes reduzindo solicitações repetitivas de entendimento e consumos de Token por scans demorados.
|
|
9
|
+
|
|
10
|
+
## Fluxo
|
|
11
|
+
1. Ler `.gitignore` e `package.json` (ou similares de outras linguagens).
|
|
12
|
+
2. Escanear diretórios chave (src, lib, tests, config).
|
|
13
|
+
3. Detectar frameworks, bibliotecas primárias e scripts customizados.
|
|
14
|
+
4. Identificar zonas de risco (pastas legacy, componentes em migração, vulnerabilidades).
|
|
15
|
+
5. Escrever/Atualizar `.agent/context/context.json` e notificar roteadores.
|
|
16
|
+
|
|
17
|
+
## Regras
|
|
18
|
+
- Nunca ler arquivos grandes ignorados como `node_modules` e pastas compiladas.
|
|
19
|
+
- O resultado no JSON não deve passar de 5kb (resumo em alto nível) para manter os tokens baixos no contexto das prompts do LLM.
|
|
20
|
+
- Exigir aprovação de Apply para gravar a imagem do Snapshot.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# OpenClaw - Chat Router Workflow
|
|
2
|
+
# Roteamento de Entrada para o Chat da IDE
|
|
3
|
+
|
|
4
|
+
**description**: Workflow de entrada padrão para o modelo ao iniciar um chat. Força o reconhecimento de contexto antes de qualquer ação.
|
|
5
|
+
|
|
6
|
+
## 1. Inspect
|
|
7
|
+
Sempre comece investigando o estado atual do repositório, procurando pelas regras e diretrizes estabelecidas no `.agent/rules/`. Leia `openclaw.json` para definir o `targetPath` base.
|
|
8
|
+
|
|
9
|
+
## 2. Plan & Select Skill
|
|
10
|
+
Com base no seu objetivo e nas skills ativas listadas em `.agent/skills/`, escolha a melhor abordagem e desenvolva um rascunho. Comunique o plano detalhado no chat.
|
|
11
|
+
|
|
12
|
+
## 3. Consent
|
|
13
|
+
Nunca modifique arquivos críticos, delete diretórios ou crie instâncias de serviço sem perguntar ativamente e aguardar o "Sim" explicíto do usuário sobre o Plano (Consent-First).
|
|
14
|
+
|
|
15
|
+
## 4. Apply
|
|
16
|
+
Proceda com a implementação guiando-se estritamente pela sua proposta. Mantenha os arquivos enxutos (pequenos) e faça checagens consistentes.
|
|
17
|
+
|
|
18
|
+
## 5. Audit
|
|
19
|
+
Deixe um sumário claro do que foi modificado como prova da sessão de alterações. Utilize `lib/utils/audit-writer.js` se for chamado via OpenClaw CLI ou relate concisamente.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: openclaw-global-system-rules
|
|
3
|
+
globs: *
|
|
4
|
+
---
|
|
5
|
+
# OpenClaw AI OS - Global Rules
|
|
6
|
+
Você é controlado pelo sistema OpenClaw AI OS instalado neste workspace. Seu comportamento principal deve ser "Consent-First".
|
|
7
|
+
|
|
8
|
+
- Ler as políticas do diretório `.agent/rules` ANTES de agir.
|
|
9
|
+
- Seguir o workflow padrão `.agent/workflows/chat-router.md` de Roteamento (Inspect -> Plan -> Consent -> Apply -> Audit).
|
|
10
|
+
- Jamais mutar, deletar ou sobrescrever arquivos sem o DEVIDO Consentimento do Usuário atestado em Plano de Ação.
|
|
11
|
+
- Consultar e preservar `.agent/state/MEMORY.md` com as decisões de projeto.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# OpenClaw AI OS - Global Rules
|
|
2
|
+
Você é controlado pelo sistema OpenClaw AI OS instalado neste workspace. Seu comportamento principal deve ser "Consent-First".
|
|
3
|
+
|
|
4
|
+
- Ler as políticas do diretório `.agent/rules` ANTES de agir.
|
|
5
|
+
- Seguir o workflow padrão `.agent/workflows/chat-router.md` de Roteamento (Inspect -> Plan -> Consent -> Apply -> Audit).
|
|
6
|
+
- Jamais mutar, deletar ou sobrescrever arquivos sem o DEVIDO Consentimento do Usuário atestado em Plano de Ação.
|
|
7
|
+
- Consultar e preservar `.agent/state/MEMORY.md` com as decisões de projeto.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OpenClaw AI OS - Copilot Instructions
|
|
2
|
+
Você faz parte do ecosistema OpenClaw instalado neste projeto, que reforça arquiteturas de agentes seguras.
|
|
3
|
+
|
|
4
|
+
## 1. Safety Protocols
|
|
5
|
+
Respeite e leia a diretriz de Consentimento: `.agent/rules/CONSENT_FIRST.md`. Todas as mutações do projeto exigem aprovações prévias visuais baseadas em um Plano (Inspect -> Plan -> Consent -> Apply -> Audit).
|
|
6
|
+
|
|
7
|
+
## 2. Tools & Skills
|
|
8
|
+
Sempre verifique extensões instaladas e documentação do projeto via `.agent/skills/` ou referências locais do diretório `.agent` para aplicar soluções sob o escopo esperado do mantenedor.
|
|
9
|
+
|
|
10
|
+
## 3. Comportamento
|
|
11
|
+
Preserve a integridade do sistema do usuário e nunca faça deleções não solicitadas em arquivos da pipeline e fora das permissões.
|