nofax 0.2.0 → 0.2.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/LICENSE +21 -21
- package/README.md +349 -351
- package/SECURITY.md +144 -144
- package/bin/nofax.mjs +4 -4
- package/package.json +56 -56
- package/src/adapters/claude.mjs +47 -47
- package/src/adapters/codex.mjs +47 -47
- package/src/adapters/gemini.mjs +57 -57
- package/src/cli.mjs +207 -191
- package/src/config.mjs +91 -91
- package/src/index.mjs +8 -8
- package/src/mcp-server.mjs +141 -141
- package/src/mcp-tools.mjs +240 -240
- package/src/ntfy.mjs +259 -259
- package/src/protocol.mjs +108 -108
- package/src/requests.mjs +160 -120
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.
|
|
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.2",
|
|
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
|
+
}
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -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
|
+
}
|