@goodandready/dsh-github-ops 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Notable changes to `@goodandready/dsh-github-ops`.
4
4
 
5
+ ## 0.1.2
6
+
7
+ - Removed the write-approval contour the plugin never should have had: no `tools/pre-execute`
8
+ listener, no `allowedActions`, `autoApprove` or `approvalMode`, no action map. Approvals are
9
+ the host's business (the permission policy and dsh-approval-gate); a GitHub tool plugin that
10
+ registers its own gate duplicates the contour and, in a session where prompts are disabled,
11
+ turns every agreed write into a rejection — which is exactly what happened on 20.09.2026.
12
+ The `confirm` flags and the outright refusals (deleting or transferring a repository,
13
+ deleting an organization) stay: those are this plugin's own contract.
14
+
5
15
  ## 0.1.1 — released
6
16
 
7
17
  - `approvalMode` with "auto" as the default: an agreed, non-destructive write goes through
package/README.md CHANGED
@@ -66,9 +66,6 @@ Requires a GitHub token in the DSH credential service. Store the token under a n
66
66
  | `timeoutMs` | `30000` | Per-request timeout. |
67
67
  | `maxRetries` | `2` | Retries for a failed read (never for a write). |
68
68
  | `reviewRulesJson` | *(empty)* | Review-rule overrides as JSON: `sensitivePaths`, `sensitiveSeverity`, `attentionPaths`, `migrationPaths`, `testsRequired`, `sourcePatterns`, `testPatterns`, `largeDiffLines`. |
69
- | `approvalMode` | `auto` | `auto` lets agreed non-destructive writes through without a prompt; deleting a release, tag, secret, variable, ruleset or branch protection, and cancelling a run, always ask. `ask` asks on every write; `off` leaves approvals to the host. |
70
- | `allowedActions` | *(all)* | Write actions the plugin may perform; anything else is denied. Each one still asks. |
71
- | `autoApprove` | *(empty)* | Actions an unattended run (`DSH_GITHUB_OPS_UNATTENDED=1`) may perform without asking. Destructive actions are never auto-approved. |
72
69
  | `reviewJobTimeoutMs` | `120000` | How long a background review job may run. |
73
70
 
74
71
  ## Tools
@@ -166,8 +163,10 @@ still passes through the approval gate.
166
163
  ## Safety
167
164
 
168
165
  - The token lives in the DSH credential service; settings hold only its name.
169
- - Read operations never change state; mutations are confirmed explicitly, and deleting a
170
- repository, transferring one or deleting an organization is refused outright.
166
+ - Read operations never change state. A state-changing tool needs an explicit `confirm`
167
+ flag, deleting a repository, transferring one or deleting an organization is refused
168
+ outright, and whether a prompt is shown is the host approval contour — the plugin does
169
+ not run an approval gate of its own.
171
170
  - The client never logs the token, and every failure is reported as a value
172
171
  (`ok: false` with `status`, `code`, `rateLimit`) instead of an exception.
173
172
  - Findings from `gh_review` are deterministic rules: they point at what needs attention
package/README.ru.md CHANGED
@@ -66,9 +66,6 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
66
66
  | `timeoutMs` | `30000` | Таймаут одного запроса. |
67
67
  | `maxRetries` | `2` | Повторы для неудачного чтения (запись не повторяется). |
68
68
  | `reviewRulesJson` | пусто | Переопределение правил ревью в JSON: `sensitivePaths`, `sensitiveSeverity`, `attentionPaths`, `migrationPaths`, `testsRequired`, `sourcePatterns`, `testPatterns`, `largeDiffLines`. |
69
- | `approvalMode` | `auto` | `auto` — согласованные неразрушительные записи проходят без вопроса; удаление релиза, тега, секрета, переменной, ruleset, защиты веток и отмена запуска спрашивают всегда. `ask` — спрашивать на каждую запись; `off` — решение целиком за штатным гейтом развёртывания. |
70
- | `allowedActions` | все | Разрешённые записи; всё остальное отклоняется, и каждая всё равно спрашивает подтверждение. |
71
- | `autoApprove` | пусто | Действия, которые неавтономный прогон (`DSH_GITHUB_OPS_UNATTENDED=1`) может выполнить без вопроса; разрушительные не автоутверждаются никогда. |
72
69
  | `reviewJobTimeoutMs` | `120000` | Сколько может работать фоновая задача ревью. |
