opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Risk Envelope — Runtime risk classification for agent actions.
|
|
3
|
+
*
|
|
4
|
+
* Implementa spec: action-risk-envelope.
|
|
5
|
+
*
|
|
6
|
+
* Categorías:
|
|
7
|
+
* - SAFE: Read-only o efectos reversibles mínimos. Autónoma.
|
|
8
|
+
* - GUARDED: Mutaciones bounded y reversibles. Autónoma si está dentro del scope.
|
|
9
|
+
* - BLOCKED: Destructivo, irreversible, o sensible. Requiere autorización explícita.
|
|
10
|
+
* - UNKNOWN: herramienta no catalogada. Se trata como BLOCKED (fail-closed).
|
|
11
|
+
*
|
|
12
|
+
* Las capabilities explícitas viven en `src/policy/action-capabilities.js`.
|
|
13
|
+
* Aquí solo se aplica la evaluación contextual (path, scope, contenido del comando).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import nodePath from "node:path";
|
|
17
|
+
import nodeFs from "node:fs";
|
|
18
|
+
import {
|
|
19
|
+
CAPABILITY_LEVELS,
|
|
20
|
+
getEffectiveCapability,
|
|
21
|
+
isMutatingCapability,
|
|
22
|
+
} from "./action-capabilities.js";
|
|
23
|
+
|
|
24
|
+
const PROTECTED_PATHS = [
|
|
25
|
+
"/etc/", "/root/", "/boot/", "/sys/", "/proc/",
|
|
26
|
+
"/var/lib/", "/usr/bin/", "/usr/sbin/",
|
|
27
|
+
"C:\\Windows", "C:\\System32",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function isProtectedPath(path) {
|
|
31
|
+
if (!path || typeof path !== "string") return false;
|
|
32
|
+
return PROTECTED_PATHS.some((p) => path.startsWith(p) || path.includes(p));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Canonicalize and resolve a path against the task root.
|
|
37
|
+
*
|
|
38
|
+
* Estrategia:
|
|
39
|
+
* 1. Si la ruta es absoluta, se resuelve contra el cwd.
|
|
40
|
+
* 2. Si es relativa y hay taskRoot, se resuelve contra taskRoot.
|
|
41
|
+
* 3. Si el path resuelto existe en disco → se usa `fs.realpathSync` para
|
|
42
|
+
* resolver symlinks (un symlink que apunta fuera del scope se bloquea).
|
|
43
|
+
* 4. Si el path NO existe todavía (caso típico: write de archivo nuevo) →
|
|
44
|
+
* se usa el realpath del directorio padre. Esto evita que un symlink en
|
|
45
|
+
* un directorio padre redirija el archivo fuera del scope.
|
|
46
|
+
* 5. Si el padre tampoco existe → se devuelve la ruta absoluta resuelta
|
|
47
|
+
* (resolución lexical), suficiente para validar prefijo.
|
|
48
|
+
*
|
|
49
|
+
* Returns the absolute canonical path, or null if unsafe (path traversal).
|
|
50
|
+
*/
|
|
51
|
+
function canonicalPath(p, taskRoot) {
|
|
52
|
+
if (!p || typeof p !== "string") return null;
|
|
53
|
+
try {
|
|
54
|
+
let resolved;
|
|
55
|
+
if (nodePath.isAbsolute(p)) {
|
|
56
|
+
resolved = nodePath.resolve(p);
|
|
57
|
+
} else if (taskRoot) {
|
|
58
|
+
resolved = nodePath.resolve(taskRoot, p);
|
|
59
|
+
} else {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolver symlinks usando realpath. Rama depende de si el path existe.
|
|
64
|
+
if (nodeFs.existsSync(resolved)) {
|
|
65
|
+
try {
|
|
66
|
+
return nodeFs.realpathSync(resolved);
|
|
67
|
+
} catch {
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// El path no existe: usamos realpath del padre (cuando el padre existe)
|
|
73
|
+
// para que un symlink en el directorio padre no redirija fuera del scope.
|
|
74
|
+
const parent = nodePath.dirname(resolved);
|
|
75
|
+
if (nodeFs.existsSync(parent)) {
|
|
76
|
+
try {
|
|
77
|
+
const realParent = nodeFs.realpathSync(parent);
|
|
78
|
+
return nodePath.join(realParent, nodePath.basename(resolved));
|
|
79
|
+
} catch {
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Padre tampoco existe: devolvemos la ruta absoluta resuelta (lexical).
|
|
85
|
+
return resolved;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isWithinScope(absPath, taskRoot) {
|
|
92
|
+
if (!taskRoot) return true; // No scope constraint
|
|
93
|
+
try {
|
|
94
|
+
const resolvedRoot = nodePath.resolve(taskRoot);
|
|
95
|
+
// Si el root tiene symlinks, también los resolvemos para la comparación.
|
|
96
|
+
let canonicalRoot;
|
|
97
|
+
try {
|
|
98
|
+
canonicalRoot = nodeFs.realpathSync(resolvedRoot);
|
|
99
|
+
} catch {
|
|
100
|
+
canonicalRoot = resolvedRoot;
|
|
101
|
+
}
|
|
102
|
+
const rel = nodePath.relative(canonicalRoot, absPath);
|
|
103
|
+
return !rel.startsWith("..") && !nodePath.isAbsolute(rel);
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Structured error for policy blocks. MUST NOT be swallowed by catch.
|
|
111
|
+
*/
|
|
112
|
+
export class WamPolicyBlock extends Error {
|
|
113
|
+
constructor(message, policy = {}) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "WamPolicyBlock";
|
|
116
|
+
this.wamPolicyBlock = true;
|
|
117
|
+
this.policy = policy;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function evaluateBashCommand(cmd) {
|
|
122
|
+
if (!cmd || typeof cmd !== "string") return { safe: true };
|
|
123
|
+
|
|
124
|
+
// NOTE: en este regex NO se usa `\b` al final del grupo porque alternativas
|
|
125
|
+
// como `dd if=` terminan en `=` (no-word), por lo que `\b` no aplica.
|
|
126
|
+
// Tampoco se envuelve todo el grupo en `\b...\b` por la misma razón.
|
|
127
|
+
const destructive = /(rm\s+-rf|rm\s+-fr|dd\s+if=|mkfs|format|:()\s*\{|\bdrop\s+database\b|\bdrop\s+table\b|\btruncate\s+table\b|delete\s+from\s+\S+\s+where|pkill\b|killall\b)/i;
|
|
128
|
+
const privileged = /\b(sudo|chmod\s+777|chown\s+root|iptables|ufw\s+disable)\b/i;
|
|
129
|
+
const network = /\b(curl[^|]*\|\s*sh|wget[^|]*\|\s*sh|nc\s+-|netcat)\b/i;
|
|
130
|
+
const gitDestructive = /\bgit\s+push\s+(-f|--force)|git\s+reset\s+--hard|git\s+clean\s+-fd|git\s+filter-branch/i;
|
|
131
|
+
|
|
132
|
+
if (destructive.test(cmd)) return { safe: false, reason: `Comando destructivo detectado: ${cmd.slice(0, 80)}` };
|
|
133
|
+
if (privileged.test(cmd)) return { safe: false, reason: `Comando privilegiado detectado: ${cmd.slice(0, 80)}` };
|
|
134
|
+
if (gitDestructive.test(cmd)) return { safe: false, reason: `Operación git destructiva: ${cmd.slice(0, 80)}` };
|
|
135
|
+
if (network.test(cmd)) return { safe: false, reason: `Pipe a shell desde red: ${cmd.slice(0, 80)}` };
|
|
136
|
+
|
|
137
|
+
return { safe: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Evalúa una acción propuesta por el agente y retorna su clasificación de riesgo.
|
|
142
|
+
*
|
|
143
|
+
* Política fail-closed (C01):
|
|
144
|
+
* - Herramientas catalogadas como SAFE/GUARDED/BLOCKED se evalúan normalmente.
|
|
145
|
+
* - Herramientas NO catalogadas → se clasifican como UNKNOWN y se tratan como
|
|
146
|
+
* BLOCKED con reason "fail-closed: herramienta no catalogada".
|
|
147
|
+
*
|
|
148
|
+
* El match contra el catálogo es EXACTO (no sub-string), para evitar colisiones
|
|
149
|
+
* como "bash-safe" resolviendo a "bash".
|
|
150
|
+
*
|
|
151
|
+
* Las validaciones de scope y rutas protegidas aplican a TODAS las
|
|
152
|
+
* herramientas (incluso SAFE): un `read` de `/etc/passwd` o de un symlink
|
|
153
|
+
* fuera del root sigue siendo un escape de scope.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} tool — Nombre de la herramienta invocada.
|
|
156
|
+
* @param {object} args — Argumentos de la invocación.
|
|
157
|
+
* @param {string} taskRoot — Root de la tarea activa (para validación de scope).
|
|
158
|
+
* @returns {{level: string, reason?: string, requiresUser: boolean}}
|
|
159
|
+
*/
|
|
160
|
+
export function evaluateAction(tool, args = {}, taskRoot = "") {
|
|
161
|
+
const lowerTool = (tool || "").toLowerCase();
|
|
162
|
+
|
|
163
|
+
// 0. Resolución de capability explícita (fail-closed para UNKNOWN).
|
|
164
|
+
const capability = getEffectiveCapability(lowerTool);
|
|
165
|
+
|
|
166
|
+
// Fail-closed: herramientas no catalogadas → BLOCKED inmediato.
|
|
167
|
+
if (capability === CAPABILITY_LEVELS.BLOCKED) {
|
|
168
|
+
// Si la herramienta tampoco está en el catálogo (UNKNOWN → BLOCKED),
|
|
169
|
+
// el reason debe indicar fail-closed para que el guard lo registre.
|
|
170
|
+
const knownBlocked = new Set([
|
|
171
|
+
"rm", "rmdir", "mv", "sudo", "chmod", "chown",
|
|
172
|
+
"docker", "ssh", "scp", "rsync",
|
|
173
|
+
"push", "force_push",
|
|
174
|
+
]);
|
|
175
|
+
if (!knownBlocked.has(lowerTool)) {
|
|
176
|
+
return {
|
|
177
|
+
level: "BLOCKED",
|
|
178
|
+
reason: `fail-closed: herramienta no catalogada (${tool})`,
|
|
179
|
+
requiresUser: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
level: "BLOCKED",
|
|
184
|
+
reason: `Herramienta peligrosa por defecto: ${tool}`,
|
|
185
|
+
requiresUser: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 1. Extracción de path crudo y validaciones universales (aplican a
|
|
190
|
+
// SAFE y GUARDED). Las rutas protegidas o fuera del scope elevan
|
|
191
|
+
// la acción a BLOCKED independientemente del capability original.
|
|
192
|
+
const rawPath = args?.path || args?.file || args?.file_path || args?.target || "";
|
|
193
|
+
if (isProtectedPath(rawPath)) {
|
|
194
|
+
return {
|
|
195
|
+
level: "BLOCKED",
|
|
196
|
+
reason: `Ruta protegida: ${rawPath}`,
|
|
197
|
+
requiresUser: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (taskRoot && rawPath) {
|
|
202
|
+
const canonical = canonicalPath(rawPath, taskRoot);
|
|
203
|
+
if (canonical && !isWithinScope(canonical, taskRoot)) {
|
|
204
|
+
return {
|
|
205
|
+
level: "BLOCKED",
|
|
206
|
+
reason: `Acción fuera del scope de tarea (path traversal): ${rawPath} → ${canonical} (root: ${taskRoot})`,
|
|
207
|
+
requiresUser: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. SAFE: autónoma (post validación de scope).
|
|
213
|
+
if (capability === CAPABILITY_LEVELS.SAFE) {
|
|
214
|
+
return { level: "SAFE", requiresUser: false };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 3. GUARDED: requiere validación contextual adicional (bash commands).
|
|
218
|
+
if (capability === CAPABILITY_LEVELS.GUARDED) {
|
|
219
|
+
if (lowerTool === "bash" || lowerTool === "sh" || lowerTool === "pty_spawn") {
|
|
220
|
+
const cmd = args?.command || args?.cmd || args?.script || "";
|
|
221
|
+
const bashEval = evaluateBashCommand(cmd);
|
|
222
|
+
if (!bashEval.safe) {
|
|
223
|
+
return {
|
|
224
|
+
level: "BLOCKED",
|
|
225
|
+
reason: bashEval.reason,
|
|
226
|
+
requiresUser: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { level: "GUARDED", requiresUser: false };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cualquier otro nivel no debería ocurrir (SAFE/GUARDED/BLOCKED cubren todo
|
|
235
|
+
// gracias a fail-closed), pero por seguridad devolvemos BLOCKED.
|
|
236
|
+
return {
|
|
237
|
+
level: "BLOCKED",
|
|
238
|
+
reason: `fail-closed: capacidad no resuelta (${tool})`,
|
|
239
|
+
requiresUser: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const RISK_LEVELS = { SAFE: "SAFE", GUARDED: "GUARDED", BLOCKED: "BLOCKED" };
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Conjunto de herramientas mutantes (GUARDED + BLOCKED explícitos).
|
|
247
|
+
* NOTA: las herramientas UNKNOWN NO aparecen aquí — la verificación
|
|
248
|
+
* de mutación se hace preferentemente con `isMutatingTool`, que aplica
|
|
249
|
+
* fail-closed para UNKNOWN. Este Set se conserva por compatibilidad.
|
|
250
|
+
*/
|
|
251
|
+
export const MUTATING_TOOLS = new Set([
|
|
252
|
+
// GUARDED
|
|
253
|
+
"write", "edit", "apply_patch", "patch",
|
|
254
|
+
"todo_write", "todowrite",
|
|
255
|
+
"bash", "sh", "pty_spawn", "pty_write",
|
|
256
|
+
"task",
|
|
257
|
+
// BLOCKED
|
|
258
|
+
"rm", "rmdir", "mv", "sudo", "chmod", "chown",
|
|
259
|
+
"docker", "ssh", "scp", "rsync",
|
|
260
|
+
"push", "force_push",
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
export function isMutatingTool(tool) {
|
|
264
|
+
// Fail-closed: una herramienta desconocida también se considera mutante.
|
|
265
|
+
return isMutatingCapability(tool);
|
|
266
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope & Strategy Enforcement — scope drift detection.
|
|
3
|
+
* Implements spec: scope-and-strategy-enforcement.
|
|
4
|
+
*
|
|
5
|
+
* Strategy = objective, scope, allowedActions, prohibitedActions
|
|
6
|
+
* Current Execution = hypothesis, experiment, files touched, actions, evidence
|
|
7
|
+
* Drift = deviation between strategy and current execution.
|
|
8
|
+
*
|
|
9
|
+
* Policy:
|
|
10
|
+
* none / small_safe → allow + record
|
|
11
|
+
* material → require authorization
|
|
12
|
+
* prohibited → block
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DRIFT_LEVELS = {
|
|
16
|
+
NONE: "none",
|
|
17
|
+
SMALL_SAFE: "small_safe",
|
|
18
|
+
MATERIAL: "material",
|
|
19
|
+
PROHIBITED: "prohibited",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SAFE_FAMILIES = new Set([
|
|
23
|
+
"read", "search", "inspect", "test", "lint", "typecheck", "list", "glob", "grep",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const MATERIAL_FAMILIES = new Set([
|
|
27
|
+
"refactor", "rename", "migrate", "schema", "schema-change", "restructure", "rewrite",
|
|
28
|
+
"scope expansion", "scope-expansion", "architectural change",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const PROHIBITED_FAMILIES = new Set([
|
|
32
|
+
"drop database", "drop schema", "production deploy", "delete production data",
|
|
33
|
+
"credential modification", "destructive operation", "rm -rf", "git reset --hard",
|
|
34
|
+
"wipe", "truncate", "shred",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function familyOf(action = "", command = "") {
|
|
38
|
+
const text = `${action} ${command}`.toLowerCase();
|
|
39
|
+
for (const p of PROHIBITED_FAMILIES) {
|
|
40
|
+
if (text.includes(p)) return { level: DRIFT_LEVELS.PROHIBITED, family: p };
|
|
41
|
+
}
|
|
42
|
+
for (const m of MATERIAL_FAMILIES) {
|
|
43
|
+
if (text.includes(m)) return { level: DRIFT_LEVELS.MATERIAL, family: m };
|
|
44
|
+
}
|
|
45
|
+
for (const s of SAFE_FAMILIES) {
|
|
46
|
+
if (text.includes(s)) return { level: DRIFT_LEVELS.SMALL_SAFE, family: s };
|
|
47
|
+
}
|
|
48
|
+
return { level: DRIFT_LEVELS.NONE, family: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isInScope(action, strategy) {
|
|
52
|
+
if (!strategy) return false;
|
|
53
|
+
const allowed = strategy.allowedActions || [];
|
|
54
|
+
const prohibited = strategy.prohibitedActions || [];
|
|
55
|
+
const a = (action || "").toLowerCase();
|
|
56
|
+
for (const p of prohibited) {
|
|
57
|
+
if (a.includes(p.toLowerCase())) return { allowed: false, reason: `prohibited: ${p}` };
|
|
58
|
+
}
|
|
59
|
+
for (const allow of allowed) {
|
|
60
|
+
if (a.includes(allow.toLowerCase())) return { allowed: true, reason: `matched: ${allow}` };
|
|
61
|
+
}
|
|
62
|
+
return { allowed: false, reason: "no allowed action matched" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function detectScopeDrift(strategy, execution) {
|
|
66
|
+
const action = execution?.action || execution?.description || "";
|
|
67
|
+
const command = execution?.command || execution?.args?.command || "";
|
|
68
|
+
|
|
69
|
+
// Empty action = no drift
|
|
70
|
+
if (!action && !command) {
|
|
71
|
+
return {
|
|
72
|
+
level: DRIFT_LEVELS.NONE,
|
|
73
|
+
family: null,
|
|
74
|
+
allowed: true,
|
|
75
|
+
reason: "no action",
|
|
76
|
+
action,
|
|
77
|
+
command,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = familyOf(action, command);
|
|
82
|
+
const scopeCheck = isInScope(action, strategy);
|
|
83
|
+
|
|
84
|
+
let drift = result.level;
|
|
85
|
+
|
|
86
|
+
// If action is explicitly allowed by strategy, it's at most SMALL_SAFE
|
|
87
|
+
if (scopeCheck.allowed === true) {
|
|
88
|
+
// Allowed by strategy: if it's already a safe family, keep it; otherwise make it SMALL_SAFE
|
|
89
|
+
if (result.level === DRIFT_LEVELS.NONE) {
|
|
90
|
+
drift = DRIFT_LEVELS.SMALL_SAFE;
|
|
91
|
+
}
|
|
92
|
+
} else if (scopeCheck.allowed === false && result.level !== DRIFT_LEVELS.PROHIBITED) {
|
|
93
|
+
// Not allowed by strategy
|
|
94
|
+
if (result.level === DRIFT_LEVELS.SMALL_SAFE) {
|
|
95
|
+
drift = DRIFT_LEVELS.SMALL_SAFE;
|
|
96
|
+
} else if (result.level === DRIFT_LEVELS.NONE) {
|
|
97
|
+
drift = DRIFT_LEVELS.MATERIAL;
|
|
98
|
+
} else {
|
|
99
|
+
drift = result.level;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
level: drift,
|
|
105
|
+
family: result.family,
|
|
106
|
+
allowed: scopeCheck.allowed,
|
|
107
|
+
reason: scopeCheck.reason,
|
|
108
|
+
action,
|
|
109
|
+
command,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function policyFor(drift) {
|
|
114
|
+
switch (drift.level) {
|
|
115
|
+
case DRIFT_LEVELS.NONE:
|
|
116
|
+
case DRIFT_LEVELS.SMALL_SAFE:
|
|
117
|
+
return "allow+record";
|
|
118
|
+
case DRIFT_LEVELS.MATERIAL:
|
|
119
|
+
return "require-authorization";
|
|
120
|
+
case DRIFT_LEVELS.PROHIBITED:
|
|
121
|
+
return "block";
|
|
122
|
+
default:
|
|
123
|
+
return "block";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function recordDrift(taskState, drift) {
|
|
128
|
+
if (!taskState) return taskState;
|
|
129
|
+
if (!Array.isArray(taskState.driftLog)) taskState.driftLog = [];
|
|
130
|
+
taskState.driftLog.push({
|
|
131
|
+
at: Date.now(),
|
|
132
|
+
level: drift.level,
|
|
133
|
+
family: drift.family,
|
|
134
|
+
action: drift.action,
|
|
135
|
+
reason: drift.reason,
|
|
136
|
+
});
|
|
137
|
+
return taskState;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const SCOPE_DRIFT = DRIFT_LEVELS;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Scanner — regex-based detection of common secret patterns.
|
|
3
|
+
*
|
|
4
|
+
* Detects: AWS keys, GitHub tokens, generic API keys, JWTs, private
|
|
5
|
+
* keys, Slack tokens. Returns findings with line numbers. Production
|
|
6
|
+
* hardening (M5-56).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} Finding
|
|
11
|
+
* @property {string} type
|
|
12
|
+
* @property {number} line
|
|
13
|
+
* @property {number} column
|
|
14
|
+
* @property {string} match
|
|
15
|
+
* @property {string} severity
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} ScanResult
|
|
20
|
+
* @property {Finding[]} findings
|
|
21
|
+
* @property {boolean} clean
|
|
22
|
+
* @property {number} count
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** @type {Array<{type: string, severity: string, regex: RegExp}>} */
|
|
26
|
+
const PATTERNS = [
|
|
27
|
+
{ type: "aws_access_key", severity: "high", regex: /AKIA[0-9A-Z]{16}/g },
|
|
28
|
+
{ type: "github_token", severity: "high", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
29
|
+
{ type: "slack_token", severity: "high", regex: /xox[abprs]-[A-Za-z0-9-]{10,}/g },
|
|
30
|
+
{ type: "private_key", severity: "high", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
31
|
+
{ type: "jwt", severity: "medium", regex: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
32
|
+
{ type: "generic_api_key", severity: "medium", regex: /(?:api[_-]?key|apikey|secret)\s*[:=]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?/gi },
|
|
33
|
+
{ type: "stripe_key", severity: "high", regex: /sk_(?:live|test)_[A-Za-z0-9]{24,}/g },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Scan text for secret patterns.
|
|
38
|
+
* @param {string} text
|
|
39
|
+
* @returns {ScanResult}
|
|
40
|
+
*/
|
|
41
|
+
export function scanSecrets(text) {
|
|
42
|
+
if (typeof text !== "string") {
|
|
43
|
+
throw new TypeError("text must be string");
|
|
44
|
+
}
|
|
45
|
+
const findings = [];
|
|
46
|
+
const lines = text.split(/\r?\n/);
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
for (const p of PATTERNS) {
|
|
50
|
+
p.regex.lastIndex = 0;
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = p.regex.exec(line)) !== null) {
|
|
53
|
+
findings.push({
|
|
54
|
+
type: p.type,
|
|
55
|
+
severity: p.severity,
|
|
56
|
+
line: i + 1,
|
|
57
|
+
column: m.index + 1,
|
|
58
|
+
match: m[0],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
findings,
|
|
65
|
+
clean: findings.length === 0,
|
|
66
|
+
count: findings.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Portable skill discovery.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order (deterministic precedence — first wins):
|
|
9
|
+
* 1. project-local: <root>/.opencode/skills
|
|
10
|
+
* 2. project-local: <root>/.opencode/.skills
|
|
11
|
+
* 3. user-global: <homedir>/.config/opencode/skills
|
|
12
|
+
* 4. user-global: <homedir>/.config/opencode/.skills
|
|
13
|
+
* 5. user-global: <homedir>/.claude/skills
|
|
14
|
+
* 6. user-global: <homedir>/.agents/skills
|
|
15
|
+
* 7. user-global: <homedir>/.opencode/skills
|
|
16
|
+
* 8. bundled: <thisFile>/../../skills
|
|
17
|
+
*
|
|
18
|
+
* The first occurrence of a skill name wins. All paths are computed
|
|
19
|
+
* dynamically from `os.homedir()` and the optional `root` argument so the
|
|
20
|
+
* module is portable across machines.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
function fileExists(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(filePath);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the ordered list of skill directories to scan.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @param {string} [opts.root] - Project root. Defaults to process.cwd().
|
|
37
|
+
* @param {string} [opts.home] - Override homedir (used by tests).
|
|
38
|
+
* @returns {{ dir: string, tier: string }[]}
|
|
39
|
+
*/
|
|
40
|
+
export function getSkillSearchPaths({ root, home } = {}) {
|
|
41
|
+
const effectiveRoot = root || process.cwd();
|
|
42
|
+
const effectiveHome = home || os.homedir();
|
|
43
|
+
const thisDir = path.dirname(new URL(import.meta.url).pathname);
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
{ dir: path.join(effectiveRoot, ".opencode", "skills"), tier: "project-local" },
|
|
47
|
+
{ dir: path.join(effectiveRoot, ".opencode", ".skills"), tier: "project-local" },
|
|
48
|
+
{ dir: path.join(effectiveHome, ".config", "opencode", "skills"), tier: "user-global" },
|
|
49
|
+
{ dir: path.join(effectiveHome, ".config", "opencode", ".skills"), tier: "user-global" },
|
|
50
|
+
{ dir: path.join(effectiveHome, ".claude", "skills"), tier: "user-global" },
|
|
51
|
+
{ dir: path.join(effectiveHome, ".agents", "skills"), tier: "user-global" },
|
|
52
|
+
{ dir: path.join(effectiveHome, ".opencode", "skills"), tier: "user-global" },
|
|
53
|
+
{ dir: path.join(thisDir, "..", "..", "skills"), tier: "bundled" },
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Discover all available skills across the configured search paths.
|
|
59
|
+
* Earlier tiers override later ones (project-local > user-global > bundled).
|
|
60
|
+
*
|
|
61
|
+
* @param {object} [opts] - forwarded to getSkillSearchPaths
|
|
62
|
+
* @returns {Object<string, { name: string, path: string, dir: string, tier: string }>}
|
|
63
|
+
*/
|
|
64
|
+
export function discoverSkills(opts = {}) {
|
|
65
|
+
const searchPaths = getSkillSearchPaths(opts);
|
|
66
|
+
const candidates = {};
|
|
67
|
+
|
|
68
|
+
for (const { dir: skillDir, tier } of searchPaths) {
|
|
69
|
+
if (!fileExists(skillDir)) continue;
|
|
70
|
+
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory()) continue;
|
|
80
|
+
const skillPath = path.join(skillDir, entry.name, "SKILL.md");
|
|
81
|
+
if (fileExists(skillPath)) {
|
|
82
|
+
if (!candidates[entry.name]) {
|
|
83
|
+
candidates[entry.name] = {
|
|
84
|
+
name: entry.name,
|
|
85
|
+
path: skillPath,
|
|
86
|
+
dir: skillDir,
|
|
87
|
+
tier,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return candidates;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Return the list of configured search paths (useful for debugging and tests).
|
|
99
|
+
* @param {object} [opts]
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function getSkillSearchDirs(opts = {}) {
|
|
103
|
+
return getSkillSearchPaths(opts).map((p) => p.dir);
|
|
104
|
+
}
|