agent-manifest 3.3.0 → 4.0.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/cli.js CHANGED
@@ -43,7 +43,7 @@ const program = new commander_1.Command();
43
43
  program
44
44
  .name('agentjson')
45
45
  .description('Universal agent manifest compiler — generates agent.json for any web app')
46
- .version('3.1.0')
46
+ .version('4.0.0')
47
47
  .option('-p, --path <path>', 'path to the project root', '.')
48
48
  .option('-o, --output <output>', 'output path for agent.json', './public/agent.json')
49
49
  .option('--author <author>', 'author name or organization')
@@ -51,6 +51,7 @@ program
51
51
  .option('--auth-type <type>', 'override detected auth type: none | bearer | api-key | oauth2 | basic | farcaster-frame | cookie')
52
52
  .option('--auth-header <header>', 'auth header name (default: Authorization)')
53
53
  .option('--auth-docs <url>', 'URL where agents can obtain credentials')
54
+ .option('--base-url <url>', 'canonical base URL for the app (e.g. https://myapp.com)')
54
55
  .action(async (options) => {
55
56
  const projectPath = path.resolve(options.path);
56
57
  const outputPath = path.resolve(options.output);
@@ -69,6 +70,7 @@ program
69
70
  const openApiParser = new index_1.OpenAPIParser();
70
71
  const prismaParser = new index_1.PrismaParser();
71
72
  const actions = [];
73
+ const dataModel = {};
72
74
  // Determine which files are OpenAPI / Prisma by extension/name
73
75
  const openApiExts = new Set(['.json', '.yaml', '.yml']);
74
76
  const prismaExt = '.prisma';
@@ -90,7 +92,8 @@ program
90
92
  // ── Prisma schema ─────────────────────────────────────────────────────
91
93
  if (ext === prismaExt || base === 'schema.prisma') {
92
94
  try {
93
- actions.push(...prismaParser.parseFile(file));
95
+ const prismaResult = prismaParser.parseFile(file);
96
+ Object.assign(dataModel, prismaResult.dataModel);
94
97
  }
95
98
  catch { /* ignore */ }
96
99
  continue;
@@ -131,7 +134,7 @@ program
131
134
  const uniqueActions = new Map();
132
135
  for (const action of actions) {
133
136
  const existing = uniqueActions.get(action.name);
134
- if (!existing || Object.keys(action.inputs).length > Object.keys(existing.inputs).length) {
137
+ if (!existing || Object.keys(action.parameters?.properties ?? {}).length > Object.keys(existing.parameters?.properties ?? {}).length) {
135
138
  uniqueActions.set(action.name, action);
136
139
  }
137
140
  }
@@ -148,11 +151,18 @@ program
148
151
  ...(options.authDocs && { docsUrl: options.authDocs }),
149
152
  };
150
153
  const generator = new index_1.ManifestGenerator();
151
- const manifest = generator.generate(Array.from(uniqueActions.values()), appMetadata, tsParser.getCapabilities(), auth);
154
+ const manifest = generator.generate(Array.from(uniqueActions.values()), appMetadata, tsParser.getCapabilities(), auth, '1.0.0', {
155
+ baseUrl: options.baseUrl ?? appMetadata.url,
156
+ dataModel,
157
+ });
152
158
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
153
159
  fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
160
+ const uiCount = Array.from(uniqueActions.values()).filter(a => a.type === 'ui').length;
161
+ const modelCount = Object.keys(dataModel).length;
154
162
  console.log(`✅ agent.json generated at: ${outputPath}`);
155
- console.log(` ${uniqueActions.size} actions · ${manifest.capabilities.length} capabilities · auth: ${auth.type}`);
163
+ console.log(` ${uniqueActions.size} actions${uiCount ? ` · ${uiCount} ui-interactions` : ''} · ${manifest.capabilities.length} capabilities · auth: ${auth.type}`);
164
+ if (modelCount > 0)
165
+ console.log(` dataModel: ${modelCount} models`);
156
166
  });
157
167
  // ─── validate ────────────────────────────────────────────────────────────────
158
168
  program
@@ -186,8 +196,12 @@ program
186
196
  });
187
197
  // ─── Structural validator ────────────────────────────────────────────────────
188
198
  const SAFETY_LEVELS = new Set(['read', 'write', 'financial', 'destructive', 'confidential']);
189
- const ACTION_TYPES = new Set(['api', 'contract', 'function', 'socket']);
190
- const AUTH_TYPES = new Set(['none', 'bearer', 'api-key', 'oauth2', 'basic', 'farcaster-frame', 'cookie']);
199
+ const ACTION_TYPES = new Set(['api', 'contract', 'function', 'socket', 'ui']);
200
+ const AUTH_TYPES = new Set([
201
+ 'none', 'bearer', 'api-key', 'oauth2', 'basic', 'cookie',
202
+ 'siwe', 'farcaster-siwf', 'farcaster-frame',
203
+ 'clerk', 'privy', 'dynamic', 'magic', 'passkey', 'saml', 'supabase',
204
+ ]);
191
205
  const INTENT_RE = /^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$/;
