chore: release 0.9.0 — security hardening + warn-don't-block network policy

Release 0.9.0 covering the merged security/reliability PRs (#106 deps/
supply-chain, #107 auth/network, #108 test stability, #110 tmux cwd) plus:

- Network policy: a non-loopback bind without CODEMAN_PASSWORD now STARTS
  with a loud warning (3 ways to secure) instead of refusing to start.
  Loopback stays the safe default. --allow-unauthenticated-network just
  acknowledges (terser note). (src/web/server.ts start())
- Post-install security note explaining the loopback default + safe exposure.
- New docs/security-architecture.md documenting the full model (binding,
  auth pipeline, tunnel req.ip caveat, file-serving, supply-chain, isolation,
  recommended setups). CLAUDE.md Security section + gotcha updated.
- Updated auth-security test: asserts warn-and-start (not throw).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-06-08 19:29:47 +02:00
co-authored by Claude Opus 4.8
parent 4d0586a2aa
commit a8e0e2a343
8 changed files with 409 additions and 23 deletions
+15 -3
View File
@@ -19,6 +19,7 @@ const AUTH_PORT = 3160;
const NOAUTH_PORT = 3161;
const NETWORK_OVERRIDE_PORT = 3162;
const AUTH_RATE_LIMIT_PORT = 3220;
const NOAUTH_NETWORK_PORT = 3221;
const TEST_USER = 'admin';
const TEST_PASS = 'test-password-12345';
@@ -361,11 +362,22 @@ describe('No-Auth Server Startup Policy', () => {
expect(res.status).toBe(200);
});
it('rejects non-loopback startup without a password or explicit override', async () => {
const networkServer = new WebServer(0, false, true, '0.0.0.0');
it('starts with a loud warning (not a hard failure) on a non-loopback bind without a password', async () => {
// Policy (0.9.0): loopback is the safe default, but opting into a non-loopback
// bind without a password no longer refuses to start — it starts and warns,
// pointing at how to secure it. See docs/security-architecture.md.
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const networkServer = new WebServer(NOAUTH_NETWORK_PORT, false, true, '0.0.0.0');
await expect(networkServer.start()).rejects.toThrow(/CODEMAN_PASSWORD/);
await expect(networkServer.start()).resolves.toBeUndefined();
const res = await fetch(`http://localhost:${NOAUTH_NETWORK_PORT}/api/status`);
expect(res.status).toBe(200);
const warned = warnSpy.mock.calls.flat().join('\n');
expect(warned).toMatch(/non-loopback host|NO password/i);
expect(warned).toMatch(/CODEMAN_PASSWORD/);
warnSpy.mockRestore();
await networkServer.stop();
});