spacehub

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

sec-default: what needs a newer engine waits for itBrowse files
The hold of a classic hook's ask is read off a field the published engine does not carry yet, so it leaves this change and follows by itself. What stays needs no new engine: any deny, an ask a settings rule decided, the organization's own log lines, and the variables managed env sets.
Alice Poteat authored 21 days ago parent 1169a7f · commit 8e7b0cc691cf
20 changed files+165 -168compared with 1169a7f
Mmods/sec-default/README.md
Mmods/sec-default/hooks/held-verdict/caught-answer.ts
Mmods/sec-default/hooks/held-verdict/held-kind.ts
Mmods/sec-default/hooks/held-verdict/held-notice.ts
Mmods/sec-default/hooks/held-verdict/verdicts/holds-over.ts
Mmods/sec-default/hooks/held-verdict/verdicts/unchecked-deny.ts
Mmods/sec-default/hooks/hooks.json
Mmods/sec-default/hooks/policy/deny-rules-hold.ts
Amods/sec-default/tests/fixtures/tool-check/allowing-bash.ts
Amods/sec-default/tests/fixtures/tool-check/allowing-read.ts
Amods/sec-default/tests/fixtures/tool-check/checked-read.ts
Dmods/sec-default/tests/fixtures/tool-check/hook-ask.ts
Dmods/sec-default/tests/fixtures/tool-check/hook-deny.ts
Mmods/sec-default/tests/fixtures/tool-check/index.ts
Mmods/sec-default/tests/fixtures/tool-check/plain-deny.ts
Mmods/sec-default/tests/held-verdict.test.ts
Amods/sec-default/tests/pinned-variable-refusal.test.ts
Mmods/sec-default/tests/policy/is-pinned-variable.test.ts
Mmods/sec-default/tests/register.test.ts
Mmods/types/claude-code.d.ts
@@ -4,9 +4,10 @@
44 say on every event, in chain order, and the plugins a person installs sit
55 in the user tier, beneath the organization's prepend tier and above its
66 append tier. Some of what an organization sets today (its classic hooks,
7-its managed CLAUDE.md and rules, its settings, its MCP allowlist, the deny
8-rules in force on its machines) was never within a person's reach before
9-function hooks; seated outermost, this plugin keeps exactly those out of the
7+its managed CLAUDE.md and rules, its settings and the environment they set,
8+its MCP allowlist, the permission rules and refusals in force on its machines)
9+was never within a person's reach before function hooks; seated outermost,
10+this plugin keeps exactly those, and the lines its own plugins log, out of the
1011 user tier's reach and adds no policy of its own. Everything else passes
1112 through untouched.
1213
@@ -32,7 +33,7 @@
3233 | `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. |
3334 | `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. |
3435 | `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. |
35-| `tool.check` | A deny, and an ask a settings rule or a classic hook decided, hold over the user tier: when a person's plugin loosened the verdict it was handed, the dispatch is run again past the user tier, and if that verdict is the stricter and is a deny, or names the rule or the hook behind it, it is the answer. See [Deny rules hold](#deny-rules-hold). Every other verdict passes as the chain left it. |
36+| `tool.check` | A deny, and an ask a settings rule decided, hold over the user tier: when a person's plugin loosened the verdict it was handed, the dispatch is run again past the user tier, and if that verdict is the stricter and is a deny, or names the rule behind it, it is the answer. See [Deny rules hold](#deny-rules-hold). Every other verdict passes as the chain left it. |
3637 | `ui.log` | A line a plugin in `prepend` or `append` logs, this one's included, continues past the user tier: no user hook rewrites or drops it, and none runs inside the hook that logged it. Every other line passes. |
3738 | `env.set` | A variable managed settings set in `env` (its name in any case) is pinned: setting or unsetting it, a `user`-tier caller is refused by name and any other caller continues past the user tier, so no user hook rewrites the value. A variable the organization does not set passes. With no policy to read, every variable counts as pinned. `allowModsToOverrideDenyRules` does not unpin. |
3839 | `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. |
@@ -89,9 +90,9 @@
8990
9091 `allowModsToOverrideDenyRules`: the plugins a person installs may answer
9192 over a settings deny rule on `tool.check`, as they could before this plugin
92-held deny rules, and over every other deny, an ask rule or a classic hook's
93-ask with it: the one option covers all that holds there. Off unless it is the
94-literal `true`; an option that reads as unset leaves them holding. See
93+held deny rules, and over every other deny and an ask rule with it: the one
94+option covers all that holds there. Off unless it is the literal `true`; an
95+option that reads as unset leaves them holding. See
9596 [Deny rules hold](#deny-rules-hold).
9697
9798 ## What it hooks
@@ -118,8 +119,8 @@
118119
119120 On `tool.check` any hook may answer any verdict, so a plugin a person
120121 installs to stop the permission prompts (`() => ({ decision: "allow" })`)
121-would also lift a deny rule, an ask rule or a `PreToolUse` hook's ask, a
122-managed one included. Where this plugin is seated it does not:
122+would also lift a deny rule or an ask rule, a managed one included. Where
123+this plugin is seated it does not:
123124
124125 - The hook first runs the chain as it is. If the answer is a deny, or no
125126 link that may hold a person's plugin answered more permissively than the
@@ -138,18 +139,18 @@
138139 changes nothing: the rules are evaluated in this run. The two runs differ
139140 by the user tier alone, so a verdict here that is stricter than the chain's
140141 answer is one the user tier loosened. A deny is returned in place of the
141- chain's answer whatever decided it; an ask, when it names what decided it:
142- a settings rule (`rule`) or a classic hook (`hook`).
143-- Any rule and any classic hook counts, whoever configured it: a verdict
144- carries the rule as written and the hook's event, never where either was
145- read from. A deny holds unnamed because it can stand in front of a rule's or
146- a hook's ask: lifted, the call would run with nobody asked. An ask that names
147- neither (the mode's own, a check of the engine's own) is not held.
142+ chain's answer whatever decided it; an ask, when it names the settings rule
143+ that decided it (`rule`).
144+- Any rule counts, whoever configured it: a verdict carries the rule as
145+ written, never where it was read from. A deny holds unnamed because it can
146+ stand in front of a rule's ask: lifted, the call would run with nobody
147+ asked. An ask that names no rule (the mode's own, a settings hook's, a check
148+ of the engine's own, an organization's plugin's) is not held.
148149 - An organization's plugin (prepend or append) or a built-in that allows
149150 over a deny rule takes part in both runs, so its answer stands (a prepended
150151 one that loosens is what brings the second run about, so its hooks run
151152 twice on such a call). The mode's own ask
152- that a person's plugin turns into an allow, with no rule or hook behind it,
153+ that a person's plugin turns into an allow, with no rule behind it,
153154 stands: that is what such a plugin is for.
154155 - `tool.check` pins the question (`tool`, `input`, `tool_use_id`), so no hook
155156 can have the rules evaluated on one command and another run; a rewrite
@@ -158,9 +159,8 @@
158159 transcript and the debug log: `<plugin> tried to lift a deny rule in your
159160 settings from a <tool> call (<rule>); the deny rule holds over the plugins
160161 you install (allowModsToOverrideDenyRules)`. The kinds are `a deny rule`,
161- `an ask rule`, `a <event> hook's ask` and `a refusal` (a deny that names no
162- rule); the last two carry neither `in your settings` nor a rule. Plugins the
163- engine ran as one
162+ `an ask rule` and `a refusal` (a deny that names no rule); the last carries
163+ neither `in your settings` nor a rule. Plugins the engine ran as one
164164 batch are named together, as it names them (`audit+easy`). A plain `-p`
165165 run has it in the debug log alone; the call is still denied, or asked
166166 about, with its own message. The line goes past the user tier (the `ui.log`
@@ -179,7 +179,7 @@
179179 - If the hook itself fails, its `.catch` answers from the one run it can
180180 read: a deny stands; a verdict no plugin of the person's loosened stands;
181181 one they loosened, or a run that rejected, is refused, since the rules
182- and hooks were never consulted.
182+ were never consulted.
183183
184184 An organization that wants the plugins its people install to override deny
185185 rules, and all else that holds with them, says so in managed settings, under
@@ -7,7 +7,7 @@
77 * can read, the failed hook's last: that run's verdict, or a refusal.
88 *
99 * A deny stands, and so does a verdict no link that may hold a person's
10- * plugin loosened. A loosened one, or none at all, met no rule or hook.
10+ * plugin loosened. A loosened one, or none at all, met no rule.
1111 *
1212 * @param last what that run settled on; undefined when it rejected
1313 * @param trace that run's `next.trace`
@@ -2,18 +2,10 @@
22
33 /**
44 * What kind of thing held over a person's plugin, as the notice calls it:
5- * `deny rule`, `ask rule`, `<event> hook's ask`, or `refusal`.
5+ * `deny rule`, `ask rule`, or `refusal`, a deny that names no rule.
66 *
7- * A deny that names no rule is a refusal even when it names a hook: the hook
8- * may only have asked, and something else refused.
9- *
107 * @param held the verdict that holds
118 * @returns the kind
129 */
13-export function heldKind(held: EventResult<'tool.check'>) {
14- if (held.rule !== undefined) {
15- return `${held.decision} rule`
16- }
17-
18- return held.decision === 'deny' ? 'refusal' : `${held.hook} hook's ask`
19-}
10+export const heldKind = (held: EventResult<'tool.check'>) =>
11+ held.rule === undefined ? 'refusal' : `${held.decision} rule`
@@ -19,14 +19,12 @@
1919 held: EventResult<'tool.check'>,
2020 ) {
2121 const kind = heldKind(held)
22- const article = kind.startsWith('a') ? 'an' : 'a'
23- const lifted =
24- held.rule === undefined
25- ? `${article} ${kind} from a ${tool} call`
26- : `${article} ${kind} in your settings from a ${tool} call (${held.rule})`
22+ const isRuled = held.rule !== undefined
2723
2824 return (
29- `${plugin} tried to lift ${lifted}; the ${kind} holds over the plugins ` +
30- 'you install (allowModsToOverrideDenyRules)'
25+ `${plugin} tried to lift ${kind.startsWith('a') ? 'an' : 'a'} ${kind}` +
26+ `${isRuled ? ' in your settings' : ''} from a ${tool} call` +
27+ `${isRuled ? ` (${held.rule})` : ''}; the ${kind} holds over the ` +
28+ 'plugins you install (allowModsToOverrideDenyRules)'
3129 )
3230 }
@@ -4,11 +4,11 @@
44
55 /**
66 * Whether the verdict reached past the user tier holds over the chain's
7- * answer: it is the stricter, and a deny, or an ask a rule or a hook decided.
7+ * answer: it is the stricter, and a deny, or an ask a settings rule decided.
88 *
9- * Any rule or classic hook counts, whoever configured it: a verdict names the
10- * rule and the hook's event, never their source. A deny holds unnamed: it may
11- * stand before a rule's or a hook's ask. The mode's own ask does not hold.
9+ * Any rule counts, whoever configured it: a verdict names the rule, never its
10+ * source. A deny holds unnamed: it may stand before a rule's ask. The mode's
11+ * own ask does not hold, nor a settings hook's.
1212 *
1313 * @param held what the run past the user tier settled on
1414 * @param answer what the whole chain settled on
@@ -19,6 +19,4 @@
1919 answer: EventResult<'tool.check'>,
2020 ) =>
2121 Ranking.isLooser(answer, held) &&
22- (held.decision === 'deny' ||
23- held.rule !== undefined ||
24- held.hook !== undefined)
22+ (held.decision === 'deny' || held.rule !== undefined)
@@ -2,11 +2,11 @@
22
33 /**
44 * What the failure handler answers when a verdict was loosened, or never
5- * reached, and no check of the rules and hooks vouches for it: a deny.
5+ * reached, and no check of the rules vouches for it: a deny.
66 */
77 export const UNCHECKED_DENY: EventResult<'tool.check'> = Object.freeze({
88 decision: 'deny',
99 reason:
10- 'the rules and hooks in your settings could not be checked for this ' +
11- 'call, so it is refused',
10+ 'the rules in your settings could not be checked for this call, ' +
11+ 'so it is refused',
1212 })
@@ -1,4 +1,4 @@
11 {
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, holds a deny, and an ask a settings rule or a classic hook decided, over a user-tier allow or ask on tool.check, keeps the organization's own log lines and the variables its managed env sets out of the user tier's reach, and refuses a user-tier hooks module at plugin.register while managed settings set its allowManagedModsOnly option",
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, holds a deny, and an ask a settings rule decided, over a user-tier allow or ask on tool.check, keeps the organization's own log lines and the variables its managed env sets out of the user tier's reach, and refuses a user-tier hooks module at plugin.register while managed settings set its allowManagedModsOnly option",
33 "modules": ["./register.ts"]
44 }
@@ -6,7 +6,7 @@
66 * Whether deny rules hold over the plugins a person installs: they do unless
77 * managed policy sets `allowModsToOverrideDenyRules` to the literal `true`.
88 *
9- * Ask rules and classic hooks' answers hold or not with them. A value mistyped
9+ * Every other deny, and ask rules, hold or not with them. A value mistyped
1010 * (`"true"`, `1`) loosens nothing. Only the policy source is handed in, so a
1111 * person's settings never reach this.
1212 *
@@ -1,0 +1,12 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that allows every Bash call and has no say
5+ * on another tool's.
6+ */
7+export const allowingBash: Plugin = {
8+ name: 'basher',
9+ register(on) {
10+ on('tool.check', { tool: 'Bash' }, () => ({ decision: 'allow' }))
11+ },
12+}
@@ -1,0 +1,12 @@
1+import type { Plugin } from 'claude-code/testing'
2+
3+/**
4+ * A plugin the person installed that allows every Read call and has no say
5+ * on another tool's.
6+ */
7+export const allowingRead: Plugin = {
8+ name: 'reader',
9+ register(on) {
10+ on('tool.check', { tool: 'Read' }, () => ({ decision: 'allow' }))
11+ },
12+}
@@ -1,0 +1,9 @@
1+import type { Args } from 'claude-code'
2+
3+/**
4+ * Another question the engine puts to `tool.check`: may Read read `notes`.
5+ */
6+export const CHECKED_READ: Args<'tool.check'> = {
7+ tool: 'Read',
8+ input: { file_path: 'notes' },
9+}
Dmods/sec-default/tests/fixtures/tool-check/hook-ask.ts+0 -11
@@ -1,11 +1,0 @@
1-import type { EventResult } from 'claude-code'
2-
3-/**
4- * The engine's verdict when a PreToolUse hook asked for a person: the ask,
5- * the hook's sentence, and the hook's event.
6- */
7-export const HOOK_ASK: EventResult<'tool.check'> = {
8- decision: 'ask',
9- reason: 'a person approves every echo',
10- hook: 'PreToolUse',
11-}
Dmods/sec-default/tests/fixtures/tool-check/hook-deny.ts+0 -11
@@ -1,11 +1,0 @@
1-import type { EventResult } from 'claude-code'
2-
3-/**
4- * The engine's verdict when a PreToolUse hook refused the call: the deny,
5- * the hook's sentence, and the hook's event.
6- */
7-export const HOOK_DENY: EventResult<'tool.check'> = {
8- decision: 'deny',
9- reason: 'no echo here',
10- hook: 'PreToolUse',
11-}
@@ -1,14 +1,15 @@
11 export * from './allowed.js'
22 export * from './allowing.js'
3+export * from './allowing-bash.js'
4+export * from './allowing-read.js'
35 export * from './asked.js'
46 export * from './asking.js'
57 export * from './blind-allowing.js'
68 export * from './checked.js'
9+export * from './checked-read.js'
710 export * from './checks-answered.js'
811 export * from './checks-answered-in-turn.js'
912 export * from './forging.js'
10-export * from './hook-ask.js'
11-export * from './hook-deny.js'
1213 export * from './link-of.js'
1314 export * from './listening.js'
1415 export * from './muting.js'
@@ -1,8 +1,8 @@
11 import type { EventResult } from 'claude-code'
22
33 /**
4- * A deny that neither a settings rule nor a classic hook decided (a tool's
5- * own check, a setting that is no rule, a plugin's answer): it names neither.
4+ * A deny no settings rule decided (a tool's own check, a setting that is no
5+ * rule, a plugin's answer): it names none.
66 */
77 export const PLAIN_DENY: EventResult<'tool.check'> = {
88 decision: 'deny',
@@ -6,70 +6,51 @@
66 tier('prepend')
77
88 describe('held-verdict', () => {
9- test('over an allow: any deny, and an ask a rule or a hook decided', () => {
9+ test('over an allow: any deny, and an ask a settings rule decided', () => {
1010 expect(
1111 [
1212 Fixtures.RULE_DENY,
1313 Fixtures.RULE_ASK,
14- Fixtures.HOOK_DENY,
15- Fixtures.HOOK_ASK,
1614 Fixtures.PLAIN_DENY,
1715 Fixtures.ASKED,
1816 Fixtures.ALLOWED,
1917 ].map(held => Hooks.holdsOver(held, Fixtures.ALLOWED)),
20- ).toEqual([true, true, true, true, true, false, false])
18+ ).toEqual([true, true, true, false, false])
2119 })
2220
2321 test('over an ask only a deny is stricter; over a deny nothing is', () => {
2422 expect(
25- [
26- Fixtures.RULE_DENY,
27- Fixtures.RULE_ASK,
28- Fixtures.HOOK_DENY,
29- Fixtures.HOOK_ASK,
30- ].map(held => [
23+ [Fixtures.RULE_DENY, Fixtures.RULE_ASK].map(held => [
3124 Hooks.holdsOver(held, Fixtures.ASKED),
3225 Hooks.holdsOver(held, Fixtures.PLAIN_DENY),
3326 ]),
3427 ).toEqual([
3528 [true, false],
3629 [false, false],
37- [true, false],
38- [false, false],
3930 ])
4031 })
4132
42- test('a rule or hook the answer itself names makes nothing hold', () => {
33+ test('a rule the answer itself names makes nothing hold', () => {
4334 expect(
4435 Hooks.holdsOver(Fixtures.ASKED, {
4536 ...Fixtures.ALLOWED,
4637 rule: 'Bash(echo *)',
47- hook: 'PreToolUse',
4838 }),
4939 ).toBe(false)
5040 })
5141
5242 test('the notice names the kind that held, and the rule when one did', () => {
5343 expect(
54- [
55- Fixtures.RULE_DENY,
56- Fixtures.RULE_ASK,
57- { ...Fixtures.RULE_ASK, ...Fixtures.HOOK_ASK },
58- Fixtures.HOOK_ASK,
59- Fixtures.HOOK_DENY,
60- Fixtures.PLAIN_DENY,
61- ].map(held => Hooks.heldNotice('easy', 'Bash', held)),
44+ [Fixtures.RULE_DENY, Fixtures.RULE_ASK, Fixtures.PLAIN_DENY].map(held =>
45+ Hooks.heldNotice('easy', 'Bash', held),
46+ ),
6247 ).toEqual(
6348 [
6449 'a deny rule in your settings from a Bash call (Bash(echo *)); ' +
6550 'the deny rule',
6651 'an ask rule in your settings from a Bash call (Bash(echo *)); ' +
6752 'the ask rule',
68- 'an ask rule in your settings from a Bash call (Bash(echo *)); ' +
69- 'the ask rule',
70- "a PreToolUse hook's ask from a Bash call; the PreToolUse hook's ask",
7153 'a refusal from a Bash call; the refusal',
72- 'a refusal from a Bash call; the refusal',
7354 ].map(
7455 middle =>
7556 `easy tried to lift ${middle} holds over the plugins you install ` +
@@ -78,12 +59,24 @@
7859 )
7960 })
8061
81- test('the refusal of a pinned variable names the setting and it', () => {
82- expect(Hooks.pinnedVariableRefusal('CORP_PROXY')).toBe(
83- 'env (managed): CORP_PROXY is not for plugins outside policy to change',
84- )
62+ test('what no check vouches for is refused, and says why', () => {
63+ expect(Hooks.UNCHECKED_DENY).toEqual({
64+ decision: 'deny',
65+ reason:
66+ 'the rules in your settings could not be checked for this call, ' +
67+ 'so it is refused',
68+ })
8569 })
8670
71+ test('an ask of theirs over a deny, caught, is no more vouched for', () => {
72+ expect(
73+ Hooks.caughtAnswer(Fixtures.ASKED, [
74+ Fixtures.linkOf('easy', 'user', Fixtures.ASKED),
75+ Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY),
76+ ]),
77+ ).toEqual(Hooks.UNCHECKED_DENY)
78+ })
79+
8780 test('a link of theirs is named when it answered looser than handed', () => {
8881 expect(
8982 Hooks.loosenedByUsers([
@@ -1,0 +1,13 @@
1+import { describe, expect, test, tier } from 'claude-code/testing'
2+
3+import Hooks from '../hooks'
4+
5+tier('prepend')
6+
7+describe('pinned-variable-refusal', () => {
8+ test('it names the setting and the variable, and not who set it', () => {
9+ expect(Hooks.pinnedVariableRefusal('CORP_PROXY')).toBe(
10+ 'env (managed): CORP_PROXY is not for plugins outside policy to change',
11+ )
12+ })
13+})
@@ -22,6 +22,12 @@
2222 ).toEqual([true, true, false])
2323 })
2424
25+ test('a name managed env sets to nothing is pinned all the same', () => {
26+ expect(
27+ Policy.isPinnedVariable({ env: { CORP_PROXY: '' } }, 'CORP_PROXY'),
28+ ).toBe(true)
29+ })
30+
2531 test('settings with no env pin nothing', () => {
2632 expect(
2733 [Fixtures.MANAGED_POLICY, {}, { env: {} }].map(policy =>
@@ -615,54 +615,6 @@
615615 )
616616
617617 test(
618- "a classic hook's ask holds over an allow from a plugin of the person",
619- { plugins: [Fixtures.allowing('easy')] },
620- async ($, on) => {
621- on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
622-
623- const lines = Fixtures.logged(on)
624-
625- Fixtures.checksAnswered(on, Fixtures.HOOK_ASK)
626-
627- expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.HOOK_ASK)
628-
629- expect(lines).toEqual([
630- `transcript: ${Hooks.heldNotice('easy', 'Bash', Fixtures.HOOK_ASK)}`,
631- ])
632- },
633- )
634-
635- test(
636- "a classic hook's deny holds over their allow",
637- { plugins: [Fixtures.allowing('easy')] },
638- async ($, on) => {
639- on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
640-
641- const lines = Fixtures.logged(on)
642-
643- Fixtures.checksAnswered(on, Fixtures.HOOK_DENY)
644-
645- expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.HOOK_DENY)
646-
647- expect(lines).toEqual([
648- `transcript: ${Hooks.heldNotice('easy', 'Bash', Fixtures.HOOK_DENY)}`,
649- ])
650- },
651- )
652-
653- test(
654- "a classic hook's deny holds over their ask",
655- { plugins: [Fixtures.asking('easy')] },
656- async ($, on) => {
657- on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
658- Fixtures.logged(on)
659- Fixtures.checksAnswered(on, Fixtures.HOOK_DENY)
660-
661- expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.HOOK_DENY)
662- },
663- )
664-
665- test(
666618 'an allow that never called next meets the ask rule all the same',
667619 { plugins: [Fixtures.blindAllowing] },
668620 async ($, on) => {
@@ -721,7 +673,7 @@
721673
722674 const lines = Fixtures.logged(on)
723675
724- Fixtures.checksAnswered(on, Fixtures.HOOK_ASK)
676+ Fixtures.checksAnswered(on, Fixtures.RULE_ASK)
725677
726678 expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.ALLOWED)
727679 expect(lines).toEqual([])
@@ -729,7 +681,7 @@
729681 )
730682
731683 test(
732- "the mode's own ask, no rule or hook behind it, is theirs to allow",
684+ "the mode's own ask, no rule behind it, is theirs to allow",
733685 { plugins: [Fixtures.allowing('easy')] },
734686 async ($, on) => {
735687 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
@@ -744,7 +696,7 @@
744696 )
745697
746698 test(
747- 'a deny holds whatever decided it: it may stand before a rule or a hook',
699+ "a deny holds whatever decided it: it may stand before a rule's ask",
748700 { plugins: [Fixtures.allowing('easy')] },
749701 async ($, on) => {
750702 on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
@@ -827,6 +779,43 @@
827779 )
828780
829781 test(
782+ 'each plugin is told of a kind, by the tool it answered on',
783+ { plugins: [Fixtures.allowingBash, Fixtures.allowingRead] },
784+ async ($, on) => {
785+ on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
786+
787+ const lines = Fixtures.logged(on)
788+
789+ Fixtures.checksAnswered(on, Fixtures.RULE_DENY)
790+
791+ await $.tool.check(Fixtures.CHECKED)
792+ await $.tool.check(Fixtures.CHECKED_READ)
793+
794+ expect(lines).toEqual([
795+ `transcript: ${Hooks.heldNotice('basher', 'Bash', Fixtures.RULE_DENY)}`,
796+ `transcript: ${Hooks.heldNotice('reader', 'Read', Fixtures.RULE_DENY)}`,
797+ ])
798+ },
799+ )
800+
801+ test(
802+ "an organization's appended plugin has its say in the run past theirs",
803+ {
804+ plugins: [Fixtures.allowing('suite', 'append'), Fixtures.blindAllowing],
805+ },
806+ async ($, on) => {
807+ on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
808+
809+ const lines = Fixtures.logged(on)
810+
811+ Fixtures.checksAnswered(on, Fixtures.RULE_ASK)
812+
813+ expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.ALLOWED)
814+ expect(lines).toEqual([])
815+ },
816+ )
817+
818+ test(
830819 'a plugin is told once of each kind of thing that held over it',
831820 { plugins: [Fixtures.allowing('easy')] },
832821 async ($, on) => {
@@ -837,24 +826,20 @@
837826 Fixtures.RULE_DENY,
838827 Fixtures.RULE_ASK,
839828 Fixtures.RULE_ASK,
840- Fixtures.HOOK_ASK,
841829 Fixtures.PLAIN_DENY,
842830 Fixtures.RULE_DENY,
843831 ]
844832 const turn = Fixtures.checksAnsweredInTurn(on, verdicts)
845833
846- for (const _ of verdicts) {
847- await $.tool.check(Fixtures.CHECKED)
834+ for (const verdict of verdicts) {
835+ expect(await $.tool.check(Fixtures.CHECKED)).toEqual(verdict)
848836 turn()
849837 }
850838
851839 expect(lines).toEqual(
852- [
853- Fixtures.RULE_DENY,
854- Fixtures.RULE_ASK,
855- Fixtures.HOOK_ASK,
856- Fixtures.PLAIN_DENY,
857- ].map(held => `transcript: ${Hooks.heldNotice('easy', 'Bash', held)}`),
840+ [Fixtures.RULE_DENY, Fixtures.RULE_ASK, Fixtures.PLAIN_DENY].map(
841+ held => `transcript: ${Hooks.heldNotice('easy', 'Bash', held)}`,
842+ ),
858843 )
859844 },
860845 )
Large file, diff not shown.