Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
74 changes: 66 additions & 8 deletions apps/example/scripts/check-vercel-output.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const queueFunctionDir = path.join(
"agent",
"continue.func",
);
const compiledAppRoot = "/__junior_content__/app";
const compiledNodeModulesRoot = "/__junior_content__/node_modules";

function fail(message) {
throw new Error(`Vercel output check failed: ${message}`);
Expand All @@ -28,6 +30,12 @@ function requireFile(filePath) {
}
}

function rejectFile(filePath) {
if (existsSync(filePath) && statSync(filePath).isFile()) {
fail(`unexpected copied file ${path.relative(appRoot, filePath)}`);
}
}

function requireDirectory(directoryPath) {
if (!existsSync(directoryPath) || !statSync(directoryPath).isDirectory()) {
fail(`missing directory ${path.relative(appRoot, directoryPath)}`);
Expand All @@ -39,6 +47,32 @@ function readJson(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}

function readVirtualContent(functionDir) {
const virtualContentPath = path.join(functionDir, "_virtual", "content.mjs");
requireFile(virtualContentPath);
const raw = readFileSync(virtualContentPath, "utf8");
const match =
/^export const content = (.*);\n?$/s.exec(raw) ??
/(?:^|\n)const content = (\{[\s\S]*\});\n\/\/#endregion\nexport \{ content \};\n?$/.exec(
raw,
);
if (!match) {
fail(
`${path.relative(appRoot, virtualContentPath)} does not export a compiled content graph`,
);
}

return JSON.parse(match[1]);
}

function requireCompiledFile(content, virtualPath, functionDir) {
if (typeof content?.files?.[virtualPath] !== "string") {
fail(
`${path.relative(appRoot, functionDir)} is missing compiled file ${virtualPath}`,
);
}
}

function packageSourceHasPlugin(packageName) {
const sourceDir = path.join(
repoRoot,
Expand Down Expand Up @@ -76,19 +110,36 @@ function assertQueueTrigger() {

function assertFunctionHasJuniorContent(functionDir, pluginPackages) {
requireFile(path.join(functionDir, "index.mjs"));
requireFile(path.join(functionDir, "app", "SOUL.md"));
requireFile(
path.join(functionDir, "app", "plugins", "example-bundle", "plugin.yaml"),
const content = readVirtualContent(functionDir);
if (content.appRoot !== compiledAppRoot) {
fail(`${path.relative(appRoot, functionDir)} has an unexpected app root`);
}

requireCompiledFile(content, `${compiledAppRoot}/SOUL.md`, functionDir);
rejectFile(path.join(functionDir, "app", "SOUL.md"));
requireCompiledFile(
content,
`${compiledAppRoot}/plugins/example-bundle/plugin.yaml`,
functionDir,
);
requireFile(
path.join(functionDir, "app", "skills", "example-local", "SKILL.md"),
requireCompiledFile(
content,
`${compiledAppRoot}/skills/example-local/SKILL.md`,
functionDir,
);

for (const packageName of pluginPackages) {
requireFile(
requireCompiledFile(
content,
`${compiledNodeModulesRoot}/${packageName}/plugin.yaml`,
functionDir,
);
rejectFile(
path.join(functionDir, "node_modules", packageName, "plugin.yaml"),
);
}

return content;
}

if (existsSync(path.join(appRoot, "api"))) {
Expand All @@ -106,10 +157,17 @@ if (pluginPackages.length === 0) {
fail("no plugin package fixtures were discovered for output validation");
}

const compiledGraphs = [];
for (const functionDir of [serverFunctionDir, queueFunctionDir]) {
assertFunctionHasJuniorContent(functionDir, pluginPackages);
compiledGraphs.push(
assertFunctionHasJuniorContent(functionDir, pluginPackages),
);
}

if (JSON.stringify(compiledGraphs[0]) !== JSON.stringify(compiledGraphs[1])) {
fail("primary and queue functions have different compiled content graphs");
}

console.log(
`Verified Vercel output for ${pluginPackages.length} plugin package(s) in primary and queue functions.`,
`Verified compiled Junior content for ${pluginPackages.length} plugin package(s) in primary and queue functions.`,
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: "createApp"

> **createApp**(`options?`): `Promise`\<`Hono`\<`BlankEnv`, `BlankSchema`, `"/"`\>\>

Defined in: [app.ts:259](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L259)
Defined in: [app.ts:284](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L284)

Create a Hono app with all Junior routes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ title: "juniorNitro"

> **juniorNitro**(`options?`): `object`

Defined in: [nitro.ts:258](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L258)
Defined in: [nitro.ts:285](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L285)

Nitro module that configures deployment wiring and copies app/plugin content into the Vercel build output.
Nitro module that configures deployment wiring and private Junior runtime content.

## Parameters

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ prev: false
title: "JuniorAppOptions"
---

Defined in: [app.ts:53](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L53)
Defined in: [app.ts:57](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L57)

## Properties

### configDefaults?

> `optional` **configDefaults?**: `Record`\<`string`, `unknown`\>

Defined in: [app.ts:55](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L55)
Defined in: [app.ts:59](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L59)

Install-wide provider defaults (`provider.key` format). Channel overrides take precedence.

Expand All @@ -23,7 +23,7 @@ Install-wide provider defaults (`provider.key` format). Channel overrides take p

> `optional` **conversationWork?**: `VercelConversationWorkCallbackOptions`

Defined in: [app.ts:57](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L57)
Defined in: [app.ts:61](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L61)

Queue consumer wiring for the durable conversation worker.

Expand All @@ -33,7 +33,7 @@ Queue consumer wiring for the durable conversation worker.

> `optional` **plugins?**: [`JuniorPluginSet`](/reference/api/interfaces/juniorpluginset/)

Defined in: [app.ts:59](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L59)
Defined in: [app.ts:63](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L63)

Direct plugin set override. Usually omitted when `juniorNitro()` uses a plugin module.

Expand All @@ -43,4 +43,4 @@ Direct plugin set override. Usually omitted when `juniorNitro()` uses a plugin m

> `optional` **waitUntil?**: `WaitUntilFn`

Defined in: [app.ts:60](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L60)
Defined in: [app.ts:64](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L64)
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ prev: false
title: "JuniorNitroOptions"
---

Defined in: [nitro.ts:39](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L39)
Defined in: [nitro.ts:37](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L37)

## Properties

### conversationWorkQueueTopic?

> `optional` **conversationWorkQueueTopic?**: `string`

Defined in: [nitro.ts:43](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L43)
Defined in: [nitro.ts:41](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L41)

Vercel Queue topic for durable conversation work. Must match the runtime queue producer topic.

Expand All @@ -23,15 +23,15 @@ Vercel Queue topic for durable conversation work. Must match the runtime queue p

> `optional` **cwd?**: `string`

Defined in: [nitro.ts:40](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L40)
Defined in: [nitro.ts:38](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L38)

---

### includeFiles?

> `optional` **includeFiles?**: `string`[]

Defined in: [nitro.ts:52](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L52)
Defined in: [nitro.ts:50](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L50)

Extra file patterns to copy into the server output for files that the
bundler cannot trace (e.g. dynamically imported providers).
Expand All @@ -44,14 +44,14 @@ module resolution. Example: `"@earendil-works/pi-ai/dist/providers/*.js"`

> `optional` **maxDuration?**: `number`

Defined in: [nitro.ts:41](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L41)
Defined in: [nitro.ts:39](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L39)

---

### plugins?

> `optional` **plugins?**: `JuniorNitroPluginSource`

Defined in: [nitro.ts:45](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L45)
Defined in: [nitro.ts:43](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L43)

Plugin catalog set or runtime-safe plugin module. Direct sets must not include trusted hooks.
25 changes: 24 additions & 1 deletion packages/junior/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getPluginProviders,
setPluginCatalogConfig,
} from "@/chat/plugins/registry";
import { setRuntimeContent, type JuniorCompiledContent } from "@/chat/content";
import {
type AgentPluginRouteRegistration,
getAgentPluginRoutes,
Expand Down Expand Up @@ -106,6 +107,23 @@ async function resolveVirtualConfig(): Promise<
}
}

/** Resolve private runtime content from the virtual module injected by juniorNitro(). */
async function resolveVirtualContent(): Promise<
JuniorCompiledContent | undefined
> {
try {
const mod: {
content?: JuniorCompiledContent;
} = await import("#junior/content");
return mod.content;
} catch (error) {
if (!isMissingVirtualModule(error, "#junior/content")) {
throw error;
}
return undefined;
}
}

/** Resolve plugin configuration from the env fallback. */
function resolveEnvPluginCatalogConfig(): PluginCatalogConfig | undefined {
const packages = readEnvPluginPackages();
Expand All @@ -116,6 +134,10 @@ function resolveEnvPluginCatalogConfig(): PluginCatalogConfig | undefined {
}

function isMissingVirtualConfig(error: unknown): boolean {
return isMissingVirtualModule(error, "#junior/config");
}

function isMissingVirtualModule(error: unknown, specifier: string): boolean {
if (!(error instanceof Error)) {
return false;
}
Expand All @@ -124,7 +146,7 @@ function isMissingVirtualConfig(error: unknown): boolean {
(code === "ERR_PACKAGE_IMPORT_NOT_DEFINED" ||
code === "ERR_MODULE_NOT_FOUND" ||
code === "MODULE_NOT_FOUND") &&
error.message.includes("#junior/config")
error.message.includes(specifier)
);
}

Expand Down Expand Up @@ -257,6 +279,7 @@ function mountAgentPluginRoutes(

/** Create a Hono app with all Junior routes. */
export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
setRuntimeContent(await resolveVirtualContent());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed createApp leaves stale content

Medium Severity

createApp hydrates compiled content before validation, but its error path only rolls back plugin catalog, agents, and config defaults—not runtime content. If the catalog rolls back to no packages, filterCompiledPluginPackageContent clears package plugins while the compiled graph still contains them, so bundled plugins vanish until process restart.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 44bd010. Configure here.

const virtualConfig = await resolveVirtualConfig();
const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
const agentPlugins =
Expand Down
Loading
Loading