Skip to content

Connected resources

A connected resource is a normal remote MCP service that accepts the identity the user already selected in WAMP. This page is sufficient to build and deploy the backend, register it, create the customer consent link, and package the Desktop connector without access to the WAMP source repository. The user does not create an account in your service, run another OAuth flow, or copy an API key.

You need a WAMP account with organization.applications.manage in the organization that will publish the service. If you do not see Apps in Account Center, ask an organization owner to grant that permission or perform the registration steps.

These values are fixed for the public WAMP deployment. Copy them as written:

Variable Value
WAMP_ACCOUNT_API https://api.vampikez.fun
WAMP_TOKEN_ISSUER https://api.vampikez.fun
WAMP_JWKS_URL https://api.vampikez.fun/.well-known/jwks.json
Account Center https://account.vampikez.fun

For another WAMP deployment, its operator supplies these values. The Account API and token issuer are intentionally distinct from the OIDC issuer ending in /oauth2; do not use the OIDC issuer or its JWKS URI for resource identity tokens.

Path Status
Backend authentication and standard Streamable HTTP MCP Available
Manifest-only connector imported into WAMP Desktop Available
Native and ACP agents in Desktop Available; the ACP runtime must lend the mcp tool category
Native and ACP agents in managed Cloud Runtime and identity path available when the connector is included in the Cloud engine image
User-installed Marketplace connector automatically appearing in Cloud Not available; arbitrary extensions are not yet delivered into a managed sandbox
Extension UI reading your HTTP API as the viewer Available in WAMP Desktop through auth.resource and pluginAPI.auth.resourceFetch; not yet in managed Cloud or the web workspace

The backend contract does not change across those paths. A backend built from this page is Cloud-compatible; until Cloud extension delivery ships, a WAMP operator must include the same manifest in the Cloud image. Do not put a token in renderer code as a workaround.

  1. An organization authorizes the exact registered resource audience and enables its App for all members or an assigned subset.
  2. An installed extension contributes your standard Streamable HTTP MCP URL and registered resource audience.
  3. WAMP reauthorizes the current Desktop identity or the Cloud Session’s durable execution owner, then mints a short-lived JWT for that exact audience and network origin.
  4. The engine sends the JWT as an ordinary bearer on every MCP HTTP request.
  5. Your service verifies the JWT locally and applies its own tenant and resource ACL using org_id and the pairwise sub.
  6. Native WAMP agents use the discovered tools directly. ACP agents receive the same tools through WAMP’s MCP lending boundary; the JWT and upstream connection never enter the ACP process.

There is no WAMP-specific MCP dialect and no WAMP SDK dependency in your service. Authentication is the only added HTTP boundary. A connector does not need an engine or ACP patch; an ACP runtime only needs its existing tool selection to lend the mcp category.

Use Account Center for the shortest path:

  1. Select your publisher organization, open Apps, and choose New app. Set a stable lowercase slug such as internal-knowledge.
  2. Open the App, choose Add key, then Generate. Download the private key when shown; WAMP stores only the public half and cannot recover the private key. Confirm that you saved it to register the public key.
  3. Under Connected resources, choose New resource and enter the name, audience, canonical HTTPS origin, and namespace described below.
  4. Copy the App ID shown above the resource list. Your backend verifies it as WAMP_RESOURCE_APP_ID.

No OAuth client or WAMP capability definition is required. The signing key is used only by your backend to create installation consent links; it is never sent to the MCP endpoint or included in the extension.

The same registration is available through the public Account API for automation. Its exact request and user-token contract are in Apps and credentials; the Account Center path above requires no token handling.

Value Contract
App slug The existing WAMP App that owns the service. It does not have to equal the extension directory name.
Resource name 1–100 characters, for organization administration.
Audience Globally unique, 2–128 characters, matching ^[a-z][a-z0-9-]{1,127}$. Do not use the platform-reserved wamp prefix.
Origin The canonical HTTPS origin only: scheme, host, and optional explicit port. No path, trailing slash, query, fragment, or embedded credentials.
Namespace Globally unique, at most 96 characters, with at least two lowercase dot-separated segments, such as internal.docs. It reserves future capability names even when this integration defines none.

