ring/platforms/opencode/__tests__/plugin/security-hardening.test.ts
Gandalf f85aa61440
feat: add OpenCode platform integration (consolidate ring-for-opencode)
Move OpenCode runtime plugin and installer into Ring monorepo under platforms/opencode/.
The installer reads skills, agents, and commands directly from the Ring monorepo's
canonical directories (default/, dev-team/, pm-team/, etc.) — zero asset duplication.

What's included:
- installer.sh: reads from Ring dirs, applies frontmatter/tool transforms, installs to ~/.config/opencode/
- plugin/: TypeScript runtime (RingUnifiedPlugin) with hooks, lifecycle, loaders
- src/: CLI (doctor, config-manager)
- prompts/: session-start and context-injection templates
- standards/: coding standards (from dev-team/docs/)
- ring.jsonc: default config with full 86-skill/35-agent/33-command inventory

What's NOT included (intentionally):
- assets/ directory: eliminated, content comes from Ring monorepo
- scripts/codereview/: eliminated, replaced by mithril
- using-ring-opencode skill: uses canonical using-ring instead

Transforms applied by installer:
- Agent: type→mode, strip version/changelog/output_schema/input_schema
- Skill: keep name+description frontmatter, body unchanged
- Command: strip argument-hint (unsupported by OpenCode)
- All: normalize tool names (Bash→bash, Read→read, etc.)
- All: strip Model Requirement sections from agents

Replaces: LerianStudio/ring-for-opencode repository
Generated-by: Gandalf
AI-Model: claude-opus-4
2026-03-07 22:46:47 -03:00

86 lines
3.4 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { deepMerge } from "../../plugin/config/loader.js"
import { loadRingAgents } from "../../plugin/loaders/agent-loader.js"
import { loadRingCommands } from "../../plugin/loaders/command-loader.js"
import { loadRingSkills } from "../../plugin/loaders/skill-loader.js"
function mkdtemp(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix))
}
function writeFile(filePath: string, content: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, content, "utf-8")
}
describe("security hardening", () => {
test("deepMerge blocks prototype pollution (__proto__/constructor/prototype)", () => {
const protoBefore = ({} as Record<string, unknown>).polluted
// JSON.parse ensures __proto__ is treated as a plain enumerable key.
const source = JSON.parse(
'{"__proto__":{"polluted":true},"constructor":{"prototype":{"polluted":true}},"prototype":{"polluted":true},"safe":{"__proto__":{"pollutedNested":true},"b":2}}',
) as Record<string, unknown>
try {
const merged = deepMerge({ safe: { a: 1 } }, source as unknown as Partial<{ safe: unknown }>)
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
expect(({} as Record<string, unknown>).pollutedNested).toBeUndefined()
expect(protoBefore).toBeUndefined()
expect(merged).toMatchObject({ safe: { a: 1, b: 2 } })
} finally {
// In case of regression, clean global prototype to avoid cascading failures.
delete (Object.prototype as unknown as Record<string, unknown>).polluted
delete (Object.prototype as unknown as Record<string, unknown>).pollutedNested
}
})
test("loaders building maps from filenames use null-prototype objects and skip forbidden keys", () => {
const tmp = mkdtemp("ring-sec-loaders-")
try {
const pluginRoot = path.join(tmp, "plugin")
const projectRoot = path.join(tmp, "project")
// commands
writeFile(path.join(pluginRoot, "assets", "command", "ok.md"), "# ok")
writeFile(path.join(pluginRoot, "assets", "command", "__proto__.md"), "# bad")
const { commands } = loadRingCommands(pluginRoot, projectRoot)
expect(Object.getPrototypeOf(commands)).toBeNull()
expect(Object.keys(commands)).toEqual(["ring:ok"])
// agents
writeFile(
path.join(pluginRoot, "assets", "agent", "good.md"),
"---\ndescription: good\n---\nhello",
)
writeFile(path.join(pluginRoot, "assets", "agent", "constructor.md"), "---\n---\nnope")
const agents = loadRingAgents(pluginRoot, projectRoot)
expect(Object.getPrototypeOf(agents)).toBeNull()
expect(Object.keys(agents)).toEqual(["ring:good"])
// skills
writeFile(path.join(pluginRoot, "assets", "skill", "skill-1", "SKILL.md"), "# skill")
writeFile(
path.join(pluginRoot, "assets", "skill", "__proto__", "SKILL.md"),
"# should be skipped",
)
const skills = loadRingSkills(pluginRoot, projectRoot)
expect(Object.getPrototypeOf(skills)).toBeNull()
expect(Object.keys(skills)).toEqual(["ring:skill-1"])
// ensure no global pollution from attempting to set __proto__ keys
expect(({} as Record<string, unknown>).ok).toBeUndefined()
} finally {
fs.rmSync(tmp, { recursive: true, force: true })
}
})
})