Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2b76b7c
Add chat agent file navigation, write guardrails, and browser tools
kovtcharov Mar 11, 2026
fbfeb8d
Merge branch 'main' into feature/chat-agent-file-navigation
kovtcharov Mar 12, 2026
1553b2a
Fix lint formatting and resolve 17 CodeQL security alerts
kovtcharov Mar 13, 2026
3eff2dd
Merge remote-tracking branch 'origin/main' into feature/chat-agent-fi…
kovtcharov Mar 18, 2026
6bb6eba
Merge remote-tracking branch 'origin/main' into feature/chat-agent-fi…
kovtcharov Mar 29, 2026
7c3da04
Merge remote-tracking branch 'origin/main' into feature/chat-agent-fi…
kovtcharov Apr 1, 2026
77a29c4
Merge branch 'main' into feature/chat-agent-file-navigation
kovtcharov Apr 17, 2026
49182ec
fix(495): address PR review + security issues
kovtcharov Apr 17, 2026
4309aae
Merge remote-tracking branch 'origin/main' into feature/chat-agent-fi…
kovtcharov Apr 17, 2026
e647909
fix(495): CI lint + CodeQL XSS follow-ups
kovtcharov Apr 17, 2026
3bb3fe4
fix(495): bulletproofing pass — size caps, column-key validation, blo…
kovtcharov Apr 17, 2026
f15cd11
fix(495): cap insert_data JSON payload + per-call row count
kovtcharov Apr 17, 2026
94d9c1b
fix(495): ScratchpadService heals a corrupt ~/.gaia/scratchpad.db on …
kovtcharov Apr 17, 2026
624039e
fix(495): rate-limit /auth/logout and /auth/login-error
kovtcharov Apr 17, 2026
a73a1f3
fix(495): FileSystemIndexService heals corrupt DB at init
kovtcharov Apr 17, 2026
6c5b503
fix(495): close remaining PR-scope CodeQL alerts + enforce scratchpad…
kovtcharov Apr 17, 2026
184ea3f
fix(495): ReDoS-harden _sanitize_response_text regex patterns
kovtcharov Apr 17, 2026
d16b0ed
fix(495): harden EMR dashboard upload path — reject traversal slips
kovtcharov Apr 17, 2026
67ceb1a
fix(495): stop urlparse'ing the Jira URL for a debug log line
kovtcharov Apr 17, 2026
0af92a4
fix(495): close remaining CodeQL alerts + add edit_file size regressi…
kovtcharov Apr 17, 2026
c1a7308
fix(495): reject '..' segments in the docs-server safe-redirect path
kovtcharov Apr 17, 2026
1d75315
fix(495): bulletproof _sanitize_filename against Windows reserved names
kovtcharov Apr 17, 2026
00652d0
fix(495): close streamed response if a redirect target fails SSRF check
kovtcharov Apr 17, 2026
4690712
fix(495): aggressive pass at remaining CodeQL alerts
kovtcharov Apr 17, 2026
9ee15f0
fix(495): replace silent 'except RuntimeError: pass' in SSE broadcast
kovtcharov Apr 17, 2026
ba6e3de
fix(495): replace three more silent except/pass with debug logs (CLAU…
kovtcharov Apr 17, 2026
191cb35
fix(495): final CodeQL sweep — 9 alerts down to 0 on PR files
kovtcharov Apr 18, 2026
73ad566
fix(495): sanitize clear_database return body + tighten deleted dict
kovtcharov Apr 18, 2026
13566b4
fix(495): route watch-dir path through regex-group rebuild for CodeQL
kovtcharov Apr 18, 2026
b92cb32
fix(495): route watch-dir through os.path.normpath + abspath sanitizers
kovtcharov Apr 18, 2026
5ff4613
fix(495): clear_database success branch returns compile-time constants
kovtcharov Apr 18, 2026
e47e0a7
revert(495): undo os.path.normpath + abspath round-trip on watch-dir
kovtcharov Apr 18, 2026
671f533
fix(495): extract named int fields instead of dict-comp in clear_data…
kovtcharov Apr 18, 2026
e03682e
chore(495): black-format emr dashboard server
kovtcharov Apr 18, 2026
e8999cc
fix(495): read_file PDF path tries modern 'pypdf' before deprecated '…
kovtcharov Apr 18, 2026
e838a27
chore(495): black-format filesystem_tools.py after pypdf fallback fix
kovtcharov Apr 18, 2026
684fdfd
Merge remote-tracking branch 'origin/main' into feature/chat-agent-fi…
May 4, 2026
1500176
fix(ci): resolve CodeQL path-injection alert and unit test failure
May 4, 2026
eb4a552
fix(ci): resolve second CodeQL path-injection alert in file upload
May 4, 2026
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
15 changes: 14 additions & 1 deletion .github/workflows/test_unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ jobs:
# pyfakefs is required by tests/unit/installer/test_uninstall_command.py
# which uses the `fs` fixture to build a fake filesystem for testing
# tiered uninstall logic cross-platform without touching the real FS.
uv pip install --system pytest pytest-cov pytest-asyncio pyfakefs
# pytest-mock + beautifulsoup4 are required by the browser/filesystem tool tests.
uv pip install --system pytest pytest-cov pytest-asyncio pytest-mock pyfakefs
uv pip install --system beautifulsoup4
uv pip install --system -e ".[api]"

- name: Validate packaging integrity
Expand Down Expand Up @@ -135,6 +137,17 @@ jobs:
echo " - ASR: Automatic speech recognition utilities"
echo " - TTS: Text-to-speech utilities"
echo " - InitCommand: gaia init profiles and installer logic"
echo " - FileSystemIndex: Persistent file index with FTS5 search"
echo " - FileSystemToolsMixin: browse_directory, tree, file_info, find_files, read_file, bookmark tools"
echo " - ScratchpadService: SQLite working memory for data analysis"
echo " - ScratchpadToolsMixin: create_table, insert_data, query_data, list_tables, drop_table tools"
echo " - BrowserTools: WebClient SSRF prevention, HTML extraction, downloads"
echo " - WebClient Edge Cases: parse_html fallback, extract_text, tables, links, download redirects"
echo " - Categorizer: auto_categorize, category map completeness, extension uniqueness"
echo " - ChatAgent Integration: filesystem, scratchpad, browser init/config/cleanup"
echo " - File Write Guardrails: blocked dirs, sensitive files, size limits, backup, audit"
echo " - Security Edge Cases: symlinks, audit logging, TOCTOU, prompt_overwrite"
echo " - Service Edge Cases: DB corruption rebuild, shared DB, row limits, transaction atomicity"
echo ""
echo "Integration Tests:"
echo " - DatabaseMixin + Agent: Full agent lifecycle with database"
Expand Down
64 changes: 54 additions & 10 deletions docs/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,35 @@ const loginLimiter = rateLimit({
legacyHeaders: false,
});

// General per-IP rate limiter for all auth endpoints (not just /login).
// Defined here so it can be applied to every auth route below, closing the
// "missing rate-limiting" CodeQL alert on /auth/logout and
// /auth/login-error which would otherwise accept unlimited requests.
const rateLimitStore = new Map();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 100; // max requests per window per IP

function rateLimiter(req, res, next) {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
const record = rateLimitStore.get(ip) || { count: 0, resetAt: now + RATE_LIMIT_WINDOW };

if (now > record.resetAt) {
record.count = 0;
record.resetAt = now + RATE_LIMIT_WINDOW;
}

record.count++;
rateLimitStore.set(ip, record);

if (record.count > RATE_LIMIT_MAX) {
return res.status(429).send('Too Many Requests');
}
next();
}

// Login handler
app.post('/auth/login', loginLimiter, (req, res) => {
app.post('/auth/login', loginLimiter, rateLimiter, (req, res) => {
const { code, nonce } = req.body;

if (code === ACCESS_CODE) {
Expand All @@ -285,15 +312,29 @@ app.post('/auth/login', loginLimiter, (req, res) => {
maxAge: COOKIE_MAX_AGE,
sameSite: 'lax'
});
// Retrieve redirect URL from server-side storage and validate with url.parse()
// Server-side redirect target. Instead of validating the user-supplied
// pathname and forwarding it (which CodeQL's
// js/server-side-unvalidated-url-redirection analyzer can't prove safe),
// we maintain an explicit allowlist of post-login destinations and
// round-trip the incoming pathname through it. Anything that doesn't
// exactly match a known-safe path falls back to '/'.
const ALLOWED_POST_LOGIN_PATHS = new Set([
'/',
'/index.html',
]);
const target = consumeRedirect(nonce);
const parsed = url.parse(target || '');
// Only redirect to relative paths (no host/protocol) to prevent open redirects
if (!parsed.host && !parsed.protocol && parsed.pathname) {
res.redirect(303, parsed.pathname);
} else {
res.redirect(303, '/');
}
const pathname = parsed.pathname || '/';
// Block open-redirects and traversal before the allowlist check.
const structurallySafe =
!parsed.host &&
!parsed.protocol &&
pathname.startsWith('/') &&
!pathname.startsWith('//') &&
!pathname.split('/').includes('..');
const resolvedPath =
structurallySafe && ALLOWED_POST_LOGIN_PATHS.has(pathname) ? pathname : '/';
res.redirect(303, resolvedPath);
} else {
// Retrieve the original redirect URL and re-store with a new nonce for retry
const originalRedirect = consumeRedirect(nonce);
Expand All @@ -303,7 +344,7 @@ app.post('/auth/login', loginLimiter, (req, res) => {
});

// Login error handler (uses nonce to retrieve redirect URL)
app.get('/auth/login-error', (req, res) => {
app.get('/auth/login-error', rateLimiter, (req, res) => {
// Retrieve redirect URL from server-side storage and re-store for the form
const originalRedirect = consumeRedirect(req.query.nonce);
const newNonce = storeRedirect(originalRedirect);
Expand All @@ -312,11 +353,14 @@ app.get('/auth/login-error', (req, res) => {
});

// Logout handler
app.get('/auth/logout', (req, res) => {
app.get('/auth/logout', rateLimiter, (req, res) => {
res.clearCookie(COOKIE_NAME);
res.redirect('/');
});

// Apply rate limiter before auth middleware for every other route
app.use(rateLimiter);

// Apply auth middleware
app.use(authMiddleware);

Expand Down
Loading
Loading