Configure the deployed service with the values from Account Center and the production values at the top of this page:

service.env
WAMP_TOKEN_ISSUER=https://api.vampikez.fun
WAMP_JWKS_URL=https://api.vampikez.fun/.well-known/jwks.json
WAMP_RESOURCE_AUDIENCE=internal-docs
WAMP_RESOURCE_ORIGIN=https://knowledge.example.com
WAMP_RESOURCE_APP_ID=YOUR_APP_ID
HOST=0.0.0.0
PORT=3000

The audience, origin, namespace, and App id are security identifiers, not display configuration. Once the origin is bound, it cannot be retargeted; use a new audience for a different production origin. Give staging its own audience and HTTPS origin as well. Although the MCP URL may include a path such as /mcp, its new URL(url).origin must equal the registered origin exactly.

List active bindings with GET /api/apps/:slug/resource-servers. Retire one with DELETE /api/apps/:slug/resource-servers/:resourceServerId. Retirement is global and irreversible: WAMP stops issuing new tokens, existing tokens drain within 15 minutes, and the audience and namespace remain reserved so they cannot silently acquire a new meaning.

The service backend creates a normal App installation intent with an empty capability list. Empty means identity only, and the Account API accepts it only when the requested audience belongs to the App that signed the assertion. The following Node 22 script creates an intent, or polls one when given its id. Copy the signing key kid from the App’s key list in Account Center:

Terminal window
npm install jose@6.2.3
installation-intent.mjs
import { readFile } from 'node:fs/promises';
import { importPKCS8, SignJWT } from 'jose';
const required = [
'WAMP_ACCOUNT_API',
'WAMP_TOKEN_ISSUER',
'WAMP_APP_SLUG',
'WAMP_APP_KID',
'WAMP_APP_PRIVATE_KEY_FILE',
'WAMP_RESOURCE_AUDIENCE',
];
for (const name of required) {
if (!process.env[name]) throw new Error(`Missing ${name}`);
}
const key = await importPKCS8(
await readFile(process.env.WAMP_APP_PRIVATE_KEY_FILE, 'utf8'),
'EdDSA',
);
async function createAssertion() {
const now = Math.floor(Date.now() / 1000);
return new SignJWT({})
.setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID })
.setIssuer(process.env.WAMP_APP_SLUG)
.setSubject(process.env.WAMP_APP_SLUG)
.setAudience(process.env.WAMP_TOKEN_ISSUER)
.setIssuedAt(now)
.setExpirationTime(now + 300)
.sign(key);
}
async function post(path, body) {
const response = await fetch(`${process.env.WAMP_ACCOUNT_API}${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(result)}`);
return result.intent;
}
const intentId = process.argv[2];
const assertion = await createAssertion();
const intent = intentId
? await post(`/api/apps/installation-intents/${encodeURIComponent(intentId)}/status`, {
assertion,
})
: await post('/api/apps/installation-intents', {
assertion,
resourceAudience: process.env.WAMP_RESOURCE_AUDIENCE,
capabilityIds: [],
});
console.log(intent);

Replace YOUR_KEY_ID, set the remaining values, and create the link:

Terminal window
export WAMP_ACCOUNT_API=https://api.vampikez.fun
export WAMP_TOKEN_ISSUER=https://api.vampikez.fun
export WAMP_APP_SLUG=internal-knowledge
export WAMP_APP_KID=YOUR_KEY_ID
export WAMP_APP_PRIVATE_KEY_FILE=./internal-knowledge.private.pem
export WAMP_RESOURCE_AUDIENCE=internal-docs
node installation-intent.mjs

Send the returned authorizeUrl to the administrator, then poll with node installation-intent.mjs <intent-id>. The administrator signs into WAMP if needed, chooses an organization, and approves disclosure of its organization identity and one pairwise service-specific user identity. Store the returned installationId when status becomes authorized. This is installation consent, not OAuth: the customer creates no account in your service and copies no key. The exact App assertion contract is in Apps and credentials.

The status is pending, authorized, or expired. Stop polling on either terminal status; create a new intent after expired.

Approval is durable for this exact audience and installation. Registering a second resource on the same App does not inherit an older installation or another resource’s approval: create and authorize another identity-only intent. Revoking the App installation revokes every resource approval attached to it.

