agent-manifest 3.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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/cli.js +174 -0
- package/dist/discovery/service.js +159 -0
- package/dist/generator/json.js +25 -0
- package/dist/index.js +36 -0
- package/dist/parser/auth-detector.js +131 -0
- package/dist/parser/capability-detector.js +115 -0
- package/dist/parser/contract-parser.js +347 -0
- package/dist/parser/express-parser.js +163 -0
- package/dist/parser/intent-classifier.js +157 -0
- package/dist/parser/ts-parser.js +419 -0
- package/dist/parser/zod-extractor.js +335 -0
- package/dist/types.js +2 -0
- package/package.json +61 -0
- package/schema/agent.schema.json +168 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ExpressParser = void 0;
|
|
37
|
+
exports.looksLikeRouteFile = looksLikeRouteFile;
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
const intent_classifier_1 = require("./intent-classifier");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Parses Express, Hono, and Fastify route definitions.
|
|
43
|
+
*
|
|
44
|
+
* Detects patterns like:
|
|
45
|
+
* // Express / Express Router
|
|
46
|
+
* app.get('/api/users', handler)
|
|
47
|
+
* router.post('/api/payments', handler)
|
|
48
|
+
*
|
|
49
|
+
* // Hono
|
|
50
|
+
* app.get('/api/users', (c) => { ... })
|
|
51
|
+
* const app = new Hono()
|
|
52
|
+
*
|
|
53
|
+
* // Fastify
|
|
54
|
+
* fastify.get('/api/users', handler)
|
|
55
|
+
* app.register(fastifyPlugin)
|
|
56
|
+
*/
|
|
57
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all'];
|
|
58
|
+
const HTTP_METHODS_UPPER = {
|
|
59
|
+
get: 'GET', post: 'POST', put: 'PUT', delete: 'DELETE',
|
|
60
|
+
patch: 'PATCH', head: 'HEAD', options: 'OPTIONS', all: 'POST',
|
|
61
|
+
};
|
|
62
|
+
class ExpressParser {
|
|
63
|
+
project;
|
|
64
|
+
/** Pass the shared Project from TSParser to avoid duplicate AST construction. */
|
|
65
|
+
constructor(sharedProject) {
|
|
66
|
+
this.project = sharedProject ?? new ts_morph_1.Project({
|
|
67
|
+
compilerOptions: { allowJs: true, checkJs: false },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async parseFile(filePath, projectPath) {
|
|
71
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
72
|
+
const relativePath = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
73
|
+
const actions = [];
|
|
74
|
+
// Walk all call expressions looking for app.METHOD / router.METHOD / fastify.METHOD
|
|
75
|
+
sourceFile.forEachDescendant(node => {
|
|
76
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
77
|
+
return;
|
|
78
|
+
const expr = node.getExpression();
|
|
79
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
80
|
+
return;
|
|
81
|
+
const methodName = expr.getName().toLowerCase();
|
|
82
|
+
if (!HTTP_METHODS.includes(methodName))
|
|
83
|
+
return;
|
|
84
|
+
const args = node.getArguments();
|
|
85
|
+
if (args.length < 2)
|
|
86
|
+
return;
|
|
87
|
+
// First arg must be a string literal route path
|
|
88
|
+
const routeArg = args[0];
|
|
89
|
+
if (!ts_morph_1.Node.isStringLiteral(routeArg))
|
|
90
|
+
return;
|
|
91
|
+
const routePath = routeArg.getLiteralValue();
|
|
92
|
+
if (!routePath.startsWith('/'))
|
|
93
|
+
return;
|
|
94
|
+
const httpMethod = HTTP_METHODS_UPPER[methodName];
|
|
95
|
+
const actionName = this.routeToActionName(routePath, httpMethod);
|
|
96
|
+
if (actions.some(a => a.name === actionName))
|
|
97
|
+
return;
|
|
98
|
+
// Extract JSDoc from the handler if it's an inline function
|
|
99
|
+
let description = `${httpMethod} ${routePath}`;
|
|
100
|
+
const handler = args[args.length - 1];
|
|
101
|
+
if (ts_morph_1.Node.isArrowFunction(handler) || ts_morph_1.Node.isFunctionExpression(handler)) {
|
|
102
|
+
const jsDocs = handler.getJsDocs?.() ?? [];
|
|
103
|
+
if (jsDocs.length > 0) {
|
|
104
|
+
description = jsDocs[0].getDescription().trim() || description;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const safety = (0, intent_classifier_1.classifySafety)({ name: actionName, httpMethod, type: 'api' });
|
|
108
|
+
actions.push({
|
|
109
|
+
name: actionName,
|
|
110
|
+
description,
|
|
111
|
+
intent: (0, intent_classifier_1.inferIntent)(actionName),
|
|
112
|
+
type: 'api',
|
|
113
|
+
location: routePath,
|
|
114
|
+
method: httpMethod,
|
|
115
|
+
safety,
|
|
116
|
+
agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
|
|
117
|
+
requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
|
|
118
|
+
inputs: this.extractRouteParams(routePath),
|
|
119
|
+
outputs: { type: 'any' },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
return actions;
|
|
123
|
+
}
|
|
124
|
+
/** Convert a route path like /api/users/:id to a snake_case action name */
|
|
125
|
+
routeToActionName(routePath, method) {
|
|
126
|
+
const slug = routePath
|
|
127
|
+
.replace(/^\//, '')
|
|
128
|
+
.replace(/\/:([^/]+)/g, '_$1') // :param → _param
|
|
129
|
+
.replace(/\//g, '_')
|
|
130
|
+
.replace(/[^a-zA-Z0-9_]/g, '');
|
|
131
|
+
return slug ? `${slug}_${method}` : `root_${method}`;
|
|
132
|
+
}
|
|
133
|
+
/** Extract path params like :id, :userId as required string inputs */
|
|
134
|
+
extractRouteParams(routePath) {
|
|
135
|
+
const params = {};
|
|
136
|
+
const matches = routePath.matchAll(/:([a-zA-Z][a-zA-Z0-9_]*)/g);
|
|
137
|
+
for (const match of matches) {
|
|
138
|
+
params[match[1]] = {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: `Path parameter: ${match[1]}`,
|
|
141
|
+
required: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return params;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.ExpressParser = ExpressParser;
|
|
148
|
+
/**
|
|
149
|
+
* Quick check: does this file look like it registers Express/Hono/Fastify routes?
|
|
150
|
+
* Used by DiscoveryService to filter files before expensive AST parsing.
|
|
151
|
+
*/
|
|
152
|
+
function looksLikeRouteFile(content) {
|
|
153
|
+
return (
|
|
154
|
+
// Express / Hono app method calls
|
|
155
|
+
/\.(get|post|put|delete|patch)\s*\(\s*['"`]\//.test(content) ||
|
|
156
|
+
// Fastify route registration
|
|
157
|
+
/fastify\.(get|post|put|delete|patch)\s*\(/.test(content) ||
|
|
158
|
+
// Hono new Hono()
|
|
159
|
+
/new\s+Hono\s*\(/.test(content) ||
|
|
160
|
+
// Express Router
|
|
161
|
+
/Router\s*\(\s*\)/.test(content) ||
|
|
162
|
+
/express\.Router/.test(content));
|
|
163
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferIntent = inferIntent;
|
|
4
|
+
exports.classifySafety = classifySafety;
|
|
5
|
+
exports.deriveAgentSafe = deriveAgentSafe;
|
|
6
|
+
exports.inferActionAuth = inferActionAuth;
|
|
7
|
+
const INTENT_RULES = [
|
|
8
|
+
// Trailing \b is intentionally omitted — camelCase names like rollDice, sendTokens,
|
|
9
|
+
// getUsers don't have a word boundary after the verb segment. Leading \b is kept
|
|
10
|
+
// to avoid matching mid-word (e.g. "undo" matching "do").
|
|
11
|
+
// Game
|
|
12
|
+
{ pattern: /\b(play|flip|roll|spin|bet|guess|draw|move|attack|defend|claim(?:Prize|Reward|Win))/i, intent: 'game.play' },
|
|
13
|
+
{ pattern: /\b(score|leaderboard|rank|highscore)/i, intent: 'game.score' },
|
|
14
|
+
{ pattern: /\b(join(?:Game|Room|Lobby)|create(?:Game|Room)|start(?:Game|Round))/i, intent: 'game.join' },
|
|
15
|
+
// Finance / DeFi
|
|
16
|
+
{ pattern: /\b(transfer|send(?:Token|ETH|USDC)?|pay(?:ment)?)/i, intent: 'finance.transfer' },
|
|
17
|
+
{ pattern: /\b(swap|exchange|trade)/i, intent: 'finance.swap' },
|
|
18
|
+
{ pattern: /\b(stake|unstake|deposit|withdraw|bond|unbond)/i, intent: 'finance.stake' },
|
|
19
|
+
{ pattern: /\b(mint(?!NFT)|buy|purchase)/i, intent: 'finance.purchase' },
|
|
20
|
+
{ pattern: /\b(approve|allowance|permit)/i, intent: 'finance.approve' },
|
|
21
|
+
{ pattern: /\b(balance|getBalance|totalSupply)/i, intent: 'finance.balance' },
|
|
22
|
+
{ pattern: /\b(borrow|repay|liquidate|collateral)/i, intent: 'finance.lending' },
|
|
23
|
+
// NFT
|
|
24
|
+
{ pattern: /\b(mint(?:NFT)?|safeMint|createNFT)/i, intent: 'nft.mint' },
|
|
25
|
+
{ pattern: /\b(listNFT|sellNFT|listFor(?:Sale)?)/i, intent: 'nft.list' },
|
|
26
|
+
{ pattern: /\b(buyNFT|purchaseNFT)/i, intent: 'nft.buy' },
|
|
27
|
+
{ pattern: /\b(burnNFT|burn)/i, intent: 'nft.burn' },
|
|
28
|
+
// Social (Farcaster-native)
|
|
29
|
+
{ pattern: /\b(cast|compose(?:Cast)?|post(?:Cast)?)/i, intent: 'social.cast' },
|
|
30
|
+
{ pattern: /\b(follow|unfollow|subscribe)/i, intent: 'social.follow' },
|
|
31
|
+
{ pattern: /\b(like|react|upvote|downvote)/i, intent: 'social.react' },
|
|
32
|
+
{ pattern: /\b(comment|reply)/i, intent: 'social.reply' },
|
|
33
|
+
{ 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
|
+
// Auth
|
|
39
|
+
{ pattern: /\b(login|logout|signIn|signOut|connect|disconnect)/i, intent: 'auth.session' },
|
|
40
|
+
{ pattern: /\b(register|signup|createAccount)/i, intent: 'auth.register' },
|
|
41
|
+
{ pattern: /\b(verify(?:Signature|Address|Identity)?)/i, intent: 'auth.verify' },
|
|
42
|
+
// Data / CRUD
|
|
43
|
+
{ pattern: /\b(get|fetch|load|read|list|query|search|find)/i, intent: 'data.read' },
|
|
44
|
+
{ pattern: /\b(create|add|insert|save|store|upload)/i, intent: 'data.create' },
|
|
45
|
+
{ pattern: /\b(update|edit|patch|set|change)/i, intent: 'data.update' },
|
|
46
|
+
{ pattern: /\b(delete|remove|destroy|archive)/i, intent: 'data.delete' },
|
|
47
|
+
// Media
|
|
48
|
+
{ pattern: /\b(upload(?:Image|File|Media)?|setAvatar|setImage)/i, intent: 'media.upload' },
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Infer a semantic intent from the action name.
|
|
52
|
+
* Returns the first matching intent, or "util.action" as fallback.
|
|
53
|
+
*/
|
|
54
|
+
function inferIntent(name, overrideIntent) {
|
|
55
|
+
if (overrideIntent)
|
|
56
|
+
return overrideIntent;
|
|
57
|
+
for (const { pattern, intent } of INTENT_RULES) {
|
|
58
|
+
if (pattern.test(name))
|
|
59
|
+
return intent;
|
|
60
|
+
}
|
|
61
|
+
return 'util.action';
|
|
62
|
+
}
|
|
63
|
+
// ─── Safety classification ────────────────────────────────────────────────
|
|
64
|
+
// No trailing \b — camelCase verbs like sendPayment, deleteAccount need prefix matching only
|
|
65
|
+
const FINANCIAL_VERBS = /\b(transfer|send|pay|swap|exchange|trade|stake|unstake|deposit|withdraw|buy|purchase|mint|approve|borrow|repay|liquidate|bond|unbond)/i;
|
|
66
|
+
const DESTRUCTIVE_VERBS = /\b(delete|remove|destroy|burn|archive|purge|wipe|clear)/i;
|
|
67
|
+
/**
|
|
68
|
+
* Matches names that handle PII, credentials, or sensitive identity data.
|
|
69
|
+
* An agent touching these fields should always require human confirmation and
|
|
70
|
+
* encrypted transport — even if the HTTP verb is a GET.
|
|
71
|
+
*
|
|
72
|
+
* Examples: resetPassword, storeCredentials, uploadPassport, submitKyc,
|
|
73
|
+
* updateSsn, exportPrivateKey, getMedicalRecord.
|
|
74
|
+
*/
|
|
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;
|
|
77
|
+
/**
|
|
78
|
+
* Classify the safety level of an action.
|
|
79
|
+
*
|
|
80
|
+
* Rules (in priority order):
|
|
81
|
+
* 1. ABI write (non view/pure) + financial verb → financial
|
|
82
|
+
* 2. Financial verb anywhere → financial
|
|
83
|
+
* 3. Confidential noun (PII/credential) → confidential
|
|
84
|
+
* 4. ABI view/pure → read
|
|
85
|
+
* 5. GET HTTP method → read
|
|
86
|
+
* 6. Destructive verb → destructive
|
|
87
|
+
* 7. Everything else → write
|
|
88
|
+
*/
|
|
89
|
+
function classifySafety(opts) {
|
|
90
|
+
const { name, httpMethod, isReadOnly, type } = opts;
|
|
91
|
+
if (type === 'contract') {
|
|
92
|
+
if (isReadOnly)
|
|
93
|
+
return 'read';
|
|
94
|
+
if (FINANCIAL_VERBS.test(name))
|
|
95
|
+
return 'financial';
|
|
96
|
+
if (CONFIDENTIAL_NOUNS.test(name))
|
|
97
|
+
return 'confidential';
|
|
98
|
+
return 'write';
|
|
99
|
+
}
|
|
100
|
+
if (FINANCIAL_VERBS.test(name))
|
|
101
|
+
return 'financial';
|
|
102
|
+
if (CONFIDENTIAL_NOUNS.test(name))
|
|
103
|
+
return 'confidential';
|
|
104
|
+
if (httpMethod === 'GET')
|
|
105
|
+
return 'read';
|
|
106
|
+
if (DESTRUCTIVE_VERBS.test(name))
|
|
107
|
+
return 'destructive';
|
|
108
|
+
return 'write';
|
|
109
|
+
}
|
|
110
|
+
/** Derive agentSafe from safety level. Non-read/write levels require human confirmation. */
|
|
111
|
+
function deriveAgentSafe(safety) {
|
|
112
|
+
return safety === 'read' || safety === 'write';
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Infer per-action auth requirement.
|
|
116
|
+
*
|
|
117
|
+
* Rules:
|
|
118
|
+
* 1. Contract: view/pure → public; write → required (or farcaster-signed if app uses frames)
|
|
119
|
+
* 2. API GET + safety=read → public (heuristic: read endpoints are often open)
|
|
120
|
+
* 3. Farcaster frame app + write/financial/confidential → farcaster-signed
|
|
121
|
+
* 4. Financial → required + payments:write scope
|
|
122
|
+
* 5. Confidential → required + pii:read or pii:write scope
|
|
123
|
+
* 6. Destructive → required
|
|
124
|
+
* 7. Everything else → required (inherits app-level auth)
|
|
125
|
+
*/
|
|
126
|
+
function inferActionAuth(opts) {
|
|
127
|
+
const { safety, httpMethod, isReadOnly, appAuthType, type } = opts;
|
|
128
|
+
// Contract view/pure: always public (read-only, on-chain data)
|
|
129
|
+
if (type === 'contract' && isReadOnly) {
|
|
130
|
+
return { required: 'public' };
|
|
131
|
+
}
|
|
132
|
+
// Farcaster frame apps: sensitive actions need frame signature
|
|
133
|
+
if (appAuthType === 'farcaster-frame' &&
|
|
134
|
+
(safety === 'write' || safety === 'financial' || safety === 'destructive' || safety === 'confidential')) {
|
|
135
|
+
return { required: 'farcaster-signed' };
|
|
136
|
+
}
|
|
137
|
+
// Financial → required + payments:write scope
|
|
138
|
+
if (safety === 'financial') {
|
|
139
|
+
return { required: 'required', scope: 'payments:write' };
|
|
140
|
+
}
|
|
141
|
+
// Confidential → required + pii scope (write for mutations, read for fetches)
|
|
142
|
+
if (safety === 'confidential') {
|
|
143
|
+
return {
|
|
144
|
+
required: 'required',
|
|
145
|
+
scope: httpMethod === 'GET' ? 'pii:read' : 'pii:write',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Destructive → required, no special scope
|
|
149
|
+
if (safety === 'destructive') {
|
|
150
|
+
return { required: 'required' };
|
|
151
|
+
}
|
|
152
|
+
// Read-only GET on a public (no-auth) app → public
|
|
153
|
+
if (httpMethod === 'GET' && safety === 'read' && (appAuthType === 'none' || !appAuthType)) {
|
|
154
|
+
return { required: 'public' };
|
|
155
|
+
}
|
|
156
|
+
return { required: 'required' };
|
|
157
|
+
}
|