ostacky 0.7.4 → 0.8.1
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 +62 -27
- package/assets/agents/ostacky.md +45 -512
- package/assets/commands/install-stack.md +2 -2
- package/assets/docs/engram-protocol.md +79 -0
- package/assets/docs/ostacky-reference.md +79 -0
- package/assets/mcp/ostacky-controller/index.js +169 -69
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/plugins/engram.ts +47 -79
- package/assets/plugins/ostacky-plugin.ts +694 -0
- package/assets/skills/brainstorming/SKILL.md +14 -14
- package/assets/skills/execution-mode-evaluation/SKILL.md +9 -9
- package/dist/cli.js +388 -106
- package/manifest.json +30 -30
- package/package.json +1 -1
- package/assets/plugins/ostacky-guard.ts +0 -212
package/manifest.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.8.1",
|
|
3
3
|
"repo": "JaimeHoracio/Ostacky",
|
|
4
|
-
"tag": "v0.
|
|
4
|
+
"tag": "v0.8.1",
|
|
5
5
|
"agents": [
|
|
6
6
|
{
|
|
7
7
|
"name": "ostacky",
|
|
8
8
|
"file": "assets/agents/ostacky.md",
|
|
9
|
-
"description": "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.
|
|
10
|
-
"version": "0.
|
|
11
|
-
"sha256": "
|
|
9
|
+
"description": "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.8.1: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
|
|
10
|
+
"version": "0.8.1",
|
|
11
|
+
"sha256": "1dea56ffe3866e830c5138b44ff3e7531324ddd2784b1d34a8956614a53c5ec9"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"commands": [
|
|
15
15
|
{
|
|
16
16
|
"name": "install-stack",
|
|
17
17
|
"file": "assets/commands/install-stack.md",
|
|
18
|
-
"description": "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram,
|
|
19
|
-
"version": "0.
|
|
20
|
-
"sha256": "
|
|
18
|
+
"description": "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.8.1: controller plugin como alma hard-gate, Context7 removido del stack.",
|
|
19
|
+
"version": "0.8.1",
|
|
20
|
+
"sha256": "43ad96a63ceab8730cf0459838e7fad2953333307cfc664a849ca39f7ba7d532"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "opsx-sync",
|
|
24
24
|
"file": "assets/commands/opsx-sync.md",
|
|
25
25
|
"description": "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.8.1",
|
|
27
27
|
"sha256": "fe0158478f2ca63b315037a85fc1632b77868532e319af6c7384285441767d64"
|
|
28
28
|
}
|
|
29
29
|
],
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
{
|
|
32
32
|
"name": "ostacky-controller",
|
|
33
33
|
"file": "assets/mcp/ostacky-controller/",
|
|
34
|
-
"description": "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.
|
|
35
|
-
"version": "0.
|
|
36
|
-
"sha256": "
|
|
34
|
+
"description": "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.8.1: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
|
|
35
|
+
"version": "0.8.1",
|
|
36
|
+
"sha256": "ec554bc97f9d9b1e64c0b42813547dedc923e86e326c43ae87afab88ba249b90"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "openspec",
|
|
40
40
|
"file": "assets/mcp/openspec/",
|
|
41
41
|
"description": "MCP server local para OpenSpec - proposal, apply, archive, sync de cambios",
|
|
42
|
-
"version": "0.
|
|
42
|
+
"version": "0.8.1",
|
|
43
43
|
"sha256": "fa4be28bfc1e75db8f1a037e2580f04a60066c80e8e5934e90e1041020d04609"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
@@ -48,105 +48,105 @@
|
|
|
48
48
|
"name": "brainstorming",
|
|
49
49
|
"file": "assets/skills/brainstorming/SKILL.md",
|
|
50
50
|
"description": "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a implementación directa o openspec-propose) y open-exploration (exploración libre)",
|
|
51
|
-
"version": "0.
|
|
52
|
-
"sha256": "
|
|
51
|
+
"version": "0.8.1",
|
|
52
|
+
"sha256": "87f8d6ed28205f9272792d3865d8cd3232ab5e511a604d6d63d41e3a02a5f422"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"name": "execution-mode-evaluation",
|
|
56
56
|
"file": "assets/skills/execution-mode-evaluation/SKILL.md",
|
|
57
57
|
"description": "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
|
|
58
|
-
"version": "0.
|
|
59
|
-
"sha256": "
|
|
58
|
+
"version": "0.8.1",
|
|
59
|
+
"sha256": "7b89dda29927c88f1eb13ffcea541a12423384c172f66c4f3d9b61efe0c7a163"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"name": "tdd",
|
|
63
63
|
"file": "assets/skills/tdd/SKILL.md",
|
|
64
64
|
"description": "Skill de test-driven development (Superpowers)",
|
|
65
|
-
"version": "0.
|
|
65
|
+
"version": "0.8.1",
|
|
66
66
|
"sha256": "aa412298980b7826165c211145c1b8e9135f68f36247bb3cb96d0a0eae274486"
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"name": "subagent-driven-development",
|
|
70
70
|
"file": "assets/skills/subagent-driven-development/SKILL.md",
|
|
71
71
|
"description": "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
|
|
72
|
-
"version": "0.
|
|
72
|
+
"version": "0.8.1",
|
|
73
73
|
"sha256": "1a42d714a9a13faf05f0bf7b580e8d837c2e77a5633839542ccce603023419e8"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "dispatching-parallel-agents",
|
|
77
77
|
"file": "assets/skills/dispatching-parallel-agents/SKILL.md",
|
|
78
78
|
"description": "Skill de dispatch paralelo de agentes (Superpowers)",
|
|
79
|
-
"version": "0.
|
|
79
|
+
"version": "0.8.1",
|
|
80
80
|
"sha256": "3c8a66d51ae2e719e877d02c3dac32bd804722c3bb9227c258ecae78252a20ab"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "review",
|
|
84
84
|
"file": "assets/skills/review/SKILL.md",
|
|
85
85
|
"description": "Skill de revisión de código (Superpowers)",
|
|
86
|
-
"version": "0.
|
|
86
|
+
"version": "0.8.1",
|
|
87
87
|
"sha256": "b19650ed4d1d4d9857a4dd5b7e08e91328a1a1fc9c45bcc2fb600fa9d0213279"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"name": "receiving-code-review",
|
|
91
91
|
"file": "assets/skills/receiving-code-review/SKILL.md",
|
|
92
92
|
"description": "Skill de recibir y procesar feedback de code review",
|
|
93
|
-
"version": "0.
|
|
93
|
+
"version": "0.8.1",
|
|
94
94
|
"sha256": "d761e884e71d8d3476ac734d287cae403a4024506a7d12e7361a448217c0a831"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"name": "openspec-propose",
|
|
98
98
|
"file": "assets/skills/openspec-propose/SKILL.md",
|
|
99
99
|
"description": "Skill de generación de proposal (OpenSpec)",
|
|
100
|
-
"version": "0.
|
|
100
|
+
"version": "0.8.1",
|
|
101
101
|
"sha256": "bb59100b9fd3c9f9a1ec6509fa975ec55bf7fc737df4fe73bb3e9bbc217b83f7"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "openspec-apply-change",
|
|
105
105
|
"file": "assets/skills/openspec-apply-change/SKILL.md",
|
|
106
106
|
"description": "Skill de aplicación de change (OpenSpec)",
|
|
107
|
-
"version": "0.
|
|
107
|
+
"version": "0.8.1",
|
|
108
108
|
"sha256": "dfc823bf89fc7505e91ab6dee9c1f004be38a410bd3d88fb67b211b1b6cbb1d0"
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"name": "openspec-archive-change",
|
|
112
112
|
"file": "assets/skills/openspec-archive-change/SKILL.md",
|
|
113
113
|
"description": "Skill de archivo de change (OpenSpec)",
|
|
114
|
-
"version": "0.
|
|
114
|
+
"version": "0.8.1",
|
|
115
115
|
"sha256": "16e4b561de7747283663fed602e506abf502452c49a8d4b86d7a5539c40f0195"
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"name": "openspec-explore",
|
|
119
119
|
"file": "assets/skills/openspec-explore/SKILL.md",
|
|
120
120
|
"description": "Modo explore para OpenSpec — thinking partner para explorar ideas, investigar problemas y clarificar requisitos antes/durante un cambio",
|
|
121
|
-
"version": "0.
|
|
121
|
+
"version": "0.8.1",
|
|
122
122
|
"sha256": "37ae4aaf17ea71a395bab6dc4d9d61b9a49d7910f6692535ae8553244c46880b"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "using-git-worktrees",
|
|
126
126
|
"file": "assets/skills/using-git-worktrees/SKILL.md",
|
|
127
127
|
"description": "Skill de uso de git worktrees para aislamiento de trabajo",
|
|
128
|
-
"version": "0.
|
|
128
|
+
"version": "0.8.1",
|
|
129
129
|
"sha256": "93341bc1b7c053618a8b6dc07e3615b77990d7b549a0201f5835d95fef67ce13"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "using-superpowers",
|
|
133
133
|
"file": "assets/skills/using-superpowers/SKILL.md",
|
|
134
134
|
"description": "Skill de orquestación de Superpowers skills",
|
|
135
|
-
"version": "0.
|
|
135
|
+
"version": "0.8.1",
|
|
136
136
|
"sha256": "7e54536f96d2a379185a10bfc1e970850caa2561b6d8aca7080f0defea0381a4"
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "writing-skills",
|
|
140
140
|
"file": "assets/skills/writing-skills/SKILL.md",
|
|
141
141
|
"description": "Skill de creación y edición de skills",
|
|
142
|
-
"version": "0.
|
|
142
|
+
"version": "0.8.1",
|
|
143
143
|
"sha256": "3d76b906cee518a2b809febb70db95697a35b9f368986bb504b4120c3bfb437a"
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"name": "graceful-degradation",
|
|
147
147
|
"file": "assets/skills/graceful-degradation/SKILL.md",
|
|
148
148
|
"description": "Skill de degradación graceful cuando múltiples tools están indisponibles",
|
|
149
|
-
"version": "0.
|
|
149
|
+
"version": "0.8.1",
|
|
150
150
|
"sha256": "40f929e8032b70d51b3548bddcc383b931d6044fd9d4acce8a5b9f7acd014f59"
|
|
151
151
|
}
|
|
152
152
|
]
|
package/package.json
CHANGED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ostacky Guard — plugin hermano de Engram
|
|
3
|
-
*
|
|
4
|
-
* Bloquea lecturas sensibles sin autorización previa y bloquea cualquier tool
|
|
5
|
-
* cuando el controller está en estado PENDING.
|
|
6
|
-
*
|
|
7
|
-
* Hard gate incluso en degraded: nunca uses bash para .env sin check_file_access → consume ALLOW con razón auditada.
|
|
8
|
-
*
|
|
9
|
-
* Se registra en `tool.execute.before` y lanza Error si la operación no está
|
|
10
|
-
* permitida. OpenCode aborta el tool antes de tocar disco.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
14
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
15
|
-
import { join, dirname, basename, resolve, relative } from "node:path"
|
|
16
|
-
|
|
17
|
-
const SENSITIVE_DEFAULT = [
|
|
18
|
-
"**/.env*",
|
|
19
|
-
"**/.secrets/**",
|
|
20
|
-
"**/*.pem",
|
|
21
|
-
"**/*.key",
|
|
22
|
-
"**/.aws/**",
|
|
23
|
-
"**/.ssh/**",
|
|
24
|
-
"**/credentials.json",
|
|
25
|
-
"**/.npmrc",
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
const BASH_SENSITIVE_RE =
|
|
29
|
-
/(?:^|[^a-zA-Z0-9_.-])(\.env(\b|[_.-])|\.secrets\b|\.pem\b|\.key\b|credentials\.json|\.aws\b|\.ssh\b|\.npmrc\b)/i
|
|
30
|
-
|
|
31
|
-
function getEnvPatterns(): string[] {
|
|
32
|
-
const raw = process.env.OSTACKY_SENSITIVE_PATTERNS
|
|
33
|
-
if (!raw) return SENSITIVE_DEFAULT
|
|
34
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isSensitive(filePath: string, patterns: string[]): boolean {
|
|
38
|
-
if (!filePath) return false
|
|
39
|
-
const normalized = filePath.replace(/\\/g, "/")
|
|
40
|
-
const lower = normalized.toLowerCase()
|
|
41
|
-
if (lower.endsWith(".env.example") || lower.endsWith(".env.template") || lower.endsWith(".env.sample")) return false
|
|
42
|
-
const base = lower.split("/").pop() || ""
|
|
43
|
-
for (const pat of patterns) {
|
|
44
|
-
if (pat.includes(".env") && base.startsWith(".env")) return true
|
|
45
|
-
if (pat.includes(".secrets") && lower.includes(".secrets")) return true
|
|
46
|
-
if (pat.includes("*.pem") && lower.endsWith(".pem")) return true
|
|
47
|
-
if (pat.includes("*.key") && lower.endsWith(".key")) return true
|
|
48
|
-
if (pat.includes(".aws") && lower.includes(".aws")) return true
|
|
49
|
-
if (pat.includes(".ssh") && lower.includes(".ssh")) return true
|
|
50
|
-
if (pat.includes("credentials.json") && lower.endsWith("credentials.json")) return true
|
|
51
|
-
if (pat.includes(".npmrc") && lower.endsWith(".npmrc")) return true
|
|
52
|
-
}
|
|
53
|
-
if (/\.(pem|key)$/i.test(normalized)) return true
|
|
54
|
-
if (base.startsWith(".env")) return true
|
|
55
|
-
return false
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function extractPathsFromBash(cmd: string): string[] {
|
|
59
|
-
if (!cmd) return []
|
|
60
|
-
const normalized = cmd.replace(/&&/g, ";").replace(/\|\|/g, ";")
|
|
61
|
-
const segments = normalized.split(/[|;><\n]+/)
|
|
62
|
-
const paths: string[] = []
|
|
63
|
-
for (const seg of segments) {
|
|
64
|
-
const trimmed = seg.trim()
|
|
65
|
-
if (!trimmed) continue
|
|
66
|
-
const tokens = trimmed.match(/(?:[^\s"'`\u0060\\]+|"[^"]*"|'[^']*'|`[^`]*`)+/g) || []
|
|
67
|
-
for (let token of tokens) {
|
|
68
|
-
const stripped = token.replace(/["'`]/g, "").replace(/\\/g, "")
|
|
69
|
-
if (!stripped) continue
|
|
70
|
-
if (["cat","grep","ls","echo","awk","sed","cut","head","tail","wc","find","xargs","bash","sh","zsh","env","printenv","node","bun","npm","npx","ls","cat"].includes(stripped)) continue
|
|
71
|
-
if (stripped.startsWith("-")) continue
|
|
72
|
-
const lower = stripped.toLowerCase()
|
|
73
|
-
if (
|
|
74
|
-
stripped.includes("/") ||
|
|
75
|
-
stripped.includes(".") ||
|
|
76
|
-
lower.startsWith(".env") ||
|
|
77
|
-
lower.includes(".secrets") ||
|
|
78
|
-
lower.endsWith(".pem") ||
|
|
79
|
-
lower.endsWith(".key") ||
|
|
80
|
-
lower.includes(".aws") ||
|
|
81
|
-
lower.includes(".ssh") ||
|
|
82
|
-
lower.endsWith("credentials.json") ||
|
|
83
|
-
lower.endsWith(".npmrc")
|
|
84
|
-
) {
|
|
85
|
-
const cleaned = stripped.replace(/[,:;)\]]+$/, "")
|
|
86
|
-
if (cleaned) paths.push(cleaned)
|
|
87
|
-
} else if (stripped === ".env") {
|
|
88
|
-
paths.push(stripped)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return [...new Set(paths)]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function getStatePath(directory: string): string {
|
|
96
|
-
if (process.env.OSTACKY_STATE_PATH) return process.env.OSTACKY_STATE_PATH
|
|
97
|
-
const candidates = [join(directory, "opencode.json"), join(directory, "opencode.jsonc")]
|
|
98
|
-
for (const cand of candidates) {
|
|
99
|
-
try {
|
|
100
|
-
const raw = readFileSync(cand, "utf-8")
|
|
101
|
-
const json = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
|
|
102
|
-
const envPath = (json as any)?.mcp?.["ostacky-controller"]?.environment?.OSTACKY_STATE_PATH
|
|
103
|
-
if (typeof envPath === "string" && envPath) return envPath
|
|
104
|
-
} catch {}
|
|
105
|
-
}
|
|
106
|
-
return join(directory, ".opencode", "ostacky-state.json")
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function readState(directory: string): any | null {
|
|
110
|
-
const p = getStatePath(directory)
|
|
111
|
-
try {
|
|
112
|
-
const raw = readFileSync(p, "utf-8")
|
|
113
|
-
return JSON.parse(raw)
|
|
114
|
-
} catch {
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export const OstackyGuard: Plugin = async (ctx) => {
|
|
120
|
-
const patterns = getEnvPatterns()
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
"tool.execute.before": async (input, output) => {
|
|
124
|
-
const tool = (input as any).tool as string
|
|
125
|
-
const args = (input as any).args as any
|
|
126
|
-
|
|
127
|
-
// 10.2: Guard genérico de PENDING — bloquear cualquier tool no-controller cuando estado es PENDING
|
|
128
|
-
const state = readState(ctx.directory)
|
|
129
|
-
const pendingStates = ["ROUTE_DECISION_PENDING", "EXECUTION_DECISION_PENDING", "CLARIFICATION_PENDING"]
|
|
130
|
-
if (state && pendingStates.includes(state.state)) {
|
|
131
|
-
const allowedTools = ["consume_route_decision", "consume_execution_decision", "record_clarification", "abandon", "check_file_access", "consume_file_access_decision", "record_user_confirmation"]
|
|
132
|
-
const isControllerTool = tool.startsWith("ostacky-controller_") || allowedTools.some((t) => tool.includes(t))
|
|
133
|
-
if (!isControllerTool) {
|
|
134
|
-
throw new Error(`BLOCKED: call consume_* first — controller is in ${state.state}. Use consume_route_decision / consume_execution_decision / record_clarification to proceed.`)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Guard hard para bash/write/edit (hardening-v2 P0) ──
|
|
139
|
-
// Hard gate incluso en degraded, nunca uses bash para .env sin check_file_access → consume ALLOW con razón auditada
|
|
140
|
-
if (tool === "bash") {
|
|
141
|
-
const cmd: string = args?.command || args?.cmd || ""
|
|
142
|
-
if (typeof cmd === "string" && cmd) {
|
|
143
|
-
const normalized = cmd.replace(/["'`]/g, "").replace(/\\/g, "")
|
|
144
|
-
const hasSensitivePattern = BASH_SENSITIVE_RE.test(normalized)
|
|
145
|
-
const paths = extractPathsFromBash(cmd)
|
|
146
|
-
const sensitivePaths = paths.filter((p) => isSensitive(p, patterns))
|
|
147
|
-
// Si hay match de regex o paths sensibles, validar ALLOW
|
|
148
|
-
if (hasSensitivePattern || sensitivePaths.length > 0) {
|
|
149
|
-
// Si extrajimos paths sensibles, validar cada uno
|
|
150
|
-
if (sensitivePaths.length > 0) {
|
|
151
|
-
for (const p of sensitivePaths) {
|
|
152
|
-
const s = readState(ctx.directory)
|
|
153
|
-
// Normalizar p para lookup: probar p tal cual, y resolved, y basename para casos relativos
|
|
154
|
-
const candidates = [p, resolve(ctx.directory, p), join(ctx.directory, p)]
|
|
155
|
-
const allowed = candidates.some((c) => s?.allowedFiles?.[c] || s?.allowedFiles?.[p])
|
|
156
|
-
// También chequear por basename si es .env (ej: .env vs /abs/.env)
|
|
157
|
-
const baseAllowed = s?.allowedFiles?.[p] || s?.allowedFiles?.[basename(p)] || s?.allowedFiles?.[resolve(ctx.directory, p)]
|
|
158
|
-
if (!allowed && !baseAllowed) {
|
|
159
|
-
const denied = s?.deniedFiles?.[p] || s?.deniedFiles?.[basename(p)]
|
|
160
|
-
if (denied) {
|
|
161
|
-
throw new Error(`BLOCKED: bash contiene acceso sensible (${p}) (previously denied) — Llamá check_file_access con reason antes.`)
|
|
162
|
-
}
|
|
163
|
-
throw new Error(`BLOCKED: bash contiene acceso sensible (${p}). Llamá check_file_access con reason antes.`)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else if (hasSensitivePattern) {
|
|
167
|
-
// Fallback genérico cuando regex matchea pero no extrajimos path (ej: cat .env con espacios raros)
|
|
168
|
-
const s = readState(ctx.directory)
|
|
169
|
-
// Buscar si hay algún allowed que cubra .env* genérico
|
|
170
|
-
const hasAllowed = Object.keys(s?.allowedFiles || {}).some((k) => isSensitive(k, patterns))
|
|
171
|
-
if (!hasAllowed) {
|
|
172
|
-
throw new Error(`BLOCKED: bash contiene acceso sensible (.env). Llamá check_file_access con reason antes.`)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Caso especial: obfuscación .e""nv → normalized ya quitó quotes, pero paths extraído ya lo cubre
|
|
177
|
-
// Si normalized contiene .env después de strip, y no hay allowed, ya bloqueamos arriba
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Extraer filePath para read/write/edit/grep/glob/read_mcp_resource
|
|
182
|
-
let filePath: string | undefined
|
|
183
|
-
if (tool === "read" || tool === "read_mcp_resource" || tool === "grep" || tool === "glob" || tool === "write" || tool === "edit") {
|
|
184
|
-
filePath = args?.filePath || args?.path || args?.pattern || args?.uri || ""
|
|
185
|
-
if (tool === "grep" && args?.include) filePath = args.include
|
|
186
|
-
if (tool === "write" || tool === "edit") {
|
|
187
|
-
filePath = args?.filePath || args?.path || ""
|
|
188
|
-
}
|
|
189
|
-
if (tool === "read_mcp_resource" && typeof args?.uri === "string") {
|
|
190
|
-
try {
|
|
191
|
-
const u = args.uri as string
|
|
192
|
-
if (u.startsWith("file://")) filePath = u.slice(7)
|
|
193
|
-
else if (u.includes("/")) filePath = u
|
|
194
|
-
} catch {}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 9.1: Guard de credenciales (hard gate incluso en degraded)
|
|
199
|
-
if (filePath && isSensitive(filePath, patterns)) {
|
|
200
|
-
const s = readState(ctx.directory)
|
|
201
|
-
const allowed = s?.allowedFiles?.[filePath] || s?.allowedFiles?.[resolve(ctx.directory, filePath)] || s?.allowedFiles?.[basename(filePath)]
|
|
202
|
-
if (!allowed) {
|
|
203
|
-
const denied = s?.deniedFiles?.[filePath] || s?.deniedFiles?.[basename(filePath)]
|
|
204
|
-
if (denied) {
|
|
205
|
-
throw new Error(`BLOCKED: File ${filePath} requires check_file_access (previously denied)`)
|
|
206
|
-
}
|
|
207
|
-
throw new Error(`BLOCKED: File ${filePath} requires check_file_access`)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
}
|