192
206
  function validateManifest(m) {
193
207
  const errors = [];
@@ -211,13 +225,19 @@ function validateManifest(m) {
211
225
  else if (!INTENT_RE.test(action.intent))
212
226
  errors.push(`${prefix}: \`intent\` must match domain.verb format`);
213
227
  if (!ACTION_TYPES.has(action.type))
214
- errors.push(`${prefix}: \`type\` must be one of api|contract|function`);
228
+ errors.push(`${prefix}: \`type\` must be one of api|contract|function|socket|ui`);
215
229
  if (!SAFETY_LEVELS.has(action.safety))
216
230
  errors.push(`${prefix}: \`safety\` must be one of read|write|financial|destructive|confidential`);
217
231
  if (typeof action.agentSafe !== 'boolean')
218
232
  errors.push(`${prefix}: \`agentSafe\` must be a boolean`);
219
233
  if (!action.requiredAuth)
220
234
  errors.push(`${prefix}: \`requiredAuth\` is missing`);
235
+ else if (!['public', 'required', 'farcaster-signed'].includes(action.requiredAuth.required))
236
+ errors.push(`${prefix}: \`requiredAuth.required\` must be "public", "required", or "farcaster-signed"`);
237
+ if (!action.parameters || typeof action.parameters.properties !== 'object')
238
+ errors.push(`${prefix}: \`parameters.properties\` must be an object`);
239
+ if (!action.returns || typeof action.returns.type !== 'string')
240
+ errors.push(`${prefix}: \`returns.type\` must be a string`);
221
241
  });
222
242
  }
223
243
  return errors;
@@ -72,6 +72,15 @@ const ALWAYS_EXCLUDE = [
72
72
  '!**/secrets/**',
73
73
  '!**/credentials/**',
74
74
  ];
75
+ /**
76
+ * Checks if a path is strictly within the project directory.
77
+ * Prevents path traversal via symbolic links or malformed paths.
78
+ */
79
+ function isPathWithinProject(filePath, projectPath) {
80
+ const resolved = path.resolve(filePath);
81
+ const root = path.resolve(projectPath);
82
+ return resolved.startsWith(root);
83
+ }
75
84
  class DiscoveryService {
76
85
  projectPath;
77
86
  cache = {};
@@ -150,6 +159,8 @@ class DiscoveryService {
150
159
  ...ALWAYS_EXCLUDE,
151
160
  ], { cwd: this.projectPath, absolute: true });
