rynster 0.1.0 → 0.1.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/CHANGELOG.md +7 -0
- package/README.md +152 -270
- package/SECURITY.md +28 -19
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Rynster release notes
|
|
2
2
|
|
|
3
|
+
## 0.1.1 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
Documentation and packaging update. No change to supported runtimes or the
|
|
6
|
+
permission model.
|
|
7
|
+
|
|
8
|
+
- Update the README's release-file install example to the current version.
|
|
9
|
+
|
|
3
10
|
## 0.1.0 — 2026-10-18
|
|
4
11
|
|
|
5
12
|
Rynster connects MCP clients to local coding projects through stdio or an
|
package/README.md
CHANGED
|
@@ -1,356 +1,238 @@
|
|
|
1
|
-
# Rynster
|
|
1
|
+
# Rynster
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Let your AI assistant work on your local code projects, with you in control.**
|
|
4
4
|
|
|
5
|
-
Rynster connects
|
|
5
|
+
Rynster is a Provider-neutral local tool that connects AI clients (such as Claude or ChatGPT) to projects on your computer through [MCP](https://modelcontextprotocol.io). The assistant can read code, search files, review Git changes, and propose edits. You decide what it is allowed to do, and you approve changes before they are written.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
┌──────────────────────────────┐
|
|
13
|
-
│ Rynster Server │
|
|
14
|
-
│ │
|
|
15
|
-
│ MCP Router │
|
|
16
|
-
│ Session Manager │
|
|
17
|
-
│ Permission Engine │
|
|
18
|
-
│ Path & Secret Guards │
|
|
19
|
-
│ Tool Executor │
|
|
20
|
-
│ Git Worktree Manager │
|
|
21
|
-
│ Audit Logger │
|
|
22
|
-
└──────────────┬───────────────┘
|
|
23
|
-
│
|
|
24
|
-
▼
|
|
25
|
-
┌──────────────────────────────┐
|
|
26
|
-
│ Isolated Worktree / Project │
|
|
27
|
-
│ │
|
|
28
|
-
│ src/ │
|
|
29
|
-
│ package.json │
|
|
30
|
-
│ ... │
|
|
31
|
-
└──────────────────────────────┘
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Key Features
|
|
37
|
-
|
|
38
|
-
1. **Project File Boundaries (`PathGuard`)**:
|
|
39
|
-
- Resolves canonical file paths and handles symbolic links.
|
|
40
|
-
- Blocks path traversal attacks (`../`, `../../`, `/etc/passwd`, symlinks escaping project root).
|
|
41
|
-
- Projects are registered by ID; paths are checked against their registered root.
|
|
42
|
-
|
|
43
|
-
2. **Protected Secrets (`SecretGuard`)**:
|
|
44
|
-
- Automatically blocks read/write/search access to `.env*`, `*.pem`, `*.key`, `*.keystore`, `*.jks`, `credentials*`, `service-account*.json`, `id_rsa*`, etc.
|
|
45
|
-
- Blocks Rynster configuration, active OAuth/audit storage, `.aws`, `.ssh`, `.codex`, `.agents`, `.rynster`, alongside `.git`, `node_modules`, `.agent`, `.gradle`, and `Pods`. Custom `protectedFiles` paths/globs also apply to project tools.
|
|
46
|
-
|
|
47
|
-
3. **Optimistic Locking (`SHA-256 Revision Tracking`)**:
|
|
48
|
-
- Every file read returns a SHA-256 revision hash.
|
|
49
|
-
- `expectedRevision` is checked before proposing an edit. The captured contents, file identity, and directory identities are checked again after approval. Changed files abort with `FILE_CHANGED`; unsafe paths are rejected.
|
|
50
|
-
|
|
51
|
-
4. **Safe Editing & Approval Workflow (`project_patch`)**:
|
|
52
|
-
- Generates unified diffs and addition/deletion statistics (`+X / -Y`).
|
|
53
|
-
- Terminal approval prompt (`[y] approve`, `[n] reject`, `[d] full diff`, `[a] allow writes this session`).
|
|
54
|
-
- Operations requiring interactive approval fail closed in non-interactive environments.
|
|
55
|
-
|
|
56
|
-
5. **Git Worktree Isolation (`WorktreeManager`)**:
|
|
57
|
-
- Sessions can spin up dedicated git worktrees (`.agent/worktrees/<session-id>`) on isolated branches (`agent/<session-id>`).
|
|
58
|
-
- A requested worktree must be created successfully; failure stops the session. Workflow handles, defaults, and session write permissions are scoped to the authenticated caller and physical project root.
|
|
59
|
-
|
|
60
|
-
6. **Restricted Command Runner**:
|
|
61
|
-
- Predefined project command categories only (`typecheck`, `lint`, `test`, `format`, `build`), with local approval by default.
|
|
62
|
-
- Tools accept predefined command categories rather than arbitrary command strings.
|
|
63
|
-
- Commands use an executable and argument array with `shell: false`. Simple legacy strings are accepted; shell operators/substitutions require migration to an explicitly configured local script.
|
|
64
|
-
- A small inherited environment excludes credentials and runtime injection variables.
|
|
65
|
-
- Non-interactive settings (`CI=true`, `TERM=dumb`), command-specific timeouts, and output capping (100 KB per stream). Cancellation and limits terminate the process group on Linux. Automatic typechecking uses installed TypeScript; Rynster never downloads it through `npx`.
|
|
66
|
-
- Configured scripts execute as the host user; path guards and environment filtering do not provide OS-level process isolation.
|
|
7
|
+
- **Bring your own AI.** Rynster is not an AI model and does not need an AI subscription of its own.
|
|
8
|
+
- **You set the rules.** Reading, editing, and running commands each have their own permission.
|
|
9
|
+
- **You review every change.** Edits show a diff and can require your approval in the terminal.
|
|
10
|
+
- **Secrets stay protected.** Common credential files and sensitive folders are blocked by default.
|
|
11
|
+
- **Git-aware.** Check status and diffs, and keep an assistant's work in an isolated worktree.
|
|
67
12
|
|
|
68
|
-
|
|
69
|
-
- **Local stdio** and **HTTP at `/mcp`**, using SDK v2 and protocol `2026-07-28`.
|
|
70
|
-
- Legacy initialization, `/sse`, `/messages`, and root MCP aliases are retired.
|
|
71
|
-
- Application editing session IDs are workflow handles bound to the owner token, OAuth grant, or local stdio caller. Token renewal retains grant ownership; another grant cannot list or use its sessions.
|
|
72
|
-
|
|
73
|
-
8. **Audit Logging**:
|
|
74
|
-
- Records operation metadata and patch statistics into private audit storage. Queries, command arguments, source contents, bearer tokens, and URL queries are excluded from routine logs.
|
|
13
|
+
> **Important:** Rynster is not a sandbox. Project scripts run with your user's file and network access. Review code before approving any command. See [Local security policy](SECURITY.md).
|
|
75
14
|
|
|
76
15
|
---
|
|
77
16
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
- **HTTP MCP always requires authentication.** The `--no-auth` and `--auto-approve` CLI flags have been removed. HTTP tool requests must carry a valid Bearer token or authenticated OAuth access token.
|
|
81
|
-
- Old configuration files with `server.noAuth: true` or `server.autoApprove: true` will fail HTTP startup with a security error. Remove those settings (including stale values set to `true`). Stdio mode also refuses legacy `server.autoApprove: true`.
|
|
82
|
-
- An action governed by the `ask` policy requires approval at the local terminal. In a headless process without interactive approval, such actions are denied; an explicit `allow` policy may still permit its corresponding action.
|
|
83
|
-
- Loopback HTTP (`127.0.0.1`) can still be published through a tunnel or reverse proxy. Do not interpret loopback binding alone as Internet isolation or readiness for production.
|
|
84
|
-
- Local security acceptance is covered by automated hostile-client and filesystem fixtures on Linux and Windows, with tested runtimes recorded in the compatibility report. Real Claude/ChatGPT token renewal, issuer-change, and revocation acceptance remain tracked separately. See [Local security policy](SECURITY.md).
|
|
85
|
-
|
|
86
|
-
## Quickstart
|
|
17
|
+
## Quick start
|
|
87
18
|
|
|
88
|
-
### 1. Install
|
|
19
|
+
### 1. Install
|
|
89
20
|
|
|
90
|
-
|
|
21
|
+
Requires Node.js 22.17+ (22.x) or 24.2+ (24.x), and npm 11+.
|
|
91
22
|
|
|
92
23
|
```bash
|
|
93
|
-
npm install
|
|
24
|
+
npm install -g rynster
|
|
94
25
|
rynster --version
|
|
95
26
|
```
|
|
96
27
|
|
|
97
|
-
|
|
28
|
+
To install from a downloaded release file instead, run `npm install -g ./rynster-0.1.1.tgz`.
|
|
29
|
+
|
|
30
|
+
### 2. Register a project
|
|
31
|
+
|
|
32
|
+
Use your own project name and the absolute path to the project folder.
|
|
98
33
|
|
|
99
34
|
```bash
|
|
100
|
-
|
|
35
|
+
rynster register my-app /path/to/my-app
|
|
36
|
+
rynster projects # list registered projects
|
|
101
37
|
```
|
|
102
38
|
|
|
103
|
-
|
|
104
|
-
and changes are recorded in [the release notes](CHANGELOG.md). Report security
|
|
105
|
-
vulnerabilities privately to <baskar12ai@gmail.com>; see the
|
|
106
|
-
[Local security policy](SECURITY.md) for reporting instructions.
|
|
39
|
+
### 3. Connect your AI client
|
|
107
40
|
|
|
108
|
-
|
|
109
|
-
Owner-token fallback and audit files use `.agent/`. OAuth grants use
|
|
110
|
-
`~/.rynster/oauth.json` by default. Use `RYNSTER_PUBLIC_URL`,
|
|
111
|
-
`RYNSTER_OAUTH_STORAGE_PATH`, and `RYNSTER_TOKEN` for environment configuration.
|
|
112
|
-
This development release starts fresh without importing earlier installations.
|
|
41
|
+
Pick the option your client supports.
|
|
113
42
|
|
|
114
|
-
|
|
43
|
+
#### Option A: Local (stdio)
|
|
115
44
|
|
|
116
|
-
|
|
117
|
-
platform-specific optional npm package such as `@vscode/ripgrep-linux-x64`.
|
|
118
|
-
The current dependency ships the binary inside that package; it does not download
|
|
119
|
-
it from GitHub in a postinstall script.
|
|
120
|
-
|
|
121
|
-
If installation or startup reports a missing ripgrep platform package, check that
|
|
122
|
-
your npm registry or corporate mirror provides the matching `@vscode/ripgrep-*`
|
|
123
|
-
package and that your proxy and certificate settings allow registry access.
|
|
124
|
-
Install on the target operating system and architecture with optional dependencies
|
|
125
|
-
enabled:
|
|
45
|
+
Best when your client can launch a local command. No tunnel needed.
|
|
126
46
|
|
|
127
47
|
```bash
|
|
128
|
-
|
|
48
|
+
rynster start my-app --stdio
|
|
129
49
|
```
|
|
130
50
|
|
|
131
|
-
|
|
132
|
-
copying an existing `node_modules` directory between different platforms.
|
|
133
|
-
|
|
134
|
-
### Runtime requirements
|
|
51
|
+
Most clients take a config like this (the file location depends on the client):
|
|
135
52
|
|
|
136
|
-
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"rynster": {
|
|
57
|
+
"command": "rynster",
|
|
58
|
+
"args": ["start", "my-app", "--stdio"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
137
63
|
|
|
138
|
-
|
|
139
|
-
`whoami`/`icacls` security tools. Credential files use Windows ACLs; Linux retains
|
|
140
|
-
POSIX private-file permissions. See the security policy for platform limits.
|
|
64
|
+
#### Option B: Remote (HTTPS)
|
|
141
65
|
|
|
142
|
-
|
|
66
|
+
Use this when the client has to reach Rynster over the internet. You need your own HTTPS tunnel, since Rynster does not include one.
|
|
143
67
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
68
|
+
1. Start a tunnel to local port 3333. For example, with Cloudflare:
|
|
69
|
+
```bash
|
|
70
|
+
cloudflared tunnel --url http://127.0.0.1:3333
|
|
71
|
+
```
|
|
72
|
+
2. Start Rynster with the HTTPS address your tunnel gave you:
|
|
73
|
+
```bash
|
|
74
|
+
rynster start my-app --port 3333 --public-url https://your-tunnel-hostname
|
|
75
|
+
```
|
|
76
|
+
3. In your AI client, add a connector pointing to:
|
|
77
|
+
```
|
|
78
|
+
https://your-tunnel-hostname/mcp
|
|
79
|
+
```
|
|
80
|
+
4. Approve the consent screen when your client opens it.
|
|
147
81
|
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
rynster projects
|
|
151
|
-
```
|
|
82
|
+
Keep your computer, the tunnel, and Rynster running while the client needs access. If the tunnel hostname changes, restart Rynster with the new `--public-url` and reconnect the client.
|
|
152
83
|
|
|
153
|
-
|
|
84
|
+
Authentication is always required over HTTP. A tunnel encrypts traffic but does not replace Rynster's own checks. Never expose an unauthenticated server.
|
|
154
85
|
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
rynster start my-app --port 3333
|
|
158
|
-
```
|
|
159
|
-
Output:
|
|
160
|
-
```
|
|
161
|
-
┌──────────────────────────────────────────────────────────────┐
|
|
162
|
-
│ RYNSTER │
|
|
163
|
-
└──────────────────────────────────────────────────────────────┘
|
|
164
|
-
Project: my-app (react, branch: main)
|
|
165
|
-
Project Root: /path/to/my-app
|
|
166
|
-
MCP Endpoint: http://127.0.0.1:3333/mcp
|
|
167
|
-
Health Check: http://127.0.0.1:3333/health
|
|
168
|
-
Owner Token: Stored privately. Run rynster token to display it explicitly.
|
|
169
|
-
|
|
170
|
-
Permissions:
|
|
171
|
-
Read: ALLOW
|
|
172
|
-
Search: ALLOW
|
|
173
|
-
Write/Create: ASK (approval required)
|
|
174
|
-
Delete: DENY
|
|
175
|
-
Tests/Lint: ASK
|
|
176
|
-
Git Commit: ASK
|
|
177
|
-
Git Push: DENY
|
|
178
|
-
```
|
|
86
|
+
Having trouble connecting? Run a read-only check:
|
|
179
87
|
|
|
180
|
-
#### Stdio Mode (for a client supporting MCP 2026-07-28):
|
|
181
88
|
```bash
|
|
182
|
-
rynster
|
|
89
|
+
rynster doctor --url https://your-tunnel-hostname/mcp
|
|
183
90
|
```
|
|
184
91
|
|
|
185
92
|
---
|
|
186
93
|
|
|
187
|
-
##
|
|
94
|
+
## What the assistant can do
|
|
95
|
+
|
|
96
|
+
| Area | Tools |
|
|
97
|
+
| :-- | :-- |
|
|
98
|
+
| Project info | `project_info`, `project_tree`, `project_list` |
|
|
99
|
+
| Read and search | `project_read`, `project_search` |
|
|
100
|
+
| Git review | `git_status`, `git_diff` |
|
|
101
|
+
| Editing sessions | `session_start`, `session_list`, `session_stop` |
|
|
102
|
+
| File changes | `project_patch` |
|
|
103
|
+
| Project checks | `project_typecheck`, `project_lint`, `project_test`, `project_format`, `project_build` |
|
|
188
104
|
|
|
189
|
-
|
|
105
|
+
Having a tool does not mean every action is allowed. Each one is checked against your permissions and approval rules.
|
|
190
106
|
|
|
191
|
-
|
|
192
|
-
discovery, CIMD/DCR, browser-bound consent, project grants, and hardened token
|
|
193
|
-
persistence. Automated Linux fixtures exercise those flows and an isolated
|
|
194
|
-
installed-package check exercises the Rynster executable. macOS and Windows are
|
|
195
|
-
unverified.
|
|
107
|
+
---
|
|
196
108
|
|
|
197
|
-
|
|
198
|
-
recorded using development builds under the old package name. Those results do
|
|
199
|
-
not verify live onboarding with the renamed Rynster tarball. Additional ChatGPT
|
|
200
|
-
token-renewal and other lifecycle checks remain deferred. Historical details are
|
|
201
|
-
in the Local OAuth runbook in the source repository.
|
|
109
|
+
## Permissions and approvals
|
|
202
110
|
|
|
203
|
-
|
|
204
|
-
2. Start Rynster with the tunnel origin:
|
|
111
|
+
Each action is set to **allow**, **ask** (you approve it in the terminal), or **deny**. Typical defaults:
|
|
205
112
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
113
|
+
| Action | Default |
|
|
114
|
+
| :-- | :-- |
|
|
115
|
+
| Read and search | Allow |
|
|
116
|
+
| Write and create files | Ask |
|
|
117
|
+
| Run checks (test, lint, build...) | Ask |
|
|
118
|
+
| Git commit | Ask |
|
|
119
|
+
| Delete files | Deny |
|
|
120
|
+
| Git push | Deny |
|
|
209
121
|
|
|
210
|
-
|
|
211
|
-
4. When the quick-tunnel hostname changes, update the public URL, restart Rynster, and recreate the client connection for the new issuer.
|
|
122
|
+
Settings live in `agent.config.json` in your working directory (use `--config` to pick another file). A good starting point is the `agent.config.example.json` file included in the package.
|
|
212
123
|
|
|
213
|
-
|
|
124
|
+
When asked to approve an edit, the terminal prompt accepts:
|
|
214
125
|
|
|
215
|
-
|
|
126
|
+
- `y` approve
|
|
127
|
+
- `n` reject
|
|
128
|
+
- `d` show the full diff
|
|
129
|
+
- `a` allow writes for this session
|
|
216
130
|
|
|
217
|
-
|
|
131
|
+
If no terminal is available (a headless run), anything that needs approval is denied.
|
|
218
132
|
|
|
219
|
-
|
|
133
|
+
Leave delete and push on **deny** unless you understand the risk.
|
|
134
|
+
|
|
135
|
+
### Project commands
|
|
136
|
+
|
|
137
|
+
Assistants can only run the five command types above, using commands you set in `agent.config.json`:
|
|
220
138
|
|
|
221
|
-
For a client that supports the modern protocol and command-based MCP servers, use the following command configuration. This is not a claim of verified Claude Desktop compatibility:
|
|
222
139
|
```json
|
|
223
140
|
{
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
"args": [
|
|
228
|
-
"start",
|
|
229
|
-
"my-app",
|
|
230
|
-
"--stdio"
|
|
231
|
-
]
|
|
232
|
-
}
|
|
141
|
+
"commands": {
|
|
142
|
+
"test": { "file": "npm", "args": ["test"] },
|
|
143
|
+
"typecheck": { "file": "npm", "args": ["run", "typecheck"] }
|
|
233
144
|
}
|
|
234
145
|
}
|
|
235
146
|
```
|
|
236
147
|
|
|
148
|
+
For multi-step commands, write a script you have reviewed and point to it. Shell operators such as `&&`, pipes, and redirects are rejected.
|
|
149
|
+
|
|
237
150
|
---
|
|
238
151
|
|
|
239
|
-
##
|
|
152
|
+
## How Rynster keeps your project safe
|
|
240
153
|
|
|
241
|
-
|
|
154
|
+
- **Stays inside the project.** Paths are checked against the registered folder. Attempts to escape it, including through symlinks, are blocked.
|
|
155
|
+
- **Protects secrets.** Files like `.env*`, `*.pem`, `*.key`, and `id_rsa*`, plus folders like `.ssh`, `.aws`, and `.git`, are off limits. Add your own with `protectedFiles` in the config.
|
|
156
|
+
- **Detects changed files.** Edits are checked against the version the assistant read, and again after you approve. If the file changed in between, the edit is rejected.
|
|
157
|
+
- **Isolated worktrees.** Sessions can work in their own Git branch (`agent/<session-id>`) so your main branch is untouched.
|
|
158
|
+
- **Private audit log.** Operations are recorded without source code, command arguments, or tokens.
|
|
242
159
|
|
|
243
|
-
|
|
244
|
-
rynster oauth grants
|
|
245
|
-
rynster oauth grants --json
|
|
246
|
-
```
|
|
160
|
+
These reduce risk but do not make untrusted code safe to run. The full details are in [Local security policy](SECURITY.md).
|
|
247
161
|
|
|
248
|
-
|
|
249
|
-
issuers, grant status, expiry times, and successful refresh counts. It excludes
|
|
250
|
-
access/refresh tokens, token hashes, client secrets, and project-root hashes. An
|
|
251
|
-
`active` status means the stored grant is unexpired and unrevoked; it does not
|
|
252
|
-
prove the client is currently connected. Listings read a snapshot without changing
|
|
253
|
-
the store or migrating legacy data. A missing store produces an empty listing.
|
|
162
|
+
---
|
|
254
163
|
|
|
255
|
-
|
|
164
|
+
## Managing connected clients
|
|
165
|
+
|
|
166
|
+
See which clients have been granted access, and revoke one:
|
|
256
167
|
|
|
257
168
|
```bash
|
|
169
|
+
rynster oauth grants
|
|
258
170
|
rynster oauth revoke FULL-GRANT-ID
|
|
259
171
|
```
|
|
260
172
|
|
|
261
|
-
|
|
262
|
-
Rynster is running, and persists across restarts. Other grants are preserved. The
|
|
263
|
-
client needs fresh consent to access the project again. These commands manage
|
|
264
|
-
OAuth grants; the separate owner bearer credential is unaffected. They are local
|
|
265
|
-
administration commands and are not exposed as MCP tools.
|
|
173
|
+
Copy the full grant ID from the list. Revoking blocks that client right away, including while Rynster is running. The client has to ask for your approval again to get back in. Add `--json` to the list command for machine-readable output.
|
|
266
174
|
|
|
267
|
-
|
|
268
|
-
`RYNSTER_OAUTH_STORAGE_PATH` override, use that same environment setting in the
|
|
269
|
-
administrative terminal, or select the file explicitly:
|
|
175
|
+
Grants are stored in `~/.rynster/oauth.json`. To use a different file, pass `--storage` or set `RYNSTER_OAUTH_STORAGE_PATH`.
|
|
270
176
|
|
|
271
|
-
|
|
272
|
-
rynster oauth grants --storage /path/to/test-oauth.json
|
|
273
|
-
rynster oauth revoke FULL-GRANT-ID --storage /path/to/test-oauth.json
|
|
274
|
-
```
|
|
177
|
+
Useful settings:
|
|
275
178
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
For a live restart test, first confirm a fresh client read, revoke its grant in
|
|
283
|
-
another terminal, restart Rynster with the same public URL and OAuth store, and
|
|
284
|
-
confirm reads still fail without fresh consent. Then reconnect the client and
|
|
285
|
-
verify a fresh read succeeds.
|
|
286
|
-
|
|
287
|
-
## Available MCP Tools
|
|
288
|
-
|
|
289
|
-
| Tool | Description |
|
|
290
|
-
| :--- | :--- |
|
|
291
|
-
| `project_info` | Returns project framework, package manager, and active git branch |
|
|
292
|
-
| `project_tree` | Compact ASCII tree view of directory structure (ignores noise/secrets) |
|
|
293
|
-
| `project_read` | Line-numbered file reading with line ranges, metadata, and SHA-256 revision hash |
|
|
294
|
-
| `project_search` | Structured ripgrep search returning file, line number, and snippet matches |
|
|
295
|
-
| `git_status` | Returns git status (clean, modified, staged, untracked) |
|
|
296
|
-
| `git_diff` | Returns unified git diff |
|
|
297
|
-
| `session_start` | Starts a session (optionally in an isolated git worktree branch) |
|
|
298
|
-
| `session_list` | Lists active sessions and read/changed file counters |
|
|
299
|
-
| `session_stop` | Cleans up session with action: `keep`, `commit`, or `discard` |
|
|
300
|
-
| `project_patch` | Safely patches a file, verifying revision hash and prompting approval |
|
|
301
|
-
| `project_typecheck` | Executes pre-configured typecheck command |
|
|
302
|
-
| `project_lint` | Executes pre-configured lint command |
|
|
303
|
-
| `project_test` | Executes pre-configured test command |
|
|
304
|
-
| `project_format` | Executes pre-configured formatting command |
|
|
305
|
-
| `project_build` | Executes pre-configured build command |
|
|
306
|
-
| `project_list` | Lists all registered projects in the agent |
|
|
179
|
+
| Setting | Purpose |
|
|
180
|
+
| :-- | :-- |
|
|
181
|
+
| `--config` | Choose the project config file |
|
|
182
|
+
| `--public-url` / `RYNSTER_PUBLIC_URL` | Public HTTPS address for remote mode |
|
|
183
|
+
| `RYNSTER_OAUTH_STORAGE_PATH` | Custom location for OAuth grants |
|
|
184
|
+
| `rynster token` | Show the owner token (treat it like a password) |
|
|
307
185
|
|
|
308
186
|
---
|
|
309
187
|
|
|
310
|
-
##
|
|
188
|
+
## Troubleshooting
|
|
311
189
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
Proprietary License](LICENSE). No subscription or donation is required for those
|
|
315
|
-
uses. Your projects and work remain yours or their respective owners'.
|
|
190
|
+
**`rynster` is not found after installing.**
|
|
191
|
+
Make sure npm's global bin folder is on your `PATH`, open a new terminal, and run `rynster --version`.
|
|
316
192
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
retain their own licenses. See LICENSE for the complete terms and exceptions.
|
|
193
|
+
**Search does not work, or a ripgrep package is missing.**
|
|
194
|
+
Reinstall with optional dependencies on the machine you will run it on:
|
|
320
195
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
requires separate terms; the CLI license does not include Connect access.
|
|
196
|
+
```bash
|
|
197
|
+
npm install -g rynster --include=optional
|
|
198
|
+
```
|
|
325
199
|
|
|
326
|
-
|
|
200
|
+
If it still fails, check that your npm registry or proxy provides the `@vscode/ripgrep-*` packages. Do not copy `node_modules` between different operating systems or CPU types.
|
|
327
201
|
|
|
328
|
-
|
|
329
|
-
|
|
202
|
+
**Remote connection stopped working.**
|
|
203
|
+
Your tunnel hostname probably changed. Restart Rynster with the new `--public-url` and recreate the connection in your client.
|
|
330
204
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
```
|
|
205
|
+
**An action is denied or seems stuck.**
|
|
206
|
+
Check the permission for that action in your config. `ask` needs you to approve in the terminal, and headless runs deny it.
|
|
334
207
|
|
|
335
|
-
|
|
208
|
+
---
|
|
336
209
|
|
|
337
|
-
##
|
|
210
|
+
## Known limitations
|
|
338
211
|
|
|
339
|
-
|
|
212
|
+
- Runs on your computer only. There is no hosted version in this release.
|
|
213
|
+
- No built-in tunnel for remote access.
|
|
214
|
+
- Not a sandbox. Scripts run with your user's access.
|
|
215
|
+
- Automated security tests cover Linux and Windows. macOS is not yet verified.
|
|
216
|
+
- On Windows, stopping a command may not end every child process it started.
|
|
217
|
+
- Earlier live testing with Claude and ChatGPT used development builds under a previous package name. Token renewal and other lifecycle checks with the current package are still pending.
|
|
218
|
+
- Supports MCP protocol version `2026-07-28` only. Check that your client supports it.
|
|
340
219
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Security
|
|
223
|
+
|
|
224
|
+
To report a vulnerability, email Baskar Ellangudi at **baskar12ai@gmail.com** with the subject `Rynster security report`. Include your Rynster and Node versions, operating system, steps to reproduce, and the impact. Please do not send tokens, credentials, or private files. You do not need GitHub access.
|
|
225
|
+
|
|
226
|
+
Full details: [Local security policy](SECURITY.md).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
Rynster is proprietary and free for personal and internal business use, including commercial work and client projects. Redistributing, reselling, modifying, or offering it as a hosted service requires written permission. Being on npm does not make it open source. See [LICENSE](LICENSE).
|
|
349
231
|
|
|
350
|
-
|
|
232
|
+
## Support
|
|
351
233
|
|
|
352
|
-
|
|
234
|
+
Support is optional. Run `rynster support` or visit [ko-fi.com/bas12](https://ko-fi.com/bas12).
|
|
353
235
|
|
|
354
|
-
|
|
236
|
+
## Release notes
|
|
355
237
|
|
|
356
|
-
See [
|
|
238
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
package/SECURITY.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
# Rynster
|
|
1
|
+
# Rynster security policy
|
|
2
2
|
|
|
3
|
-
Reviewed on 2026-10-
|
|
3
|
+
Reviewed on 2026-10-23. Rynster runs on your own computer; there is no hosted service in this release.
|
|
4
|
+
|
|
5
|
+
## In plain terms
|
|
6
|
+
|
|
7
|
+
- Rynster trusts you (the operating-system user) and the project code you approve.
|
|
8
|
+
- It is not a sandbox. Approved scripts run with your full file and network access.
|
|
9
|
+
- It guards against an assistant escaping the project, reading secrets, or editing files you did not approve. It does not defend against another malicious process already running as you.
|
|
4
10
|
|
|
5
11
|
## Reporting a vulnerability
|
|
6
12
|
|
|
@@ -17,9 +23,13 @@ sharing them: do not send access or refresh tokens, owner credentials, client
|
|
|
17
23
|
secrets, private keys, or private project files. Coordinate any sensitive follow-up
|
|
18
24
|
material with the maintainer by email.
|
|
19
25
|
|
|
26
|
+
**Supported versions:** security fixes are provided for the latest 0.1.x release.
|
|
27
|
+
|
|
28
|
+
**Response time:** reports are acknowledged within 5 business days. Confirmed issues are fixed in a new release, and reporters are credited if they wish.
|
|
29
|
+
|
|
20
30
|
## Authorization and editing sessions
|
|
21
31
|
|
|
22
|
-
HTTP MCP requires an owner bearer token or a valid OAuth grant. OAuth grants authorize one project and bind to its canonical root plus filesystem device/inode identity. Revocation, expiry, issuer changes, and physical root replacement deny further tool execution.
|
|
32
|
+
HTTP MCP requires an owner bearer token or a valid OAuth grant. OAuth grants authorize one project and bind to its canonical root plus filesystem device/inode identity. Revocation, expiry, issuer changes, and physical root replacement deny further tool execution.
|
|
23
33
|
|
|
24
34
|
Editing sessions, default sessions, and session-wide write permissions belong to an authenticated principal. The owner token, each OAuth grant, and the local stdio caller have separate ownership. Refreshing a token retains its grant's workflow ownership. Another grant cannot list, read through, patch through, execute commands through, or stop its sessions. Session IDs use random UUIDs and are never credentials.
|
|
25
35
|
|
|
@@ -31,11 +41,11 @@ Project reads, searches, trees, patches, and Git diffs protect credential patter
|
|
|
31
41
|
|
|
32
42
|
Patches capture contents, file identity, and existing directory identities before approval. They recheck those values, authorization, and permissions before writing. Symlink patch paths and hard-linked targets are refused. Replacing a file or directory, editing contents, or creating a formerly absent target during approval invalidates the proposed edit. Atomic temporary-file replacement avoids exposing a partially written file; Linux writes pin the parent through a directory descriptor.
|
|
33
43
|
|
|
34
|
-
These checks mitigate approval-time and path-substitution races. They do not provide kernel-level compare-and-swap or protection against an unrestricted malicious local process changing the filesystem during the final synchronous commit.
|
|
44
|
+
These checks mitigate approval-time and path-substitution races. They do not provide kernel-level compare-and-swap or protection against an unrestricted malicious local process changing the filesystem during the final synchronous commit. Rynster's threat model trusts the OS user and approved project code. macOS remains unverified.
|
|
35
45
|
|
|
36
|
-
Configuration uses atomic private replacement, an exclusive writer lock, and detection of concurrent edits. Malformed/unreadable configuration fails startup rather than silently applying defaults. OAuth persistence
|
|
46
|
+
Configuration uses atomic private replacement, an exclusive writer lock, and detection of concurrent edits. Malformed/unreadable configuration fails startup rather than silently applying defaults. OAuth persistence is versioned, private, hashed, locked, and atomic. Startup does not print the owner token. `rynster token` explicitly displays it for local consent; treat that output as a credential.
|
|
37
47
|
|
|
38
|
-
On Windows, private credential files use protected ACLs granting access to the current user, SYSTEM, and local Administrators. Newly created application storage directories receive the same protection; existing project or parent directories are not recursively repermissioned.
|
|
48
|
+
On Windows, private credential files use protected ACLs granting access to the current user, SYSTEM, and local Administrators. Newly created application storage directories receive the same protection; existing project or parent directories are not recursively repermissioned. Applying and verifying these ACLs requires the built-in Windows PowerShell and the system `whoami` and `icacls` tools. Failure to establish permissions denies storage access. Read-only grant listing does not change permissions. Explicit file/descriptor identity checks reject linked stores; Windows additionally rejects credential paths through junctions and project paths addressing alternate data streams. These checks retain the local-user race limitation above and do not claim the same kernel protections as Linux `O_NOFOLLOW` or directory-descriptor pinning.
|
|
39
49
|
|
|
40
50
|
Both systems flush private file data before same-directory replacement. Linux also flushes the OAuth directory after replacement. Windows omits that directory flush because Node cannot perform it here; identical power-loss durability across operating systems is not claimed. Storage errors remain failures and are never treated as successful writes. As on Linux, an error after replacement can occur after the new file has become visible.
|
|
41
51
|
|
|
@@ -43,11 +53,11 @@ Both systems flush private file data before same-directory replacement. Linux al
|
|
|
43
53
|
|
|
44
54
|
A local approval request has a unique operation ID/digest, project/root/revision context, authenticated owner, and five-minute expiry. Its decision is valid only for the captured operation. Changed revisions, paths, executable identity, package manifests/locks, or command configuration require a fresh request. Cancellation, expiry, and grant revocation deny dispatch. Terminal prompts are serialized and use stderr, so MCP stdio frames remain intact. Closed input and headless processes deny approval-required operations. No global automatic-approval flag is supported.
|
|
45
55
|
|
|
46
|
-
New configurations ask before every command category. The only categories exposed to clients are `typecheck`, `lint`, `test`, `format`, and `build`; remote clients cannot supply executable names or command arguments.
|
|
56
|
+
New configurations ask before every command category. The only categories exposed to clients are `typecheck`, `lint`, `test`, `format`, and `build`; remote clients cannot supply executable names or command arguments. Configuration specifies `{ "file": "npm", "args": ["test"] }`. Simple legacy strings such as `npm test` still work, but shell operators and substitutions are refused. Put command sequences into a locally reviewed script instead.
|
|
47
57
|
|
|
48
58
|
Rynster launches resolved executables with `shell: false`, an environment allowlist, closed stdin, bounded output, and timeouts. It does not implicitly install TypeScript using `npx`. On Linux, cancellation, timeout, and output overflow terminate the process group, including ordinary descendants. Processes that deliberately escape their group are outside this trusted-code policy.
|
|
49
59
|
|
|
50
|
-
On Windows, npm commands run through Node and the installed npm CLI JavaScript entry point. Both executable and entry-point identity/content are checked across approval. Arbitrary batch launchers are refused; configure their real executable and script arguments explicitly. Quoted Windows executable paths are supported in legacy command strings; structured commands remain preferred. Windows process termination does not provide the Linux process-group descendant cleanup guarantee.
|
|
60
|
+
On Windows, npm commands run through Node and the installed npm CLI JavaScript entry point. Both executable and entry-point identity/content are checked across approval. Arbitrary batch launchers are refused; configure their real executable and script arguments explicitly. Quoted Windows executable paths are supported in legacy command strings; structured commands remain preferred. Windows process termination does not provide the Linux process-group descendant cleanup guarantee: stopping a command may not end every child process it started.
|
|
51
61
|
|
|
52
62
|
**Project scripts execute as the host user with host filesystem and network access.** npm itself can execute scripts through a shell. An executable/argument array, environment filtering, a working directory, and a Git worktree are not an OS sandbox. Existing explicit `allow` policies mean the owner trusts project code, including code changed by an assistant. Review that code before approving execution. No managed or hosted sandbox guarantee is claimed.
|
|
53
63
|
|
|
@@ -55,21 +65,20 @@ On Windows, npm commands run through Node and the installed npm CLI JavaScript e
|
|
|
55
65
|
|
|
56
66
|
Host validation restricts accepted hostnames. Origin checks require an exact public origin or the same local origin; null/untrusted origins are refused. CORS does not use a wildcard. Query credentials are never accepted. Routine request/audit logs omit URL queries, untrusted tool/session labels, search strings, command arguments, source contents, and tokens. Project scripts can still print secrets they choose to read; approving them grants their host capabilities.
|
|
57
67
|
|
|
58
|
-
##
|
|
68
|
+
## Managing OAuth grants
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
Owners can inspect credential-free grant metadata with `rynster oauth grants`
|
|
61
71
|
and revoke an exact grant ID with `rynster oauth revoke <grant-id>`. The commands use
|
|
62
72
|
the same default OAuth store and `RYNSTER_OAUTH_STORAGE_PATH` override as the server,
|
|
63
|
-
with an explicit `--storage` option.
|
|
64
|
-
format are supported. Earlier stores are not imported or migrated.
|
|
65
|
-
Listings do not write, migrate, or change file
|
|
73
|
+
with an explicit `--storage` option. Listings do not write, migrate, or change file
|
|
66
74
|
permissions. Revocations use the existing writer lock and atomic private save,
|
|
67
|
-
reject unknown IDs and invalid
|
|
75
|
+
reject unknown IDs and invalid storage, and remain effective after restart.
|
|
68
76
|
The commands are not exposed as MCP tools and do not revoke the separate owner
|
|
69
|
-
bearer credential. Client labels in terminal output are sanitized.
|
|
70
|
-
tests confirm that a targeted revocation blocks access and refresh while retaining
|
|
71
|
-
another grant's access; live-client acceptance remains a separate check.
|
|
77
|
+
bearer credential. Client labels in terminal output are sanitized.
|
|
72
78
|
|
|
73
|
-
|
|
79
|
+
## Verification status
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
- **Automated tests:** Linux and Windows, covering HTTP/stdio authorization, grant and refresh ownership, physical root replacement, credential guards, stale/canceled/expired approvals, symlink replacement, approved edits, headless denial, command grammar/environment/output/process cleanup, configuration races, and targeted grant revocation. Installed-package checks cover production-only dependency resolution, CLI configuration, native search assets, workflow handles, secret guards, headless command denial, protocol rejection, and shutdown.
|
|
82
|
+
- **macOS:** unverified.
|
|
83
|
+
- **Live client testing:** earlier development builds were tested with Claude and ChatGPT (onboarding, reads, revocation, token renewal). The renamed package has not been re-tested live, and ChatGPT lifecycle checks are pending. Passing automated tests is not live-client evidence.
|
|
84
|
+
- **Not provided:** a hosted service, an authenticated headless approval channel, and an OS-isolated command executor.
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ ${t?`<p class="error">${Ae(t)}</p>`:""}
|
|
|
35
35
|
<label for="owner_token">Rynster owner authorization token</label><p>Run <code>rynster token</code> in your local terminal to display it.</p>
|
|
36
36
|
<input type="password" id="owner_token" name="owner_token" autocomplete="off" required>
|
|
37
37
|
<button name="action" value="approve">Approve & Connect ${r}</button><button name="action" value="deny" formnovalidate>Deny</button>
|
|
38
|
-
</form></body></html>`}},so=g(a=>Ve.createHash("sha256").update(a).digest("base64url"),"digestChallenge")});var Qe,hr=j(()=>{Qe={name:"rynster",version:"0.1.
|
|
38
|
+
</form></body></html>`}},so=g(a=>Ve.createHash("sha256").update(a).digest("base64url"),"digestChallenge")});var Qe,hr=j(()=>{Qe={name:"rynster",version:"0.1.1",description:"Provider-neutral local coding agent MCP server",bugs:{email:"baskar12ai@gmail.com"},type:"module",license:"SEE LICENSE IN LICENSE",funding:{type:"ko-fi",url:"https://ko-fi.com/bas12"},keywords:["mcp","ai","coding-agent","claude","chatgpt","local"],scripts:{prepack:"node ../../scripts/release.mjs prepack",prepublishOnly:"node ../../scripts/release.mjs publish-check"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},engines:{node:">=22.17 <23 || >=24.2 <25"},main:"./dist/index.js",bin:{rynster:"./dist/index.js"},files:["dist/index.js","README.md","CHANGELOG.md","agent.config.example.json","LICENSE","SECURITY.md"],dependencies:{"@modelcontextprotocol/express":"^2.0.2","@modelcontextprotocol/node":"^2.1.1","@modelcontextprotocol/server":"^2.3.1","@vscode/ripgrep":"^1.18.0",commander:"^15.0.0",cors:"^2.8.6",diff:"^9.0.0",express:"^5.2.1",jose:"^6.2.12",zod:"^4.6.5"},devDependencies:{"@rynster/contracts":"*","@rynster/local-engine":"*","@rynster/mcp-adapter":"*"}}});var Ee,_e,bt,jt=j(()=>{"use strict";hr();Ee=Qe.name,_e=Qe.version,bt=Qe.funding.url});function G(){let a={CI:"true",TERM:"dumb",FORCE_COLOR:"0",NODE_ENV:"test"};for(let e of["PATH","HOME","USER","LANG","LC_ALL","TMPDIR","TMP","TEMP","SystemRoot","WINDIR","PATHEXT",...process.platform==="win32"?["ComSpec","USERPROFILE","APPDATA","LOCALAPPDATA","HOMEDRIVE","HOMEPATH"]:[]])process.env[e]&&(a[e]=process.env[e]);return a}var Ce=j(()=>{"use strict";g(G,"commandEnvironment")});import R from"node:fs";import W from"node:path";import{execFileSync as lo}from"node:child_process";var Me,xt=j(()=>{"use strict";Ce();Me=class{static{g(this,"ProjectManager")}configManager;constructor(e){this.configManager=e}getProject(e){let t=this.configManager.getProject(e);if(!t){let o=Object.keys(this.configManager.listProjects());throw o.length===0?new Error("No projects registered. Register a project first using 'coding-agent-mcp register <name> <path>' or in agent.config.json"):new Error(`Project '${e}' not found. Available registered projects: ${o.join(", ")}`)}if(!R.existsSync(t.config.root))throw new Error(`Registered project root directory does not exist: ${t.config.root}`);let r=R.realpathSync(t.config.root);for(let[o,n]of Object.entries(this.configManager.listProjects()))if(o!==t.id&&R.existsSync(n.root)&&R.realpathSync(n.root)===r)throw new Error("Duplicate project roots; remove aliases from configuration");return{id:t.id,config:t.config,root:R.realpathSync(t.config.root)}}listProjects(){return this.configManager.listProjects()}getProjectInfo(e){let{id:t,config:r,root:o}=this.getProject(e),n=this.detectFramework(o),s=this.detectPackageManager(o),{branch:c,hasGit:i}=this.detectGitInfo(o);return{name:t,root:o,framework:n,packageManager:s,branch:c,description:r.description,hasGit:i,commands:r.commands}}detectFramework(e){let t=W.join(e,"package.json");if(R.existsSync(t))try{let r=JSON.parse(R.readFileSync(t,"utf-8")),o={...r.dependencies,...r.devDependencies};return o["react-native"]?"react-native":o.next?"nextjs":o["@nestjs/core"]?"nestjs":o.vue||o.nuxt?"vue":o.svelte?"svelte":o["@angular/core"]?"angular":o.react?"react":o.express?"express":"node"}catch{return"node"}return R.existsSync(W.join(e,"Cargo.toml"))?"rust":R.existsSync(W.join(e,"go.mod"))?"go":R.existsSync(W.join(e,"requirements.txt"))||R.existsSync(W.join(e,"pyproject.toml"))||R.existsSync(W.join(e,"setup.py"))?"python":"unknown"}detectPackageManager(e){let t=W.join(e,"package.json");if(R.existsSync(t))try{let r=JSON.parse(R.readFileSync(t,"utf-8"));if(r.packageManager)return r.packageManager.split("@")[0]}catch{}return R.existsSync(W.join(e,"pnpm-lock.yaml"))?"pnpm":R.existsSync(W.join(e,"yarn.lock"))?"yarn":R.existsSync(W.join(e,"bun.lockb"))||R.existsSync(W.join(e,"bun.lock"))?"bun":R.existsSync(W.join(e,"package-lock.json"))?"npm":R.existsSync(W.join(e,"Cargo.lock"))?"cargo":R.existsSync(W.join(e,"poetry.lock"))?"poetry":"npm"}detectGitInfo(e){let t=W.join(e,".git");if(!R.existsSync(t))return{branch:"none",hasGit:!1};try{return{branch:lo("git",["-c","core.fsmonitor=false","rev-parse","--abbrev-ref","HEAD"],{env:G(),timeout:15e3,cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()||"HEAD",hasGit:!0}}catch{return{branch:"unknown",hasGit:!0}}}}});import Re from"node:path";import fr from"node:fs";import{AsyncLocalStorage as po}from"node:async_hooks";var I,ge=j(()=>{"use strict";he();I=class{static{g(this,"SecretGuard")}static scope=new po;static withProtectedPaths(e,t){return this.scope.run(e.map(r=>Re.resolve(r)),t)}static isProtectedAbsolute(e){let t=Re.resolve(e),r=t;try{r=fr.realpathSync(t)}catch{}return(this.scope.getStore()||[]).some(o=>{let n=o;try{n=fr.realpathSync(o)}catch{}return[o,n].some(s=>[t,r].some(c=>Re.matchesGlob(c,s)||c===s||c.startsWith(s+Re.sep)||c.startsWith(s+".")))})}static assertSafeFile(e,t){if(this.isProtectedAbsolute(Re.resolve(e,t))||this.isBlockedPath(t))throw new $("Access denied: protected credential or configuration path")}static PROTECTED_FILE_PATTERNS=[/^\.env(\..+)?$/i,/^agent\.config\.json(?:\..+)?$/i,/^\.(npmrc|netrc|pypirc)$/i,/\.pem$/i,/\.key$/i,/\.p12$/i,/\.pfx$/i,/\.jks$/i,/\.keystore$/i,/^credentials(\..+)?$/i,/^service-account.*\.json$/i,/^id_rsa/i,/^id_ed25519/i,/\.asc$/i];static BLOCKED_DIRECTORIES=new Set([".git","node_modules",".gradle","Pods",".agent",".rynster",".aws",".ssh",".codex",".agents",".idea",".vscode"]);static IGNORED_DIRECTORIES=new Set([".git","node_modules",".gradle","Pods",".agent",".rynster",".aws",".ssh",".codex",".agents","build","dist","out",".next",".nuxt",".turbo",".cache",".expo","coverage",".idea",".vscode"]);static isProtectedFile(e){let t=Re.basename(e);return this.PROTECTED_FILE_PATTERNS.some(r=>r.test(t))}static isBlockedDirectory(e){return e.split(/[/\\]/).some(r=>this.BLOCKED_DIRECTORIES.has(r))}static isBlockedPath(e){let t=e.split(/[/\\]/);for(let r of t)if(this.BLOCKED_DIRECTORIES.has(r))return!0;return this.isProtectedFile(e)}static shouldIgnoreInTree(e){return this.IGNORED_DIRECTORIES.has(e)}static assertFileReadable(e){if(this.isBlockedPath(e))throw new $(`Access Denied: '${e}' is a protected secret or directory. File contents cannot be accessed.`)}static assertFileWritable(e){if(this.isBlockedPath(e))throw new $(`Write Denied: '${e}' is a protected secret or directory and cannot be modified.`)}}});import M from"node:path";import Te from"node:fs";var $,D,he=j(()=>{"use strict";ge();$=class extends Error{static{g(this,"SecurityViolationError")}constructor(e){super(e),this.name="SecurityViolationError"}},D=class{static{g(this,"PathGuard")}static resolveSafePath(e,t,r=!0){if(!e)throw new $("Project root is required for path validation");if(!t||t.trim()==="")throw new $("Target path cannot be empty");if(process.platform==="win32"&&t.slice(M.parse(t).root.length).includes(":"))throw new $("Windows alternate data streams are unsupported");let o;try{o=Te.realpathSync(M.resolve(e))}catch{throw new $("Project root does not exist or cannot be resolved")}I.assertSafeFile(o,t);let n=M.normalize(t);if(t.split(/[/\\]/).includes("..")){let d=M.resolve(o,n);if(!this.isSubpath(o,d))throw new $(`Directory traversal detected: '${t}' escapes project root`)}let c;M.isAbsolute(n)?c=M.normalize(n):c=M.resolve(o,n);let i=this.resolveSymlinks(c,r);if(!this.isSubpath(o,i))throw new $(`Access denied: '${t}' resolves to '${i}', outside project root '${o}'`);I.assertSafeFile(o,M.relative(o,i));let l=M.relative(o,i);return{absolutePath:i,relativePath:l||"."}}static resolveSymlinks(e,t){if(Te.existsSync(e))try{return Te.realpathSync(e)}catch(n){throw new $(`Failed to resolve symlink for '${e}': ${n}`)}if(!t)throw new $(`Path does not exist: '${e}'`);let r=M.dirname(e),o=[M.basename(e)];for(;r!==M.dirname(r);){if(Te.existsSync(r))try{let n=Te.realpathSync(r);return M.resolve(n,...o)}catch(n){throw new $(`Failed to resolve ancestor symlink for '${r}': ${n}`)}o.unshift(M.basename(r)),r=M.dirname(r)}return M.normalize(e)}static isSubpath(e,t){let r=M.normalize(e),o=M.normalize(t);if(o===r)return!0;let n=M.relative(r,o);return!n.startsWith("..")&&!M.isAbsolute(n)}}});import z from"node:fs";import et from"node:path";import uo from"node:crypto";var oe,tt=j(()=>{"use strict";he();ge();oe=class{static{g(this,"ProjectFilesystem")}static readDescriptor(e){let t=z.statSync(e),r=z.openSync(e,z.constants.O_RDONLY|(z.constants.O_NOFOLLOW||0));try{let o=z.fstatSync(r);if(!o.isFile()||o.nlink!==1||o.dev!==t.dev||o.ino!==t.ino)throw new Error("Unsafe or changed file identity");return{content:z.readFileSync(r,"utf8"),stat:o}}finally{z.closeSync(r)}}static getTree(e,t=".",r={}){let{absolutePath:o,relativePath:n}=D.resolveSafePath(e,t);if(!z.existsSync(o))throw new Error(`Path does not exist: ${t}`);if(!z.statSync(o).isDirectory())return{tree:`${n}
|
|
39
39
|
`,entryCount:1,truncated:!1};let c=r.depth!==void 0?Math.max(1,r.depth):3,i=r.maxEntries||500,l=0,d=!1,u=[],p=n==="."?et.basename(e)+"/":n+"/";u.push(p);let f=g((h,m,v)=>{if(m>c||l>=i){l>=i&&(d=!0);return}let w;try{w=z.readdirSync(h,{withFileTypes:!0})}catch{return}let S=w.filter(x=>!(I.shouldIgnoreInTree(x.name)||I.isBlockedPath(x.name)||I.isProtectedAbsolute(et.join(h,x.name))||x.isSymbolicLink()));S.sort((x,_)=>x.isDirectory()&&!_.isDirectory()?-1:!x.isDirectory()&&_.isDirectory()?1:x.name.localeCompare(_.name));for(let x=0;x<S.length;x++){if(l>=i){d=!0;return}let _=S[x],ee=x===S.length-1,te=et.join(h,_.name),F=ee?"\u2514\u2500\u2500 ":"\u251C\u2500\u2500 ",H=v+(ee?" ":"\u2502 ");l++,_.isDirectory()?(u.push(`${v}${F}${_.name}/`),f(te,m+1,H)):u.push(`${v}${F}${_.name}`)}},"traverse");return f(o,1,""),d&&u.push(`
|
|
40
40
|
... [Truncated: reached max limit of ${i} entries]`),{tree:u.join(`
|
|
41
41
|
`),entryCount:l,truncated:d}}static readFile(e,t,r,o){let{absolutePath:n,relativePath:s}=D.resolveSafePath(e,t);if(I.assertFileReadable(s),!z.existsSync(n))throw new Error(`File does not exist: ${t}`);let c=z.statSync(n);if(c.isDirectory())throw new Error(`Cannot read directory as file: ${t}`);let i=this.readDescriptor(n).content,l=this.calculateHash(i),d=i.split(/\r?\n/),u=d.length,p=Math.max(1,r||1),f=o!==void 0?Math.min(u,o):u;return p>u?{content:`(startLine ${p} is beyond total lines ${u})`,totalLines:u,startLine:p,endLine:f,language:this.detectLanguage(t),modifiedTime:c.mtime.toISOString(),revision:l,relativePath:s}:{content:d.slice(p-1,f).map((v,w)=>`${(p+w).toString().padStart(4," ")} | ${v}`).join(`
|