Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
81dc2bd
feat(adt-mcp): enforce scoped safe execution
ThePlenkov Jul 24, 2026
31c73be
fix: format mcp-runtime, refactor run-unit-tests for code health, swa…
devin-ai-integration[bot] Jul 24, 2026
1da266d
fix(review): address PR #143 review threads\n\n- Deduplicate scope de…
devin-ai-integration[bot] Jul 24, 2026
d6807f5
refactor: split wrapToolHandler and initializeCsrf to improve code he…
devin-ai-integration[bot] Jul 24, 2026
8f83e8c
fix(review): address Baz and Gitar threads on safe-execute dispatch\n…
devin-ai-integration[bot] Jul 24, 2026
d15941c
fix(codacy): suppress false-positive header-injection on Map counters
devin-ai-integration[bot] Jul 24, 2026
5132a02
fix(codacy): inline nosemgrep suppression for Map.set false positive
devin-ai-integration[bot] Jul 24, 2026
ee0fd77
fix(review): address remaining Baz/Cubic/CodeRabbit review threads
devin-ai-integration[bot] Jul 24, 2026
6746f00
refactor: reduce CodeScene complexity/argument warnings
devin-ai-integration[bot] Jul 24, 2026
3fbb0b3
fix: Sonar/Codacy issues in scoped execution
devin-ai-integration[bot] Jul 24, 2026
a948ab4
fix(session): pass URL objects directly to fetch to satisfy Codacy ta…
devin-ai-integration[bot] Jul 24, 2026
7d493f3
fix(session): suppress Opengrep SSRF false positives on validated ADT…
devin-ai-integration[bot] Jul 24, 2026
3dd2368
fix(adt-mcp, adt-client): address P1 review threads
devin-ai-integration[bot] Jul 24, 2026
6d2bfc4
fix(adt-client, adt-mcp): SSRF session-path normalization and safe-ex…
devin-ai-integration[bot] Jul 24, 2026
d3bd8ae
fix(adt-mcp): return normal MCP error for missing ATC object under sa…
devin-ai-integration[bot] Jul 24, 2026
848b15c
fix(adt-mcp): stable ATC object-not-found error and avoid orphan work…
devin-ai-integration[bot] Jul 24, 2026
911b007
Merge branch 'main' into feat/scoped-safe-execution
ThePlenkov Jul 24, 2026
89e5490
fix(adt-mcp): address Devin Review thread gaps
devin-ai-integration[bot] Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 61 additions & 28 deletions packages/adt-client/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { HttpAdapter, HttpRequestOptions } from '@abapify/adt-contracts';
import type { ResponsePlugin, ResponseContext } from './plugins/types';
import { SessionManager } from './utils/session';
import { createAdtError } from './errors';
import { activeAdtAbortSignal } from './cancellation';

// Re-export HttpAdapter type for consumers
/**
Expand Down Expand Up @@ -123,6 +124,32 @@ async function readResponseTextBounded(
return new TextDecoder().decode(combined);
}

function executionAbortController(): {
abortController: AbortController;
dispose: () => void;
} {
const abortController = new AbortController();
const executionSignal = activeAdtAbortSignal();
if (!executionSignal) {
return { abortController, dispose: () => undefined };
}

const abortFromExecution = () =>
abortController.abort(executionSignal.reason);
if (executionSignal.aborted) {
abortFromExecution();
} else {
executionSignal.addEventListener('abort', abortFromExecution, {
once: true,
});
}
return {
abortController,
dispose: () =>
executionSignal.removeEventListener('abort', abortFromExecution),
};
}

/**
* Create ADT HTTP adapter with Basic or SAML Authentication and plugin support
*/
Expand Down Expand Up @@ -176,6 +203,8 @@ export function createAdtAdapter(config: AdtAdapterConfig): AdtHttpAdapter {
async request<TResponse = unknown>(
options: HttpRequestOptions,
): Promise<TResponse> {
const executionSignal = activeAdtAbortSignal();
executionSignal?.throwIfAborted();
logger?.debug('=== ADAPTER REQUEST START ===');
logger?.debug('options.url:', options.url);
logger?.debug('options.method:', options.method);
Expand Down Expand Up @@ -218,12 +247,12 @@ export function createAdtAdapter(config: AdtAdapterConfig): AdtHttpAdapter {
logger?.debug(
'Adapter: Initializing CSRF token before write operation',
);
await sessionManager.initializeCsrf(
baseUrl,
await sessionManager.initializeCsrf(baseUrl, {
authHeader, // undefined for cookie auth — SessionManager uses stored cookies
client,
language,
);
signal: executionSignal,
});
}

// Prepare headers (pass URL for ETag lookup on PUT/PATCH)
Expand All @@ -241,8 +270,7 @@ export function createAdtAdapter(config: AdtAdapterConfig): AdtHttpAdapter {
// Get schemas from speci's standard fields
// Check if bodySchema is Serializable (has build method from adt-schemas)
let bodySerializableSchema:
| { build: (data: unknown) => string }
| undefined;
{ build: (data: unknown) => string } | undefined;
if (options.bodySchema && typeof options.bodySchema === 'object') {
if (
'build' in options.bodySchema &&
Expand Down Expand Up @@ -363,6 +391,7 @@ export function createAdtAdapter(config: AdtAdapterConfig): AdtHttpAdapter {
method: options.method,
headers,
body: requestBody,
signal: executionSignal,
});

// Process response for session management (cookies, CSRF, ETags)
Expand Down Expand Up @@ -538,32 +567,36 @@ export function createAdtAdapter(config: AdtAdapterConfig): AdtHttpAdapter {
};
if (authHeader) headers.Authorization = authHeader;

const abortController = new AbortController();
// nosemgrep
const response = await fetch(url, {
method: 'GET',
headers,
signal: abortController.signal,
});
sessionManager.processResponse(response, url.pathname);

const text = await readResponseTextBounded(
response,
maxBytes,
abortController,
);
const { abortController, dispose } = executionAbortController();
try {
// nosemgrep
const response = await fetch(url, {
method: 'GET',
headers,
signal: abortController.signal,
});
sessionManager.processResponse(response, url.pathname);

if (!response.ok) {
throw createAdtError(
response.status,
response.statusText,
url.toString(),
'GET',
text,
const text = await readResponseTextBounded(
response,
maxBytes,
abortController,
);
}

return text;
if (!response.ok) {
throw createAdtError(
response.status,
response.statusText,
url.toString(),
'GET',
text,
);
}

return text;
} finally {
dispose();
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
},
};
}
Expand Down
20 changes: 20 additions & 0 deletions packages/adt-client/src/cancellation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AsyncLocalStorage } from 'node:async_hooks';

const adtAbortSignal = new AsyncLocalStorage<AbortSignal>();

/**
* Runs an ADT operation with an execution-scoped abort signal. Async local
* storage keeps concurrent clients and invocations isolated without mutating
* shared client state.
*/
export async function runWithAdtAbortSignal<T>(
signal: AbortSignal,
operation: () => Promise<T>,
): Promise<T> {
signal.throwIfAborted();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return await adtAbortSignal.run(signal, operation);
}

export function activeAdtAbortSignal(): AbortSignal | undefined {
return adtAbortSignal.getStore();
}
1 change: 1 addition & 0 deletions packages/adt-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
type AdtHttpAdapter,
type BoundedTextRequestOptions,
} from './adapter';
export { runWithAdtAbortSignal } from './cancellation';

// Export plugins
export {
Expand Down
Loading
Loading