@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 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
-
@@ -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
- return this.authStore.validateCredentials(username, password);
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
- return this.authStore.getStatus();
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
- return this.authStore.setPassword({ password, markUserProvided: true });
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);
@@ -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}`);
@@ -3,6 +3,6 @@
3
3
  "version": "0.5.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@opencode-ai/plugin": "1.1.30"
6
+ "@opencode-ai/plugin": "1.1.36"
7
7
  }
8
8
  }
@@ -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 }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralnomads/codenomad",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "CodeNomad Server",
5
5
  "author": {
6
6
  "name": "Neural Nomads",