Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/example/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
SLACK_BOT_TOKEN=
SLACK_SIGNING_SECRET=
JUNIOR_SECRET=
DATABASE_URL=
JUNIOR_DATABASE_URL=
REDIS_URL=
JUNIOR_BOT_NAME=junior-example
GITHUB_APP_ID=
Expand Down
3 changes: 2 additions & 1 deletion apps/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ It demonstrates:
- one local skill (`/example-local`)
- one plugin-bundled skill (`/example-bundle-help`)
- one bundle-only plugin (`app/plugins/example-bundle/plugin.yaml`) with no credential broker config
- installed plugin packages (`@sentry/junior-agent-browser`, `@sentry/junior-datadog`, `@sentry/junior-github`, `@sentry/junior-hex`, `@sentry/junior-linear`, `@sentry/junior-notion`, `@sentry/junior-sentry`, `@sentry/junior-vercel`)
- installed plugin packages (`@sentry/junior-agent-browser`, `@sentry/junior-datadog`, `@sentry/junior-github`, `@sentry/junior-hex`, `@sentry/junior-linear`, `@sentry/junior-notion`, `@sentry/junior-scheduler`, `@sentry/junior-sentry`, `@sentry/junior-vercel`)

## Run

Expand All @@ -28,6 +28,7 @@ Copy `.env.example` and set:

