> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Credential Isolation

> The caller that proposes an action never holds the credential that executes it. Session credentials are issued fresh, scoped to one call, and consumed once.

<Info>**\[AVAILABLE] as a mechanism, \[PARTIAL] as a system-wide guarantee.** `packages/execution-control`, `packages/connector-sdk`. See the scope note below before treating this as true of every connector.</Info>

## What it is

Credential resolution happens exclusively inside the gateway boundary, after every
[gateway](/concepts/the-gateway) check has passed. The caller that proposed the action, the
Runtime, and the Policy Engine never see the credential a connector uses to actually act.

```typescript theme={null}
// packages/execution-control/src/SessionCredentialVault.ts
interface SessionCredentialVault {
  issue(...): Promise<SessionCredential>;   // single-use, time-bounded lease
  consume(sessionCredentialId: string): Promise<ExecutionCredential>;  // only once
  revoke(sessionCredentialId: string): Promise<void>;
}
```

## Why it exists

If the entity proposing an action, increasingly an AI agent, ever held a live, reusable
credential, every guarantee upstream (deterministic policy, signed authorization, gateway
re-verification) would still leave that credential extractable and reusable outside the
authorized flow. Credential isolation closes that gap structurally: there is no code path
where a proposer receives a credential, because credentials are never routed to it in the
first place.

## How it behaves

```typescript theme={null}
export interface CredentialProvider {
  readonly providerId: string;
  resolve(connectorId: string): Promise<CredentialHandle>;
}
```

A `CredentialHandle` is opaque, resolved material, `{ providerId, credentialId, value }`.
`CredentialVaultAdapter` adapts any `CredentialProvider` into `execution-control`'s
`CredentialVault` interface, so `InMemorySecureConnector` resolves credentials through it
exactly as it always has. Two generic providers ship in `@parmana/connector-sdk`:
`StaticCredentialProvider` (an in-memory map) and `EnvironmentCredentialProvider` (resolves
from `process.env` via a single connector-to-variable-name mapping). `StaticCredentialProvider`
is what production actually wires up for `NODE_ENV=test`; `EnvironmentCredentialProvider`
is real, tested library infrastructure with its own unit test
(`packages/connector-sdk/tests/unit/credential-provider.test.ts`), but neither of the two
connectors registered in production today uses it directly — see "Minimal example" below
for why.

A session credential is issued, consumed exactly once, and can be revoked:

```typescript theme={null}
// packages/execution-control/src/SessionCredentialVault.ts:86-110
async consume(sessionCredentialId: string): Promise<ExecutionCredential> {
  const record = /* ... */;
  if (record.revoked) {
    throw new Error(`Session credential has been revoked: ${sessionCredentialId}.`);
  }
  // ...
}
```

### What "never leaks" means, precisely

A `CredentialHandle`'s `value` necessarily carries the resolved secret, a connector (e.g.
`HttpConnector`) needs it to set an `Authorization` header. What must never happen is the
secret reaching anywhere durable or observable outside that one ephemeral use:

* **Never in `ConnectorEvidence`.** `buildConnectorEvidence` only ever reads
  `ConnectorRequest`/`ConnectorResponse` fields, never `ConnectorExecutionContext.credential`.
  `redactSensitiveKeys` additionally strips any response-metadata key that looks
  credential-shaped, as defense in depth against a connector author's mistake.
* **Never in a thrown error.** Both providers' failure messages name only identifiers
  (`connectorId`, environment variable *name*), never a resolved value. Tested in
  `packages/connector-sdk/tests/unit/credential-provider.test.ts`.
* **Never as a raw value reaching a Connector.** Every `CredentialHandle` is branded at
  creation (`brandCredentialHandle`); `SdkConnectorExecutor` rejects any credential that
  isn't a branded handle. Tested end-to-end in `gateway-integration.test.ts` ("rejects a raw
  (non-provider-resolved) credential").
