@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/atp-compiler",
3
- "version": "0.22.1",
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.1",
50
- "@mondaydotcomorg/atp-runtime": "0.22.1",
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.1",
56
- "@mondaydotcomorg/atp-runtime": "0.22.1"
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';