From 09c13ae35d63803333be781d0f2f6945f04c9f98 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Sat, 25 Jul 2026 18:14:38 +0200 Subject: [PATCH 1/4] feat(adt-mcp): add delegated assistant read scope --- .../design.md | 44 ++++++++++ .../proposal.md | 28 +++++++ .../specs/adt-mcp/spec.md | 40 +++++++++ .../tasks.md | 13 +++ packages/adt-mcp/src/index.ts | 2 + packages/adt-mcp/src/lib/http/invocation.ts | 40 ++++++++- .../delegated-assistant-catalogue.test.ts | 56 +++++++++++++ .../tests/delegated-assistant-policy.test.ts | 84 +++++++++++++++++++ .../adt-mcp/tests/http-invocation.test.ts | 22 +++++ 9 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/add-delegated-assistant-read-scope/design.md create mode 100644 openspec/changes/add-delegated-assistant-read-scope/proposal.md create mode 100644 openspec/changes/add-delegated-assistant-read-scope/specs/adt-mcp/spec.md create mode 100644 openspec/changes/add-delegated-assistant-read-scope/tasks.md create mode 100644 packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts create mode 100644 packages/adt-mcp/tests/delegated-assistant-policy.test.ts diff --git a/openspec/changes/add-delegated-assistant-read-scope/design.md b/openspec/changes/add-delegated-assistant-read-scope/design.md new file mode 100644 index 000000000..eb4971235 --- /dev/null +++ b/openspec/changes/add-delegated-assistant-read-scope/design.md @@ -0,0 +1,44 @@ +# Design: delegated-assistant read scope + +## Decision + +`delegated-assistant` is a product-neutral signed invocation identity for an +interactive MCP client acting on behalf of one authenticated principal. Its +credential carries the fixed `server` and `read` classes plus one +System/Destination binding. The server's catalogue remains the only +tool-to-class authority. + +The exact constraint is: + +```ts +{ + kind: 'delegated-assistant-read-v1'; + threadId: string; + executionId: string; + systemSid: string; +} +``` + +`threadId` and `executionId` are UUIDs. Limits are empty because operational +rate limits remain server-owned and do not determine tool admission. + +## Enforcement + +The invocation verifier accepts only the exact claim shape. HTTP session +identity remains bound to the credential JTI. `tools/list` filters registered +tools through the operation-class catalogue, and dispatch repeats the same +class and Destination checks before acquiring a lease. + +The client requests the coarse read envelope by obtaining this credential. It +does not send tool names, Destination keys, or resource-broadening arguments. + +## Alternatives rejected + +- Product-specific agent IDs: rejected because the public MCP server must + remain reusable outside one consumer. +- Client-maintained tool allowlists: rejected because they drift from the + server catalogue. +- Reusing `system-assistant`: rejected because it lacks explicit thread and + execution binding and is retained only for compatibility. +- Adding `safe_execute`: rejected because checks require a separate exact, + single-use grant. diff --git a/openspec/changes/add-delegated-assistant-read-scope/proposal.md b/openspec/changes/add-delegated-assistant-read-scope/proposal.md new file mode 100644 index 000000000..b61ddf7e3 --- /dev/null +++ b/openspec/changes/add-delegated-assistant-read-scope/proposal.md @@ -0,0 +1,28 @@ +# Add a delegated-assistant read scope + +## Why + +An interactive assistant needs to discover and call the read tools permitted +to its authenticated principal without duplicating the MCP server's tool +catalogue in every client. + +## What changes + +- Add one product-neutral `delegated-assistant` signed invocation policy. +- Bind the policy to the authenticated principal, MCP execution, thread, and + one System/Destination. +- Let the server's operation-class catalogue select all permitted `server` + and `read` tools at discovery and dispatch. + +## Non-goals + +- This change adds no product-specific agent name or workflow. +- This change adds no `safe_execute` or `write` authority. +- This change adds no client-provided tool-name allowlist or call-count limit. +- This change does not alter ordinary OAuth, bearer, proxy, or stdio modes. + +## Validation + +- Invocation tests prove only the exact delegated read policy is accepted. +- HTTP integration proves multiple read tools are advertised. +- Scope tests prove write and `safe_execute` tools remain absent and denied. diff --git a/openspec/changes/add-delegated-assistant-read-scope/specs/adt-mcp/spec.md b/openspec/changes/add-delegated-assistant-read-scope/specs/adt-mcp/spec.md new file mode 100644 index 000000000..fa029dfb6 --- /dev/null +++ b/openspec/changes/add-delegated-assistant-read-scope/specs/adt-mcp/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Delegated assistants receive a server-owned read catalogue + +The server SHALL accept an exact signed delegated-assistant policy bound to +one principal, thread, execution, System, and Destination. The resulting MCP +catalogue SHALL contain every registered tool whose server-owned operation +class is `server` or `read`. + +#### Scenario: Delegated assistant lists tools + +- **GIVEN** a valid delegated-assistant credential requests the read envelope +- **WHEN** the client calls `tools/list` +- **THEN** the server advertises multiple permitted read tools without a + client-provided tool-name allowlist + +#### Scenario: A new read tool is registered + +- **GIVEN** a new tool has a complete `read` catalogue classification +- **WHEN** a delegated assistant refreshes `tools/list` +- **THEN** the new tool is admitted without changing the client credential + contract + +### Requirement: Delegated read authority cannot widen + +The server SHALL reject malformed delegated-assistant policies and SHALL deny +`safe_execute`, `write`, unknown, and out-of-Destination operations at both +catalogue and dispatch. + +#### Scenario: Delegated assistant attempts a write + +- **WHEN** the client requests or directly calls a write-class tool +- **THEN** the tool is absent from discovery and dispatch returns + `mcp_scope_denied` before a Destination lease or SAP operation + +#### Scenario: Delegated policy carries additional authority + +- **WHEN** the signed claim adds a tool list, resource override, non-empty + limits, another operation class, or an additional Destination +- **THEN** the invocation exposes no MCP tools diff --git a/openspec/changes/add-delegated-assistant-read-scope/tasks.md b/openspec/changes/add-delegated-assistant-read-scope/tasks.md new file mode 100644 index 000000000..d7eb55872 --- /dev/null +++ b/openspec/changes/add-delegated-assistant-read-scope/tasks.md @@ -0,0 +1,13 @@ +# Tasks + +- [x] Define the product-neutral delegated-assistant read policy. +- [x] Add failing policy-parser and HTTP catalogue tests. +- [x] Implement exact invocation verification. +- [x] Prove catalogue and dispatch remain server-owned and read-only. +- [ ] Run adt-mcp build, typecheck, tests, lint, and full-tree formatting. + - Focused policy, catalogue, and signed-JWT tests pass. + - Scoped ESLint and Prettier checks pass; strict OpenSpec validation passes. + - Full build/typecheck and server-construction tests remain blocked by the + pre-existing dependency-tree mismatch (Zod 4 versus + `zod-to-json-schema`, stale generated declarations). A clean frozen + install is unavailable under the current registry policy. diff --git a/packages/adt-mcp/src/index.ts b/packages/adt-mcp/src/index.ts index 5a7d12174..d412bbb55 100644 --- a/packages/adt-mcp/src/index.ts +++ b/packages/adt-mcp/src/index.ts @@ -47,8 +47,10 @@ export { } from './lib/http/server'; export { createMcpInvocationVerifier, + parseDelegatedAssistantReadPolicy, parseScopedAdtInvocationPolicy, parseSafeExecutePolicy, + type DelegatedAssistantReadPolicy, type ScopedAdtInvocationPolicy, type McpInvocationJsonValue, type McpInvocationVerifier, diff --git a/packages/adt-mcp/src/lib/http/invocation.ts b/packages/adt-mcp/src/lib/http/invocation.ts index b9da57067..2b00ea62b 100644 --- a/packages/adt-mcp/src/lib/http/invocation.ts +++ b/packages/adt-mcp/src/lib/http/invocation.ts @@ -33,6 +33,7 @@ const trustedAgentIds = new Set([ 'system-assistant', 'autonomous-review-agent', 'adt-execution', + 'delegated-assistant', ]); const trustedOperationClasses = new Set(['server', 'read', 'safe_execute']); const scopedReadTools = new Set(['get_object', 'get_object_structure']); @@ -55,7 +56,8 @@ export interface TrustedMcpInvocationClaims { | 'ai-review' | 'system-assistant' | 'autonomous-review-agent' - | 'adt-execution'; + | 'adt-execution' + | 'delegated-assistant'; readonly classes: readonly McpTrustedOperationClass[]; readonly destinationKeys: readonly string[]; readonly correlationId: string; @@ -131,6 +133,12 @@ export interface ScopedAdtInvocationPolicy { readonly authorizationToken?: string; } +export interface DelegatedAssistantReadPolicy { + readonly threadId: string; + readonly executionId: string; + readonly systemSid: string; +} + export interface McpInvocationVerifierOptions { /** ES256 public key mounted at ADT Server; it cannot issue credentials. */ publicKey: CryptoKey | KeyObject; @@ -188,6 +196,33 @@ function hasServerReadClasses(claims: TrustedMcpInvocationClaims): boolean { ); } +export function parseDelegatedAssistantReadPolicy( + claims: TrustedMcpInvocationClaims, +): DelegatedAssistantReadPolicy | undefined { + if ( + claims.agentId !== 'delegated-assistant' || + claims.destinationKeys.length !== 1 || + !hasServerReadClasses(claims) || + Object.keys(claims.limits).length !== 0 || + !hasExactSortedKeys(claims.constraint, [ + 'kind', + 'threadId', + 'executionId', + 'systemSid', + ]) || + claims.constraint.kind !== 'delegated-assistant-read-v1' + ) { + return undefined; + } + + const threadId = requiredUuid(claims.constraint.threadId); + const executionId = requiredUuid(claims.constraint.executionId); + const systemSid = requiredSystemSid(claims.constraint.systemSid); + if (!threadId || !executionId || !systemSid) return undefined; + + return Object.freeze({ threadId, executionId, systemSid }); +} + export function isMcpInvocationDispatchPolicySupported( claims: TrustedMcpInvocationClaims, ): boolean { @@ -204,6 +239,9 @@ export function isMcpInvocationDispatchPolicySupported( if (claims.agentId === 'adt-execution') { return parseScopedAdtInvocationPolicy(claims) !== undefined; } + if (claims.agentId === 'delegated-assistant') { + return parseDelegatedAssistantReadPolicy(claims) !== undefined; + } if (claims.agentId === 'ai-review') { return ( hasServerReadClasses(claims) && diff --git a/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts b/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts new file mode 100644 index 000000000..019ed2e72 --- /dev/null +++ b/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + isMcpToolAllowed, + isMcpToolListed, + MCP_TOOL_SCOPE_CATALOGUE, + type McpRequestAccess, +} from '../src/lib/tools/scope-catalogue.js'; + +const access: McpRequestAccess = { + classes: ['server', 'read'], + destinationKeys: ['dev'], +}; + +test('delegated read envelope projects the complete server-owned read catalogue', () => { + const listed = Object.keys(MCP_TOOL_SCOPE_CATALOGUE) + .filter((name) => isMcpToolListed(access, name)) + .sort(); + + assert.ok(listed.length > 10); + for (const readTool of [ + 'system_info', + 'get_object', + 'find_references', + 'cts_get_transport', + ]) { + assert.ok(listed.includes(readTool), `missing read tool ${readTool}`); + } + assert.ok(!listed.includes('lock_object')); + assert.ok(!listed.includes('atc_run')); +}); + +test('delegated read envelope denies write and safe-execute dispatch', () => { + assert.strictEqual( + isMcpToolAllowed(access, 'get_object', { + destination: 'dev', + objectName: 'ZCL_SCOPE_TEST', + }), + true, + ); + assert.strictEqual( + isMcpToolAllowed(access, 'lock_object', { + destination: 'dev', + objectName: 'ZCL_SCOPE_TEST', + }), + false, + ); + assert.strictEqual( + isMcpToolAllowed(access, 'atc_run', { + destination: 'dev', + scope: { kind: 'package', packageName: 'ZPACKAGE' }, + }), + false, + ); + assert.strictEqual(isMcpToolAllowed(access, 'unknown_tool'), false); +}); diff --git a/packages/adt-mcp/tests/delegated-assistant-policy.test.ts b/packages/adt-mcp/tests/delegated-assistant-policy.test.ts new file mode 100644 index 000000000..9f72d59ba --- /dev/null +++ b/packages/adt-mcp/tests/delegated-assistant-policy.test.ts @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + parseDelegatedAssistantReadPolicy, + type TrustedMcpInvocationClaims, +} from '../src/index.js'; +import { isMcpInvocationDispatchPolicySupported } from '../src/lib/http/invocation.js'; + +const threadId = '11111111-1111-4111-8111-111111111111'; +const executionId = '22222222-2222-4222-8222-222222222222'; + +function claims( + overrides: Partial = {}, +): TrustedMcpInvocationClaims { + return { + tokenId: 'jti-delegated-assistant-001', + principal: 'engineer@example.invalid', + agentId: 'delegated-assistant', + classes: ['server', 'read'], + destinationKeys: ['tst-adt'], + correlationId: 'delegated-assistant:001', + constraint: { + kind: 'delegated-assistant-read-v1', + threadId, + executionId, + systemSid: 'TST', + }, + limits: {}, + ...overrides, + }; +} + +describe('Delegated-assistant read policy', () => { + it('accepts the exact principal-scoped read envelope', () => { + assert.deepStrictEqual(parseDelegatedAssistantReadPolicy(claims()), { + threadId, + executionId, + systemSid: 'TST', + }); + assert.strictEqual(isMcpInvocationDispatchPolicySupported(claims()), true); + }); + + it('rejects tool lists, limits, extra classes, and extra Destinations', () => { + for (const candidate of [ + claims({ + constraint: { + ...claims().constraint, + toolNames: ['get_object'], + }, + }), + claims({ limits: { maxToolCalls: 12 } }), + claims({ classes: ['server', 'read', 'safe_execute'] }), + claims({ destinationKeys: ['tst-adt', 'prd-adt'] }), + ]) { + assert.strictEqual( + parseDelegatedAssistantReadPolicy(candidate), + undefined, + ); + assert.strictEqual( + isMcpInvocationDispatchPolicySupported(candidate), + false, + ); + } + }); + + it('rejects malformed or incomplete execution identity', () => { + for (const constraint of [ + { ...claims().constraint, kind: 'unknown' }, + { ...claims().constraint, threadId: 'not-a-uuid' }, + { ...claims().constraint, executionId: 'not-a-uuid' }, + { ...claims().constraint, systemSid: 'TST!' }, + { + kind: 'delegated-assistant-read-v1', + threadId, + executionId, + }, + ]) { + assert.strictEqual( + parseDelegatedAssistantReadPolicy(claims({ constraint })), + undefined, + ); + } + }); +}); diff --git a/packages/adt-mcp/tests/http-invocation.test.ts b/packages/adt-mcp/tests/http-invocation.test.ts index 6e8e87b5f..c14797aa2 100644 --- a/packages/adt-mcp/tests/http-invocation.test.ts +++ b/packages/adt-mcp/tests/http-invocation.test.ts @@ -134,6 +134,28 @@ describe('MCP invocation verifier', () => { assert.strictEqual(isMcpInvocationDispatchPolicySupported(verified), false); }); + it('verifies and dispatches a signed delegated-assistant read envelope', async () => { + const credential = await sign({ + agentId: 'delegated-assistant', + classes: ['server', 'read'], + destinationKeys: ['tst-adt'], + correlationId: 'delegated-assistant:001', + constraint: { + kind: 'delegated-assistant-read-v1', + threadId: '11111111-1111-4111-8111-111111111111', + executionId: '22222222-2222-4222-8222-222222222222', + systemSid: 'TST', + }, + limits: {}, + }); + + const verified = await verifier.verify(`Bearer ${credential}`); + + assert.ok(verified); + assert.strictEqual(verified.agentId, 'delegated-assistant'); + assert.strictEqual(isMcpInvocationDispatchPolicySupported(verified), true); + }); + it('rejects a credential with an invalid ES256 signature', async () => { const { privateKey: untrustedPrivateKey } = await generateKeyPair('ES256'); const credential = await sign({}, { signingKey: untrustedPrivateKey }); From 0f9aa4cd1ae5360c059b0de1d29149d074587556 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Sat, 25 Jul 2026 18:19:54 +0200 Subject: [PATCH 2/4] docs(agents): enable autonomous feature delivery --- .agents/repo-guide.md | 14 +++---- .agents/rules/git/autonomous-delivery.md | 26 ++++++++++++ .agents/rules/git/no-auto-commit.md | 40 ------------------- docs/roadmap/README.md | 4 +- docs/roadmap/epics/_template.md | 2 +- docs/roadmap/epics/e01-include.md | 2 +- docs/roadmap/epics/e02-function.md | 2 +- docs/roadmap/epics/e03-badi.md | 2 +- docs/roadmap/epics/e04-strust.md | 2 +- docs/roadmap/epics/e05-format-plugin-api.md | 2 +- docs/roadmap/epics/e06-gcts-format-plugin.md | 2 +- docs/roadmap/epics/e07-gcts-command-plugin.md | 2 +- docs/roadmap/epics/e08-checkin.md | 2 +- docs/roadmap/epics/e09-acds-parser.md | 2 +- docs/roadmap/epics/e10-rap-bdef.md | 2 +- docs/roadmap/epics/e11-rap-srvd.md | 2 +- docs/roadmap/epics/e12-rap-srvb.md | 2 +- docs/roadmap/epics/e13-startrfc.md | 2 +- docs/roadmap/epics/e14-flp.md | 2 +- docs/roadmap/epics/e15-wb.md | 2 +- 20 files changed, 52 insertions(+), 64 deletions(-) create mode 100644 .agents/rules/git/autonomous-delivery.md delete mode 100644 .agents/rules/git/no-auto-commit.md diff --git a/.agents/repo-guide.md b/.agents/repo-guide.md index 1804498b1..de54e9c9d 100644 --- a/.agents/repo-guide.md +++ b/.agents/repo-guide.md @@ -110,13 +110,13 @@ All AI agent rules live in `.agents/rules/` (single source of truth). ### Always On -| Rule | Description | -| ------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| [`git/no-auto-commit`](rules/git/no-auto-commit.md) | Never commit or push without explicit user approval | -| [`development/coding-conventions`](rules/development/coding-conventions.md) | TS strict, ESM only, naming, formatting, import conventions | -| [`development/file-lifecycle`](rules/development/file-lifecycle.md) | Generated/downloaded file guardrails | -| [`openspec/project-planning-memory`](rules/openspec/project-planning-memory.md) | OpenSpec workflow and project memory | -| [`verification/after-changes`](rules/verification/after-changes.md) | Build, typecheck, test, lint, format checklist | +| Rule | Description | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| [`git/autonomous-delivery`](rules/git/autonomous-delivery.md) | Commit and push authorized feature work without re-prompting | +| [`development/coding-conventions`](rules/development/coding-conventions.md) | TS strict, ESM only, naming, formatting, import conventions | +| [`development/file-lifecycle`](rules/development/file-lifecycle.md) | Generated/downloaded file guardrails | +| [`openspec/project-planning-memory`](rules/openspec/project-planning-memory.md) | OpenSpec workflow and project memory | +| [`verification/after-changes`](rules/verification/after-changes.md) | Build, typecheck, test, lint, format checklist | ### On Demand (model_decision) diff --git a/.agents/rules/git/autonomous-delivery.md b/.agents/rules/git/autonomous-delivery.md new file mode 100644 index 000000000..5e2ca5c60 --- /dev/null +++ b/.agents/rules/git/autonomous-delivery.md @@ -0,0 +1,26 @@ +--- +trigger: always_on +description: Commit and push authorized feature work without re-prompting. +--- + +# Autonomous Delivery Rule + +## Rule + +Once the user authorizes implementation, that authorization includes creating +and pushing verified atomic commits to the scoped feature branch. Do not pause +to request a second commit or push confirmation. + +Before committing and pushing: + +1. Inspect the exact staged files and diff. +2. Run the relevant verification gates and record known blockers. +3. Use a scoped, public-safe commit message and author identity where required. +4. Push only the intended feature branch and report the resulting commit/MR. + +## Actions that remain gated + +- destructive history rewrites or force-pushes; +- direct protected-branch updates or merges; +- releases and package publication; +- expanding the branch, repository, or delivery scope. diff --git a/.agents/rules/git/no-auto-commit.md b/.agents/rules/git/no-auto-commit.md deleted file mode 100644 index 4242cfa3a..000000000 --- a/.agents/rules/git/no-auto-commit.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -trigger: always_on -description: Never commit or push without explicit user approval. ---- - -# No Auto-Commit Rule - -## Rule - -**NEVER run `git commit` or `git push` without explicit user approval.** - -Before committing: - -1. Show the planned commit(s): message, staged files, diff summary -2. Wait for the user to confirm -3. Only then execute `git commit` - -## Exception: `$monitor-ci` Skill - -When the `$monitor-ci` skill is active (CI self-healing workflow), autonomous commits and pushes are permitted for: - -- Applying self-healing fixes -- Retrying CI with empty commits -- Updating lockfiles for pre-CI failures - -The skill has its own git safety rules (never `git add -A`, stage only fix-related files). - -## Applies To - -- All AI assistants (Devin, Windsurf, Claude, etc.) -- Both interactive and background/subagent sessions - -## Why - -Commits are permanent project history. The user must review and approve: - -- What files are staged -- The commit message -- Whether changes should be split into multiple commits -- Whether to push after committing diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index d755fc374..19181c177 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -119,5 +119,7 @@ Otherwise: open https://app.devin.ai/ → New session → paste the **Devin prom - Every operation needs an entry in `packages/adt-cli/tests/e2e/parity..test.ts`. - Real SAP fixtures in `@abapify/adt-fixtures` (mark `TODO-synthetic` only when no capture available). - abapGit-style filenames everywhere a file path is emitted (use `adtUriToAbapGitPath`). -- No commits without explicit user approval (per repo `.agents/rules/git/no-auto-commit`). +- Authorized implementation is committed and pushed as verified atomic slices + without a separate confirmation prompt (see + `.agents/rules/git/autonomous-delivery`). - Run `bunx nx format:write` before signalling done. diff --git a/docs/roadmap/epics/_template.md b/docs/roadmap/epics/_template.md index b1dcb3030..7eeb6ef69 100644 --- a/docs/roadmap/epics/_template.md +++ b/docs/roadmap/epics/_template.md @@ -91,7 +91,7 @@ Read these files first: Implement everything in the "Scope" section. Stay strictly within "Out of scope" boundaries. Run the full Acceptance block before declaring done. -Do NOT commit unless explicitly approved by the operator. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. If you encounter blockers (missing contract, undecided design choice), document them in docs/roadmap/epics/eXX-NAME.md under a new "Open questions" section and stop — don't guess. diff --git a/docs/roadmap/epics/e01-include.md b/docs/roadmap/epics/e01-include.md index e23960765..a1ff31921 100644 --- a/docs/roadmap/epics/e01-include.md +++ b/docs/roadmap/epics/e01-include.md @@ -82,6 +82,6 @@ Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e01-include.md Read AGENTS.md, docs/roadmap/README.md, then this epic file. Implement Scope strictly. Reference impl at /tmp/sapcli-ref/sapcli/ (clone if missing). -Do NOT commit without explicit user approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. Run the Acceptance block before declaring done. ``` diff --git a/docs/roadmap/epics/e02-function.md b/docs/roadmap/epics/e02-function.md index 07a19f874..98b8f1b44 100644 --- a/docs/roadmap/epics/e02-function.md +++ b/docs/roadmap/epics/e02-function.md @@ -93,5 +93,5 @@ Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e02-function.md Read AGENTS.md, docs/roadmap/README.md, then this epic file. Implement Scope strictly. Reference: /tmp/sapcli-ref/sapcli/sap/cli/function.py + sap/adt/function.py -Do NOT commit without explicit user approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` diff --git a/docs/roadmap/epics/e03-badi.md b/docs/roadmap/epics/e03-badi.md index 4beb4d225..177ede2d8 100644 --- a/docs/roadmap/epics/e03-badi.md +++ b/docs/roadmap/epics/e03-badi.md @@ -68,7 +68,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write ``` Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e03-badi.md Read AGENTS.md + docs/roadmap/README.md first. Reference: /tmp/sapcli-ref/sapcli/sap/cli/badi.py. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Implementation notes (first pass) diff --git a/docs/roadmap/epics/e04-strust.md b/docs/roadmap/epics/e04-strust.md index 2888d66fa..913e933f9 100644 --- a/docs/roadmap/epics/e04-strust.md +++ b/docs/roadmap/epics/e04-strust.md @@ -67,7 +67,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write ``` Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e04-strust.md Read AGENTS.md + docs/roadmap/README.md. Reference: /tmp/sapcli-ref/sapcli/sap/cli/strust.py. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions (post-landing) diff --git a/docs/roadmap/epics/e05-format-plugin-api.md b/docs/roadmap/epics/e05-format-plugin-api.md index 74f8f27a0..e3efa654e 100644 --- a/docs/roadmap/epics/e05-format-plugin-api.md +++ b/docs/roadmap/epics/e05-format-plugin-api.md @@ -104,7 +104,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e05-format-plugin-api.md Read AGENTS.md + docs/roadmap/README.md, then this spec. This epic is foundation for E06/E07/E08; breakages will block multiple downstream sessions. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e06-gcts-format-plugin.md b/docs/roadmap/epics/e06-gcts-format-plugin.md index 482cdf789..571fec601 100644 --- a/docs/roadmap/epics/e06-gcts-format-plugin.md +++ b/docs/roadmap/epics/e06-gcts-format-plugin.md @@ -109,7 +109,7 @@ Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e06-gcts-format-plugi Read AGENTS.md, docs/roadmap/README.md, e05-format-plugin-api.md, then this file. Reference SAP help docs for gCTS file layout — capture and pin locally before coding handlers. This epic must NOT introduce gCTS *commands* (those are E07). Only serialization. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e07-gcts-command-plugin.md b/docs/roadmap/epics/e07-gcts-command-plugin.md index 6ee105fbb..3968405b7 100644 --- a/docs/roadmap/epics/e07-gcts-command-plugin.md +++ b/docs/roadmap/epics/e07-gcts-command-plugin.md @@ -114,7 +114,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e07-gcts-command-plugin.md Read AGENTS.md, docs/roadmap/README.md, e05-format-plugin-api.md, e06-gcts-format-plugin.md. Reference: /tmp/sapcli-ref/sapcli/sap/cli/gcts.py and sap/rest/gcts/. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e08-checkin.md b/docs/roadmap/epics/e08-checkin.md index dc6b7fff5..7743cc37d 100644 --- a/docs/roadmap/epics/e08-checkin.md +++ b/docs/roadmap/epics/e08-checkin.md @@ -91,7 +91,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e08-checkin.md Read AGENTS.md, docs/roadmap/README.md, e05-format-plugin-api.md, packages/adk/AGENTS.md. Reference: /tmp/sapcli-ref/sapcli/sap/cli/checkin.py. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e09-acds-parser.md b/docs/roadmap/epics/e09-acds-parser.md index 8d64ca918..136a96ea8 100644 --- a/docs/roadmap/epics/e09-acds-parser.md +++ b/docs/roadmap/epics/e09-acds-parser.md @@ -72,7 +72,7 @@ bunx nx build acds && bunx nx test acds Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e09-acds-parser.md Read packages/acds/AGENTS.md and the existing grammar/parser. Refer to SAP CDS DDL spec. This is foundation for RAP epics — keep API stable. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e10-rap-bdef.md b/docs/roadmap/epics/e10-rap-bdef.md index a9bd3e3d6..086f84241 100644 --- a/docs/roadmap/epics/e10-rap-bdef.md +++ b/docs/roadmap/epics/e10-rap-bdef.md @@ -77,7 +77,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e10-rap-bdef.md Reads: AGENTS.md, docs/roadmap/README.md, e09-acds-parser.md, packages/adk/AGENTS.md. Reference: /tmp/sapcli-ref/sapcli/sap/cli/behaviordefinition.py. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Delivered (2025-PR-103) diff --git a/docs/roadmap/epics/e11-rap-srvd.md b/docs/roadmap/epics/e11-rap-srvd.md index cf593764d..727fa94fa 100644 --- a/docs/roadmap/epics/e11-rap-srvd.md +++ b/docs/roadmap/epics/e11-rap-srvd.md @@ -64,7 +64,7 @@ packages/adt-plugin-abapgit/tests/filename/adt-uri-to-path.test.ts Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e11-rap-srvd.md Reads: AGENTS.md, docs/roadmap/README.md, e09-acds-parser.md. Capture a real SRVD source from any S/4HANA sample app for reference. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Status: Landed diff --git a/docs/roadmap/epics/e12-rap-srvb.md b/docs/roadmap/epics/e12-rap-srvb.md index 01689c80a..e2c867231 100644 --- a/docs/roadmap/epics/e12-rap-srvb.md +++ b/docs/roadmap/epics/e12-rap-srvb.md @@ -73,7 +73,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write ``` Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e12-rap-srvb.md Reads: AGENTS.md, docs/roadmap/README.md, packages/adt-mcp/src/lib/tools/publish-service-binding.ts. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Status: Landed diff --git a/docs/roadmap/epics/e13-startrfc.md b/docs/roadmap/epics/e13-startrfc.md index 23598482b..00aaeef9d 100644 --- a/docs/roadmap/epics/e13-startrfc.md +++ b/docs/roadmap/epics/e13-startrfc.md @@ -80,7 +80,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e13-startrfc.md Reads: AGENTS.md, docs/roadmap/README.md. Reference: /tmp/sapcli-ref/sapcli/sap/cli/startrfc.py and sap/rfc/. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions diff --git a/docs/roadmap/epics/e14-flp.md b/docs/roadmap/epics/e14-flp.md index 41a76c3c3..8e98f3650 100644 --- a/docs/roadmap/epics/e14-flp.md +++ b/docs/roadmap/epics/e14-flp.md @@ -60,5 +60,5 @@ packages/adt-fixtures/src/mock-server/routes.ts ``` Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e14-flp.md Reference: /tmp/sapcli-ref/sapcli/sap/cli/flp.py. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` diff --git a/docs/roadmap/epics/e15-wb.md b/docs/roadmap/epics/e15-wb.md index 652a74ba6..12c459a33 100644 --- a/docs/roadmap/epics/e15-wb.md +++ b/docs/roadmap/epics/e15-wb.md @@ -68,7 +68,7 @@ bunx nx typecheck && bunx nx lint && bunx nx format:write ``` Spec: /mnt/wsl/workspace/ubuntu/adt-cli/docs/roadmap/epics/e15-wb.md Reference: /tmp/sapcli-ref/sapcli/sap/cli/wb.py and our existing MCP tools. -Do NOT commit without approval. +Commit and push verified changes to the scoped feature branch without a separate confirmation prompt. ``` ## Open questions (post-real-SAP sweep, TRL 2025-11) From b475567558e8467bd5d3e2e836b4f4faa9e47ad6 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Fri, 24 Jul 2026 10:49:20 +0200 Subject: [PATCH 3/4] feat: expose REST runtime composition seam --- packages/adt-server/src/index.ts | 5 +++++ packages/adt-server/src/rest-runtime.ts | 20 +++++++++---------- .../adt-server/tests/rest-runtime.test.ts | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/adt-server/src/index.ts b/packages/adt-server/src/index.ts index 299ca6e87..9bf7c8804 100644 --- a/packages/adt-server/src/index.ts +++ b/packages/adt-server/src/index.ts @@ -18,3 +18,8 @@ export { createHttpDestinationContexts, type HttpBrokerOptions, } from './broker.js'; +export { + loadRestRuntimeSecurity, + type RestRuntimeSecurity, + type RestRuntimeSecurityOptions, +} from './rest-runtime.js'; diff --git a/packages/adt-server/src/rest-runtime.ts b/packages/adt-server/src/rest-runtime.ts index 75dcd780a..4fc3f3c1d 100644 --- a/packages/adt-server/src/rest-runtime.ts +++ b/packages/adt-server/src/rest-runtime.ts @@ -3,6 +3,7 @@ import { createRestPageCursorService } from './page-cursors.js'; import { loadOptionalRestBearerAuthorizer } from './rest-auth.js'; import { createRestAtcDocumentationCapabilityService } from './atc-documentation-capabilities.js'; import { createRestSourceCapabilityService } from './source-capabilities.js'; +import type { AdtServerOptions } from './server.js'; export interface RestRuntimeSecurityOptions { tokenFile?: string; @@ -10,6 +11,14 @@ export interface RestRuntimeSecurityOptions { pageCursorSecretFile?: string; } +export type RestRuntimeSecurity = Pick< + AdtServerOptions, + | 'restAuthorizer' + | 'sourceCapabilities' + | 'atcDocumentationCapabilities' + | 'pageCursors' +>; + async function readOptionalMountedSecret( secretFile: string | undefined, ): Promise { @@ -25,16 +34,7 @@ async function readOptionalMountedSecret( */ export async function loadRestRuntimeSecurity( options: RestRuntimeSecurityOptions, -): Promise<{ - restAuthorizer: Awaited>; - sourceCapabilities: - | ReturnType - | undefined; - atcDocumentationCapabilities: - | ReturnType - | undefined; - pageCursors: ReturnType | undefined; -}> { +): Promise { const restAuthorizer = await loadOptionalRestBearerAuthorizer( options.tokenFile, ); diff --git a/packages/adt-server/tests/rest-runtime.test.ts b/packages/adt-server/tests/rest-runtime.test.ts index e2cf9cb2a..73d910337 100644 --- a/packages/adt-server/tests/rest-runtime.test.ts +++ b/packages/adt-server/tests/rest-runtime.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; -import { loadRestRuntimeSecurity } from '../src/rest-runtime.js'; +import { loadRestRuntimeSecurity } from '../src/index.js'; test('keeps REST disabled when its mounted bearer secret is empty', async () => { const directory = await mkdtemp(path.join(os.tmpdir(), 'adt-rest-runtime-')); From a74fb02ab2468c5216359f894c4caa20263f4244 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Sun, 26 Jul 2026 00:57:45 +0200 Subject: [PATCH 4/4] fix(adt-mcp): expose code review checks to read assistants Classify ATC, AUnit, and coverage as non-mutating read analysis while preserving optional exact scoped execution policies. --- .../classify-atc-as-read-analysis/design.md | 23 +++++++++++ .../classify-atc-as-read-analysis/proposal.md | 24 ++++++++++++ .../specs/adt-mcp/spec.md | 38 +++++++++++++++++++ .../classify-atc-as-read-analysis/tasks.md | 5 +++ .../adt-mcp/src/lib/tools/scope-catalogue.ts | 32 +++++++++------- .../delegated-assistant-catalogue.test.ts | 19 ++++++++-- .../adt-mcp/tests/scope-enforcement.test.ts | 31 ++------------- 7 files changed, 128 insertions(+), 44 deletions(-) create mode 100644 openspec/changes/classify-atc-as-read-analysis/design.md create mode 100644 openspec/changes/classify-atc-as-read-analysis/proposal.md create mode 100644 openspec/changes/classify-atc-as-read-analysis/specs/adt-mcp/spec.md create mode 100644 openspec/changes/classify-atc-as-read-analysis/tasks.md diff --git a/openspec/changes/classify-atc-as-read-analysis/design.md b/openspec/changes/classify-atc-as-read-analysis/design.md new file mode 100644 index 000000000..8bb35aa90 --- /dev/null +++ b/openspec/changes/classify-atc-as-read-analysis/design.md @@ -0,0 +1,23 @@ +# Design: Code Review checks as non-mutating analysis + +## Decision + +`atc_run` and `run_unit_tests` belong to the ordinary `read` catalogue because +they inspect or execute existing ABAP code and return findings, test results, +and coverage without changing repository objects, transport contents, +configuration, or approval state. + +The SAP implementation creates ephemeral ATC/AUnit execution state. That +implementation detail is not a business mutation and does not justify +interrupting every Code Review with a user approval. + +Existing object-bound `safe_execute` credentials remain valid for workflows +that require stricter resource and replay limits. This is an optional narrowing +of authority, not a prerequisite for ATC. + +## Boundaries + +- The authenticated credential still binds the Destination. +- MCP response and transport limits remain enforced. +- All repository, CTS, lock, activation, configuration, and write operations + remain excluded from read authority. diff --git a/openspec/changes/classify-atc-as-read-analysis/proposal.md b/openspec/changes/classify-atc-as-read-analysis/proposal.md new file mode 100644 index 000000000..acb1058d3 --- /dev/null +++ b/openspec/changes/classify-atc-as-read-analysis/proposal.md @@ -0,0 +1,24 @@ +# Classify Code Review checks as read analysis + +## Why + +ATC, AUnit, and code coverage are fundamental Code Review operations. +Requiring a separate `safe_execute` approval removes them from ordinary +non-mutating assistant catalogues and prevents an assistant from completing a +transport review. + +## What changes + +- Classify `atc_run` and `run_unit_tests` (with or without coverage) as + non-mutating `read` operations. +- Advertise and dispatch these checks for an authenticated read-only + Destination. +- Retain support for stricter object-bound `safe_execute` credentials when a + workflow elects to use them. +- Keep repository mutations outside ordinary read authority. + +## Impact + +Delegated read assistants can run ATC, AUnit, and coverage without user approval. Destination +binding, authentication, response bounds, and optional stricter execution +policies remain server-enforced. diff --git a/openspec/changes/classify-atc-as-read-analysis/specs/adt-mcp/spec.md b/openspec/changes/classify-atc-as-read-analysis/specs/adt-mcp/spec.md new file mode 100644 index 000000000..f36a8c5ec --- /dev/null +++ b/openspec/changes/classify-atc-as-read-analysis/specs/adt-mcp/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: Review checks are available as non-mutating analysis + +The server SHALL classify `atc_run` and `run_unit_tests`, including coverage, +as read operations and SHALL advertise and dispatch them for an authenticated +credential containing `server` and `read` authority for the selected +Destination. + +#### Scenario: Delegated assistant runs ATC for a transport + +- **GIVEN** a delegated assistant has read authority for one Destination +- **WHEN** it lists tools and calls `atc_run` with a transport-request scope +- **THEN** `atc_run` is advertised and the ATC findings are returned without a + separate approval + +#### Scenario: Delegated assistant runs AUnit with coverage + +- **GIVEN** a delegated assistant has read authority for one Destination +- **WHEN** it calls `run_unit_tests` with coverage enabled for an object +- **THEN** test and coverage results are returned without a separate approval + +#### Scenario: Read authority remains non-mutating + +- **WHEN** the same assistant lists or calls a mutation +- **THEN** the operation is absent or denied before SAP mutation + +### Requirement: Stricter scoped ATC remains supported + +The server SHALL continue to accept an exact object-bound `safe_execute` +credential for `atc_run` or `run_unit_tests` when a workflow chooses that +narrower execution policy. + +#### Scenario: Workflow supplies an exact ATC grant + +- **WHEN** a valid scoped `safe_execute` credential names `atc_run` and exact + object keys +- **THEN** catalogue and dispatch enforce the existing scoped policy diff --git a/openspec/changes/classify-atc-as-read-analysis/tasks.md b/openspec/changes/classify-atc-as-read-analysis/tasks.md new file mode 100644 index 000000000..c84cebe35 --- /dev/null +++ b/openspec/changes/classify-atc-as-read-analysis/tasks.md @@ -0,0 +1,5 @@ +# Tasks + +- [x] Classify ATC, AUnit, and coverage as read analysis. +- [x] Preserve stricter scoped check execution. +- [x] Prove delegated reads expose checks while mutations remain denied. diff --git a/packages/adt-mcp/src/lib/tools/scope-catalogue.ts b/packages/adt-mcp/src/lib/tools/scope-catalogue.ts index b2e19ef7e..340963466 100644 --- a/packages/adt-mcp/src/lib/tools/scope-catalogue.ts +++ b/packages/adt-mcp/src/lib/tools/scope-catalogue.ts @@ -83,13 +83,6 @@ const read = (names: readonly string[]): Record => const write = (names: readonly string[]): Record => Object.fromEntries(names.map((name) => [name, { operationClass: 'write' }])); -const safeExecute = ( - names: readonly string[], -): Record => - Object.fromEntries( - names.map((name) => [name, { operationClass: 'safe_execute' }]), - ); - /** * Every registered MCP tool has one entry. A mixed-action tool resolves its * operation class from validated arguments; the default remains `write`. @@ -161,10 +154,9 @@ export const MCP_TOOL_SCOPE_CATALOGUE: Readonly> = 'cts_search_transports', 'cts_transport_objects', 'cts_transport_source_manifest', + 'atc_run', + 'run_unit_tests', ]), - // ATC creates a server-side worklist even though it does not mutate ABAP - // repository objects. It therefore needs an explicit execution grant. - ...safeExecute(['atc_run', 'run_unit_tests']), ...write([ 'activate_object', 'activate_package', @@ -266,16 +258,22 @@ export function isMcpToolAllowed( arguments_: Record = {}, ): boolean { const classes = access?.classes; + const scopedAnalysis = + access?.scoped?.operationClass === 'safe_execute' && + (name === 'atc_run' || name === 'run_unit_tests'); const classAllowed = Boolean( Array.isArray(classes) && classes.every(isMcpOperationClass) && - classes.includes(operationClassForMcpTool(name, arguments_)), + (classes.includes(operationClassForMcpTool(name, arguments_)) || + (scopedAnalysis && classes.includes('safe_execute'))), ); if (!classAllowed) return false; const scoped = access?.scoped; return ( !scoped || - (scoped.operationClass === operationClassForMcpTool(name, arguments_) && + ((scoped.operationClass === operationClassForMcpTool(name, arguments_) || + ((name === 'atc_run' || name === 'run_unit_tests') && + scoped.operationClass === 'safe_execute')) && scoped.toolNames.includes(name)) ); } @@ -551,7 +549,15 @@ export function isMcpToolListed( if (access.scoped) { const operationClass = 'actionClasses' in entry ? undefined : entry.operationClass; - if (operationClass !== access.scoped.operationClass) return false; + if ( + operationClass !== access.scoped.operationClass && + !( + (name === 'atc_run' || name === 'run_unit_tests') && + access.scoped.operationClass === 'safe_execute' + ) + ) { + return false; + } if (!access.scoped.toolNames.includes(name)) return false; if (operationClass === 'read' && access.scoped.resourceKeys.length === 0) { return false; diff --git a/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts b/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts index 019ed2e72..80a914eba 100644 --- a/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts +++ b/packages/adt-mcp/tests/delegated-assistant-catalogue.test.ts @@ -23,14 +23,15 @@ test('delegated read envelope projects the complete server-owned read catalogue' 'get_object', 'find_references', 'cts_get_transport', + 'atc_run', + 'run_unit_tests', ]) { assert.ok(listed.includes(readTool), `missing read tool ${readTool}`); } assert.ok(!listed.includes('lock_object')); - assert.ok(!listed.includes('atc_run')); }); -test('delegated read envelope denies write and safe-execute dispatch', () => { +test('delegated read envelope permits review checks but denies mutation', () => { assert.strictEqual( isMcpToolAllowed(access, 'get_object', { destination: 'dev', @@ -48,9 +49,19 @@ test('delegated read envelope denies write and safe-execute dispatch', () => { assert.strictEqual( isMcpToolAllowed(access, 'atc_run', { destination: 'dev', - scope: { kind: 'package', packageName: 'ZPACKAGE' }, + scope: { kind: 'transport_request', trkorr: 'DEVK900001' }, }), - false, + true, + ); + assert.strictEqual( + isMcpToolAllowed(access, 'run_unit_tests', { + destination: 'dev', + objectType: 'CLAS', + objectName: 'ZCL_SCOPE_TEST', + withCoverage: true, + coverageFormat: 'sonar-generic', + }), + true, ); assert.strictEqual(isMcpToolAllowed(access, 'unknown_tool'), false); }); diff --git a/packages/adt-mcp/tests/scope-enforcement.test.ts b/packages/adt-mcp/tests/scope-enforcement.test.ts index e51dc3db8..2d0e8ef50 100644 --- a/packages/adt-mcp/tests/scope-enforcement.test.ts +++ b/packages/adt-mcp/tests/scope-enforcement.test.ts @@ -80,50 +80,27 @@ test('a read-scoped caller can dispatch a permitted read tool', async () => { assert.strictEqual(calls, 1); }); -test('ATC analysis requires an explicit safe-execution class', async () => { +test('ATC analysis is permitted by ordinary read authority', async () => { const target = new CapturingServer(); let calls = 0; const readServer = destinationModeServer(target as unknown as McpServer, { requestAccess: () => ({ classes: ['read'], destinationKeys: ['dev'] }), }); readServer.tool('atc_run', {}, async () => { - calls++; - return { content: [{ type: 'text' as const, text: 'unexpected' }] }; - }); - - const denied = await target.handlers.get('atc_run')!( - { destination: 'dev' }, - { sessionId: 'session-1' }, - ); - assert.strictEqual(denied.isError, true); - assert.strictEqual(denied.content[0]?.text, 'mcp_scope_denied'); - assert.strictEqual(calls, 0); - - const permittedTarget = new CapturingServer(); - const safeExecutionServer = destinationModeServer( - permittedTarget as unknown as McpServer, - { - requestAccess: () => ({ - classes: ['safe_execute'], - destinationKeys: ['dev'], - }), - }, - ); - safeExecutionServer.tool('atc_run', {}, async () => { calls++; return { content: [{ type: 'text' as const, text: 'permitted' }] }; }); - const permitted = await permittedTarget.handlers.get('atc_run')!( + const permitted = await target.handlers.get('atc_run')!( { destination: 'dev' }, - { sessionId: 'session-2' }, + { sessionId: 'session-1' }, ); assert.notStrictEqual(permitted.isError, true); assert.strictEqual(permitted.content[0]?.text, 'permitted'); assert.strictEqual(calls, 1); assert.strictEqual( MCP_TOOL_SCOPE_CATALOGUE.run_unit_tests?.operationClass, - 'safe_execute', + 'read', ); });