Skip to content

feat(agent-memory): multitenancy via per-tenant bindings#232

Draft
cassiofariasmachado wants to merge 7 commits into
mainfrom
feat/enable-multitenancy-support-for-agent-memory-using-bindings
Draft

feat(agent-memory): multitenancy via per-tenant bindings#232
cassiofariasmachado wants to merge 7 commits into
mainfrom
feat/enable-multitenancy-support-for-agent-memory-using-bindings

Conversation

@cassiofariasmachado

@cassiofariasmachado cassiofariasmachado commented Jul 15, 2026

Copy link
Copy Markdown
Member

Disclaimer: Do not include SAP-internal or customer-specific information in this PR (e.g. internal system URLs, customer names, tenant IDs, or confidential configurations). This is a public repository.

Description

Adds multitenancy support to the agent_memory module using a per-tenant binding approach. This is a temporary implementation while the Hana Agent Memory service does not yet support native multitenancy via token scoping.

How it works

The runtime provisions one dedicated service instance per tenant and mounts its binding at:

  • /etc/secrets/appfnd/hana-agent-memory/default/ — provider
  • /etc/secrets/appfnd/hana-agent-memory/<tenant-subdomain>/ — one per subscriber tenant

Instead of deriving a subscriber token URL from the provider's binding (as in PR #222), this branch loads each tenant's own binding and creates a dedicated HttpTransport per tenant, lazily cached.

Public API

from sap_cloud_sdk.agent_memory import create_client, AccessStrategy

# Client-level default — binding loaded from /…/acme-corp/ at init time
client = create_client(
    access_strategy=AccessStrategy.SUBSCRIBER,
    tenant="acme-corp",
)
memories = client.list_memories(agent_id="x", invoker_id="u")

# Per-call override — loads beta-corp binding lazily on first call
memories = client.list_memories(
    agent_id="x", invoker_id="u",
    access_strategy=AccessStrategy.SUBSCRIBER,
    tenant="beta-corp",
)

# Provider-only — no tenant isolation, emits a logger.warning()
client = create_client(access_strategy=AccessStrategy.PROVIDER)

The public API is intentionally identical to PR #222 so that once the Agent Memory team adds native multitenancy, only the internal mechanics change — zero changes to caller code.

Code review changes

  • AccessStrategy.SUBSCRIBER_ONLY / PROVIDER_ONLY renamed to SUBSCRIBER / PROVIDER for brevity
  • logger.warning() emitted whenever AccessStrategy.PROVIDER is resolved, making the no-isolation behaviour explicit at runtime

Related Issue

N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Dependency update

How to Test

  1. Unit tests: pytest tests/agent_memory/unit/ — 260 tests pass
  2. Integration tests (provider only): pytest tests/agent_memory/integration/ — 14 pass, 12 skip
  3. Integration tests with subscriber binding configured:
    CLOUD_SDK_CFG_HANA_AGENT_MEMORY_<TENANT>_APPLICATION_URL=...
    CLOUD_SDK_CFG_HANA_AGENT_MEMORY_<TENANT>_UAA=...
    CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_SUBSCRIBER_TENANT=<tenant>
    pytest tests/agent_memory/integration/
    

Checklist

  • I have read the Contributing Guidelines
  • I have verified that my changes solve the issue
  • I have added/updated automated tests to cover my changes
  • All tests pass locally
  • I have verified that my code follows the Code Guidelines
  • I have updated documentation (if applicable)
  • I have added type hints for all public APIs
  • My code does not contain sensitive information (credentials, tokens, etc.)
  • I have followed Conventional Commits for commit messages

Breaking Changes

create_client() with no args raises AgentMemoryConfigError (default SUBSCRIBER requires tenant).

Migration:

# Subscriber
client = create_client(tenant="my-subdomain")

# Provider (no isolation — use only for provider-owned operations)
client = create_client(access_strategy=AccessStrategy.PROVIDER)

Additional Notes

  • _load_config_for_instance(instance) in config.py mirrors the destination module's load_from_env_or_mount(instance) — uses the secret resolver with the tenant subdomain as the instance name.
  • AgentMemoryClient holds _provider_transport + _subscriber_transport_cache (dict keyed by subdomain). _get_transport(strategy, tenant) selects the right transport per call.
  • The identityzone field remains in AgentMemoryConfig for forward compatibility with PR feat(agent-memory): add access_strategy and tenant params for multitenancy #222's token-URL-swap approach, but plays no role in this branch.
  • Subscriber integration tests skip gracefully when the tenant binding is not mounted/configured.
  • AccessStrategy.PROVIDER emits logger.warning() on every use to surface the no-isolation risk at runtime.

- create_client() and AgentMemoryClient.__init__ accept access_strategy
  (default SUBSCRIBER_ONLY) and tenant as constructor params
- _resolve_tenant is now an instance method; per-call params override
  instance defaults
- All method signatures change to Optional[AccessStrategy] = None
  (sentinel: use instance default)
- user-guide: add client-level configuration section and PROVIDER_ONLY
  isolation warning
- tests: update TestAccessStrategy and TestCreateClient for instance method
- Add _load_config_for_instance(instance) to config.py — loads secrets
  from /etc/secrets/appfnd/hana-agent-memory/{instance}/ following the
  same pattern as the destination module
- Refactor AgentMemoryClient to maintain a per-tenant HttpTransport cache:
  provider binding ('default') and subscriber bindings (one per tenant)
  are each loaded into their own transport and lazily cached
- create_client() selects the initial binding based on access_strategy and
  tenant, and wires config_loader for per-call tenant override support
- Integration test conftest now skips subscriber tests when the tenant
  binding itself is not configured (not just the tenant name env var)
- Unit tests cover _load_config_for_instance, _get_transport routing,
  per-call binding cache, and create_client() factory wiring
Comment thread src/sap_cloud_sdk/agent_memory/__init__.py Outdated
Comment thread src/sap_cloud_sdk/agent_memory/user-guide.md Outdated
@cassiofariasmachado cassiofariasmachado changed the title feat(agent-memory): multitenancy via per-tenant bindings (temporary) feat(agent-memory): multitenancy via per-tenant bindings Jul 15, 2026
… warning

- SUBSCRIBER_ONLY → SUBSCRIBER, PROVIDER_ONLY → PROVIDER across all
  agent_memory source, tests, feature files, and user guide
- Add logger.warning() when AccessStrategy.PROVIDER is used, making
  the no-isolation behaviour explicit at runtime
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants