chore(openspec): archive phase-5-scale-ecosystem — 68/68 tasks complete
WS1 (Rust SDK), WS2 (A2A Authorization), WS5 (Developer Experience) all delivered, QA gates passed, committed to main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
## 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=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
|
||||
├── .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=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
|
||||
├── .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=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
|
||||
├── .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 `<your-client-secret>` 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` (`<API>` 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 (
|
||||
<main className="h-screen w-full">
|
||||
<API
|
||||
apiDescriptionUrl={`${process.env.NEXT_PUBLIC_API_URL}/openapi.json`}
|
||||
router="hash"
|
||||
layout="sidebar"
|
||||
hideSchemas={false}
|
||||
tryItCredentialsPolicy="same-origin"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Files modified:**
|
||||
- `portal/app/api-explorer/page.tsx` — replace Swagger UI component with Elements `<API>` 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 <id> [--language typescript|python|go|java|rust] [--out <directory>]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Alias | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `--agent-id <id>` | `-a` | (required) | Agent ID to scaffold for |
|
||||
| `--language <lang>` | `-l` | `typescript` | Target language for scaffold |
|
||||
| `--out <dir>` | `-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=<your-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
|
||||
Reference in New Issue
Block a user