152
161
  for (const file of allTsFiles) {
162
+ if (!isPathWithinProject(file, this.projectPath))
163
+ continue;
153
164
  if (relevantFiles.includes(file))
154
165
  continue;
155
166
  const hash = fileHash(file);
@@ -2,13 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ManifestGenerator = void 0;
4
4
  class ManifestGenerator {
5
- generate(actions, metadata = {}, capabilities = [], auth = { type: 'none' }, version = '1.0.0') {
5
+ generate(actions, metadata = {}, capabilities = [], auth = { type: 'none' }, version = '1.0.0', options = {}) {
6
6
  return {
7
7
  name: metadata.name ?? 'Web App',
8
8
  description: metadata.description ?? 'Auto-generated agent manifest',
9
9
  version,
10
10
  ...(metadata.author && { author: metadata.author }),
11
11
  ...(metadata.url && { url: metadata.url }),
12
+ ...(options.baseUrl && { baseUrl: options.baseUrl }),
12
13
  auth,
13
14
  metadata: {
14
15
  ...(metadata.iconUrl && { iconUrl: metadata.iconUrl }),
@@ -19,6 +20,7 @@ class ManifestGenerator {
19
20
  },
20
21
  capabilities,
21
22
  actions,
23
+ ...(options.dataModel && Object.keys(options.dataModel).length > 0 && { dataModel: options.dataModel }),
22
24
  };
23
25
  }
24
26
  }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AuthDetector = void 0;
36
+ exports.AuthFlowInferrer = exports.AuthDetector = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const AUTH_SIGNALS = [
@@ -42,6 +42,15 @@ const AUTH_SIGNALS = [
42
42
  { pattern: '@farcaster/frame-node', type: 'farcaster-frame', priority: 100 },
43
43
  { pattern: 'validateFrameMessage', type: 'farcaster-frame', priority: 100 },
44
44
  { pattern: 'frameActionPayload', type: 'farcaster-frame', priority: 100 },
45
+ // Farcaster SIWF — must be before SIWE (higher priority)
46
+ { pattern: '@farcaster/auth-kit', type: 'farcaster-siwf', priority: 98 },
47
+ { pattern: 'verifySignInMessage', type: 'farcaster-siwf', priority: 98 },
48
+ { pattern: 'useSignIn', type: 'farcaster-siwf', priority: 95 },
49
+ // SIWE
50
+ { pattern: 'SiweMessage', type: 'siwe', priority: 95 },
51
+ { pattern: "from 'siwe'", type: 'siwe', priority: 95 },
52
+ { pattern: 'verifySiweMessage', type: 'siwe', priority: 95 },
53
+ { pattern: 'generateNonce', type: 'siwe', priority: 90 },
45
54
  // OAuth2 / NextAuth / Auth.js
46
55
  { pattern: 'NextAuth', type: 'oauth2', priority: 90 },
47
56
  { pattern: 'next-auth', type: 'oauth2', priority: 90 },
@@ -51,8 +60,14 @@ const AUTH_SIGNALS = [
51
60
  { pattern: 'oauth2', type: 'oauth2', priority: 80 },
52
61
  { pattern: 'access_token', type: 'oauth2', priority: 75 },
53
62
  { pattern: 'refresh_token', type: 'oauth2', priority: 75 },
54
- { pattern: 'clerkMiddleware', type: 'bearer', scheme: 'Bearer', priority: 88 },
55
- { pattern: 'currentUser', type: 'bearer', scheme: 'Bearer', priority: 70 },
63
+ // SAML
64
+ { pattern: 'samlify', type: 'saml', priority: 92 },
65
+ { pattern: 'passport-saml', type: 'saml', priority: 92 },
66
+ { pattern: '@node-saml', type: 'saml', priority: 92 },
67
+ { pattern: 'SAMLResponse', type: 'saml', priority: 88 },
68
+ // Clerk
69
+ { pattern: 'clerkMiddleware', type: 'clerk', priority: 88 },
70
+ { pattern: 'currentUser', type: 'clerk', priority: 70 },
56
71
  // JWT / Bearer
57
72
  { pattern: 'jwt.verify', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
58
73
  { pattern: 'jsonwebtoken', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
@@ -61,6 +76,25 @@ const AUTH_SIGNALS = [
61
76
  { pattern: 'authorization.split', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 },
62
77
  { pattern: "headers.get('authorization')", type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 65 },
63
78
  { pattern: "req.headers.authorization", type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 65 },
79
+ // Privy
80
+ { pattern: '@privy-io/react-auth', type: 'privy', priority: 85 },
81
+ { pattern: 'usePrivy', type: 'privy', priority: 85 },
82
+ { pattern: 'PrivyProvider', type: 'privy', priority: 85 },
83
+ // Dynamic
84
+ { pattern: '@dynamic-labs/sdk-react-core', type: 'dynamic', priority: 85 },
85
+ { pattern: 'DynamicContextProvider', type: 'dynamic', priority: 85 },
86
+ { pattern: 'useDynamicContext', type: 'dynamic', priority: 80 },
87
+ // Magic
88
+ { pattern: 'magic-sdk', type: 'magic', priority: 80 },
89
+ { pattern: '@magic-sdk', type: 'magic', priority: 80 },
90
+ { pattern: 'new Magic(', type: 'magic', priority: 80 },
91
+ // Passkey / WebAuthn
92
+ { pattern: '@simplewebauthn/browser', type: 'passkey', priority: 78 },
93
+ { pattern: 'startAuthentication', type: 'passkey', priority: 78 },
94
+ { pattern: 'startRegistration', type: 'passkey', priority: 78 },
95
+ { pattern: 'navigator.credentials.create', type: 'passkey', priority: 75 },
96
+ // Supabase
97
+ { pattern: 'supabase.auth', type: 'supabase', priority: 75 },
64
98
  // API key
65
99
  { pattern: "'x-api-key'", type: 'api-key', header: 'X-API-Key', priority: 60 },
66
100
  { pattern: '"x-api-key"', type: 'api-key', header: 'X-API-Key', priority: 60 },
@@ -80,6 +114,7 @@ const AUTH_SIGNALS = [
80
114
  ];
81
115
  class AuthDetector {
82
116
  best = null;
117
+ _flowInferrer = new AuthFlowInferrer();
83
118
  scanContent(content) {
84
119
  for (const signal of AUTH_SIGNALS) {
85
120
  if (content.includes(signal.pattern)) {
@@ -88,6 +123,7 @@ class AuthDetector {
88
123
  }
89
124
  }
90
125
  }
126
+ this._flowInferrer.addContent(content);
91
127
  }
92
128
  /** Read package.json to detect auth libraries in dependencies */
93
129
  readPackageJson(projectPath) {
@@ -99,8 +135,24 @@ class AuthDetector {
99
135
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
100
136
  if (deps['next-auth'] || deps['@auth/core'])
101
137
  this.applySignal({ pattern: 'next-auth', type: 'oauth2', priority: 90 });
138
+ if (deps['@farcaster/auth-kit'])
139
+ this.applySignal({ pattern: 'farcaster-siwf', type: 'farcaster-siwf', priority: 98 });
140
+ if (deps['siwe'])
141
+ this.applySignal({ pattern: 'siwe', type: 'siwe', priority: 95 });
142
+ if (deps['samlify'] || deps['passport-saml'])
143
+ this.applySignal({ pattern: 'saml', type: 'saml', priority: 92 });
102
144
  if (deps['@clerk/nextjs'] || deps['@clerk/clerk-sdk-node'])
103
- this.applySignal({ pattern: 'clerk', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 88 });
145
+ this.applySignal({ pattern: 'clerk', type: 'clerk', priority: 88 });
146
+ if (deps['@privy-io/react-auth'])
147
+ this.applySignal({ pattern: 'privy', type: 'privy', priority: 85 });
148
+ if (deps['@dynamic-labs/sdk-react-core'])
149
+ this.applySignal({ pattern: 'dynamic', type: 'dynamic', priority: 85 });
150
+ if (deps['magic-sdk'] || deps['@magic-sdk/admin'])
151
+ this.applySignal({ pattern: 'magic', type: 'magic', priority: 80 });
152
+ if (deps['@simplewebauthn/browser'])
153
+ this.applySignal({ pattern: 'passkey', type: 'passkey', priority: 78 });
154
+ if (deps['@supabase/supabase-js'])
155
+ this.applySignal({ pattern: 'supabase', type: 'supabase', priority: 75 });
104
156
  if (deps['jsonwebtoken'] || deps['jose'])
105
157
  this.applySignal({ pattern: 'jwt', type: 'bearer', header: 'Authorization', scheme: 'Bearer', priority: 70 });
106
158
  if (deps['iron-session'])
@@ -118,14 +170,44 @@ class AuthDetector {
118
170
  getAuth() {
119
171
  if (!this.best)
120
172
  return { type: 'none' };
121
- const config = { type: this.best.type };
173
+ const type = this.best.type;
174
+ const config = { type };
122
175
  if (this.best.header)
123
176
  config.header = this.best.header;
124
177
  if (this.best.scheme)
125
178
  config.scheme = this.best.scheme;
126
179
  if (this.best.queryParam)
127
180
  config.queryParam = this.best.queryParam;
128
- return config;
181
+ const flowFields = this._flowInferrer.infer(type);
182
+ return { ...config, ...flowFields };
129
183
  }
130
184
  }
131
185
  exports.AuthDetector = AuthDetector;
186
+ class AuthFlowInferrer {
187
+ scannedContents = [];
188
+ addContent(content) {
189
+ this.scannedContents.push(content);
190
+ }
191
+ infer(authType) {
192
+ const result = {};
193
+ for (const content of this.scannedContents) {
194
+ if (!result.nonceUrl && (authType === 'siwe' || authType === 'farcaster-siwf')) {
195
+ const m = content.match(/['"`](\/api\/auth\/nonce[^'"`\s]*)['"`]/);
196
+ if (m)
197
+ result.nonceUrl = m[1];
198
+ }
199
+ if (!result.callbackUrl && authType === 'oauth2') {
200
+ const m = content.match(/['"`](\/api\/auth\/callback[^'"`\s]*)['"`]/);
201
+ if (m)
202
+ result.callbackUrl = m[1];
203
+ }
204
+ if (!result.loginUrl) {
205
+ const m = content.match(/['"`](\/sign-in|\/login|\/auth\/login)['"`]/);
206
+ if (m)
207
+ result.loginUrl = m[1];
208
+ }
209
+ }
210
+ return result;
211
+ }
212
+ }
213
+ exports.AuthFlowInferrer = AuthFlowInferrer;
@@ -93,10 +93,10 @@ class ContractParser {
93
93
  abiFunction: item.name,
94
94
  isReadOnly,
95
95
  safety,
96
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
96
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, item.name),
97
97
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, isReadOnly, type: 'contract' }),
98
- inputs: this.mapAbiInputs(item.inputs ?? []),
99
- outputs: {
98
+ parameters: { properties: this.mapAbiInputs(item.inputs ?? []) },
99
+ returns: {
100
100
  type: this.mapAbiOutputs(item.outputs ?? []),
101
101
  description: '',
102
102
  },
@@ -172,10 +172,10 @@ class ContractParser {
172
172
  ...(chainId !== undefined ? { chainId } : {}),
173
173
  ...(contractAddress !== undefined ? { contractAddress } : {}),
174
174
  safety,
175
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
175
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, functionName),
176
176
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, isReadOnly: false, type: 'contract' }),
177
- inputs: parameters,
178
- outputs: { type: 'any' },
177
+ parameters: { properties: parameters },
178
+ returns: { type: 'any' },
179
179
  });
180
180
  });
181
181
  return actions;
@@ -113,10 +113,10 @@ class ExpressParser {
113
113
  location: routePath,
114
114
  method: httpMethod,
115
115
  safety,
116
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
116
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
117
117
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
118
- inputs: this.extractRouteParams(routePath),
119
- outputs: { type: 'any' },
118
+ parameters: { properties: this.extractRouteParams(routePath) },
119
+ returns: { type: 'any' },
120
120
  });
121
121
  });
122
122
  return actions;
@@ -25,16 +25,16 @@ const INTENT_RULES = [
25
25
  { pattern: /\b(listNFT|sellNFT|listFor(?:Sale)?)/i, intent: 'nft.list' },
26
26
  { pattern: /\b(buyNFT|purchaseNFT)/i, intent: 'nft.buy' },
27
27
  { pattern: /\b(burnNFT|burn)/i, intent: 'nft.burn' },
28
+ // Governance (must come BEFORE Social so castVote → governance.vote, not social.cast)
29
+ { pattern: /\b(vote|castVote|submitVote)/i, intent: 'governance.vote' },
30
+ { pattern: /\b(propose|createProposal|submitProposal)/i, intent: 'governance.propose' },
31
+ { pattern: /\b(delegate|undelegate)/i, intent: 'governance.delegate' },
28
32
  // Social (Farcaster-native)
29
- { pattern: /\b(cast|compose(?:Cast)?|post(?:Cast)?)/i, intent: 'social.cast' },
33
+ { pattern: /\b(cast(?!Vote)|compose(?:Cast)?|post(?:Cast)?)/i, intent: 'social.cast' },
30
34
  { pattern: /\b(follow|unfollow|subscribe)/i, intent: 'social.follow' },
31
35
  { pattern: /\b(like|react|upvote|downvote)/i, intent: 'social.react' },
32
36
  { pattern: /\b(comment|reply)/i, intent: 'social.reply' },
33
37
  { pattern: /\b(share|recast|repost)/i, intent: 'social.share' },
34
- // Governance
35
- { pattern: /\b(vote|castVote|submitVote)/i, intent: 'governance.vote' },
36
- { pattern: /\b(propose|createProposal|submitProposal)/i, intent: 'governance.propose' },
37
- { pattern: /\b(delegate|undelegate)/i, intent: 'governance.delegate' },
38
38
  // Auth
39
39
  { pattern: /\b(login|logout|signIn|signOut|connect|disconnect)/i, intent: 'auth.session' },
40
40
  { pattern: /\b(register|signup|createAccount)/i, intent: 'auth.register' },
@@ -73,7 +73,9 @@ const DESTRUCTIVE_VERBS = /\b(delete|remove|destroy|burn|archive|purge|wipe|clea
73
73
  * updateSsn, exportPrivateKey, getMedicalRecord.
74
74
  */
75
75
  // No \b — these nouns appear anywhere in camelCase (resetPassword, submitKyc, getSsn)
76
- const CONFIDENTIAL_NOUNS = /(password|credential|privateKey|secretKey|biometric|ssn|taxId|pii|kyc|medicalRecord|healthRecord|passport|driverLicense|creditCard|cvv|encryptedData|identityVerif)/i;
76
+ const CONFIDENTIAL_NOUNS = /(password|credential|privateKey|secretKey|biometric|ssn|taxId|tax_id|pii|kyc|medical|health|passport|license|creditCard|cvv|encrypted|identity|dob|birthDate|address|phone|mobile|email)/i;
77
+ /** Verbs that imply high-privilege administrative or sensitive state changes. */
78
+ const SENSITIVE_WRITE_VERBS = /\b(admin|role|permission|password|owner|ban|block|lock|unlock|grant|revoke|delegate|sudo|root|secret)/i;
77
79
  /**
78
80
  * Classify the safety level of an action.
79
81
  *
@@ -97,19 +99,40 @@ function classifySafety(opts) {
97
99
  return 'confidential';
98
100
  return 'write';
99
101
  }
102
+ // Financial and confidential always win regardless of type
100
103
  if (FINANCIAL_VERBS.test(name))
101
104
  return 'financial';
102
105
  if (CONFIDENTIAL_NOUNS.test(name))
103
106
  return 'confidential';
107
+ if (type === 'function' || type === 'ui') {
108
+ if (DESTRUCTIVE_VERBS.test(name))
109
+ return 'destructive';
110
+ // Auth functions are confidential (handle credentials)
111
+ if (/\b(login|logout|signIn|signOut|register|signup|createAccount)/i.test(name))
112
+ return 'confidential';
113
+ // Social/game → write
114
+ return 'write';
115
+ }
104
116
  if (httpMethod === 'GET')
105
117
  return 'read';
118
+ if (isReadOnly)
119
+ return 'read';
106
120
  if (DESTRUCTIVE_VERBS.test(name))
107
121
  return 'destructive';
108
122
  return 'write';
109
123
  }
110
- /** Derive agentSafe from safety level. Non-read/write levels require human confirmation. */
111
- function deriveAgentSafe(safety) {
112
- return safety === 'read' || safety === 'write';
124
+ /**
125
+ * Derive agentSafe from safety level.
126
+ * Only 'read' and non-sensitive 'write' actions are safe for autonomous execution.
127
+ */
128
+ function deriveAgentSafe(safety, name) {
129
+ if (safety === 'read')
130
+ return true;
131
+ if (safety === 'write') {
132
+ // Narrowing: check if the write action name contains high-privilege keywords
133
+ return !SENSITIVE_WRITE_VERBS.test(name);
134
+ }
135
+ return false;
113
136
  }
114
137
  /**
115
138
  * Infer per-action auth requirement.
@@ -149,8 +172,10 @@ function inferActionAuth(opts) {
149
172
  if (safety === 'destructive') {
150
173
  return { required: 'required' };
151
174
  }
152
- // Read-only GET on a public (no-auth) app → public
153
- if (httpMethod === 'GET' && safety === 'read' && (appAuthType === 'none' || !appAuthType)) {
175
+ // No-auth app read and write actions are public (non-contract only).
176
+ // Financial/confidential/destructive retain their required auth even on no-auth apps
177
+ // (those actions handle money or PII and should always require confirmation).
178
+ if (type !== 'contract' && (appAuthType === 'none' || !appAuthType) && (safety === 'read' || safety === 'write')) {
154
179
  return { required: 'public' };
155
180
  }
156
181
  return { required: 'required' };
@@ -63,9 +63,9 @@ class OpenAPIParser {
63
63
  const httpMethod = method.toUpperCase();
64
64
  const safety = (0, intent_classifier_1.classifySafety)({ name: operationId, httpMethod, type: 'api' });
65
65
  // Extract parameters (path + query + header)
66
- const inputs = {};
66
+ const props = {};
67
67
  for (const param of operation.parameters ?? []) {
68
- inputs[param.name] = {
68
+ props[param.name] = {
69
69
  type: param.schema?.type ?? 'string',
70
70
  description: param.description,
71
71
  required: param.required ?? false,
@@ -76,7 +76,7 @@ class OpenAPIParser {
76
76
  if (bodySchema?.properties) {
77
77
  const required = bodySchema.required ?? [];
78
78
  for (const [field, schema] of Object.entries(bodySchema.properties)) {
79
- inputs[field] = {
79
+ props[field] = {
80
80
  type: schema.type ?? 'any',
81
81
  description: schema.description,
82
82
  required: required.includes(field),
@@ -94,12 +94,12 @@ class OpenAPIParser {
94
94
  location: routePath,
95
95
  method: httpMethod,
96
96
  safety,
97
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
97
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, operationId),
98
98
  requiredAuth: hasSecurity
99
99
  ? { required: 'required' }
100
100
  : (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
101
- inputs,
102
- outputs: this.extractOutputSchema(operation),
101
+ parameters: { properties: props },
102
+ returns: this.extractOutputSchema(operation),
103
103
  });
104
104
  }
105
105
  }
@@ -35,159 +35,55 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.PRISMA_PATTERNS = exports.PrismaParser = void 0;
37
37
  const fs = __importStar(require("fs"));
38
- const intent_classifier_1 = require("./intent-classifier");
39
38
  /**
40
- * Reads a Prisma schema file and infers CRUD agent actions for every model.
39
+ * Reads a Prisma schema file and extracts the data model as a structured
40
+ * `dataModel` map. Each Prisma model becomes an entry with its writeable
41
+ * fields (auto-managed fields like @id, @default(now()), @updatedAt are excluded).
41
42
  *
42
- * For a model named `User` it emits:
43
- * listUsers → data.read (GET-like)
44
- * getUser → data.read (GET-like, by id)
45
- * createUser → data.create (POST-like)
46
- * updateUser → data.update (PATCH-like)
47
- * deleteUser → data.delete (DELETE-like)
48
- *
49
- * Fields declared on the model are mapped to action inputs where relevant.
43
+ * Returns `{ actions: [], dataModel }` no CRUD actions are generated.
44
+ * Prisma models are data schema, not API endpoints.
50
45
  */
51
46
  class PrismaParser {
52
47
  parseFile(filePath) {
53
48
  const content = fs.readFileSync(filePath, 'utf8');
54
- const models = this.extractModels(content);
55
- const actions = [];
56
- for (const model of models) {
57
- const name = model.name;
58
- const lower = name.charAt(0).toLowerCase() + name.slice(1);
59
- const location = filePath;
60
- // Writeable fields (non-id, non-auto) used as create/update inputs
61
- const writeableFields = model.fields.filter(f => !f.isId && !f.isAuto && !f.isList);
62
- const idField = model.fields.find(f => f.isId) ?? { name: 'id', type: 'String' };
63
- const idInput = {
64
- [idField.name]: { type: this.prismaTypeToJson(idField.type), required: true },
65
- };
66
- const writeInputs = {};
67
- for (const f of writeableFields) {
68
- writeInputs[f.name] = {
69
- type: this.prismaTypeToJson(f.type),
70
- required: !f.isOptional,
49
+ const dataModel = {};
50
+ const modelRegex = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
51
+ let match;
52
+ while ((match = modelRegex.exec(content)) !== null) {
53
+ const modelName = match[1];
54
+ const body = match[2];
55
+ const fields = {};
56
+ for (const line of body.split('\n')) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
59
+ continue;
60
+ const fieldMatch = /^(\w+)\s+(\w+)(\[\])?([\?])?(.*)?$/.exec(trimmed);
61
+ if (!fieldMatch)
62
+ continue;
63
+ const [, fieldName, fieldType, isList, isOptional, attrs] = fieldMatch;
64
+ // Skip auto-managed fields (any @default(...) variant, not just @default(now()))
65
+ if (attrs?.includes('@id') || attrs?.includes('@default(') || attrs?.includes('@updatedAt'))
66
+ continue;
67
+ fields[fieldName] = {
68
+ type: this.mapFieldType(fieldType, !!isList),
69
+ required: !isOptional,
71
70
  };
72
71
  }
73
- // list
74
- actions.push({
75
- name: `list${name}s`,
76
- description: `List all ${name} records`,
77
- intent: 'data.read',
78
- type: 'function',
79
- location,
80
- safety: 'read',
81
- agentSafe: true,
82
- requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'read', httpMethod: 'GET', type: 'function' }),
83
- inputs: {},
84
- outputs: { type: 'array', description: `Array of ${name}` },
85
- });
86
- // get
87
- actions.push({
88
- name: `get${name}`,
89
- description: `Get a single ${name} by ${idField.name}`,
90
- intent: 'data.read',
91
- type: 'function',
92
- location,
93
- safety: 'read',
94
- agentSafe: true,
95
- requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'read', httpMethod: 'GET', type: 'function' }),
96
- inputs: idInput,
97
- outputs: { type: 'object', description: name },
98
- });
99
- // create
100
- actions.push({
101
- name: `create${name}`,
102
- description: `Create a new ${name} record`,
103
- intent: 'data.create',
104
- type: 'function',
105
- location,
106
- safety: 'write',
107
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)('write'),
108
- requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'write', type: 'function' }),
109
- inputs: writeInputs,
110
- outputs: { type: 'object', description: `Created ${name}` },
111
- });
112
- // update
113
- actions.push({
114
- name: `update${name}`,
115
- description: `Update an existing ${name} record`,
116
- intent: 'data.update',
117
- type: 'function',
118
- location,
119
- safety: 'write',
120
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)('write'),
121
- requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'write', type: 'function' }),
122
- inputs: { ...idInput, ...writeInputs },
123
- outputs: { type: 'object', description: `Updated ${name}` },
124
- });
125
- // delete
126
- actions.push({
127
- name: `delete${name}`,
128
- description: `Delete a ${name} record`,
129
- intent: 'data.delete',
130
- type: 'function',
131
- location,
132
- safety: 'destructive',
133
- agentSafe: false,
134
- requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'destructive', type: 'function' }),
135
- inputs: idInput,
136
- outputs: { type: 'object', description: `Deleted ${name}` },
137
- });
138
- void lower; // suppress unused-variable warning
139
- }
140
- return actions;
141
- }
142
- // ─── Schema parsing helpers ───────────────────────────────────────────────
143
- extractModels(content) {
144
- const models = [];
145
- // Match: model ModelName { ... }
146
- const modelRe = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
147
- let modelMatch;
148
- while ((modelMatch = modelRe.exec(content)) !== null) {
149
- const modelName = modelMatch[1];
150
- const body = modelMatch[2];
151
- const fields = this.parseFields(body);
152
- models.push({ name: modelName, fields });
153
- }
154
- return models;
155
- }
156
- parseFields(body) {
157
- const fields = [];
158
- const lines = body.split('\n');
159
- for (const raw of lines) {
160
- const line = raw.trim();
161
- if (!line || line.startsWith('//') || line.startsWith('@@'))
162
- continue;
163
- // field Type @attr @attr
164
- const match = line.match(/^(\w+)\s+(\w+)(\[\])?(\?)?(.*)$/);
165
- if (!match)
166
- continue;
167
- const [, name, type, isList, isOptional, attrs] = match;
168
- fields.push({
169
- name,
170
- type,
171
- isList: !!isList,
172
- isOptional: !!isOptional,
173
- isId: attrs.includes('@id'),
174
- isAuto: attrs.includes('@default(autoincrement())') || attrs.includes('@default(uuid())') || attrs.includes('@default(cuid())') || attrs.includes('@updatedAt'),
175
- });
72
+ dataModel[modelName] = {
73
+ description: `Prisma model: ${modelName}`,
74
+ fields,
75
+ };
176
76
  }
177
- return fields;
77
+ return { actions: [], dataModel };
178
78
  }
179
- prismaTypeToJson(prismaType) {
180
- switch (prismaType) {
181
- case 'String': return 'string';
182
- case 'Int':
183
- case 'Float':
184
- case 'Decimal': return 'number';
185
- case 'Boolean': return 'boolean';
186
- case 'DateTime': return 'string';
187
- case 'Json': return 'object';
188
- case 'Bytes': return 'string';
189
- default: return 'object'; // relations / enums
190
- }
79
+ mapFieldType(prismaType, isList) {
80
+ if (isList)
81
+ return 'array';
82
+ const map = {
83
+ String: 'string', Int: 'number', Float: 'number', Decimal: 'number',
84
+ Boolean: 'boolean', DateTime: 'string', Json: 'object', Bytes: 'string',
85
+ };
86
+ return map[prismaType] ?? 'object';
191
87
  }
192
88
  }
193
89
  exports.PrismaParser = PrismaParser;
@@ -78,10 +78,10 @@ class RemixParser {
78
78
  location: routePath,
79
79
  method: httpMethod,
80
80
  safety,
81
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
81
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
82
82
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
83
- inputs: this.extractRouteParams(routePath),
84
- outputs: { type: 'object' },
83
+ parameters: { properties: this.extractRouteParams(routePath) },
84
+ returns: { type: 'object' },
85
85
  });