Installing the connector extension does not silently install the App. Keep the explicit consent link: the extension controls whether the connector exists on a host, while the Account installation controls which organization and members may disclose identity to the service.

An organization administrator can later narrow or revoke the installation through the standard product access controls. No token is issued when the organization, App, installation, membership, or assigned-member access is inactive.

The connector is a manifest-only WAMP extension. It has no JavaScript, package.json, SDK dependency, build step, or stored credential. Create a directory named internal-knowledge-connector containing one file:

extension.json
{
"name": "Internal Knowledge",
"version": "1.0.0",
"contributes": {
"mcpServers": [
{
"id": "knowledge",
"transport": "http",
"url": "https://knowledge.example.com/mcp",
"wampAuth": { "audience": "internal-docs" }
}
]
}
}

The directory name is the extension id. To hand the connector to a Desktop user, zip the directory itself:

Terminal window
zip -r internal-knowledge-connector.zip internal-knowledge-connector

In WAMP Desktop, open Marketplace, choose Add → Import package…, select the zip, and enable the extension. The WAMP operator may curate the same manifest-only package into the public catalog. Installing an extension and approving its App installation are separate actions: both must be present before WAMP can mint identity for the MCP connection.

wampAuth is valid only for http or sse at an HTTPS URL without embedded credentials. It cannot be combined with oauth or a static Authorization header. Other static headers are allowed, but they are not identity. The bearer is obtained and attached by the host; it never enters extension.json, extension storage, renderer JavaScript, or agent context.

Managed identity may come only from the host-owned global MCP layer, normally through an installed extension. A repository-owned .wamp/mcp-servers.json is allowed to declare ordinary MCP servers but cannot set wampAuth. This prevents a checked out repository from silently requesting the signed-in user’s identity for an arbitrary service.

wampAuth authenticates the host-managed MCP transport only. Extension UI that reads your service directly declares auth.resource instead; see Read your service from extension UI. Never copy a token into renderer code.

The backend developer hands the extension publisher only two security-relevant strings: the full MCP endpoint URL and the registered audience. The App id, JWKS URL, token issuer, and App private key stay in the backend’s configuration; none belongs in extension.json.

Removing or disabling the extension removes the MCP contribution and its token demand. Your service is therefore optional: a failed or absent connector does not stop WAMP or the agent runtime.

An extension panel can call your HTTP API as the person viewing it. WAMP Desktop holds the resource identity JWT and sends the request; the panel receives the response, never the token.

Declare the audience and the API base in extension.json:

{
"compat": { "pluginApi": "^4.0.0" },
"requiresElectron": true,
"permissions": [
{
"auth.resource": [
{ "audience": "internal-docs", "baseUrl": "https://docs.example.com/api/v1" }
]
}
]
}

baseUrl is a canonical HTTPS URL with a path and no credentials, query, fragment or trailing slash. The audience is the one registered for your resource. An extension may declare up to eight audiences, each once. Keep requiresElectron until Cloud hosts support these reads.

From the extension UI:

import { pluginAPI } from '@wamp/plugin-api';
const response = await pluginAPI.auth.resourceFetch(
{ audience: 'internal-docs', path: '/api/v1/documents?limit=20' },
{ signal },
);
// { status, headers: { 'content-type'?, 'retry-after'?, 'x-request-id'? }, body }

path is origin-relative, includes the base path and must stay under it. WAMP refuses a path containing a fragment, whitespace, a control character or a backslash; a pathname with a . or .. segment or an encoded /, \, . or %; and a query that does not round-trip through URLSearchParams. It sends exactly the declared origin followed by path.

Your backend receives one GET with Accept: application/json and Authorization: Bearer <resource identity JWT>, the same token described in What WAMP sends. No cookie, Origin or extension-chosen header is sent.

Rule Behavior
Methods GET only
Redirects Not followed; any 3xx fails the call
Timeout 20 seconds
Response size 256 KiB after decompression
Response type application/json or a +json type; anything else fails the call
401 WAMP exchanges a fresh identity once and retries once; a second 401 is returned

HTTP statuses, including 403, 404, 429 and 5xx, resolve as responses with their parsed JSON body. A call without a trustworthy response rejects with a ResourceFetchError whose code is:

code Meaning
not_declared The audience is not in this extension’s auth.resource
invalid_path The path breaks the rules above
signed_out Nobody is signed in to WAMP
access_denied Account refused the identity; reason is app_not_installed, member_not_assigned or resource_not_registered when Account names one
unavailable Account or your service could not be reached; retry, after retryAfter seconds when present
redirect_refused, response_too_large, invalid_response, timeout The response broke a rule in the table above
identity_changed The viewer signed out or changed account while the request was out; start over
host_unsupported This host has no renderer resource reads (currently Cloud and the web workspace)

Every MCP HTTP request carries:

Authorization: Bearer <resource identity JWT>

The JWT is Ed25519-signed, has typ: at+jwt, and is short-lived: 15 minutes by default. Its audience is one exact string.

Claim Meaning
iss The exact WAMP platform token issuer supplied during registration.
aud The registered resource audience, for example internal-docs.
sub A pairwise subject. A human subject is stable for this resource App across Desktop and Cloud. An App-installation subject is stable while that source installation exists and changes after it is reinstalled.
org_id The WAMP organization whose installation authorized disclosure.
subject_kind human identifies the execution owner. app_installation identifies a non-human App installation executing a Cloud workload; it is not a WAMP user.
resource_app_id The immutable App that owns the resource server.
resource_installation_id That App’s current installation in org_id. This changes after revocation and reinstallation.
resource_origin The registered origin allowed to receive the token.
token_use Exactly resource_identity.
iat, nbf, exp, jti Issuance window and a unique token id. jti is useful for audit correlation; it is not a single-use nonce.

There is deliberately no email, name, global WAMP user id, membership id, parent grant, service permission, or service capability in this token. Store a local principal under the compound key (iss, org_id, sub). Do not use resource_installation_id as the user key. Your service may create the local principal lazily on the first accepted request, so the user never needs a second registration flow.

Persist subject_kind with that principal. Apply member/user ACLs only to human; handle app_installation as a service principal with its own ACL, or deny it if your service does not support non-human callers.

The following Node 22 service is a complete authenticated Streamable HTTP MCP endpoint with one diagnostic tool. It verifies identity before parsing the MCP body, keeps no WAMP credential, and creates a stateless MCP transport per request. Start with these two files:

package.json
{
"name": "internal-knowledge-mcp",
"private": true,
"type": "module",
"scripts": { "start": "node server.mjs" },
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"express": "5.2.1",
"jose": "6.2.3",
"zod": "3.25.76"
}
}
resource-auth.mjs
import { createRemoteJWKSet, errors, jwtVerify } from 'jose';
const ISSUER = process.env.WAMP_TOKEN_ISSUER;
const JWKS_URL = process.env.WAMP_JWKS_URL;
const AUDIENCE = process.env.WAMP_RESOURCE_AUDIENCE;
const ORIGIN = process.env.WAMP_RESOURCE_ORIGIN;
const APP_ID = process.env.WAMP_RESOURCE_APP_ID;
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!ISSUER || !JWKS_URL || !AUDIENCE || !ORIGIN || !APP_ID) {
throw new Error('WAMP resource identity configuration is incomplete');
}
const jwks = createRemoteJWKSet(new URL(JWKS_URL));
export class ResourceIdentityAuthorityUnavailableError extends Error {}
function isAuthorityUnavailable(error) {
return error instanceof TypeError
|| error instanceof errors.JWKSTimeout
|| error instanceof errors.JWKSInvalid
|| error instanceof errors.JWKInvalid
|| error?.constructor === errors.JOSEError;
}
export async function verifyWampResourceBearer(authorization) {
const match = /^Bearer ([^\s]+)$/i.exec(authorization ?? '');
if (!match) throw new Error('missing WAMP resource bearer');
let payload;
try {
({ payload } = await jwtVerify(match[1], jwks, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['EdDSA'],
typ: 'at+jwt',
clockTolerance: 30,
requiredClaims: [
'iss', 'aud', 'sub', 'exp', 'iat', 'nbf', 'jti',
'org_id', 'subject_kind', 'resource_app_id',
'resource_installation_id', 'resource_origin', 'token_use',
],
}));
} catch (error) {
if (isAuthorityUnavailable(error)) {
throw new ResourceIdentityAuthorityUnavailableError(
'WAMP resource identity authority is unavailable',
{ cause: error },
);
}
throw error;
}
if (payload.aud !== AUDIENCE
|| payload.token_use !== 'resource_identity'
|| payload.resource_origin !== ORIGIN
|| payload.resource_app_id !== APP_ID
|| (payload.subject_kind !== 'human'
&& payload.subject_kind !== 'app_installation')
|| typeof payload.sub !== 'string'
|| payload.sub.length === 0
|| typeof payload.org_id !== 'string'
|| !UUID.test(payload.org_id)
|| typeof payload.resource_installation_id !== 'string'
|| !UUID.test(payload.resource_installation_id)) {
throw new Error('invalid WAMP resource identity');
}
return {
issuer: payload.iss,
organizationId: payload.org_id,
subject: payload.sub,
subjectKind: payload.subject_kind,
installationId: payload.resource_installation_id,
tokenId: payload.jti,
expiresAt: payload.exp,
};
}
server.mjs
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
ResourceIdentityAuthorityUnavailableError,
verifyWampResourceBearer,
} from './resource-auth.mjs';
const host = process.env.HOST ?? '127.0.0.1';
const port = Number(process.env.PORT ?? 3000);
const app = express();
app.get('/healthz', (_request, response) => {
response.json({ ok: true });
});
async function authenticate(request, response, next) {
try {
request.wampPrincipal = await verifyWampResourceBearer(
request.get('authorization'),
);
next();
} catch (error) {
if (error instanceof ResourceIdentityAuthorityUnavailableError) {
response
.set('Retry-After', '5')
.status(503)
.json({ error: 'identity_authority_unavailable' });
return;
}
response
.set('WWW-Authenticate', 'Bearer error="invalid_token"')
.status(401)
.json({ error: 'invalid_token' });
}
}
function createServer(principal) {
const server = new McpServer({
name: 'internal-knowledge',
version: '1.0.0',
});
server.registerTool(
'wamp_identity',
{
description: 'Return the WAMP organization and pairwise service identity.',
inputSchema: {},
},
async () => ({
content: [{
type: 'text',
text: JSON.stringify({
organizationId: principal.organizationId,
subject: principal.subject,
subjectKind: principal.subjectKind,
}),
}],
}),
);
return server;
}
app.post('/mcp', authenticate, express.json({ limit: '1mb' }), async (request, response) => {
const server = createServer(request.wampPrincipal);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
response.on('close', () => {
void transport.close();
void server.close();
});
try {
await server.connect(transport);
await transport.handleRequest(request, response, request.body);
} catch (error) {
console.error('MCP request failed', error);
if (!response.headersSent) {
response.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null,
});
}
}
});
app.all('/mcp', authenticate, (_request, response) => {
response.status(405).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Method not allowed' },
id: null,
});
});
app.listen(port, host, () => {
console.log(`MCP server listening on http://${host}:${port}/mcp`);
});

Replace YOUR_APP_ID, run npm install, then start locally with node --env-file=service.env server.mjs. In production, load the same values through your deployment’s secret/config mechanism. Deploy the final /mcp endpoint at the exact registered HTTPS origin without a redirect. Replace the diagnostic tool with your service tools after the identity smoke test. Every data query must scope its own authorization by principal.organizationId and, when user-level access matters, principal.subject.

The JWKS URL normally ends in /.well-known/jwks.json. It is public and cacheable; createRemoteJWKSet caches keys and refreshes on a new kid, so do not fetch it separately on every request. Keep the issuer, audience, algorithm, type, origin, App id, and semantic claim checks even though the signature is valid. Accepting a correctly signed JWT for another WAMP service is still an authorization bug.

Verify the bearer before parsing or executing the MCP request. Then apply your own ACL:

Result HTTP response
Missing, malformed, expired, or semantically invalid bearer 401 and, optionally, WWW-Authenticate: Bearer error="invalid_token"
WAMP JWKS cannot be fetched or parsed and no usable cached key exists 503; never mislabel an authority outage as invalid_token
Valid identity without access to the requested document or operation 403
Valid identity and allowed operation Continue into the normal MCP handler

