nofax 0.2.0 → 0.2.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/SECURITY.md CHANGED
@@ -1,144 +1,144 @@
1
- # Security Policy
2
-
3
- ## Supported versions
4
-
5
- Nofax is pre-1.0 software. Security fixes are applied to the latest release on the default branch.
6
-
7
- ## Reporting a vulnerability
8
-
9
- Do not publish credentials, exploit details, private ntfy topics, one-time response URLs, remote MCP capability URLs, or sensitive hook/MCP payloads in a public issue.
10
-
11
- If GitHub private vulnerability reporting is enabled for this repository, use it. Otherwise, open a minimal public issue asking for a private maintainer contact channel without including sensitive details.
12
-
13
- ## Local Nofax security
14
-
15
- ### Public ntfy is not end-to-end encryption
16
-
17
- With the default `https://ntfy.sh` configuration, notification content transits and may be cached by the ntfy service. A random topic name reduces unauthorized discovery but does not encrypt content from the service operator.
18
-
19
- Use a trusted authenticated/self-hosted ntfy server for sensitive source code, production operations, credentials, regulated data, or confidential prompts.
20
-
21
- ### Topics and one-time response URLs are capabilities
22
-
23
- On anonymous ntfy servers, knowledge of a topic can be sufficient to subscribe or publish. Treat the local ntfy topic as a bearer secret.
24
-
25
- Local Nofax generates fresh one-time response topics for interactive requests. Do not log, share, bookmark, or persist those callback URLs outside the state Nofax itself requires.
26
-
27
- ### Durable local request state
28
-
29
- Local MCP human-response requests are stored under `~/.nofax/requests/` so an MCP/client restart does not erase an unresolved human decision gate.
30
-
31
- Nofax writes these files with user-only permissions where supported. MCP-facing request projections deliberately omit the secret response topic. Protect the Nofax home directory like other local application state.
32
-
33
- ### Pending never means approved
34
-
35
- A local interactive request remains pending until a matching terminal response is accepted. Passage of time, timeout, tool failure, network failure, or client disconnect never means approval.
36
-
37
- `nofax_wait_for_response` uses bounded long-polls. If it returns pending, the caller must wait again rather than continue the guarded action.
38
-
39
- ### First terminal response wins
40
-
41
- Local durable requests are single-use. Once a valid terminal response is accepted, later responses cannot intentionally replace it.
42
-
43
- ### Native hook failures never become approval
44
-
45
- The Claude Code and Codex adapters fail closed. If the local transport times out, returns malformed data, or encounters a network/polling error, Nofax does not synthesize an Allow decision.
46
-
47
- ### Redaction is best-effort
48
-
49
- Nofax redacts values under common secret-bearing object keys and bounds serialized payloads. It cannot reliably detect a credential embedded in arbitrary free-form command text. Treat all notification content accordingly.
50
-
51
- ## Remote Worker security
52
-
53
- The optional Cloudflare Worker in v0.3 is a **one-way notification plus inspection remote MCP endpoint**.
54
-
55
- ### Remote side effects are structurally bounded
56
-
57
- MCP tool annotations are descriptive metadata, not the security boundary.
58
-
59
- The remote Worker bounds side effects in code:
60
-
61
- - only `nofax_notify`, `nofax_get_request`, and `nofax_list_pending` are registered as MCP tools;
62
- - the remote handler object exposes one-way notification plus two read operations;
63
- - there is no remote approval, choice, refinement, wait, callback, or webhook handler;
64
- - public Worker routing is limited to health and authenticated MCP paths;
65
- - `/telegram/webhook` and `/r/*` are not routes and return 404;
66
- - pending-list reads filter expired rows without deleting them or performing hidden cleanup writes.
67
-
68
- The two inspection tools are annotated with `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, and `openWorldHint: false`. `nofax_notify` is accurately marked side-effecting/non-idempotent/open-world and non-destructive.
69
-
70
- ### Remote response minimization
71
-
72
- Remote MCP responses deliberately omit capability-bearing or unnecessary fields. Public request projections do not expose:
73
-
74
- - callback hashes;
75
- - callback tokens/URLs;
76
- - original request title;
77
- - original request message/prompt material;
78
- - internal allowed-decision lists.
79
-
80
- Terminal state may include the stored terminal decision and historical refinement text when present in existing durable state.
81
-
82
- ### Remote MCP key is a bearer secret
83
-
84
- The Worker is a private single-user deployment protected by `NOFAX_REMOTE_KEY`.
85
-
86
- Preferred clients send:
87
-
88
- ```text
89
- Authorization: Bearer <NOFAX_REMOTE_KEY>
90
- ```
91
-
92
- Clients that cannot attach a static header may use:
93
-
94
- ```text
95
- /mcp/<NOFAX_REMOTE_KEY>
96
- ```
97
-
98
- The complete capability URL is equivalent to a password and may leak through browser history, screenshots, copied configuration, proxies, or third-party logging. Prefer the Authorization header whenever the MCP host supports it.
99
-
100
- Nofax uses equal-length constant-time credential comparison. After capability-path authentication, the Worker normalizes the request internally to `/mcp` before MCP protocol handling.
101
-
102
- Rotate `NOFAX_REMOTE_KEY` immediately if it is exposed.
103
-
104
- A single deployment-wide key is not sufficient for a shared/public multi-user service. Use a delegated authentication and authorization design before operating a multi-user deployment.
105
-
106
- ### Durable Object compatibility
107
-
108
- The v0.3 Worker preserves the existing SQLite request schema so upgrading from experimental pre-read-only Worker builds does not require destructive storage migration.
109
-
110
- Only request-inspection methods are reachable for Durable Object state. Existing legacy rows may therefore be inspected after upgrade, but the Worker cannot create, resolve, or delete them through MCP or HTTP routes. The separate `nofax_notify` method can publish an explicitly requested one-way notification and does not mutate request state.
111
-
112
- ### Cloudflare is the remote trust boundary
113
-
114
- Remote mode adds Cloudflare as an infrastructure boundary. For notification calls, ntfy is an additional provider boundary and receives the bounded notification title/message plus the configured topic identifier.
115
-
116
- The Worker does not automatically forward durable request records to ntfy. Only explicit `nofax_notify` content is published; Telegram, WhatsApp, SMS, and human-response callbacks remain absent remotely.
117
-
118
- Do not expose the deployment key in source, Wrangler vars, `.env`, `.dev.vars`, CI logs, PR text, screenshots, or issue reports. Use Wrangler secrets for production values and keep local secret files untracked.
119
-
120
- ### Hosted ntfy quotas are an external availability boundary
121
-
122
- The public `ntfy.sh` service applies its own publisher limits independently of Cloudflare Workers quotas. Serverless egress may be shared between unrelated workloads, so a Cloudflare Worker can receive an ntfy `42908` daily-quota response even when that individual Worker has published little traffic.
123
-
124
- Treat public ntfy availability and quota policy as an external dependency. Reliability-sensitive deployments should use a transport with account-scoped quota/identity or a trusted self-hosted service rather than assuming anonymous public-topic capacity.
125
-
126
- ## Nofax is not a policy engine
127
-
128
- Local Nofax answers approval requests that an upstream agent or workflow explicitly delegates to it. It does not decide which operations should require approval and must not be used to bypass an agent's deny rules, sandbox, or existing authorization boundaries.
129
-
130
- An `allow` result permits the caller to continue only within authority it already possessed.
131
-
132
- The remote Worker does not grant authority. Its only mutation is sending an informational notification; request-state operations remain inspection-only.
133
-
134
- ## MCP transport notes
135
-
136
- ### Local stdio
137
-
138
- `nofax mcp` is a local stdio server. Stdout is reserved for MCP protocol traffic. Do not wrap it with tooling that injects banners or diagnostics into stdout.
139
-
140
- ### Remote Streamable HTTP
141
-
142
- The optional Worker uses authenticated Streamable HTTP for MCP. The only unauthenticated functional route is the secret-free `/healthz` health check.
143
-
144
- See [`docs/remote-mcp.md`](docs/remote-mcp.md) for the remote architecture and qualification checklist.
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Nofax is pre-1.0 software. Security fixes are applied to the latest release on the default branch.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Do not publish credentials, exploit details, private ntfy topics, one-time response URLs, remote MCP capability URLs, or sensitive hook/MCP payloads in a public issue.
10
+
11
+ If GitHub private vulnerability reporting is enabled for this repository, use it. Otherwise, open a minimal public issue asking for a private maintainer contact channel without including sensitive details.
12
+
13
+ ## Local Nofax security
14
+
15
+ ### Public ntfy is not end-to-end encryption
16
+
17
+ With the default `https://ntfy.sh` configuration, notification content transits and may be cached by the ntfy service. A random topic name reduces unauthorized discovery but does not encrypt content from the service operator.
18
+
19
+ Use a trusted authenticated/self-hosted ntfy server for sensitive source code, production operations, credentials, regulated data, or confidential prompts.
20
+
21
+ ### Topics and one-time response URLs are capabilities
22
+
23
+ On anonymous ntfy servers, knowledge of a topic can be sufficient to subscribe or publish. Treat the local ntfy topic as a bearer secret.
24
+
25
+ Local Nofax generates fresh one-time response topics for interactive requests. Do not log, share, bookmark, or persist those callback URLs outside the state Nofax itself requires.
26
+
27
+ ### Durable local request state
28
+
29
+ Local MCP human-response requests are stored under `~/.nofax/requests/` so an MCP/client restart does not erase an unresolved human decision gate.
30
+
31
+ Nofax writes these files with user-only permissions where supported. MCP-facing request projections deliberately omit the secret response topic. Protect the Nofax home directory like other local application state.
32
+
33
+ ### Pending never means approved
34
+
35
+ A local interactive request remains pending until a matching terminal response is accepted. Passage of time, timeout, tool failure, network failure, or client disconnect never means approval.
36
+
37
+ `nofax_wait_for_response` uses bounded long-polls. If it returns pending, the caller must wait again rather than continue the guarded action.
38
+
39
+ ### First terminal response wins
40
+
41
+ Local durable requests are single-use. Once a valid terminal response is accepted, later responses cannot intentionally replace it.
42
+
43
+ ### Native hook failures never become approval
44
+
45
+ The Claude Code and Codex adapters fail closed. If the local transport times out, returns malformed data, or encounters a network/polling error, Nofax does not synthesize an Allow decision.
46
+
47
+ ### Redaction is best-effort
48
+
49
+ Nofax redacts values under common secret-bearing object keys and bounds serialized payloads. It cannot reliably detect a credential embedded in arbitrary free-form command text. Treat all notification content accordingly.
50
+
51
+ ## Remote Worker security
52
+
53
+ The optional Cloudflare Worker in v0.3 is a **one-way notification plus inspection remote MCP endpoint**.
54
+
55
+ ### Remote side effects are structurally bounded
56
+
57
+ MCP tool annotations are descriptive metadata, not the security boundary.
58
+
59
+ The remote Worker bounds side effects in code:
60
+
61
+ - only `nofax_notify`, `nofax_get_request`, and `nofax_list_pending` are registered as MCP tools;
62
+ - the remote handler object exposes one-way notification plus two read operations;
63
+ - there is no remote approval, choice, refinement, wait, callback, or webhook handler;
64
+ - public Worker routing is limited to health and authenticated MCP paths;
65
+ - `/telegram/webhook` and `/r/*` are not routes and return 404;
66
+ - pending-list reads filter expired rows without deleting them or performing hidden cleanup writes.
67
+
68
+ The two inspection tools are annotated with `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, and `openWorldHint: false`. `nofax_notify` is accurately marked side-effecting/non-idempotent/open-world and non-destructive.
69
+
70
+ ### Remote response minimization
71
+
72
+ Remote MCP responses deliberately omit capability-bearing or unnecessary fields. Public request projections do not expose:
73
+
74
+ - callback hashes;
75
+ - callback tokens/URLs;
76
+ - original request title;
77
+ - original request message/prompt material;
78
+ - internal allowed-decision lists.
79
+
80
+ Terminal state may include the stored terminal decision and historical refinement text when present in existing durable state.
81
+
82
+ ### Remote MCP key is a bearer secret
83
+
84
+ The Worker is a private single-user deployment protected by `NOFAX_REMOTE_KEY`.
85
+
86
+ Preferred clients send:
87
+
88
+ ```text
89
+ Authorization: Bearer <NOFAX_REMOTE_KEY>
90
+ ```
91
+
92
+ Clients that cannot attach a static header may use:
93
+
94
+ ```text
95
+ /mcp/<NOFAX_REMOTE_KEY>
96
+ ```
97
+
98
+ The complete capability URL is equivalent to a password and may leak through browser history, screenshots, copied configuration, proxies, or third-party logging. Prefer the Authorization header whenever the MCP host supports it.
99
+
100
+ Nofax uses equal-length constant-time credential comparison. After capability-path authentication, the Worker normalizes the request internally to `/mcp` before MCP protocol handling.
101
+
102
+ Rotate `NOFAX_REMOTE_KEY` immediately if it is exposed.
103
+
104
+ A single deployment-wide key is not sufficient for a shared/public multi-user service. Use a delegated authentication and authorization design before operating a multi-user deployment.
105
+
106
+ ### Durable Object compatibility
107
+
108
+ The v0.3 Worker preserves the existing SQLite request schema so upgrading from experimental pre-read-only Worker builds does not require destructive storage migration.
109
+
110
+ Only request-inspection methods are reachable for Durable Object state. Existing legacy rows may therefore be inspected after upgrade, but the Worker cannot create, resolve, or delete them through MCP or HTTP routes. The separate `nofax_notify` method can publish an explicitly requested one-way notification and does not mutate request state.
111
+
112
+ ### Cloudflare is the remote trust boundary
113
+
114
+ Remote mode adds Cloudflare as an infrastructure boundary. For notification calls, ntfy is an additional provider boundary and receives the bounded notification title/message plus the configured topic identifier.
115
+
116
+ The Worker does not automatically forward durable request records to ntfy. Only explicit `nofax_notify` content is published; Telegram, WhatsApp, SMS, and human-response callbacks remain absent remotely.
117
+
118
+ Do not expose the deployment key in source, Wrangler vars, `.env`, `.dev.vars`, CI logs, PR text, screenshots, or issue reports. Use Wrangler secrets for production values and keep local secret files untracked.
119
+
120
+ ### Hosted ntfy quotas are an external availability boundary
121
+
122
+ The public `ntfy.sh` service applies its own publisher limits independently of Cloudflare Workers quotas. Serverless egress may be shared between unrelated workloads, so a Cloudflare Worker can receive an ntfy `42908` daily-quota response even when that individual Worker has published little traffic.
123
+
124
+ Treat public ntfy availability and quota policy as an external dependency. Reliability-sensitive deployments should use a transport with account-scoped quota/identity or a trusted self-hosted service rather than assuming anonymous public-topic capacity.
125
+
126
+ ## Nofax is not a policy engine
127
+
128
+ Local Nofax answers approval requests that an upstream agent or workflow explicitly delegates to it. It does not decide which operations should require approval and must not be used to bypass an agent's deny rules, sandbox, or existing authorization boundaries.
129
+
130
+ An `allow` result permits the caller to continue only within authority it already possessed.
131
+
132
+ The remote Worker does not grant authority. Its only mutation is sending an informational notification; request-state operations remain inspection-only.
133
+
134
+ ## MCP transport notes
135
+
136
+ ### Local stdio
137
+
138
+ `nofax mcp` is a local stdio server. Stdout is reserved for MCP protocol traffic. Do not wrap it with tooling that injects banners or diagnostics into stdout.
139
+
140
+ ### Remote Streamable HTTP
141
+
142
+ The optional Worker uses authenticated Streamable HTTP for MCP. The only unauthenticated functional route is the secret-free `/healthz` health check.
143
+
144
+ See [`docs/remote-mcp.md`](docs/remote-mcp.md) for the remote architecture and qualification checklist.
package/bin/nofax.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
2
- import { runCli } from '../src/cli.mjs';
3
-
4
- process.exitCode = await runCli(process.argv.slice(2));
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../src/cli.mjs';
3
+
4
+ process.exitCode = await runCli(process.argv.slice(2));
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "nofax",
3
- "version": "0.2.0",
4
- "mcpName": "io.github.AKzar1el/nofax",
5
- "description": "Human-in-the-loop approvals and notifications for AI coding agents via CLI, MCP, and agent hooks.",
6
- "type": "module",
7
- "bin": {
8
- "nofax": "./bin/nofax.mjs"
9
- },
10
- "exports": {
11
- ".": "./src/index.mjs",
12
- "./mcp": "./src/mcp-server.mjs"
13
- },
14
- "files": [
15
- "bin/",
16
- "src/",
17
- "README.md",
18
- "LICENSE",
19
- "SECURITY.md"
20
- ],
21
- "scripts": {
22
- "test": "node --test test/*.test.mjs",
23
- "check": "node --check bin/nofax.mjs && node --check src/cli.mjs && node --check src/config.mjs && node --check src/protocol.mjs && node --check src/ntfy.mjs && node --check src/requests.mjs && node --check src/mcp-tools.mjs && node --check src/mcp-server.mjs && node --check src/adapters/claude.mjs && node --check src/adapters/codex.mjs && node --check src/adapters/gemini.mjs"
24
- },
25
- "engines": {
26
- "node": ">=20.0.0"
27
- },
28
- "dependencies": {
29
- "@modelcontextprotocol/server": "2.0.0",
30
- "zod": "4.4.3"
31
- },
32
- "keywords": [
33
- "ai-agent",
34
- "approval",
35
- "claude-code",
36
- "codex",
37
- "gemini-cli",
38
- "human-in-the-loop",
39
- "mcp",
40
- "model-context-protocol",
41
- "ntfy",
42
- "notifications",
43
- "remote",
44
- "developer-tools"
45
- ],
46
- "license": "MIT",
47
- "author": "Tomi Šeregi",
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://github.com/AKzar1el/nofax.git"
51
- },
52
- "homepage": "https://github.com/AKzar1el/nofax#readme",
53
- "bugs": {
54
- "url": "https://github.com/AKzar1el/nofax/issues"
55
- }
56
- }
1
+ {
2
+ "name": "nofax",
3
+ "version": "0.2.1",
4
+ "mcpName": "io.github.AKzar1el/nofax",
5
+ "description": "Human-in-the-loop approvals and notifications for AI coding agents via CLI, MCP, and agent hooks.",
6
+ "type": "module",
7
+ "bin": {
8
+ "nofax": "./bin/nofax.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.mjs",
12
+ "./mcp": "./src/mcp-server.mjs"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "README.md",
18
+ "LICENSE",
19
+ "SECURITY.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.test.mjs",
23
+ "check": "node --check bin/nofax.mjs && node --check src/cli.mjs && node --check src/config.mjs && node --check src/protocol.mjs && node --check src/ntfy.mjs && node --check src/requests.mjs && node --check src/mcp-tools.mjs && node --check src/mcp-server.mjs && node --check src/adapters/claude.mjs && node --check src/adapters/codex.mjs && node --check src/adapters/gemini.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/server": "2.0.0",
30
+ "zod": "4.4.3"
31
+ },
32
+ "keywords": [
33
+ "ai-agent",
34
+ "approval",
35
+ "claude-code",
36
+ "codex",
37
+ "gemini-cli",
38
+ "human-in-the-loop",
39
+ "mcp",
40
+ "model-context-protocol",
41
+ "ntfy",
42
+ "notifications",
43
+ "remote",
44
+ "developer-tools"
45
+ ],
46
+ "license": "MIT",
47
+ "author": "Tomi Šeregi",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/AKzar1el/nofax.git"
51
+ },
52
+ "homepage": "https://github.com/AKzar1el/nofax#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/AKzar1el/nofax/issues"
55
+ }
56
+ }
@@ -1,47 +1,47 @@
1
- import { buildAgentSummary } from '../protocol.mjs';
2
- import { requestApproval } from '../ntfy.mjs';
3
-
4
- function assertClaudePermissionRequest(input) {
5
- if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CLAUDE_EVENT');
6
- if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CLAUDE_TOOL');
7
- }
8
-
9
- export async function handleClaudePermissionRequest(input, {
10
- config,
11
- requestApprovalImpl = requestApproval,
12
- onError = () => {}
13
- } = {}) {
14
- assertClaudePermissionRequest(input);
15
- try {
16
- const result = await requestApprovalImpl({
17
- config,
18
- title: `Claude Code needs approval: ${input.tool_name}`,
19
- message: buildAgentSummary({
20
- source: 'Claude Code',
21
- toolName: input.tool_name,
22
- cwd: input.cwd,
23
- toolInput: input.tool_input
24
- })
25
- });
26
- if (result.decision === 'allow') {
27
- return {
28
- hookSpecificOutput: {
29
- hookEventName: 'PermissionRequest',
30
- decision: { behavior: 'allow' }
31
- }
32
- };
33
- }
34
- if (result.decision === 'deny') {
35
- return {
36
- hookSpecificOutput: {
37
- hookEventName: 'PermissionRequest',
38
- decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
- }
40
- };
41
- }
42
- return null;
43
- } catch (error) {
44
- onError(error);
45
- return null;
46
- }
47
- }
1
+ import { buildAgentSummary } from '../protocol.mjs';
2
+ import { requestApproval } from '../ntfy.mjs';
3
+
4
+ function assertClaudePermissionRequest(input) {
5
+ if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CLAUDE_EVENT');
6
+ if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CLAUDE_TOOL');
7
+ }
8
+
9
+ export async function handleClaudePermissionRequest(input, {
10
+ config,
11
+ requestApprovalImpl = requestApproval,
12
+ onError = () => {}
13
+ } = {}) {
14
+ assertClaudePermissionRequest(input);
15
+ try {
16
+ const result = await requestApprovalImpl({
17
+ config,
18
+ title: `Claude Code needs approval: ${input.tool_name}`,
19
+ message: buildAgentSummary({
20
+ source: 'Claude Code',
21
+ toolName: input.tool_name,
22
+ cwd: input.cwd,
23
+ toolInput: input.tool_input
24
+ })
25
+ });
26
+ if (result.decision === 'allow') {
27
+ return {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'PermissionRequest',
30
+ decision: { behavior: 'allow' }
31
+ }
32
+ };
33
+ }
34
+ if (result.decision === 'deny') {
35
+ return {
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PermissionRequest',
38
+ decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
+ }
40
+ };
41
+ }
42
+ return null;
43
+ } catch (error) {
44
+ onError(error);
45
+ return null;
46
+ }
47
+ }
@@ -1,47 +1,47 @@
1
- import { buildAgentSummary } from '../protocol.mjs';
2
- import { requestApproval } from '../ntfy.mjs';
3
-
4
- function assertCodexPermissionRequest(input) {
5
- if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CODEX_EVENT');
6
- if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CODEX_TOOL');
7
- }
8
-
9
- export async function handleCodexPermissionRequest(input, {
10
- config,
11
- requestApprovalImpl = requestApproval,
12
- onError = () => {}
13
- } = {}) {
14
- assertCodexPermissionRequest(input);
15
- try {
16
- const result = await requestApprovalImpl({
17
- config,
18
- title: `Codex needs approval: ${input.tool_name}`,
19
- message: buildAgentSummary({
20
- source: 'Codex',
21
- toolName: input.tool_name,
22
- cwd: input.cwd,
23
- toolInput: input.tool_input
24
- })
25
- });
26
- if (result.decision === 'allow') {
27
- return {
28
- hookSpecificOutput: {
29
- hookEventName: 'PermissionRequest',
30
- decision: { behavior: 'allow' }
31
- }
32
- };
33
- }
34
- if (result.decision === 'deny') {
35
- return {
36
- hookSpecificOutput: {
37
- hookEventName: 'PermissionRequest',
38
- decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
- }
40
- };
41
- }
42
- return null;
43
- } catch (error) {
44
- onError(error);
45
- return null;
46
- }
47
- }
1
+ import { buildAgentSummary } from '../protocol.mjs';
2
+ import { requestApproval } from '../ntfy.mjs';
3
+
4
+ function assertCodexPermissionRequest(input) {
5
+ if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CODEX_EVENT');
6
+ if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CODEX_TOOL');
7
+ }
8
+
9
+ export async function handleCodexPermissionRequest(input, {
10
+ config,
11
+ requestApprovalImpl = requestApproval,
12
+ onError = () => {}
13
+ } = {}) {
14
+ assertCodexPermissionRequest(input);
15
+ try {
16
+ const result = await requestApprovalImpl({
17
+ config,
18
+ title: `Codex needs approval: ${input.tool_name}`,
19
+ message: buildAgentSummary({
20
+ source: 'Codex',
21
+ toolName: input.tool_name,
22
+ cwd: input.cwd,
23
+ toolInput: input.tool_input
24
+ })
25
+ });
26
+ if (result.decision === 'allow') {
27
+ return {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'PermissionRequest',
30
+ decision: { behavior: 'allow' }
31
+ }
32
+ };
33
+ }
34
+ if (result.decision === 'deny') {
35
+ return {
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PermissionRequest',
38
+ decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
+ }
40
+ };
41
+ }
42
+ return null;
43
+ } catch (error) {
44
+ onError(error);
45
+ return null;
46
+ }
47
+ }