## WS5: Developer Experience (DX) Improvements ### Purpose Reduce time-to-first-successful-agent-call to under 5 minutes for a new developer. Three concrete improvements: (1) upgrade the developer portal's API explorer from Swagger UI v4 to Stoplight Elements — a modern, component-based API documentation experience with better navigation, code samples, and mock server support; (2) add a scaffold generator endpoint that returns a language-specific starter project pre-wired with the developer's agent credentials as a downloadable ZIP; (3) add a `sentryagent scaffold` CLI command that calls the scaffold endpoint and extracts the ZIP into the current directory. ### New Endpoint #### `GET /sdk/scaffold/:agentId` **Summary:** Generate and return a language-specific scaffold ZIP for the specified agent. **Authentication:** Bearer token (tenant-scoped). The authenticated tenant must own the specified agent. **Path Parameter:** | Parameter | Type | Description | |---|---|---| | `agentId` | string (UUID) | The agent for which to generate the scaffold | **Query Parameters:** | Parameter | Type | Required | Default | Constraints | |---|---|---|---|---| | `language` | string | no | `typescript` | Enum: `typescript`, `python`, `go`, `java`, `rust` | **Response 200:** - Content-Type: `application/zip` - Content-Disposition: `attachment; filename="sentryagent-scaffold-{agentName}-{language}.zip"` - Body: Binary ZIP archive stream **ZIP Archive Contents (TypeScript example):** ``` sentryagent-scaffold-my-agent-typescript/ ├── package.json (name: my-agent, version: 0.1.0, deps: sentryagent-idp-sdk) ├── tsconfig.json (strict mode, ES2022 target) ├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=, AGENTIDP_CLIENT_SECRET=) ├── .gitignore (.env on first line) ├── src/ │ └── index.ts (imports SDK, creates client from env, issues token, logs success) └── README.md (step-by-step: cp .env.example .env, fill secret, npm install, npm start) ``` **ZIP Archive Contents (Python example):** ``` sentryagent-scaffold-my-agent-python/ ├── requirements.txt (sentryagent-idp) ├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=, AGENTIDP_CLIENT_SECRET=) ├── .gitignore (.env on first line) ├── main.py (imports SDK, creates client from env, issues token, prints success) └── README.md (step-by-step: cp .env.example .env, fill secret, pip install -r requirements.txt, python main.py) ``` **ZIP Archive Contents (Go example):** ``` sentryagent-scaffold-my-agent-go/ ├── go.mod (module: my-agent, dep: github.com/sentryagent/sentryagent-idp-go) ├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=, AGENTIDP_CLIENT_SECRET=) ├── .gitignore (.env on first line) ├── main.go (imports SDK, creates client from env, issues token, logs success) └── README.md (step-by-step instructions) ``` **Error Responses:** | Status | Code | Description | |---|---|---| | 400 | `INVALID_LANGUAGE` | `language` query param is not one of the supported values | | 401 | `UNAUTHORIZED` | Missing or invalid Bearer token | | 403 | `FORBIDDEN` | Authenticated tenant does not own this agent | | 404 | `AGENT_NOT_FOUND` | No agent with `agentId` found | | 429 | `RATE_LIMITED` | Rate limit exceeded | **Business Rules:** - `clientId` is pre-filled in `.env.example` — taken from the agent's credentials in the database - `clientSecret` is always a `` placeholder — never returned in scaffold (credentials security policy) - The ZIP is generated in memory using `archiver` — no disk writes on the server - Scaffold generation is rate-limited to 10 requests per minute per tenant (separate from the main tier rate limit) - An audit log entry is created with `event_type: "scaffold.generated"`, `metadata.language` --- ### Developer Portal: Elements API Explorer Upgrade **File to modify:** `portal/app/api-explorer/page.tsx` **Current state (Phase 4):** Embeds `swagger-ui-react` (Swagger UI v4) loaded from `NEXT_PUBLIC_API_URL/openapi.json`. **New state (Phase 5):** Replaces `swagger-ui-react` with `@stoplight/elements` (`` component). Stoplight Elements provides: three-panel layout (navigation, docs, try-it), built-in code samples in multiple languages, mock server support, and better mobile responsiveness. **Implementation:** ```tsx // portal/app/api-explorer/page.tsx (complete replacement) 'use client'; import { API } from '@stoplight/elements'; import '@stoplight/elements/styles.min.css'; export default function ApiExplorerPage() { return (
); } ``` **Files modified:** - `portal/app/api-explorer/page.tsx` — replace Swagger UI component with Elements `` component - `portal/package.json` — replace `swagger-ui-react` with `@stoplight/elements` --- ### CLI: `sentryagent scaffold` Command **File to create:** `cli/src/commands/scaffold.ts` **Command syntax:** ``` sentryagent scaffold --agent-id [--language typescript|python|go|java|rust] [--out ] ``` **Options:** | Option | Alias | Default | Description | |---|---|---|---| | `--agent-id ` | `-a` | (required) | Agent ID to scaffold for | | `--language ` | `-l` | `typescript` | Target language for scaffold | | `--out ` | `-o` | `.` (current dir) | Directory to extract scaffold ZIP into | **Behavior:** 1. Load config from `~/.sentryagent/config.json` — fail with helpful message if not configured 2. Issue an API call: `GET /sdk/scaffold/{agentId}?language={language}` with Bearer token from `POST /oauth2/token` 3. Receive ZIP stream, pipe through `unzipper` to extract into `--out` directory 4. Print success message: `Scaffold generated at ./{agentName}-{language}/` 5. Print next steps: ``` Next steps: 1. cd {agentName}-{language} 2. cp .env.example .env 3. Add your AGENTIDP_CLIENT_SECRET to .env 4. npm install (or equivalent for your language) 5. npm start ``` **Error handling:** - Agent not found: print `Agent {agentId} not found.` - Forbidden: print `You do not own agent {agentId}.` - Invalid language: print `Unsupported language '{lang}'. Choose: typescript, python, go, java, rust` - Output directory does not exist: create it (with user prompt for confirmation if non-empty) **New CLI dependencies** (add to `cli/package.json`): - `unzipper` — streaming ZIP extraction (pure JS, no native deps) ### New Source Files | File | Description | |---|---| | `src/services/ScaffoldService.ts` | Business logic: build ZIP archive in memory using `archiver` | | `src/controllers/ScaffoldController.ts` | HTTP handler: stream ZIP response | | `src/routes/scaffold.ts` | Express router: `GET /sdk/scaffold/:agentId` | | `src/types/scaffold.ts` | TypeScript interfaces: `ScaffoldLanguage`, `ScaffoldOptions`, `ScaffoldTemplate` | | `src/templates/scaffold/typescript/` | Template files for TypeScript scaffold (package.json, tsconfig.json, index.ts, .env.example, .gitignore, README.md) | | `src/templates/scaffold/python/` | Template files for Python scaffold (requirements.txt, main.py, .env.example, .gitignore, README.md) | | `src/templates/scaffold/go/` | Template files for Go scaffold (go.mod, main.go, .env.example, .gitignore, README.md) | | `src/templates/scaffold/java/` | Template files for Java scaffold (pom.xml, Main.java, .env.example, .gitignore, README.md) | | `src/templates/scaffold/rust/` | Template files for Rust scaffold (Cargo.toml, src/main.rs, .env.example, .gitignore, README.md) | | `cli/src/commands/scaffold.ts` | CLI scaffold command implementation | ### Modified Source Files | File | Change | |---|---| | `src/routes/index.ts` | Register `scaffold` router | | `src/app.ts` | No change needed (routes registered via index) | | `package.json` (API) | Add `archiver` and `@types/archiver` | | `portal/app/api-explorer/page.tsx` | Replace Swagger UI with Elements | | `portal/package.json` | Replace `swagger-ui-react` with `@stoplight/elements` | | `cli/src/index.ts` | Register `scaffold` command with Commander | | `cli/package.json` | Add `unzipper` and `@types/unzipper` | | `docs/openapi.yaml` | Add `GET /sdk/scaffold/:agentId` endpoint | ### `ScaffoldService` Interface ```typescript interface IScaffoldService { /** * Generate an in-memory ZIP archive for the given agent and language. * Returns a Node.js Readable stream of the ZIP binary. * Template variables injected: {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}} */ generateScaffold( agentId: string, language: ScaffoldLanguage, apiUrl: string ): Promise<{ stream: NodeJS.ReadableStream; filename: string }>; } ``` ### Prometheus Metrics | Metric | Type | Labels | Description | |---|---|---|---| | `agentidp_scaffold_generated_total` | Counter | `language` | Scaffold ZIPs generated by language | | `agentidp_scaffold_generation_duration_ms` | Histogram | `language` | Time to generate scaffold ZIP | ### Acceptance Criteria - `GET /sdk/scaffold/:agentId?language=typescript` returns a valid ZIP with all 6 template files - ZIP contains `.env.example` with `AGENTIDP_CLIENT_ID` pre-filled and `AGENTIDP_CLIENT_SECRET=` as placeholder - ZIP never contains the actual client secret - `GET /sdk/scaffold/:agentId?language=python` returns Python-specific template files - All 5 languages (typescript, python, go, java, rust) return valid ZIPs - HTTP 400 on unknown `language` query param - HTTP 403 when authenticated tenant does not own the agent - `sentryagent scaffold --agent-id abc123 --language go` extracts scaffold to current directory - `sentryagent scaffold --agent-id abc123 --language python --out /tmp/myagent` extracts to `/tmp/myagent` - Developer portal `/api-explorer` renders Elements v5 with sidebar layout — TypeScript build passes - Unit tests cover: scaffold generation (each language), forbidden access, invalid language - Integration tests cover: scaffold endpoint response type, content-disposition header, ZIP validity