Return real HTTP 401/403 responses rather than a successful JSON-RPC response containing an authentication error. WAMP uses the HTTP status to retire a rejected managed connection. It does not launch MCP OAuth for a wampAuth connector. The user can open Settings → MCP and click Retry to request a fresh managed token; sign-in, organization switch, and WAMP credential rotation also rebuild the connection. A persistent 401 remains a connection error until the service accepts the fresh token or the organization restores its approval.

Verify the bearer on every HTTP request. For SSE, Streamable HTTP GET, or any other long-lived response, close the response and its server-side MCP session no later than the verified exp. WAMP also rotates its client transport before expiry, but your service must enforce the JWT boundary itself. Offline verification means installation or membership revocation is bounded by the token’s remaining lifetime.

Rotation retires the managed transport before expiry, so a tool call still in flight at that boundary can end with a transport error. Make mutating tools idempotent and safe to retry; WAMP does not silently replay an ambiguous call.

Do not redirect an authenticated MCP request. WAMP refuses every 3xx on this path so a bearer cannot cross origins, and it does not replay an ambiguous mutating MCP request. Serve the final MCP endpoint at the URL in the manifest.

Runtime behavior after the connector is present

Section titled “Runtime behavior after the connector is present”
Runtime Identity and network path
Desktop WAMP silently exchanges the current Desktop OIDC grant. The local engine calls your MCP URL from the user’s machine. Login, logout, organization switch, or credential rotation retires the old identity-bound connection.
Cloud native agent When the connector manifest is included in the Cloud engine image, the control plane reauthorizes the Session’s durable execution owner and installs the JWT in the sandbox engine’s memory. The sandbox or organization runner calls your MCP URL directly; the Account service is not a traffic proxy.
Cloud ACP agent With that same image requirement, the sandbox engine connects to your service, discovers the tool, and lends a credential-free WAMP MCP facade to the ACP process. Tool calls return through that facade. The ACP agent never receives your URL credential or direct upstream connection.

Installing a connector from Desktop Marketplace does not currently copy it into managed Cloud sandboxes. A WAMP operator must add its unchanged manifest to the Cloud engine image. After that delivery step, your DNS, TLS, firewall, and routing must work from the selected sandbox runner. A private service reachable only on an organization’s network can use that organization’s self-hosted runner; managed sandboxes need a route to the service. Network source addresses are not identity — always authorize the JWT.

Cloud keeps the Session execution owner as the connected-resource subject. Sharing a Session intentionally shares that sandbox’s capabilities; unrelated Sessions and organizations do not reuse the same lease. Design agent behavior so an optional or temporarily unavailable tool may be absent.

A Cloud workspace may require at most eight distinct managed (audience, origin) pairs. On the first tool snapshot for a new configuration or credential set, WAMP waits up to five seconds for their MCP discovery; a slower or failed optional server is absent from that snapshot and continues connecting in the background.

  1. Register a staging audience and HTTPS origin in Account Center. Create an identity-only installation link and approve it for a test organization. Keep ALL_MEMBERS for the first smoke test.
  2. Deploy the example MCP service at the exact manifest URL and configure the five service values listed above. Confirm /healthz answers before involving WAMP.
  3. Import the connector zip through Marketplace → Add → Import package…, sign into WAMP Desktop, select the test organization, and enable the extension. You do not extract or paste a bearer.
  4. Ask a native agent to call wamp_identity. Confirm the service sees the expected iss, org_id, pairwise sub, resource_app_id, and resource_origin.
  5. Run the same call through a Desktop ACP runtime whose manifest lends the mcp tool category. Confirm the service sees the same principal and no ACP credential.
  6. If a WAMP operator has included the connector in the Cloud image, run the same native and ACP calls from a Cloud Session. This is not a self-service extension installation step today.
  7. Change the installation to ASSIGNED_MEMBERS, exclude the test member, and confirm a fresh Desktop login or Cloud Session cannot obtain the tool. A bearer already issued to an existing Session remains valid only until its exp.

Log claim identifiers and authorization decisions, never the bearer. Treat the pairwise sub as a pseudonymous user identifier and protect it like other account data.