86
86
  }
87
87
  return actions;
@@ -108,10 +108,10 @@ class SocketIOParser {
108
108
  location: relativePath,
109
109
  socketEvent: eventName,
110
110
  safety,
111
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
111
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, camelName),
112
112
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, type: 'socket' }),
113
- inputs,
114
- outputs: { type: 'object' },
113
+ parameters: { properties: inputs },
114
+ returns: { type: 'object' },
115
115
  });
116
116
  });
117
117
  return actions;
@@ -92,10 +92,10 @@ class SSEParser {
92
92
  location: relativePath,
93
93
  method: 'GET',
94
94
  safety,
95
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
95
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, name),
96
96
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod: 'GET', type: 'api' }),
97
- inputs: {},
98
- outputs: { type: 'stream', description: 'Server-Sent Events stream' },
97
+ parameters: { properties: {} },
98
+ returns: { type: 'stream', description: 'Server-Sent Events stream' },
99
99
  });
100
100
  }
101
101
  return actions;
@@ -101,15 +101,15 @@ class TRPCParser {
101
101
  location: relativePath,
102
102
  method: methodName,
103
103
  safety,
104
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
104
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, procedureName),
105
105
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({
106
106
  safety,
107
107
  httpMethod: httpLike,
108
108
  appAuthType: isProtected ? 'bearer' : undefined,
109
109
  type: 'function',
110
110
  }),