73
70
 
74
71
  ## Инструменты
@@ -166,8 +163,7 @@ Allowlist — состав `package.json → files` плюс `.gitignore`, `LICE
166
163
  ## Безопасность
167
164
 
168
165
  - Токен живёт в сервисе учётных данных DSH; в настройках только его имя.
169
- - Чтение не меняет состояние; изменения подтверждаются явно; удаление репозитория, его
170
- перенос и удаление организации отклоняются сразу.
166
+ - Чтение не меняет состояние. Изменяющий инструмент требует явного флага `confirm`, удаление репозитория, его перенос и удаление организации отклоняются сразу, а показывать ли подтверждение — решает штатный контур развёртывания: своего гейта подтверждений плагин не запускает.
171
167
  - Клиент никогда не печатает токен, а любая ошибка возвращается значением
172
168
  (`ok: false` с `status`, `code`, `rateLimit`), а не исключением.
173
169
 
package/README.zh.md CHANGED
@@ -63,9 +63,6 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
63
63
  | `timeoutMs` | `30000` | 单次请求超时。 |
64
64
  | `maxRetries` | `2` | 读取失败时的重试次数(写入不重试)。 |
65
65
  | `reviewRulesJson` | 空 | 以 JSON 覆盖评审规则:`sensitivePaths`、`sensitiveSeverity`、`attentionPaths`、`migrationPaths`、`testsRequired`、`sourcePatterns`、`testPatterns`、`largeDiffLines`。 |
66
- | `approvalMode` | `auto` | `auto` 允许已同意的非破坏性写入免询问;删除 release、tag、secret、变量、ruleset、分支保护或取消运行仍会询问。`ask` 每次询问;`off` 交由宿主决定。 |
67
- | `allowedActions` | 全部 | 允许执行的写操作;其余一律拒绝,且每个都会询问确认。 |
68
- | `autoApprove` | 空 | 非交互运行(`DSH_GITHUB_OPS_UNATTENDED=1`)可免除询问的操作;破坏性操作永远不会被自动批准。 |
69
66
  | `reviewJobTimeoutMs` | `120000` | 后台评审任务的最长运行时间。 |
70
67
 
71
68
  ## 工具
@@ -158,7 +155,7 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
158
155
  ## 安全边界
159
156
 
160
157
  - token 只存在于 DSH 凭据服务,设置中只有名称;
161
- - 读取操作不改变状态,写入必须显式确认;删除仓库、转移仓库、删除组织会被直接拒绝;
158
+ - 读取操作不改变状态;写入需要显式 `confirm` 参数,删除仓库、转移仓库、删除组织会被直接拒绝。是否显示确认提示由宿主的审批机制决定——本插件不自行运行审批关卡;
162
159
  - 客户端不会输出 token,失败以返回值形式报告(`ok: false` 及 `status`、`code`、`rateLimit`),
163
160
  不会中断整个回合;
164
161
  - `gh_review` 的发现是确定性规则:指出需要关注的地方,绝不冒充正确性结论。
package/lib/client.js CHANGED
@@ -35,8 +35,6 @@ window.__ModuleLoader__.load({
35
35
  timeoutMs: 'Request timeout, ms',
36
36
  maxRetries: 'Retries for a failed read',
37
37
  maxRetriesHint: 'A read (GET/HEAD) is retried on a network failure, a timeout or a 5xx. Writes are never retried automatically.',
38
- approvalMode: 'Write approval',
39
- approvalModeHint: '"auto" lets agreed non-destructive writes through without a prompt; deleting a release, tag, secret, variable, ruleset, branch protection or cancelling a run always asks. "ask" asks for every write; "off" leaves approvals to the host.',
40
38
  reviewRules: 'Review rule overrides (JSON)',
41
39
  reviewRulesHint: 'Optional. sensitivePaths, sensitiveSeverity, attentionPaths, migrationPaths, testsRequired, sourcePatterns, testPatterns, largeDiffLines. Example: {"largeDiffLines": 400, "sensitivePaths": [".env", "private/**"]}',
42
40
  timeoutMsHint: 'How long one GitHub request may take before it is aborted.',
@@ -66,8 +64,6 @@ window.__ModuleLoader__.load({
66
64
  timeoutMs: '请求超时(毫秒)',
67
65
  maxRetries: '读取失败重试次数',
68
66
  maxRetriesHint: '读取(GET/HEAD)在网络故障、超时或 5xx 时重试;写入不会自动重试。',
69
- approvalMode: '写入审批',
70
- approvalModeHint: '"auto" 允许已同意的非破坏性写入免询问;删除 release、tag、secret、变量、ruleset、分支保护或取消运行仍会询问。"ask" 每次写入都询问;"off" 交由宿主决定。',
71
67
  reviewRules: '评审规则覆盖(JSON)',
72
68
  reviewRulesHint: '可选。sensitivePaths、sensitiveSeverity、attentionPaths、migrationPaths、testsRequired、sourcePatterns、testPatterns、largeDiffLines。',
73
69
  timeoutMsHint: '单个 GitHub 请求在被中断前允许的时长。',
@@ -185,7 +181,7 @@ window.__ModuleLoader__.load({
185
181
  if (!scope || typeof scope.set !== 'function') return
186
182
  setSaving(true)
187
183
  const failed = []
188
- const keys = ['tokenEnv', 'tokenSource', 'defaultRepository', 'baseUrl', 'timeoutMs', 'maxRetries', 'reviewRulesJson', 'approvalMode']
184
+ const keys = ['tokenEnv', 'tokenSource', 'defaultRepository', 'baseUrl', 'timeoutMs', 'maxRetries', 'reviewRulesJson']
189
185
  for (const key of keys) {
190
186
  if (current[key] === value[key]) continue
191
187
  let next = current[key]
@@ -241,17 +237,6 @@ window.__ModuleLoader__.load({
241
237
  field('baseUrl', 'baseUrl', 'baseUrlHint'),
242
238
  field('timeoutMs', 'timeoutMs', 'timeoutMsHint', 'number'),
243
239
  field('maxRetries', 'maxRetries', 'maxRetriesHint', 'number'),
244
- React.createElement('div', { className: 'gho-field' },
245
- React.createElement('label', { className: 'gho-label', htmlFor: `${NS}-approvalMode` }, t('approvalMode')),
246
- React.createElement('div', { className: 'gho-hint' }, t('approvalModeHint')),
247
- React.createElement('select', {
248
- id: `${NS}-approvalMode`,
249
- className: 'gho-input',
250
- value: current.approvalMode || 'auto',
251
- disabled: !writable,
252
- onChange: (event) => change('approvalMode', event.target.value),
253
- }, ['auto', 'ask', 'off'].map((option) => React.createElement('option', { key: option, value: option }, option))),
254
- ),
255
240
  field('reviewRulesJson', 'reviewRules', 'reviewRulesHint', 'json'),
256
241
  React.createElement('div', { className: 'gho-foot' },
257
242
  React.createElement('span', { className: 'gho-note' }, message),
package/lib/index.js CHANGED
@@ -21,7 +21,6 @@ import { presentationFor } from './present.js'
21
21
  import { createRecords, startReviewJob } from './jobs.js'
22
22
  import { registerCommands } from './commands.js'
23
23
  import { registerJobTools } from './tools/register-jobs.js'
24
- import { registerApprovalGate, ALL_ACTIONS } from './approval.js'
25
24
  import { registerCoreTools } from './tools/register-core.js'
26
25
  import { registerCollabTools } from './tools/register-collab.js'
27
26
  import { registerMirrorTools } from './tools/register-mirror.js'
@@ -67,9 +66,6 @@ export const OUTPUT_SCHEMA = {
67
66
 
68
67
  const MAX_RENDER_CHARS = 20000
69
68
 
70
- /** Actions this plugin can be asked to perform; exposed for settings validation. */
71
- export const KNOWN_ACTIONS = ALL_ACTIONS
72
-
73
69
  /** Render a tool result for the model: readable text, bounded so one call cannot flood the turn. */
74
70
  export function renderToolResult(toolName, value) {
75
71
  if (!value || value.ok !== true) {
@@ -108,12 +104,6 @@ export const Config = Schema.object({
108
104
  .description('Retries for a read (GET/HEAD) on a network failure, a timeout or a 5xx. Writes are never retried automatically.'),
109
105
  reviewRulesJson: Schema.string().default('')
110
106
  .description('Optional review-rule overrides as JSON: sensitivePaths, sensitiveSeverity, attentionPaths, migrationPaths, testsRequired, sourcePatterns, testPatterns, largeDiffLines.'),
111
- approvalMode: Schema.string().default('auto')
112
- .description('"auto" (default): an agreed, non-destructive write goes through without a prompt, while deleting a release, tag, secret, variable, ruleset, branch protection or cancelling a run always asks. "ask": every write asks. "off": the plugin has no opinion and the host approval contour decides.'),
113
- allowedActions: Schema.array(Schema.string()).default([])
114
- .description('Write actions this plugin may perform; anything else is denied. Empty means every known action is allowed, and each one still asks for confirmation.'),
115
- autoApprove: Schema.array(Schema.string()).default([])
116
- .description('Actions an unattended run (DSH_GITHUB_OPS_UNATTENDED=1) may perform without asking. Destructive actions are never auto-approved.'),
117
107
  reviewJobTimeoutMs: Schema.number().default(120000)
118
108
  .description('How long a background review job may run before it is cancelled.'),
119
109
  })
@@ -294,7 +284,7 @@ export function apply(ctx, rawConfig) {
294
284
  })
295
285
 
296
286
  // Slash commands are a fast path for a human: they only hand the model an instruction, so
297
- // the write still goes through the approval gate.
287
+ // the write still passes through the host approval contour.
298
288
  ctx.inject(['commands'], (cctx) => {
299
289
  try {
300
290
  const git = makeGitRunner()
@@ -317,13 +307,6 @@ export function apply(ctx, rawConfig) {
317
307
  }
318
308
  })
319
309
 
320
- // Writes go through the approval contour: a named action, a reason a human can decide
321
- // on, and no auto-approval for anything destructive.
322
- try {
323
- registerApprovalGate(ctx, () => liveConfig())
324
- } catch (err) {
325
- ctx.logger?.warn?.('[dsh-github-ops] approval gate not registered:', err && err.message)
326
- }
327
310
 
328
311
  if (skippedTools.length) {
329
312
  ctx.logger?.warn?.(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-github-ops",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/lib/approval.js DELETED
@@ -1,193 +0,0 @@
1
- // Who approves a GitHub write, and why.
2
- //
3
- // Every state-changing tool maps to a named action. The action must be in `allowedActions`
4
- // (otherwise it is denied outright), a destructive action is always asked even in an
5
- // unattended run, and anything else asks the human with a sentence that says what is about
6
- // to happen — not just "perform a GitHub write".
7
- //
8
- // `decide()` is pure: the policy is a function of the call and the settings, so it is fully
9
- // testable and the listener stays trivial.
10
-
11
- /** Tool → action name. A tool absent from this map is not a write. */
12
- export const ACTION_BY_TOOL = {
13
- gh_release_create: 'release-create',
14
- gh_release_edit: 'release-edit',
15
- gh_release_delete: 'release-delete',
16
- gh_tag_create: 'tag-create',
17
- gh_tag_delete: 'tag-delete',
18
- gh_repo_create: 'repo-create',
19
- gh_repo_edit: 'repo-edit',
20
- issue_open: 'issue-open',
21
- issue_comment: 'issue-comment',
22
- issue_close: 'issue-close',
23
- pr_create: 'pr-create',
24
- pr_update: 'pr-update',
25
- pr_merge: 'pr-merge',
26
- review_post: 'review-post',
27
- gh_variable_set: 'variable-set',
28
- gh_variable_delete: 'variable-delete',
29
- gh_secret_set: 'secret-set',
30
- gh_secret_delete: 'secret-delete',
31
- gh_ruleset_apply: 'ruleset-apply',
32
- gh_ruleset_delete: 'ruleset-delete',
33
- gh_branch_protection_set: 'branch-protection-set',
34
- gh_branch_protection_delete: 'branch-protection-delete',
35
- gh_run_rerun: 'run-rerun',
36
- gh_run_cancel: 'run-cancel',
37
- gh_mirror_publish: 'mirror-publish',
38
- }
39
-
40
- /** Actions a single confirmation cannot make safe: always asked, never auto-approved. */
41
- export const DESTRUCTIVE_ACTIONS = new Set([
42
- 'release-delete', 'tag-delete', 'variable-delete', 'secret-delete',
43
- 'ruleset-delete', 'branch-protection-delete', 'run-cancel',
44
- ])
45
-
46
- export const ALL_ACTIONS = Object.freeze([...new Set(Object.values(ACTION_BY_TOOL))])
47
-
48
- const MUTATING_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE'])
49
-
50
- /** Which action a call is, or null when it changes nothing. */
51
- export function actionFor(exec) {
52
- const name = exec && exec.name
53
- if (!name) return null
54
- if (name === 'gh_api') {
55
- const args = (exec && exec.arguments) || {}
56
- if (args.graphql) return null // a query may still mutate; the GraphQL text cannot be judged here
57
- const method = String(args.method || 'GET').toUpperCase()
58
- return MUTATING_METHODS.has(method) ? 'api-call' : null
59
- }
60
- return ACTION_BY_TOOL[name] || null
61
- }
62
-
63
- /** A sentence a human can decide on. */
64
- export function askReason(exec) {
65
- const args = (exec && exec.arguments) || {}
66
- const repo = args.repository || 'the default repository'
67
- const name = exec && exec.name
68
- switch (name) {
69
- case 'gh_release_create':
70
- return `create GitHub release ${args.tag}${args.draft ? ' (draft)' : ''} in ${repo}`
71
- case 'gh_release_edit':
72
- return `edit GitHub release ${args.tag || args.releaseId} in ${repo}${args.makeLatest ? ' and mark it latest' : ''}`
73
- case 'gh_release_delete':
74
- return `delete GitHub release ${args.tag || args.releaseId} in ${repo} (the git tag stays)`
75
- case 'gh_tag_create':
76
- return `create tag ${args.tag} in ${repo}`
77
- case 'gh_tag_delete':
78
- return `delete tag ${args.tag} in ${repo}`
79
- case 'gh_repo_create':
80
- return `create repository ${args.name}${args.org ? ` in ${args.org}` : ''}${args.private === false ? ' as PUBLIC' : ' (private)'}`
81
- case 'gh_repo_edit':
82
- return `change settings of ${repo}${args.private !== undefined ? ` (visibility: ${args.private ? 'private' : 'PUBLIC'})` : ''}`
83
- case 'issue_open':
84
- return `open issue "${args.title}" in ${repo}`
85
- case 'issue_comment':
86
- return `comment on #${args.number} in ${repo}`
87
- case 'issue_close':
88
- return `close #${args.number} in ${repo}`
89
- case 'pr_create':
90
- return `open pull request "${args.title}" (${args.head} → ${args.base || 'main'}) in ${repo}`
91
- case 'pr_update':
92
- return `update pull request #${args.number} in ${repo}`
93
- case 'pr_merge':
94
- return `MERGE pull request #${args.number} in ${repo} (${args.method || 'merge'})`
95
- case 'review_post':
96
- return `post a review on #${args.number} in ${repo}`
97
- case 'gh_variable_set':
98
- return `set variable ${args.name} in ${repo}`
99
- case 'gh_variable_delete':
100
- return `delete variable ${args.name} in ${repo}`
101
- case 'gh_secret_set':
102
- return `set secret ${args.name} in ${repo}`
103
- case 'gh_secret_delete':
104
- return `delete secret ${args.name} in ${repo}`
105
- case 'gh_ruleset_apply':
106
- return `apply ruleset ${args.name || args.rulesetId} to ${repo}`
107
- case 'gh_ruleset_delete':
108
- return `delete ruleset ${args.rulesetId} from ${repo}`
109
- case 'gh_branch_protection_set':
110
- return `replace branch protection on ${args.branch} in ${repo}`
111
- case 'gh_branch_protection_delete':
112
- return `remove branch protection from ${args.branch} in ${repo}`
113
- case 'gh_run_rerun':
114
- return `re-run workflow run ${args.runId} in ${repo}`
115
- case 'gh_run_cancel':
116
- return `cancel workflow run ${args.runId} in ${repo}`
117
- case 'gh_mirror_publish':
118
- return `publish a sanitized tree to the mirror of ${args.cwd || 'the checkout'} (${args.mirror || 'github'}/${args.branch || 'main'})`
119
- case 'gh_api':
120
- return `call the GitHub API: ${String(args.method || 'GET').toUpperCase()} ${args.path}`
121
- default:
122
- return `perform a GitHub write action (${name})`
123
- }
124
- }
125
-
126
- /** Whether an unattended run may proceed without a human. */
127
- export function isUnattended(env = process.env) {
128
- return String((env && env.DSH_GITHUB_OPS_UNATTENDED) || '') === '1'
129
- }
130
-
131
- /** How the plugin treats a write. */
132
- export const APPROVAL_MODES = ['auto', 'ask', 'off']
133
-
134
- /**
135
- * The policy, as a pure function.
136
- *
137
- * - `auto` (default): an agreed, non-destructive write goes through without a prompt, so a
138
- * session whose host has approvals disabled is not blocked by a question it cannot answer.
139
- * A destructive action still asks — and where prompts are unavailable that means it is
140
- * refused, which is the intent: deleting a release, a tag, a secret, a ruleset or branch
141
- * protection is never automatic.
142
- * - `ask`: every write asks.
143
- * - `off`: the plugin has no opinion; the host's own approval contour decides everything.
144
- *
145
- * `allowedActions` is the fence in every mode: an action outside it is denied outright.
146
- *
147
- * @returns {{kind: 'allow'|'ask'|'deny'|'continue', reason?: string, action?: string|null}}
148
- */
149
- export function decide({ exec, allowedActions, autoApprove, unattended = false, mode = 'auto' } = {}) {
150
- const action = actionFor(exec)
151
- if (!action) return { kind: 'continue', action: null }
152
- if (mode === 'off') return { kind: 'continue', action }
153
-
154
- const allowed = Array.isArray(allowedActions) && allowedActions.length ? allowedActions : ALL_ACTIONS
155
- if (!allowed.includes(action)) {
156
- return { kind: 'deny', action, reason: `dsh-github-ops: action "${action}" is not in allowedActions` }
157
- }
158
-
159
- const destructive = DESTRUCTIVE_ACTIONS.has(action)
160
- if (destructive) {
161
- return {
162
- kind: 'ask',
163
- action,
164
- reason: `dsh-github-ops: ${askReason(exec)}. This cannot be undone here, so it is never automatic.`,
165
- }
166
- }
167
-
168
- if (mode === 'auto') return { kind: 'allow', action }
169
- if (unattended && Array.isArray(autoApprove) && autoApprove.includes(action)) return { kind: 'allow', action }
170
- return { kind: 'ask', action, reason: `dsh-github-ops: ${askReason(exec)}.` }
171
- }
172
-
173
- /**
174
- * Register the gate. Registration is an effect: disposing the plugin removes the listener.
175
- */
176
- export function registerApprovalGate(ctx, getConfig) {
177
- const first = (typeof getConfig === 'function' ? getConfig() : getConfig) || {}
178
- if (first.approvalMode === 'off') return () => {}
179
- return ctx.on('tools/pre-execute', async (exec, next) => {
180
- const cfg = (typeof getConfig === 'function' ? getConfig() : getConfig) || {}
181
- const decision = decide({
182
- exec,
183
- allowedActions: cfg.allowedActions,
184
- autoApprove: cfg.autoApprove,
185
- mode: cfg.approvalMode || 'auto',
186
- unattended: cfg.unattended !== undefined ? cfg.unattended : isUnattended(),
187
- })
188
- if (decision.kind === 'continue') return next()
189
- if (decision.kind === 'allow') return { kind: 'allow' }
190
- if (decision.kind === 'deny') return { kind: 'deny', reason: decision.reason }
191
- return { kind: 'ask', reason: decision.reason }
192
- })
193
- }