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: a batch listed under prepend may hold a person's plugin, and the line says liftBrowse files
poteat authored 24 days ago parent 9c5e283 · commit 96eed3f76228
9 changed files+87 -26compared with 9c5e283
Mmods/sec-default/README.md
Mmods/sec-default/hooks/held-verdict/held-notice.ts
Mmods/sec-default/hooks/held-verdict/verdicts/loosened-by-users.ts
Mmods/sec-default/hooks/held-verdict/verdicts/ranking/index.ts
Amods/sec-default/hooks/held-verdict/verdicts/ranking/tiers-holding-users/index.ts
Amods/sec-default/hooks/held-verdict/verdicts/ranking/tiers-holding-users/tiers-holding-users.ts
Mmods/sec-default/tests/fixtures/tool-check/listening.ts
Mmods/sec-default/tests/held-verdict.test.ts
Mmods/sec-default/tests/register.test.ts
@@ -58,11 +58,15 @@
5858 seated it does not:
5959
6060 - The hook first runs the chain as it is. If the answer is a deny, or no
61- user-tier link answered more permissively than the verdict handed up to it
62- (read off `next.trace`, whose tiers the engine pins; a link that never
63- called `next` is measured against a deny), the answer passes: nothing of
64- the person's loosened anything, and a plugin that only listens costs
65- nothing more.
61+ link that may hold a person's plugin answered more permissively than the
62+ verdict handed up to it, the answer passes: nothing of the person's
63+ loosened anything, and a plugin that only listens costs nothing more. This
64+ is read off `next.trace`, whose tiers the engine pins: a link that never
65+ called `next` is measured against a deny, and since the engine lists
66+ neighbouring plugins that share a worker as one batch under its first
67+ member's tier, a `prepend` entry beneath this plugin counts as well as a
68+ `user` one. Whether a person's plugin did the loosening is never settled
69+ here, only by the next step.
6670 - Otherwise it runs the dispatch once more with the user tier left out
6771 (`next.to(e, "append")`). That verdict never passed through a person's
6872 plugin, so neither the decision nor the rule it names can have been
@@ -81,10 +85,12 @@
8185 can have the rules evaluated on one command and another run; a rewrite
8286 belongs to `tool.call`, which runs before any of this.
8387 - The person is told once for each plugin in a session, in the transcript
84- and the debug log: `<plugin> tried to allow a <tool> call your settings
85- deny (<rule>); the deny rule holds over the plugins you install
86- (allowModsToOverrideDenyRules)`. A plain `-p` run has it in the debug log
87- alone; the call is still denied with the rule's own message.
88+ and the debug log: `<plugin> tried to lift a deny rule in your settings
89+ from a <tool> call (<rule>); the deny rule holds over the plugins you
90+ install (allowModsToOverrideDenyRules)`. Plugins the engine ran as one
91+ batch are named together, as it names them (`audit+easy`). A plain `-p`
92+ run has it in the debug log alone; the call is still denied with the
93+ rule's own message.
8894 - If the hook itself fails, its `.catch` answers from the one run it can
8995 read: a deny stands; a verdict no plugin of the person's loosened stands;
9096 one they loosened, or a run that rejected, is refused, since the deny
@@ -1,15 +1,15 @@
11 /**
22 * What a person reads, once for each plugin in a session, when a plugin they
3- * installed allowed a call that a deny rule in their settings refuses.
3+ * installed answered allow or ask over a deny rule in their settings.
44 *
55 * It names the option an administrator sets to let such plugins override.
66 *
7- * @param plugin the plugin's name, as the chain's trace names its link
7+ * @param plugin the plugin's name, or its batch's, as the trace names it
88 * @param tool the tool the call named
99 * @param rule the deny rule that decided, as written
1010 * @returns the line
1111 */
1212 export const heldNotice = (plugin: string, tool: string, rule: string) =>
13- `${plugin} tried to allow a ${tool} call your settings deny (${rule}); ` +
14- 'the deny rule holds over the plugins you install ' +
13+ `${plugin} tried to lift a deny rule in your settings from a ${tool} ` +
14+ `call (${rule}); the deny rule holds over the plugins you install ` +
1515 '(allowModsToOverrideDenyRules)'
@@ -3,11 +3,12 @@
33 import Ranking from './ranking'
44
55 /**
6- * The plugins a person installed that loosened the verdict in one run of
7- * `tool.check`: each answered more permissively than it was handed.
6+ * The links of one run of `tool.check` that may hold a plugin a person
7+ * installed and that answered more permissively than they were handed.
88 *
9- * Read off `next.trace`, whose `tier` the engine pins. A link that passed a
10- * verdict on, tightened it, or was skipped is not named.
9+ * Read off `next.trace`, whose `tier` the engine pins. A batch is named as
10+ * the engine names it, its members joined; whether a person's plugin did
11+ * the loosening is settled by the run past the user tier, never here.
1112 *
1213 * @param trace what settled beneath the hook on its latest `next` call
1314 * @returns their names, nearest the caller first; none when none loosened
@@ -18,7 +19,7 @@
1819 trace
1920 .filter(
2021 (link, at) =>
21- link.tier === 'user' &&
22+ Ranking.TIERS_HOLDING_USERS.includes(link.tier) &&
2223 Ranking.isLooser(link.returned, Ranking.handedTo(trace, at)),
2324 )
2425 .map(link => link.plugin)
@@ -1,5 +1,6 @@
11 export * from './handed-to.js'
22 export * from './is-looser.js'
33 export * from './leniency'
4+export * from './tiers-holding-users'
45
56 export * as default from '.'
@@ -1,0 +1,3 @@
1+export * from './tiers-holding-users.js'
2+
3+export * as default from '.'
@@ -1,0 +1,12 @@
1+/**
2+ * The tiers a trace entry can carry when a plugin a person installed ran in
3+ * it: `user`, and `prepend` beneath this plugin's own seat.
4+ *
5+ * The engine runs neighbouring plugins that share a worker as one batch and
6+ * lists the batch under its first member's tier, so an organization's
7+ * prepended plugin can stand for a batch that holds a person's.
8+ */
9+export const TIERS_HOLDING_USERS: readonly unknown[] = Object.freeze([
10+ 'user',
11+ 'prepend',
12+])
@@ -1,12 +1,17 @@
11 import type { Plugin } from 'claude-code/testing'
22
33 /**
4- * A plugin the person installed that hears every `tool.check` and hands up
5- * the verdict beneath it as it is, as one that only logs would.
4+ * A plugin that hears every `tool.check` and hands up the verdict beneath it
5+ * as it is, as one that only logs would: the person's own by default.
6+ *
7+ * @param name the plugin's name
8+ * @param tier the tier it loads in
9+ * @returns the plugin
610 */
7-export const listening: Plugin = {
8- name: 'listening',
11+export const listening = (name: string, tier?: Plugin['tier']): Plugin => ({
12+ name,
13+ tier,
914 register(on) {
1015 on('tool.check', ($, e, next) => next(e))
1116 },
12-}
17+})
@@ -33,18 +33,27 @@
3333 ).toEqual(['blind'])
3434 })
3535
36- test('a tightening, a skipped link and an organization link are not', () => {
36+ test('a tightening, a skipped link and links beneath theirs are not', () => {
3737 expect(
3838 Hooks.loosenedByUsers([
39- Fixtures.linkOf('guard', 'prepend', Fixtures.ALLOWED),
4039 Fixtures.linkOf('strict', 'user', Fixtures.PLAIN_DENY),
4140 Fixtures.linkOf('passed-over', 'user'),
41+ Fixtures.linkOf('suite', 'append', Fixtures.ALLOWED),
4242 Fixtures.linkOf('bundled', 'builtin', Fixtures.ALLOWED),
4343 Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY),
4444 ]),
4545 ).toEqual([])
4646 })
4747
48+ test('a batch listed under prepend may hold a plugin of theirs', () => {
49+ expect(
50+ Hooks.loosenedByUsers([
51+ Fixtures.linkOf('audit+easy', 'prepend', Fixtures.ALLOWED),
52+ Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY),
53+ ]),
54+ ).toEqual(['audit+easy'])
55+ })
56+
4857 test('the handler keeps a deny, and a verdict none of theirs made', () => {
4958 const pastUsers = [
5059 Fixtures.linkOf('easy', 'user'),
@@ -456,8 +456,32 @@
456456 )
457457
458458 test(
459+ "a deny rule holds when an organization's plugin listens above theirs",
460+ {
461+ plugins: [
462+ Fixtures.listening('audit', 'prepend'),
463+ Fixtures.allowing('easy'),
464+ ],
465+ },
466+ async ($, on) => {
467+ on('settings.read', () => ({ value: Fixtures.MANAGED_POLICY }))
468+
469+ const lines = Fixtures.logged(on)
470+
471+ Fixtures.checksAnswered(on, Fixtures.RULE_DENY)
472+
473+ expect(await $.tool.check(Fixtures.CHECKED)).toEqual(Fixtures.RULE_DENY)
474+
475+ expect(
476+ lines.map(line => line.includes('easy')),
477+ 'one line, naming the plugin of theirs, alone or in its batch',
478+ ).toEqual([true])
479+ },
480+ )
481+
482+ test(
459483 'a plugin of the person that only listens costs no second evaluation',
460- { plugins: [Fixtures.listening] },
484+ { plugins: [Fixtures.listening('listening')] },
461485 async ($, on) => {
462486 const reads = Fixtures.policyReads(on, Fixtures.MANAGED_POLICY)
463487 const evaluations = Fixtures.checksAnswered(on, Fixtures.ASKED)