Conversation
…mpiler
Two additions unlocked by the same governance-layer use case
(DaPulse/mcp-tools ATP POC): both are pure-function / small-surface
changes that a gateway-style consumer needs to do pre-dispatch static
analysis WITHOUT re-implementing bits that already exist in ATP.
## 1. In-process toolRulesProvider
Before this change, `toolRulesProvider` registered on `createServer`
only fired for HTTP requests (via `handleHTTPRequest`). In-process
callers (`new AgentToolProtocolClient({ server })`) went
straight into `server.handleExecute(ctx)` / `server.handleExplore(ctx)`
and had to set `body.toolRules` / `body.config.toolRules` explicitly,
re-wrapping the client to inject rules per call.
Threading `toolRulesProvider` through to the handlers and consulting
it when `body.toolRules` is absent lets HTTP and in-process converge
on the same rule-source mechanism. Explicit body rules still win
when supplied — behavior for existing callers is unchanged.
Changes:
- packages/server/src/handlers/explorer.handler.ts: accept optional
toolRulesProvider; use it when body.toolRules absent.
- packages/server/src/handlers/execute.handler.ts: same; merged into
executionConfig.toolRules.
- packages/server/src/create-server.ts: pass this.toolRulesProvider
through to both handlers.
- packages/client/src/core/in-process-session.ts: execute / explore
accept per-call `headers` option that merges into ctx.headers so
the provider sees them. For execute(code, config), headers pulled
out of `config.requestContext.headers` automatically (the
documented per-call header entry point).
New test at __tests__/unit/in-process-tool-rules-provider.test.ts
(3 cases): provider applies from header, body rules take precedence,
neither → unrestricted.
## 2. analyzeApiCalls in atp-compiler
Governance layers (gateway PDPs, CI linters, future policy services)
that need to know which `api.<group>.<op>(...)` calls code will touch
BEFORE dispatch were re-implementing the Babel walk locally. Exporting
the canonical implementation from `@mondaydotcomorg/atp-compiler`
gives every consumer one source of truth.
Changes:
- packages/atp-compiler/src/api-call-analyzer.ts (new): pure function
`analyzeApiCalls(code): { apiCalls, dynamicCallsDetected }`. Extracts
direct `api.<group>.<op>(...)` calls; flags patterns that defeat
static analysis (destructuring, aliasing, computed members). Fails
closed on parse errors.
- packages/atp-compiler/src/index.ts: re-export the function + types.
13 unit tests covering: direct calls, dedup, cross-group, destructure
(with/without rename), alias-api / alias-group, computed-group /
computed-op, trivial code, syntax error, empty string.
Regressions: none. All 17 suites / 224 unit tests + 111 server vitest
cases green on Node 22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…escape paths
Live probing of the analyzer surfaced 5 escape patterns that silently
bypassed static detection. All five now either resolve to a concrete
call OR flip the dynamic flag:
1. Optional chaining (api.calendar?.events_list?.({})) —
added OptionalMemberExpression / OptionalCallExpression visitors;
the call target is STATICALLY resolvable, so we record it.
2. .call() / .apply() / .bind() redirection —
unwrap one layer before matching api.<group>.<op>.
3. Object.values(api) / Object.keys(api) / {...api} —
added Identifier visitor; any `api` not in a whitelisted position
(member object, declarator init, declaration id, import spec,
property key, function param name) flips dynamic.
4. Passing api as a function argument (fn(api)) —
same Identifier visitor catches it.
5. Returning api / assigning to api — same.
Whitelisted (NOT flagged) to avoid false positives:
- this.api / window.api / someObj.api (api in property position)
- { api: ... } (object literal key)
- function f(api) {} (parameter name)
- class { api() {} } (method name)
- function api() {} / class api {} (declaration names)
- import { api } / import api from ..(import specifier local names)
The 13 original tests keep passing unchanged. Test count 13 → 38 with
five new suites:
- dedup — ternary, loop, try/catch, multi-statement
- complex control flow — IIFE, Promise.all, .then() chain, class
method, nested try/catch, switch
- multi-group — full Google Suite workload (6 groups, 11 ops) +
mixed groups with dedup
- dynamic dispatch additional escapes — the 5 fixes above + bind
- unrelated "api" identifiers — this.api, someObj.api, deep nested
- idempotency — same input → same output on repeated calls
Regressions: none. Full repo unit suite 17 suites / 224 tests pass on
Node 22. atp-core nx build clean. tsc --noEmit clean on atp-compiler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two additions, one theme: let governance-layer consumers reuse what ATP already has. Both surfaced while building a gateway-layer PDP on top of ATP (DaPulse/mcp-tools ATP POC) that had to re-implement logic locally because the server's own mechanisms weren't reachable from in-process callers.
1. In-process
toolRulesProvidersupportBefore:
toolRulesProviderregistered viacreateServer({ toolRulesProvider })only fires on HTTP requests (handleHTTPRequest→ctx.toolRules→runInRequestScope). In-process callers (new AgentToolProtocolClient({ server })) go straight intoserver.handleExecute(ctx)/server.handleExplore(ctx)and bypass the provider entirely. They must pass rules explicitly viabody.toolRulesorbody.config.toolRules, re-wrapping the client per call.After:
handleExploreandhandleExecuteconsulttoolRulesProvider(ctx)whenbody.toolRulesis absent.body.toolRulesstill takes precedence. HTTP and in-process converge on the same rule-source mechanism.Header plumbing: the in-process session's
execute(code, config)/explore(path, options)now accept per-callheadersthat merge intoctx.headers— so a provider that reads request headers (the primary pattern per theServerConfigdocstring) works identically on both paths. Forexecute, headers are pulled fromconfig.requestContext.headersautomatically (the documented per-call header entry point).Diff is 4 files:
packages/server/src/handlers/{execute,explorer}.handler.ts— accept optionaltoolRulesProviderparam; call when body rules absentpackages/server/src/create-server.ts— passthis.toolRulesProviderthroughpackages/client/src/core/in-process-session.ts— per-call headers merge into ctx.headers2. Export
analyzeApiCallsfromatp-compilerMotivating use case: governance layers that deny unauthorized code BEFORE dispatch need to statically extract
api.<group>.<op>(...)calls. Until now every such consumer re-implemented the Babel walk. With this export, they converge on one canonical implementation alongside@mondaydotcomorg/atp-compiler's existing AST infrastructure.API:
Fail-closed on parse errors (returns
{ apiCalls: [], dynamicCallsDetected: true }).Test plan
__tests__/unit/in-process-tool-rules-provider.test.ts— 3 cases: provider applies from header, body rules take precedence, neither → unrestrictedpackages/atp-compiler/__tests__/unit/api-call-analyzer.test.ts— 13 cases covering direct calls / dedup / cross-group / destructure (with/without rename) / alias-api / alias-group / computed-group / computed-op / trivial / syntax error / empty string__tests__/unit/— 17 suites, 224 tests passing (Node 22)packages/servervitest — 111 cases passingtsc --noEmiton server / client / atp-compiler — cleanLive verification
Both changes were prototyped in-tree in
DaPulse/mcp-tools(the mcp-gateway POC) before this PR. With the provider path live, the gateway's bridge can drop its per-request Proxy wrap and just pass forwarded headers through the documented entry point. WithanalyzeApiCallsexported, the gateway's localatp-code-analyzer.tscan be deleted in favor of the upstream import.Backwards compat
Both changes are additive:
toolRulesProviderwas optional before and remains so. New param to handlers is optional withundefineddefault — existing callers (the HTTP middleware and both existing in-process session methods) compile unchanged.handleExplore/handleExecutesignatures gain one trailing optional param.analyzeApiCallsis net-new export.🤖 Generated with Claude Code