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 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 (`rynster`)
1
+ # Rynster
2
2
 
3
- A provider-neutral local coding agent with MCP (Model Context Protocol) as its external interface.
3
+ **Let your AI assistant work on your local code projects, with you in control.**
4
4
 
5
- Rynster connects MCP clients to registered local projects through configured permissions, file guards, and local approvals:
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
- Claude / Grok / Future AI
9
- │
10
- │ MCP (stdio or HTTPS + Bearer Auth)
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
- 7. **Modern MCP Transports**:
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
- ## Security defaults
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 the Local package
19
+ ### 1. Install
89
20
 
90
- Install published releases from the npm registry:
21
+ Requires Node.js 22.17+ (22.x) or 24.2+ (24.x), and npm 11+.
91
22
 
92
23
  ```bash
93
- npm install --global rynster
24
+ npm install -g rynster
94
25
  rynster --version
95
26
  ```
96
27
 
97
- Alternatively, install a release tarball:
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
- npm install -g /path/to/rynster-0.1.0.tgz
35
+ rynster register my-app /path/to/my-app
36
+ rynster projects # list registered projects
101
37
  ```
102
38
 
103
- The npm package and executable are both named `rynster`. Current release status
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
- Project configuration is stored in `agent.config.json` in your working directory.
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
- ### Installation troubleshooting
43
+ #### Option A: Local (stdio)
115
44
 
116
- Rynster's `@vscode/ripgrep` dependency supplies native search through a
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
- npm install --global rynster --include=optional
48
+ rynster start my-app --stdio
129
49
  ```
130
50
 
131
- The same `--include=optional` flag works when installing a release tarball. Avoid
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
- Requires Node.js 22.17+ within the 22.x release line, or Node.js 24.2+ within 24.x, and npm 11+.
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "rynster": {
57
+ "command": "rynster",
58
+ "args": ["start", "my-app", "--stdio"]
59
+ }
60
+ }
61
+ }
62
+ ```
137
63
 
138
- Windows private credential storage requires the built-in Windows PowerShell and
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
- ### 2. Register a Project
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
- ```bash
145
- rynster register my-app /path/to/my-app
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
- List registered projects:
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
- ### 3. Start Rynster
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
- #### Remote MCP Mode (modern HTTP):
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 start my-app --stdio
89
+ rynster doctor --url https://your-tunnel-hostname/mcp
183
90
  ```
184
91
 
185
92
  ---
186
93
 
187
- ## Connecting AI Clients
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
- ### Connecting a modern remote MCP client
105
+ Having a tool does not mean every action is allowed. Each one is checked against your permissions and approval rules.
190
106
 
191
- Use a client supporting protocol `2026-07-28`. Rynster implements canonical
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
- Earlier live Claude/ChatGPT onboarding, tool listing, and fixture reads were
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
- 1. Expose the local port through your user-managed HTTPS tunnel.
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
- ```bash
207
- rynster start my-app --public-url https://<your-tunnel-hostname>
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
- 3. Configure the client endpoint as `https://<your-tunnel-hostname>/mcp`, using an authentication method supported by that client. Rynster accepts its owner bearer token or an issued OAuth access token.
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
- The public origin precedence is `--public-url` → `RYNSTER_PUBLIC_URL` → `server.publicUrl`. Startup prints the effective connector URL. Run `rynster doctor --url https://<your-tunnel-hostname>/mcp` for read-only discovery checks. OAuth consent grants only the selected project. Only the current version 2 OAuth storage format is accepted; earlier formats are not imported or migrated.
124
+ When asked to approve an edit, the terminal prompt accepts:
214
125
 
215
- ### Compatibility changes
126
+ - `y` approve
127
+ - `n` reject
128
+ - `d` show the full diff
129
+ - `a` allow writes for this session
216
130
 
217
- The supported protocol is `2026-07-28` only. Modern clients discover the server and send protocol/client metadata on each request. Legacy `initialize` requests receive an unsupported-version error. `/sse` and `/messages` return 410; `POST /` returns 405 and points to `/mcp`. `GET /` remains JSON information even when an SSE Accept header is supplied. There is no legacy fallback.
131
+ If no terminal is available (a headless run), anything that needs approval is denied.
218
132
 
