@davisliu2006/pi-permission-system 0.0.0-stage → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davis Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,129 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # pi-permission-system
2
+
3
+ Permissions management for the `pi` coding agent.
4
+
5
+ Roughly based on [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system).
6
+
7
+
8
+ ## Permissions Overview
9
+
10
+ A permission can be one of the following:
11
+
12
+ ```jsonc
13
+ "allow" // allows the action
14
+ "ask" // asks the user for permission
15
+ "deny" // automatically denies the action
16
+ ```
17
+
18
+ Ask states the purpose of the request and gives the following options:
19
+
20
+ ```
21
+ Allow Once
22
+ Allow Always
23
+ Reject
24
+ ```
25
+
26
+ "Reject" stops the current agent task and asks the user what to do next.
27
+
28
+ "Allow always" is persisted for the session only. It uses an AI-generated command wildcard for `bash` and an exact match for other tools.
29
+
30
+ ## Configuration
31
+
32
+ The supported top-level keys are: `default`, `tools`, `bash`, and `external_directory`.
33
+
34
+ `bash` body matching supports wildcard:
35
+ - `*` matches any sequence and `?` matches one character
36
+ - `\*` and `\?` are match literal `*` and `?`
37
+
38
+ `tools` matches exact tool names.
39
+
40
+ The builtin path tools in pi are: `read`, `write`, `edit`, `find`, `grep`, `ls`. `external_directory` overrides the settings for these path tools. The primary use is to override `write` and `edit` to a stronger policy for external directories.
41
+
42
+ ### Rule Precedence
43
+
44
+ More specific rules override less specific rules:
45
+
46
+ - `tools.*` overrides `default`
47
+ - `bash.*` overrides `tools.bash`
48
+ - Within `bash`, the last match wins
49
+
50
+ ### Global Configuration
51
+
52
+ Located in `~/.pi/agent/pi-permissions.jsonc`.
53
+
54
+ Example:
55
+
56
+ ```jsonc
57
+ {
58
+ "default": "ask",
59
+ "tools": {
60
+ "read": "allow",
61
+ "write": "allow",
62
+ "edit": "allow",
63
+ "find": "allow",
64
+ "grep": "allow",
65
+ "ls": "allow",
66
+ "bash": "ask",
67
+
68
+ // add any non builtin tools
69
+ "mcp": "ask"
70
+ },
71
+ "bash": {
72
+ "git status*": "allow",
73
+ "git log*": "allow",
74
+ "git diff*": "allow",
75
+ "git branch*": "allow",
76
+ "git show*": "allow",
77
+
78
+ "ls*": "allow",
79
+ "pwd": "allow",
80
+ "find*": "allow",
81
+ "tree*": "allow",
82
+ "cat*": "allow",
83
+ "head*": "allow",
84
+ "tail*": "allow",
85
+ "wc*": "allow",
86
+ "echo*": "allow",
87
+ "sed*": "allow",
88
+
89
+ "grep*": "allow",
90
+ "rg*": "allow"
91
+ },
92
+ "external_directory": {
93
+ "write": "ask"
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Local Configuration
99
+
100
+ Located in the local Pi config folder:
101
+
102
+ `<project-root>/.pi/pi-permissions.jsonc`
103
+
104
+ Local configuration overrides global configuration, including `deny` rules.
105
+
106
+ ## "Allow Always" Matching
107
+
108
+ Matching is done as `(tool, body)`.
109
+
110
+ Body content:
111
+
112
+ - For built-in path tools, it is the path
113
+ - For bash, it is the command
114
+ - For other tools, it is a stable JSON serialization of the full input
115
+
116
+ Body matching rules:
117
+
118
+ - For non-bash, it is exact match
119
+ - Bash supports wildcard matching
120
+
121
+ ## Bash Matching Safeguards
122
+
123
+ Bash matching safeguards against unsafe shell syntax.
124
+
125
+ Simple sequences separated only by `;`, `&&`, `||`, or newlines are split and each segment is matched against `bash` rules. The strictest result wins (`deny` > `ask` > `allow`).
126
+
127
+ If a command contains other chaining or control syntax (`|`, `&`, command substitution, process substitution, or parentheses), `bash` pattern rules are skipped and the `tools.bash` policy is used instead, falling back to `default`.
128
+
129
+ "Allow always" for complex or multi-command sequences saves the exact command, not a wildcard.
package/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerPermissionSystem } from "./src/extension.ts";
3
+
4
+ export default function piPermissionSystem(pi: ExtensionAPI): void {
5
+ registerPermissionSystem(pi);
6
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
- {
2
- "name": "@davisliu2006/pi-permission-system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@davisliu2006/pi-permission-system",
3
+ "version": "0.2.0",
4
+ "description": "Permission enforcement extension for the Pi coding agent.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc -p tsconfig.json --noEmit",
17
+ "test": "node --test tests/*.test.ts",
18
+ "check": "npm run typecheck && npm test"
19
+ },
20
+ "keywords": [
21
+ "pi-package",
22
+ "pi",
23
+ "pi-extension",
24
+ "permissions"
25
+ ],
26
+ "author": "davisliu2006",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/davisliu2006/pi-permission-system.git"
31
+ },
32
+ "pi": {
33
+ "extensions": [
34
+ "./index.ts"
35
+ ]
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "peerDependencies": {
41
+ "@earendil-works/pi-ai": ">=0.78.0",
42
+ "@earendil-works/pi-coding-agent": ">=0.78.0",
43
+ "@earendil-works/pi-tui": ">=0.78.0"
44
+ },
45
+ "devDependencies": {
46
+ "@earendil-works/pi-ai": "^0.78.1",
47
+ "@earendil-works/pi-coding-agent": "^0.78.1",
48
+ "@earendil-works/pi-tui": "^0.78.1",
49
+ "@types/node": "^22.10.0",
50
+ "typescript": "^5.7.3"
51
+ }
52
+ }
package/src/ai-gen.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { complete, type Message } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ export interface CompleteTextOptions {
5
+ systemPrompt: string;
6
+ userText: string;
7
+ maxTokens: number;
8
+ timeoutMs: number;
9
+ /** How to join multiple text blocks. Defaults to a single space. */
10
+ joinWith?: string;
11
+ }
12
+
13
+ /**
14
+ * Signal sent ot the model to abort if the request takes too long.
15
+ */
16
+ function timeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
17
+ const timeout = AbortSignal.timeout(timeoutMs);
18
+ if (!signal) {return timeout;}
19
+ if (typeof AbortSignal.any === "function") {
20
+ return AbortSignal.any([signal, timeout]);
21
+ }
22
+ return timeout;
23
+ }
24
+
25
+ /**
26
+ * Runs a single-turn text completion against the extension model.
27
+ * Returns undefined when the model/auth is unavailable, the call aborts, or no text is returned.
28
+ */
29
+ export async function completeText(
30
+ ctx: ExtensionContext,
31
+ options: CompleteTextOptions
32
+ ): Promise<string | undefined> {
33
+ if (!ctx.model) {return undefined;}
34
+
35
+ try {
36
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
37
+ if (!auth.ok || !auth.apiKey) {return undefined;}
38
+
39
+ // prompt
40
+ const message: Message = {
41
+ role: "user",
42
+ timestamp: Date.now(),
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: options.userText,
47
+ },
48
+ ],
49
+ };
50
+
51
+ // call model
52
+ const response = await complete(
53
+ ctx.model,
54
+ {
55
+ systemPrompt: options.systemPrompt,
56
+ messages: [message],
57
+ },
58
+ {
59
+ apiKey: auth.apiKey,
60
+ headers: auth.headers,
61
+ maxTokens: options.maxTokens,
62
+ signal: timeoutSignal(ctx.signal, options.timeoutMs),
63
+ }
64
+ );
65
+
66
+ if (response.stopReason === "aborted") {return undefined;}
67
+
68
+ // extract text
69
+ const text = response.content
70
+ .filter((content): content is { type: "text"; text: string } => content.type === "text")
71
+ .map((content) => content.text)
72
+ .join(options.joinWith ?? " ")
73
+ .trim();
74
+
75
+ return text || undefined;
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
@@ -0,0 +1,68 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { completeText } from "./ai-gen.ts";
3
+ import { buildPurposeInstructions, PURPOSE_SYSTEM_PROMPT } from "./prompts.ts";
4
+ import type { PermissionRequest } from "./types.ts";
5
+
6
+ const PURPOSE_TIMEOUT_MS = 5000;
7
+ const MAX_PURPOSE_LENGTH = 200;
8
+ const MAX_PURPOSE_TOKENS = 120;
9
+
10
+ const purposeCache = new Map<string, string>();
11
+
12
+ function cacheKey(request: PermissionRequest): string {
13
+ return `${request.toolName}\u0000${request.body}`;
14
+ }
15
+
16
+ /**
17
+ * Sanitizes against:
18
+ * - ANSI terminal escapes
19
+ * - Code blocks, markdown formatting characters, quotes
20
+ * - Consecutive whitespace
21
+ */
22
+ function sanitizePurpose(value: string): string | undefined {
23
+ const normalized = value
24
+ .replace(/\u001b\[[0-9;]*m/g, "")
25
+ .replace(/```[\s\S]*?```/g, " ")
26
+ .replace(/[`*_#>]/g, "")
27
+ .replace(/\s+/g, " ")
28
+ .trim()
29
+ .replace(/^["'“”‘’]+|["'“”‘’]+$/g, "")
30
+ .trim();
31
+
32
+ if (!normalized) {return undefined;}
33
+ return normalized.length > MAX_PURPOSE_LENGTH
34
+ ? `${normalized.slice(0, MAX_PURPOSE_LENGTH)}...`
35
+ : normalized;
36
+ }
37
+
38
+ /**
39
+ * Generates a request purpose, using the cached version if it exists.
40
+ */
41
+ export async function generateRequestPurpose(
42
+ ctx: ExtensionContext,
43
+ request: PermissionRequest,
44
+ context: string
45
+ ): Promise<string | undefined> {
46
+ const key = cacheKey(request);
47
+ const cached = purposeCache.get(key);
48
+ if (cached) {return cached;}
49
+
50
+ const text = await completeText(ctx, {
51
+ systemPrompt: PURPOSE_SYSTEM_PROMPT,
52
+ userText: buildPurposeInstructions(context),
53
+ maxTokens: MAX_PURPOSE_TOKENS,
54
+ timeoutMs: PURPOSE_TIMEOUT_MS,
55
+ });
56
+ if (!text) {return undefined;}
57
+
58
+ const purpose = sanitizePurpose(text);
59
+ if (purpose) {purposeCache.set(key, purpose);}
60
+ return purpose;
61
+ }
62
+
63
+ /**
64
+ * Clears the generated purpose cache.
65
+ */
66
+ export function clearPurposeCache(): void {
67
+ purposeCache.clear();
68
+ }
@@ -0,0 +1,88 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { completeText } from "./ai-gen.ts";
3
+ import {
4
+ buildWildcardInstructions,
5
+ buildWildcardListInstructions,
6
+ WILDCARD_LIST_SYSTEM_PROMPT,
7
+ WILDCARD_SYSTEM_PROMPT,
8
+ } from "./prompts.ts";
9
+ import {
10
+ validateWildcardSuggestion,
11
+ validateWildcardSuggestionList,
12
+ } from "./wildcard-validation.ts";
13
+
14
+ const WILDCARD_TIMEOUT_MS = 8000;
15
+ const MAX_WILDCARD_TOKENS = 120;
16
+ const MAX_WILDCARD_LIST_TOKENS = 240;
17
+
18
+ /**
19
+ * Sanitizes against:
20
+ * - ANSI terminal escapes
21
+ * - Code blocks, quotes, backticks
22
+ * - Consecutive whitespace
23
+ */
24
+ function sanitizeWildcard(value: string): string | undefined {
25
+ const normalized = value
26
+ .replace(/\u001b\[[0-9;]*m/g, "")
27
+ .replace(/```(?:\w*\n)?([\s\S]*?)```/g, "$1")
28
+ .replace(/\s+/g, " ")
29
+ .trim()
30
+ .replace(/^["'`]+|["'`]+$/g, "")
31
+ .trim();
32
+
33
+ if (!normalized) {return undefined;}
34
+ return normalized;
35
+ }
36
+
37
+ /**
38
+ * Generates one bash wildcard recommendation.
39
+ */
40
+ export async function generateBashWildcard(
41
+ ctx: ExtensionContext,
42
+ command: string
43
+ ): Promise<string | undefined> {
44
+ const text = await completeText(ctx, {
45
+ systemPrompt: WILDCARD_SYSTEM_PROMPT,
46
+ userText: buildWildcardInstructions(command),
47
+ maxTokens: MAX_WILDCARD_TOKENS,
48
+ timeoutMs: WILDCARD_TIMEOUT_MS,
49
+ joinWith: "\n",
50
+ });
51
+ if (!text) {return undefined;}
52
+
53
+ const pattern = sanitizeWildcard(text);
54
+ if (!pattern) {return undefined;}
55
+
56
+ const validation = validateWildcardSuggestion(pattern, command);
57
+ if (!validation.valid) {return undefined;}
58
+
59
+ return pattern;
60
+ }
61
+
62
+ /**
63
+ * Generates a list of bash wildcards.
64
+ * Expects semicolon-separated patterns back.
65
+ */
66
+ export async function generateBashWildcards(
67
+ ctx: ExtensionContext,
68
+ commands: readonly string[]
69
+ ): Promise<string[] | undefined> {
70
+ if (commands.length === 0) {return undefined;}
71
+
72
+ const text = await completeText(ctx, {
73
+ systemPrompt: WILDCARD_LIST_SYSTEM_PROMPT,
74
+ userText: buildWildcardListInstructions(commands),
75
+ maxTokens: MAX_WILDCARD_LIST_TOKENS,
76
+ timeoutMs: WILDCARD_TIMEOUT_MS,
77
+ joinWith: "\n",
78
+ });
79
+ if (!text) {return undefined;}
80
+
81
+ const sanitized = sanitizeWildcard(text);
82
+ if (!sanitized) {return undefined;}
83
+
84
+ const validation = validateWildcardSuggestionList(sanitized, commands);
85
+ if (!validation.valid) {return undefined;}
86
+
87
+ return validation.patterns;
88
+ }
package/src/config.ts ADDED
@@ -0,0 +1,187 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { ASK } from "./constants.ts";
3
+ import { parseJsonc } from "./jsonc.ts";
4
+ import {
5
+ createDefaultPermissionConfig,
6
+ toPermissionState,
7
+ toRecord,
8
+ type ConfigWarning,
9
+ type PermissionConfig,
10
+ type PermissionSection,
11
+ type PermissionState,
12
+ } from "./types.ts";
13
+ import {
14
+ CONFIG_MUST_BE_OBJECT,
15
+ formatConfigParseFailure,
16
+ formatConfigReadFailure,
17
+ formatIgnoredSectionEntryWarning,
18
+ formatIgnoredSectionWarning,
19
+ IGNORED_DEFAULT_WARNING,
20
+ } from "./warning-strings.ts";
21
+
22
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
23
+
24
+ /**
25
+ * Helper used by normalizeConfig.
26
+ * Ensures section is a valid PermissionSection.
27
+ */
28
+ function normalizeSection(
29
+ raw: unknown,
30
+ sectionName: string,
31
+ configPath: string | undefined,
32
+ warnings: ConfigWarning[]
33
+ ): PermissionSection {
34
+ if (raw === undefined) {return {};}
35
+ const record = toRecord(raw);
36
+ if (!record) {
37
+ warnings.push({
38
+ path: configPath,
39
+ message: formatIgnoredSectionWarning(sectionName),
40
+ });
41
+ return {};
42
+ }
43
+
44
+ const result: PermissionSection = {};
45
+ for (const [key, entry] of Object.entries(record)) {
46
+ if (UNSAFE_KEYS.has(key)) {continue;}
47
+ const state = toPermissionState(entry);
48
+ if (state !== undefined) {
49
+ result[key] = state;
50
+ } else {
51
+ warnings.push({
52
+ path: configPath,
53
+ message: formatIgnoredSectionEntryWarning(sectionName, key),
54
+ });
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Convert the raw config to a PermissionConfig.
62
+ * Ensures the config is valid.
63
+ */
64
+ function normalizeConfig(
65
+ rawConfig: unknown,
66
+ configPath: string | undefined,
67
+ warnings: ConfigWarning[]
68
+ ): PermissionConfig {
69
+ const config = toRecord(rawConfig);
70
+ if (!config) {
71
+ warnings.push({
72
+ path: configPath,
73
+ message: CONFIG_MUST_BE_OBJECT,
74
+ });
75
+ return createDefaultPermissionConfig();
76
+ }
77
+
78
+ const parsedDefault = toPermissionState(config.default);
79
+ if (config.default !== undefined && parsedDefault === undefined) {
80
+ warnings.push({
81
+ path: configPath,
82
+ message: IGNORED_DEFAULT_WARNING,
83
+ });
84
+ }
85
+ const defaultState = parsedDefault ?? ASK;
86
+
87
+ return {
88
+ default: defaultState,
89
+ tools: normalizeSection(config.tools, "tools", configPath, warnings),
90
+ bash: normalizeSection(config.bash, "bash", configPath, warnings),
91
+ external_directory: normalizeSection(
92
+ config.external_directory,
93
+ "external_directory",
94
+ configPath,
95
+ warnings
96
+ ),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Loads the config file and appends to the list of warnings.
102
+ */
103
+ async function loadConfigFile(
104
+ path: string,
105
+ warnings: ConfigWarning[]
106
+ ): Promise<PermissionConfig | undefined> {
107
+ let raw: string;
108
+ try {
109
+ raw = await readFile(path, "utf8");
110
+ } catch (error) {
111
+ const code = (error as NodeJS.ErrnoException).code;
112
+ if (code === "ENOENT") {return undefined;} // file does not exist
113
+ warnings.push({
114
+ path,
115
+ message: formatConfigReadFailure(error),
116
+ });
117
+ return undefined;
118
+ }
119
+
120
+ try {
121
+ return normalizeConfig(parseJsonc(raw), path, warnings);
122
+ } catch (error) {
123
+ warnings.push({
124
+ path,
125
+ message: formatConfigParseFailure(error),
126
+ });
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Local overrides global, preserves order.
133
+ */
134
+ function mergeBashSections(
135
+ globalSection: PermissionSection,
136
+ localSection: PermissionSection
137
+ ): PermissionSection {
138
+ const merged = new Map<string, PermissionState>();
139
+ for (const [pattern, state] of Object.entries(globalSection)) {
140
+ merged.set(pattern, state);
141
+ }
142
+ for (const [pattern, state] of Object.entries(localSection)) {
143
+ merged.delete(pattern);
144
+ merged.set(pattern, state);
145
+ }
146
+ return Object.fromEntries(merged);
147
+ }
148
+
149
+ /**
150
+ * Merges configs such that local config overrides global config.
151
+ */
152
+ export function mergeConfigs(
153
+ globalConfig: PermissionConfig | undefined,
154
+ localConfig: PermissionConfig | undefined
155
+ ): PermissionConfig {
156
+ if (!globalConfig && !localConfig) {
157
+ return createDefaultPermissionConfig();
158
+ }
159
+
160
+ globalConfig = globalConfig ?? createDefaultPermissionConfig();
161
+
162
+ return {
163
+ default: localConfig?.default ?? globalConfig.default,
164
+ tools: { ...globalConfig.tools, ...(localConfig?.tools ?? {}) },
165
+ bash: mergeBashSections(globalConfig.bash, localConfig?.bash ?? {}),
166
+ external_directory: {
167
+ ...globalConfig.external_directory,
168
+ ...(localConfig?.external_directory ?? {}),
169
+ },
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Loads merged config where local overrides global.
175
+ */
176
+ export async function loadMergedConfig(
177
+ globalPath: string,
178
+ localPath: string
179
+ ): Promise<{ config: PermissionConfig; warnings: ConfigWarning[] }> {
180
+ const warnings: ConfigWarning[] = [];
181
+ const globalConfig = await loadConfigFile(globalPath, warnings);
182
+ const localConfig = await loadConfigFile(localPath, warnings);
183
+ return {
184
+ config: mergeConfigs(globalConfig, localConfig),
185
+ warnings,
186
+ };
187
+ }
@@ -0,0 +1,18 @@
1
+ export const EXTENSION_NAME = "pi-permission-system";
2
+ export const STATUS_KEY = EXTENSION_NAME;
3
+ export const MAX_CONFIG_WARNINGS = 5;
4
+
5
+ export const CONFIG_FILENAME = "pi-permissions.jsonc";
6
+ /** Local config directory under the cwd. */
7
+ export const LOCAL_CONFIG_DIR_SEGMENTS = [".pi"] as const;
8
+
9
+ export const ALLOW = "allow";
10
+ export const ASK = "ask";
11
+ export const DENY = "deny";
12
+ export const PERMISSION_STATES = [ALLOW, ASK, DENY] as const;
13
+ export const PERMISSION_STATES_DESCRIPTION = "allow, ask, or deny";
14
+
15
+ export const ALLOW_ONCE = "Allow Once";
16
+ export const ALLOW_ALWAYS_EXACT = "Allow Always (exact match)";
17
+ export const ALLOW_ALWAYS_WILDCARD = "Allow Always (generate wildcard)";
18
+ export const REJECT = "Reject";