Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
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
4 changes: 4 additions & 0 deletions agent-docs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TUTORIAL_API_URL=http://localhost:3201
VECTOR_STORE_NAME=docs
# Run `agentuity auth` -- this will be populated in .env
AGENTUITY_SDK_KEY=
12 changes: 10 additions & 2 deletions app/api/page-content/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextRequest } from 'next/server';
import docsJson from '@/content/docs.json';
import { validatePathString } from '@/lib/utils/secure-path';

interface Doc {
file: string;
Expand All @@ -18,8 +19,15 @@ export async function GET(request: NextRequest) {
return new Response('Path parameter required', { status: 400 });
}

if (path.includes('..') || path.includes('\\') || path.startsWith('/')) {
return new Response('Invalid path parameter', { status: 400 });
// Validate path for security issues (but don't require leading slash for this API)
const validation = validatePathString(path);
if (!validation.valid) {
return new Response(`Invalid path parameter: ${validation.error}`, { status: 400 });
}

// Additional check: path shouldn't start with '/' for this specific API
if (path.startsWith('/')) {
return new Response('Invalid path parameter: path should not start with "/"', { status: 400 });
}

const doc = docs.find(
Expand Down
10 changes: 8 additions & 2 deletions app/api/tutorials/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { join } from 'path';
import { parseTutorialMDXCached } from '@/lib/tutorial/mdx-parser';
import { TutorialIdParamsSchema } from '@/lib/tutorial/schemas';
import { getTutorialFilePath } from '@/lib/tutorial';

interface RouteParams {
params: Promise<{ id: string }>;
Expand All @@ -21,7 +21,13 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
}

const { id } = validationResult.data;
const filePath = join(process.cwd(), 'content', 'Tutorial', `${id}.mdx`);
const filePath = await getTutorialFilePath(id);
if (!filePath) {
return NextResponse.json(
{ success: false, error: 'Tutorial not found' },
{ status: 404 }
);
}

const parsed = await parseTutorialMDXCached(filePath);

Expand Down
21 changes: 13 additions & 8 deletions app/api/tutorials/[id]/steps/[stepNumber]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { join } from 'path';
import { parseTutorialMDXCached } from '@/lib/tutorial/mdx-parser';
import { StepParamsSchema } from '@/lib/tutorial/schemas';
import { getTutorialFilePath } from '@/lib/tutorial';

interface RouteParams {
params: Promise<{ id: string; stepNumber: string }>;
Expand All @@ -10,29 +10,34 @@ interface RouteParams {
export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const rawParams = await params;

// Validate and transform parameters with Zod

const validationResult = StepParamsSchema.safeParse(rawParams);
if (!validationResult.success) {
return NextResponse.json(
{ success: false, error: 'Invalid parameters', details: validationResult.error.format() },
{ status: 400 }
);
}

const { id, stepNumber: stepNum } = validationResult.data;

const filePath = join(process.cwd(), 'content', 'Tutorial', `${id}.mdx`);

const filePath = await getTutorialFilePath(id);
if (!filePath) {
return NextResponse.json(
{ success: false, error: 'Tutorial not found' },
{ status: 404 }
);
}
const parsed = await parseTutorialMDXCached(filePath);

const step = parsed.steps.find(s => s.stepNumber === stepNum);
if (!step) {
return NextResponse.json(
{ success: false, error: 'Step not found' },
{ status: 404 }
);
}

return NextResponse.json({
success: true,
data: {
Expand Down
48 changes: 20 additions & 28 deletions app/api/tutorials/route.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,49 @@
import { NextResponse } from 'next/server';
import { readdir } from 'fs/promises';
import { join } from 'path';
import { parseTutorialMDXCached } from '@/lib/tutorial/mdx-parser';
import { TutorialListItemSchema, type TutorialListItem } from '@/lib/tutorial/schemas';
import { getTutorialsConfig, getTutorialFilePath } from '@/lib/tutorial';

export async function GET() {
try {
const tutorialsDir = join(process.cwd(), 'content', 'Tutorial');

// Check if Tutorial directory exists, if not create it for future use
let entries: string[];
try {
entries = await readdir(tutorialsDir);
} catch (error) {
// Return empty array if Tutorial directory doesn't exist yet
return NextResponse.json([]);
}

const mdxFiles = entries.filter(file => file.endsWith('.mdx'));

const config = await getTutorialsConfig();

const tutorials = await Promise.all(
mdxFiles.map(async (file): Promise<TutorialListItem | null> => {
config.tutorials.map(async (tutorialMeta): Promise<TutorialListItem | null> => {
try {
const filePath = join(tutorialsDir, file);
const filePath = await getTutorialFilePath(tutorialMeta.id);
if (!filePath) {
console.warn(`Tutorial file not found for ${tutorialMeta.id}`);
return null;
}
const parsed = await parseTutorialMDXCached(filePath);

const tutorialItem = {
id: file.replace('.mdx', ''),
title: parsed.metadata.title,
description: parsed.metadata.description,
totalSteps: parsed.metadata.totalSteps,
difficulty: parsed.metadata.difficulty,
estimatedTime: parsed.metadata.estimatedTime,
id: tutorialMeta.id,
title: tutorialMeta.title,
description: tutorialMeta.description,
totalSteps: parsed.metadata.totalSteps || parsed.steps.length,
difficulty: tutorialMeta.difficulty,
estimatedTime: tutorialMeta.estimatedTime,
};

// Validate the tutorial list item
const validationResult = TutorialListItemSchema.safeParse(tutorialItem);
if (!validationResult.success) {
console.warn(`Invalid tutorial item ${file}:`, validationResult.error.message);
console.warn(`Invalid tutorial item ${tutorialMeta.id}:`, validationResult.error.message);
return null;
}

return validationResult.data;
} catch (error) {
console.warn(`Failed to parse tutorial ${file}:`, error);
console.warn(`Failed to parse tutorial ${tutorialMeta.id} at ${tutorialMeta.path}:`, error);
return null;
}
})
);

// Filter out failed tutorials
const validTutorials = tutorials.filter(tutorial => tutorial !== null);

return NextResponse.json(validTutorials);
} catch (error) {
console.error('Failed to load tutorials:', error);
Expand Down
25 changes: 8 additions & 17 deletions components/CodeFromFiles.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { readFile } from 'fs/promises';
import path from 'path';
import CodeBlock from '@/app/chat/components/CodeBlock';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
import { readSecureFile } from '@/lib/utils/secure-path';

export interface CodeFromFilesSnippet {
path: string; // repo-root-relative, e.g. "/examples/poc-tutorial/src/agent.ts"
Expand Down Expand Up @@ -41,20 +41,15 @@ export default async function CodeFromFiles(props: CodeFromFilesProps) {
return null;
}

const repoRoot = process.cwd();

const loaded = await Promise.all(
snippets.map(async (s) => {
if (!s.path.startsWith('/')) {
throw new Error('CodeFromFiles: each snippet.path must start with "/" (repo-root-relative)');
}
const absolutePath = path.resolve(repoRoot, `.${s.path}`);
if (!absolutePath.startsWith(repoRoot)) {
throw new Error('CodeFromFiles: resolved path escapes repository root');
}
let fileContent = '';
let content = '';
try {
fileContent = await readFile(absolutePath, 'utf-8');
content = await readSecureFile(s.path, {
from: s.from,
to: s.to,
requireLeadingSlash: true
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
Expand All @@ -63,14 +58,10 @@ export default async function CodeFromFiles(props: CodeFromFilesProps) {
content: `// Failed to load ${s.path}: ${message}`,
};
}
const lines = fileContent.split(/\r?\n/);
const startIdx = Math.max(0, (s.from ? s.from - 1 : 0));
const endIdx = Math.min(lines.length, s.to ? s.to : lines.length);
const sliced = lines.slice(startIdx, endIdx).join('\n');
return {
label: s.title || path.basename(s.path) || s.lang || 'code',
lang: s.lang || inferLanguageFromExtension(s.path) || 'text',
content: sliced,
content,
};
})
);
Expand Down
Loading