- `SLACK_BOT_TOKEN`
- `SLACK_SIGNING_SECRET`
- `DATABASE_URL` or `JUNIOR_DATABASE_URL`
- `REDIS_URL`
- `AI_MODEL` (optional)
- `AI_FAST_MODEL` (optional)
Expand Down
5 changes: 3 additions & 2 deletions apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"predev": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build",
"predev": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior-scheduler build",
"dev": "nitro dev",
"prebuild": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build",
"prebuild": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior-scheduler build",
"build": "junior snapshot create && nitro build",
"postbuild": "node scripts/check-vercel-output.mjs",
"preview": "nitro preview",
Expand All @@ -20,6 +20,7 @@
"@sentry/junior-hex": "workspace:*",
"@sentry/junior-linear": "workspace:*",
"@sentry/junior-notion": "workspace:*",
"@sentry/junior-scheduler": "workspace:*",
"@sentry/junior-sentry": "workspace:*",
"@sentry/junior-vercel": "workspace:*",
"hono": "^4.12.22"
Expand Down
2 changes: 2 additions & 0 deletions apps/example/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineJuniorPlugins } from "@sentry/junior";
import { juniorDashboardPlugin } from "@sentry/junior-dashboard";
import { githubPlugin } from "@sentry/junior-github";
import { schedulerPlugin } from "@sentry/junior-scheduler";
import {
exampleDashboardAuthRequired,
exampleDashboardMockConversations,
Expand All @@ -25,6 +26,7 @@ export const plugins = defineJuniorPlugins([
"@sentry/junior-hex",
"@sentry/junior-linear",
"@sentry/junior-notion",
schedulerPlugin(),
"@sentry/junior-sentry",
"@sentry/junior-vercel",
]);
1 change: 1 addition & 0 deletions packages/junior/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"chat": "4.29.0",
"drizzle-orm": "catalog:",
"hono": "^4.12.22",
"jiti": "^2.7.0",
"jose": "^6.2.3",
"just-bash": "3.0.1",
"node-html-markdown": "^2.0.0",
Expand Down
12 changes: 10 additions & 2 deletions packages/junior/skills/jr-rpc/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
---
name: jr-rpc
description: Manage low-level config flows via jr-rpc bash commands, including setting a default GitHub repo. Use only when the user explicitly asks to read or update provider defaults/config. Do not use for PR, branch, push, or auth-order questions; load the matching provider skill instead.
description: Manage low-level config and plugin introspection flows via jr-rpc bash commands. Use only when the user explicitly asks to read or update provider defaults/config or list installed plugins. Do not use for PR, branch, push, or auth-order questions; load the matching provider skill instead.
allowed-tools: bash
---

# jr-rpc

Manage low-level config flows for the current agent turn.
Manage low-level config and plugin introspection flows for the current agent turn.

## Plugins

`jr-rpc plugins list` — list installed plugins visible to the current Junior runtime.

Command syntax:

- `jr-rpc plugins list`

## Configuration

Expand Down
77 changes: 5 additions & 72 deletions packages/junior/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { generateAssistantReply } from "@/chat/respond";
import { normalizeSandboxEgressTracePropagationDomains } from "@/chat/sandbox/egress-tracing";
import {
getPluginCatalogSignature,
getPluginProviders,
setPluginCatalogConfig,
} from "@/chat/plugins/registry";
import {
Expand All @@ -20,9 +19,13 @@ import {
} from "@/chat/plugins/agent-hooks";
import { validatePluginDatabaseRequirements } from "@/chat/plugins/db";
import type { PluginCatalogConfig } from "@/chat/plugins/types";
import {
validatePluginEgressCredentialHooks,
validatePluginRegistrations,
} from "@/chat/plugins/validation";
import type {
PluginRouteMethod,
PluginRegistration,
PluginRouteMethod,
} from "@sentry/junior-plugin-api";
import {
pluginCatalogConfigFromEnv,
Expand Down Expand Up @@ -205,76 +208,6 @@ function validateBuildIncludesPluginHookRegistrations(
);
}

function validatePluginRegistrations(
registrations: PluginRegistration[],
): void {
const loadedPlugins = getPluginProviders();
const loadedNames = new Set(
loadedPlugins.map((plugin) => plugin.manifest.name),
);

for (const registration of registrations) {
if (!loadedNames.has(registration.manifest.name)) {
throw new Error(
`Plugin registration "${registration.manifest.name}" does not have a matching plugin manifest. Add an inline manifest, packageName, or app-local plugin.yaml with the same name.`,
);
}
}
}

function validatePluginEgressCredentialHooks(
registrations: PluginRegistration[],
): void {
const plugins = new Map(
registrations.map((registration) => [
registration.manifest.name,
registration,
]),
);

for (const provider of getPluginProviders()) {
const hooks = plugins.get(provider.manifest.name)?.hooks;
const hasGrantHook = Boolean(hooks?.grantForEgress);
const hasIssueHook = Boolean(hooks?.issueCredential);
const hasGenericCredentials = Boolean(
provider.manifest.credentials || provider.manifest.apiHeaders,
);
const hasDomains = Boolean(provider.manifest.domains?.length);
const hasHookManagedOAuth = Boolean(
provider.manifest.oauth && !provider.manifest.credentials,
);
if (!hasGrantHook && !hasIssueHook) {
if (hasDomains && !hasGenericCredentials) {
throw new Error(
`Plugin "${provider.manifest.name}" manifest.domains requires egress credential hooks when no generic credentials or apiHeaders are configured.`,
);
}
if (hasHookManagedOAuth) {
throw new Error(
`Plugin "${provider.manifest.name}" manifest.oauth without oauth-bearer credentials requires egress credential hooks.`,
);
}
continue;
}

if (!hasGrantHook || !hasIssueHook) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks must include both grantForEgress and issueCredential.`,
);
}
if (hasGenericCredentials) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks must use manifest.domains instead of generic credentials or apiHeaders.`,
);
}
if (!hasDomains) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks require manifest.domains to list sandbox egress hosts.`,
);
}
}
}

/** Mount plugin HTTP handlers before core routes claim those paths. */
function mountPluginRoutes(app: Hono, routes: PluginRouteRegistration[]): void {
for (const route of routes) {
Expand Down
41 changes: 41 additions & 0 deletions packages/junior/src/chat/capabilities/jr-rpc-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Bash, defineCommand } from "just-bash";
import type { ChannelConfigurationService } from "@/chat/configuration/types";
import { logInfo } from "@/chat/logging";
import { getPluginProviders } from "@/chat/plugins/registry";
import type { Skill } from "@/chat/skills";

type JrRpcDeps = {
Expand Down Expand Up @@ -256,18 +257,58 @@ async function handleConfigCommand(
});
}

async function handlePluginsCommand(
args: string[],
): Promise<ReturnType<typeof commandResult>> {
const usage = "jr-rpc plugins list";
const subverb = (args[0] ?? "").trim();
if (subverb !== "list" || args.length !== 1) {
return commandResult({
stderr: `Usage:\n${usage}\n`,
exitCode: 2,
});
}

const plugins = getPluginProviders()
.map((plugin) => ({
name: plugin.manifest.name,
displayName: plugin.manifest.displayName,
description: plugin.manifest.description,
capabilities: [...plugin.manifest.capabilities],
configKeys: [...plugin.manifest.configKeys],
hasCredentials: Boolean(plugin.manifest.credentials),
hasMcp: Boolean(plugin.manifest.mcp),
hasOAuth: Boolean(plugin.manifest.oauth),
hasSkills: Boolean(plugin.skillsDir),
hasMigrations: Boolean(plugin.migrationsDir),
}))
.sort((left, right) => left.name.localeCompare(right.name));

return commandResult({
stdout: {
ok: true,
plugins,
},
exitCode: 0,
});
}

function createJrRpcCommand(deps: JrRpcDeps) {
return defineCommand("jr-rpc", async (args) => {
const usage = [
"jr-rpc config get <key>",
"jr-rpc config set <key> <value> [--json]",
"jr-rpc config unset <key>",
"jr-rpc config list [--prefix <value>]",
"jr-rpc plugins list",
].join("\n");
const verb = (args[0] ?? "").trim();
if (verb === "config") {
return handleConfigCommand(args.slice(1), deps);
}
if (verb === "plugins") {
return handlePluginsCommand(args.slice(1));
}
return commandResult({
stderr: `Unsupported jr-rpc command. Use:\n${usage}\n`,
exitCode: 2,
Expand Down
74 changes: 74 additions & 0 deletions packages/junior/src/chat/plugins/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { PluginRegistration } from "@sentry/junior-plugin-api";
import { getPluginProviders } from "@/chat/plugins/registry";

/** Validate hook registrations against the loaded plugin manifest catalog. */
export function validatePluginRegistrations(
registrations: PluginRegistration[],
): void {
const loadedPlugins = getPluginProviders();
const loadedNames = new Set(
loadedPlugins.map((plugin) => plugin.manifest.name),
);

for (const registration of registrations) {
if (!loadedNames.has(registration.manifest.name)) {
throw new Error(
`Plugin registration "${registration.manifest.name}" does not have a matching plugin manifest. Add an inline manifest, packageName, or app-local plugin.yaml with the same name.`,
);
}
}
}

/** Validate credential hook registrations against the loaded plugin manifests. */
export function validatePluginEgressCredentialHooks(
registrations: PluginRegistration[],
): void {
const plugins = new Map(
registrations.map((registration) => [
registration.manifest.name,
registration,
]),
);

for (const provider of getPluginProviders()) {
const hooks = plugins.get(provider.manifest.name)?.hooks;
const hasGrantHook = Boolean(hooks?.grantForEgress);
const hasIssueHook = Boolean(hooks?.issueCredential);
const hasGenericCredentials = Boolean(
provider.manifest.credentials || provider.manifest.apiHeaders,
);
const hasDomains = Boolean(provider.manifest.domains?.length);
const hasHookManagedOAuth = Boolean(
provider.manifest.oauth && !provider.manifest.credentials,
);
if (!hasGrantHook && !hasIssueHook) {
if (hasDomains && !hasGenericCredentials) {
throw new Error(
`Plugin "${provider.manifest.name}" manifest.domains requires egress credential hooks when no generic credentials or apiHeaders are configured.`,
);
}
if (hasHookManagedOAuth) {
throw new Error(
`Plugin "${provider.manifest.name}" manifest.oauth without oauth-bearer credentials requires egress credential hooks.`,
);
}
continue;
}

if (!hasGrantHook || !hasIssueHook) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks must include both grantForEgress and issueCredential.`,
);
}
if (hasGenericCredentials) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks must use manifest.domains instead of generic credentials or apiHeaders.`,
);
}
if (!hasDomains) {
throw new Error(
`Plugin "${provider.manifest.name}" egress credential hooks require manifest.domains to list sandbox egress hosts.`,
);
}
}
}
3 changes: 2 additions & 1 deletion packages/junior/src/chat/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ const TOOL_POLICY_RULES = [
`- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
"- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
"- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
"- Run `jr-rpc config get|set|unset|list` for provider defaults and `jr-rpc plugins list` for installed plugin introspection as standalone bash commands; do not chain them with `cd`, `&&`, pipes, or provider commands.",
"- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified.",
];

Expand Down Expand Up @@ -526,7 +527,7 @@ function buildContextSection(params: {
if (configLines) {
blocks.push(
renderTag("configuration", [
"Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
"Ambient provider defaults; explicit targets win.",
...configLines,
]),
);
Expand Down
Loading
Loading