sync: update from private repo (d31b280)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-11 11:28:40 +00:00
parent d061ad08d8
commit 3b1645cc91
96 changed files with 4627 additions and 287 deletions
+112 -14
View File
@@ -16,6 +16,7 @@ import { mountBrandingApi, resolveBranding } from './branding-api.js';
import { createBrowserApi } from './browser-api.js';
import { createBrowserSessionApi } from './browser-session-api.js';
import { createSubtaskActivityRouter } from './subtask-activity-api.js';
import { createUsageRouter } from './usage-api.js';
import { SessionManager } from '../engine/browser-session.js';
import { createNovncRouter, setupNovncWebSocketProxy } from './novnc-proxy.js';
import { setSessionManager } from '../engine/tools/browser.js';
@@ -104,6 +105,11 @@ import { createNotesApi } from './notes-api.js';
import { mountGateway, type GatewayMountHandle } from './gateway-mount.js';
import { readGatewayConfig } from '../gateway/config.js';
import { createAdminGatewayStatusRouter } from './admin-gateway-status-api.js';
import { createServer as createHttpsServer } from 'https';
import { X509Certificate } from 'crypto';
import { mergeServerConfig } from '../server/config.js';
import { resolveTlsOptions } from '../net/tls-options.js';
import { createHttpRedirectServer } from '../net/http-redirect.js';
const __filenameServer = fileURLToPath(import.meta.url);
const __dirnameServer = dirname(__filenameServer);
@@ -112,7 +118,7 @@ export interface CoreServerOptions {
repo: Repository;
worktreeDir?: string;
configuredRepos?: string[];
generateTitle?: (body: string) => Promise<string>;
generateTitle?: (body: string, ownerId?: string) => Promise<string>;
selectPiece?: (body: string, fileNames: string[], userId?: string) => Promise<string>;
configManager?: ConfigManager;
piecesDir?: string;
@@ -170,6 +176,14 @@ export function createCoreServer(opts: CoreServerOptions): {
gatewayMount: GatewayMountHandle | null;
/** True when an OAuth provider or local auth is active. False = no-auth mode. */
authActive: boolean;
/**
* Resolved server config snapshot — computed once with the real listen port
* so both the cookie-secure decision (inside createCoreServer) and the
* listener branch (inside startCoreServer) share the SAME object.
* A config hot-reload between the two would otherwise cause tls.enabled to
* disagree between the cookie flag and the actual listener type.
*/
serverConfig: ReturnType<typeof mergeServerConfig>;
} {
const { repo, worktreeDir } = opts;
const app = express();
@@ -276,6 +290,16 @@ export function createCoreServer(opts: CoreServerOptions): {
}
let authenticateUpgrade: import('./auth.js').UpgradeAuthChecker | undefined;
// Resolve server config ONCE here with the real listen port (threaded in via
// opts.listenPort by startCoreServer). Both the cookie-secure decision below
// and the listener branch in startCoreServer consume this same snapshot so a
// config hot-reload between the two calls cannot produce a mismatch between
// tls.enabled and the cookie secure flag.
const serverCfg = mergeServerConfig(loadConfig().server, {
freshInstall: false,
httpsPort: opts.listenPort ?? Number(process.env['PORT'] ?? 9876),
});
if (authActive) {
// Idempotently seed the shared `local` system admin (id='local', the same
// owner the no-auth path uses) so an existing single-user / no-auth
@@ -286,9 +310,21 @@ export function createCoreServer(opts: CoreServerOptions): {
logger.info(`[auth] seeded local system admin id=local email=${bootstrap.email}`);
}
// Compose effective secureCookie: native TLS termination also requires
// the secure flag on session cookies, even when no upstream proxy is
// present. IMPORTANT: trust-proxy (line ~191) stays keyed on the
// ORIGINAL opts.authConfig.secureCookie — native TLS must NOT enable it.
const effectiveSecureCookie = computeEffectiveSecureCookie(
!!opts.authConfig?.secureCookie,
serverCfg.tls.enabled,
);
const authConfigForSetup = opts.authConfig
? { ...opts.authConfig, secureCookie: effectiveSecureCookie }
: opts.authConfig;
const auth = setupAuth(
repo,
opts.authConfig!,
authConfigForSetup!,
() => {
const b = resolveBranding(opts.configManager);
return { appName: b.appName, loginPageTitle: b.loginPageTitle };
@@ -330,6 +366,7 @@ export function createCoreServer(opts: CoreServerOptions): {
// per-piece write authz (built-in/global-custom → admin, user-custom → owner)
// is enforced inside pieces-api.ts handlers.
app.use('/api/pieces', requireAuth);
app.use('/api/usage', requireAuth);
// Scheduled tasks: any authenticated user can create/list (visibility-filtered).
// PATCH/DELETE owner-or-admin enforcement lives in the handlers (Task 14).
app.use('/api/scheduled-tasks', requireAuth);
@@ -893,10 +930,11 @@ export function createCoreServer(opts: CoreServerOptions): {
});
// --- Local files API ---
mountLocalFilesApi(app, repo);
mountLocalFilesApi(app, repo, { authActive });
// --- Subtask activity API ---
app.use('/api/local/tasks', createSubtaskActivityRouter(repo));
app.use('/api/usage', createUsageRouter(repo, { authActive }));
// --- Subtask files API (listing MUST come before wildcard) ---
mountSubtaskFilesApi(app, repo);
@@ -1176,7 +1214,19 @@ export function createCoreServer(opts: CoreServerOptions): {
return isOwner || user.role === 'admin';
};
return { app, browserSessionManager, authenticateUpgrade, authorizeNovncSession, sshConsole, backendStatusRegistry, workerMetrics, gatewayMount, authActive };
return { app, browserSessionManager, authenticateUpgrade, authorizeNovncSession, sshConsole, backendStatusRegistry, workerMetrics, gatewayMount, authActive, serverConfig: serverCfg };
}
/** Cookie `secure` must be set whenever the user-facing scheme is https —
* via an upstream TLS proxy (secureCookie) OR native TLS termination. */
export function computeEffectiveSecureCookie(secureCookie: boolean, tlsEnabled: boolean): boolean {
return secureCookie || tlsEnabled;
}
/** Heuristic: native TLS + the proxy signal (secure_cookie) likely means a
* reverse proxy is also terminating TLS → double-TLS misconfiguration. */
export function shouldWarnDoubleTls(tlsEnabled: boolean, secureCookie: boolean): boolean {
return tlsEnabled && secureCookie;
}
export function finalizeServer(app: express.Application): express.Application {
@@ -1231,6 +1281,7 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
workerMetrics,
gatewayMount,
authActive,
serverConfig,
// Forward the actual port to createCoreServer so the admin gateway
// status endpoint reports the real bind port (not the PORT env
// guess). See `listenPort` doc on CoreServerOptions.
@@ -1255,18 +1306,65 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
// 127.0.0.1:9876 port mapping instead.
const host = process.env['HOST'] ?? '127.0.0.1';
const isLoopbackBind = host === '127.0.0.1' || host === '::1' || host === 'localhost';
const server = finalApp.listen(port, host, () => {
logger.info(`Core server listening on ${host}:${port}`);
if (!isLoopbackBind && !authActive) {
logger.warn(
`[security] Listening on ${host} with authentication DISABLED. The agent API ` +
`(including the Bash tool) is reachable by anyone who can reach this host — ` +
`this is effectively unauthenticated remote code execution. Enable auth in ` +
`config.yaml (auth.local or an OAuth provider) before exposing a non-loopback ` +
`interface, or unset HOST to bind 127.0.0.1.`,
// Use the config snapshot already resolved in createCoreServer (same port,
// same loadConfig() call) — no second read so a hot-reload between the two
// cannot make the cookie-secure flag disagree with the listener type.
const tls = serverConfig.tls;
let server: import('http').Server | import('https').Server;
if (tls.enabled) {
// Augment the self-signed SAN list with the redirect target host and the
// (non-wildcard) bind host so that browsers following the HTTP→HTTPS redirect
// always land on a hostname that is covered by the certificate. Provided-cert
// deployments are unaffected because resolveTlsOptions ignores selfSignedHosts
// when cert_file/key_file are set.
const extraSan = [
tls.redirectHost,
host && host !== '0.0.0.0' && host !== '::' ? host : null,
].filter((h): h is string => !!h);
const tlsForResolve = extraSan.length
? { ...tls, selfSignedHosts: [...tls.selfSignedHosts, ...extraSan] }
: tls;
const resolved = resolveTlsOptions(tlsForResolve); // fatal throw on bad operator cert
server = createHttpsServer({ cert: resolved.cert, key: resolved.key, minVersion: resolved.minVersion }, finalApp);
server.listen(port, host, () => {
const source = tls.certFile ? `provided(${tls.certFile})` : 'self-signed';
const fp = new X509Certificate(resolved.cert).fingerprint256;
logger.info(`Core server listening on https://${host}:${port} cert=${source} sha256=${fp}`);
if (shouldWarnDoubleTls(true, !!opts.authConfig?.secureCookie)) {
logger.warn(
`[security] server.tls.enabled is ON while auth.secure_cookie is also ON (reverse-proxy signal). ` +
`If a TLS-terminating proxy is in front of this app, set server.tls.enabled: false to avoid double TLS.`,
);
}
});
if (tls.httpRedirect) {
if (tls.redirectHost == null && (host === '0.0.0.0' || host === '::')) {
logger.warn(
`[server] HTTP->HTTPS redirect host falls back to the wildcard bind address (${host}); browsers cannot follow it. ` +
`Set server.tls.redirect_host to the externally reachable hostname.`,
);
}
const pinnedHost = tls.redirectHost ?? (isLoopbackBind ? 'localhost' : host);
const redirector = createHttpRedirectServer({ httpsPort: port, pinnedHost });
redirector.on('error', (e) => logger.warn(`[server] HTTP redirect listener error: ${(e as Error).message}`));
redirector.listen(tls.httpRedirectPort, host, () =>
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${tls.httpRedirectPort}`),
);
}
});
} else {
server = finalApp.listen(port, host, () => {
logger.info(`Core server listening on ${host}:${port}`);
if (!isLoopbackBind && !authActive) {
logger.warn(
`[security] Listening on ${host} with authentication DISABLED. The agent API ` +
`(including the Bash tool) is reachable by anyone who can reach this host — ` +
`this is effectively unauthenticated remote code execution. Enable auth in ` +
`config.yaml (auth.local or an OAuth provider) before exposing a non-loopback ` +
`interface, or unset HOST to bind 127.0.0.1.`,
);
}
});
}
// 起動と同時に CAPTCHA Pool の idle GC を回す (task session を 5 分アイドルで GC)
if (browserSessionManager) browserSessionManager.startIdleGc();