auto-mode 0.3.0 → 0.4.1

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/dist/cli.js CHANGED
@@ -1,7 +1,56 @@
1
1
  #!/usr/bin/env node
2
- import { _ as resolveConfigPath, a as loadPolicy, c as SETUP_NOTES, d as parsePayload, h as loadConfig, l as buildHookConfig, n as classifyWithModel, o as EVENT_NOTES, s as SETTINGS_PATHS, t as classifyLocally, u as renderVerdict } from "./classify-locally-BRMllwcd.js";
3
- import { parseArgs } from "node:util";
2
+ import { c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, i as loadClaudeRules, l as SETUP_NOTES, n as classifyWithModel, o as loadPolicy, s as EVENT_NOTES, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-Dsyc0O6N.js";
3
+ import { appendFile, mkdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { createHash, randomUUID } from "node:crypto";
4
7
  import { text } from "node:stream/consumers";
8
+ import { parseArgs } from "node:util";
9
+ //#region src/config/build-jev-only-config.ts
10
+ function buildJevOnlyConfig(config) {
11
+ if (config.provider.protocol !== "system-one") return null;
12
+ return {
13
+ ...config,
14
+ provider: {
15
+ ...config.provider,
16
+ timeoutMs: Math.min(config.provider.timeoutMs, 5e3)
17
+ }
18
+ };
19
+ }
20
+ //#endregion
21
+ //#region src/diagnostics/write-action-diagnostic.ts
22
+ async function writeActionDiagnostic(payload, entry) {
23
+ const stateHome = process.env["XDG_STATE_HOME"];
24
+ const stateDir = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
25
+ const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(stateDir, "auto-mode", "actions.jsonl");
26
+ if (path === "") return;
27
+ const actionID = payload.raw["tool_use_id"] ?? payload.raw["prompt_id"] ?? payload.raw["turn_id"];
28
+ const record = {
29
+ schemaVersion: 1,
30
+ time: (/* @__PURE__ */ new Date()).toISOString(),
31
+ invocationID: entry.invocationID,
32
+ sessionHash: toHash(payload.sessionId),
33
+ actionHash: typeof actionID === "string" ? toHash(actionID) : null,
34
+ harness: payload.harness,
35
+ event: payload.event,
36
+ status: entry.status,
37
+ verdict: entry.verdict ?? null,
38
+ diagnostics: entry.diagnostics ?? null
39
+ };
40
+ try {
41
+ await mkdir(dirname(path), {
42
+ recursive: true,
43
+ mode: 448
44
+ });
45
+ await appendFile(path, `${JSON.stringify(record)}\n`, { mode: 384 });
46
+ } catch {
47
+ process.stderr.write("auto-mode: diagnostics unavailable\n");
48
+ }
49
+ }
50
+ function toHash(value) {
51
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
52
+ }
53
+ //#endregion
5
54
  //#region src/cli.ts
6
55
  const USAGE = `auto-mode — a permission classifier that runs as a hook
7
56
 
@@ -16,11 +65,13 @@ Options:
16
65
  --event <event> With init: pre-tool-use, the default, or permission-request
17
66
  --explain With run: also write the reasoning to stderr
18
67
  --local-only With run: skip the model tier
68
+ --jev-only With run: require Jev and cap its API timeout at 5 seconds
69
+ --evaluation-deadline <unix-ms> With --jev-only: share the helper and API deadline
19
70
  `;
20
71
  function readStdin() {
21
72
  return text(process.stdin);
22
73
  }
23
- async function run(explain, localOnly) {
74
+ async function run(explain, localOnly, jevOnly, deadline) {
24
75
  const raw = await readStdin();
25
76
  let body;
26
77
  try {
@@ -30,16 +81,99 @@ async function run(explain, localOnly) {
30
81
  }
31
82
  const payload = parsePayload(body);
32
83
  if (payload === null) return printNote(explain, "not a tool gate this hook judges, so no verdict");
33
- const local = classifyLocally(payload);
84
+ const invocationID = randomUUID();
85
+ await writeActionDiagnostic(payload, {
86
+ invocationID,
87
+ status: "started"
88
+ });
89
+ let loaded;
90
+ try {
91
+ loaded = await loadConfig();
92
+ } catch {
93
+ await writeActionDiagnostic(payload, {
94
+ invocationID,
95
+ status: "failure",
96
+ verdict: "defer"
97
+ });
98
+ return printNote(true, "configuration unreadable; deferring to the harness");
99
+ }
100
+ const config = jevOnly ? buildJevOnlyConfig(loaded) : loaded;
101
+ if (config === null) {
102
+ await writeActionDiagnostic(payload, {
103
+ invocationID,
104
+ status: "failure",
105
+ verdict: "defer"
106
+ });
107
+ return printNote(true, "Jev-only evaluation requires system-one; deferring to the harness");
108
+ }
109
+ let configured = null;
110
+ if (config.provider.protocol === "system-one") try {
111
+ configured = await loadClaudeRules(config.claudeSettingsPath);
112
+ } catch {
113
+ if (config.onFailure === "deny") writeVerdict(payload.event, {
114
+ kind: "deny",
115
+ rule: "Classifier Unavailable",
116
+ reason: "Claude settings unreadable"
117
+ });
118
+ await writeActionDiagnostic(payload, {
119
+ invocationID,
120
+ status: "failure",
121
+ verdict: config.onFailure === "deny" ? "deny" : "defer"
122
+ });
123
+ return printNote(true, "Claude settings unreadable; classifier unavailable");
124
+ }
125
+ const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(payload);
34
126
  if (local.kind === "allow") {
35
127
  writeVerdict(payload.event, { kind: "allow" });
128
+ await writeActionDiagnostic(payload, {
129
+ invocationID,
130
+ status: "allow",
131
+ verdict: "allow"
132
+ });
36
133
  return printNote(explain, `allowed by ${local.exception} (${payload.harness}, local)`);
37
134
  }
38
- if (localOnly) return printNote(explain, `${payload.toolName} needs the model tier, which --local-only skipped`);
39
- const config = await loadConfig();
40
- const outcome = await classifyWithModel(payload, config);
41
- if (outcome.verdict !== null) writeVerdict(payload.event, outcome.verdict);
42
- return printNote(explain, outcome.note);
135
+ if (localOnly) {
136
+ await writeActionDiagnostic(payload, {
137
+ invocationID,
138
+ status: "skipped",
139
+ verdict: "defer"
140
+ });
141
+ return printNote(explain, `${payload.toolName} needs the model tier, which --local-only skipped`);
142
+ }
143
+ const deadlineAt = deadline === void 0 ? void 0 : Number(deadline);
144
+ if (deadlineAt !== void 0 && (!jevOnly || !Number.isSafeInteger(deadlineAt) || deadlineAt <= 0)) {
145
+ await writeActionDiagnostic(payload, {
146
+ invocationID,
147
+ status: "failure",
148
+ verdict: "defer"
149
+ });
150
+ return printNote(true, "invalid Jev evaluation deadline; deferring to the harness");
151
+ }
152
+ const controller = new AbortController();
153
+ const stopEvaluation = () => {
154
+ controller.abort();
155
+ };
156
+ if (jevOnly) {
157
+ process.on("SIGTERM", stopEvaluation);
158
+ process.on("SIGINT", stopEvaluation);
159
+ }
160
+ try {
161
+ const outcome = await classifyWithModel(payload, config, {
162
+ deadlineAt,
163
+ signal: controller.signal
164
+ });
165
+ if (outcome.verdict !== null) writeVerdict(payload.event, outcome.verdict);
166
+ await writeActionDiagnostic(payload, {
167
+ invocationID,
168
+ status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure",
169
+ verdict: outcome.verdict?.kind ?? "defer",
170
+ ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
171
+ });
172
+ return printNote(explain || outcome.unavailable === true, outcome.note);
173
+ } finally {
174
+ process.off("SIGTERM", stopEvaluation);
175
+ process.off("SIGINT", stopEvaluation);
176
+ }
43
177
  }
44
178
  function writeVerdict(event, verdict) {
45
179
  const rendered = renderVerdict(event, verdict);
@@ -63,6 +197,8 @@ async function main(argv) {
63
197
  event: { type: "string" },
64
198
  explain: { type: "boolean" },
65
199
  "local-only": { type: "boolean" },
200
+ "jev-only": { type: "boolean" },
201
+ "evaluation-deadline": { type: "string" },
66
202
  help: {
67
203
  type: "boolean",
68
204
  short: "h"
@@ -74,7 +210,12 @@ async function main(argv) {
74
210
  process.stdout.write(USAGE);
75
211
  return 0;
76
212
  }
77
- if (command === "run") return run(args.values.explain === true, args.values["local-only"] === true);
213
+ if (command === "run") try {
214
+ return await run(args.values.explain === true, args.values["local-only"] === true, args.values["jev-only"] === true, args.values["evaluation-deadline"]);
215
+ } catch {
216
+ process.stderr.write("auto-mode: configuration unreadable; deferring to the harness\n");
217
+ return 0;
218
+ }
78
219
  if (command === "init") {
79
220
  const [, harness] = args.positionals;
80
221
  if (harness !== "claude" && harness !== "codex" && harness !== "muse") {
@@ -97,10 +238,12 @@ async function main(argv) {
97
238
  return 0;
98
239
  }
99
240
  if (command === "print-prompt") {
241
+ const config = await loadConfig();
242
+ const framework = config.provider.protocol === "system-one" ? "decision.md" : "classifier.md";
100
243
  const prompt = await loadPolicy({
101
- classifierPath: args.values.classifier,
102
- rulesPath: args.values.rules
103
- });
244
+ classifierPath: args.values.classifier ?? config.classifierPath,
245
+ rulesPath: args.values.rules ?? config.rulesPath
246
+ }, framework);
104
247
  process.stdout.write(`${prompt}\n`);
105
248
  return 0;
106
249
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
+ //#region src/config/types.d.ts
2
+ interface EvaluationOptions {
3
+ readonly deadlineAt?: number | undefined;
4
+ readonly signal?: Readonly<AbortSignal> | undefined;
5
+ }
6
+ //#endregion
1
7
  //#region src/config/config.d.ts
2
8
  interface ProviderConfig {
9
+ readonly protocol?: 'messages' | 'system-one';
3
10
  readonly baseURL: string;
4
11
  readonly model: string;
5
12
  readonly apiKeyEnv?: string | undefined;
@@ -14,16 +21,35 @@ interface Config {
14
21
  readonly rulesPath?: string | undefined;
15
22
  readonly transcriptEntries: number;
16
23
  readonly onFailure: 'defer' | 'deny';
24
+ readonly claudeSettingsPath?: string | null | undefined;
25
+ readonly minConfidence?: number | undefined;
17
26
  }
18
27
  export declare const DEFAULT_CONFIG: Config;
19
28
  export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
20
29
  export declare function resolveConfigPath(): string;
21
30
  export declare function loadConfig(path?: string): Promise<Config>;
22
- export declare function resolveApiKey(provider: ProviderConfig): Promise<string | null>;
31
+ export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
23
32
  //#endregion
24
33
  //#region src/harness/types.d.ts
25
34
  type Harness = 'claude' | 'codex' | 'muse';
26
35
  type HookEvent = 'PreToolUse' | 'PermissionRequest';
36
+ interface UserTask {
37
+ readonly text: string;
38
+ readonly origin: 'composer' | 'bridge' | 'sdk';
39
+ }
40
+ interface DecisionContext {
41
+ readonly agentID: string | null;
42
+ readonly originalUserTask: UserTask | null;
43
+ readonly delegatedTask: {
44
+ readonly text: string;
45
+ readonly origin: 'agent.spawn';
46
+ } | null;
47
+ readonly lastDirectUserMessage: UserTask | null;
48
+ readonly omittedTaskContext: readonly {
49
+ readonly field: 'originalUserTask' | 'delegatedTask';
50
+ readonly reason: 'unavailable' | 'budget';
51
+ }[];
52
+ }
27
53
  interface HookPayload {
28
54
  readonly harness: Harness;
29
55
  readonly event: HookEvent;
@@ -32,6 +58,7 @@ interface HookPayload {
32
58
  readonly toolName: string;
33
59
  readonly toolInput: Readonly<Record<string, unknown>>;
34
60
  readonly transcriptPath?: string | undefined;
61
+ readonly decisionContext?: DecisionContext | undefined;
35
62
  readonly raw: Readonly<Record<string, unknown>>;
36
63
  }
37
64
  type Verdict = {
@@ -57,12 +84,44 @@ export declare function renderVerdict(event: HookEvent, verdict: Verdict): strin
57
84
  export declare function buildHookConfig(harness: Harness, command: string, event?: HookEvent): string;
58
85
  export declare const SETTINGS_PATHS: Readonly<Record<Harness, string>>;
59
86
  //#endregion
87
+ //#region src/model/types.d.ts
88
+ interface DecisionRule {
89
+ readonly name: string;
90
+ readonly tier: 'hard' | 'soft';
91
+ readonly source: 'shipped' | 'replacement' | 'configured';
92
+ readonly text: string;
93
+ }
94
+ interface DecisionAnswer {
95
+ readonly type: 'choice';
96
+ readonly choice: 'allow' | 'block' | 'ask';
97
+ readonly probabilities: Readonly<Record<'allow' | 'block' | 'ask', number>>;
98
+ readonly confidence: number;
99
+ }
100
+ interface DecisionDiagnostics {
101
+ readonly status: 'allow' | 'deny' | 'ask' | 'failure' | 'timeout' | 'cancelled';
102
+ readonly stage: 'credential' | 'evidence' | 'request' | 'response';
103
+ readonly keyResolved: boolean;
104
+ readonly keySource: 'environment' | 'command' | 'none';
105
+ readonly elapsedMs: number;
106
+ readonly minConfidence: number;
107
+ readonly contributors: readonly {
108
+ readonly rule: string;
109
+ readonly source: DecisionRule['source'];
110
+ readonly tier: DecisionRule['tier'];
111
+ readonly choice: DecisionAnswer['choice'];
112
+ readonly confidence: number;
113
+ readonly probability: number;
114
+ }[];
115
+ }
116
+ //#endregion
60
117
  //#region src/model/classify-with-model.d.ts
61
118
  interface ModelOutcome {
62
119
  readonly verdict: Verdict | null;
63
120
  readonly note: string;
121
+ readonly unavailable?: boolean;
122
+ readonly diagnostics?: DecisionDiagnostics;
64
123
  }
65
- export declare function classifyWithModel(payload: HookPayload, config: Config): Promise<ModelOutcome>;
124
+ export declare function classifyWithModel(payload: HookPayload, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
66
125
  //#endregion
67
126
  //#region src/model/parse-verdict.d.ts
68
127
  export declare function parseModelVerdict(text: string): Verdict;
@@ -72,7 +131,7 @@ interface PolicyPaths {
72
131
  readonly classifierPath?: string | undefined;
73
132
  readonly rulesPath?: string | undefined;
74
133
  }
75
- export declare function loadPolicy(paths?: PolicyPaths): Promise<string>;
134
+ export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
76
135
  //#endregion
77
136
  //#region src/rules/classify-locally.d.ts
78
137
  type LocalVerdict = {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as resolveConfigPath, a as loadPolicy, d as parsePayload, f as detectHarness, g as resolveApiKey, h as loadConfig, i as readTranscript, l as buildHookConfig, m as PRESETS, n as classifyWithModel, p as DEFAULT_CONFIG, r as parseModelVerdict, s as SETTINGS_PATHS, t as classifyLocally, u as renderVerdict } from "./classify-locally-BRMllwcd.js";
1
+ import { _ as resolveApiKey, a as readTranscript, c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, h as PRESETS, m as DEFAULT_CONFIG, n as classifyWithModel, o as loadPolicy, p as detectHarness, r as parseModelVerdict, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-Dsyc0O6N.js";
2
2
  export { DEFAULT_CONFIG, PRESETS, SETTINGS_PATHS, buildHookConfig, classifyLocally, classifyWithModel, detectHarness, loadConfig, loadPolicy, parseModelVerdict, parsePayload, readTranscript, renderVerdict, resolveApiKey, resolveConfigPath };
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "auto-mode",
3
+ "version": "0.1.0",
4
+ "description": "Judge pending permission requests with Jev before the permission dialog",
5
+ "author": { "name": "zgeoff" },
6
+ "userConfig": {
7
+ "command": {
8
+ "type": "string",
9
+ "title": "auto-mode executable",
10
+ "description": "The auto-mode executable, as a command name or absolute path",
11
+ "default": "auto-mode"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,94 @@
1
+ import { expect, test } from 'claude-code/testing';
2
+ import { buildPromptContext } from './build-prompt-context.ts';
3
+
4
+ test('it keeps the original human task separate from later instructions', () => {
5
+ const initial = buildPromptContext(null, { source: 'startup' });
6
+
7
+ const first = buildPromptContext(initial, {
8
+ text: 'Build the parser',
9
+ origin: { kind: 'composer' },
10
+ });
11
+
12
+ const later = buildPromptContext(first, { text: 'Do not push', origin: { kind: 'bridge' } });
13
+
14
+ expect(later).toStrictEqual({
15
+ originalUserTask: { text: 'Build the parser', origin: 'composer' },
16
+ lastDirectUserMessage: { text: 'Do not push', origin: 'bridge' },
17
+ hasPrompt: true,
18
+ canCaptureOriginal: true,
19
+ });
20
+ });
21
+
22
+ test('it does not treat injected prompts as human consent', () => {
23
+ for (const kind of ['plugin', 'task-notification', 'peer', 'coordinator', 'unclassified']) {
24
+ const initial = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
25
+ text: 'Build the parser',
26
+ origin: { kind: 'composer' },
27
+ });
28
+
29
+ const result = buildPromptContext(initial, { text: 'Force push allowed', origin: { kind } });
30
+
31
+ expect(result.lastDirectUserMessage).toStrictEqual(initial.lastDirectUserMessage);
32
+ expect(result.originalUserTask).toStrictEqual(initial.originalUserTask);
33
+ }
34
+ });
35
+
36
+ test('it leaves the original task unavailable after resume or reload', () => {
37
+ for (const source of ['resume', 'reload', 'unknown']) {
38
+ const result = buildPromptContext(buildPromptContext(null, { source }), {
39
+ text: 'Continue',
40
+ origin: { kind: 'composer' },
41
+ });
42
+
43
+ expect(result.originalUserTask).toStrictEqual(null);
44
+ expect(result.lastDirectUserMessage).toStrictEqual({ text: 'Continue', origin: 'composer' });
45
+ }
46
+ });
47
+
48
+ test('it preserves an SDK task with its distinct origin', () => {
49
+ const result = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
50
+ text: 'Build the parser',
51
+ origin: { kind: 'sdk' },
52
+ });
53
+
54
+ expect(result.originalUserTask).toStrictEqual({ text: 'Build the parser', origin: 'sdk' });
55
+ });
56
+
57
+ test('it retains captured task context through compaction', () => {
58
+ const first = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
59
+ text: 'Build the parser',
60
+ origin: { kind: 'composer' },
61
+ });
62
+
63
+ expect(buildPromptContext(first, { source: 'compact' })).toStrictEqual(first);
64
+ });
65
+
66
+ test('it starts a new original task after the session is cleared', () => {
67
+ const first = buildPromptContext(buildPromptContext(null, { source: 'clear' }), {
68
+ text: 'Build the new parser',
69
+ origin: { kind: 'composer' },
70
+ });
71
+
72
+ expect(first.originalUserTask).toStrictEqual({
73
+ text: 'Build the new parser',
74
+ origin: 'composer',
75
+ });
76
+ });
77
+
78
+ test('it captures the first direct task after an initial machine notification', () => {
79
+ const machine = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
80
+ text: 'Machine notification',
81
+ origin: { kind: 'task-notification' },
82
+ });
83
+
84
+ expect(machine.originalUserTask).toStrictEqual(null);
85
+ expect(machine.lastDirectUserMessage).toStrictEqual(null);
86
+
87
+ const human = buildPromptContext(machine, {
88
+ text: 'Build the parser',
89
+ origin: { kind: 'composer' },
90
+ });
91
+
92
+ expect(human.originalUserTask).toStrictEqual({ text: 'Build the parser', origin: 'composer' });
93
+ expect(human.lastDirectUserMessage).toStrictEqual(human.originalUserTask);
94
+ });
@@ -0,0 +1,37 @@
1
+ import type { PromptContext, UserTask } from './types.ts';
2
+
3
+ export function buildPromptContext(
4
+ previous: PromptContext | null,
5
+ input:
6
+ | { readonly source: string }
7
+ | { readonly text: string; readonly origin: { readonly kind: string } },
8
+ ): PromptContext {
9
+ if ('source' in input) {
10
+ if (input.source === 'compact' && previous !== null) {
11
+ return previous;
12
+ }
13
+
14
+ return {
15
+ originalUserTask: null,
16
+ lastDirectUserMessage: null,
17
+ hasPrompt: false,
18
+ canCaptureOriginal: input.source === 'startup' || input.source === 'clear',
19
+ };
20
+ }
21
+
22
+ const origin = input.origin.kind;
23
+
24
+ const lastDirectUserMessage: UserTask | null =
25
+ origin === 'composer' || origin === 'bridge' || origin === 'sdk'
26
+ ? { text: input.text, origin }
27
+ : (previous?.lastDirectUserMessage ?? null);
28
+
29
+ return {
30
+ originalUserTask:
31
+ previous?.originalUserTask ??
32
+ (previous?.canCaptureOriginal === true ? lastDirectUserMessage : null),
33
+ lastDirectUserMessage,
34
+ hasPrompt: true,
35
+ canCaptureOriginal: previous?.canCaptureOriginal ?? false,
36
+ };
37
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.ts"] }
@@ -0,0 +1,51 @@
1
+ interface Decision {
2
+ readonly decision: 'allow' | 'deny';
3
+ readonly reason?: string;
4
+ }
5
+
6
+ export function parseDecision(stdout: string): Decision | null {
7
+ let body: unknown;
8
+
9
+ try {
10
+ body = JSON.parse(stdout);
11
+ } catch {
12
+ return null;
13
+ }
14
+
15
+ if (typeof body !== 'object' || body === null || !('hookSpecificOutput' in body)) {
16
+ return null;
17
+ }
18
+
19
+ const output = body.hookSpecificOutput;
20
+
21
+ if (
22
+ typeof output !== 'object' ||
23
+ output === null ||
24
+ !('hookEventName' in output) ||
25
+ output.hookEventName !== 'PermissionRequest' ||
26
+ !('decision' in output)
27
+ ) {
28
+ return null;
29
+ }
30
+
31
+ const value = output.decision;
32
+
33
+ if (typeof value !== 'object' || value === null || !('behavior' in value)) {
34
+ return null;
35
+ }
36
+
37
+ if (value.behavior === 'allow') {
38
+ return { decision: 'allow' };
39
+ }
40
+
41
+ if (
42
+ value.behavior === 'deny' &&
43
+ 'message' in value &&
44
+ typeof value.message === 'string' &&
45
+ value.message.trim() !== ''
46
+ ) {
47
+ return { decision: 'deny', reason: value.message };
48
+ }
49
+
50
+ return null;
51
+ }