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) 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 });