* **Never in the Execution Trust Record.** Only `ConnectorEvidence` (never the handle
  itself) is placed on `ExecutionResult.metadata.connector`, see
  [Execution trust records](/concepts/execution-trust-records).

## Minimal example

Real production connectors write a small, dedicated `CredentialProvider` rather than
configuring the generic `EnvironmentCredentialProvider` directly — usually because the
credential shape needs a specific field name (Razorpay's `key_id`/`key_secret` pair isn't
expressible as `EnvironmentCredentialProvider`'s single connector-to-variable-name mapping
at all) or because a resolved field name needs to match the connector's own credential
type exactly. HubSpot's is the simplest real example (a single token), copied verbatim from
current source:

```typescript theme={null}
// packages/api/src/bootstrap/createHubSpotCredentialProvider.ts
class HubSpotEnvironmentCredentialProvider implements CredentialProvider {
  readonly providerId = "environment";

  async resolve(connectorId: string): Promise<CredentialHandle> {
    if (connectorId !== "hubspot") {
      throw new Error(
        `HubSpotEnvironmentCredentialProvider cannot resolve credentials for connector "${connectorId}".`,
      );
    }

    const privateAppToken = process.env.HUBSPOT_PRIVATE_APP_TOKEN;

    if (privateAppToken === undefined) {
      throw new Error(
        `Environment variable HUBSPOT_PRIVATE_APP_TOKEN for connector "hubspot" is not set.`,
      );
    }

    return brandCredentialHandle({
      providerId: this.providerId,
      credentialId: "HUBSPOT_PRIVATE_APP_TOKEN",
      value: Object.freeze({ privateAppToken }),
    });
  }
}
```

`CredentialVaultAdapter` wraps whichever provider a connector registration supplies; the
caller-facing pipeline (Runtime, Policy Engine) never touches it either way — this class
being bespoke rather than the generic `EnvironmentCredentialProvider` is an implementation
choice inside the credential-resolution boundary, not a different boundary.

<Warning>
  **Scope, precisely: this mechanism is real and tested, and in the default server it is
  exercised for the two connectors registered in production, `razorpay` and `hubspot`.**
  `packages/api/src/bootstrap/createRazorpayCredentialProvider.ts` and
  `createHubSpotCredentialProvider.ts` each construct a dedicated `CredentialProvider`
  implementation, registered against their respective connector
  (see [The gateway](/concepts/the-gateway)). The isolation guarantee (caller never sees the
  credential, single-use, revocable) is architecturally true for *any* connector routed
  through this seam, and is proven generically by the tests cited above, but making it true
  for a *new* (third) connector today means writing bootstrap code (a new `CredentialProvider`
  entry, a new registry registration), not configuration. There is no dynamic,
  general-purpose "any connector automatically gets isolated credentials" path yet. Treat the
  isolation guarantee as proven for the mechanism and for `razorpay`/`hubspot` specifically,
  not as a system-wide property of arbitrary connectors you haven't wired yet.

  A third connector, `vendor-payment`, was previously registered here as the reference
  example — it has since been removed from the repository entirely (not merely superseded),
  and `createCredentialProvider.ts` (the file this warning used to cite) no longer exists.
  See `docs/VERIFICATION-GAPS.md` G-27 in the source repository for the full account.
</Warning>

## \[FUTURE]: cloud secret managers

Only the `CredentialProvider` interface seam is defined. No integration exists yet for
HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or Google Secret Manager. Any of
these would implement `CredentialProvider` exactly like `EnvironmentCredentialProvider`
does, no cloud SDK dependency has been added to this package, and none is implemented in
this milestone.

## Next

<CardGroup cols={2}>
  <Card title="Gateway attestation" icon="stamp" href="/concepts/gateway-attestation">
    The request-bound signature that authenticates the gateway to a connector.
  </Card>

  <Card title="Add a connector with the Connector SDK" icon="plug" href="/integrations/connector-development-guide">
    What wiring a new connector into this seam actually requires.
  </Card>
</CardGroup>
