surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| 1 | import { describe, expect, test, tier } from 'claude-code/testing' |
| 2 | |
| 3 | import Hooks from '../hooks' |
| 4 | import Fixtures from './fixtures' |
| 5 | |
| 6 | tier('prepend') |
| 7 | |
| 8 | describe('register', () => { |
| 9 | test( |
| 10 | 'under an MCP allowlist a plugin the person installed may not add a tool', |
| 11 | { |
| 12 | plugins: [ |
| 13 | Fixtures.registering('suite', 'prepend'), |
| 14 | Fixtures.registering('mine'), |
| 15 | Fixtures.registering('bundled', 'builtin'), |
| 16 | ], |
| 17 | }, |
| 18 | async ($, on) => { |
| 19 | on('settings.read', () => ({ value: Fixtures.ALLOWLIST })) |
| 20 | |
| 21 | const registered = Fixtures.toolsRegistered(on) |
| 22 | |
| 23 | await $.session.start(Fixtures.SESSION) |
| 24 | |
| 25 | expect(registered).toEqual(['suite', 'bundled']) |
| 26 | }, |
| 27 | ) |
| 28 | |
| 29 | test( |
| 30 | 'with no allowlist, a plugin the person installed adds its tool', |
| 31 | { plugins: [Fixtures.registering('mine')] }, |
| 32 | async ($, on) => { |
| 33 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 34 | |
| 35 | const registered = Fixtures.toolsRegistered(on) |
| 36 | |
| 37 | await $.session.start(Fixtures.SESSION) |
| 38 | |
| 39 | expect(registered).toEqual(['mine']) |
| 40 | }, |
| 41 | ) |
| 42 | |
| 43 | test( |
| 44 | "an organization's registration passes over a refusing user plugin", |
| 45 | { |
| 46 | plugins: [ |
| 47 | Fixtures.registering('suite', 'prepend'), |
| 48 | Fixtures.denying, |
| 49 | Fixtures.registering('bundled', 'builtin'), |
| 50 | ], |
| 51 | }, |
| 52 | async ($, on) => { |
| 53 | on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) |
| 54 | |
| 55 | const registered = Fixtures.toolsRegistered(on) |
| 56 | |
| 57 | await $.session.start(Fixtures.SESSION) |
| 58 | |
| 59 | expect( |
| 60 | registered, |
| 61 | "a built-in's registration still meets the user's deny", |
| 62 | ).toEqual(['suite']) |
| 63 | }, |
| 64 | ) |
| 65 | |
| 66 | test( |
| 67 | 'a policy that cannot be read counts as one in force', |
| 68 | { plugins: [Fixtures.registering('mine')] }, |
| 69 | async ($, on) => { |
| 70 | const stopPolicy = Fixtures.policyUntilStopped( |
| 71 | on, |
| 72 | Fixtures.NO_ALLOWLIST, |
| 73 | 'managed settings unreadable', |
| 74 | ) |
| 75 | |
| 76 | const registered = Fixtures.toolsRegistered(on) |
| 77 | |
| 78 | on('prompt.section', ($, e) => ({ text: e.text })) |
| 79 | |
| 80 | await $.prompt.section(Fixtures.MEMORY) |
| 81 | stopPolicy() |
| 82 | await $.session.start(Fixtures.SESSION) |
| 83 | |
| 84 | expect(registered).toEqual([]) |
| 85 | }, |
| 86 | ) |
| 87 | |
| 88 | test( |
| 89 | 'the organization tools are listed as its tiers listed them', |
| 90 | { plugins: [Fixtures.relabeling, Fixtures.listing] }, |
| 91 | async ($, on) => { |
| 92 | on('settings.read', () => ({ value: Fixtures.ALLOWLIST })) |
| 93 | on('tool.list', () => ({ value: [...Fixtures.TOOLS] })) |
| 94 | |
| 95 | const { text } = await $.command.run(Fixtures.TOOLS_COMMAND) |
| 96 | |
| 97 | expect(text?.split('\n')).toEqual([ |
| 98 | 'mcp__corp__search: Searches the corp wiki.', |
| 99 | 'Bash: relabeled', |
| 100 | ]) |
| 101 | }, |
| 102 | ) |
| 103 | |
| 104 | test( |
| 105 | 'a server policy delivers itself counts as a tool policy, unlisted', |
| 106 | { plugins: [Fixtures.relabeling, Fixtures.listing] }, |
| 107 | async ($, on) => { |
| 108 | on('settings.read', () => ({ value: Fixtures.SERVER_POLICY })) |
| 109 | on('tool.list', () => ({ value: [...Fixtures.TOOLS] })) |
| 110 | |
| 111 | const { text } = await $.command.run(Fixtures.TOOLS_COMMAND) |
| 112 | |
| 113 | expect(text?.split('\n')).toEqual([ |
| 114 | 'mcp__corp__search: Searches the corp wiki.', |
| 115 | 'Bash: relabeled', |
| 116 | ]) |
| 117 | }, |
| 118 | ) |
| 119 | |
| 120 | test( |
| 121 | "with no policy to read the organization's listing stands whole", |
| 122 | { plugins: [Fixtures.relabeling, Fixtures.listing] }, |
| 123 | async ($, on) => { |
| 124 | const stopPolicy = Fixtures.policyUntilStopped( |
| 125 | on, |
| 126 | Fixtures.ALLOWLIST, |
| 127 | 'settings unreadable', |
| 128 | ) |
| 129 | |
| 130 | on('prompt.section', ($, e) => ({ text: e.text })) |
| 131 | on('tool.list', () => ({ value: [...Fixtures.TOOLS] })) |
| 132 | |
| 133 | await $.prompt.section(Fixtures.MEMORY) |
| 134 | stopPolicy() |
| 135 | |
| 136 | const { text } = await $.command.run(Fixtures.TOOLS_COMMAND) |
| 137 | |
| 138 | expect(text?.split('\n')).toEqual([ |
| 139 | 'mcp__corp__search: Searches the corp wiki.', |
| 140 | 'Bash: Runs a command.', |
| 141 | ]) |
| 142 | }, |
| 143 | ) |
| 144 | |
| 145 | test( |
| 146 | 'a prompt section passes over the plugins the person installed', |
| 147 | { plugins: [Fixtures.dropping, Fixtures.signing] }, |
| 148 | async ($, on) => { |
| 149 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 150 | on('prompt.section', ($, e) => ({ text: e.text })) |
| 151 | |
| 152 | expect(await $.prompt.section(Fixtures.MEMORY)).toEqual({ |
| 153 | text: 'the org says hi (signed)', |
| 154 | }) |
| 155 | }, |
| 156 | ) |
| 157 | |
| 158 | test( |
| 159 | "the system prompt's sections pass over the plugins the person installed", |
| 160 | { plugins: [Fixtures.emptying, Fixtures.heading] }, |
| 161 | async ($, on) => { |
| 162 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 163 | |
| 164 | on('prompt.compose', () => ({ |
| 165 | sections: [{ id: 'body', text: 'the body', scope: 'shared' }], |
| 166 | })) |
| 167 | |
| 168 | expect(await $.prompt.compose(Fixtures.COMPOSED)).toEqual({ |
| 169 | sections: [ |
| 170 | { id: 'heading:org', text: 'the org says hi', scope: 'shared' }, |
| 171 | { id: 'body', text: 'the body', scope: 'shared' }, |
| 172 | ], |
| 173 | }) |
| 174 | }, |
| 175 | ) |
| 176 | |
| 177 | test( |
| 178 | "nor does a person's plugin drop or reword a section it asked for", |
| 179 | { plugins: [Fixtures.rewording, Fixtures.heading] }, |
| 180 | async ($, on) => { |
| 181 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 182 | |
| 183 | on('prompt.compose', () => ({ |
| 184 | sections: [{ id: 'body', text: 'the body', scope: 'shared' }], |
| 185 | })) |
| 186 | |
| 187 | expect(await $.prompt.compose(Fixtures.COMPOSED)).toEqual({ |
| 188 | sections: [ |
| 189 | { id: 'heading:org', text: 'the org says hi', scope: 'shared' }, |
| 190 | { id: 'body', text: 'the body', scope: 'shared' }, |
| 191 | ], |
| 192 | }) |
| 193 | }, |
| 194 | ) |
| 195 | |
| 196 | test( |
| 197 | "a user plugin's rewrite of policy is skipped for every other reader", |
| 198 | { |
| 199 | plugins: [ |
| 200 | Fixtures.stripping, |
| 201 | Fixtures.reading, |
| 202 | Fixtures.registering('mine'), |
| 203 | ], |
| 204 | }, |
| 205 | async ($, on) => { |
| 206 | on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY })) |
| 207 | |
| 208 | const registered = Fixtures.toolsRegistered(on) |
| 209 | const { text } = await $.command.run(Fixtures.POLICY_COMMAND) |
| 210 | |
| 211 | await $.session.start(Fixtures.SESSION) |
| 212 | |
| 213 | expect( |
| 214 | JSON.parse(text ?? 'null'), |
| 215 | "another user plugin's read sees the allowlist the stripper hides", |
| 216 | ).toEqual(Fixtures.MANAGED_POLICY) |
| 217 | |
| 218 | expect( |
| 219 | registered, |
| 220 | "sec-default's own tool.register hook still reads the allowlist", |
| 221 | ).toEqual([]) |
| 222 | }, |
| 223 | ) |
| 224 | |
| 225 | test( |
| 226 | "an organization provider's subject passes over the user plugins", |
| 227 | { plugins: [Fixtures.marking] }, |
| 228 | async ($, on) => { |
| 229 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 230 | Fixtures.subjectsEchoed(on) |
| 231 | |
| 232 | for (const provider of Fixtures.ORG_PROVIDERS) { |
| 233 | expect( |
| 234 | await $.tool.describe( |
| 235 | Fixtures.toolDescribed('mcp__corp__search', provider), |
| 236 | ), |
| 237 | ).toEqual({ description: 'd' }) |
| 238 | |
| 239 | expect( |
| 240 | ( |
| 241 | await $.command.describe( |
| 242 | Fixtures.commandDescribed('suite:deploy', provider), |
| 243 | ) |
| 244 | ).isHidden, |
| 245 | ).toBe(false) |
| 246 | |
| 247 | expect( |
| 248 | await $.agent.offer( |
| 249 | Fixtures.agentOffered('suite:reviewer', provider), |
| 250 | ), |
| 251 | ).toEqual({ isOffered: true }) |
| 252 | |
| 253 | expect(await $.agent.spawn(Fixtures.agentSpawned(provider))).toEqual({ |
| 254 | model: 'core', |
| 255 | }) |
| 256 | } |
| 257 | |
| 258 | for (const provider of Fixtures.USER_REACHABLE_PROVIDERS) { |
| 259 | expect( |
| 260 | await $.tool.describe( |
| 261 | Fixtures.toolDescribed('mcp__mine__search', provider), |
| 262 | ), |
| 263 | ).toEqual({ description: 'user: d' }) |
| 264 | |
| 265 | expect( |
| 266 | ( |
| 267 | await $.command.describe( |
| 268 | Fixtures.commandDescribed('mine:deploy', provider), |
| 269 | ) |
| 270 | ).isHidden, |
| 271 | ).toBe(true) |
| 272 | |
| 273 | expect( |
| 274 | await $.agent.offer(Fixtures.agentOffered('reviewer', provider)), |
| 275 | ).toEqual({ isOffered: false }) |
| 276 | |
| 277 | expect(await $.agent.spawn(Fixtures.agentSpawned(provider))).toEqual({ |
| 278 | model: 'user', |
| 279 | }) |
| 280 | } |
| 281 | }, |
| 282 | ) |
| 283 | |
| 284 | test( |
| 285 | 'an odd provider passes over the user plugins: it fails closed', |
| 286 | { plugins: [Fixtures.marking] }, |
| 287 | async ($, on) => { |
| 288 | on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) |
| 289 | Fixtures.subjectsEchoed(on) |
| 290 | |
| 291 | for (const provider of Fixtures.ODD_PROVIDERS) { |
| 292 | expect( |
| 293 | await $.tool.describe(Fixtures.toolDescribed('Bash', provider)), |
| 294 | ).toEqual({ description: 'd' }) |
| 295 | |
| 296 | expect(await $.agent.spawn(Fixtures.agentSpawned(provider))).toEqual({ |
| 297 | model: 'core', |
| 298 | }) |
| 299 | } |
| 300 | }, |
| 301 | ) |
| 302 | |
| 303 | test( |
| 304 | 'a subject decision reads no policy; a burst of listings reads once', |
| 305 | { plugins: [Fixtures.listing] }, |
| 306 | async ($, on) => { |
| 307 | const reads = Fixtures.policyReads(on, Fixtures.MANAGED_POLICY) |
| 308 | |
| 309 | Fixtures.subjectsEchoed(on) |
| 310 | on('tool.list', () => ({ value: [...Fixtures.TOOLS] })) |
| 311 | |
| 312 | for (const provider of Fixtures.ORG_PROVIDERS) { |
| 313 | await $.tool.describe(Fixtures.toolDescribed('mcp__corp__x', provider)) |
| 314 | await $.agent.spawn(Fixtures.agentSpawned(provider)) |
| 315 | } |
| 316 | |
| 317 | expect(reads(), "one read: the person's plugin was admitted").toBe(1) |
| 318 | |
| 319 | await Promise.all([ |
| 320 | $.command.run(Fixtures.TOOLS_COMMAND), |
| 321 | $.command.run(Fixtures.TOOLS_COMMAND), |
| 322 | $.command.run(Fixtures.TOOLS_COMMAND), |
| 323 | ]) |
| 324 | |
| 325 | expect(reads()).toBe(2) |
| 326 | }, |
| 327 | ) |
| 328 | |
| 329 | test( |
| 330 | 'the refusal a user-tier caller reads names the allowlist', |
| 331 | { |
| 332 | plugins: [ |
| 333 | { |
| 334 | name: 'asking', |
| 335 | register(on) { |
| 336 | on('command.run', { command: 'greet' }, $ => |
| 337 | $.tool |
| 338 | .register({ |
| 339 | name: 'greet', |
| 340 | description: 'Says hello.', |
| 341 | inputSchema: { type: 'object' }, |
| 342 | }) |
| 343 | .then( |
| 344 | () => ({ text: 'registered' }), |
| 345 | (error: unknown) => ({ text: String(error) }), |
| 346 | ), |
| 347 | ) |
| 348 | }, |
| 349 | }, |
| 350 | ], |
| 351 | }, |
| 352 | async ($, on) => { |
| 353 | on('settings.read', () => ({ value: Fixtures.ALLOWLIST })) |
| 354 | |
| 355 | const { text } = await $.command.run({ |
| 356 | command: 'greet', |
| 357 | args: '', |
| 358 | origin: { kind: 'composer' }, |
| 359 | presentation: Fixtures.FULLSCREEN, |
| 360 | }) |
| 361 | |
| 362 | expect(text).toEndWith( |
| 363 | `asking: $.tool.register: ${Hooks.TOOL_REGISTER_REFUSAL}`, |
| 364 | ) |
| 365 | }, |
| 366 | ) |
| 367 | |
| 368 | test( |
| 369 | "under allowManagedModsOnly a person's mod is kept out, told why", |
| 370 | { plugins: [Fixtures.registering('mine')] }, |
| 371 | async ($, on) => { |
| 372 | on('settings.read', () => ({ value: Fixtures.MANAGED_MODS_ONLY })) |
| 373 | |
| 374 | const registered = Fixtures.toolsRegistered(on) |
| 375 | |
| 376 | await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( |
| 377 | Hooks.managedModsOnlyRefusal('mine'), |
| 378 | ) |
| 379 | |
| 380 | expect(registered, 'no hook of it ever ran').toEqual([]) |
| 381 | }, |
| 382 | ) |
| 383 | |
| 384 | test( |
| 385 | "under it the organization's mods and the built-ins load", |
| 386 | { |
| 387 | plugins: [ |
| 388 | Fixtures.registering('suite', 'prepend'), |
| 389 | Fixtures.registering('after', 'append'), |
| 390 | Fixtures.registering('bundled', 'builtin'), |
| 391 | ], |
| 392 | }, |
| 393 | async ($, on) => { |
| 394 | on('settings.read', () => ({ value: Fixtures.MANAGED_MODS_ONLY })) |
| 395 | |
| 396 | const registered = Fixtures.toolsRegistered(on) |
| 397 | |
| 398 | await $.session.start(Fixtures.SESSION) |
| 399 | |
| 400 | expect(registered).toEqual(['suite', 'after', 'bundled']) |
| 401 | }, |
| 402 | ) |
| 403 | |
| 404 | test( |
| 405 | 'set to false, a mod the person installed loads as before', |
| 406 | { plugins: [Fixtures.registering('mine')] }, |
| 407 | async ($, on) => { |
| 408 | on('settings.read', () => ({ value: Fixtures.modsPolicyOf(false) })) |
| 409 | |
| 410 | const registered = Fixtures.toolsRegistered(on) |
| 411 | |
| 412 | await $.session.start(Fixtures.SESSION) |
| 413 | |
| 414 | expect(registered).toEqual(['mine']) |
| 415 | }, |
| 416 | ) |
| 417 | |
| 418 | test( |
| 419 | "a person's copy under an organization mod's name is still theirs", |
| 420 | { plugins: [Fixtures.registering('suite')] }, |
| 421 | async ($, on) => { |
| 422 | on('settings.read', () => ({ |
| 423 | value: { ...Fixtures.MANAGED_POLICY, ...Fixtures.MANAGED_MODS_ONLY }, |
| 424 | })) |
| 425 | |
| 426 | const registered = Fixtures.toolsRegistered(on) |
| 427 | |
| 428 | await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( |
| 429 | Hooks.managedModsOnlyRefusal('suite'), |
| 430 | ) |
| 431 | |
| 432 | expect(registered).toEqual([]) |
| 433 | }, |
| 434 | ) |
| 435 | |
| 436 | test( |
| 437 | "only managed settings are asked: a person's own cannot loosen it", |
| 438 | { plugins: [Fixtures.registering('mine')] }, |
| 439 | async ($, on) => { |
| 440 | Fixtures.policyBySource( |
| 441 | on, |
| 442 | Fixtures.MANAGED_MODS_ONLY, |
| 443 | Fixtures.modsPolicyOf(false), |
| 444 | ) |
| 445 | |
| 446 | const registered = Fixtures.toolsRegistered(on) |
| 447 | |
| 448 | await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( |
| 449 | Hooks.managedModsOnlyRefusal('mine'), |
| 450 | ) |
| 451 | |
| 452 | expect(registered).toEqual([]) |
| 453 | }, |
| 454 | ) |
| 455 | |
| 456 | test( |
| 457 | "nor turn it on: set only in a person's settings, it is not read", |
| 458 | { plugins: [Fixtures.registering('mine')] }, |
| 459 | async ($, on) => { |
| 460 | Fixtures.policyBySource( |
| 461 | on, |
| 462 | Fixtures.NO_ALLOWLIST, |
| 463 | Fixtures.MANAGED_MODS_ONLY, |
| 464 | ) |
| 465 | |
| 466 | const registered = Fixtures.toolsRegistered(on) |
| 467 | |
| 468 | await $.session.start(Fixtures.SESSION) |
| 469 | |
| 470 | expect(registered).toEqual(['mine']) |
| 471 | }, |
| 472 | ) |
| 473 | |
| 474 | test( |
| 475 | "a policy that cannot be read keeps a person's mod out: the hook's catch", |
| 476 | { plugins: [Fixtures.registering('mine')] }, |
| 477 | async ($, on) => { |
| 478 | on('settings.read', () => ({ deny: 'managed settings unreadable' })) |
| 479 | |
| 480 | const lines = Fixtures.logged(on) |
| 481 | const registered = Fixtures.toolsRegistered(on) |
| 482 | |
| 483 | await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( |
| 484 | Hooks.managedModsOnlyRefusal('mine'), |
| 485 | ) |
| 486 | |
| 487 | expect({ registered, lines }).toEqual({ |
| 488 | registered: [], |
| 489 | lines: [ |
| 490 | 'debug: plugin.register hook failed judging mine (throw): ' + |
| 491 | 'sec-default: $.settings.read: managed settings unreadable', |
| 492 | ], |
| 493 | }) |
| 494 | }, |
| 495 | ) |
| 496 | }) |