geneseed 2.0.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/CHANGELOG.md +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Geneseed — OpenCode runtime guard plugin.
|
|
2
|
+
//
|
|
3
|
+
// Enforces the safety Laws at the tool boundary (`tool.execute.before`), the same
|
|
4
|
+
// "enforce by injection, don't just instruct" stance as the context plugin:
|
|
5
|
+
// - Law I (Sealed Secrets): block writes to private-key / credential files.
|
|
6
|
+
// - Law IV (Deletion Is Deliberate): block catastrophic shell commands.
|
|
7
|
+
// - Law VI (rule vs memory): speed-bump the first write to user-rules.md or a
|
|
8
|
+
// memory file — that choice belongs to the user, via the rule skill.
|
|
9
|
+
// - Wiki (AGENT.md §7): block mutations under a declared wiki's `protected`
|
|
10
|
+
// folders — the user's knowledge base sets its own no-go zones in wiki.jsonc.
|
|
11
|
+
// High-confidence patterns only, so legitimate work is never caught. Borderline cases
|
|
12
|
+
// (.env edits, force-push) are WARNED, not blocked.
|
|
13
|
+
//
|
|
14
|
+
// GENESEED_GUARD=off disable entirely.
|
|
15
|
+
// GENESEED_GUARD=warn downgrade every block to a warning (log, but allow).
|
|
16
|
+
//
|
|
17
|
+
// Install: dropped into the plugins dir by `build --emit opencode[-global]` (the *.js
|
|
18
|
+
// glob), exactly like the context and learn plugins. Errors never break a tool call.
|
|
19
|
+
|
|
20
|
+
import { promises as fs } from "node:fs"
|
|
21
|
+
import * as path from "node:path"
|
|
22
|
+
import { fileURLToPath } from "node:url"
|
|
23
|
+
import { homedir } from "node:os"
|
|
24
|
+
|
|
25
|
+
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
|
|
27
|
+
// Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
|
|
28
|
+
// <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
|
|
29
|
+
// this GLOBAL install goes dormant. Any error degrades to "not excluded".
|
|
30
|
+
const norm = (p) => {
|
|
31
|
+
// Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
|
|
32
|
+
// like "~/vault" must resolve the same way here as it does for the Claude/Bob
|
|
33
|
+
// hook guard.
|
|
34
|
+
let raw = String(p)
|
|
35
|
+
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
36
|
+
raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
|
|
37
|
+
}
|
|
38
|
+
let s = path.resolve(raw)
|
|
39
|
+
return process.platform === "win32" ? s.toLowerCase() : s
|
|
40
|
+
}
|
|
41
|
+
async function sovereignBypass(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
const cfg = path.dirname(PLUGIN_DIR) // plugins/ sits directly in <cfg>
|
|
44
|
+
const raw = await fs.readFile(path.join(cfg, "excludes.json"), "utf8")
|
|
45
|
+
const entries = (JSON.parse(raw).excludes) || []
|
|
46
|
+
const here = norm(cwd || process.cwd())
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
const p = typeof e === "string" ? e : e && e.path
|
|
49
|
+
if (typeof p !== "string" || !p.trim()) continue
|
|
50
|
+
const base = norm(p.trim()).replace(/[\\/]+$/, "")
|
|
51
|
+
if (here === base || here.startsWith(base + path.sep)) return true
|
|
52
|
+
}
|
|
53
|
+
} catch { /* degrade to active */ }
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MODE = (process.env.GENESEED_GUARD || "on").toLowerCase()
|
|
58
|
+
const OFF = ["off", "0", "false", "no"].includes(MODE)
|
|
59
|
+
const WARN_ONLY = MODE === "warn"
|
|
60
|
+
|
|
61
|
+
function log(msg) { console.error(`[geneseed-guard] ${msg}`) }
|
|
62
|
+
|
|
63
|
+
// Files the agent must not write (secrets / private keys) → BLOCK.
|
|
64
|
+
const SECRET_RE = [
|
|
65
|
+
/(^|\/)id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/i,
|
|
66
|
+
/\.(pem|key|p12|pfx|kdbx|keystore|jks)$/i,
|
|
67
|
+
/(^|\/)\.aws\/credentials$/i,
|
|
68
|
+
/(^|\/)\.ssh\//i,
|
|
69
|
+
/(^|\/)\.npmrc$/i,
|
|
70
|
+
/(^|\/)\.pypirc$/i,
|
|
71
|
+
]
|
|
72
|
+
// .env files are often edited legitimately → WARN, don't hard-block.
|
|
73
|
+
const SECRET_WARN_RE = [/(^|\/)\.env(\.[\w.-]+)?$/i]
|
|
74
|
+
|
|
75
|
+
// Catastrophic, effectively irreversible shell → BLOCK. Both Unix and Windows shells,
|
|
76
|
+
// since OpenCode runs natively on Windows and the agent may emit cmd / PowerShell.
|
|
77
|
+
const SHELL_BLOCK_RE = [
|
|
78
|
+
/\brm\s+-[a-z]*r[a-z]*f?[a-z]*\s+(--no-preserve-root\s+)?\/(\s|$)/, // rm -rf /
|
|
79
|
+
/\brm\s+-[a-z]*r[a-z]*f?[a-z]*\s+~(\/)?(\s|$)/, // rm -rf ~
|
|
80
|
+
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // fork bomb
|
|
81
|
+
/\bmkfs\.\w+\s+\/dev\//, // format a device
|
|
82
|
+
/\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|hd|disk)/, // dd over a raw disk
|
|
83
|
+
/\bformat\s+[a-z]:/i, // Windows: format a drive
|
|
84
|
+
/\b(rmdir|rd)\b[^\n]*\/s\b[^\n]*\b[a-z]:[\\/]?(\s|"|$)/i, // Windows: rd /s /q C:\
|
|
85
|
+
/\bdel\b[^\n]*\/s\b[^\n]*\b[a-z]:[\\/]?(\s|"|$)/i, // Windows: del /s /q C:\
|
|
86
|
+
/\bremove-item\b[^\n]*-recurse\b[^\n]*-force\b/i, // PowerShell rm -rf
|
|
87
|
+
/\bremove-item\b[^\n]*-force\b[^\n]*-recurse\b/i, // (either flag order)
|
|
88
|
+
]
|
|
89
|
+
// History-rewriting / irreversible git ops → WARN.
|
|
90
|
+
const SHELL_WARN_RE = [/\bgit\s+push\b[^\n]*(--force\b|-f\b)/, /\bgit\s+reset\s+--hard\b/]
|
|
91
|
+
|
|
92
|
+
function pickPath(args) {
|
|
93
|
+
for (const k of ["filePath", "path", "file", "target", "filename"]) {
|
|
94
|
+
// Normalize Windows backslashes to `/` so the secret-path patterns (which use `/`
|
|
95
|
+
// as the segment separator) match `C:\Users\me\.ssh\id_rsa` the same as a POSIX path.
|
|
96
|
+
if (args && typeof args[k] === "string") return args[k].replace(/\\/g, "/")
|
|
97
|
+
}
|
|
98
|
+
return ""
|
|
99
|
+
}
|
|
100
|
+
function pickCommand(args) {
|
|
101
|
+
for (const k of ["command", "cmd", "script"]) {
|
|
102
|
+
if (args && typeof args[k] === "string") return args[k]
|
|
103
|
+
}
|
|
104
|
+
return ""
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tool-name classes, matched as SUBSTRINGS so a host's variant names
|
|
108
|
+
// (write_file, str_replace_editor, apply_patch, run_command, execute_bash, …) are
|
|
109
|
+
// still covered as the tool surface evolves. Over-matching the class is harmless:
|
|
110
|
+
// the inner guards act only when a real path/command argument is present AND matches
|
|
111
|
+
// a high-confidence secret/catastrophe pattern, so a misclassified tool with no such
|
|
112
|
+
// argument simply does nothing.
|
|
113
|
+
const WRITE_TOOLS = ["write", "edit", "patch", "create", "save", "insert", "replace"]
|
|
114
|
+
const SHELL_TOOLS = ["bash", "shell", "exec", "command", "terminal", "run"]
|
|
115
|
+
const hasAny = (name, parts) => parts.some((s) => name.includes(s))
|
|
116
|
+
|
|
117
|
+
// ---- protected wiki folders (AGENT.md §7) --------------------------------------
|
|
118
|
+
// wiki.jsonc (the machine-level knowledge-base manifest) may list `protected` folders
|
|
119
|
+
// per wiki. Mutating anything under one is denied. Same resolution chain as the
|
|
120
|
+
// context plugin: $GENESEED_WIKI -> $GENESEED_HARNESS/wiki.jsonc -> beside the install.
|
|
121
|
+
async function isFile(p) { try { return (await fs.stat(p)).isFile() } catch { return false } }
|
|
122
|
+
|
|
123
|
+
// wiki.jsonc is JSONC (the seeded stub carries a commented example): strip // and
|
|
124
|
+
// /* */ comments plus trailing commas before parsing — string-aware, so quoted
|
|
125
|
+
// "https://…" or "C:/…" values are untouched. Kept in sync with the context plugin's
|
|
126
|
+
// copy (plugins stay self-contained, like the other shared helpers).
|
|
127
|
+
function stripJsonc(text) {
|
|
128
|
+
let out = "", inStr = false, esc = false
|
|
129
|
+
for (let i = 0; i < text.length; i++) {
|
|
130
|
+
const c = text[i]
|
|
131
|
+
if (inStr) {
|
|
132
|
+
out += c
|
|
133
|
+
if (esc) esc = false
|
|
134
|
+
else if (c === "\\") esc = true
|
|
135
|
+
else if (c === '"') inStr = false
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (c === '"') { inStr = true; out += c; continue }
|
|
139
|
+
if (c === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; out += "\n"; continue }
|
|
140
|
+
if (c === "/" && text[i + 1] === "*") { i += 2; while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++; i++; continue }
|
|
141
|
+
out += c
|
|
142
|
+
}
|
|
143
|
+
let res = ""
|
|
144
|
+
inStr = false; esc = false
|
|
145
|
+
for (let i = 0; i < out.length; i++) {
|
|
146
|
+
const c = out[i]
|
|
147
|
+
if (inStr) {
|
|
148
|
+
res += c
|
|
149
|
+
if (esc) esc = false
|
|
150
|
+
else if (c === "\\") esc = true
|
|
151
|
+
else if (c === '"') inStr = false
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (c === '"') { inStr = true; res += c; continue }
|
|
155
|
+
if (c === ",") {
|
|
156
|
+
let j = i + 1
|
|
157
|
+
while (j < out.length && /\s/.test(out[j])) j++
|
|
158
|
+
if (out[j] === "]" || out[j] === "}") continue
|
|
159
|
+
}
|
|
160
|
+
res += c
|
|
161
|
+
}
|
|
162
|
+
return res
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function wikiFile() {
|
|
166
|
+
const explicit = process.env.GENESEED_WIKI
|
|
167
|
+
if (explicit && (await isFile(explicit))) return explicit
|
|
168
|
+
const bases = []
|
|
169
|
+
if (process.env.GENESEED_HARNESS) bases.push(process.env.GENESEED_HARNESS)
|
|
170
|
+
bases.push(path.resolve(PLUGIN_DIR, ".."))
|
|
171
|
+
for (const base of bases) {
|
|
172
|
+
// wiki.json is the legacy name from earlier seeds — still honoured.
|
|
173
|
+
for (const name of ["wiki.jsonc", "wiki.json"]) {
|
|
174
|
+
const p = path.join(base, name)
|
|
175
|
+
if (await isFile(p)) return p
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Cached absolute prefixes, refreshed on a short TTL so a wiki.jsonc edit lands
|
|
182
|
+
// without a restart. Compared slash-normalized and case-insensitive — vault paths on
|
|
183
|
+
// Windows and macOS are case-insensitive in practice, and for a guard the rare
|
|
184
|
+
// case-only over-match is the safe direction.
|
|
185
|
+
let _prot = { at: 0, prefixes: [] }
|
|
186
|
+
const PROT_TTL_MS = 30000
|
|
187
|
+
async function protectedPrefixes() {
|
|
188
|
+
const now = Date.now()
|
|
189
|
+
if (now - _prot.at < PROT_TTL_MS) return _prot.prefixes
|
|
190
|
+
const prefixes = []
|
|
191
|
+
try {
|
|
192
|
+
const file = await wikiFile()
|
|
193
|
+
if (file) {
|
|
194
|
+
const data = JSON.parse(stripJsonc(await fs.readFile(file, "utf8")))
|
|
195
|
+
for (const w of Array.isArray(data?.wikis) ? data.wikis : []) {
|
|
196
|
+
if (!w?.path || !Array.isArray(w.protected)) continue
|
|
197
|
+
for (const d of w.protected) {
|
|
198
|
+
if (typeof d !== "string" || !d) continue
|
|
199
|
+
const abs = path.resolve(w.path, d).replace(/\\/g, "/").replace(/\/+$/, "")
|
|
200
|
+
prefixes.push({ prefix: abs.toLowerCase() + "/",
|
|
201
|
+
label: `${w.name || path.basename(w.path)}: ${d}` })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch { /* unreadable manifest = no extra protection; never break a tool call */ }
|
|
206
|
+
_prot = { at: now, prefixes }
|
|
207
|
+
return prefixes
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- the rule / memory stores (Law VI) -----------------------------------------
|
|
211
|
+
// Whether something the user wants kept is a standing rule or a durable fact is THEIR
|
|
212
|
+
// call, settled through the rule skill. `tool.execute.before` can only allow or throw —
|
|
213
|
+
// it has no "ask the user" tier like the Claude/Bob rule-gate hook —
|
|
214
|
+
// so this is a SPEED BUMP, not a wall: the first write to a given path is refused with
|
|
215
|
+
// the Law and the skill named, and a re-issued write goes through. One beat of
|
|
216
|
+
// reconsideration, and a legitimate write is never trapped.
|
|
217
|
+
//
|
|
218
|
+
// Install folder and file names are theme-independent by design, so the match is on
|
|
219
|
+
// literal names. `user-rules.md` / `MEMORY.md` are Geneseed's own coinage and match
|
|
220
|
+
// anywhere; other markdown matches only inside this install's own memory/ dir, so a
|
|
221
|
+
// project's unrelated memory/ folder is never caught.
|
|
222
|
+
const RULE_STORE_BUMPED = new Set()
|
|
223
|
+
const nocase = (s) => (process.platform === "win32" ? s.toLowerCase() : s)
|
|
224
|
+
function ruleStoreTarget(p) {
|
|
225
|
+
const abs = (path.isAbsolute(p) ? p : path.resolve(p)).replace(/\\/g, "/")
|
|
226
|
+
const base = path.basename(abs).toLowerCase()
|
|
227
|
+
if (base === "user-rules.md") return "user-rules.md"
|
|
228
|
+
if (base === "memory.md") return "the memory index"
|
|
229
|
+
if (!base.endsWith(".md")) return null
|
|
230
|
+
const mem = path.join(path.dirname(PLUGIN_DIR), "memory").replace(/\\/g, "/")
|
|
231
|
+
return nocase(abs).startsWith(nocase(mem) + "/") ? "memory" : null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Mutation-class tools for the wiki check — wider than WRITE_TOOLS because moving,
|
|
235
|
+
// renaming, or deleting a protected note is as destructive as overwriting it. Same
|
|
236
|
+
// substring stance: over-matching the class is harmless (see WRITE_TOOLS note).
|
|
237
|
+
const WIKI_MUTATE_TOOLS = [...WRITE_TOOLS, "delete", "remove", "rename", "move", "trash"]
|
|
238
|
+
|
|
239
|
+
export const GeneseedGuard = async () => {
|
|
240
|
+
return {
|
|
241
|
+
"tool.execute.before": async (input, output) => {
|
|
242
|
+
if (OFF) return
|
|
243
|
+
if (await sovereignBypass(process.cwd())) return
|
|
244
|
+
const tool = (input?.tool || input?.name || "").toLowerCase()
|
|
245
|
+
const args = output?.args || input?.args || {}
|
|
246
|
+
const deny = (why) => {
|
|
247
|
+
if (WARN_ONLY) { log(`WARN (would block): ${why}`); return false }
|
|
248
|
+
log(`BLOCKED: ${why}`)
|
|
249
|
+
throw new Error(`[geneseed-guard] blocked: ${why} — set GENESEED_GUARD=off to allow`)
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
if (hasAny(tool, WRITE_TOOLS)) {
|
|
253
|
+
const p = pickPath(args)
|
|
254
|
+
if (p && SECRET_RE.some((re) => re.test(p))) { deny(`write to secret/key file ${p} (Law I)`); return }
|
|
255
|
+
if (p && SECRET_WARN_RE.some((re) => re.test(p))) log(`WARN: writing ${p} — keep secrets out of tracked files (Law I)`)
|
|
256
|
+
const store = p && ruleStoreTarget(p)
|
|
257
|
+
if (store && !RULE_STORE_BUMPED.has(p)) {
|
|
258
|
+
RULE_STORE_BUMPED.add(p)
|
|
259
|
+
deny(`writing to ${store} — a standing rule, or a fact to remember? That ` +
|
|
260
|
+
`choice is the user's (Law VI). Settle it through the rule skill, ` +
|
|
261
|
+
`then re-issue this write`)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (hasAny(tool, WIKI_MUTATE_TOOLS)) {
|
|
266
|
+
const p = pickPath(args)
|
|
267
|
+
if (p) {
|
|
268
|
+
const abs = (path.isAbsolute(p) ? p : path.resolve(p)).replace(/\\/g, "/").toLowerCase()
|
|
269
|
+
const hit = (await protectedPrefixes()).find(
|
|
270
|
+
(x) => abs.startsWith(x.prefix) || abs === x.prefix.slice(0, -1))
|
|
271
|
+
if (hit) { deny(`mutation in protected wiki folder — ${hit.label} (AGENT.md §7)`); return }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// NOT else-if: a compound tool name (e.g. exec_and_save) can match both
|
|
275
|
+
// classes, and the shell check must still run after the write check.
|
|
276
|
+
if (hasAny(tool, SHELL_TOOLS)) {
|
|
277
|
+
const c = pickCommand(args)
|
|
278
|
+
if (c && SHELL_BLOCK_RE.some((re) => re.test(c))) { deny(`catastrophic command (Law IV): ${c.slice(0, 80)}`); return }
|
|
279
|
+
if (c && SHELL_WARN_RE.some((re) => re.test(c))) log(`WARN: irreversible op — confirm intent (Law IV): ${c.slice(0, 80)}`)
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// Our own deny must propagate; any inspection error must never break a tool call.
|
|
283
|
+
if (err && String(err.message || "").startsWith("[geneseed-guard]")) throw err
|
|
284
|
+
log(`inspect error (ignored): ${err?.message ?? err}`)
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default GeneseedGuard
|