Skip to content

feat: in-process toolRulesProvider + analyzeApiCalls in atp-compiler#69

Merged
encodedz merged 2 commits intomasterfrom
feat/odedgo/in-process-tool-rules-provider-and-analyze-api-calls
May 5, 2026
Merged

feat: in-process toolRulesProvider + analyzeApiCalls in atp-compiler#69
encodedz merged 2 commits intomasterfrom
feat/odedgo/in-process-tool-rules-provider-and-analyze-api-calls

Conversation

@encodedz
Copy link
Copy Markdown
Collaborator

@encodedz encodedz commented May 5, 2026

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 toolRulesProvider support

Before: toolRulesProvider registered via createServer({ toolRulesProvider }) only fires on HTTP requests (handleHTTPRequestctx.toolRulesrunInRequestScope). In-process callers (new AgentToolProtocolClient({ server })) go straight into server.handleExecute(ctx) / server.handleExplore(ctx) and bypass the provider entirely. They must pass rules explicitly via body.toolRules or body.config.toolRules, re-wrapping the client per call.

After: handleExplore and handleExecute consult toolRulesProvider(ctx) when body.toolRules is absent. body.toolRules still 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-call headers that merge into ctx.headers — so a provider that reads request headers (the primary pattern per the ServerConfig docstring) works identically on both paths. For execute, headers are pulled from config.requestContext.headers automatically (the documented per-call header entry point).

Diff is 4 files:

  • packages/server/src/handlers/{execute,explorer}.handler.ts — accept optional toolRulesProvider param; call when body rules absent
  • packages/server/src/create-server.ts — pass this.toolRulesProvider through
  • packages/client/src/core/in-process-session.ts — per-call headers merge into ctx.headers

2. Export analyzeApiCalls from atp-compiler

Motivating 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:

import { analyzeApiCalls } from '@mondaydotcomorg/atp-compiler';

const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
// apiCalls: [{ apiGroup, operationId }, ...]  deduplicated
// dynamicCallsDetected: true if code does anything static analysis can't
//   resolve to a concrete call — destructuring, aliasing, computed
//   member expressions. Governance should fail-closed on this unless
//   the grant explicitly allows dynamic dispatch.

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 → unrestricted
  • packages/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
  • Full repo __tests__/unit/ — 17 suites, 224 tests passing (Node 22)
  • packages/server vitest — 111 cases passing
  • tsc --noEmit on server / client / atp-compiler — clean

Live 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. With analyzeApiCalls exported, the gateway's local atp-code-analyzer.ts can be deleted in favor of the upstream import.

Backwards compat

Both changes are additive:

  • toolRulesProvider was optional before and remains so. New param to handlers is optional with undefined default — existing callers (the HTTP middleware and both existing in-process session methods) compile unchanged.
  • handleExplore / handleExecute signatures gain one trailing optional param.
  • analyzeApiCalls is net-new export.

🤖 Generated with Claude Code

encodedz and others added 2 commits May 5, 2026 17:07
…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>
@encodedz encodedz merged commit 6cde739 into master May 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant