@mondaydotcomorg/atp-compiler 0.22.1 → 0.23.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/__tests__/unit/api-call-analyzer.test.ts +387 -0
- package/dist/api-call-analyzer.d.ts +45 -0
- package/dist/api-call-analyzer.d.ts.map +1 -0
- package/dist/api-call-analyzer.js +192 -0
- package/dist/api-call-analyzer.js.map +1 -0
- package/dist/index.cjs +144 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -13
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/api-call-analyzer.ts +232 -0
- package/src/index.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/atp-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Production-ready compiler for transforming async iteration patterns into resumable operations with checkpoint-based state management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
"@babel/parser": "^7.26.0",
|
|
47
47
|
"@babel/traverse": "^7.26.0",
|
|
48
48
|
"@babel/types": "^7.26.0",
|
|
49
|
-
"@mondaydotcomorg/atp-protocol": "0.22.
|
|
50
|
-
"@mondaydotcomorg/atp-runtime": "0.22.
|
|
49
|
+
"@mondaydotcomorg/atp-protocol": "0.22.3",
|
|
50
|
+
"@mondaydotcomorg/atp-runtime": "0.22.3",
|
|
51
51
|
"@types/babel__generator": "^7.6.0",
|
|
52
52
|
"@types/babel__traverse": "^7.20.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@mondaydotcomorg/atp-protocol": "0.22.
|
|
56
|
-
"@mondaydotcomorg/atp-runtime": "0.22.
|
|
55
|
+
"@mondaydotcomorg/atp-protocol": "0.22.3",
|
|
56
|
+
"@mondaydotcomorg/atp-runtime": "0.22.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@stryker-mutator/core": "^8.0.0",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-dispatch static analysis of ATP agent code.
|
|
3
|
+
*
|
|
4
|
+
* Extracts every `api.<group>.<op>(...)` call chain from submitted code
|
|
5
|
+
* and flags patterns that defeat static analysis (dynamic dispatch,
|
|
6
|
+
* aliasing, destructuring).
|
|
7
|
+
*
|
|
8
|
+
* Intended caller: governance layers that need to know WHICH api groups
|
|
9
|
+
* and operations code will touch BEFORE dispatching it to the sandbox.
|
|
10
|
+
* Paired with runtime `filterApiGroups` enforcement in atp-server for
|
|
11
|
+
* defense-in-depth; the static pass catches unauthorized references
|
|
12
|
+
* up-front without paying sandbox startup cost.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
|
|
16
|
+
* for (const call of apiCalls) {
|
|
17
|
+
* // check (call.apiGroup, call.operationId) against a grant
|
|
18
|
+
* }
|
|
19
|
+
* if (dynamicCallsDetected) {
|
|
20
|
+
* // deny unless the grant explicitly allows dynamic dispatch
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { parse } from '@babel/parser';
|
|
25
|
+
// @babel/traverse exports default; interop handles the CJS/ESM quirk
|
|
26
|
+
// (see https://github.com/babel/babel/issues/13855).
|
|
27
|
+
import _traverse from '@babel/traverse';
|
|
28
|
+
import * as t from '@babel/types';
|
|
29
|
+
|
|
30
|
+
const traverse = ((_traverse as unknown as { default?: typeof _traverse }).default ??
|
|
31
|
+
_traverse) as typeof _traverse;
|
|
32
|
+
|
|
33
|
+
export interface DetectedApiCall {
|
|
34
|
+
apiGroup: string;
|
|
35
|
+
operationId: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AnalysisResult {
|
|
39
|
+
/** Unique `(apiGroup, operationId)` pairs statically visible in the code. */
|
|
40
|
+
apiCalls: DetectedApiCall[];
|
|
41
|
+
/**
|
|
42
|
+
* True iff the code contains patterns we cannot statically resolve to a
|
|
43
|
+
* concrete `(apiGroup, operationId)` — e.g. `api[varName].fn(...)`,
|
|
44
|
+
* destructuring (`const { calendar } = api`), or aliasing
|
|
45
|
+
* (`const x = api.calendar`). Governance layers should fail-closed on
|
|
46
|
+
* this flag unless the caller's policy opts into dynamic dispatch.
|
|
47
|
+
*/
|
|
48
|
+
dynamicCallsDetected: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Analyze agent code and return its statically-visible api.* call set plus a
|
|
53
|
+
* dynamic-dispatch flag. Pure function, no I/O, fail-closed on parse errors
|
|
54
|
+
* (returns `{ apiCalls: [], dynamicCallsDetected: true }`).
|
|
55
|
+
*/
|
|
56
|
+
export function analyzeApiCalls(code: string): AnalysisResult {
|
|
57
|
+
const calls: DetectedApiCall[] = [];
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
let dynamicCallsDetected = false;
|
|
60
|
+
|
|
61
|
+
let ast;
|
|
62
|
+
try {
|
|
63
|
+
ast = parse(code, {
|
|
64
|
+
sourceType: 'module',
|
|
65
|
+
allowReturnOutsideFunction: true,
|
|
66
|
+
plugins: ['typescript'],
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
// Fail-closed: syntax error → treat as dynamic so governance denies.
|
|
70
|
+
return { apiCalls: [], dynamicCallsDetected: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Helper records a static api.<group>.<op>(...) call, or flips
|
|
74
|
+
// dynamicCallsDetected when the call expression escapes static resolution.
|
|
75
|
+
const tryRecordCall = (calleeNode: t.Node) => {
|
|
76
|
+
let callee: t.Node = calleeNode;
|
|
77
|
+
|
|
78
|
+
// Unwrap one `.call` / `.apply` / `.bind` redirection:
|
|
79
|
+
// api.calendar.events_list.call(null, {...})
|
|
80
|
+
// has callee = MemberExpression { object: api.calendar.events_list, property: 'call' }
|
|
81
|
+
if (
|
|
82
|
+
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
83
|
+
!callee.computed &&
|
|
84
|
+
t.isIdentifier(callee.property) &&
|
|
85
|
+
(callee.property.name === 'call' ||
|
|
86
|
+
callee.property.name === 'apply' ||
|
|
87
|
+
callee.property.name === 'bind')
|
|
88
|
+
) {
|
|
89
|
+
callee = callee.object;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) return;
|
|
93
|
+
|
|
94
|
+
const groupExpr = callee.object;
|
|
95
|
+
if (!t.isMemberExpression(groupExpr) && !t.isOptionalMemberExpression(groupExpr)) return;
|
|
96
|
+
if (!t.isIdentifier(groupExpr.object, { name: 'api' })) return;
|
|
97
|
+
|
|
98
|
+
// api[groupVar].op(...) or api.group[fnVar](...) → dynamic
|
|
99
|
+
if (groupExpr.computed || callee.computed) {
|
|
100
|
+
dynamicCallsDetected = true;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const groupNode = groupExpr.property;
|
|
105
|
+
const opNode = callee.property;
|
|
106
|
+
if (!t.isIdentifier(groupNode) || !t.isIdentifier(opNode)) {
|
|
107
|
+
dynamicCallsDetected = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const key = `${groupNode.name}.${opNode.name}`;
|
|
112
|
+
if (!seen.has(key)) {
|
|
113
|
+
seen.add(key);
|
|
114
|
+
calls.push({ apiGroup: groupNode.name, operationId: opNode.name });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
traverse(ast, {
|
|
120
|
+
// Assignments / destructures that alias `api` or `api.<group>` — any of
|
|
121
|
+
// these lets the code reach an api group via an opaque identifier later.
|
|
122
|
+
VariableDeclarator(path) {
|
|
123
|
+
const init = path.node.init;
|
|
124
|
+
if (!init) return;
|
|
125
|
+
|
|
126
|
+
// const { calendar } = api
|
|
127
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
|
|
128
|
+
dynamicCallsDetected = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// const x = api
|
|
132
|
+
if (t.isIdentifier(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
|
|
133
|
+
dynamicCallsDetected = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// const x = api.<group> OR const x = api['<group>']
|
|
137
|
+
if (
|
|
138
|
+
t.isIdentifier(path.node.id) &&
|
|
139
|
+
t.isMemberExpression(init) &&
|
|
140
|
+
t.isIdentifier(init.object, { name: 'api' })
|
|
141
|
+
) {
|
|
142
|
+
dynamicCallsDetected = true;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Any other mention of `api` that hands it off to an opaque consumer:
|
|
147
|
+
// fn(api) — alias escape via function argument
|
|
148
|
+
// Object.values(api) / keys(…) — enumeration
|
|
149
|
+
// { ...api } / [ ...api ] — spread
|
|
150
|
+
// return api — caller gets the alias
|
|
151
|
+
// api = x (reassignment) — later reads hit a different object
|
|
152
|
+
//
|
|
153
|
+
// The api.<group>.<op>(...) pattern is recognised by the CallExpression
|
|
154
|
+
// visitor below; skip it here via parent-shape whitelisting.
|
|
155
|
+
Identifier(path) {
|
|
156
|
+
if (path.node.name !== 'api') return;
|
|
157
|
+
|
|
158
|
+
// Skip the PROPERTY position of a member expression — e.g.
|
|
159
|
+
// `this.api`, `window.api`, `someObj.api`. That's not the
|
|
160
|
+
// global `api` binding we care about.
|
|
161
|
+
if (
|
|
162
|
+
(t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
|
|
163
|
+
path.parent.property === path.node &&
|
|
164
|
+
!path.parent.computed
|
|
165
|
+
) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// `api.<group>` (non-computed member) — safe, handled by CallExpression.
|
|
169
|
+
if (
|
|
170
|
+
(t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
|
|
171
|
+
path.parent.object === path.node &&
|
|
172
|
+
!path.parent.computed
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Left-hand side of `const x = api` / `const { calendar } = api`
|
|
177
|
+
// — handled by VariableDeclarator above (dynamic flag set there).
|
|
178
|
+
if (t.isVariableDeclarator(path.parent) && path.parent.init === path.node) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Skip declaration positions where `api` is a local binding name,
|
|
182
|
+
// not a reference:
|
|
183
|
+
// { api: value } — object property key
|
|
184
|
+
// function f(api) { ... } — param name
|
|
185
|
+
// class { api() {...} } — method name
|
|
186
|
+
// function api() {} — function name
|
|
187
|
+
// class api {} — class name
|
|
188
|
+
if (
|
|
189
|
+
(t.isObjectProperty(path.parent) || t.isObjectMethod(path.parent)) &&
|
|
190
|
+
path.parent.key === path.node &&
|
|
191
|
+
!path.parent.computed
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (t.isClassMethod(path.parent) && path.parent.key === path.node && !path.parent.computed) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
(t.isFunctionDeclaration(path.parent) ||
|
|
200
|
+
t.isFunctionExpression(path.parent) ||
|
|
201
|
+
t.isClassDeclaration(path.parent) ||
|
|
202
|
+
t.isClassExpression(path.parent)) &&
|
|
203
|
+
path.parent.id === path.node
|
|
204
|
+
) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (path.parentPath?.isFunction() && path.listKey === 'params') {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (t.isImportSpecifier(path.parent) || t.isImportDefaultSpecifier(path.parent) || t.isImportNamespaceSpecifier(path.parent)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Anything else (`Object.values(api)`, `fn(api)`, `{ ...api }`,
|
|
215
|
+
// `return api`, `api = ...`) escapes static resolution.
|
|
216
|
+
dynamicCallsDetected = true;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
CallExpression(path) {
|
|
220
|
+
tryRecordCall(path.node.callee);
|
|
221
|
+
},
|
|
222
|
+
OptionalCallExpression(path) {
|
|
223
|
+
tryRecordCall(path.node.callee);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
// Visitor error → fail-closed (should be unreachable under our plugin set).
|
|
228
|
+
return { apiCalls: [], dynamicCallsDetected: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { apiCalls: calls, dynamicCallsDetected };
|
|
232
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ export * from './plugin-system/index.js';
|
|
|
12
12
|
// Checkpoint exports
|
|
13
13
|
export * from './checkpoint/index.js';
|
|
14
14
|
|
|
15
|
+
// Pre-dispatch static analysis of agent code (api.<group>.<op>(...) extraction).
|
|
16
|
+
export { analyzeApiCalls } from './api-call-analyzer.js';
|
|
17
|
+
export type { DetectedApiCall, AnalysisResult } from './api-call-analyzer.js';
|
|
18
|
+
|
|
15
19
|
// Main exports
|
|
16
20
|
export { ATPCompiler } from './transformer/index.js';
|
|
17
21
|
export { initializeRuntime, cleanupRuntime } from './runtime/index.js';
|