Skip to content

Implement config saving logic in gateway server - #5

Open
Ayush-Vish wants to merge 1 commit into
mainfrom
feature-config-saving-logic-4704657250173284408
Open

Implement config saving logic in gateway server#5
Ayush-Vish wants to merge 1 commit into
mainfrom
feature-config-saving-logic-4704657250173284408

Conversation

@Ayush-Vish

@Ayush-Vish Ayush-Vish commented Apr 2, 2026

Copy link
Copy Markdown
Owner

This change implements the logic to save the updated configuration to a persistent scholar.json file in the project root whenever an init_config request is received by the Gateway server. The implementation ensures that the file is written before broadcasting the config_updated event and sending a success response to the client. It also includes error handling for the file I/O operations.


PR created automatically by Jules for task 4704657250173284408 started by @Ayush-Vish

Summary by CodeRabbit

  • Documentation

    • Added design choices documentation covering configuration management strategies and error handling approaches.
  • New Features

    • Configuration settings are now automatically persisted and saved across sessions.
    • Improved error handling ensures users receive clear feedback when configuration save operations encounter issues.

- Add `saveConfig` method to `GatewayServer` using `node:fs/promises`.
- Update `init_config` request handler to save payload to `scholar.json`.
- Ensure success response and event broadcast occur after successful write.
- Add error handling for configuration persistence.
- Document design choices in `DESIGN_CHOICES.md`.
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI review requested due to automatic review settings April 2, 2026 06:08
@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 89fe3aac-6582-42d6-a580-34c121380b12

📥 Commits

Reviewing files that changed from the base of the PR and between 557329a and f694987.

📒 Files selected for processing (2)
  • DESIGN_CHOICES.md
  • src/daemon/gateway/server.ts

📝 Walkthrough

Walkthrough

Added design documentation for config persistence strategy and implemented async file-based persistence in the gateway server. The server now saves received ScholarConfig to scholar.json with error handling, returning success or failure responses to clients.

Changes

Cohort / File(s) Summary
Design Documentation
DESIGN_CHOICES.md
New file documenting design decisions for persisting ScholarConfig to scholar.json and async error handling patterns.
Config Persistence Implementation
src/daemon/gateway/server.ts
Added saveConfig async method and writeFile import from node:fs/promises. Modified init_config handler to persist config payload, send success/failure responses based on write result, and broadcast config_updated on successful save.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 hops with delight
A scholar's config finds its home,
In scholar.json, no more to roam,
Async whispers, promises caught,
Saved with care—a feature well-wrought! 📝✨


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Implement configuration persistence in gateway server

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement config persistence to scholar.json file
• Add saveConfig method using node:fs/promises
• Update init_config handler with async/await pattern
• Include error handling for file I/O operations
• Document design choices and rationale
Diagram
flowchart LR
  A["init_config request"] --> B["saveConfig method"]
  B --> C["writeFile to scholar.json"]
  C --> D["Success response"]
  D --> E["Broadcast config_updated event"]
  C --> F["Error handling"]
  F --> G["Error response to client"]
Loading

Grey Divider

File Changes

1. src/daemon/gateway/server.ts ✨ Enhancement +17/-5

Add config persistence with error handling

• Import writeFile from node:fs/promises for async file operations
• Add private saveConfig method that persists config to scholar.json
• Refactor init_config handler to use async/await with .then().catch() pattern
• Ensure response and event broadcast occur only after successful file write
• Add error handling that sends meaningful error response to client on failure

src/daemon/gateway/server.ts


2. DESIGN_CHOICES.md 📝 Documentation +10/-0

Document configuration persistence design decisions

• Document rationale for persisting ScholarConfig to scholar.json in project root
• Explain choice to use node:fs/promises for async file operations
• Justify .then().catch() pattern for error handling in WebSocket requests
• Clarify that response and event broadcast only occur after successful write

DESIGN_CHOICES.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unauthenticated config overwrite 🐞 Bug ⛨ Security
Description
init_config persists client-provided payloads to scholar.json without any
authentication/authorization, so any connected client can overwrite local configuration (including
secret fields like API keys/tokens). This enables unauthorized configuration tampering via the
Gateway WebSocket.
Code

