policy.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { describe, expect } from "bun:test"
  2. import { Document, Event, Info, type Entry } from "@opencode-ai/schema/config"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { Config } from "@opencode-ai/core/config"
  5. import { ConfigPolicyPlugin } from "@opencode-ai/core/config/plugin/policy"
  6. import { Bus } from "@opencode-ai/core/bus"
  7. import { Plugin } from "@opencode-ai/core/plugin"
  8. import { PluginHost } from "@opencode-ai/core/plugin/host"
  9. import { Provider } from "@opencode-ai/core/provider"
  10. import { Effect, Schema, Stream } from "effect"
  11. import { testEffect } from "../lib/effect"
  12. import { PluginTestLayer } from "../plugin/fixture"
  13. const it = testEffect(PluginTestLayer)
  14. const decode = Schema.decodeUnknownSync(Info)
  15. const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) =>
  16. new Document({
  17. type: "document",
  18. info: decode({
  19. experimental: {
  20. policies: items.map((item) => ({ action: "provider.use", ...item })),
  21. },
  22. }),
  23. })
  24. const addPlugin = Effect.fn(function* (entries: Entry[]) {
  25. const plugin = yield* Plugin.Service
  26. const host = yield* PluginHost.make(plugin)
  27. yield* ConfigPolicyPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries)))
  28. })
  29. describe("ConfigPolicyPlugin.Plugin", () => {
  30. it.effect("filters plugin-provided providers with ordered wildcard policies", () =>
  31. Effect.gen(function* () {
  32. const catalog = yield* Catalog.Service
  33. yield* catalog.transform((catalog) => {
  34. catalog.provider.update(Provider.ID.openai, () => {})
  35. catalog.provider.update(Provider.ID.anthropic, () => {})
  36. catalog.provider.update(Provider.ID.make("company-internal"), () => {})
  37. })
  38. yield* addPlugin([
  39. policies(
  40. { effect: "deny", resource: "*" },
  41. { effect: "allow", resource: "anthropic" },
  42. { effect: "allow", resource: "company-*" },
  43. ),
  44. ])
  45. expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined()
  46. expect(yield* catalog.provider.get(Provider.ID.anthropic)).toBeDefined()
  47. expect(yield* catalog.provider.get(Provider.ID.make("company-internal"))).toBeDefined()
  48. }),
  49. )
  50. it.effect("prevents project policy from overriding user-global policy", () =>
  51. Effect.gen(function* () {
  52. const catalog = yield* Catalog.Service
  53. yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {}))
  54. yield* addPlugin([
  55. policies({ effect: "deny", resource: "openai" }),
  56. policies({ effect: "allow", resource: "openai" }),
  57. ])
  58. expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined()
  59. }),
  60. )
  61. it.live("reloads changed policies", () =>
  62. Effect.gen(function* () {
  63. const catalog = yield* Catalog.Service
  64. const bus = yield* Bus.Service
  65. const test = yield* Config.Test
  66. const plugin = yield* Plugin.Service
  67. const host = yield* PluginHost.make(plugin)
  68. yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {}))
  69. yield* ConfigPolicyPlugin.Plugin.effect(host)
  70. expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined()
  71. yield* test.setEntries([policies({ effect: "allow", resource: "openai" })])
  72. yield* bus.publish(Event.Updated, {})
  73. yield* waitUntil(catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined)))
  74. }).pipe(Effect.provide(Config.testLayer([policies({ effect: "deny", resource: "openai" })]))),
  75. )
  76. })
  77. const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
  78. for (let attempt = 0; attempt < 200; attempt++) {
  79. if (yield* condition) return
  80. yield* Effect.sleep("10 millis")
  81. }
  82. return yield* Effect.die("Timed out waiting for policy reload")
  83. })