openAgent
gitlabThe open source UNIVERSAL agent.
clone
https://gitlab.com/bitzzdev/openAgent.gitAIagentautomationopen-sourceuniversal-agent
import { afterEach, expect } from "bun:test"import { LayerNode } from "@opencode-ai/core/effect/layer-node"import { Cause, Effect, Exit, Layer } from "effect"import path from "path"import { disposeAllInstances, TestInstance } from "../fixture/fixture"import { testEffect } from "../lib/effect"import { Agent } from "../../src/agent/agent"import { Auth } from "../../src/auth"import { Config } from "../../src/config/config"import { RuntimeFlags } from "../../src/effect/runtime-flags"import { Global } from "@opencode-ai/core/global"import { Permission } from "../../src/permission"import { PermissionV1 } from "@opencode-ai/core/v1/permission"import { Plugin } from "../../src/plugin"import { Provider } from "../../src/provider/provider"import { Skill } from "../../src/skill"import { Truncate } from "../../src/tool/truncate"const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>LayerNode.compile(LayerNode.group([Agent.node, Plugin.node, Provider.node, Auth.node, Config.node, Skill.node, RuntimeFlags.node]),[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],)const it = testEffect(agentLayer())// Helper to evaluate permission for a tool with wildcard patternfunction evalPerm(agent: Agent.Info | undefined, permission: string): PermissionV1.Action | undefined {if (!agent) return undefinedreturn Permission.evaluate(permission, "*", agent.permission).action}function load<A>(fn: (svc: Agent.Interface) => Effect.Effect<A>) {return Agent.Service.use(fn)}const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) {const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit)expect(Exit.isFailure(exit)).toBe(true)if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain(message)})afterEach(async () => {await disposeAllInstances()})it.instance("returns default native agents when no config", () =>Effect.gen(function* () {const agents = yield* load((svc) => svc.list())const names = agents.map((a) => a.name)expect(names).toContain("build")expect(names).toContain("plan")expect(names).toContain("general")expect(names).toContain("explore")expect(names).toContain("compaction")expect(names).toContain("title")expect(names).toContain("summary")}),)it.instance("build agent has correct default properties", () =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build).toBeDefined()expect(build?.mode).toBe("primary")expect(build?.native).toBe(true)expect(evalPerm(build, "edit")).toBe("allow")expect(evalPerm(build, "bash")).toBe("allow")}),)it.instance("plan agent denies edits except .opencode/plans/*", () =>Effect.gen(function* () {const plan = yield* load((svc) => svc.get("plan"))expect(plan).toBeDefined()// Wildcard is deniedexpect(evalPerm(plan, "edit")).toBe("deny")// But specific path is allowedexpect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow")}),)it.instance("plan agent denies the general subagent by default", () =>Effect.gen(function* () {const plan = yield* load((svc) => svc.get("plan"))expect(plan).toBeDefined()expect(Permission.evaluate("task", "general", plan!.permission).action).toBe("deny")expect(Permission.evaluate("task", "explore", plan!.permission).action).toBe("allow")expect(Permission.evaluate("task", "custom", plan!.permission).action).toBe("allow")}),)it.instance("user permission can allow the general subagent from plan mode",() =>Effect.gen(function* () {const plan = yield* load((svc) => svc.get("plan"))expect(plan).toBeDefined()expect(Permission.evaluate("task", "general", plan!.permission).action).toBe("allow")}),{config: {permission: {task: {general: "allow",},},},},)it.instance("explore agent denies edit and write", () =>Effect.gen(function* () {const explore = yield* load((svc) => svc.get("explore"))expect(explore).toBeDefined()expect(explore?.mode).toBe("subagent")expect(evalPerm(explore, "edit")).toBe("deny")expect(evalPerm(explore, "write")).toBe("deny")expect(evalPerm(explore, "todowrite")).toBe("deny")}),)it.instance("explore agent asks for external directories and allows whitelisted external paths", () =>Effect.gen(function* () {const explore = yield* load((svc) => svc.get("explore"))expect(explore).toBeDefined()expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")expect(Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,).toBe("allow")}),)it.instance("reference config does not create subagents",() =>Effect.gen(function* () {const agents = yield* load((svc) => svc.list())const names = agents.map((agent) => agent.name)expect(names).not.toContain("effect")expect(names).not.toContain("effectFull")expect(names).not.toContain("localdocs")expect(names).not.toContain("localdocsFull")}),{config: {references: {effect: "github.com/effect/effect-smol",effectFull: {repository: "Effect-TS/effect",branch: "main",},localdocs: "../docs",localdocsFull: {path: "../local-docs",},},},},)it.instance("general agent denies todo tools", () =>Effect.gen(function* () {const general = yield* load((svc) => svc.get("general"))expect(general).toBeDefined()expect(general?.mode).toBe("subagent")expect(general?.hidden).toBeUndefined()expect(evalPerm(general, "todowrite")).toBe("deny")}),)it.instance("compaction agent denies all permissions", () =>Effect.gen(function* () {const compaction = yield* load((svc) => svc.get("compaction"))expect(compaction).toBeDefined()expect(compaction?.hidden).toBe(true)expect(evalPerm(compaction, "bash")).toBe("deny")expect(evalPerm(compaction, "edit")).toBe("deny")expect(evalPerm(compaction, "read")).toBe("deny")}),)it.instance("custom agent from config creates new agent",() =>Effect.gen(function* () {const custom = yield* load((svc) => svc.get("my_custom_agent"))expect(custom).toBeDefined()expect(String(custom?.model?.providerID)).toBe("openai")expect(String(custom?.model?.modelID)).toBe("gpt-4")expect(custom?.description).toBe("My custom agent")expect(custom?.temperature).toBe(0.5)expect(custom?.topP).toBe(0.9)expect(custom?.native).toBe(false)expect(custom?.mode).toBe("all")}),{config: {agent: {my_custom_agent: {model: "openai/gpt-4",description: "My custom agent",temperature: 0.5,top_p: 0.9,},},},},)it.instance("custom agent config overrides native agent properties",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build).toBeDefined()expect(String(build?.model?.providerID)).toBe("anthropic")expect(String(build?.model?.modelID)).toBe("claude-3")expect(build?.description).toBe("Custom build agent")expect(build?.temperature).toBe(0.7)expect(build?.color).toBe("#FF0000")expect(build?.native).toBe(true)}),{config: {agent: {build: {model: "anthropic/claude-3",description: "Custom build agent",temperature: 0.7,color: "#FF0000",},},},},)it.instance("agent disable removes agent from list",() =>Effect.gen(function* () {const explore = yield* load((svc) => svc.get("explore"))expect(explore).toBeUndefined()const agents = yield* load((svc) => svc.list())const names = agents.map((a) => a.name)expect(names).not.toContain("explore")}),{config: {agent: {explore: { disable: true },},},},)it.instance("agent permission config merges with defaults",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build).toBeDefined()// Specific pattern is deniedexpect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")// Edit still allowedexpect(evalPerm(build, "edit")).toBe("allow")}),{config: {agent: {build: {permission: {bash: {"rm -rf *": "deny",},},},},},},)it.instance("global permission config applies to all agents",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build).toBeDefined()expect(evalPerm(build, "bash")).toBe("deny")}),{config: {permission: {bash: "deny",},},},)it.instance("agent steps/maxSteps config sets steps property",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))const plan = yield* load((svc) => svc.get("plan"))expect(build?.steps).toBe(50)expect(plan?.steps).toBe(100)}),{config: {agent: {build: { steps: 50 },plan: { maxSteps: 100 },},},},)it.instance("agent mode can be overridden",() =>Effect.gen(function* () {const explore = yield* load((svc) => svc.get("explore"))expect(explore?.mode).toBe("primary")}),{config: {agent: {explore: { mode: "primary" },},},},)it.instance("agent name can be overridden",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build?.name).toBe("Builder")}),{config: {agent: {build: { name: "Builder" },},},},)it.instance("agent prompt can be set from config",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build?.prompt).toBe("Custom system prompt")}),{config: {agent: {build: { prompt: "Custom system prompt" },},},},)it.instance("unknown agent properties are placed into options",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build?.options.random_property).toBe("hello")expect(build?.options.another_random).toBe(123)}),{config: {agent: {build: {random_property: "hello",another_random: 123,},},},},)it.instance("agent options merge correctly",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(build?.options.custom_option).toBe(true)expect(build?.options.another_option).toBe("value")}),{config: {agent: {build: {options: {custom_option: true,another_option: "value",},},},},},)it.instance("multiple custom agents can be defined",() =>Effect.gen(function* () {const agentA = yield* load((svc) => svc.get("agent_a"))const agentB = yield* load((svc) => svc.get("agent_b"))expect(agentA?.description).toBe("Agent A")expect(agentA?.mode).toBe("subagent")expect(agentB?.description).toBe("Agent B")expect(agentB?.mode).toBe("primary")}),{config: {agent: {agent_a: {description: "Agent A",mode: "subagent",},agent_b: {description: "Agent B",mode: "primary",},},},},)it.instance("Agent.list keeps the default agent first and sorts the rest by name",() =>Effect.gen(function* () {const names = (yield* load((svc) => svc.list())).map((a) => a.name)expect(names[0]).toBe("plan")expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))}),{config: {default_agent: "plan",agent: {zebra: {description: "Zebra",mode: "subagent",},alpha: {description: "Alpha",mode: "subagent",},},},},)it.instance("Agent.get returns undefined for non-existent agent", () =>Effect.gen(function* () {const nonExistent = yield* load((svc) => svc.get("does_not_exist"))expect(nonExistent).toBeUndefined()}),)it.instance("default permission includes doom_loop and external_directory as ask", () =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(evalPerm(build, "doom_loop")).toBe("ask")expect(evalPerm(build, "external_directory")).toBe("ask")}),)it.instance("webfetch is allowed by default", () =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(evalPerm(build, "webfetch")).toBe("allow")}),)it.instance("legacy tools config converts to permissions",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(evalPerm(build, "bash")).toBe("deny")expect(evalPerm(build, "read")).toBe("deny")}),{config: {agent: {build: {tools: {bash: false,read: false,},},},},},)it.instance("legacy tools config maps write/edit/patch to edit permission",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(evalPerm(build, "edit")).toBe("deny")}),{config: {agent: {build: {tools: {write: false,},},},},},)it.instance("Truncate.GLOB is allowed even when user denies external_directory globally",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")}),{config: {permission: {external_directory: "deny",},},},)it.instance("global tmp directory children are allowed for external_directory", () =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,).toBe("allow")expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")}),)it.instance("Truncate.GLOB is allowed even when user denies external_directory per-agent",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")}),{config: {agent: {build: {permission: {external_directory: "deny",},},},},},)it.instance("explicit Truncate.GLOB deny is respected",() =>Effect.gen(function* () {const build = yield* load((svc) => svc.get("build"))expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")}),{config: {permission: {external_directory: {"*": "deny",[Truncate.GLOB]: "deny",},},},},)it.instance("skill directories are allowed for external_directory",() =>Effect.gen(function* () {const test = yield* TestInstanceconst skillDir = path.join(test.directory, ".opencode", "skill", "perm-skill")yield* Effect.promise(() =>Bun.write(path.join(skillDir, "SKILL.md"),`---name: perm-skilldescription: Permission skill.---# Permission Skill`,),)const home = process.env.OPENCODE_TEST_HOMEprocess.env.OPENCODE_TEST_HOME = test.directoryyield* Effect.addFinalizer(() =>Effect.sync(() => {process.env.OPENCODE_TEST_HOME = home}),)const build = yield* load((svc) => svc.get("build"))const target = path.join(skillDir, "reference", "notes.md")expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")}),{ git: true },)it.instance("project reference directories are allowed for external_directory",() =>Effect.gen(function* () {const test = yield* TestInstanceconst build = yield* load((svc) => svc.get("build"))const target = path.resolve(test.directory, "../docs/reference/notes.md")expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")}),{git: true,config: {references: {docs: "../docs",},},},)it.instance("defaultAgent returns build when no default_agent config", () =>Effect.gen(function* () {const agent = yield* load((svc) => svc.defaultAgent())expect(agent).toBe("build")}),)it.instance("defaultInfo returns resolved build agent when no default_agent config", () =>Effect.gen(function* () {const agent = yield* load((svc) => svc.defaultInfo())expect(agent.name).toBe("build")expect(agent.mode).toBe("primary")}),)it.instance("defaultAgent respects default_agent config set to plan",() =>Effect.gen(function* () {const agent = yield* load((svc) => svc.defaultAgent())expect(agent).toBe("plan")}),{config: {default_agent: "plan",},},)it.instance("defaultAgent respects default_agent config set to custom agent with mode all",() =>Effect.gen(function* () {const agent = yield* load((svc) => svc.defaultAgent())expect(agent).toBe("my_custom")}),{config: {default_agent: "my_custom",agent: {my_custom: {description: "My custom agent",},},},},)it.instance("defaultAgent throws when default_agent points to subagent",() => expectDefaultAgentError('default agent "explore" is a subagent'),{config: {default_agent: "explore",},},)it.instance("defaultAgent throws when default_agent points to hidden agent",() => expectDefaultAgentError('default agent "compaction" is hidden'),{config: {default_agent: "compaction",},},)it.instance("defaultAgent throws when default_agent points to non-existent agent",() => expectDefaultAgentError('default agent "does_not_exist" not found'),{config: {default_agent: "does_not_exist",},},)it.instance("defaultAgent returns plan when build is disabled and default_agent not set",() =>Effect.gen(function* () {const agent = yield* load((svc) => svc.defaultAgent())// build is disabled, so it should return plan (next primary agent)expect(agent).toBe("plan")}),{config: {agent: {build: { disable: true },},},},)it.instance("defaultAgent throws when all primary agents are disabled",() => expectDefaultAgentError("no primary visible agent found"),{config: {agent: {build: { disable: true },plan: { disable: true },},},},)