@neuralnomads/codenomad 0.9.1 → 0.9.3
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/README.md +10 -1
- package/dist/auth/manager.js +35 -3
- package/dist/config/schema.js +2 -0
- package/dist/index.js +13 -1
- package/dist/opencode-config/package.json +1 -1
- package/dist/server/http-server.js +6 -0
- package/package.json +1 -1
- package/public/assets/index-Ciqv3YgW.js +1 -0
- package/public/assets/{index-DszBjqMJ.css → index-D0YCVzdv.css} +1 -1
- package/public/assets/index-DPFZXALm.js +1 -0
- package/public/assets/loading-CnHVCykW.js +1 -0
- package/public/assets/main-CgwL5PpX.js +184 -0
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/ui-version.json +1 -1
- package/public/assets/index-t5GTLZC8.js +0 -1
- package/public/assets/loading-A1Zxq2rg.js +0 -1
- package/public/assets/main-4pIHKAk7.js +0 -184
package/README.md
CHANGED
|
@@ -51,8 +51,17 @@ You can configure the server using flags or environment variables:
|
|
|
51
51
|
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
|
52
52
|
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
|
53
53
|
| `--log-level <level>` | `CLI_LOG_LEVEL` | Logging level (trace, debug, info, warn, error) |
|
|
54
|
+
| `--username <username>` | `CODENOMAD_SERVER_USERNAME` | Username for CodeNomad's internal auth (default `codenomad`) |
|
|
55
|
+
| `--password <password>` | `CODENOMAD_SERVER_PASSWORD` | Password for CodeNomad's internal auth |
|
|
56
|
+
| `--generate-token` | `CODENOMAD_GENERATE_TOKEN` | Emit a one-time local bootstrap token for desktop flows |
|
|
57
|
+
| `--dangerously-skip-auth` | `CODENOMAD_SKIP_AUTH` | Disable CodeNomad's internal auth (use only behind a trusted perimeter) |
|
|
58
|
+
|
|
59
|
+
### Authentication
|
|
60
|
+
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
|
61
|
+
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
|
62
|
+
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
|
63
|
+
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
|
54
64
|
|
|
55
65
|
### Data Storage
|
|
56
66
|
- **Config**: `~/.config/codenomad/config.json`
|
|
57
67
|
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
|
58
|
-
|
package/dist/auth/manager.js
CHANGED
|
@@ -12,6 +12,12 @@ export class AuthManager {
|
|
|
12
12
|
this.logger = logger;
|
|
13
13
|
this.sessionManager = new SessionManager();
|
|
14
14
|
this.cookieName = DEFAULT_AUTH_COOKIE_NAME;
|
|
15
|
+
this.authEnabled = !Boolean(init.dangerouslySkipAuth);
|
|
16
|
+
if (!this.authEnabled) {
|
|
17
|
+
this.authStore = null;
|
|
18
|
+
this.tokenManager = null;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
15
21
|
const authFilePath = resolveAuthFilePath(init.configPath);
|
|
16
22
|
this.authStore = new AuthStore(authFilePath, logger.child({ component: "auth" }));
|
|
17
23
|
// Startup: password comes from CLI/env, auth.json, or bootstrap-only mode.
|
|
@@ -22,6 +28,9 @@ export class AuthManager {
|
|
|
22
28
|
});
|
|
23
29
|
this.tokenManager = init.generateToken ? new TokenManager(60000) : null;
|
|
24
30
|
}
|
|
31
|
+
isAuthEnabled() {
|
|
32
|
+
return this.authEnabled;
|
|
33
|
+
}
|
|
25
34
|
getCookieName() {
|
|
26
35
|
return this.cookieName;
|
|
27
36
|
}
|
|
@@ -39,21 +48,38 @@ export class AuthManager {
|
|
|
39
48
|
return this.tokenManager.consume(token);
|
|
40
49
|
}
|
|
41
50
|
validateLogin(username, password) {
|
|
42
|
-
|
|
51
|
+
if (!this.authEnabled) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return this.requireAuthStore().validateCredentials(username, password);
|
|
43
55
|
}
|
|
44
56
|
createSession(username) {
|
|
57
|
+
if (!this.authEnabled) {
|
|
58
|
+
return { id: "auth-disabled", createdAt: Date.now(), username: this.init.username };
|
|
59
|
+
}
|
|
45
60
|
return this.sessionManager.createSession(username);
|
|
46
61
|
}
|
|
47
62
|
getStatus() {
|
|
48
|
-
|
|
63
|
+
if (!this.authEnabled) {
|
|
64
|
+
return { username: this.init.username, passwordUserProvided: false };
|
|
65
|
+
}
|
|
66
|
+
return this.requireAuthStore().getStatus();
|
|
49
67
|
}
|
|
50
68
|
setPassword(password) {
|
|
51
|
-
|
|
69
|
+
if (!this.authEnabled) {
|
|
70
|
+
throw new Error("Internal authentication is disabled");
|
|
71
|
+
}
|
|
72
|
+
return this.requireAuthStore().setPassword({ password, markUserProvided: true });
|
|
52
73
|
}
|
|
53
74
|
isLoopbackRequest(request) {
|
|
54
75
|
return isLoopbackAddress(request.socket.remoteAddress);
|
|
55
76
|
}
|
|
56
77
|
getSessionFromRequest(request) {
|
|
78
|
+
if (!this.authEnabled) {
|
|
79
|
+
// When auth is disabled, treat all requests as authenticated.
|
|
80
|
+
// We still return a stable username so callers can display it.
|
|
81
|
+
return { username: this.init.username, sessionId: "auth-disabled" };
|
|
82
|
+
}
|
|
57
83
|
const cookies = parseCookies(request.headers.cookie);
|
|
58
84
|
const sessionId = cookies[this.cookieName];
|
|
59
85
|
const session = this.sessionManager.getSession(sessionId);
|
|
@@ -67,6 +93,12 @@ export class AuthManager {
|
|
|
67
93
|
clearSessionCookie(reply) {
|
|
68
94
|
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0 }));
|
|
69
95
|
}
|
|
96
|
+
requireAuthStore() {
|
|
97
|
+
if (!this.authStore) {
|
|
98
|
+
throw new Error("Auth store is unavailable");
|
|
99
|
+
}
|
|
100
|
+
return this.authStore;
|
|
101
|
+
}
|
|
70
102
|
}
|
|
71
103
|
function resolveAuthFilePath(configPath) {
|
|
72
104
|
const resolvedConfigPath = resolvePath(configPath);
|
package/dist/config/schema.js
CHANGED
|
@@ -10,8 +10,10 @@ const PreferencesSchema = z.object({
|
|
|
10
10
|
thinkingBlocksExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
|
11
11
|
showTimelineTools: z.boolean().default(true),
|
|
12
12
|
lastUsedBinary: z.string().optional(),
|
|
13
|
+
locale: z.string().optional(),
|
|
13
14
|
environmentVariables: z.record(z.string()).default({}),
|
|
14
15
|
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
|
16
|
+
modelFavorites: z.array(ModelPreferenceSchema).default([]),
|
|
15
17
|
modelThinkingSelections: z.record(z.string(), z.string()).default({}),
|
|
16
18
|
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
|
17
19
|
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
package/dist/index.js
CHANGED
|
@@ -51,9 +51,16 @@ function parseCliOptions(argv) {
|
|
|
51
51
|
.addOption(new Option("--password <password>", "Password for server authentication").env("CODENOMAD_SERVER_PASSWORD"))
|
|
52
52
|
.addOption(new Option("--generate-token", "Emit a one-time bootstrap token for desktop")
|
|
53
53
|
.env("CODENOMAD_GENERATE_TOKEN")
|
|
54
|
+
.default(false))
|
|
55
|
+
.addOption(new Option("--dangerously-skip-auth", "Disable CodeNomad's internal auth. Use only behind a trusted perimeter (SSO/VPN/etc).")
|
|
56
|
+
.env("CODENOMAD_SKIP_AUTH")
|
|
54
57
|
.default(false));
|
|
55
58
|
program.parse(argv, { from: "user" });
|
|
56
59
|
const parsed = program.opts();
|
|
60
|
+
const parseBooleanEnv = (value) => {
|
|
61
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
62
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on";
|
|
63
|
+
};
|
|
57
64
|
const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd();
|
|
58
65
|
const normalizedHost = resolveHost(parsed.host);
|
|
59
66
|
const autoUpdateString = (parsed.uiAutoUpdate ?? "true").trim().toLowerCase();
|
|
@@ -75,6 +82,7 @@ function parseCliOptions(argv) {
|
|
|
75
82
|
authUsername: parsed.username,
|
|
76
83
|
authPassword: parsed.password,
|
|
77
84
|
generateToken: Boolean(parsed.generateToken),
|
|
85
|
+
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
function parsePort(input) {
|
|
@@ -110,6 +118,9 @@ async function main() {
|
|
|
110
118
|
authPassword: options.authPassword ? "[REDACTED]" : undefined,
|
|
111
119
|
};
|
|
112
120
|
logger.info({ options: logOptions }, "Starting CodeNomad CLI server");
|
|
121
|
+
if (options.dangerouslySkipAuth) {
|
|
122
|
+
logger.warn("DANGEROUS: internal authentication is disabled (--dangerously-skip-auth / CODENOMAD_SKIP_AUTH).");
|
|
123
|
+
}
|
|
113
124
|
const eventBus = new EventBus(eventLogger);
|
|
114
125
|
const isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
115
126
|
const serverMeta = {
|
|
@@ -127,8 +138,9 @@ async function main() {
|
|
|
127
138
|
username: options.authUsername,
|
|
128
139
|
password: options.authPassword,
|
|
129
140
|
generateToken: options.generateToken,
|
|
141
|
+
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
|
130
142
|
}, logger.child({ component: "auth" }));
|
|
131
|
-
if (options.generateToken) {
|
|
143
|
+
if (options.generateToken && !options.dangerouslySkipAuth) {
|
|
132
144
|
const token = authManager.issueBootstrapToken();
|
|
133
145
|
if (token) {
|
|
134
146
|
console.log(`${BOOTSTRAP_TOKEN_STDOUT_PREFIX}${token}`);
|
|
@@ -288,6 +288,12 @@ async function proxyWorkspaceRequest(args) {
|
|
|
288
288
|
if (instanceAuthHeader) {
|
|
289
289
|
headers.authorization = instanceAuthHeader;
|
|
290
290
|
}
|
|
291
|
+
// Enforce per-workspace directory scoping for all proxied OpenCode requests.
|
|
292
|
+
// OpenCode expects the *full* path; we send it via header to avoid query tampering.
|
|
293
|
+
const directory = workspace.path;
|
|
294
|
+
const isNonASCII = /[^\x00-\x7F]/.test(directory);
|
|
295
|
+
const encodedDirectory = isNonASCII ? encodeURIComponent(directory) : directory;
|
|
296
|
+
headers["x-opencode-directory"] = encodedDirectory;
|
|
291
297
|
return headers;
|
|
292
298
|
},
|
|
293
299
|
onError: (proxyReply, { error }) => {
|