111
- inputs,
112
- outputs: { type: 'object' },
111
+ parameters: { properties: inputs },
112
+ returns: { type: 'object' },
113
113
  });
114
114
  });
115
115
  return actions;
@@ -151,10 +151,10 @@ class TSParser {
151
151
  location,
152
152
  method: methodName,
153
153
  safety,
154
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
154
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
155
155
  requiredAuth: this.actionAuth(safety, methodName),
156
- inputs: zodInputs,
157
- outputs: { type: 'any' },
156
+ parameters: { properties: zodInputs },
157
+ returns: { type: 'any' },
158
158
  });
159
159
  }
160
160
  // export const POST = async (request: Request) => { ... }
@@ -177,10 +177,10 @@ class TSParser {
177
177
  location,
178
178
  method: methodName,
179
179
  safety,
180
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
180
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
181
181
  requiredAuth: this.actionAuth(safety, methodName),
182
- inputs: zodInputs,
183
- outputs: { type: 'any' },
182
+ parameters: { properties: zodInputs },
183
+ returns: { type: 'any' },
184
184
  });
185
185
  }
186
186
  }
@@ -201,10 +201,10 @@ class TSParser {
201
201
  location,
202
202
  method,
203
203
  safety,
204
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
204
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
205
205
  requiredAuth: this.actionAuth(safety, method),
206
- inputs: zodShape ?? {},
207
- outputs: { type: 'any' },
206
+ parameters: { properties: zodShape ?? {} },
207
+ returns: { type: 'any' },
208
208
  });
