create-openagent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1412 +1,723 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { intro, outro, isCancel as isCancel2 } from "@clack/prompts";
5
- import pc from "picocolors";
4
+ import { resolve as resolve2 } from "path";
5
+ import { existsSync, realpathSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "create-openagent",
11
+ version: "0.2.0",
12
+ description: "Create a working OpenAgentID app with local identity, real verification and live avatars.",
13
+ license: "Apache-2.0 OR MIT",
14
+ author: "L1fe Labs, Inc. <eng@l1fe.ai>",
15
+ homepage: "https://openagent.id",
16
+ repository: {
17
+ type: "git",
18
+ url: "https://github.com/OpenAgentID/openagent-sdk.git",
19
+ directory: "packages/create-openagent"
20
+ },
21
+ type: "module",
22
+ bin: {
23
+ "create-openagent": "./dist/index.js"
24
+ },
25
+ files: [
26
+ "dist",
27
+ "README.md",
28
+ "CHANGELOG.md",
29
+ "LICENSE-APACHE",
30
+ "LICENSE-MIT"
31
+ ],
32
+ publishConfig: {
33
+ access: "public"
34
+ },
35
+ scripts: {
36
+ build: "tsup",
37
+ dev: "tsup --watch",
38
+ typecheck: "tsc --noEmit",
39
+ start: "node dist/index.js",
40
+ test: "node --test test/*.test.mjs"
41
+ },
42
+ dependencies: {
43
+ "@clack/prompts": "^0.7.0",
44
+ picocolors: "^1.0.0"
45
+ },
46
+ devDependencies: {
47
+ "@types/node": "^20.11.30",
48
+ tsup: "^8.0.2",
49
+ typescript: "^5.4.5"
50
+ },
51
+ engines: {
52
+ node: ">=20"
53
+ },
54
+ keywords: [
55
+ "create",
56
+ "openagent",
57
+ "agent",
58
+ "scaffold",
59
+ "cli",
60
+ "mcp",
61
+ "did",
62
+ "oas"
63
+ ]
64
+ };
6
65
 
7
66
  // src/prompts.ts
8
67
  import * as p from "@clack/prompts";
