agent.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. import { afterEach, expect } from "bun:test"
  2. import { Cause, Effect, Exit } from "effect"
  3. import path from "path"
  4. import { disposeAllInstances, TestInstance } from "../fixture/fixture"
  5. import { testEffect } from "../lib/effect"
  6. import { Agent } from "../../src/agent/agent"
  7. import { Global } from "@opencode-ai/core/global"
  8. import { Flag } from "@opencode-ai/core/flag/flag"
  9. import { Permission } from "../../src/permission"
  10. import { Truncate } from "../../src/tool/truncate"
  11. const it = testEffect(Agent.defaultLayer)
  12. // Helper to evaluate permission for a tool with wildcard pattern
  13. function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
  14. if (!agent) return undefined
  15. return Permission.evaluate(permission, "*", agent.permission).action
  16. }
  17. function load<A>(fn: (svc: Agent.Interface) => Effect.Effect<A>) {
  18. return Agent.Service.use(fn)
  19. }
  20. function withExperimentalScout<A, E, R>(enabled: boolean, self: Effect.Effect<A, E, R>) {
  21. return Effect.acquireUseRelease(
  22. Effect.sync(() => {
  23. const original = Flag.OPENCODE_EXPERIMENTAL_SCOUT
  24. Flag.OPENCODE_EXPERIMENTAL_SCOUT = enabled
  25. return original
  26. }),
  27. () => self,
  28. (original) =>
  29. Effect.sync(() => {
  30. Flag.OPENCODE_EXPERIMENTAL_SCOUT = original
  31. }),
  32. )
  33. }
  34. const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) {
  35. const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit)
  36. expect(Exit.isFailure(exit)).toBe(true)
  37. if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain(message)
  38. })
  39. afterEach(async () => {
  40. await disposeAllInstances()
  41. })
  42. it.instance("returns default native agents when no config", () =>
  43. withExperimentalScout(
  44. false,
  45. Effect.gen(function* () {
  46. const agents = yield* load((svc) => svc.list())
  47. const names = agents.map((a) => a.name)
  48. expect(names).toContain("build")
  49. expect(names).toContain("plan")
  50. expect(names).toContain("general")
  51. expect(names).toContain("explore")
  52. expect(names).not.toContain("scout")
  53. expect(names).toContain("compaction")
  54. expect(names).toContain("title")
  55. expect(names).toContain("summary")
  56. }),
  57. ),
  58. )
  59. it.instance("build agent has correct default properties", () =>
  60. Effect.gen(function* () {
  61. const build = yield* load((svc) => svc.get("build"))
  62. expect(build).toBeDefined()
  63. expect(build?.mode).toBe("primary")
  64. expect(build?.native).toBe(true)
  65. expect(evalPerm(build, "edit")).toBe("allow")
  66. expect(evalPerm(build, "bash")).toBe("allow")
  67. expect(evalPerm(build, "repo_clone")).toBe("deny")
  68. expect(evalPerm(build, "repo_overview")).toBe("deny")
  69. }),
  70. )
  71. it.instance("plan agent denies edits except .opencode/plans/*", () =>
  72. Effect.gen(function* () {
  73. const plan = yield* load((svc) => svc.get("plan"))
  74. expect(plan).toBeDefined()
  75. // Wildcard is denied
  76. expect(evalPerm(plan, "edit")).toBe("deny")
  77. // But specific path is allowed
  78. expect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow")
  79. }),
  80. )
  81. it.instance("explore agent denies edit and write", () =>
  82. Effect.gen(function* () {
  83. const explore = yield* load((svc) => svc.get("explore"))
  84. expect(explore).toBeDefined()
  85. expect(explore?.mode).toBe("subagent")
  86. expect(evalPerm(explore, "edit")).toBe("deny")
  87. expect(evalPerm(explore, "write")).toBe("deny")
  88. expect(evalPerm(explore, "todowrite")).toBe("deny")
  89. }),
  90. )
  91. it.instance("explore agent asks for external directories and allows whitelisted external paths", () =>
  92. Effect.gen(function* () {
  93. const explore = yield* load((svc) => svc.get("explore"))
  94. expect(explore).toBeDefined()
  95. expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
  96. expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
  97. expect(
  98. Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,
  99. ).toBe("allow")
  100. }),
  101. )
  102. it.instance("scout agent allows repo cloning and repo cache reads", () =>
  103. withExperimentalScout(
  104. true,
  105. Effect.gen(function* () {
  106. const scout = yield* load((svc) => svc.get("scout"))
  107. expect(scout).toBeDefined()
  108. expect(scout?.mode).toBe("subagent")
  109. expect(evalPerm(scout, "repo_clone")).toBe("allow")
  110. expect(evalPerm(scout, "repo_overview")).toBe("allow")
  111. expect(evalPerm(scout, "edit")).toBe("deny")
  112. expect(
  113. Permission.evaluate(
  114. "external_directory",
  115. path.join(Global.Path.repos, "github.com", "owner", "repo", "README.md"),
  116. scout!.permission,
  117. ).action,
  118. ).toBe("allow")
  119. }),
  120. ),
  121. )
  122. it.instance(
  123. "reference config does not create subagents",
  124. () =>
  125. withExperimentalScout(
  126. true,
  127. Effect.gen(function* () {
  128. const agents = yield* load((svc) => svc.list())
  129. const names = agents.map((agent) => agent.name)
  130. expect(names).toContain("scout")
  131. expect(names).not.toContain("effect")
  132. expect(names).not.toContain("effectFull")
  133. expect(names).not.toContain("localdocs")
  134. expect(names).not.toContain("localdocsFull")
  135. }),
  136. ),
  137. {
  138. config: {
  139. reference: {
  140. effect: "github.com/effect/effect-smol",
  141. effectFull: {
  142. repository: "Effect-TS/effect",
  143. branch: "main",
  144. },
  145. localdocs: "../docs",
  146. localdocsFull: {
  147. path: "../local-docs",
  148. },
  149. },
  150. },
  151. },
  152. )
  153. it.instance("general agent denies todo tools", () =>
  154. Effect.gen(function* () {
  155. const general = yield* load((svc) => svc.get("general"))
  156. expect(general).toBeDefined()
  157. expect(general?.mode).toBe("subagent")
  158. expect(general?.hidden).toBeUndefined()
  159. expect(evalPerm(general, "todowrite")).toBe("deny")
  160. }),
  161. )
  162. it.instance("compaction agent denies all permissions", () =>
  163. Effect.gen(function* () {
  164. const compaction = yield* load((svc) => svc.get("compaction"))
  165. expect(compaction).toBeDefined()
  166. expect(compaction?.hidden).toBe(true)
  167. expect(evalPerm(compaction, "bash")).toBe("deny")
  168. expect(evalPerm(compaction, "edit")).toBe("deny")
  169. expect(evalPerm(compaction, "read")).toBe("deny")
  170. }),
  171. )
  172. it.instance(
  173. "custom agent from config creates new agent",
  174. () =>
  175. Effect.gen(function* () {
  176. const custom = yield* load((svc) => svc.get("my_custom_agent"))
  177. expect(custom).toBeDefined()
  178. expect(String(custom?.model?.providerID)).toBe("openai")
  179. expect(String(custom?.model?.modelID)).toBe("gpt-4")
  180. expect(custom?.description).toBe("My custom agent")
  181. expect(custom?.temperature).toBe(0.5)
  182. expect(custom?.topP).toBe(0.9)
  183. expect(custom?.native).toBe(false)
  184. expect(custom?.mode).toBe("all")
  185. }),
  186. {
  187. config: {
  188. agent: {
  189. my_custom_agent: {
  190. model: "openai/gpt-4",
  191. description: "My custom agent",
  192. temperature: 0.5,
  193. top_p: 0.9,
  194. },
  195. },
  196. },
  197. },
  198. )
  199. it.instance(
  200. "custom agent config overrides native agent properties",
  201. () =>
  202. Effect.gen(function* () {
  203. const build = yield* load((svc) => svc.get("build"))
  204. expect(build).toBeDefined()
  205. expect(String(build?.model?.providerID)).toBe("anthropic")
  206. expect(String(build?.model?.modelID)).toBe("claude-3")
  207. expect(build?.description).toBe("Custom build agent")
  208. expect(build?.temperature).toBe(0.7)
  209. expect(build?.color).toBe("#FF0000")
  210. expect(build?.native).toBe(true)
  211. }),
  212. {
  213. config: {
  214. agent: {
  215. build: {
  216. model: "anthropic/claude-3",
  217. description: "Custom build agent",
  218. temperature: 0.7,
  219. color: "#FF0000",
  220. },
  221. },
  222. },
  223. },
  224. )
  225. it.instance(
  226. "agent disable removes agent from list",
  227. () =>
  228. Effect.gen(function* () {
  229. const explore = yield* load((svc) => svc.get("explore"))
  230. expect(explore).toBeUndefined()
  231. const agents = yield* load((svc) => svc.list())
  232. const names = agents.map((a) => a.name)
  233. expect(names).not.toContain("explore")
  234. }),
  235. {
  236. config: {
  237. agent: {
  238. explore: { disable: true },
  239. },
  240. },
  241. },
  242. )
  243. it.instance(
  244. "agent permission config merges with defaults",
  245. () =>
  246. Effect.gen(function* () {
  247. const build = yield* load((svc) => svc.get("build"))
  248. expect(build).toBeDefined()
  249. // Specific pattern is denied
  250. expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
  251. // Edit still allowed
  252. expect(evalPerm(build, "edit")).toBe("allow")
  253. }),
  254. {
  255. config: {
  256. agent: {
  257. build: {
  258. permission: {
  259. bash: {
  260. "rm -rf *": "deny",
  261. },
  262. },
  263. },
  264. },
  265. },
  266. },
  267. )
  268. it.instance(
  269. "global permission config applies to all agents",
  270. () =>
  271. Effect.gen(function* () {
  272. const build = yield* load((svc) => svc.get("build"))
  273. expect(build).toBeDefined()
  274. expect(evalPerm(build, "bash")).toBe("deny")
  275. }),
  276. {
  277. config: {
  278. permission: {
  279. bash: "deny",
  280. },
  281. },
  282. },
  283. )
  284. it.instance(
  285. "agent steps/maxSteps config sets steps property",
  286. () =>
  287. Effect.gen(function* () {
  288. const build = yield* load((svc) => svc.get("build"))
  289. const plan = yield* load((svc) => svc.get("plan"))
  290. expect(build?.steps).toBe(50)
  291. expect(plan?.steps).toBe(100)
  292. }),
  293. {
  294. config: {
  295. agent: {
  296. build: { steps: 50 },
  297. plan: { maxSteps: 100 },
  298. },
  299. },
  300. },
  301. )
  302. it.instance(
  303. "agent mode can be overridden",
  304. () =>
  305. Effect.gen(function* () {
  306. const explore = yield* load((svc) => svc.get("explore"))
  307. expect(explore?.mode).toBe("primary")
  308. }),
  309. {
  310. config: {
  311. agent: {
  312. explore: { mode: "primary" },
  313. },
  314. },
  315. },
  316. )
  317. it.instance(
  318. "agent name can be overridden",
  319. () =>
  320. Effect.gen(function* () {
  321. const build = yield* load((svc) => svc.get("build"))
  322. expect(build?.name).toBe("Builder")
  323. }),
  324. {
  325. config: {
  326. agent: {
  327. build: { name: "Builder" },
  328. },
  329. },
  330. },
  331. )
  332. it.instance(
  333. "agent prompt can be set from config",
  334. () =>
  335. Effect.gen(function* () {
  336. const build = yield* load((svc) => svc.get("build"))
  337. expect(build?.prompt).toBe("Custom system prompt")
  338. }),
  339. {
  340. config: {
  341. agent: {
  342. build: { prompt: "Custom system prompt" },
  343. },
  344. },
  345. },
  346. )
  347. it.instance(
  348. "unknown agent properties are placed into options",
  349. () =>
  350. Effect.gen(function* () {
  351. const build = yield* load((svc) => svc.get("build"))
  352. expect(build?.options.random_property).toBe("hello")
  353. expect(build?.options.another_random).toBe(123)
  354. }),
  355. {
  356. config: {
  357. agent: {
  358. build: {
  359. random_property: "hello",
  360. another_random: 123,
  361. },
  362. },
  363. },
  364. },
  365. )
  366. it.instance(
  367. "agent options merge correctly",
  368. () =>
  369. Effect.gen(function* () {
  370. const build = yield* load((svc) => svc.get("build"))
  371. expect(build?.options.custom_option).toBe(true)
  372. expect(build?.options.another_option).toBe("value")
  373. }),
  374. {
  375. config: {
  376. agent: {
  377. build: {
  378. options: {
  379. custom_option: true,
  380. another_option: "value",
  381. },
  382. },
  383. },
  384. },
  385. },
  386. )
  387. it.instance(
  388. "multiple custom agents can be defined",
  389. () =>
  390. Effect.gen(function* () {
  391. const agentA = yield* load((svc) => svc.get("agent_a"))
  392. const agentB = yield* load((svc) => svc.get("agent_b"))
  393. expect(agentA?.description).toBe("Agent A")
  394. expect(agentA?.mode).toBe("subagent")
  395. expect(agentB?.description).toBe("Agent B")
  396. expect(agentB?.mode).toBe("primary")
  397. }),
  398. {
  399. config: {
  400. agent: {
  401. agent_a: {
  402. description: "Agent A",
  403. mode: "subagent",
  404. },
  405. agent_b: {
  406. description: "Agent B",
  407. mode: "primary",
  408. },
  409. },
  410. },
  411. },
  412. )
  413. it.instance(
  414. "Agent.list keeps the default agent first and sorts the rest by name",
  415. () =>
  416. Effect.gen(function* () {
  417. const names = (yield* load((svc) => svc.list())).map((a) => a.name)
  418. expect(names[0]).toBe("plan")
  419. expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
  420. }),
  421. {
  422. config: {
  423. default_agent: "plan",
  424. agent: {
  425. zebra: {
  426. description: "Zebra",
  427. mode: "subagent",
  428. },
  429. alpha: {
  430. description: "Alpha",
  431. mode: "subagent",
  432. },
  433. },
  434. },
  435. },
  436. )
  437. it.instance("Agent.get returns undefined for non-existent agent", () =>
  438. Effect.gen(function* () {
  439. const nonExistent = yield* load((svc) => svc.get("does_not_exist"))
  440. expect(nonExistent).toBeUndefined()
  441. }),
  442. )
  443. it.instance("default permission includes doom_loop and external_directory as ask", () =>
  444. Effect.gen(function* () {
  445. const build = yield* load((svc) => svc.get("build"))
  446. expect(evalPerm(build, "doom_loop")).toBe("ask")
  447. expect(evalPerm(build, "external_directory")).toBe("ask")
  448. }),
  449. )
  450. it.instance("webfetch is allowed by default", () =>
  451. Effect.gen(function* () {
  452. const build = yield* load((svc) => svc.get("build"))
  453. expect(evalPerm(build, "webfetch")).toBe("allow")
  454. }),
  455. )
  456. it.instance(
  457. "legacy tools config converts to permissions",
  458. () =>
  459. Effect.gen(function* () {
  460. const build = yield* load((svc) => svc.get("build"))
  461. expect(evalPerm(build, "bash")).toBe("deny")
  462. expect(evalPerm(build, "read")).toBe("deny")
  463. }),
  464. {
  465. config: {
  466. agent: {
  467. build: {
  468. tools: {
  469. bash: false,
  470. read: false,
  471. },
  472. },
  473. },
  474. },
  475. },
  476. )
  477. it.instance(
  478. "legacy tools config maps write/edit/patch to edit permission",
  479. () =>
  480. Effect.gen(function* () {
  481. const build = yield* load((svc) => svc.get("build"))
  482. expect(evalPerm(build, "edit")).toBe("deny")
  483. }),
  484. {
  485. config: {
  486. agent: {
  487. build: {
  488. tools: {
  489. write: false,
  490. },
  491. },
  492. },
  493. },
  494. },
  495. )
  496. it.instance(
  497. "Truncate.GLOB is allowed even when user denies external_directory globally",
  498. () =>
  499. Effect.gen(function* () {
  500. const build = yield* load((svc) => svc.get("build"))
  501. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
  502. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  503. expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
  504. }),
  505. {
  506. config: {
  507. permission: {
  508. external_directory: "deny",
  509. },
  510. },
  511. },
  512. )
  513. it.instance("global tmp directory children are allowed for external_directory", () =>
  514. Effect.gen(function* () {
  515. const build = yield* load((svc) => svc.get("build"))
  516. expect(
  517. Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,
  518. ).toBe("allow")
  519. expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")
  520. }),
  521. )
  522. it.instance(
  523. "Truncate.GLOB is allowed even when user denies external_directory per-agent",
  524. () =>
  525. Effect.gen(function* () {
  526. const build = yield* load((svc) => svc.get("build"))
  527. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
  528. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  529. expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
  530. }),
  531. {
  532. config: {
  533. agent: {
  534. build: {
  535. permission: {
  536. external_directory: "deny",
  537. },
  538. },
  539. },
  540. },
  541. },
  542. )
  543. it.instance(
  544. "explicit Truncate.GLOB deny is respected",
  545. () =>
  546. Effect.gen(function* () {
  547. const build = yield* load((svc) => svc.get("build"))
  548. expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
  549. expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
  550. }),
  551. {
  552. config: {
  553. permission: {
  554. external_directory: {
  555. "*": "deny",
  556. [Truncate.GLOB]: "deny",
  557. },
  558. },
  559. },
  560. },
  561. )
  562. it.instance(
  563. "skill directories are allowed for external_directory",
  564. () =>
  565. Effect.gen(function* () {
  566. const test = yield* TestInstance
  567. const skillDir = path.join(test.directory, ".opencode", "skill", "perm-skill")
  568. yield* Effect.promise(() =>
  569. Bun.write(
  570. path.join(skillDir, "SKILL.md"),
  571. `---
  572. name: perm-skill
  573. description: Permission skill.
  574. ---
  575. # Permission Skill
  576. `,
  577. ),
  578. )
  579. const home = process.env.OPENCODE_TEST_HOME
  580. process.env.OPENCODE_TEST_HOME = test.directory
  581. yield* Effect.addFinalizer(() =>
  582. Effect.sync(() => {
  583. process.env.OPENCODE_TEST_HOME = home
  584. }),
  585. )
  586. const build = yield* load((svc) => svc.get("build"))
  587. const target = path.join(skillDir, "reference", "notes.md")
  588. expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
  589. }),
  590. { git: true },
  591. )
  592. it.instance("defaultAgent returns build when no default_agent config", () =>
  593. Effect.gen(function* () {
  594. const agent = yield* load((svc) => svc.defaultAgent())
  595. expect(agent).toBe("build")
  596. }),
  597. )
  598. it.instance("defaultInfo returns resolved build agent when no default_agent config", () =>
  599. Effect.gen(function* () {
  600. const agent = yield* load((svc) => svc.defaultInfo())
  601. expect(agent.name).toBe("build")
  602. expect(agent.mode).toBe("primary")
  603. }),
  604. )
  605. it.instance(
  606. "defaultAgent respects default_agent config set to plan",
  607. () =>
  608. Effect.gen(function* () {
  609. const agent = yield* load((svc) => svc.defaultAgent())
  610. expect(agent).toBe("plan")
  611. }),
  612. {
  613. config: {
  614. default_agent: "plan",
  615. },
  616. },
  617. )
  618. it.instance(
  619. "defaultAgent respects default_agent config set to custom agent with mode all",
  620. () =>
  621. Effect.gen(function* () {
  622. const agent = yield* load((svc) => svc.defaultAgent())
  623. expect(agent).toBe("my_custom")
  624. }),
  625. {
  626. config: {
  627. default_agent: "my_custom",
  628. agent: {
  629. my_custom: {
  630. description: "My custom agent",
  631. },
  632. },
  633. },
  634. },
  635. )
  636. it.instance(
  637. "defaultAgent throws when default_agent points to subagent",
  638. () => expectDefaultAgentError('default agent "explore" is a subagent'),
  639. {
  640. config: {
  641. default_agent: "explore",
  642. },
  643. },
  644. )
  645. it.instance(
  646. "defaultAgent throws when default_agent points to hidden agent",
  647. () => expectDefaultAgentError('default agent "compaction" is hidden'),
  648. {
  649. config: {
  650. default_agent: "compaction",
  651. },
  652. },
  653. )
  654. it.instance(
  655. "defaultAgent throws when default_agent points to non-existent agent",
  656. () => expectDefaultAgentError('default agent "does_not_exist" not found'),
  657. {
  658. config: {
  659. default_agent: "does_not_exist",
  660. },
  661. },
  662. )
  663. it.instance(
  664. "defaultAgent returns plan when build is disabled and default_agent not set",
  665. () =>
  666. Effect.gen(function* () {
  667. const agent = yield* load((svc) => svc.defaultAgent())
  668. // build is disabled, so it should return plan (next primary agent)
  669. expect(agent).toBe("plan")
  670. }),
  671. {
  672. config: {
  673. agent: {
  674. build: { disable: true },
  675. },
  676. },
  677. },
  678. )
  679. it.instance(
  680. "defaultAgent throws when all primary agents are disabled",
  681. () => expectDefaultAgentError("no primary visible agent found"),
  682. {
  683. config: {
  684. agent: {
  685. build: { disable: true },
  686. plan: { disable: true },
  687. },
  688. },
  689. },
  690. )