209
209
  }
210
210
  // 3d. Generic non-Next.js API routes: api/**
@@ -224,10 +224,10 @@ class TSParser {
224
224
  location,
225
225
  method,
226
226
  safety,
227
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
227
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, actionName),
228
228
  requiredAuth: this.actionAuth(safety, method),
229
- inputs: zodShape ?? {},
230
- outputs: { type: 'any' },
229
+ parameters: { properties: zodShape ?? {} },
230
+ returns: { type: 'any' },
231
231
  });
232
232
  }
233
233
  // 3e. Server Actions: files with 'use server' directive
@@ -261,10 +261,10 @@ class TSParser {
261
261
  type: 'function',
262
262
  location: `./${relativePath}`,
263
263
  safety,
264
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
264
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, name),
265
265
  requiredAuth: this.actionAuth(safety, undefined, undefined, 'function'),
266
- inputs: this.extractFunctionParams(init),
267
- outputs: { type: 'any' },
266
+ parameters: { properties: this.extractFunctionParams(init) },
267
+ returns: { type: 'any' },
268
268
  });
269
269
  }
270
270
  }
@@ -357,14 +357,14 @@ class TSParser {
357
357
  ?.getComment()
358
358
  ?.toString()
359
359
  .match(/safety=(read|write|financial|destructive)/)?.[1];
360
- const inputs = {};
360
+ const properties = {};
361
361
  if ('getParameters' in declaration) {
362
362
  for (const param of declaration.getParameters()) {
363
363
  const paramName = param.getName();
364
364
  const paramDoc = jsDoc
365
365
  ?.getTags()
366
366
  .find(t => t.getTagName() === 'param' && t.getName() === paramName);
367
- inputs[paramName] = {
367
+ properties[paramName] = {
368
368
  type: this.mapType(param.getType()),
369
369
  description: paramDoc?.getComment()?.toString().trim() || '',
370
370
  required: !param.isOptional(),
@@ -380,10 +380,10 @@ class TSParser {
380
380
  type: 'function',
381
381
  location: `./${relativePath}`,
382
382
  safety,
383
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
383
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, name),
384
384
  requiredAuth: this.actionAuth(safety, undefined, undefined, 'function'),
385
- inputs,
386
- outputs: {
385
+ parameters: { properties },
386
+ returns: {
387
387
  type: returnType ? this.mapType(returnType) : 'any',
388
388
  description: jsDoc?.getTags().find(t => t.getTagName() === 'returns')?.getComment()?.toString().trim() || '',
389
389
  },
@@ -104,10 +104,10 @@ class WebSocketParser {
104
104
  location: relativePath,
105
105
  socketEvent: msgType,
106
106
  safety,
107
- agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
107
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety, msgType),
108
108
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, type: 'socket' }),
109
- inputs: { type: { type: 'string', required: true }, payload: { type: 'object', required: false } },
110
- outputs: { type: 'object' },
109
+ parameters: { properties: { type: { type: 'string', required: true }, payload: { type: 'object', required: false } } },
110
+ returns: { type: 'object' },
111
111
  });
112
112
  }
113
113
  }