9
- function parseFlagValue(args, flag) {
10
- const idx = args.indexOf(flag);
11
- if (idx >= 0 && idx + 1 < args.length) {
12
- return args[idx + 1];
68
+ function validateProjectName(value) {
69
+ if (typeof value !== "string" || /\s/.test(value) || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(value) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|node_modules)$/i.test(value)) {
70
+ throw new Error("Use a project name of 1\u201364 lowercase letters, numbers, dashes or underscores; no paths or reserved names.");
13
71
  }
14
- return void 0;
15
- }
16
- function hasFlag(args, flag) {
17
- return args.includes(flag);
18
- }
19
- async function collectOptions(projectNameArg, args) {
20
- const projectName = projectNameArg ?? await p.text({
21
- message: "Project name:",
22
- placeholder: "my-agent-app",
23
- validate: (value) => {
24
- if (!value.trim()) return "Project name is required.";
25
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(value)) {
26
- return "Invalid project name (use alphanumeric, dashes, dots, underscores).";
72
+ return value;
73
+ }
74
+ function parseArguments(args) {
75
+ let projectName;
76
+ let yes = false;
77
+ let special;
78
+ const seen = /* @__PURE__ */ new Set();
79
+ for (let index = 0; index < args.length; index++) {
80
+ const arg = args[index];
81
+ if (typeof arg !== "string" || arg.length === 0) throw new Error("Empty arguments are not supported.");
82
+ const flag = arg === "-y" ? "--yes" : arg === "-h" ? "--help" : arg === "-v" ? "--version" : arg;
83
+ if (["--yes", "--help", "--version", "--template"].includes(flag)) {
84
+ if (seen.has(flag)) throw new Error("Duplicate option: " + flag);
85
+ seen.add(flag);
86
+ if (flag === "--yes") yes = true;
87
+ else if (flag === "--help" || flag === "--version") {
88
+ if (special) throw new Error("Use --help or --version on its own.");
89
+ special = flag === "--help" ? "help" : "version";
90
+ } else {
91
+ const value = args[++index];
92
+ if (value !== "starter") throw new Error("Only --template starter is supported in 0.2. Historical templates need migration to the current SDK.");
27
93
  }
28
- return void 0;
94
+ } else if (arg.startsWith("-")) {
95
+ throw new Error("Unsupported option: " + arg + ". Use the local starter: create-openagent app --yes. Adapter, bridge, deploy and historical template options are not supported in 0.2.");
96
+ } else {
97
+ if (projectName !== void 0) throw new Error("Provide exactly one project name.");
98
+ projectName = validateProjectName(arg);
29
99
  }
30
- });
31
- if (p.isCancel(projectName)) throw new Error("Cancelled");
32
- const templateFlag = parseFlagValue(args, "--template");
33
- const template = templateFlag ?? await p.select({
34
- message: "What type of agent app?",
35
- options: [
36
- { value: "agent", label: "Agent", hint: "Standalone agent with identity and tools" },
37
- { value: "mcp-server", label: "MCP Server", hint: "Model Context Protocol server with auth" },
38
- { value: "web-dashboard", label: "Web Dashboard", hint: "Next.js dashboard for agent management" },
39
- { value: "full-stack", label: "Full Stack", hint: "Agent + MCP + Dashboard combined" }
40
- ]
41
- });
42
- if (p.isCancel(template)) throw new Error("Cancelled");
43
- const adapterFlag = parseFlagValue(args, "--adapter");
44
- const adapters = adapterFlag ? [adapterFlag] : await p.multiselect({
45
- message: "Authentication adapters?",
46
- options: [
47
- { value: "http", label: "HTTP", hint: "Bearer token / challenge-response" },
48
- { value: "websocket", label: "WebSocket", hint: "Persistent connection auth" },
49
- { value: "grpc", label: "gRPC", hint: "Protobuf-based auth" }
50
- ],
51
- initialValues: ["http"],
52
- required: true
53
- });
54
- if (p.isCancel(adapters)) throw new Error("Cancelled");
55
- const bridgeFlag = parseFlagValue(args, "--bridge");
56
- const bridge = bridgeFlag ?? await p.select({
57
- message: "Enterprise bridge?",
58
- options: [
59
- { value: "none", label: "None", hint: "Skip enterprise integration" },
60
- { value: "oidc", label: "OIDC", hint: "OpenID Connect bridge for human SSO" },
61
- { value: "scim", label: "SCIM", hint: "User provisioning bridge" }
62
- ]
63
- });
64
- if (p.isCancel(bridge)) throw new Error("Cancelled");
65
- const deployFlag = parseFlagValue(args, "--deploy");
66
- const deploy = deployFlag ?? await p.select({
67
- message: "Deploy target?",
68
- options: [
69
- { value: "local", label: "Local", hint: "Run locally with Node.js" },
70
- { value: "cloudflare", label: "Cloudflare Workers", hint: "Edge-first deployment" },
71
- { value: "vercel", label: "Vercel Edge", hint: "Next.js-optimized deployment" },
72
- { value: "aws", label: "AWS Lambda", hint: "Serverless with API Gateway" },
73
- { value: "docker", label: "Docker", hint: "Container deployment" }
74
- ]
75
- });
76
- if (p.isCancel(deploy)) throw new Error("Cancelled");
77
- const bioagentic = hasFlag(args, "--bioagentic") ? true : await p.confirm({
78
- message: "Include Bioagentic entity classification?",
79
- initialValue: false
80
- });
81
- if (p.isCancel(bioagentic)) throw new Error("Cancelled");
82
- const fabrics = hasFlag(args, "--fabrics") ? true : await p.confirm({
83
- message: "Include Fabrics routing?",
84
- initialValue: false
85
- });
86
- if (p.isCancel(fabrics)) throw new Error("Cancelled");
100
+ }
101
+ if (special) {
102
+ if (args.length !== 1) throw new Error("Use --help or --version on its own.");
103
+ return Object.freeze({ kind: special });
104
+ }
105
+ if (yes && projectName === void 0) throw new Error("With --yes, provide a project name: create-openagent app --yes.");
106
+ return Object.freeze({ kind: "create", projectName, yes, template: "starter" });
107
+ }
108
+ async function collectOptions(input, interactive = Boolean(process.stdin.isTTY)) {
109
+ let name = input.projectName;
110
+ if (name === void 0) {
111
+ if (input.yes || !interactive) throw new Error("Provide a project name: create-openagent app --yes.");
112
+ const answer = await p.text({
113
+ message: "Project name:",
114
+ placeholder: "my-agent-app",
115
+ validate(value) {
116
+ try {
117
+ validateProjectName(value);
118
+ return void 0;
119
+ } catch (error) {
120
+ return error.message;
121
+ }
122
+ }
123
+ });
124
+ if (p.isCancel(answer)) throw new Error("Cancelled.");
125
+ name = validateProjectName(answer);
126
+ }
87
127
  return {
88
- projectName: typeof projectName === "string" ? projectName : "my-agent-app",
89
- template,
90
- adapters,
91
- bridge,
92
- deploy,
93
- bioagentic,
94
- fabrics
128
+ projectName: validateProjectName(name),
129
+ template: "starter",
130
+ adapters: [],
131
+ bridge: "none",
132
+ deploy: "local",
133
+ bioagentic: false,
134
+ fabrics: false
95
135
  };
96
136
  }
97
137
 
98
138
  // src/scaffold.ts
99
- import { existsSync, mkdirSync, writeFileSync } from "fs";
100
- import { join, dirname } from "path";
101
- import { spinner } from "@clack/prompts";
139
+ import { mkdirSync, writeFileSync } from "fs";
140
+ import { join, dirname, resolve } from "path";
102
141
 
103
- // src/templates/agent.ts
104
- function generateAgentTemplate(options) {
142
+ // src/templates/starter.ts
143
+ function generateStarterTemplate(projectName) {
144
+ const name = validateProjectName(projectName);
105
145
  return [
106
- { path: "package.json", content: packageJson(options) },
107
- { path: "src/index.ts", content: indexTs() },
108
- { path: "src/agent.ts", content: agentTs(options) },
109
- { path: "src/tools.ts", content: toolsTs() },
110
- { path: "README.md", content: readme(options) }
111
- ];
112
- }
113
- function packageJson(options) {
114
- const deps = {
115
- "@openagentid/sdk": "^0.1.0"
116
- };
117
- if (options.bioagentic) {
118
- deps["@openagentid/bioagentic"] = "^0.1.0";
119
- }
120
- if (options.fabrics) {
121
- deps["@openagentid/fabrics"] = "^0.1.0";
122
- }
123
- return JSON.stringify(
124
- {
125
- name: options.projectName,
146
+ { path: "package.json", content: JSON.stringify({
147
+ name,
126
148
  version: "0.1.0",
127
149
  private: true,
128
150
  type: "module",
151
+ engines: { node: ">=20" },
129
152
  scripts: {
130
- build: "tsup src/index.ts --format esm --target node20",
131
- start: "node dist/index.js",
132
- dev: "tsx src/index.ts",
133
- typecheck: "tsc --noEmit"
153
+ dev: "node src/server.mjs",
154
+ agent: "node src/agent.mjs",
155
+ identity: "node src/identity.mjs",
156
+ test: "node --test test/*.test.mjs"
134
157
  },
135
- dependencies: deps,
136
- devDependencies: {
137
- "@types/node": "^20.11.30",
138
- tsup: "^8.0.2",
139
- tsx: "^4.7.0",
140
- typescript: "^5.4.5"
141
- }
142
- },
143
- null,
144
- 2
145
- );
146
- }
147
- function indexTs() {
148
- return `/**
149
- * Entry point \u2014 creates the agent and runs it.
150
- */
151
-
152
- import { createAgent } from './agent.js';
153
- import { tools } from './tools.js';
154
-
155
- async function main(): Promise<void> {
156
- const agent = await createAgent();
157
-
158
- process.stdout.write(\`Agent created: \${agent.did}\\n\`);
159
- process.stdout.write(\`Available tools: \${tools.map((t) => t.name).join(', ')}\\n\`);
160
-
161
- // Example: fetch credentials for a provider
162
- // const creds = await agent.credentialsFor('openai');
163
- // const res = await creds.fetch('https://api.openai.com/v1/chat/completions', { ... });
164
- }
165
-
166
- main().catch((err) => {
167
- process.stderr.write(\`Fatal: \${err instanceof Error ? err.message : String(err)}\\n\`);
168
- process.exit(1);
169
- });
170
- `;
171
- }
172
- function agentTs(options) {
173
- return `/**
174
- * Agent identity and authentication setup.
175
- */
176
-
177
- import { OpenAgent, type OpenAgentInstance } from '@openagentid/sdk';
178
-
179
- /** Configure the OpenAgent runtime. */
180
- OpenAgent.configure({
181
- endpoints: {
182
- oasResolver: process.env.OAS_RESOLVER_URL,
183
- arsenalBroker: process.env.ARSENAL_BROKER_URL,
184
- aegisVerifier: process.env.AEGIS_VERIFIER_URL,
185
- },
186
- });
187
-
188
- /**
189
- * Create a new agent identity.
190
- *
191
- * In production, replace the parent DID and scopes with your own.
192
- */
193
- export async function createAgent(): Promise<OpenAgentInstance> {
194
- const parentDid = process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:alice';
195
- const agentName = process.env.OPENAGENT_AGENT_NAME ?? '${options.projectName}';
196
-
197
- return OpenAgent.createAgent({
198
- parent: parentDid,
199
- name: agentName,
200
- scopes: ['openai:chat:completions', 'github:repo:read'],
201
- });
202
- }
203
- `;
204
- }
205
- function toolsTs() {
206
- return `/**
207
- * Example MCP tools \u2014 define your agent's capabilities here.
208
- */
209
-
210
- export interface Tool {
211
- readonly name: string;
212
- readonly description: string;
213
- execute(input: Record<string, unknown>): Promise<unknown>;
214
- }
215
-
216
- /** Echo tool \u2014 returns the input as-is. Useful for testing. */
217
- const echoTool: Tool = {
218
- name: 'echo',
219
- description: 'Returns the input unchanged.',
220
- async execute(input) {
221
- return { echoed: input };
222
- },
223
- };
224
-
225
- /** Timestamp tool \u2014 returns the current ISO timestamp. */
226
- const timestampTool: Tool = {
227
- name: 'timestamp',
228
- description: 'Returns the current UTC timestamp.',
229
- async execute() {
230
- return { timestamp: new Date().toISOString() };
231
- },
232
- };
233
-
234
- export const tools: readonly Tool[] = [echoTool, timestampTool];
235
- `;
236
- }
237
- function readme(options) {
238
- return `# ${options.projectName}
239
-
240
- An OpenAgent-powered agent application.
241
-
242
- ## Setup
243
-
244
- \`\`\`bash
245
- npm install
246
- cp .env.example .env
247
- # Edit .env with your agent identity
248
- \`\`\`
249
-
250
- ## Development
251
-
252
- \`\`\`bash
253
- npm run dev
254
- \`\`\`
255
-
256
- ## Build & Run
257
-
258
- \`\`\`bash
259
- npm run build
260
- npm start
261
- \`\`\`
262
-
263
- ## Configuration
264
-
265
- Set environment variables in \`.env\`:
266
-
267
- - \`OPENAGENT_PARENT_DID\` \u2014 Your human root DID
268
- - \`OPENAGENT_AGENT_NAME\` \u2014 Agent name
269
- - \`OAS_RESOLVER_URL\` \u2014 OAS identity resolver (optional for dev)
270
- - \`ARSENAL_BROKER_URL\` \u2014 Arsenal credential broker (optional for dev)
271
- - \`AEGIS_VERIFIER_URL\` \u2014 AEGIS verifier (optional for dev)
272
-
273
- ## Learn More
274
-
275
- - [OpenAgent Docs](https://openagent.id/docs)
276
- - [OAS Specification](https://openagent.id/spec)
277
- `;
278
- }
279
-
280
- // src/templates/mcp-server.ts
281
- function generateMcpServerTemplate(options) {
282
- return [
283
- { path: "package.json", content: packageJson2(options) },
284
- { path: "src/index.ts", content: indexTs2() },
285
- { path: "src/server.ts", content: serverTs(options) },
286
- { path: "src/tools/echo.ts", content: echoToolTs() },
287
- { path: "src/tools/index.ts", content: toolsIndexTs() },
288
- { path: "README.md", content: readme2(options) }
158
+ dependencies: { "@openagentid/sdk": "0.4.0" }
159
+ }, null, 2) + "\n" },
160
+ { path: ".gitignore", content: "node_modules/\n.openagent/\ncoverage/\n.env\n" },
161
+ { path: "src/workspaces.mjs", content: workspaces },
162
+ { path: "src/server.mjs", content: server },
163
+ { path: "src/agent.mjs", content: agent },
164
+ { path: "src/identity.mjs", content: identity },
165
+ { path: "public/index.html", content: html },
166
+ { path: "public/viewer.js", content: viewer },
167
+ { path: "public/style.css", content: css },
168
+ { path: "test/starter.test.mjs", content: tests },
169
+ { path: "README.md", content: readme }
289
170
  ];
290
171
  }
291
- function packageJson2(options) {
292
- const deps = {
293
- "@openagentid/sdk": "^0.1.0",
294
- "@modelcontextprotocol/sdk": "^1.0.0"
295
- };
296
- if (options.bioagentic) {
297
- deps["@openagentid/bioagentic"] = "^0.1.0";
298
- }
299
- if (options.fabrics) {
300
- deps["@openagentid/fabrics"] = "^0.1.0";
301
- }
302
- return JSON.stringify(
303
- {
304
- name: options.projectName,
305
- version: "0.1.0",
306
- private: true,
307
- type: "module",
308
- scripts: {
309
- build: "tsup src/index.ts --format esm --target node20",
310
- start: "node dist/index.js",
311
- dev: "tsx src/index.ts",
312
- typecheck: "tsc --noEmit"
313
- },
314
- dependencies: deps,
315
- devDependencies: {
316
- "@types/node": "^20.11.30",
317
- tsup: "^8.0.2",
318
- tsx: "^4.7.0",
319
- typescript: "^5.4.5"
320
- }
321
- },
322
- null,
323
- 2
324
- );
325
- }
326
- function indexTs2() {
327
- return `/**
328
- * Entry point \u2014 starts the MCP server.
329
- */
330
-
331
- import { startServer } from './server.js';
332
-
333
- startServer().catch((err) => {
334
- process.stderr.write(\`Fatal: \${err instanceof Error ? err.message : String(err)}\\n\`);
335
- process.exit(1);
336
- });
337
- `;
338
- }
339
- function serverTs(options) {
340
- return `/**
341
- * MCP Server with OpenAgent authentication middleware.
342
- *
343
- * Authenticates incoming tool calls using the OpenAgent SDK before
344
- * dispatching to tool handlers.
345
- */
346
-
347
- import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
348
- import { OpenAgent, type AuthContext } from '@openagentid/sdk';
349
- import { tools } from './tools/index.js';
350
-
351
- const PORT = parseInt(process.env.PORT ?? '3200', 10);
352
- const HOST = process.env.HOST ?? '0.0.0.0';
353
-
354
- /** Configure OpenAgent runtime. */
355
- OpenAgent.configure({
356
- endpoints: {
357
- oasResolver: process.env.OAS_RESOLVER_URL,
358
- arsenalBroker: process.env.ARSENAL_BROKER_URL,
359
- aegisVerifier: process.env.AEGIS_VERIFIER_URL,
360
- },
361
- });
362
-
363
- function sendJson(res: ServerResponse, body: unknown, status = 200): void {
364
- const payload = JSON.stringify(body);
365
- res.writeHead(status, {
366
- 'Content-Type': 'application/json',
367
- 'Content-Length': Buffer.byteLength(payload),
368
- });
369
- res.end(payload);
370
- }
371
-
372
- async function readBody(req: IncomingMessage): Promise<string> {
373
- const chunks: Buffer[] = [];
374
- for await (const chunk of req) {
375
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
376
- }
377
- return Buffer.concat(chunks).toString('utf-8');
378
- }
379
-
380
- /** Convert IncomingMessage to WHATWG Request. */
381
- function toRequest(req: IncomingMessage, body: string): Request {
382
- const host = req.headers.host ?? \`\${HOST}:\${PORT}\`;
383
- const url = \`http://\${host}\${req.url ?? '/'}\`;
384
- const headers = new Headers();
385
- for (const [key, value] of Object.entries(req.headers)) {
386
- if (value !== undefined) {
387
- headers.set(key, Array.isArray(value) ? value.join(', ') : value);
172
+ var workspaces = String.raw`import { OpenAgentWorkspace } from '@openagentid/sdk/local';
173
+ import { mkdir, lstat, readFile, writeFile } from 'node:fs/promises';
174
+ import { dirname, resolve, join } from 'node:path';
175
+ import { fileURLToPath } from 'node:url';
176
+ import { randomUUID } from 'node:crypto';
177
+
178
+ export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
179
+ async function stat(path) {
180
+ try { return await lstat(path); } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
181
+ }
182
+
183
+ /** First setup deliberately imports the public identity into a separate receiver.
184
+ * Subsequent opens never recreate keys or trust, including after revocation. */
185
+ export async function openStarter(root = projectRoot) {
186
+ const directory = join(resolve(root), '.openagent');
187
+ const existing = await stat(directory);
188
+ if (existing && (!existing.isDirectory() || existing.isSymbolicLink())) throw new Error('Invalid starter state directory.');
189
+ if (existing && process.platform !== 'win32' && ((existing.mode & 0o777) !== 0o700 || existing.uid !== process.getuid())) throw new Error('Starter state directory must be owned by this user with mode 0700.');
190
+ if (!existing) await mkdir(directory, { mode: 0o700 });
191
+ const signerPath = join(directory, 'signer');
192
+ const receiverPath = join(directory, 'receiver');
193
+ const markerPath = join(directory, 'starter.json');
194
+ const marker = await stat(markerPath);
195
+ let signer, receiver, did, audience;
196
+ if (!marker) {
197
+ if (await stat(signerPath) || await stat(receiverPath)) {
198
+ throw new Error('Incomplete starter setup. Existing state was preserved; inspect it before explicitly choosing a new workspace.');
388
199
  }
200
+ audience = 'urn:openagent:starter:' + randomUUID();
201
+ signer = await OpenAgentWorkspace.initialize(signerPath, { audience });
202
+ const created = await signer.createAgent('starter');
203
+ receiver = await OpenAgentWorkspace.initialize(receiverPath, { audience });
204
+ const document = await signer.exportIdentity(created.did);
205
+ await receiver.trustIdentity(document);
206
+ did = created.did;
207
+ await writeFile(join(directory, 'identity.json'), JSON.stringify(document, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
208
+ await writeFile(markerPath, JSON.stringify({ schemaVersion: 1, did, audience }) + '\n', { flag: 'wx', mode: 0o600 });
209
+ } else {
210
+ if (!marker.isFile() || marker.isSymbolicLink() || marker.size > 2048) throw new Error('Invalid starter metadata.');
211
+ if (process.platform !== 'win32' && ((marker.mode & 0o777) !== 0o600 || marker.uid !== process.getuid())) throw new Error('Starter metadata must be owned by this user with mode 0600.');
212
+ const saved = JSON.parse(await readFile(markerPath, 'utf8'));
213
+ if (!saved || Array.isArray(saved) || Object.keys(saved).sort().join(',') !== 'audience,did,schemaVersion' ||
214
+ saved.schemaVersion !== 1 || typeof saved.did !== 'string' || typeof saved.audience !== 'string' ||
215
+ !/^urn:openagent:starter:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(saved.audience)) throw new Error('Invalid starter metadata.');
216
+ signer = await OpenAgentWorkspace.open(signerPath);
217
+ receiver = await OpenAgentWorkspace.open(receiverPath);
218
+ did = saved.did;
219
+ audience = saved.audience;
389
220
  }
390
- const method = req.method ?? 'GET';
391
- const hasBody = method !== 'GET' && method !== 'HEAD' && body.length > 0;
392
- return new Request(url, { method, headers, body: hasBody ? body : undefined });
393
- }
394
-
395
- /** Authenticate the request and return the auth context, or null. */
396
- async function authenticate(
397
- req: IncomingMessage,
398
- body: string,
399
- ): Promise<AuthContext | null> {
400
- const request = toRequest(req, body);
401
- try {
402
- return await OpenAgent.authenticate(request);
403
- } catch {
404
- return null;
221
+ const signers = await signer.listAgents();
222
+ const peers = await receiver.listAgents();
223
+ const selected = signers.find(value => value.did === did && value.hasPrivateKey);
224
+ if (!selected || !peers.some(value => value.did === did)) throw new Error('Starter identity or receiver trust is missing. State was preserved.');
225
+ if ((await signer.inspect()).audience !== audience || (await receiver.inspect()).audience !== audience) {
226
+ throw new Error('Unexpected starter audience.');
405
227
  }
228
+ return Object.freeze({ signer, receiver, did, audience, directory });
406
229
  }
407
-
408
- interface McpRequest {
409
- jsonrpc: '2.0';
410
- id: string | number;
411
- method: string;
412
- params?: Record<string, unknown>;
413
- }
414
-
415
- async function handleMcpRequest(
416
- mcpReq: McpRequest,
417
- _authCtx: AuthContext,
418
- ): Promise<unknown> {
419
- if (mcpReq.method === 'tools/list') {
420
- return {
421
- jsonrpc: '2.0',
422
- id: mcpReq.id,
423
- result: {
424
- tools: tools.map((t) => ({
425
- name: t.name,
426
- description: t.description,
427
- inputSchema: t.inputSchema,
428
- })),
429
- },
230
+ `;
231
+ var server = String.raw`import { createServer } from 'node:http';
232
+ import { webcrypto } from 'node:crypto';
233
+ import { readFile } from 'node:fs/promises';
234
+ import { createRequire } from 'node:module';
235
+ import { dirname, join, resolve } from 'node:path';
236
+ import { fileURLToPath } from 'node:url';
237
+ import { openStarter, projectRoot } from './workspaces.mjs';
238
+
239
+ const require = createRequire(import.meta.url);
240
+ const contentTypes = { '/': 'text/html; charset=utf-8', '/viewer.js': 'text/javascript; charset=utf-8',
241
+ '/style.css': 'text/css; charset=utf-8', '/character-player.js': 'text/javascript; charset=utf-8' };
242
+ class Refusal extends Error {
243
+ constructor(status, code) { super(code); this.status = status; this.code = code; }
244
+ }
245
+ function closed(value, keys) {
246
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
247
+ Object.keys(value).sort().join(',') !== [...keys].sort().join(',')) throw new Refusal(400, 'invalid_request');
248
+ return value;
249
+ }
250
+ async function jsonBody(request) {
251
+ if (!/^application\/json(?:;\s*charset=utf-8)?$/i.test(request.headers['content-type'] || '')) throw new Refusal(415, 'json_required');
252
+ const length = request.headers['content-length'];
253
+ if (length !== undefined && (!/^[0-9]+$/.test(length) || Number(length) > 24576)) throw new Refusal(413, 'body_too_large');
254
+ let timer;
255
+ const body = new Promise((accept, reject) => {
256
+ let size = 0;
257
+ const chunks = [];
258
+ const cleanup = () => { clearTimeout(timer); request.off('data', data); request.off('end', end); request.off('error', fail); request.off('aborted', aborted); };
259
+ const fail = error => { cleanup(); reject(error); };
260
+ const aborted = () => fail(new Refusal(400, 'invalid_request'));
261
+ const data = chunk => {
262
+ size += chunk.length;
263
+ if (size > 24576) { fail(new Refusal(413, 'body_too_large')); request.pause(); }
264
+ else chunks.push(chunk);
430
265
  };
431
- }
432
-
433
- if (mcpReq.method === 'tools/call') {
434
- const toolName = (mcpReq.params?.name as string) ?? '';
435
- const args = (mcpReq.params?.arguments as Record<string, unknown>) ?? {};
436
- const tool = tools.find((t) => t.name === toolName);
437
-
438
- if (!tool) {
439
- return {
440
- jsonrpc: '2.0',
441
- id: mcpReq.id,
442
- error: { code: -32601, message: \`Tool not found: \${toolName}\` },
443
- };
444
- }
445
-
446
- const result = await tool.execute(args);
447
- return {
448
- jsonrpc: '2.0',
449
- id: mcpReq.id,
450
- result: { content: [{ type: 'text', text: JSON.stringify(result) }] },
266
+ const end = () => {
267
+ cleanup();
268
+ try { accept(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
269
+ catch { reject(new Refusal(400, 'invalid_request')); }
451
270
  };
271
+ request.on('data', data); request.once('end', end); request.once('error', fail); request.once('aborted', aborted);
272
+ timer = setTimeout(() => { fail(new Refusal(408, 'body_timeout')); request.pause(); }, 2000);
273
+ });
274
+ return body;
275
+ }
276
+ function headers(response) {
277
+ response.setHeader('cache-control', 'no-store');
278
+ response.setHeader('x-content-type-options', 'nosniff');
279
+ response.setHeader('referrer-policy', 'no-referrer');
280
+ response.setHeader('content-security-policy', "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
281
+ }
282
+ function json(response, status, value) {
283
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
284
+ response.end(JSON.stringify(value));
285
+ }
286
+ export async function startServer({ root = projectRoot, port = 3210 } = {}) {
287
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new TypeError('Invalid loopback port.');
288
+ const workspaces = await openStarter(root);
289
+ const assets = new Map();
290
+ for (const [route, name] of [['/', 'index.html'], ['/viewer.js', 'viewer.js'], ['/style.css', 'style.css']]) {
291
+ assets.set(route, await readFile(new URL('../public/' + name, import.meta.url)));
452
292
  }
453
-
454
- return {
455
- jsonrpc: '2.0',
456
- id: mcpReq.id,
457
- error: { code: -32601, message: \`Method not found: \${mcpReq.method}\` },
458
- };
459
- }
460
-
461
- export async function startServer(): Promise<void> {
462
- const server = createServer(async (req, res) => {
463
- const url = new URL(req.url ?? '/', \`http://\${req.headers.host ?? 'localhost'}\`);
464
-
465
- if (url.pathname === '/health') {
466
- sendJson(res, { status: 'ok', service: '${options.projectName}' });
467
- return;
468
- }
469
-
470
- if (url.pathname === '/.well-known/openagent/agent.json') {
471
- sendJson(res, {
472
- did: \`did:oas:l1fe:agent:\${process.env.OPENAGENT_AGENT_NAME ?? '${options.projectName}'}\`,
473
- name: process.env.OPENAGENT_AGENT_NAME ?? '${options.projectName}',
474
- parent: process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:unconfigured',
475
- capabilities: { tools: tools.map((t) => t.name) },
476
- });
477
- return;
478
- }
479
-
480
- if (req.method !== 'POST') {
481
- sendJson(res, { error: 'Method not allowed' }, 405);
482
- return;
483
- }
484
-
485
- const body = await readBody(req);
486
- const authCtx = await authenticate(req, body);
487
-
488
- if (!authCtx) {
489
- sendJson(res, { error: 'Authentication required' }, 401);
490
- return;
491
- }
492
-
293
+ // Resolve the public SDK package, then serve only its self-contained browser entry.
294
+ const sdkDirectory = dirname(require.resolve('@openagentid/sdk/characters'));
295
+ assets.set('/character-player.js', await readFile(join(sdkDirectory, 'browser', 'character-player.js')));
296
+ let revision = 0, completedRequests = 0, inFlight = 0, lastCompletedAtMs = null;
297
+ const snapshot = () => ({ schemaVersion: 1, did: workspaces.did, revision,
298
+ status: inFlight > 0 ? 'working' : completedRequests > 0 ? 'success' : 'idle',
299
+ completedRequests, lastCompletedAtMs });
300
+ let address;
301
+ const server = createServer({ maxHeaderSize: 8192 }, async (request, response) => {
302
+ headers(response);
493
303
  try {
494
- const mcpReq = JSON.parse(body) as McpRequest;
495
- const result = await handleMcpRequest(mcpReq, authCtx);
496
- sendJson(res, result);
497
- } catch (err) {
498
- const message = err instanceof Error ? err.message : 'Internal error';
499
- sendJson(res, { jsonrpc: '2.0', id: null, error: { code: -32700, message } }, 400);
304
+ // An exact numeric host prevents DNS rebinding. Cross-origin browser writes
305
+ // are refused; the read-only viewer sends no control requests.
306
+ if (!['127.0.0.1', '::ffff:127.0.0.1'].includes(request.socket.remoteAddress) || request.headers.host !== address) throw new Refusal(403, 'loopback_only');
307
+ if (request.headers.origin !== undefined && request.headers.origin !== 'http://' + address) throw new Refusal(403, 'origin_refused');
308
+ if (request.headers['sec-fetch-site'] === 'cross-site') throw new Refusal(403, 'origin_refused');
309
+ const route = request.url;
310
+ if (request.method === 'GET' && assets.has(route)) {
311
+ if (request.headers['content-length'] !== undefined || request.headers['transfer-encoding'] !== undefined) throw new Refusal(400, 'invalid_request');
312
+ response.writeHead(200, { 'content-type': contentTypes[route] }); response.end(assets.get(route)); return;
313
+ }
314
+ if (request.method === 'GET' && route === '/state') {
315
+ if (request.headers['content-length'] !== undefined || request.headers['transfer-encoding'] !== undefined) throw new Refusal(400, 'invalid_request');
316
+ json(response, 200, snapshot()); return;
317
+ }
318
+ if (request.method !== 'POST' || !['/challenge', '/request'].includes(route)) throw new Refusal(404, 'not_found');
319
+ if (request.headers.origin !== undefined || request.headers['sec-fetch-site'] && request.headers['sec-fetch-site'] !== 'none') throw new Refusal(403, 'operator_command_required');
320
+ const body = await jsonBody(request);
321
+ if (route === '/challenge') {
322
+ closed(body, ['did']);
323
+ if (body.did !== workspaces.did) throw new Refusal(403, 'identity_refused');
324
+ let challenge;
325
+ try { challenge = await workspaces.receiver.issueChallenge(body.did); }
326
+ catch { throw new Refusal(403, 'identity_refused'); }
327
+ json(response, 200, challenge); return;
328
+ }
329
+ closed(body, ['nonce', 'proof', 'message']);
330
+ if (typeof body.nonce !== 'string' || body.nonce.length === 0 || body.nonce.length > 256 ||
331
+ typeof body.message !== 'string' || Buffer.byteLength(body.message, 'utf8') === 0 || Buffer.byteLength(body.message, 'utf8') > 4096) throw new Refusal(400, 'invalid_request');
332
+ closed(body.proof, ['token', 'delegations']);
333
+ if (typeof body.proof.token !== 'string' || body.proof.token.length > 16384 ||
334
+ !Array.isArray(body.proof.delegations) || body.proof.delegations.length !== 0) throw new Refusal(400, 'invalid_request');
335
+ let verified;
336
+ try { verified = await workspaces.receiver.verify(body.proof, { nonce: body.nonce }); }
337
+ catch { throw new Refusal(403, 'identity_refused'); }
338
+ if (verified.did !== workspaces.did) throw new Refusal(403, 'identity_refused');
339
+ if (revision >= Number.MAX_SAFE_INTEGER - 2 || completedRequests >= Number.MAX_SAFE_INTEGER - 1) throw new Refusal(503, 'request_unavailable');
340
+ inFlight++; revision++;
341
+ try {
342
+ const digest = Buffer.from(await webcrypto.subtle.digest('SHA-256', Buffer.from(body.message, 'utf8'))).toString('hex');
343
+ completedRequests++; lastCompletedAtMs = Date.now();
344
+ json(response, 200, { schemaVersion: 1, did: verified.did, message: body.message, digest });
345
+ } finally { inFlight--; revision++; }
346
+ } catch (error) {
347
+ const status = error instanceof Refusal ? error.status : 503;
348
+ const code = error instanceof Refusal ? error.code : 'request_unavailable';
349
+ if (status === 408 || status === 413) response.once('finish', () => request.destroy());
350
+ if (!response.headersSent && !response.destroyed) json(response, status, { error: code });
500
351
  }
501
352
  });
502
-
503
- server.listen(PORT, HOST, () => {
504
- process.stdout.write(\`MCP server listening on http://\${HOST}:\${PORT}\\n\`);
505
- process.stdout.write(\`Tools: \${tools.map((t) => t.name).join(', ')}\\n\`);
353
+ server.requestTimeout = 5000;
354
+ server.headersTimeout = 5000;
355
+ server.keepAliveTimeout = 1000;
356
+ await new Promise((accept, reject) => {
357
+ server.once('error', reject);
358
+ server.listen(port, '127.0.0.1', () => { server.off('error', reject); accept(); });
359
+ });
360
+ address = '127.0.0.1:' + server.address().port;
361
+ return Object.freeze({ server, url: 'http://' + address, did: workspaces.did, workspaces,
362
+ close: () => new Promise((accept, reject) => { server.close(error => error ? reject(error) : accept()); server.closeAllConnections(); }),
506
363
  });
507
364
  }
508
- `;
509
- }
510
- function echoToolTs() {
511
- return `/**
512
- * Echo tool \u2014 returns the input unchanged. Useful for testing.
513
- */
514
-
515
- import type { McpTool } from './index.js';
516
-
517
- export const echoTool: McpTool = {
518
- name: 'echo',
519
- description: 'Returns the input unchanged.',
520
- inputSchema: {
521
- type: 'object',
522
- properties: {
523
- message: { type: 'string', description: 'Message to echo back.' },
524
- },
525
- required: ['message'],
526
- },
527
- async execute(input) {
528
- return { echoed: input };
529
- },
530
- };
531
- `;
532
- }
533
- function toolsIndexTs() {
534
- return `/**
535
- * Tool registry \u2014 add your tools here.
536
- */
537
-
538
- import { echoTool } from './echo.js';
539
-
540
- export interface McpTool {
541
- readonly name: string;
542
- readonly description: string;
543
- readonly inputSchema: Record<string, unknown>;
544
- execute(input: Record<string, unknown>): Promise<unknown>;
545
- }
546
-
547
- export const tools: readonly McpTool[] = [echoTool];
548
- `;
365
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
366
+ try {
367
+ const raw = process.env.PORT || '3210';
368
+ if (!/^[0-9]+$/.test(raw) || Number(raw) < 1024 || Number(raw) > 65535) throw new Error('PORT must be an integer from 1024 to 65535.');
369
+ const app = await startServer({ port: Number(raw) });
370
+ console.log('OpenAgent viewer: ' + app.url);
371
+ console.log('Agent DID: ' + app.did);
372
+ console.log('Run in another terminal: npm run agent -- "Hello from my agent"');
373
+ for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { app.close().then(() => process.exit(0), () => process.exit(1)); });
374
+ } catch (error) { console.error('Starter could not open: ' + error.message); process.exitCode = 1; }
549
375
  }
550
- function readme2(options) {
551
- return `# ${options.projectName}
552
-
553
- An MCP server with OpenAgent authentication.
554
-
555
- ## Setup
556
-
557
- \`\`\`bash
558
- npm install
559
- cp .env.example .env
560
- \`\`\`
561
-
562
- ## Development
563
-
564
- \`\`\`bash
565
- npm run dev
566
- \`\`\`
567
-
568
- ## Adding Tools
569
-
570
- 1. Create a new file in \`src/tools/\`
571
- 2. Export an \`McpTool\` object
572
- 3. Add it to the \`tools\` array in \`src/tools/index.ts\`
573
-
574
- ## Learn More
575
-
576
- - [OpenAgent Docs](https://openagent.id/docs)
577
- - [MCP Specification](https://modelcontextprotocol.io)
578
376
  `;
579
- }
580
-
581
- // src/templates/web-dashboard.ts
582
- function generateWebDashboardTemplate(options) {
583
- return [
584
- { path: "package.json", content: packageJson3(options) },
585
- { path: "next.config.ts", content: nextConfig() },
586
- { path: "src/app/layout.tsx", content: layoutTsx(options) },
587
- { path: "src/app/page.tsx", content: pageTsx(options) },
588
- { path: "src/app/api/auth/route.ts", content: authRoute() },
589
- { path: "src/app/api/agents/route.ts", content: agentsRoute() },
590
- { path: "src/lib/openagent.ts", content: openagentLib() },
591
- { path: "src/lib/auth.ts", content: authLib(options) },
592
- { path: "README.md", content: readme3(options) }
593
- ];
594
- }
595
- function packageJson3(options) {
596
- const deps = {
597
- "@openagentid/sdk": "^0.1.0",
598
- next: "^15.0.0",
599
- react: "^19.0.0",
600
- "react-dom": "^19.0.0"
601
- };
602
- if (options.bioagentic) {
603
- deps["@openagentid/bioagentic"] = "^0.1.0";
377
+ var agent = String.raw`import { resolve } from 'node:path';
378
+ import { fileURLToPath } from 'node:url';
379
+ import { openStarter, projectRoot } from './workspaces.mjs';
380
+
381
+ async function exchange(url, body) {
382
+ const response = await fetch(url, { method: 'POST', redirect: 'error',
383
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(5000) });
384
+ if (!response.ok || response.headers.get('cache-control') !== 'no-store') throw new Error('Receiver refused the request (' + response.status + ').');
385
+ const chunks = [];
386
+ let size = 0;
387
+ for await (const chunk of response.body) {
388
+ size += chunk.length;
389
+ if (size > 16384) throw new Error('Receiver response exceeded its limit.');
390
+ chunks.push(chunk);
604
391
  }
605
- return JSON.stringify(
606
- {
607
- name: options.projectName,
608
- version: "0.1.0",
609
- private: true,
610
- scripts: {
611
- dev: "next dev",
612
- build: "next build",
613
- start: "next start",
614
- typecheck: "tsc --noEmit"
615
- },
616
- dependencies: deps,
617
- devDependencies: {
618
- "@types/node": "^20.11.30",
619
- "@types/react": "^19.0.0",
620
- "@types/react-dom": "^19.0.0",
621
- typescript: "^5.4.5"
622
- }
623
- },
624
- null,
625
- 2
626
- );
627
- }
628
- function nextConfig() {
629
- return `import type { NextConfig } from 'next';
630
-
631
- const config: NextConfig = {
632
- reactStrictMode: true,
633
- experimental: {
634
- serverActions: {
635
- bodySizeLimit: '2mb',
636
- },
637
- },
638
- };
639
-
640
- export default config;
641
- `;
642
- }
643
- function layoutTsx(options) {
644
- return `import type { Metadata } from 'next';
645
- import type { ReactNode } from 'react';
646
-
647
- export const metadata: Metadata = {
648
- title: '${options.projectName} \u2014 OpenAgent Dashboard',
649
- description: 'Agent management dashboard powered by OpenAgent.',
650
- };
651
-
652
- export default function RootLayout({ children }: { children: ReactNode }) {
653
- return (
654
- <html lang="en">
655
- <body style={{ fontFamily: 'system-ui, sans-serif', margin: 0, padding: '2rem' }}>
656
- <header style={{ borderBottom: '1px solid #eee', paddingBottom: '1rem', marginBottom: '2rem' }}>
657
- <h1 style={{ fontSize: '1.5rem', margin: 0 }}>
658
- ${options.projectName}
659
- </h1>
660
- <p style={{ color: '#666', margin: '0.25rem 0 0' }}>OpenAgent Dashboard</p>
661
- </header>
662
- <main>{children}</main>
663
- </body>
664
- </html>
665
- );
666
- }
667
- `;
668
- }
669
- function pageTsx(options) {
670
- return `/**
671
- * Dashboard home \u2014 shows agent status and recent activity.
672
- */
673
-
674
- export default async function HomePage() {
675
- const agentName = process.env.OPENAGENT_AGENT_NAME ?? '${options.projectName}';
676
- const parentDid = process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:unconfigured';
677
-
678
- return (
679
- <div>
680
- <section style={{ marginBottom: '2rem' }}>
681
- <h2>Agent Status</h2>
682
- <table style={{ borderCollapse: 'collapse', width: '100%', maxWidth: '600px' }}>
683
- <tbody>
684
- <tr>
685
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Name</td>
686
- <td style={{ padding: '0.5rem' }}>{agentName}</td>
687
- </tr>
688
- <tr>
689
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Parent DID</td>
690
- <td style={{ padding: '0.5rem', fontFamily: 'monospace', fontSize: '0.85rem' }}>
691
- {parentDid}
692
- </td>
693
- </tr>
694
- <tr>
695
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Status</td>
696
- <td style={{ padding: '0.5rem', color: '#22c55e' }}>Active</td>
697
- </tr>
698
- </tbody>
699
- </table>
700
- </section>
701
-
702
- <section>
703
- <h2>API Endpoints</h2>
704
- <ul style={{ lineHeight: 2 }}>
705
- <li>
706
- <code>GET /api/agents</code> \u2014 List managed agents
707
- </li>
708
- <li>
709
- <code>POST /api/auth</code> \u2014 Authenticate agent request
710
- </li>
711
- <li>
712
- <code>GET /.well-known/openagent/agent.json</code> \u2014 Agent discovery
713
- </li>
714
- </ul>
715
- </section>
716
- </div>
717
- );
718
- }
719
- `;
720
- }
721
- function authRoute() {
722
- return `/**
723
- * POST /api/auth \u2014 Authenticate an incoming agent request.
724
- */
725
-
726
- import { NextResponse, type NextRequest } from 'next/server';
727
- import { getRuntime } from '@/lib/openagent';
728
-
729
- export async function POST(request: NextRequest): Promise<NextResponse> {
730
- const runtime = getRuntime();
731
-
392
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
393
+ }
394
+ export async function sendAgentRequest({ root = projectRoot, url = 'http://127.0.0.1:3210', message = 'Hello from my agent' } = {}) {
395
+ const target = new URL(url);
396
+ if (target.protocol !== 'http:' || target.hostname !== '127.0.0.1' || !target.port || target.username ||
397
+ target.password || target.pathname !== '/' || target.search || target.hash) throw new Error('Use the numeric loopback receiver origin shown by npm run dev.');
398
+ if (typeof message !== 'string' || Buffer.byteLength(message, 'utf8') === 0 || Buffer.byteLength(message, 'utf8') > 4096) throw new Error('Message must contain 1–4096 UTF-8 bytes.');
399
+ const { signer, did, audience } = await openStarter(root);
400
+ const challenge = await exchange(target.origin + '/challenge', { did });
401
+ if (!challenge || Array.isArray(challenge) || Object.keys(challenge).sort().join(',') !== 'audience,expiresAt,nonce' ||
402
+ challenge.audience !== audience || typeof challenge.nonce !== 'string' || challenge.nonce.length === 0 ||
403
+ challenge.nonce.length > 256 || !Number.isSafeInteger(challenge.expiresAt) || challenge.expiresAt <= Date.now() / 1000) throw new Error('Invalid receiver challenge.');
404
+ const proof = await signer.prove(did, { nonce: challenge.nonce, audience: challenge.audience });
405
+ return exchange(target.origin + '/request', { nonce: challenge.nonce, proof, message });
406
+ }
407
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
732
408
  try {
733
- const ctx = await runtime.authenticate(request as unknown as Request);
734
- return NextResponse.json({
735
- success: true,
736
- data: {
737
- did: ctx.did,
738
- root: ctx.root,
739
- scopes: ctx.scopes,
740
- expiresAt: ctx.expiresAt,
741
- },
742
- });
743
- } catch (err) {
744
- const message = err instanceof Error ? err.message : 'Authentication failed';
745
- return NextResponse.json({ success: false, error: message }, { status: 401 });
746
- }
409
+ if (process.argv.length > 3) throw new Error('Pass one quoted message: npm run agent -- "Hello from my agent"');
410
+ const result = await sendAgentRequest({ url: process.env.OPENAGENT_RECEIVER_URL || 'http://127.0.0.1:' + (process.env.PORT || '3210'), message: process.argv[2] || 'Hello from my agent' });
411
+ console.log(JSON.stringify(result, null, 2));
412
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
747
413
  }
748
414
  `;
749
- }
750
- function agentsRoute() {
751
- return `/**
752
- * GET /api/agents \u2014 List managed agents.
753
- * POST /api/agents \u2014 Create a new agent.
754
- */
415
+ var identity = String.raw`import { openStarter } from './workspaces.mjs';
755
416
 
756
- import { NextResponse, type NextRequest } from 'next/server';
757
- import { getRuntime } from '@/lib/openagent';
758
-
759
- /**
760
- * In-memory agent registry for demonstration.
761
- * Replace with a persistent store in production.
762
- */
763
- const agents: Array<{ did: string; name: string; createdAt: string }> = [];
764
-
765
- export async function GET(): Promise<NextResponse> {
766
- return NextResponse.json({ success: true, data: agents });
767
- }
768
-
769
- export async function POST(request: NextRequest): Promise<NextResponse> {
770
- const runtime = getRuntime();
771
-
772
- try {
773
- const body = (await request.json()) as { name: string; scopes: string[] };
774
-
775
- if (!body.name || !body.scopes?.length) {
776
- return NextResponse.json(
777
- { success: false, error: 'name and scopes are required' },
778
- { status: 400 },
779
- );
780
- }
781
-
782
- const parentDid = process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:unconfigured';
783
- const agent = await runtime.createAgent({
784
- parent: parentDid,
785
- name: body.name,
786
- scopes: body.scopes,
787
- });
788
-
789
- const record = {
790
- did: agent.did,
791
- name: body.name,
792
- createdAt: new Date().toISOString(),
793
- };
794
- agents.push(record);
795
-
796
- return NextResponse.json({ success: true, data: record }, { status: 201 });
797
- } catch (err) {
798
- const message = err instanceof Error ? err.message : 'Agent creation failed';
799
- return NextResponse.json({ success: false, error: message }, { status: 500 });
417
+ try {
418
+ const args = process.argv.slice(2);
419
+ if (args.length !== 1 || !['export', 'revoke'].includes(args[0])) throw new Error('Use npm run identity -- export or npm run identity -- revoke.');
420
+ const { signer, receiver, did } = await openStarter();
421
+ if (args[0] === 'export') console.log(JSON.stringify(await signer.exportIdentity(did), null, 2));
422
+ else {
423
+ await receiver.revoke(did);
424
+ console.log('Revoked receiver trust for ' + did + '. Restart does not undo this decision.');
800
425
  }
801
- }
426
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
802
427
  `;
803
- }
804
- function openagentLib() {
805
- return `/**
806
- * OpenAgent SDK setup \u2014 shared runtime instance.
807
- */
808
-
809
- import { OpenAgent, type OpenAgentRuntime } from '@openagentid/sdk';
810
-
811
- let runtime: OpenAgentRuntime | undefined;
812
-
813
- export function getRuntime(): OpenAgentRuntime {
814
- if (!runtime) {
815
- runtime = OpenAgent.configure({
816
- endpoints: {
817
- oasResolver: process.env.OAS_RESOLVER_URL,
818
- arsenalBroker: process.env.ARSENAL_BROKER_URL,
819
- aegisVerifier: process.env.AEGIS_VERIFIER_URL,
820
- },
821
- });
822
- }
823
- return runtime;
824
- }
428
+ var html = String.raw`<!doctype html>
429
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
430
+ <title>My OpenAgent</title><link rel="stylesheet" href="/style.css"><script type="module" src="/viewer.js"></script></head>
431
+ <body><main><p class="eyebrow">OpenAgent · local starter</p><h1>Your agent, alive.</h1>
432
+ <div id="avatar" aria-label="Agent avatar"></div><p id="status" role="status">Connecting to the local receiver…</p>
433
+ <p id="identity"></p><div class="controls"><label>Skin <select id="skin"><option value="autos">Autos</option><option value="spacex">SpaceX</option></select></label>
434
+ <label>Expression <select id="expression"><option value="">Follow actual status</option></select></label></div>
435
+ <p>In another terminal, run:</p><pre>npm run agent -- "Hello from my agent"</pre>
436
+ <p>This viewer is read-only. A completed request appears after the receiver verifies a fresh signed proof.</p>
437
+ <p>Keys stay on this computer. No account or identity service is required.</p></main></body></html>
825
438
  `;
826
- }
827
- function authLib(options) {
828
- const oidcSetup = options.bridge === "oidc" ? `
829
- /**
830
- * OIDC configuration for human authentication.
831
- * Set OIDC_* env vars to enable the bridge.
832
- */
833
- export const oidcConfig = {
834
- issuerUrl: process.env.OIDC_ISSUER_URL,
835
- clientId: process.env.OIDC_CLIENT_ID,
836
- clientSecret: process.env.OIDC_CLIENT_SECRET,
837
- redirectUri: process.env.OIDC_REDIRECT_URI ?? 'http://localhost:3000/api/auth/callback',
838
- } as const;
839
-
840
- export function isOidcConfigured(): boolean {
841
- return Boolean(oidcConfig.issuerUrl && oidcConfig.clientId);
842
- }
843
- ` : "";
844
- return `/**
845
- * Authentication configuration \u2014 human + agent auth.
846
- */
847
-
848
- import { getRuntime } from './openagent.js';
849
-
850
- /**
851
- * Authenticate an incoming request. Returns the agent's auth context
852
- * or null if authentication fails.
853
- */
854
- export async function authenticateRequest(request: Request) {
855
- const runtime = getRuntime();
439
+ var css = String.raw`:root{font-family:system-ui,sans-serif;color:#ecf0f5;background:#10151c;color-scheme:dark}body{margin:0}main{max-width:760px;margin:0 auto;padding:48px 24px}h1{font-size:clamp(2rem,6vw,3.8rem);letter-spacing:-.05em;margin:12px 0}p{line-height:1.6;color:#bdc8d6}.eyebrow{font-size:.85rem;letter-spacing:.1em;text-transform:uppercase}#avatar{height:300px;display:flex;align-items:center;justify-content:center;margin:12px 0}#avatar svg{max-width:100%;height:280px}#identity{font-family:monospace;font-size:.85rem;overflow-wrap:anywhere}.controls{display:flex;gap:24px;flex-wrap:wrap}label{display:flex;gap:10px;align-items:center}select{padding:8px;border-radius:8px;background:#222d3b;color:inherit;border:1px solid #516176}pre{padding:18px;white-space:pre-wrap;background:#1b2532;border-radius:12px;border:1px solid #344458}#status{font-weight:600;color:#fff}
440
+ `;
441
+ var viewer = String.raw`import { mountCharacter, getCharacterExpressions } from '/character-player.js';
442
+
443
+ const skin = document.querySelector('#skin');
444
+ const expression = document.querySelector('#expression');
445
+ const label = document.querySelector('#status');
446
+ const identity = document.querySelector('#identity');
447
+ for (const value of getCharacterExpressions()) {
448
+ const option = document.createElement('option'); option.value = value; option.textContent = value;
449
+ expression.append(option);
450
+ }
451
+ let player, did, revision = -1, latestStatus = 'idle', stopped = false, timer;
452
+ skin.addEventListener('change', () => player?.setAppearance({ skin: skin.value }));
453
+ expression.addEventListener('change', () => player?.update({ status: latestStatus, expression: expression.value || null }));
454
+ async function refresh() {
856
455
  try {
857
- return await runtime.authenticate(request);
456
+ const response = await fetch('/state', { cache: 'no-store', signal: AbortSignal.timeout(3000) });
457
+ if (!response.ok) throw new Error('Receiver unavailable.');
458
+ const state = await response.json();
459
+ if (stopped) return;
460
+ if (state.schemaVersion !== 1 || typeof state.did !== 'string' || !Number.isSafeInteger(state.revision) || state.revision < 0 ||
461
+ !['idle', 'working', 'success'].includes(state.status) || !Number.isSafeInteger(state.completedRequests) || state.completedRequests < 0) throw new Error('Invalid receiver state.');
462
+ if (!player || state.did !== did) {
463
+ player?.dispose(); document.querySelector('#avatar').replaceChildren();
464
+ did = state.did; player = mountCharacter(document.querySelector('#avatar'), did, { skin: skin.value, size: 280, reducedMotion: 'system' });
465
+ identity.textContent = did; revision = -1;
466
+ }
467
+ if (state.revision !== revision) {
468
+ latestStatus = state.status; player.update({ status: latestStatus, expression: expression.value || null }); revision = state.revision;
469
+ }
470
+ label.textContent = state.status + ' · ' + state.completedRequests + ' verified request' + (state.completedRequests === 1 ? '' : 's');
858
471
  } catch {
859
- return null;
860
- }
861
- }
862
- ${oidcSetup}`;
472
+ if (!stopped) label.textContent = 'Receiver unavailable · showing the last verified request state';
473
+ } finally { if (!stopped) timer = setTimeout(refresh, 750); }
863
474
  }
864
- function readme3(options) {
865
- return `# ${options.projectName}
866
-
867
- A Next.js dashboard for managing OpenAgent-powered agents.
868
-
869
- ## Setup
870
-
871
- \`\`\`bash
872
- npm install
873
- cp .env.example .env
874
- \`\`\`
875
-
876
- ## Development
877
-
878
- \`\`\`bash
879
- npm run dev
880
- \`\`\`
881
-
882
- Open [http://localhost:3000](http://localhost:3000).
883
-
884
- ## API Routes
885
-
886
- - \`GET /api/agents\` \u2014 List agents
887
- - \`POST /api/agents\` \u2014 Create agent
888
- - \`POST /api/auth\` \u2014 Authenticate request
889
-
890
- ## Learn More
891
-
892
- - [OpenAgent Docs](https://openagent.id/docs)
893
- - [Next.js Docs](https://nextjs.org/docs)
475
+ window.addEventListener('pagehide', () => { stopped = true; clearTimeout(timer); player?.dispose(); player = undefined; });
476
+ window.addEventListener('pageshow', event => { if (event.persisted) { stopped = false; refresh(); } });
477
+ refresh();
894
478
  `;
895
- }
479
+ var readme = String.raw`# Your OpenAgent starter
896
480
 
897
- // src/templates/full-stack.ts
898
- function generateFullStackTemplate(options) {
899
- return [
900
- { path: "package.json", content: packageJson4(options) },
901
- { path: "next.config.ts", content: nextConfig2() },
902
- { path: "src/app/layout.tsx", content: layoutTsx2(options) },
903
- { path: "src/app/page.tsx", content: pageTsx2(options) },
904
- { path: "src/app/api/auth/route.ts", content: authRoute2() },
905
- { path: "src/app/api/agents/route.ts", content: agentsRoute2() },
906
- { path: "src/lib/openagent.ts", content: openagentLib2() },
907
- { path: "src/lib/auth.ts", content: authLib2(options) },
908
- { path: "src/tools/echo.ts", content: echoToolTs2() },
909
- { path: "src/tools/index.ts", content: toolsIndexTs2() },
910
- { path: "README.md", content: readme4(options) }
911
- ];
912
- }
913
- function packageJson4(options) {
914
- const deps = {
915
- "@openagentid/sdk": "^0.1.0",
916
- "@modelcontextprotocol/sdk": "^1.0.0",
917
- next: "^15.0.0",
918
- react: "^19.0.0",
919
- "react-dom": "^19.0.0"
920
- };
921
- if (options.bioagentic) {
922
- deps["@openagentid/bioagentic"] = "^0.1.0";
923
- }
924
- if (options.fabrics) {
925
- deps["@openagentid/fabrics"] = "^0.1.0";
926
- }
927
- return JSON.stringify(
928
- {
929
- name: options.projectName,
930
- version: "0.1.0",
931
- private: true,
932
- scripts: {
933
- dev: "next dev",
934
- build: "next build",
935
- start: "next start",
936
- typecheck: "tsc --noEmit"
937
- },
938
- dependencies: deps,
939
- devDependencies: {
940
- "@types/node": "^20.11.30",
941
- "@types/react": "^19.0.0",
942
- "@types/react-dom": "^19.0.0",
943
- typescript: "^5.4.5"
944
- }
945
- },
946
- null,
947
- 2
948
- );
949
- }
950
- function nextConfig2() {
951
- return `import type { NextConfig } from 'next';
481
+ Requires Node 20+. Run npm install, then npm run dev. Open the numeric loopback URL printed by the server. In another terminal run:
952
482
 
953
- const config: NextConfig = {
954
- reactStrictMode: true,
955
- experimental: {
956
- serverActions: {
957
- bodySizeLimit: '2mb',
958
- },
959
- },
960
- };
483
+ npm run agent -- "Hello from my agent"
961
484
 
962
- export default config;
963
- `;
964
- }
965
- function layoutTsx2(options) {
966
- return `import type { Metadata } from 'next';
967
- import type { ReactNode } from 'react';
968
-
969
- export const metadata: Metadata = {
970
- title: '${options.projectName} \u2014 OpenAgent Full Stack',
971
- description: 'Full-stack agent platform powered by OpenAgent.',
972
- };
485
+ The command obtains a receiver-issued challenge, signs it with a persisted Ed25519 key, and sends a proof. After verification, the receiver hashes and echoes the message. The browser shows the real completed-request count, animated Autos by default, optional SpaceX and selectable expressions. It does not simulate work or broadcast runtime state elsewhere. Reduced motion follows your browser preference.
973
486
 
974
- export default function RootLayout({ children }: { children: ReactNode }) {
975
- return (
976
- <html lang="en">
977
- <body style={{ fontFamily: 'system-ui, sans-serif', margin: 0, padding: '2rem' }}>
978
- <header style={{ borderBottom: '1px solid #eee', paddingBottom: '1rem', marginBottom: '2rem' }}>
979
- <h1 style={{ fontSize: '1.5rem', margin: 0 }}>
980
- ${options.projectName}
981
- </h1>
982
- <p style={{ color: '#666', margin: '0.25rem 0 0' }}>OpenAgent Full Stack</p>
983
- </header>
984
- <main>{children}</main>
985
- </body>
986
- </html>
987
- );
988
- }
989
- `;
990
- }
991
- function pageTsx2(options) {
992
- return `/**
993
- * Dashboard home \u2014 agent status, tools, and management.
994
- */
487
+ On first setup, src/workspaces.mjs creates two separate local authorities, persists a unique receiver audience and explicitly imports the signer's public DID/key document into the receiver with trustIdentity. The receiver has no signing key for that agent. The public export is .openagent/identity.json. Private keys, challenge and replay state remain under .openagent/ and are excluded from git. Inspect public identity with npm run identity -- export. Never publish .openagent/ or serve it as static assets.
995
488
 
996
- import { tools } from '@/tools';
489
+ Run npm test for real generated-key HTTP verification, restart/replay/revocation and transport refusals. No account, hosted identity service, private package or network identity lookup is involved. Installing the public SDK is the only dependency setup.
997
490
 
998
- export default async function HomePage() {
999
- const agentName = process.env.OPENAGENT_AGENT_NAME ?? '${options.projectName}';
1000
- const parentDid = process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:unconfigured';
491
+ npm run identity -- revoke
1001
492
 
1002
- return (
1003
- <div>
1004
- <section style={{ marginBottom: '2rem' }}>
1005
- <h2>Agent Status</h2>
1006
- <table style={{ borderCollapse: 'collapse', width: '100%', maxWidth: '600px' }}>
1007
- <tbody>
1008
- <tr>
1009
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Name</td>
1010
- <td style={{ padding: '0.5rem' }}>{agentName}</td>
1011
- </tr>
1012
- <tr>
1013
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Parent DID</td>
1014
- <td style={{ padding: '0.5rem', fontFamily: 'monospace', fontSize: '0.85rem' }}>
1015
- {parentDid}
1016
- </td>
1017
- </tr>
1018
- <tr>
1019
- <td style={{ padding: '0.5rem', fontWeight: 'bold' }}>Status</td>
1020
- <td style={{ padding: '0.5rem', color: '#22c55e' }}>Active</td>
1021
- </tr>
1022
- </tbody>
1023
- </table>
1024
- </section>
493
+ Revocation is the receiver's durable local trust decision. Restarting does not restore trust. A revoked starter will refuse further agent requests. Existing or incomplete state is preserved and never silently reset; deliberately choose a separate workspace if you want a new identity/receiver. This example does not claim global revocation, human ownership, application roles or a distributed database.
1025
494
 
1026
- <section style={{ marginBottom: '2rem' }}>
1027
- <h2>Available Tools</h2>
1028
- <ul style={{ lineHeight: 2 }}>
1029
- {tools.map((tool) => (
1030
- <li key={tool.name}>
1031
- <code>{tool.name}</code> \u2014 {tool.description}
1032
- </li>
1033
- ))}
1034
- </ul>
1035
- </section>
495
+ The HTTP service listens only on 127.0.0.1:3210. Set PORT consistently for the server and agent command to choose another port, or set OPENAGENT_RECEIVER_URL on the command to the exact printed numeric origin. Host checks prevent DNS rebinding, cross-origin browser controls are refused, and only fixed public assets are served. Trust/revoke are local operator actions, never HTTP controls. GET /state contains only public DID and transient request presentation metadata; it contains no message, output, token or key.
1036
496
 
1037
- <section>
1038
- <h2>API Endpoints</h2>
1039
- <ul style={{ lineHeight: 2 }}>
1040
- <li><code>GET /api/agents</code> \u2014 List managed agents</li>
1041
- <li><code>POST /api/agents</code> \u2014 Create a new agent</li>
1042
- <li><code>POST /api/auth</code> \u2014 Authenticate request</li>
1043
- </ul>
1044
- </section>
1045
- </div>
1046
- );
1047
- }
497
+ This starter deliberately has no cloud-deployment, MCP, enterprise bridge or optional identity-stack dependency. Historical 0.1 templates need migration and are not generated by create-openagent 0.2.
1048
498
  `;
1049
- }
1050
- function authRoute2() {
1051
- return `/**
1052
- * POST /api/auth \u2014 Authenticate an incoming agent request.
1053
- */
1054
-
1055
- import { NextResponse, type NextRequest } from 'next/server';
1056
- import { getRuntime } from '@/lib/openagent';
1057
-
1058
- export async function POST(request: NextRequest): Promise<NextResponse> {
1059
- const runtime = getRuntime();
1060
-
1061
- try {
1062
- const ctx = await runtime.authenticate(request as unknown as Request);
1063
- return NextResponse.json({
1064
- success: true,
1065
- data: {
1066
- did: ctx.did,
1067
- root: ctx.root,
1068
- scopes: ctx.scopes,
1069
- expiresAt: ctx.expiresAt,
1070
- },
499
+ var tests = String.raw`import test from 'node:test';
500
+ import assert from 'node:assert/strict';
501
+ import { mkdtemp, rm, readFile, realpath } from 'node:fs/promises';
502
+ import { tmpdir } from 'node:os';
503
+ import { join } from 'node:path';
504
+ import { createHash } from 'node:crypto';
505
+ import { request as httpRequest } from 'node:http';
506
+ import { OpenAgentWorkspace } from '@openagentid/sdk/local';
507
+ import { openStarter } from '../src/workspaces.mjs';
508
+ import { startServer } from '../src/server.mjs';
509
+ import { sendAgentRequest } from '../src/agent.mjs';
510
+
511
+ async function fixture(t) {
512
+ const root = await realpath(await mkdtemp(join(tmpdir(), 'openagent-starter-test-')));
513
+ t.after(() => rm(root, { recursive: true, force: true }));
514
+ let app = await startServer({ root, port: 0 });
515
+ t.after(async () => { if (app) await app.close(); });
516
+ return { root, get app() { return app; }, async restart() { await app.close(); app = await startServer({ root, port: 0 }); } };
517
+ }
518
+ async function post(app, route, body, extra = {}) {
519
+ return fetch(app.url + route, { method: 'POST', headers: { 'content-type': 'application/json', ...extra },
520
+ body: JSON.stringify(body), signal: AbortSignal.timeout(5000) });
521
+ }
522
+ async function signed(app, message = 'Real signed request') {
523
+ const response = await post(app, '/challenge', { did: app.did });
524
+ assert.equal(response.status, 200);
525
+ const challenge = await response.json();
526
+ const proof = await app.workspaces.signer.prove(app.did, { nonce: challenge.nonce, audience: challenge.audience });
527
+ return { nonce: challenge.nonce, proof, message };
528
+ }
529
+ test('separate persisted signer and explicit public receiver trust complete actual HTTP work', async t => {
530
+ const f = await fixture(t);
531
+ const agents = await f.app.workspaces.receiver.listAgents();
532
+ assert.equal(agents.find(value => value.did === f.app.did).hasPrivateKey, false);
533
+ const exported = JSON.parse(await readFile(join(f.root, '.openagent', 'identity.json'), 'utf8'));
534
+ assert.equal(exported.did, f.app.did); assert.equal('d' in exported.publicKeyJwk, false);
535
+ const message = 'Hello, actual receiver';
536
+ const output = await sendAgentRequest({ root: f.root, url: f.app.url, message });
537
+ assert.deepEqual(output, { schemaVersion: 1, did: f.app.did, message, digest: createHash('sha256').update(message).digest('hex') });
538
+ const state = await (await fetch(f.app.url + '/state')).json();
539
+ assert.equal(state.status, 'success'); assert.equal(state.completedRequests, 1);
540
+ assert.equal(state.revision, 2); assert.equal(typeof state.lastCompletedAtMs, 'number');
541
+ assert.equal('message' in state, false); assert.equal('proof' in state, false);
542
+ });
543
+ test('issued challenge, public key and replay consumption survive actual reopen', async t => {
544
+ const f = await fixture(t);
545
+ const did = f.app.did;
546
+ const audience = f.app.workspaces.audience;
547
+ const body = await signed(f.app);
548
+ await f.restart(); assert.equal(f.app.did, did); assert.equal(f.app.workspaces.audience, audience);
549
+ assert.equal((await post(f.app, '/request', body)).status, 200);
550
+ await f.restart(); assert.equal(f.app.did, did);
551
+ const replay = await post(f.app, '/request', body);
552
+ assert.equal(replay.status, 403); assert.deepEqual(await replay.json(), { error: 'identity_refused' });
553
+ assert.equal((await (await fetch(f.app.url + '/state')).json()).completedRequests, 0);
554
+ });
555
+ test('one issued proof has exactly one winner under concurrent real HTTP verification', async t => {
556
+ const f = await fixture(t);
557
+ const body = await signed(f.app);
558
+ const responses = await Promise.all([post(f.app, '/request', body), post(f.app, '/request', body)]);
559
+ assert.deepEqual(responses.map(response => response.status).sort(), [200, 403]);
560
+ assert.equal((await (await fetch(f.app.url + '/state')).json()).completedRequests, 1);
561
+ });
562
+ test('each installation pins a different durable intended audience and refuses valid signatures for another receiver', async t => {
563
+ const f = await fixture(t);
564
+ const other = await fixture(t);
565
+ assert.notEqual(f.app.workspaces.audience, other.app.workspaces.audience);
566
+ const body = await signed(f.app);
567
+ const wrongRecipient = await f.app.workspaces.signer.prove(f.app.did, { nonce: body.nonce, audience: other.app.workspaces.audience });
568
+ assert.equal((await post(f.app, '/request', { ...body, proof: wrongRecipient })).status, 403);
569
+ assert.equal((await post(f.app, '/request', body)).status, 200);
570
+ });
571
+ test('receiver revocation denies already issued proof and is never undone by restart', async t => {
572
+ const f = await fixture(t);
573
+ const body = await signed(f.app);
574
+ await f.app.workspaces.receiver.revoke(f.app.did);
575
+ await f.restart();
576
+ assert.equal((await post(f.app, '/request', body)).status, 403);
577
+ assert.equal((await post(f.app, '/challenge', { did: f.app.did })).status, 403);
578
+ assert.equal((await f.app.workspaces.receiver.listAgents()).find(agent => agent.did === f.app.did).revoked, true);
579
+ });
580
+ test('altered signature, never-issued nonce and unknown generated key fail without changing viewer state', async t => {
581
+ const f = await fixture(t);
582
+ const body = await signed(f.app);
583
+ const token = body.proof.token.split('.');
584
+ token[2] = (token[2][0] === 'A' ? 'B' : 'A') + token[2].slice(1);
585
+ assert.equal((await post(f.app, '/request', { ...body, proof: { ...body.proof, token: token.join('.') } })).status, 403);
586
+ assert.equal((await post(f.app, '/request', { ...body, nonce: 'never-issued' })).status, 403);
587
+ const audience = f.app.workspaces.audience;
588
+ const stranger = await OpenAgentWorkspace.initialize(join(f.root, 'stranger'), { audience });
589
+ const foreign = await stranger.createAgent('stranger');
590
+ const proof = await stranger.prove(foreign.did, { nonce: body.nonce, audience });
591
+ assert.equal((await post(f.app, '/request', { ...body, proof })).status, 403);
592
+ assert.equal((await (await fetch(f.app.url + '/state')).json()).completedRequests, 0);
593
+ assert.equal((await post(f.app, '/request', body)).status, 200);
594
+ });
595
+ test('host, browser-origin, closed-schema and private/static paths are refused', async t => {
596
+ const f = await fixture(t);
597
+ // fetch may replace Host with its URL's authority. Native HTTP sends the
598
+ // adversarial wire header while retaining the real loopback destination.
599
+ const rogueHost = await new Promise((accept, reject) => {
600
+ const body = JSON.stringify({ did: f.app.did });
601
+ const req = httpRequest(f.app.url + '/challenge', { method: 'POST', headers: {
602
+ host: 'attacker.example', 'content-type': 'application/json', 'content-length': Buffer.byteLength(body),
603
+ } }, response => {
604
+ const chunks = [];
605
+ response.on('data', chunk => chunks.push(chunk));
606
+ response.once('error', reject);
607
+ response.once('end', () => accept({ status: response.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
1071
608
  });
1072
- } catch (err) {
1073
- const message = err instanceof Error ? err.message : 'Authentication failed';
1074
- return NextResponse.json({ success: false, error: message }, { status: 401 });
609
+ assert.equal(req.getHeader('host'), 'attacker.example');
610
+ req.once('error', reject); req.setTimeout(4500, () => req.destroy(new Error('Host refusal timed out.')));
611
+ req.end(body);
612
+ });
613
+ assert.equal(rogueHost.status, 403);
614
+ assert.deepEqual(JSON.parse(rogueHost.body), { error: 'loopback_only' });
615
+ assert.equal((await post(f.app, '/challenge', { did: f.app.did }, { origin: 'https://attacker.example' })).status, 403);
616
+ assert.equal((await post(f.app, '/challenge', { did: f.app.did }, { origin: f.app.url })).status, 403);
617
+ assert.equal((await post(f.app, '/challenge', { did: f.app.did, owner: 'invented' })).status, 400);
618
+ assert.equal((await post(f.app, '/challenge?did=x', { did: f.app.did })).status, 404);
619
+ assert.equal((await post(f.app, '/revoke', { did: f.app.did })).status, 404);
620
+ for (const path of ['/.openagent/identity.json', '/.openagent/signer/state.json', '/%2e%2e/src/workspaces.mjs']) {
621
+ assert.equal((await fetch(f.app.url + path)).status, 404);
1075
622
  }
1076
- }
1077
- `;
1078
- }
1079
- function agentsRoute2() {
1080
- return `/**
1081
- * GET /api/agents \u2014 List managed agents.
1082
- * POST /api/agents \u2014 Create a new agent.
1083
- */
1084
-
1085
- import { NextResponse, type NextRequest } from 'next/server';
1086
- import { getRuntime } from '@/lib/openagent';
1087
-
1088
- const agents: Array<{ did: string; name: string; createdAt: string }> = [];
1089
-
1090
- export async function GET(): Promise<NextResponse> {
1091
- return NextResponse.json({ success: true, data: agents });
1092
- }
1093
-
1094
- export async function POST(request: NextRequest): Promise<NextResponse> {
1095
- const runtime = getRuntime();
1096
-
1097
- try {
1098
- const body = (await request.json()) as { name: string; scopes: string[] };
1099
-
1100
- if (!body.name || !body.scopes?.length) {
1101
- return NextResponse.json(
1102
- { success: false, error: 'name and scopes are required' },
1103
- { status: 400 },
1104
- );
1105
- }
1106
-
1107
- const parentDid = process.env.OPENAGENT_PARENT_DID ?? 'did:oas:l1fe:hmr:unconfigured';
1108
- const agent = await runtime.createAgent({
1109
- parent: parentDid,
1110
- name: body.name,
1111
- scopes: body.scopes,
623
+ const viewer = await fetch(f.app.url + '/');
624
+ assert.equal(viewer.status, 200); assert.equal(viewer.headers.get('cache-control'), 'no-store');
625
+ assert.match(viewer.headers.get('content-security-policy'), /frame-ancestors 'none'/);
626
+ assert.match(await viewer.text(), /Autos/);
627
+ const script = await fetch(f.app.url + '/character-player.js');
628
+ assert.equal(script.status, 200); assert.match(await script.text(), /mountCharacter/);
629
+ const huge = await post(f.app, '/request', { value: 'x'.repeat(25000) }); assert.equal(huge.status, 413);
630
+ });
631
+ test('chunked oversized and unfinished request bodies are bounded by the real HTTP server', async t => {
632
+ const f = await fixture(t);
633
+ async function raw(body, incomplete) {
634
+ return new Promise((accept, reject) => {
635
+ const req = httpRequest(f.app.url + '/challenge', { method: 'POST', headers: { 'content-type': 'application/json',
636
+ ...(incomplete ? { 'content-length': '20' } : {}) } }, response => {
637
+ response.resume(); response.once('end', () => accept(response.statusCode));
638
+ });
639
+ req.once('error', reject); req.setTimeout(4500, () => req.destroy(new Error('Body refusal timed out.')));
640
+ req.write(body); if (!incomplete) req.end();
1112
641
  });
1113
-
1114
- const record = {
1115
- did: agent.did,
1116
- name: body.name,
1117
- createdAt: new Date().toISOString(),
1118
- };
1119
- agents.push(record);
1120
-
1121
- return NextResponse.json({ success: true, data: record }, { status: 201 });
1122
- } catch (err) {
1123
- const message = err instanceof Error ? err.message : 'Agent creation failed';
1124
- return NextResponse.json({ success: false, error: message }, { status: 500 });
1125
642
  }
1126
- }
643
+ assert.equal(await raw('x'.repeat(25000), false), 413);
644
+ assert.equal(await raw('{', true), 408);
645
+ });
646
+ test('incomplete setup is retained rather than creating a replacement identity', async t => {
647
+ const f = await fixture(t);
648
+ const before = await f.app.workspaces.signer.exportIdentity(f.app.did);
649
+ await rm(join(f.root, '.openagent', 'starter.json'));
650
+ await assert.rejects(openStarter(f.root), /Incomplete starter setup/);
651
+ assert.deepEqual(await f.app.workspaces.signer.exportIdentity(f.app.did), before);
652
+ });
1127
653
  `;
1128
- }
1129
- function openagentLib2() {
1130
- return `/**
1131
- * OpenAgent SDK setup \u2014 shared runtime instance.
1132
- */
1133
-
1134
- import { OpenAgent, type OpenAgentRuntime } from '@openagentid/sdk';
1135
-
1136
- let runtime: OpenAgentRuntime | undefined;
1137
654
 
1138
- export function getRuntime(): OpenAgentRuntime {
1139
- if (!runtime) {
1140
- runtime = OpenAgent.configure({
1141
- endpoints: {
1142
- oasResolver: process.env.OAS_RESOLVER_URL,
1143
- arsenalBroker: process.env.ARSENAL_BROKER_URL,
1144
- aegisVerifier: process.env.AEGIS_VERIFIER_URL,
1145
- },
1146
- });
655
+ // src/scaffold.ts
656
+ async function scaffold(options, cwd = process.cwd()) {
657
+ const name = validateProjectName(options.projectName);
658
+ if (options.template !== "starter" || options.adapters.length !== 0 || options.bridge !== "none" || options.deploy !== "local" || options.bioagentic !== false || options.fabrics !== false) {
659
+ throw new Error("Only the local starter is supported in 0.2; historical template options require migration.");
1147
660
  }
1148
- return runtime;
1149
- }
1150
- `;
1151
- }
1152
- function authLib2(options) {
1153
- const oidcPart = options.bridge === "oidc" ? `
1154
-
1155
- export const oidcConfig = {
1156
- issuerUrl: process.env.OIDC_ISSUER_URL,
1157
- clientId: process.env.OIDC_CLIENT_ID,
1158
- clientSecret: process.env.OIDC_CLIENT_SECRET,
1159
- redirectUri: process.env.OIDC_REDIRECT_URI ?? 'http://localhost:3000/api/auth/callback',
1160
- } as const;
1161
-
1162
- export function isOidcConfigured(): boolean {
1163
- return Boolean(oidcConfig.issuerUrl && oidcConfig.clientId);
1164
- }
1165
- ` : "";
1166
- return `/**
1167
- * Authentication configuration \u2014 human + agent auth.
1168
- */
1169
-
1170
- import { getRuntime } from './openagent.js';
1171
-
1172
- export async function authenticateRequest(request: Request) {
1173
- const runtime = getRuntime();
661
+ const files = generateStarterTemplate(name);
662
+ const names = /* @__PURE__ */ new Set();
663
+ for (const file of files) {
664
+ if (!/^(?:[a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(file.path) || file.path.split("/").some((part) => part === "." || part === "..") || names.has(file.path)) {
665
+ throw new Error("Invalid starter file path.");
666
+ }
667
+ names.add(file.path);
668
+ }
669
+ const root = resolve(cwd, name);
1174
670
  try {
1175
- return await runtime.authenticate(request);
1176
- } catch {
1177
- return null;
671
+ mkdirSync(root, { recursive: false });
672
+ } catch (error) {
673
+ if (error.code === "EEXIST") throw new Error("Destination already exists: " + name + ". Choose a new directory.");
674
+ throw error;
675
+ }
676
+ for (const file of files) {
677
+ const destination = join(root, file.path);
678
+ mkdirSync(dirname(destination), { recursive: true });
679
+ writeFileSync(destination, file.content, { encoding: "utf8", flag: "wx" });
1178
680
  }
1179
681
  }
1180
- ${oidcPart}`;
1181
- }
1182
- function echoToolTs2() {
1183
- return `/**
1184
- * Echo tool \u2014 returns the input unchanged. Useful for testing.
1185
- */
1186
-
1187
- import type { McpTool } from './index.js';
1188
-
1189
- export const echoTool: McpTool = {
1190
- name: 'echo',
1191
- description: 'Returns the input unchanged.',
1192
- inputSchema: {
1193
- type: 'object',
1194
- properties: {
1195
- message: { type: 'string', description: 'Message to echo back.' },
1196
- },
1197
- required: ['message'],
1198
- },
1199
- async execute(input) {
1200
- return { echoed: input };
1201
- },
1202
- };
1203
- `;
1204
- }
1205
- function toolsIndexTs2() {
1206
- return `/**
1207
- * Tool registry \u2014 add your tools here.
1208
- */
1209
-
1210
- import { echoTool } from './echo.js';
1211
-
1212
- export interface McpTool {
1213
- readonly name: string;
1214
- readonly description: string;
1215
- readonly inputSchema: Record<string, unknown>;
1216
- execute(input: Record<string, unknown>): Promise<unknown>;
1217
- }
1218
-
1219
- export const tools: readonly McpTool[] = [echoTool];
1220
- `;
1221
- }
1222
- function readme4(options) {
1223
- return `# ${options.projectName}
1224
-
1225
- A full-stack OpenAgent application: agent identity, MCP tools, and web dashboard.
1226
-
1227
- ## Setup
1228
-
1229
- \`\`\`bash
1230
- npm install
1231
- cp .env.example .env
1232
- \`\`\`
1233
-
1234
- ## Development
1235
-
1236
- \`\`\`bash
1237
- npm run dev
1238
- \`\`\`
1239
-
1240
- Open [http://localhost:3000](http://localhost:3000).
1241
-
1242
- ## Structure
1243
682
 
1244
- - \`src/app/\` \u2014 Next.js app directory (dashboard + API routes)
1245
- - \`src/lib/\` \u2014 OpenAgent SDK setup and auth configuration
1246
- - \`src/tools/\` \u2014 MCP tool implementations
1247
-
1248
- ## Adding Tools
1249
-
1250
- 1. Create a new file in \`src/tools/\`
1251
- 2. Export an \`McpTool\` object
1252
- 3. Add it to the \`tools\` array in \`src/tools/index.ts\`
683
+ // src/index.ts
684
+ var help = `create-openagent \u2014 local, open agent identity starter
1253
685
 
1254
- ## Learn More
686
+ Usage: create-openagent <project-name> [--yes] [--template starter]
1255
687
 
1256
- - [OpenAgent Docs](https://openagent.id/docs)
1257
- - [MCP Specification](https://modelcontextprotocol.io)
1258
- - [Next.js Docs](https://nextjs.org/docs)
1259
- `;
1260
- }
688
+ --yes, -y Create without prompts (requires a name)
689
+ --template starter The runnable Node 20+ starter (default)
690
+ --help, -h Show help
691
+ --version, -v Show version
1261
692
 
1262
- // src/templates/shared.ts
1263
- function generateSharedFiles(options) {
1264
- return [
1265
- { path: "tsconfig.json", content: tsconfig(options) },
1266
- { path: ".gitignore", content: gitignore() },
1267
- { path: ".env.example", content: envExample(options) }
1268
- ];
1269
- }
1270
- function tsconfig(options) {
1271
- const jsx = options.template === "web-dashboard" || options.template === "full-stack";
1272
- return JSON.stringify(
1273
- {
1274
- compilerOptions: {
1275
- target: "ES2022",
1276
- module: "ESNext",
1277
- moduleResolution: "bundler",
1278
- lib: jsx ? ["ES2022", "DOM", "DOM.Iterable"] : ["ES2022"],
1279
- strict: true,
1280
- noEmit: true,
1281
- skipLibCheck: true,
1282
- forceConsistentCasingInFileNames: true,
1283
- isolatedModules: true,
1284
- resolveJsonModule: true,
1285
- esModuleInterop: true,
1286
- ...jsx ? { jsx: "preserve" } : {},
1287
- ...jsx ? { plugins: [{ name: "next" }] } : {},
1288
- ...jsx ? { paths: { "@/*": ["./src/*"] } } : {}
1289
- },
1290
- include: jsx ? ["src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"] : ["src/**/*.ts"],
1291
- exclude: ["node_modules", "dist"]
1292
- },
1293
- null,
1294
- 2
1295
- );
1296
- }
1297
- function gitignore() {
1298
- return `node_modules/
1299
- dist/
1300
- .next/
1301
- .env
1302
- .env.local
1303
- *.tsbuildinfo
1304
- coverage/
693
+ No account or hosted identity service is required.
694
+ Historical templates, adapters, bridges and deployment flags need migration
695
+ and are not generated by this release.
1305
696
  `;
1306
- }
1307
- function envExample(options) {
1308
- const lines = [
1309
- "# OpenAgent Configuration",
1310
- "OPENAGENT_PARENT_DID=did:oas:l1fe:hmr:your-human-root",
1311
- "OPENAGENT_AGENT_NAME=my-agent",
1312
- "",
1313
- "# Endpoints (optional \u2014 SDK uses stubs when omitted)",
1314
- "# OAS_RESOLVER_URL=https://resolver.openagent.id",
1315
- "# ARSENAL_BROKER_URL=https://broker.arsenal.id",
1316
- "# AEGIS_VERIFIER_URL=https://verify.aegis.id"
1317
- ];
1318
- if (options.bridge === "oidc") {
1319
- lines.push(
1320
- "",
1321
- "# OIDC Bridge",
1322
- "# OIDC_ISSUER_URL=https://your-idp.example.com",
1323
- "# OIDC_CLIENT_ID=your-client-id",
1324
- "# OIDC_CLIENT_SECRET=your-client-secret"
1325
- );
1326
- }
1327
- if (options.bioagentic) {
1328
- lines.push("", "# Bioagentic", "# BIOAGENTIC_THRESHOLD=0.8");
1329
- }
1330
- lines.push("");
1331
- return lines.join("\n");
1332
- }
1333
-
1334
- // src/scaffold.ts
1335
- async function scaffold(options) {
1336
- const s = spinner();
1337
- s.start("Scaffolding project...");
1338
- const root = join(process.cwd(), options.projectName);
1339
- if (existsSync(root)) {
1340
- s.stop("Directory already exists.");
1341
- throw new Error(`Directory "${options.projectName}" already exists.`);
1342
- }
1343
- const files = [];
1344
- switch (options.template) {
1345
- case "agent":
1346
- files.push(...generateAgentTemplate(options));
1347
- break;
1348
- case "mcp-server":
1349
- files.push(...generateMcpServerTemplate(options));
1350
- break;
1351
- case "web-dashboard":
1352
- files.push(...generateWebDashboardTemplate(options));
1353
- break;
1354
- case "full-stack":
1355
- files.push(...generateFullStackTemplate(options));
1356
- break;
1357
- }
1358
- files.push(...generateSharedFiles(options));
1359
- for (const file of files) {
1360
- const fullPath = join(root, file.path);
1361
- const dir = dirname(fullPath);
1362
- mkdirSync(dir, { recursive: true });
1363
- writeFileSync(fullPath, file.content, "utf-8");
1364
- }
1365
- s.stop(`Created ${files.length} files in ${options.projectName}/`);
1366
- }
1367
-
1368
- // src/index.ts
1369
- async function main() {
1370
- const args = process.argv.slice(2);
1371
- const projectName = args.find((arg) => !arg.startsWith("-"));
1372
- intro(pc.bgCyan(pc.black(" create-openagent ")));
1373
- let options;
1374
- try {
1375
- options = await collectOptions(projectName, args);
1376
- } catch {
1377
- outro(pc.red("Cancelled."));
1378
- process.exit(1);
1379
- }
1380
- if (isCancel2(options)) {
1381
- outro(pc.red("Cancelled."));
1382
- process.exit(1);
1383
- }
697
+ async function runCli(args = process.argv.slice(2)) {
1384
698
  try {
699
+ const input = parseArguments(args);
700
+ if (input.kind !== "create") {
701
+ process.stdout.write(input.kind === "help" ? help : package_default.version + "\n");
702
+ return 0;
703
+ }
704
+ const options = await collectOptions(input);
1385
705
  await scaffold(options);
1386
- } catch (err) {
1387
- const message = err instanceof Error ? err.message : "Unknown error";
1388
- outro(pc.red(`Failed to scaffold project: ${message}`));
1389
- process.exit(1);
706
+ process.stdout.write("Created " + options.projectName + "/\n\n cd " + options.projectName + '\n npm install\n npm run dev\n\nThen, in another terminal: npm run agent -- "Hello from my agent"\n');
707
+ return 0;
708
+ } catch (error) {
709
+ process.stderr.write("create-openagent: " + (error instanceof Error ? error.message : "Failed to create project.") + "\n");
710
+ return 1;
1390
711
  }
1391
- outro(pc.green("Project created successfully!"));
1392
- const relativePath = options.projectName;
1393
- process.stdout.write("\n");
1394
- process.stdout.write(pc.bold("Next steps:\n"));
1395
- process.stdout.write(` cd ${relativePath}
1396
- `);
1397
- process.stdout.write(" npm install\n");
1398
- if (options.template === "web-dashboard" || options.template === "full-stack") {
1399
- process.stdout.write(" npm run dev\n");
1400
- } else {
1401
- process.stdout.write(" npm run build\n");
1402
- process.stdout.write(" npm start\n");
1403
- }
1404
- process.stdout.write("\n");
1405
- process.stdout.write(pc.dim(`Docs: https://openagent.id/docs
1406
- `));
1407
712
  }
1408
- main().catch((err) => {
1409
- process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
1410
- `);
1411
- process.exit(1);
1412
- });
713
+ if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(resolve2(process.argv[1])) === fileURLToPath(import.meta.url)) {
714
+ process.exitCode = await runCli();
715
+ }
716
+ export {
717
+ collectOptions,
718
+ generateStarterTemplate,
719
+ parseArguments,
720
+ runCli,
721
+ scaffold,
722
+ validateProjectName
723
+ };