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,7 +1,7 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "sec-default", |
| 3 | 3 | "version": "0.1.0", |
| 4 | - "description": "Security default for organizations: seated outermost, it keeps the organization's classic hooks, prompt content, settings and tool policy out of reach of the plugins a person installs, and adds no policy of its own.", | |
| 4 | + "description": "Security default for organizations: seated outermost, it keeps the organization's classic hooks, prompt content, settings and tool policy out of reach of the plugins a person installs, and adds no policy of its own; one managed option, allowManagedModsOnly, limits mods to the organization's.", | |
| 5 | 5 | "author": { |
| 6 | 6 | "name": "Anthropic" |
| 7 | 7 | } |
| @@ -10,11 +10,12 @@ | ||
| 10 | 10 | of its own. Everything else passes through untouched. |
| 11 | 11 | |
| 12 | 12 | It has three moves and nothing else: continue past the user tier |
| 13 | -(`next.to(e, "append")`), refuse a user-tier caller by name (`{ deny }` | |
| 14 | -when `next.origin.tier` is `user`), or pass (`next(e)`). A subject's | |
| 15 | -provenance is the event's pinned `e.provider`; policy is read through | |
| 16 | -`$.settings.read({ source: "policy" })`, one read serving a burst; both | |
| 17 | -fail closed, so an unreadable policy counts as a policy in force. | |
| 13 | +(`next.to(e, "append")`), refuse a user-tier caller or module by name | |
| 14 | +(`{ deny }` when `next.origin.tier` is `user`, `{ refuse }` when a module's | |
| 15 | +pinned `e.tier` is), or pass (`next(e)`). A subject's provenance is the | |
| 16 | +event's pinned `e.provider`; policy is read through | |
| 17 | +`$.settings.read({ source: "policy" })`, one read serving a burst of tool | |
| 18 | +calls; both fail closed, so an unreadable policy counts as a policy in force. | |
| 18 | 19 | |
| 19 | 20 | `hooks/register.ts` is the module; `hooks/policy/` reads the managed |
| 20 | 21 | settings it decides by. |
| @@ -29,18 +30,69 @@ | ||
| 29 | 30 | | `tool.describe`, `command.describe`, `agent.offer`, `agent.spawn` | When the subject's pinned `e.provider.tier` is `prepend` or `append` (a policy-installed plugin, the managed folder, a policy MCP server), continue past the user tier; a subject provided by `user`, `builtin` or `core` passes. | |
| 30 | 31 | | `tool.register` | A caller in `prepend` or `append` continues past the user tier. A `user`-tier caller is refused by name while managed settings hold `allowedMcpServers` (set at all, empty included); otherwise it passes. | |
| 31 | 32 | | `tool.list` | The tools of the organization's managed MCP servers are listed as the organization's tiers listed them; every other tool as the user tier left it. With no policy to read, or a refusal from either listing, the organization's listing stands whole. | |
| 33 | +| `plugin.register` | A hooks module in the `user` tier (one a person installed, named with `--plugin-dir`, or keeps in their mods folder) is refused while managed settings set this plugin's `allowManagedModsOnly` option; otherwise it passes. Modules in `prepend`, `append` and `builtin` are never asked about. | | |
| 32 | 34 | | everything else | Passes: `prompt.submit`, `turn.*`, `tool.call`, `tool.check`, `command.run`, `command.register`, `session.*`, `ui.*`, `fs.*`, `http.fetch`, `process.run`, `store.*`, `clock.*`, `model.*`, `mcp.call`, `audio.*`, `agent.list`, `engine.create`. | |
| 33 | 35 | |
| 36 | +## Options an administrator sets | |
| 37 | + | |
| 38 | +One, in managed settings, under this plugin's own `pluginConfigs` entry, | |
| 39 | +keyed by the id the CLI builds the plugin in under (only this spelling of the | |
| 40 | +id is read): | |
| 41 | + | |
| 42 | +```json | |
| 43 | +{ | |
| 44 | + "pluginConfigs": { | |
| 45 | + "cc-plugin-sec-default@builtin": { | |
| 46 | + "options": { "allowManagedModsOnly": true } | |
| 47 | + } | |
| 48 | + } | |
| 49 | +} | |
| 50 | +``` | |
| 51 | + | |
| 52 | +`allowManagedModsOnly`: only the mods the organization deploys through | |
| 53 | +managed settings, and the ones built into Claude Code, load. A hooks module | |
| 54 | +a person installed, named with `--plugin-dir` or keeps in their mods folder | |
| 55 | +is refused whenever it loads or reloads (one already running when the option | |
| 56 | +is set keeps running until then), with one line that names it: `mods are | |
| 57 | +limited to your organization's by policy (allowManagedModsOnly); <plugin> | |
| 58 | +was not loaded` | |
| 59 | +(in the debug log, and on screen where the session hot-reloads the mod's | |
| 60 | +folder). A plain `-p` run has it in the debug log alone; the mod is still | |
| 61 | +not loaded. Settings hooks, status lines and `/goal` are not touched by it. | |
| 62 | + | |
| 63 | +- The decision reads the tier the CLI pins on the module and nothing the | |
| 64 | + module says of itself: a person's copy carrying an organization mod's name | |
| 65 | + is still in the `user` tier and is refused. | |
| 66 | +- Refused means nothing of the module joins: no hook, no tool, no command. | |
| 67 | + Its top-level code has run once by then, in the closed context every hooks | |
| 68 | + module is evaluated in, with no call on `$` served. | |
| 69 | +- Only managed settings are read (`$.settings.read({ source: "policy" })`): | |
| 70 | + the same entry in a person's, a project's or a `--settings` file neither | |
| 71 | + turns it on nor off. On unless absent or `false`, so a mistyped `"true"` or | |
| 72 | + `1` still locks. A value the settings schema rejects (`null`, an object), | |
| 73 | + here or in any other `pluginConfigs` entry, makes the CLI ignore the whole | |
| 74 | + `pluginConfigs` key with a settings warning, and the option reads as unset. | |
| 75 | +- It fails closed: when the read of managed settings is refused (a hook beneath | |
| 76 | + denies it) or this plugin's hook fails, its `.catch` refuses the module, | |
| 77 | + with the same line, and names the failure in the debug log; so a policy | |
| 78 | + that cannot be read keeps every person's mod out at load. Where this plugin | |
| 79 | + is not seated there is no such rule, and mods load as they do without it. | |
| 80 | +- Where managed settings define `prependPlugins`, that list must name this | |
| 81 | + plugin (next section) for the option to apply. | |
| 82 | +- `claude plugin test` is not covered: it runs a mod's tests in an engine of | |
| 83 | + their own and loads nothing into a session. | |
| 84 | + | |
| 34 | 85 | ## What it hooks |
| 35 | 86 | |
| 36 | 87 | `classic.*`, `prompt.section`, `prompt.context`, `skill.prompt`, |
| 37 | 88 | `attribution.text`, `settings.read`, `tool.describe`, `command.describe`, |
| 38 | -`agent.offer`, `agent.spawn`, `tool.register`, `tool.list`. | |
| 89 | +`agent.offer`, `agent.spawn`, `tool.register`, `tool.list`, | |
| 90 | +`plugin.register`. | |
| 39 | 91 | |
| 40 | 92 | ## What it calls on `$` |
| 41 | 93 | |
| 42 | -`settings.read`. It continues to the `append` tier with `next.to`, which | |
| 43 | -only a plugin in a managed tier may do. | |
| 94 | +`settings.read`, and `ui.log` to the debug log. It continues to the `append` | |
| 95 | +tier with `next.to`, which only a plugin in a managed tier may do. | |
| 44 | 96 | |
| 45 | 97 | ## Where it is seated |
| 46 | 98 |
| @@ -1,0 +1,13 @@ | ||
| 1 | +import type { HookFailure } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * The debug line for a `plugin.register` hook of this plugin that failed: | |
| 5 | + * the module it was judging, how it failed, and what the failure said. | |
| 6 | + * | |
| 7 | + * @param name the judged plugin's name, as its own manifest gives it | |
| 8 | + * @param error why the hook failed, as its `.catch` reads it | |
| 9 | + * @returns the line | |
| 10 | + */ | |
| 11 | +export const admissionFailure = (name: string, error: HookFailure) => | |
| 12 | + `plugin.register hook failed judging ${name} (${error.kind}): ` + | |
| 13 | + (error.message ?? 'no message') |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './admission-failure.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | { |
| 2 | - "description": "Security default: from the outermost seat, continues past the user tier on the organization's classic hooks, prompt content, settings and subjects, refuses a user-tier tool.register under an MCP allowlist, and restores the organization's tools in tool.list", | |
| 2 | + "description": "Security default: from the outermost seat, continues past the user tier on the organization's classic hooks, prompt content, settings and subjects, refuses a user-tier tool.register under an MCP allowlist, restores the organization's tools in tool.list, and refuses a user-tier hooks module at plugin.register while managed settings set its allowManagedModsOnly option", | |
| 3 | 3 | "modules": ["./register.ts"] |
| 4 | 4 | } |
| @@ -1,3 +1,5 @@ | ||
| 1 | +export * from './admission-failure' | |
| 2 | +export * from './managed-mods-only-refusal' | |
| 1 | 3 | export * from './past-users' |
| 2 | 4 | export * from './policy' |
| 3 | 5 | export * from './register.js' |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './managed-mods-only-refusal.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,10 @@ | ||
| 1 | +/** | |
| 2 | + * What a person reads when managed policy keeps a mod of theirs out: the | |
| 3 | + * rule, the option that set it, and the mod that was not loaded. | |
| 4 | + * | |
| 5 | + * @param name the refused plugin's name, as its own manifest gives it | |
| 6 | + * @returns the refusal line | |
| 7 | + */ | |
| 8 | +export const managedModsOnlyRefusal = (name: string) => | |
| 9 | + "mods are limited to your organization's by policy " + | |
| 10 | + `(allowManagedModsOnly); ${name} was not loaded` |
| @@ -1,7 +1,9 @@ | ||
| 1 | 1 | export * from './create-policy-memo' |
| 2 | 2 | export * from './decided-by-policy.js' |
| 3 | 3 | export * from './has-mcp-allowlist.js' |
| 4 | +export * from './is-managed-mods-only.js' | |
| 4 | 5 | export * from './managed-tools-restored' |
| 6 | +export * from './own-option' | |
| 5 | 7 | export * from './policy-memo-ms.js' |
| 6 | 8 | export * from './source.js' |
| 7 | 9 |
| @@ -1,0 +1,19 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +import { ownOption } from './own-option' | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * Whether managed policy limits mods to the organization's: this plugin's | |
| 7 | + * own option `allowManagedModsOnly` in managed settings. | |
| 8 | + * | |
| 9 | + * On unless absent or `false`: a value mistyped (`"true"`, `1`) still | |
| 10 | + * locks, as the CLI's own managed locks read. | |
| 11 | + * | |
| 12 | + * @param policy the managed settings, as `$.settings.read` answers them | |
| 13 | + * @returns true when the person's own mods are not to load | |
| 14 | + */ | |
| 15 | +export function isManagedModsOnly(policy: Settings) { | |
| 16 | + const value = ownOption(policy, 'allowManagedModsOnly') | |
| 17 | + | |
| 18 | + return value !== undefined && value !== false | |
| 19 | +} |
| @@ -1,0 +1,5 @@ | ||
| 1 | +export * from './member-of' | |
| 2 | +export * from './own-option.js' | |
| 3 | +export * from './own-options-path' | |
| 4 | + | |
| 5 | +export * as default from '.' |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './member-of.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,13 @@ | ||
| 1 | +/** | |
| 2 | + * One step into settings read as plain data: the value's own member of that | |
| 3 | + * name, or undefined when the value is no object or holds none. | |
| 4 | + * | |
| 5 | + * @param value what was read so far | |
| 6 | + * @param key the member's name | |
| 7 | + * @returns the member, or undefined | |
| 8 | + */ | |
| 9 | +export function memberOf(value: unknown, key: string): unknown { | |
| 10 | + const isObject = typeof value === 'object' && value !== null | |
| 11 | + | |
| 12 | + return isObject ? new Map(Object.entries(value)).get(key) : undefined | |
| 13 | +} |
| @@ -1,0 +1,17 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +import { memberOf } from './member-of' | |
| 4 | +import { OWN_OPTIONS_PATH } from './own-options-path' | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * One option an administrator set for this plugin in managed settings: its | |
| 8 | + * value under the plugin's own `pluginConfigs` entry, as written. | |
| 9 | + * | |
| 10 | + * Hand it the policy source alone, so a person's settings never reach it. | |
| 11 | + * | |
| 12 | + * @param policy the managed settings, as `$.settings.read` answers them | |
| 13 | + * @param name the option's name | |
| 14 | + * @returns the value, or undefined when the option is not set | |
| 15 | + */ | |
| 16 | +export const ownOption = (policy: Settings, name: string): unknown => | |
| 17 | + [...OWN_OPTIONS_PATH, name].reduce<unknown>(memberOf, policy) |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './own-options-path.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,9 @@ | ||
| 1 | +/** | |
| 2 | + * Where managed settings hold this plugin's options: its own `pluginConfigs` | |
| 3 | + * entry, keyed by the id the CLI builds it in under. | |
| 4 | + */ | |
| 5 | +export const OWN_OPTIONS_PATH = [ | |
| 6 | + 'pluginConfigs', | |
| 7 | + 'cc-plugin-sec-default@builtin', | |
| 8 | + 'options', | |
| 9 | +] as const |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | import type { On } from 'claude-code' |
| 2 | 2 | |
| 3 | +import { admissionFailure } from './admission-failure' | |
| 4 | +import { managedModsOnlyRefusal } from './managed-mods-only-refusal' | |
| 3 | 5 | import { pastUsers } from './past-users' |
| 4 | 6 | import Policy from './policy' |
| 5 | 7 | import { TOOL_REGISTER_REFUSAL } from './tool-register-refusal' |
| @@ -9,8 +11,8 @@ | ||
| 9 | 11 | * organization has today out of reach of the plugins a person installs. |
| 10 | 12 | * |
| 11 | 13 | * Three moves: continue past the user tier (`next.to(e, "append")`), refuse |
| 12 | - * a user-tier caller by name, or pass. Provenance is the event's pinned | |
| 13 | - * `provider`; policy is `$.settings.read`, memoized per burst; fail closed. | |
| 14 | + * a user-tier caller or module by name, or pass. Provenance is the event's | |
| 15 | + * pinned `provider` or `tier`; policy is `$.settings.read`; fail closed. | |
| 14 | 16 | * |
| 15 | 17 | * @param on the engine's registrar |
| 16 | 18 | */ |
| @@ -58,4 +60,14 @@ | ||
| 58 | 60 | await next(e), |
| 59 | 61 | ), |
| 60 | 62 | ) |
| 63 | + | |
| 64 | + on('plugin.register', { tier: 'user' }, async ($, e, next) => | |
| 65 | + Policy.isManagedModsOnly(await $.settings.read(Policy.SOURCE)) | |
| 66 | + ? { refuse: managedModsOnlyRefusal(e.name) } | |
| 67 | + : next(e), | |
| 68 | + ).catch(($, e, next) => { | |
| 69 | + $.ui.log(admissionFailure(e.name, next.error), { to: 'debug' }) | |
| 70 | + | |
| 71 | + return next.called ? next(e) : { refuse: managedModsOnlyRefusal(e.name) } | |
| 72 | + }) | |
| 61 | 73 | } |
| @@ -6,14 +6,19 @@ | ||
| 6 | 6 | export * from './dropping.js' |
| 7 | 7 | export * from './fullscreen.js' |
| 8 | 8 | export * from './listing.js' |
| 9 | +export * from './logged.js' | |
| 10 | +export * from './managed-mods-only.js' | |
| 9 | 11 | export * from './managed-policy.js' |
| 10 | 12 | export * from './marking.js' |
| 11 | 13 | export * from './memory.js' |
| 14 | +export * from './mods-policy-of.js' | |
| 12 | 15 | export * from './no-allowlist.js' |
| 13 | 16 | export * from './odd-providers.js' |
| 14 | 17 | export * from './org-providers.js' |
| 18 | +export * from './policy-by-source.js' | |
| 15 | 19 | export * from './policy-command.js' |
| 16 | 20 | export * from './policy-reads.js' |
| 21 | +export * from './policy-until-stopped.js' | |
| 17 | 22 | export * from './reading.js' |
| 18 | 23 | export * from './registered-tool-of.js' |
| 19 | 24 | export * from './registering.js' |
| @@ -1,0 +1,20 @@ | ||
| 1 | +import type { On } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Answers every `$.ui.log` beneath the plugins and keeps each line as | |
| 5 | + * `<to>: <text>`, in order. | |
| 6 | + * | |
| 7 | + * @param on the test's `on` | |
| 8 | + * @returns the lines logged so far | |
| 9 | + */ | |
| 10 | +export function logged(on: On) { | |
| 11 | + const lines: string[] = [] | |
| 12 | + | |
| 13 | + on('ui.log', ($, e) => { | |
| 14 | + lines.push(`${e.to}: ${e.text}`) | |
| 15 | + | |
| 16 | + return { value: undefined } | |
| 17 | + }) | |
| 18 | + | |
| 19 | + return lines | |
| 20 | +} |
| @@ -1,0 +1,8 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +import { modsPolicyOf } from './mods-policy-of.js' | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * Managed settings that limit mods to the organization's. | |
| 7 | + */ | |
| 8 | +export const MANAGED_MODS_ONLY: Settings = modsPolicyOf(true) |
| @@ -1,0 +1,16 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Managed settings holding `allowManagedModsOnly` at the value given, where | |
| 5 | + * an administrator writes it: the built-in's own `pluginConfigs` entry. | |
| 6 | + * | |
| 7 | + * @param value what the option is set to | |
| 8 | + * @returns the managed settings | |
| 9 | + */ | |
| 10 | +export const modsPolicyOf = (value: boolean | number | string): Settings => ({ | |
| 11 | + pluginConfigs: { | |
| 12 | + 'cc-plugin-sec-default@builtin': { | |
| 13 | + options: { allowManagedModsOnly: value }, | |
| 14 | + }, | |
| 15 | + }, | |
| 16 | +}) |
| @@ -1,0 +1,15 @@ | ||
| 1 | +import type { On, Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Answers a read of the policy source with one settings object and every | |
| 5 | + * other read, a named source or the merge, with another. | |
| 6 | + * | |
| 7 | + * @param on the test's `on` | |
| 8 | + * @param policy what `{ source: "policy" }` reads | |
| 9 | + * @param others what any other read answers | |
| 10 | + * @returns the registration | |
| 11 | + */ | |
| 12 | +export const policyBySource = (on: On, policy: Settings, others: Settings) => | |
| 13 | + on('settings.read', ($, e) => ({ | |
| 14 | + value: e.source === 'policy' ? policy : others, | |
| 15 | + })) |
| @@ -1,0 +1,23 @@ | ||
| 1 | +import type { On, Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Answers every settings read beneath the plugins with the policy given | |
| 5 | + * until the answer is called; every read after it is refused. | |
| 6 | + * | |
| 7 | + * So a test's plugins load under a policy that reads, and meet one that | |
| 8 | + * does not. | |
| 9 | + * | |
| 10 | + * @param on the test's `on` | |
| 11 | + * @param policy the managed settings in force while reads are answered | |
| 12 | + * @param reason what a read is denied with once stopped | |
| 13 | + * @returns `() => void`: from then on a read is denied | |
| 14 | + */ | |
| 15 | +export function policyUntilStopped(on: On, policy: Settings, reason: string) { | |
| 16 | + let isStopped = false | |
| 17 | + | |
| 18 | + on('settings.read', () => (isStopped ? { deny: reason } : { value: policy })) | |
| 19 | + | |
| 20 | + return () => { | |
| 21 | + isStopped = true | |
| 22 | + } | |
| 23 | +} |
| @@ -1,0 +1,45 @@ | ||
| 1 | +import { describe, expect, test, tier } from 'claude-code/testing' | |
| 2 | + | |
| 3 | +import Policy from '../../hooks/policy' | |
| 4 | +import Fixtures from '../fixtures' | |
| 5 | + | |
| 6 | +tier('prepend') | |
| 7 | + | |
| 8 | +describe('is-managed-mods-only', () => { | |
| 9 | + test('true locks; absent or false leaves mods as they are', () => { | |
| 10 | + expect( | |
| 11 | + [ | |
| 12 | + Fixtures.modsPolicyOf(true), | |
| 13 | + Fixtures.modsPolicyOf(false), | |
| 14 | + Fixtures.NO_ALLOWLIST, | |
| 15 | + {}, | |
| 16 | + ].map(Policy.isManagedModsOnly), | |
| 17 | + ).toEqual([true, false, false, false]) | |
| 18 | + }) | |
| 19 | + | |
| 20 | + test('a value mistyped still locks, as a managed lock reads', () => { | |
| 21 | + expect( | |
| 22 | + [ | |
| 23 | + Fixtures.modsPolicyOf('true'), | |
| 24 | + Fixtures.modsPolicyOf('false'), | |
| 25 | + Fixtures.modsPolicyOf(1), | |
| 26 | + Fixtures.modsPolicyOf(0), | |
| 27 | + ].map(Policy.isManagedModsOnly), | |
| 28 | + ).toEqual([true, true, true, true]) | |
| 29 | + }) | |
| 30 | + | |
| 31 | + test('only its own pluginConfigs entry is read', () => { | |
| 32 | + expect( | |
| 33 | + [ | |
| 34 | + { allowManagedModsOnly: true }, | |
| 35 | + { pluginConfigs: { allowManagedModsOnly: true } }, | |
| 36 | + { | |
| 37 | + pluginConfigs: { | |
| 38 | + 'guard@corp-market': { options: { allowManagedModsOnly: true } }, | |
| 39 | + }, | |
| 40 | + }, | |
| 41 | + { pluginConfigs: 'cc-plugin-sec-default@builtin' }, | |
| 42 | + ].map(Policy.isManagedModsOnly), | |
| 43 | + ).toEqual([false, false, false, false]) | |
| 44 | + }) | |
| 45 | +}) |
| @@ -67,10 +67,18 @@ | ||
| 67 | 67 | 'a policy that cannot be read counts as one in force', |
| 68 | 68 | { plugins: [Fixtures.registering('mine')] }, |
| 69 | 69 | async ($, on) => { |
| 70 | - on('settings.read', () => ({ deny: 'managed settings unreadable' })) | |
| 70 | + const stopPolicy = Fixtures.policyUntilStopped( | |
| 71 | + on, | |
| 72 | + Fixtures.NO_ALLOWLIST, | |
| 73 | + 'managed settings unreadable', | |
| 74 | + ) | |
| 71 | 75 | |
| 72 | 76 | const registered = Fixtures.toolsRegistered(on) |
| 73 | 77 | |
| 78 | + on('prompt.section', ($, e) => ({ text: e.text })) | |
| 79 | + | |
| 80 | + await $.prompt.section(Fixtures.MEMORY) | |
| 81 | + stopPolicy() | |
| 74 | 82 | await $.session.start(Fixtures.SESSION) |
| 75 | 83 | |
| 76 | 84 | expect(registered).toEqual([]) |
| @@ -113,9 +121,18 @@ | ||
| 113 | 121 | "with no policy to read the organization's listing stands whole", |
| 114 | 122 | { plugins: [Fixtures.relabeling, Fixtures.listing] }, |
| 115 | 123 | async ($, on) => { |
| 116 | - on('settings.read', () => ({ deny: 'settings unreadable' })) | |
| 124 | + const stopPolicy = Fixtures.policyUntilStopped( | |
| 125 | + on, | |
| 126 | + Fixtures.ALLOWLIST, | |
| 127 | + 'settings unreadable', | |
| 128 | + ) | |
| 129 | + | |
| 130 | + on('prompt.section', ($, e) => ({ text: e.text })) | |
| 117 | 131 | on('tool.list', () => ({ value: [...Fixtures.TOOLS] })) |
| 118 | 132 | |
| 133 | + await $.prompt.section(Fixtures.MEMORY) | |
| 134 | + stopPolicy() | |
| 135 | + | |
| 119 | 136 | const { text } = await $.command.run(Fixtures.TOOLS_COMMAND) |
| 120 | 137 | |
| 121 | 138 | expect(text?.split('\n')).toEqual([ |
| @@ -129,6 +146,7 @@ | ||
| 129 | 146 | 'a prompt section passes over the plugins the person installed', |
| 130 | 147 | { plugins: [Fixtures.dropping, Fixtures.signing] }, |
| 131 | 148 | async ($, on) => { |
| 149 | + on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) | |
| 132 | 150 | on('prompt.section', ($, e) => ({ text: e.text })) |
| 133 | 151 | |
| 134 | 152 | expect(await $.prompt.section(Fixtures.MEMORY)).toEqual({ |
| @@ -170,6 +188,7 @@ | ||
| 170 | 188 | "an organization provider's subject passes over the user plugins", |
| 171 | 189 | { plugins: [Fixtures.marking] }, |
| 172 | 190 | async ($, on) => { |
| 191 | + on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) | |
| 173 | 192 | Fixtures.subjectsEchoed(on) |
| 174 | 193 | |
| 175 | 194 | for (const provider of Fixtures.ORG_PROVIDERS) { |
| @@ -228,6 +247,7 @@ | ||
| 228 | 247 | 'an odd provider passes over the user plugins: it fails closed', |
| 229 | 248 | { plugins: [Fixtures.marking] }, |
| 230 | 249 | async ($, on) => { |
| 250 | + on('settings.read', () => ({ value: Fixtures.NO_ALLOWLIST })) | |
| 231 | 251 | Fixtures.subjectsEchoed(on) |
| 232 | 252 | |
| 233 | 253 | for (const provider of Fixtures.ODD_PROVIDERS) { |
| @@ -256,7 +276,7 @@ | ||
| 256 | 276 | await $.agent.spawn(Fixtures.agentSpawned(provider)) |
| 257 | 277 | } |
| 258 | 278 | |
| 259 | - expect(reads()).toBe(0) | |
| 279 | + expect(reads(), "one read: the person's plugin was admitted").toBe(1) | |
| 260 | 280 | |
| 261 | 281 | await Promise.all([ |
| 262 | 282 | $.command.run(Fixtures.TOOLS_COMMAND), |
| @@ -264,7 +284,7 @@ | ||
| 264 | 284 | $.command.run(Fixtures.TOOLS_COMMAND), |
| 265 | 285 | ]) |
| 266 | 286 | |
| 267 | - expect(reads()).toBe(1) | |
| 287 | + expect(reads()).toBe(2) | |
| 268 | 288 | }, |
| 269 | 289 | ) |
| 270 | 290 | |
| @@ -306,4 +326,133 @@ | ||
| 306 | 326 | ) |
| 307 | 327 | }, |
| 308 | 328 | ) |
| 329 | + | |
| 330 | + test( | |
| 331 | + "under allowManagedModsOnly a person's mod is kept out, told why", | |
| 332 | + { plugins: [Fixtures.registering('mine')] }, | |
| 333 | + async ($, on) => { | |
| 334 | + on('settings.read', () => ({ value: Fixtures.MANAGED_MODS_ONLY })) | |
| 335 | + | |
| 336 | + const registered = Fixtures.toolsRegistered(on) | |
| 337 | + | |
| 338 | + await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( | |
| 339 | + Hooks.managedModsOnlyRefusal('mine'), | |
| 340 | + ) | |
| 341 | + | |
| 342 | + expect(registered, 'no hook of it ever ran').toEqual([]) | |
| 343 | + }, | |
| 344 | + ) | |
| 345 | + | |
| 346 | + test( | |
| 347 | + "under it the organization's mods and the built-ins load", | |
| 348 | + { | |
| 349 | + plugins: [ | |
| 350 | + Fixtures.registering('suite', 'prepend'), | |
| 351 | + Fixtures.registering('after', 'append'), | |
| 352 | + Fixtures.registering('bundled', 'builtin'), | |
| 353 | + ], | |
| 354 | + }, | |
| 355 | + async ($, on) => { | |
| 356 | + on('settings.read', () => ({ value: Fixtures.MANAGED_MODS_ONLY })) | |
| 357 | + | |
| 358 | + const registered = Fixtures.toolsRegistered(on) | |
| 359 | + | |
| 360 | + await $.session.start(Fixtures.SESSION) | |
| 361 | + | |
| 362 | + expect(registered).toEqual(['suite', 'after', 'bundled']) | |
| 363 | + }, | |
| 364 | + ) | |
| 365 | + | |
| 366 | + test( | |
| 367 | + 'set to false, a mod the person installed loads as before', | |
| 368 | + { plugins: [Fixtures.registering('mine')] }, | |
| 369 | + async ($, on) => { | |
| 370 | + on('settings.read', () => ({ value: Fixtures.modsPolicyOf(false) })) | |
| 371 | + | |
| 372 | + const registered = Fixtures.toolsRegistered(on) | |
| 373 | + | |
| 374 | + await $.session.start(Fixtures.SESSION) | |
| 375 | + | |
| 376 | + expect(registered).toEqual(['mine']) | |
| 377 | + }, | |
| 378 | + ) | |
| 379 | + | |
| 380 | + test( | |
| 381 | + "a person's copy under an organization mod's name is still theirs", | |
| 382 | + { plugins: [Fixtures.registering('suite')] }, | |
| 383 | + async ($, on) => { | |
| 384 | + on('settings.read', () => ({ | |
| 385 | + value: { ...Fixtures.MANAGED_POLICY, ...Fixtures.MANAGED_MODS_ONLY }, | |
| 386 | + })) | |
| 387 | + | |
| 388 | + const registered = Fixtures.toolsRegistered(on) | |
| 389 | + | |
| 390 | + await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( | |
| 391 | + Hooks.managedModsOnlyRefusal('suite'), | |
| 392 | + ) | |
| 393 | + | |
| 394 | + expect(registered).toEqual([]) | |
| 395 | + }, | |
| 396 | + ) | |
| 397 | + | |
| 398 | + test( | |
| 399 | + "only managed settings are asked: a person's own cannot loosen it", | |
| 400 | + { plugins: [Fixtures.registering('mine')] }, | |
| 401 | + async ($, on) => { | |
| 402 | + Fixtures.policyBySource( | |
| 403 | + on, | |
| 404 | + Fixtures.MANAGED_MODS_ONLY, | |
| 405 | + Fixtures.modsPolicyOf(false), | |
| 406 | + ) | |
| 407 | + | |
| 408 | + const registered = Fixtures.toolsRegistered(on) | |
| 409 | + | |
| 410 | + await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( | |
| 411 | + Hooks.managedModsOnlyRefusal('mine'), | |
| 412 | + ) | |
| 413 | + | |
| 414 | + expect(registered).toEqual([]) | |
| 415 | + }, | |
| 416 | + ) | |
| 417 | + | |
| 418 | + test( | |
| 419 | + "nor turn it on: set only in a person's settings, it is not read", | |
| 420 | + { plugins: [Fixtures.registering('mine')] }, | |
| 421 | + async ($, on) => { | |
| 422 | + Fixtures.policyBySource( | |
| 423 | + on, | |
| 424 | + Fixtures.NO_ALLOWLIST, | |
| 425 | + Fixtures.MANAGED_MODS_ONLY, | |
| 426 | + ) | |
| 427 | + | |
| 428 | + const registered = Fixtures.toolsRegistered(on) | |
| 429 | + | |
| 430 | + await $.session.start(Fixtures.SESSION) | |
| 431 | + | |
| 432 | + expect(registered).toEqual(['mine']) | |
| 433 | + }, | |
| 434 | + ) | |
| 435 | + | |
| 436 | + test( | |
| 437 | + "a policy that cannot be read keeps a person's mod out: the hook's catch", | |
| 438 | + { plugins: [Fixtures.registering('mine')] }, | |
| 439 | + async ($, on) => { | |
| 440 | + on('settings.read', () => ({ deny: 'managed settings unreadable' })) | |
| 441 | + | |
| 442 | + const lines = Fixtures.logged(on) | |
| 443 | + const registered = Fixtures.toolsRegistered(on) | |
| 444 | + | |
| 445 | + await expect($.session.start(Fixtures.SESSION)).rejects.toThrow( | |
| 446 | + Hooks.managedModsOnlyRefusal('mine'), | |
| 447 | + ) | |
| 448 | + | |
| 449 | + expect({ registered, lines }).toEqual({ | |
| 450 | + registered: [], | |
| 451 | + lines: [ | |
| 452 | + 'debug: plugin.register hook failed judging mine (throw): ' + | |
| 453 | + 'sec-default: $.settings.read: managed settings unreadable', | |
| 454 | + ], | |
| 455 | + }) | |
| 456 | + }, | |
| 457 | + ) | |
| 309 | 458 | }) |