@@ -124,10 +124,10 @@ class WebSocketParser {
124
124
  location: relativePath,
125
125
  socketEvent: 'message',
126
126
  safety: 'write',
127
- agentSafe: true,
127
+ agentSafe: (0, intent_classifier_1.deriveAgentSafe)('write', name),
128
128
  requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety: 'write', type: 'socket' }),
129
- inputs: { data: { type: 'string', required: true } },
130
- outputs: { type: 'object' },
129
+ parameters: { properties: { data: { type: 'string', required: true } } },
130
+ returns: { type: 'object' },
131
131
  });
132
132
  }
133
133
  }
@@ -319,8 +319,15 @@ class ZodExtractor {
319
319
  const envMatch = text.match(/process\.env\.([A-Z0-9_]+)/);
320
320
  if (envMatch)
321
321
  return { $env: envMatch[1] };
322
- if (ts_morph_1.Node.isStringLiteral(node))
323
- return node.getLiteralValue();
322
+ if (ts_morph_1.Node.isStringLiteral(node)) {
323
+ const val = node.getLiteralValue();
324
+ // SECURITY: Redact high-entropy strings or common secret prefixes
325
+ if (/^(sk_|pk_|api_key|token|secret|key-|passwd|auth_|ghp_)/i.test(val) ||
326
+ (val.length > 20 && /[a-z]/i.test(val) && /[0-9]/.test(val) && /[A-Z]/.test(val))) {
327
+ return '[REDACTED_SECRET]';
328
+ }
329
+ return val;
330
+ }
324
331
  if (ts_morph_1.Node.isNumericLiteral(node))
325
332
  return Number(node.getLiteralValue());
326
333
  if (text === 'true')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-manifest",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "description": "Universal agent manifest compiler — generates agent.json for any web app so AI agents can discover and interact with it",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "properties": {
38
38
  "type": {
39
39
  "type": "string",
40
- "enum": ["none", "bearer", "api-key", "oauth2", "basic", "farcaster-frame", "cookie"],
40
+ "enum": ["none", "bearer", "api-key", "oauth2", "basic", "cookie", "siwe", "farcaster-siwf", "farcaster-frame", "clerk", "privy", "dynamic", "magic", "passkey", "saml", "supabase"],
41
41
  "description": "Authentication scheme"
42
42
  },
43
43
  "header": { "type": "string", "description": "Header name, e.g. Authorization or X-API-Key" },
@@ -65,6 +65,14 @@
65
65
  "actions": {
66
66
  "type": "array",
67
67
  "items": { "$ref": "#/definitions/AgentAction" }
68
+ },
69
+ "baseUrl": {
70
+ "type": "string",
71
+ "format": "uri"
72
+ },
73
+ "dataModel": {
74
+ "type": "object",
75
+ "additionalProperties": { "$ref": "#/definitions/DataModelEntry" }
68
76
  }
69
77
  },
