ostacky 0.7.3 → 0.8.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 +29 -24
- package/assets/agents/ostacky.md +82 -525
- 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 +698 -228
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/mcp/ostacky-controller/security.js +87 -0
- package/assets/plugins/engram.ts +47 -79
- package/assets/plugins/ostacky-guard.ts +11 -124
- package/assets/plugins/ostacky-plugin.ts +646 -0
- package/assets/skills/brainstorming/SKILL.md +198 -197
- package/assets/skills/execution-mode-evaluation/SKILL.md +9 -9
- package/assets/skills/graceful-degradation/SKILL.md +251 -248
- package/dist/cli.js +432 -135
- package/manifest.json +31 -31
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* security.js — source-of-truth mirror of src/security.ts for controller (Node)
|
|
3
|
+
* Generado desde src/security.ts — mantener sincronizado via `bun run hash:check` y test controller-source
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SENSITIVE_DEFAULT = [
|
|
7
|
+
"**/.env*",
|
|
8
|
+
"**/.secrets/**",
|
|
9
|
+
"**/*.pem",
|
|
10
|
+
"**/*.key",
|
|
11
|
+
"**/.aws/**",
|
|
12
|
+
"**/.ssh/**",
|
|
13
|
+
"**/credentials.json",
|
|
14
|
+
"**/.npmrc",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const BASH_SENSITIVE_RE =
|
|
18
|
+
/(?:^|[^a-zA-Z0-9_.-])(\.env(\b|[_.-])|\.secrets\b|\.pem\b|\.key\b|credentials\.json|\.aws\b|\.ssh\b|\.npmrc\b)/i;
|
|
19
|
+
|
|
20
|
+
export function isSensitive(filePath, patterns = SENSITIVE_DEFAULT) {
|
|
21
|
+
if (!filePath) return false;
|
|
22
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
23
|
+
const lower = normalized.toLowerCase();
|
|
24
|
+
if (
|
|
25
|
+
lower.endsWith(".env.example") ||
|
|
26
|
+
lower.endsWith(".env.template") ||
|
|
27
|
+
lower.endsWith(".env.sample")
|
|
28
|
+
)
|
|
29
|
+
return false;
|
|
30
|
+
const base = lower.split("/").pop() || "";
|
|
31
|
+
for (const pat of patterns) {
|
|
32
|
+
if (pat.includes(".env") && base.startsWith(".env")) return true;
|
|
33
|
+
if (pat.includes(".secrets") && lower.includes(".secrets")) return true;
|
|
34
|
+
if (pat.includes("*.pem") && lower.endsWith(".pem")) return true;
|
|
35
|
+
if (pat.includes("*.key") && lower.endsWith(".key")) return true;
|
|
36
|
+
if (pat.includes(".aws") && lower.includes(".aws")) return true;
|
|
37
|
+
if (pat.includes(".ssh") && lower.includes(".ssh")) return true;
|
|
38
|
+
if (pat.includes("credentials.json") && lower.endsWith("credentials.json")) return true;
|
|
39
|
+
if (pat.includes(".npmrc") && lower.endsWith(".npmrc")) return true;
|
|
40
|
+
}
|
|
41
|
+
if (/\.(pem|key)$/i.test(normalized)) return true;
|
|
42
|
+
if (base.startsWith(".env")) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractPathsFromBash(cmd) {
|
|
47
|
+
if (!cmd) return [];
|
|
48
|
+
const normalized = cmd.replace(/&&/g, ";").replace(/\|\|/g, ";");
|
|
49
|
+
const segments = normalized.split(/[|;><\n]+/);
|
|
50
|
+
const paths = [];
|
|
51
|
+
for (const seg of segments) {
|
|
52
|
+
const trimmed = seg.trim();
|
|
53
|
+
if (!trimmed) continue;
|
|
54
|
+
const tokens = trimmed.match(/(?:[^\s"'`\\]+|"[^"]*"|'[^']*'|`[^`]*`)+/g) || [];
|
|
55
|
+
for (let token of tokens) {
|
|
56
|
+
const stripped = token.replace(/["'`]/g, "").replace(/\\/g, "");
|
|
57
|
+
if (!stripped) continue;
|
|
58
|
+
if (["cat","grep","ls","echo","awk","sed","cut","head","tail","wc","find","xargs","bash","sh","zsh","env","printenv","node","bun","npm","npx","ls"].includes(stripped)) continue;
|
|
59
|
+
if (stripped.startsWith("-")) continue;
|
|
60
|
+
const lower = stripped.toLowerCase();
|
|
61
|
+
if (
|
|
62
|
+
stripped.includes("/") ||
|
|
63
|
+
stripped.includes(".") ||
|
|
64
|
+
lower.startsWith(".env") ||
|
|
65
|
+
lower.includes(".secrets") ||
|
|
66
|
+
lower.endsWith(".pem") ||
|
|
67
|
+
lower.endsWith(".key") ||
|
|
68
|
+
lower.includes(".aws") ||
|
|
69
|
+
lower.includes(".ssh") ||
|
|
70
|
+
lower.endsWith("credentials.json") ||
|
|
71
|
+
lower.endsWith(".npmrc")
|
|
72
|
+
) {
|
|
73
|
+
const cleaned = stripped.replace(/[,:;)\]]+$/, "");
|
|
74
|
+
if (cleaned) paths.push(cleaned);
|
|
75
|
+
} else if (stripped === ".env") {
|
|
76
|
+
paths.push(stripped);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(paths)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSensitivePatterns() {
|
|
84
|
+
const raw = process.env.OSTACKY_SENSITIVE_PATTERNS;
|
|
85
|
+
if (!raw) return SENSITIVE_DEFAULT;
|
|
86
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
87
|
+
}
|
package/assets/plugins/engram.ts
CHANGED
|
@@ -54,86 +54,17 @@ const ENGRAM_TOOLS = new Set([
|
|
|
54
54
|
])
|
|
55
55
|
|
|
56
56
|
// ─── Memory Instructions ─────────────────────────────────────────────────────
|
|
57
|
-
//
|
|
57
|
+
// Lazy: full protocol lives in assets/docs/engram-protocol.md (on-demand via Read).
|
|
58
|
+
// System injects only pointer + nudge, not full 1.2k. Source-of-truth: src/tiered.ts for isTrivial.
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
+
const MEMORY_POINTER = "Engram disponible — para formato mem_save/mem_search lee assets/docs/engram-protocol.md (on-demand). Usa mem_search proactivamente si el tema pudo verse antes."
|
|
61
|
+
const MEMORY_INSTRUCTIONS_LAZY = `## Engram — pointer (lazy)
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
${MEMORY_POINTER}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- Bug fix completed
|
|
67
|
-
- Architecture or design decision made
|
|
68
|
-
- Non-obvious discovery about the codebase
|
|
69
|
-
- Configuration change or environment setup
|
|
70
|
-
- Pattern established (naming, structure, convention)
|
|
71
|
-
- User preference or constraint learned
|
|
72
|
-
|
|
73
|
-
Format for \`mem_save\`:
|
|
74
|
-
- **title**: Verb + what — short, searchable (e.g. "Fixed N+1 query in UserList", "Chose Zustand over Redux")
|
|
75
|
-
- **type**: bugfix | decision | architecture | discovery | pattern | config | preference
|
|
76
|
-
- **scope**: \`project\` (default) | \`personal\`
|
|
77
|
-
- **topic_key** (optional, recommended for evolving decisions): stable key like \`architecture/auth-model\`
|
|
78
|
-
- **content**:
|
|
79
|
-
**What**: One sentence — what was done
|
|
80
|
-
**Why**: What motivated it (user request, bug, performance, etc.)
|
|
81
|
-
**Where**: Files or paths affected
|
|
82
|
-
**Learned**: Gotchas, edge cases, things that surprised you (omit if none)
|
|
83
|
-
|
|
84
|
-
Topic rules:
|
|
85
|
-
- Different topics must not overwrite each other (e.g. architecture vs bugfix)
|
|
86
|
-
- Reuse the same \`topic_key\` to update an evolving topic instead of creating new observations
|
|
87
|
-
- If unsure about the key, call \`mem_suggest_topic_key\` first and then reuse it
|
|
88
|
-
- Use \`mem_update\` when you have an exact observation ID to correct
|
|
89
|
-
|
|
90
|
-
### WHEN TO SEARCH MEMORY
|
|
91
|
-
|
|
92
|
-
When the user asks to recall something — any variation of "remember", "recall", "what did we do",
|
|
93
|
-
"how did we solve", or the equivalent in the user's language, or references to past work:
|
|
94
|
-
1. First call \`mem_context\` — checks recent session history (fast, cheap)
|
|
95
|
-
2. If not found, call \`mem_search\` with relevant keywords (FTS5 full-text search)
|
|
96
|
-
3. If you find a match, use \`mem_get_observation\` for full untruncated content
|
|
97
|
-
|
|
98
|
-
Also search memory PROACTIVELY when:
|
|
99
|
-
- Starting work on something that might have been done before
|
|
100
|
-
- The user mentions a topic you have no context on — check if past sessions covered it
|
|
101
|
-
- The user's FIRST message references the project, a feature, or a problem — call \`mem_search\` with keywords from their message to check for prior work before responding
|
|
102
|
-
|
|
103
|
-
### SESSION CLOSE PROTOCOL (mandatory)
|
|
104
|
-
|
|
105
|
-
Before ending a session or saying "done" / "that's it", you MUST:
|
|
106
|
-
1. Call \`mem_session_summary\` with this structure:
|
|
107
|
-
|
|
108
|
-
## Goal
|
|
109
|
-
[What we were working on this session]
|
|
110
|
-
|
|
111
|
-
## Instructions
|
|
112
|
-
[User preferences or constraints discovered — skip if none]
|
|
113
|
-
|
|
114
|
-
## Discoveries
|
|
115
|
-
- [Technical findings, gotchas, non-obvious learnings]
|
|
116
|
-
|
|
117
|
-
## Accomplished
|
|
118
|
-
- [Completed items with key details]
|
|
119
|
-
|
|
120
|
-
## Next Steps
|
|
121
|
-
- [What remains to be done — for the next session]
|
|
122
|
-
|
|
123
|
-
## Relevant Files
|
|
124
|
-
- path/to/file — [what it does or what changed]
|
|
125
|
-
|
|
126
|
-
This is NOT optional. If you skip this, the next session starts blind.
|
|
127
|
-
|
|
128
|
-
### AFTER COMPACTION
|
|
129
|
-
|
|
130
|
-
If you see a message about compaction or context reset, or if you see "FIRST ACTION REQUIRED" in your context:
|
|
131
|
-
1. IMMEDIATELY call \`mem_session_summary\` with the compacted summary content — this persists what was done before compaction
|
|
132
|
-
2. Then call \`mem_context\` to recover any additional context from previous sessions
|
|
133
|
-
3. Only THEN continue working
|
|
134
|
-
|
|
135
|
-
Do not skip step 1. Without it, everything done before compaction is lost from memory.
|
|
136
|
-
`
|
|
65
|
+
Cuando necesites guardar/buscar, lee el protocolo completo con Read. No alucines formato.`
|
|
66
|
+
// Compat: keep full for fallback if file missing — but never inject full in system.transform
|
|
67
|
+
const MEMORY_INSTRUCTIONS = MEMORY_INSTRUCTIONS_LAZY
|
|
137
68
|
|
|
138
69
|
// ─── HTTP Client ─────────────────────────────────────────────────────────────
|
|
139
70
|
|
|
@@ -270,6 +201,26 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
270
201
|
// inflation (e.g. 170 sessions for 1 real conversation, issue #116).
|
|
271
202
|
const subAgentSessions = new Set<string>()
|
|
272
203
|
|
|
204
|
+
// Tiered cache-friendly: single source of truth via src/tiered.ts
|
|
205
|
+
const trivialBySession = new Map<string, boolean>()
|
|
206
|
+
// isTrivial y getControllerState importados lógicamente desde src/tiered.ts
|
|
207
|
+
// Inlined para evitar import dinámico en plugin bundle — mantener regex idéntico a src/tiered.ts
|
|
208
|
+
function isTrivialMessage(msg: string, state: string): boolean {
|
|
209
|
+
if (!msg || state !== "DONE") return false
|
|
210
|
+
if (msg.trim().length >= 30) return false
|
|
211
|
+
if (!/^(hola|hey|gracias|buenas|hi|hello)\b/i.test(msg.trim())) return false
|
|
212
|
+
if (/(necesito|quiero|agregá|fix|bug|feature|auth|spec|implementar)/i.test(msg)) return false
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
function getControllerState(directory: string): string {
|
|
216
|
+
try {
|
|
217
|
+
const statePath = process.env.OSTACKY_STATE_PATH || join(directory, ".opencode", "ostacky-state.json")
|
|
218
|
+
const raw = readFileSync(statePath, "utf-8")
|
|
219
|
+
const j = JSON.parse(raw)
|
|
220
|
+
return j.state ?? "DONE"
|
|
221
|
+
} catch { return "DONE" }
|
|
222
|
+
}
|
|
223
|
+
|
|
273
224
|
/**
|
|
274
225
|
* Ensure a session exists in engram. Idempotent — calls POST /sessions
|
|
275
226
|
* which uses INSERT OR IGNORE. Safe to call multiple times.
|
|
@@ -407,6 +358,12 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
407
358
|
|
|
408
359
|
const finalContent = content || fallback
|
|
409
360
|
|
|
361
|
+
// Tiered: set trivial flag for system.transform lazy
|
|
362
|
+
try {
|
|
363
|
+
const state = getControllerState(ctx.directory)
|
|
364
|
+
trivialBySession.set(sessionId, isTrivialMessage(finalContent, state))
|
|
365
|
+
} catch {}
|
|
366
|
+
|
|
410
367
|
// Only capture non-trivial prompts (>10 chars)
|
|
411
368
|
if (finalContent.length > 10) {
|
|
412
369
|
await ensureSession(sessionId)
|
|
@@ -465,13 +422,24 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
465
422
|
// messages that would break these models. See: GitHub issue #23.
|
|
466
423
|
|
|
467
424
|
"experimental.chat.system.transform": async (input, output) => {
|
|
425
|
+
// Tiered lazy: SIEMPRE pointer (cache-friendly, ~1 línea). Full vive en assets/docs/engram-protocol.md on-demand.
|
|
426
|
+
const sessionId: string = (input as any).sessionID ?? ""
|
|
427
|
+
const isTrivial = trivialBySession.get(sessionId) ?? false
|
|
428
|
+
const state = getControllerState(ctx.directory)
|
|
429
|
+
const shouldBeTrivial = isTrivial && state === "DONE"
|
|
430
|
+
const pointer = shouldBeTrivial
|
|
431
|
+
? "Engram disponible — detalles a demanda (usa mem_search si necesitas recordar)."
|
|
432
|
+
: MEMORY_POINTER
|
|
468
433
|
if (output.system.length > 0) {
|
|
469
|
-
output.system[output.system.length - 1] += "\n\n" +
|
|
434
|
+
output.system[output.system.length - 1] += "\n\n" + pointer
|
|
470
435
|
} else {
|
|
471
|
-
output.system.push(
|
|
436
|
+
output.system.push(pointer)
|
|
472
437
|
}
|
|
438
|
+
// No inyectar MEMORY_INSTRUCTIONS completo nunca — se lee on-demand via Read
|
|
473
439
|
|
|
474
440
|
// ── Save nudge ──────────────────────────────────────────────────────────
|
|
441
|
+
// Skip nudge for trivial greeting (cache-friendly, no extra injection)
|
|
442
|
+
if (shouldBeTrivial) return
|
|
475
443
|
// If it has been a long time since the last mem_save, append a reminder
|
|
476
444
|
// to the system prompt so the agent notices. All fetches are fire-and-
|
|
477
445
|
// forget with short timeouts — any failure silently skips the nudge.
|
|
@@ -1,131 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Se registra en `tool.execute.before` y lanza Error si la operación no está
|
|
8
|
-
* permitida. OpenCode aborta el tool antes de tocar disco.
|
|
2
|
+
* @deprecated Fusionado en ostacky-plugin.ts — re-export legacy.
|
|
3
|
+
* Single source: importa desde src/security.ts vía controller.
|
|
4
|
+
* Fresh install solo escribe ostacky-plugin.ts + engram.ts.
|
|
5
|
+
* Este archivo no debe ser instalado por ostacky install (deprecado).
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
13
|
-
import { join, dirname, basename, resolve, relative } from "node:path"
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"**/.secrets/**",
|
|
18
|
-
"**/*.pem",
|
|
19
|
-
"**/*.key",
|
|
20
|
-
"**/.aws/**",
|
|
21
|
-
"**/.ssh/**",
|
|
22
|
-
"**/credentials.json",
|
|
23
|
-
"**/.npmrc",
|
|
24
|
-
]
|
|
10
|
+
// Re-export controller como guard por compatibilidad — no duplica isSensitive/BASH_SENSITIVE_RE
|
|
11
|
+
export { OstackyController as OstackyGuard } from "./ostacky-plugin.ts"
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
30
|
-
}
|
|
13
|
+
// También export default por compatibilidad con loaders que esperan default
|
|
14
|
+
import { OstackyController } from "./ostacky-plugin.ts"
|
|
15
|
+
export default OstackyController
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const lower = filePath.toLowerCase()
|
|
35
|
-
if (lower.endsWith(".env.example") || lower.endsWith(".env.template") || lower.endsWith(".env.sample")) return false
|
|
36
|
-
for (const pat of patterns) {
|
|
37
|
-
if (pat.includes(".env") && lower.split("/").pop()!.startsWith(".env")) return true
|
|
38
|
-
if (pat.includes(".secrets") && lower.includes(".secrets")) return true
|
|
39
|
-
if (pat.includes("*.pem") && lower.endsWith(".pem")) return true
|
|
40
|
-
if (pat.includes("*.key") && lower.endsWith(".key")) return true
|
|
41
|
-
if (pat.includes(".aws") && lower.includes(".aws")) return true
|
|
42
|
-
if (pat.includes(".ssh") && lower.includes(".ssh")) return true
|
|
43
|
-
if (pat.includes("credentials.json") && lower.endsWith("credentials.json")) return true
|
|
44
|
-
if (pat.includes(".npmrc") && lower.endsWith(".npmrc")) return true
|
|
45
|
-
}
|
|
46
|
-
if (/\.(pem|key)$/i.test(filePath)) return true
|
|
47
|
-
if (filePath.includes(".env")) {
|
|
48
|
-
const base = filePath.split("/").pop() || ""
|
|
49
|
-
if (base.startsWith(".env")) return true
|
|
50
|
-
}
|
|
51
|
-
return false
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function getStatePath(directory: string): string {
|
|
55
|
-
if (process.env.OSTACKY_STATE_PATH) return process.env.OSTACKY_STATE_PATH
|
|
56
|
-
// try opencode.json
|
|
57
|
-
const candidates = [join(directory, "opencode.json"), join(directory, "opencode.jsonc")]
|
|
58
|
-
for (const cand of candidates) {
|
|
59
|
-
try {
|
|
60
|
-
const raw = readFileSync(cand, "utf-8")
|
|
61
|
-
// naive parse, ignore comments
|
|
62
|
-
const json = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
|
|
63
|
-
const envPath = (json as any)?.mcp?.["ostacky-controller"]?.environment?.OSTACKY_STATE_PATH
|
|
64
|
-
if (typeof envPath === "string" && envPath) return envPath
|
|
65
|
-
} catch {}
|
|
66
|
-
}
|
|
67
|
-
return join(directory, ".opencode", "ostacky-state.json")
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function readState(directory: string): any | null {
|
|
71
|
-
const p = getStatePath(directory)
|
|
72
|
-
try {
|
|
73
|
-
const raw = readFileSync(p, "utf-8")
|
|
74
|
-
return JSON.parse(raw)
|
|
75
|
-
} catch {
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const OstackyGuard: Plugin = async (ctx) => {
|
|
81
|
-
const patterns = getEnvPatterns()
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
"tool.execute.before": async (input, output) => {
|
|
85
|
-
const tool = (input as any).tool as string
|
|
86
|
-
const args = (input as any).args as any
|
|
87
|
-
|
|
88
|
-
// Extraer filePath de args según tool
|
|
89
|
-
let filePath: string | undefined
|
|
90
|
-
if (tool === "read" || tool === "read_mcp_resource" || tool === "grep" || tool === "glob") {
|
|
91
|
-
filePath = args?.filePath || args?.path || args?.pattern || args?.uri || ""
|
|
92
|
-
// Para grep, el pattern puede ser contenido, no file; intentar detectar file
|
|
93
|
-
if (tool === "grep" && args?.include) filePath = args.include
|
|
94
|
-
if (tool === "read_mcp_resource" && typeof args?.uri === "string") {
|
|
95
|
-
// uri puede ser file:// o path
|
|
96
|
-
try {
|
|
97
|
-
const u = args.uri as string
|
|
98
|
-
if (u.startsWith("file://")) filePath = u.slice(7)
|
|
99
|
-
else if (u.includes("/") ) filePath = u
|
|
100
|
-
} catch {}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 10.2: Guard genérico de PENDING — bloquear cualquier tool no-controller cuando estado es PENDING
|
|
105
|
-
const state = readState(ctx.directory)
|
|
106
|
-
const pendingStates = ["ROUTE_DECISION_PENDING", "EXECUTION_DECISION_PENDING", "CLARIFICATION_PENDING"]
|
|
107
|
-
if (state && pendingStates.includes(state.state)) {
|
|
108
|
-
// Permitir solo tools del controller que desbloquean
|
|
109
|
-
const allowedTools = ["consume_route_decision", "consume_execution_decision", "record_clarification", "abandon", "check_file_access", "consume_file_access_decision", "record_user_confirmation"]
|
|
110
|
-
const isControllerTool = tool.startsWith("ostacky-controller_") || allowedTools.some((t) => tool.includes(t))
|
|
111
|
-
if (!isControllerTool) {
|
|
112
|
-
// Bloquear cualquier tool no-controller (Read/Grep/Glob/Bash/Edit/Write) cuando está en PENDING — hard gate genérico
|
|
113
|
-
throw new Error(`BLOCKED: call consume_* first — controller is in ${state.state}. Use consume_route_decision / consume_execution_decision / record_clarification to proceed.`)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 9.1: Guard de credenciales
|
|
118
|
-
if (filePath && isSensitive(filePath, patterns)) {
|
|
119
|
-
const s = readState(ctx.directory)
|
|
120
|
-
const allowed = s?.allowedFiles?.[filePath]
|
|
121
|
-
if (!allowed) {
|
|
122
|
-
const denied = s?.deniedFiles?.[filePath]
|
|
123
|
-
if (denied) {
|
|
124
|
-
throw new Error(`BLOCKED: File ${filePath} requires check_file_access (previously denied)`)
|
|
125
|
-
}
|
|
126
|
-
throw new Error(`BLOCKED: File ${filePath} requires check_file_access`)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
}
|
|
131
|
-
}
|
|
17
|
+
// Nota: isSensitive, BASH_SENSITIVE_RE, extractPathsFromBash vienen de src/security.ts
|
|
18
|
+
// a través de ostacky-plugin.ts, no se copian aquí (verificación check:security).
|