219
- ### Stdio command configuration
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
- "mcpServers": {
225
- "rynster": {
226
- "command": "rynster",
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
- ## Local OAuth grant management
152
+ ## How Rynster keeps your project safe
240
153
 
241
- List the OAuth permissions granted to connected clients:
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
- ```bash
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
- The listing includes complete grant IDs, client identities, projects, public
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
- To cancel one connection's grant, copy its complete ID from the listing:
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
- Revocation blocks that grant's access tokens and refresh tokens, including while
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
- Both commands use `~/.rynster/oauth.json` by default. If Rynster was started with a
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
- ```bash
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
- Selection priority is `--storage` → `RYNSTER_OAUTH_STORAGE_PATH` → `~/.rynster/oauth.json`.
277
- `--config` selects project configuration for other commands; it does not select
278
- OAuth storage. Revocation requires an exact ID, reports unknown IDs as errors,
279
- and is safe to repeat for an already-revoked grant. Malformed, legacy, or aliased
280
- stores are rejected; revocation also respects the server's storage writer lock.
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
- ## License and distribution
188
+ ## Troubleshooting
311
189
 
312
- The Rynster CLI is proprietary and free to use for personal and internal business
313
- purposes, including commercial work and client projects, under the [Rynster CLI
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
- Redistributing, reselling, modifying, or offering the CLI itself as a hosted or
318
- managed service requires separate written permission. Third-party components
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
- The GitHub source repository remains private. The public npm package contains
322
- the runnable JavaScript CLI, which recipients can inspect. Public availability
323
- does not grant an open-source license. Rynster Connect remains proprietary and
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
- ## Support Rynster
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
- You can [support Rynster's development on Ko-fi](https://ko-fi.com/bas12).
329
- Support is optional. To display the link in your terminal:
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
- ```bash
332
- rynster support
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
- The link also appears in `rynster --help` and the npm package's funding metadata.
208
+ ---
336
209
 
337
- ## Local command configuration and migration
210
+ ## Known limitations
338
211
 
339
- Use structured commands in `agent.config.json`:
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
- ```json
342
- {
343
- "commands": {
344
- "test": { "file": "npm", "args": ["test"] },
345
- "typecheck": { "file": "npm", "args": ["run", "typecheck"] }
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
- This `commands` object belongs inside a registered project. For several commands, put the sequence in a locally reviewed script and configure its executable and arguments. An existing simple value such as `"npm test"` still works; values with `&&`, pipes, redirection, or substitutions are rejected. Executable resolution, package manifests/locks, and command configuration are captured before approval and rechecked before execution. New configurations ask for approval for every command category. Existing explicit `allow` policies remain an owner decision to trust project code, including code changed by an assistant.
232
+ ## Support
351
233
 
352
- Project scripts run with the host user's filesystem and network access, including home-directory files. Neither `shell: false`, environment filtering, a project directory, nor a Git worktree provides an OS sandbox. Review project code before approving execution.
234
+ Support is optional. Run `rynster support` or visit [ko-fi.com/bas12](https://ko-fi.com/bas12).
353
235
 
354
- Configuration is saved atomically with mode `0600` and an exclusive writer lock; malformed files and concurrent changes fail visibly. Duplicate project roots, including symlink aliases, are rejected. Startup masks the owner token; use `rynster token --config /path/to/agent.config.json` only when you intend to display it. After upgrading to physical-root grant binding, reconnect existing OAuth clients; earlier path-only grants are rejected.
236
+ ## Release notes
355
237
 
356
- See [Local security policy](SECURITY.md) for vulnerability reporting, the security model, and verification limits. Development roadmaps and historical verification records are maintained in the private source repository.
238
+ See [CHANGELOG.md](CHANGELOG.md).
package/SECURITY.md CHANGED
@@ -1,6 +1,12 @@
1
- # Rynster Local security policy
1
+ # Rynster security policy
2
2
 
3
- Reviewed on 2026-10-18. See the source compatibility report for tested Linux/Windows runtimes and remaining platform limits. Local runs on the developer's machine; Connect hosting is not part of this release.
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. Earlier path-only project grants require fresh consent after this upgrade.
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. Local's threat model trusts the OS user and approved project code. macOS remains unverified.
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 remains 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.
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. Built-in Windows PowerShell is required to apply and verify these ACLs. 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.
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. Local 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.
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
- ## Acceptance and release limits
68
+ ## Managing OAuth grants
59
69
 
60
- Local owners can inspect credential-free grant metadata with `rynster oauth grants`
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. Only Rynster settings and the current storage
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/legacy storage, and remain effective after restart.
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. CLI integration
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
- The automated suite covers HTTP/stdio authorization, same-project grant ownership, refresh ownership, physical root replacement, credential guards, stale/canceled/expired approvals, symlink replacement, approved edits, headless denial, command grammar/environment/output/process cleanup, and configuration races. Installed-tarball checks cover production-only dependency resolution, CLI configuration, native search assets, workflow handles, secret guards, headless command denial, protocol rejection, and shutdown.
79
+ ## Verification status
74
80
 
75
- Earlier development builds under the old package name had user-confirmed real Claude/ChatGPT onboarding, tool listing, and fixture reads. Claude automatic renewal and recovery through a fresh connector after a hostname change are recorded in the private Local OAuth runbook. The user also reported immediate access denial after revoking Claude's grant, a new authorization prompt after restart, and a successful read after fresh consent. On 2026-10-15 the user confirmed the manual helper rejected the same unexpired OAuth token at the new issuer after an HTTP 200 baseline at the old issuer. These historical results do not verify live onboarding with the renamed Rynster tarball. ChatGPT lifecycle checks remain unverified and deferred at the user's request. Passing automated fixtures does not supply live-client evidence. See the source compatibility report for dated Windows/Linux automated and installed-package results; macOS remains unverified. Connect's authenticated headless approval channel and any OS-isolated executor remain future work.
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 &amp; 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.0",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}
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(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rynster",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Provider-neutral local coding agent MCP server",
5
5
  "bugs": {
6
6
  "email": "baskar12ai@gmail.com"