70
78
  "definitions": {
@@ -88,7 +96,7 @@
88
96
  },
89
97
  "AgentAction": {
90
98
  "type": "object",
91
- "required": ["name", "description", "intent", "type", "location", "safety", "agentSafe", "inputs", "outputs"],
99
+ "required": ["name", "description", "intent", "type", "location", "safety", "agentSafe", "parameters", "returns"],
92
100
  "additionalProperties": false,
93
101
  "properties": {
94
102
  "name": { "type": "string", "description": "Unique action identifier" },
@@ -100,7 +108,7 @@
100
108
  },
101
109
  "type": {
102
110
  "type": "string",
103
- "enum": ["api", "contract", "function", "socket"]
111
+ "enum": ["api", "contract", "function", "socket", "ui"]
104
112
  },
105
113
  "location": { "type": "string" },
106
114
  "method": {
@@ -149,11 +157,14 @@
149
157
  }
150
158
  }
151
159
  },
152
- "inputs": {
160
+ "parameters": {
153
161
  "type": "object",
154
- "additionalProperties": { "$ref": "#/definitions/ParameterProperty" }
162
+ "properties": {
163
+ "properties": { "type": "object" }
164
+ },
165
+ "required": ["properties"]
155
166
  },
156
- "outputs": {
167
+ "returns": {
157
168
  "type": "object",
158
169
  "required": ["type"],
159
170
  "additionalProperties": false,
@@ -163,6 +174,25 @@
163
174
  }
164
175
  }
165
176
  }
177
+ },
178
+ "DataModelEntry": {
179
+ "type": "object",
180
+ "properties": {
181
+ "description": { "type": "string" },
182
+ "fields": {
183
+ "type": "object",
184
+ "additionalProperties": {
185
+ "type": "object",
186
+ "properties": {
187
+ "type": { "type": "string" },
188
+ "required": { "type": "boolean" },
189
+ "description": { "type": "string" }
190
+ },
191
+ "required": ["type"]
192
+ }
193
+ }
194
+ },
195
+ "required": ["fields"]
166
196
  }
167
197
  }
168
198
  }