Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
languages: javascript-typescript
queries: +security-extended,security-and-quality
config-file: .github/codeql/codeql-config.yml

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
category: /language:javascript-typescript
16 changes: 8 additions & 8 deletions .github/workflows/dashboard-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ jobs:
CODECOV_TOKEN_PRESENT: ${{ secrets.CODECOV_TOKEN != '' }}
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

Expand All @@ -47,7 +47,7 @@ jobs:
run: bun run test:frontend:coverage

- name: Upload frontend coverage artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: frontend-coverage-lcov
Expand All @@ -56,7 +56,7 @@ jobs:
retention-days: 14

- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
if: ${{ env.CODECOV_TOKEN_PRESENT == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -75,12 +75,12 @@ jobs:
CODECOV_TOKEN_PRESENT: ${{ secrets.CODECOV_TOKEN != '' }}
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

Expand All @@ -98,7 +98,7 @@ jobs:
run: test ! -f backend/coverage/lcov.info || perl -0pi -e 's{^SF:src/}{SF:backend/src/}mg' backend/coverage/lcov.info

- name: Upload backend coverage artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: backend-coverage-lcov
Expand All @@ -107,7 +107,7 @@ jobs:
retention-days: 14

- name: Upload backend coverage to Codecov
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
if: ${{ env.CODECOV_TOKEN_PRESENT == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion backend/src/development/developmentOpenClaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const SENSITIVE_WORKSPACE_FILE_SUFFIXES = [
const SENSITIVE_WORKSPACE_PATH_SEGMENT =
/(?:^|[._-])(?:api[._-]?keys?|credentials?|passwords?|private[._-]?keys?|secrets?|service[._-]?accounts?|tokens?)(?:$|[._-]|\d)/iu;
const SENSITIVE_AGENT_CONFIG_KEY =
/(?:^|[._-])(?:api[._-]?keys?|credentials?|passwords?|secrets?|tokens?)(?:$|[._-]|\d)/iu;
/(?:^|[._-])(?:access[._-]?keys?|api[._-]?keys?|authorization|cookies?|credentials?|keys?|passphrases?|passwords?|private[._-]?keys?|raw|seeds?|secrets?|tokens?|webhook[._-]?urls?)(?:$|[._-]|\d)/iu;

export type DevelopmentWorkspaceState = "copied" | "empty" | "reused";

Expand Down
148 changes: 124 additions & 24 deletions backend/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ChatRuntimeMetrics, GatewayMetrics } from "../../contracts/metrics
import type { Session } from "../../contracts/sessions.ts";
import type { DashboardSettingsResponse } from "../../contracts/settings.ts";
import {
MAX_DASHBOARD_SOCKET_REQUEST_TIMEOUT_MS,
parseDashboardSocketRequest,
readSessionsResponseContainer,
} from "../../contracts/socket.ts";
Expand Down Expand Up @@ -43,6 +44,8 @@ import {
} from "./services/logStreams.ts";

const logger = createStructuredLogger("gateway");
const DEFAULT_FORWARDED_GATEWAY_REQUEST_TIMEOUT_MS = 30_000;
const SESSION_COMPACT_REQUEST_TIMEOUT_MS = 15 * 60_000;

function validateOpenClawRoot(rootPath: string, environmentName: string): string {
const resolved = Path.resolve(rootPath);
Expand Down Expand Up @@ -746,6 +749,108 @@ function isCurrentGatewayClient(expectedClient: OpenClawGatewayClientInstance):
return gatewayState.client === expectedClient;
}

function gatewayString(record: Record<string, unknown>, key: string): string | undefined {
return typeof record[key] === "string" ? record[key] : undefined;
}

function gatewayFiniteNumber(
record: Record<string, unknown>,
key: string
): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

function gatewayBoolean(
record: Record<string, unknown>,
key: string
): boolean | undefined {
return typeof record[key] === "boolean" ? record[key] : undefined;
}

function gatewayTimestamp(value: unknown): number | undefined {
let timestamp = Number.NaN;
if (typeof value === "string") {
timestamp = Date.parse(value);
} else if (typeof value === "number") {
timestamp = value;
}
return Number.isFinite(timestamp) && Math.abs(timestamp) <= 8_640_000_000_000_000
? timestamp
: undefined;
}

function gatewaySessionFromRecord(record: Record<string, unknown>): GatewaySession {
const thinkingLevels = Array.isArray(record.thinkingLevels)
? record.thinkingLevels
.flatMap((value) => {
const level = asRecord(value);
const id = level ? gatewayString(level, "id")?.trim() : undefined;
const label = level ? gatewayString(level, "label")?.trim() : undefined;
return id && label ? [{ id, label }] : [];
})
.slice(0, 100)
: undefined;
const thinkingOptions = Array.isArray(record.thinkingOptions)
? record.thinkingOptions
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean)
.slice(0, 100)
: undefined;
const fastMode =
typeof record.fastMode === "boolean" || record.fastMode === "auto"
? record.fastMode
: undefined;
const effectiveFastMode =
typeof record.effectiveFastMode === "boolean" ||
record.effectiveFastMode === "auto"
? record.effectiveFastMode
: undefined;
const endedAt =
typeof record.endedAt === "string" ||
(typeof record.endedAt === "number" && Number.isFinite(record.endedAt))
? record.endedAt
: undefined;
const startedAt =
typeof record.startedAt === "string" ||
(typeof record.startedAt === "number" && Number.isFinite(record.startedAt))
? record.startedAt
: undefined;
return {
activeRunId: gatewayString(record, "activeRunId"),
channel: gatewayString(record, "channel"),
contextTokens: gatewayFiniteNumber(record, "contextTokens"),
currentRunId: gatewayString(record, "currentRunId"),
displayName: gatewayString(record, "displayName"),
effectiveFastMode,
elevatedLevel: gatewayString(record, "elevatedLevel"),
endedAt,
fastMode,
hasActiveRun: gatewayBoolean(record, "hasActiveRun"),
isRunning: gatewayBoolean(record, "isRunning"),
key: gatewayString(record, "key"),
kind: gatewayString(record, "kind"),
label: gatewayString(record, "label"),
model: gatewayString(record, "model"),
modelProvider: gatewayString(record, "modelProvider"),
reasoningLevel: gatewayString(record, "reasoningLevel"),
runId: gatewayString(record, "runId"),
running: gatewayBoolean(record, "running"),
sessionId: gatewayString(record, "sessionId"),
startedAt,
status: gatewayString(record, "status"),
thinkingDefault: gatewayString(record, "thinkingDefault"),
thinkingLevel: gatewayString(record, "thinkingLevel"),
thinkingLevels,
thinkingOptions,
totalTokens: gatewayFiniteNumber(record, "totalTokens"),
totalTokensFresh: gatewayBoolean(record, "totalTokensFresh"),
updatedAt: gatewayTimestamp(record.updatedAt),
verboseLevel: gatewayString(record, "verboseLevel"),
};
}

/**
* Normalizes one raw Gateway sessions.list response for Dashboard consumers.
* @param response Raw Gateway response.
Expand All @@ -754,7 +859,10 @@ function isCurrentGatewayClient(expectedClient: OpenClawGatewayClientInstance):
export function normalizeGatewaySessionList(response: unknown): Session[] {
const container = readSessionsResponseContainer(response);
const sessions = container?.sessions ?? [];
const defaults = asRecord(container?.defaults) as GatewaySession | undefined;
const defaultsRecord = asRecord(container?.defaults);
const defaults = defaultsRecord
? gatewaySessionFromRecord(defaultsRecord)
: undefined;
return sessions
.map((entry) => asRecord(entry))
.filter(
Expand All @@ -763,25 +871,13 @@ export function normalizeGatewaySessionList(response: unknown): Session[] {
(entry.sessionId === undefined || typeof entry.sessionId === "string") &&
(entry.key === undefined || typeof entry.key === "string") &&
(entry.updatedAt === undefined ||
(typeof entry.updatedAt === "number" &&
Number.isFinite(entry.updatedAt)) ||
(typeof entry.updatedAt === "string" &&
!Number.isNaN(Date.parse(entry.updatedAt)))) &&
gatewayTimestamp(entry.updatedAt) !== undefined) &&
(stringFallback(entry.sessionId).trim() ||
stringFallback(entry.key).trim()) !== ""
)
.map((entry) => {
const session = entry as GatewaySession & {
activeRunId?: string | null | undefined;
currentRunId?: string | null | undefined;
endedAt?: string | number | null | undefined;
runId?: string | null | undefined;
startedAt?: string | number | null | undefined;
};
const updatedAt =
typeof entry.updatedAt === "string"
? Date.parse(entry.updatedAt)
: entry.updatedAt;
const session = gatewaySessionFromRecord(entry);
const updatedAt = gatewayTimestamp(entry.updatedAt);
const shouldApplyDefaults =
(!session.model || session.model === defaults?.model) &&
(!session.modelProvider ||
Expand Down Expand Up @@ -820,13 +916,12 @@ export function normalizeGatewaySessionList(response: unknown): Session[] {
session.effectiveFastMode ??
matchingDefaults?.effectiveFastMode ??
matchingDefaults?.fastMode,
activeRunId:
session.activeRunId === null ? undefined : session.activeRunId,
activeRunId: entry.activeRunId === null ? undefined : session.activeRunId,
currentRunId:
session.currentRunId === null ? undefined : session.currentRunId,
endedAt: session.endedAt === null ? undefined : session.endedAt,
runId: session.runId === null ? undefined : session.runId,
startedAt: session.startedAt === null ? undefined : session.startedAt,
entry.currentRunId === null ? undefined : session.currentRunId,
endedAt: entry.endedAt === null ? undefined : session.endedAt,
runId: entry.runId === null ? undefined : session.runId,
startedAt: entry.startedAt === null ? undefined : session.startedAt,
updatedAt:
typeof updatedAt === "number" && Number.isFinite(updatedAt)
? updatedAt
Expand Down Expand Up @@ -1209,8 +1304,13 @@ async function forwardRequest(
}
const activeGateway = gatewayState.client;
const requestOptions = {
timeoutMs,
shouldWaitIndefinitely: method === "sessions.compact",
timeoutMs:
method === "sessions.compact"
? SESSION_COMPACT_REQUEST_TIMEOUT_MS
: Math.min(
timeoutMs ?? DEFAULT_FORWARDED_GATEWAY_REQUEST_TIMEOUT_MS,
MAX_DASHBOARD_SOCKET_REQUEST_TIMEOUT_MS
),
};

if (clientWs && clientId) {
Expand Down
2 changes: 0 additions & 2 deletions backend/src/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ export function evaluateReadiness(signals: ReadinessSignals): DashboardReadiness
database: signals.database,
frontend: { ready: signals.frontendReady },
release: {
backendCommit: signals.release.backendCommit,
frontendCommit: signals.release.frontendCommit,
...(signals.release.issue && { issue: signals.release.issue }),
...(signals.release.manifestFormatVersion !== undefined && {
manifestFormatVersion: signals.release.manifestFormatVersion,
Expand Down
31 changes: 23 additions & 8 deletions backend/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,19 @@ export function isSecureRequest(request: Request, server: Server<unknown>): bool
);
}

function shouldUseSecureCookies(
request: Request,
server: Server<unknown>,
environment = process.env.NODE_ENV
): boolean {
return environment === "production" || isSecureRequest(request, server);
}

export function sessionCookie(
request: Request,
server: Server<unknown>,
sessionId: string
sessionId: string,
environment = process.env.NODE_ENV
): string {
const cookieParts = [
`${SESSION_COOKIE}=${encodeURIComponent(sessionId)}`,
Expand All @@ -276,21 +285,25 @@ export function sessionCookie(
"SameSite=Strict",
`Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`,
];
if (isSecureRequest(request, server)) {
if (shouldUseSecureCookies(request, server, environment)) {
cookieParts.push("Secure");
}
return cookieParts.join("; ");
}

export function clearSessionCookie(request: Request, server: Server<unknown>): string {
export function clearSessionCookie(
request: Request,
server: Server<unknown>,
environment = process.env.NODE_ENV
): string {
const cookieParts = [
`${SESSION_COOKIE}=`,
"Path=/",
"HttpOnly",
"SameSite=Strict",
"Max-Age=0",
];
if (isSecureRequest(request, server)) {
if (shouldUseSecureCookies(request, server, environment)) {
cookieParts.push("Secure");
}
return cookieParts.join("; ");
Expand All @@ -299,7 +312,8 @@ export function clearSessionCookie(request: Request, server: Server<unknown>): s
export function pendingLoginCookie(
request: Request,
server: Server<unknown>,
pendingLogin: string
pendingLogin: string,
environment = process.env.NODE_ENV
): string {
const cookieParts = [
`${PENDING_LOGIN_COOKIE}=${encodeURIComponent(pendingLogin)}`,
Expand All @@ -308,15 +322,16 @@ export function pendingLoginCookie(
"SameSite=Strict",
`Max-Age=${Math.floor(PENDING_LOGIN_TTL_MS / 1000)}`,
];
if (isSecureRequest(request, server)) {
if (shouldUseSecureCookies(request, server, environment)) {
cookieParts.push("Secure");
}
return cookieParts.join("; ");
}

export function clearPendingLoginCookie(
request: Request,
server: Server<unknown>
server: Server<unknown>,
environment = process.env.NODE_ENV
): string {
const cookieParts = [
`${PENDING_LOGIN_COOKIE}=`,
Expand All @@ -325,7 +340,7 @@ export function clearPendingLoginCookie(
"SameSite=Strict",
"Max-Age=0",
];
if (isSecureRequest(request, server)) {
if (shouldUseSecureCookies(request, server, environment)) {
cookieParts.push("Secure");
}
return cookieParts.join("; ");
Expand Down
Loading