src/daemon/gateway/server.ts[R51-62]

            case 'init_config':
                console.log('Received config init payload:', request.payload);
-                // Implementation for saving the config logic would go here
-                ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
-
-                // Broadcast an event to all connected clients that config changed
-                this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
+                this.saveConfig(request.payload)
+                    .then(() => {
+                        ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
+                        // Broadcast an event to all connected clients that config changed
+                        this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
+                    })
+                    .catch((err) => {
+                        console.error('Failed to save config:', err);
+                        ws.send(JSON.stringify(createResponse(request.id, false, undefined, `Failed to save config: ${err.message}`)));
+                    });
Evidence
The Gateway accepts WebSocket connections and handles init_config by writing request.payload to
disk via saveConfig() with no authentication/authorization checks. The config shape includes
credential fields, so an unauthenticated overwrite materially impacts security.

src/daemon/gateway/server.ts[10-33]
src/daemon/gateway/server.ts[44-67]
src/daemon/gateway/server.ts[83-87]
src/core/schema.ts[1-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`init_config` writes arbitrary client-provided data to `scholar.json` without any authentication/authorization. Any connected WebSocket client can therefore persist attacker-controlled config (including credentials), which is a security vulnerability.

## Issue Context
The Gateway currently accepts connections and processes requests without identifying the caller. The `ScholarConfig` schema includes sensitive fields (API keys/tokens), increasing the impact of an unauthorized overwrite.

## Fix Focus Areas
- src/daemon/gateway/server.ts[10-33]
- src/daemon/gateway/server.ts[44-67]
- src/daemon/gateway/server.ts[83-87]

## Suggested fix (implementation guidance)
- Restrict exposure by binding the server to loopback only (e.g., set `host: '127.0.0.1'` / `localhost`) and/or validating the remote address.
- Add an authentication mechanism for mutating commands (at minimum `init_config`), e.g.:
 - Require a pre-shared token from env (`SCHOLAR_DAEMON_TOKEN`) and validate it in the request payload/headers before calling `saveConfig`.
 - Optionally enforce an Origin/Host allowlist if used from browsers.
- Validate that `request.payload` conforms to `ScholarConfig` before persisting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Send on closed socket 🐞 Bug ☼ Reliability
Description
The init_config handler calls ws.send() after an async file write without checking
ws.readyState, so a client disconnect during the write can make ws.send() throw. The error-path
ws.send() in .catch() is also unguarded, which can lead to an unhandled rejection and
potentially crash the server.
Code

src/daemon/gateway/server.ts[R53-62]

+                this.saveConfig(request.payload)
+                    .then(() => {
+                        ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
+                        // Broadcast an event to all connected clients that config changed
+                        this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
+                    })
+                    .catch((err) => {
+                        console.error('Failed to save config:', err);
+                        ws.send(JSON.stringify(createResponse(request.id, false, undefined, `Failed to save config: ${err.message}`)));
+                    });
Evidence
broadcastEvent() checks readyState before sending, but the init_config success/error responses
do not, and are executed asynchronously after I/O, increasing the chance the socket has closed by
the time send() is invoked.

src/daemon/gateway/server.ts[51-63]
src/daemon/gateway/server.ts[69-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ws.send()` is called in the `init_config` `.then()` and `.catch()` without verifying the socket is still open. Because the send happens after an awaited file write, the client may have disconnected, causing `ws.send()` to throw; the `.catch()` send is currently unprotected by any outer error handler.

## Issue Context
`broadcastEvent()` already guards sends with `client.readyState === WebSocket.OPEN`, but `init_config` responses do not.

## Fix Focus Areas
- src/daemon/gateway/server.ts[51-63]
- src/daemon/gateway/server.ts[69-77]

## Suggested fix (implementation guidance)
- Before calling `ws.send`, check `ws.readyState === WebSocket.OPEN`.
- Wrap `ws.send` calls in `try/catch`, or use the callback form `ws.send(data, (err) => ...)` and log errors.
- Consider centralizing send logic into a helper like `safeSend(ws, msg)` to avoid repeating this pattern.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Cwd-dependent config path 🐞 Bug ☼ Reliability
Description
saveConfig() writes to ./scholar.json, which depends on the daemon process’s current working
directory, so the file may be saved somewhere other than the project root. This conflicts with the
documented intent to persist scholar.json in the project root.
Code

src/daemon/gateway/server.ts[R83-86]

+    private async saveConfig(config: any) {
+        const configPath = './scholar.json';
+        await writeFile(configPath, JSON.stringify(config, null, 4), 'utf-8');
+        console.log(`Config saved to ${configPath}`);
Evidence
The implementation uses a relative path (./scholar.json) while the design note explicitly states
the file should be persisted to scholar.json in the project root; relative paths resolve from the
process working directory, which may vary depending on how the daemon is started.

src/daemon/gateway/server.ts[83-86]
DESIGN_CHOICES.md[3-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The daemon writes the config to `./scholar.json`, which is relative to `process.cwd()`. If the daemon is launched from another directory, the config will be written to an unexpected location rather than the project root.

## Issue Context
Project documentation states `scholar.json` should live in the project root.

## Fix Focus Areas
- src/daemon/gateway/server.ts[83-86]
- DESIGN_CHOICES.md[3-6]

## Suggested fix (implementation guidance)
- Make the config path explicit and stable, e.g.:
 - Support an env var like `SCHOLAR_CONFIG_PATH` (absolute path recommended).
 - Otherwise resolve deterministically (e.g., `path.resolve(process.cwd(), 'scholar.json')`) and document that the daemon must be started from the repo root if you rely on cwd.
- Log the resolved absolute path to improve operability/debuggability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 51 to +62
case 'init_config':
console.log('Received config init payload:', request.payload);
// Implementation for saving the config logic would go here
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));

// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
this.saveConfig(request.payload)
.then(() => {
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
})
.catch((err) => {
console.error('Failed to save config:', err);
ws.send(JSON.stringify(createResponse(request.id, false, undefined, `Failed to save config: ${err.message}`)));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unauthenticated config overwrite 🐞 Bug ⛨ Security

init_config persists client-provided payloads to scholar.json without any
authentication/authorization, so any connected client can overwrite local configuration (including
secret fields like API keys/tokens). This enables unauthorized configuration tampering via the
Gateway WebSocket.
Agent Prompt
## Issue description
`init_config` writes arbitrary client-provided data to `scholar.json` without any authentication/authorization. Any connected WebSocket client can therefore persist attacker-controlled config (including credentials), which is a security vulnerability.

## Issue Context
The Gateway currently accepts connections and processes requests without identifying the caller. The `ScholarConfig` schema includes sensitive fields (API keys/tokens), increasing the impact of an unauthorized overwrite.

## Fix Focus Areas
- src/daemon/gateway/server.ts[10-33]
- src/daemon/gateway/server.ts[44-67]
- src/daemon/gateway/server.ts[83-87]

## Suggested fix (implementation guidance)
- Restrict exposure by binding the server to loopback only (e.g., set `host: '127.0.0.1'` / `localhost`) and/or validating the remote address.
- Add an authentication mechanism for mutating commands (at minimum `init_config`), e.g.:
  - Require a pre-shared token from env (`SCHOLAR_DAEMON_TOKEN`) and validate it in the request payload/headers before calling `saveConfig`.
  - Optionally enforce an Origin/Host allowlist if used from browsers.
- Validate that `request.payload` conforms to `ScholarConfig` before persisting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +53 to +62
this.saveConfig(request.payload)
.then(() => {
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
})
.catch((err) => {
console.error('Failed to save config:', err);
ws.send(JSON.stringify(createResponse(request.id, false, undefined, `Failed to save config: ${err.message}`)));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Send on closed socket 🐞 Bug ☼ Reliability

The init_config handler calls ws.send() after an async file write without checking
ws.readyState, so a client disconnect during the write can make ws.send() throw. The error-path
ws.send() in .catch() is also unguarded, which can lead to an unhandled rejection and
potentially crash the server.
Agent Prompt
## Issue description
`ws.send()` is called in the `init_config` `.then()` and `.catch()` without verifying the socket is still open. Because the send happens after an awaited file write, the client may have disconnected, causing `ws.send()` to throw; the `.catch()` send is currently unprotected by any outer error handler.

## Issue Context
`broadcastEvent()` already guards sends with `client.readyState === WebSocket.OPEN`, but `init_config` responses do not.

## Fix Focus Areas
- src/daemon/gateway/server.ts[51-63]
- src/daemon/gateway/server.ts[69-77]

## Suggested fix (implementation guidance)
- Before calling `ws.send`, check `ws.readyState === WebSocket.OPEN`.
- Wrap `ws.send` calls in `try/catch`, or use the callback form `ws.send(data, (err) => ...)` and log errors.
- Consider centralizing send logic into a helper like `safeSend(ws, msg)` to avoid repeating this pattern.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements persistence for init_config requests handled by the Gateway WebSocket server by writing the received configuration to scholar.json before responding and broadcasting config_updated.

Changes:

  • Added saveConfig helper that writes the incoming config payload to ./scholar.json.
  • Updated init_config request handling to write the file before sending the success response and broadcasting config_updated, with a basic error response on failure.
  • Added DESIGN_CHOICES.md documenting the persistence and error-handling approach.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/daemon/gateway/server.ts Adds config persistence via writeFile and wires it into the init_config request flow.
DESIGN_CHOICES.md Documents the chosen persistence location and async error-handling strategy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +83 to +86
private async saveConfig(config: any) {
const configPath = './scholar.json';
await writeFile(configPath, JSON.stringify(config, null, 4), 'utf-8');
console.log(`Config saved to ${configPath}`);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configPath is a relative path, so the daemon will write scholar.json relative to the process working directory, which may not be the project root (e.g., when started via a service manager). Consider resolving an absolute path for the intended root (e.g., via path.resolve(...) from a known base) to match the stated behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to +57
case 'init_config':
console.log('Received config init payload:', request.payload);
// Implementation for saving the config logic would go here
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));

// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
this.saveConfig(request.payload)
.then(() => {
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request.payload is optional per RequestMessage, but saveConfig assumes it can always be stringified and written. If payload is missing/invalid, JSON.stringify can produce undefined and writeFile will throw a TypeError. Consider validating request.payload (and ideally its shape) before calling saveConfig, and return a clear client-facing error like "init_config requires a config payload".

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +62
this.saveConfig(request.payload)
.then(() => {
ws.send(JSON.stringify(createResponse(request.id, true, { status: 'saved' })));
// Broadcast an event to all connected clients that config changed
this.broadcastEvent('config_updated', { version: request.payload?.version || '1.0' });
})
.catch((err) => {
console.error('Failed to save config:', err);
ws.send(JSON.stringify(createResponse(request.id, false, undefined, `Failed to save config: ${err.message}`)));
});

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the async init_config flow, ws.send(...) is called inside the Promise callbacks without checking ws.readyState. If the client disconnects before the write completes, ws.send can throw, and the .catch handler may also throw when trying to send the error response. Consider guarding sends with if (ws.readyState === WebSocket.OPEN) (similar to broadcastEvent) or wrapping sends in a try/catch.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +86
const configPath = './scholar.json';
await writeFile(configPath, JSON.stringify(config, null, 4), 'utf-8');
console.log(`Config saved to ${configPath}`);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing directly to scholar.json with writeFile risks leaving a partially-written/truncated config if the process crashes mid-write. For a config file that controls startup/runtime behavior, consider an atomic write strategy (write to a temp file in the same directory, then rename it over the target).

Copilot uses AI. Check for mistakes.
Comment thread DESIGN_CHOICES.md

## Error Handling in WebSocket Requests
- **Choice**: Use `.then().catch()` for asynchronous file operations within the request handler.
- **Rationale**: This prevents the server from hanging if an error occurs during file I/O and allows sending a meaningful error response to the client.

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale says using .then().catch() "prevents the server from hanging" during file I/O. The server/event loop won't inherently hang with async/await either; the key benefit here is ensuring errors are handled and a response is sent back. Consider rewording this to focus on handled rejections / returning an error response rather than implying .then().catch() prevents hangs.

Suggested change
- **Rationale**: This prevents the server from hanging if an error occurs during file I/O and allows sending a meaningful error response to the client.
- **Rationale**: This ensures that promise rejections from file I/O are explicitly handled so the server can always send a meaningful error response to the client instead of leaving the request unresolved.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants