PromptisePromptise
Docs
GitHub
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Runtime, triggers and deployment

AI Agent Secrets Management: Scoped, Expiring Credentials

Manage AI agent secrets safely: scope credentials per agent, expire and rotate them, keep them out of prompts and logs, and audit each access. Real output.

Level
Intermediate
Reading time
17 min
Published
Oct 19, 2026
Last updated
Oct 19, 2026
Tested with
Promptise 1.4.3
By
Promptise Team
  • Agent Runtime
  • System Prompt Leakage
  • Authentication and Access Control
  • Deployment
Contents · 15 sections↓
  1. 01How do you manage secrets for AI agents?
  2. 02How agent secrets management works
  3. 03When to use scoped secrets (and when not to)
  4. 04What you need
  5. 05Step 1: Declare scoped secrets for a process
  6. 06Step 2: Read, expire, rotate and revoke
  7. 07Step 3: Keep secrets away from the model
  8. 08Step 4: Rotate over the API and watch an agent fetch a secret
  9. 09Step 5: Redact secret values from output
  10. 10The complete example
  11. 11Running it in production
  12. 12Common problems and fixes
  13. 13Limitations
  14. 14Frequently asked questions
  15. 15Next steps

AI agent secrets management is about one question: which credentials can each agent use, for how long, and who can see them? An agent that runs unattended calls billing APIs, ticket systems and databases. If every agent reads every key from one shared environment, a single prompt injection or a careless log line exposes all of them. This guide shows how to scope secrets per agent with Promptise Foundry, expire and rotate them, keep them out of prompts and logs, and audit every access.

[01]

How do you manage secrets for AI agents?

Give each agent only the credentials it needs, keep the values out of the model's context, and make every use visible. Load values from the environment when the agent starts, hold them in a per-agent store with an expiry, rotate without a restart, log each access by name, and wipe them on stop. In the Promptise agent runtime, a SecretScopeConfig on the process does this:

Python
1
2
3
4
5
6
7
8
9
10
11
from promptise.runtime import ProcessConfig, SecretScopeConfig

config = ProcessConfig(
    model="openai:gpt-5-mini",
    instructions="Answer billing tickets.",
    secrets=SecretScopeConfig(
        enabled=True,
        secrets={"billing_key": "${BILLING_API_KEY}"},  # read from the environment at start
        default_ttl=8 * 3600,                            # expires after 8 hours
    ),
)

Key takeaways:

  • The best secret is one the model never sees. Put tool credentials in the MCP server or its connection spec, not in the conversation.

  • Scope per agent. Each process gets its own encrypted, in-memory store with only its own secrets.

  • Expire and rotate. TTLs bound how long a leaked value works, and the API rotates a value at once.

  • Audit by name. Every read, rotation, expiry and revocation is journaled without the value.

  • Redaction is your job. The runtime gives you sanitize_text(), but doesn't apply it to replies or logs for you.


[02]

How agent secrets management works

Tools need credentials to call APIs. The model almost never does: it decides which tool to call, and the tool carries the key. Problems start when a key travels through the model, because anything the model sees can end up in a reply, a log, a trace or a tool call to the wrong place. The secrets in prompts guide explains why system prompts leak.

Text
1
2
3
4
5
6
7
8
9
10
11
12
 secret manager / platform (Vault, Kubernetes, Docker, CI)
          │  injects environment variables
          ▼
 process start ── resolve ${VAR} ──▶ SecretScope (per process, in memory, encrypted)
          │                              │  get(name)     → value, logged "access"
          │                              │  TTL passed    → None,  logged "expired"
          │                              │  rotate()      → new value, logged "rotation"
          │                              │  stop / DELETE → zero-filled, logged "revoke_all"
          │                              │
          │                     open mode only: get_secret tool ──▶ value enters the model's context
          ▼
 MCP server specs (bearer_token, headers, env) ──▶ tool credentials the model never sees

The Promptise SecretScope is the per-process store. At start it reads each configured secret, a literal or an ${ENV_VAR} reference, and keeps it encrypted in memory with a key that exists only in that process. A secret can expire after a time-to-live (TTL). Every read is journaled as a secret_access entry with the name and action, never the value. On stop, the scope overwrites and deletes every value.

The scope itself never writes values into the system prompt, context state, status or journal. The model reaches a scoped value only through the get_secret meta-tool, which exists only in open mode. MCP server credentials are a separate path, and the model never sees them.


[03]

When to use scoped secrets (and when not to)

Situation

Use

Why

A tool needs an API key

The MCP server's own config or the server spec

The model never handles the key

Several agents on one host need different keys

SecretScopeConfig per process

Each agent sees only its own names

A credential should stop working after a shift or a job

default_ttl / ttls

Expiry limits a leaked value's lifetime

You rotate keys on a schedule

The rotate route of the API

New value at once, no restart

An open-mode agent must pass a token to a tool it built itself

get_secret

The only way the model reads a scoped value; treat it as exposure

Secrets for a single short script

Plain environment variables

A scope adds little when one agent runs once


[04]

What you need

  • Python 3.10 or later and Promptise Foundry 1.4.3 or later.

  • An OpenAI API key in OPENAI_API_KEY for step 4, which makes one gpt-5-mini call. Steps 2 and 3 need no key.

  • About 15 minutes.

>_Terminal
pip install "promptise>=1.4.3"

[05]

Step 1: Declare scoped secrets for a process

SecretScopeConfig has five fields:

Field

Default

Effect

enabled

False

Creates the scope (and the get_secret tool in open mode)

secrets

JSON
{}

Name to value: a literal, or ${ENV_VAR} as the whole string

default_ttl

None

Seconds until each secret expires; None means never

ttls

JSON
{}

Per-name TTL overrides in seconds

revoke_on_stop

True

Documented as "revoke on stop"; in 1.4.3 every stop revokes, whatever its value

An ${ENV_VAR} reference must be the entire value, and it is resolved when the process starts. A missing variable raises KeyError and the process moves to FAILED, so a deploy without its secrets fails loudly instead of running with blanks. Anything else is kept as literal text, including "Bearer ${TOKEN}"; store the bare token and add the prefix in code.

The same block works in a .agent manifest:

YAML
1
2
3
4
5
6
7
8
name: support-agent
model: openai:gpt-5-mini
instructions: Answer support tickets.
secrets:
  enabled: true
  secrets:
    tickets_token: "${TICKETS_TOKEN}"
  default_ttl: 28800

Manifests differ in one way: the loader resolves every ${VAR} in the file at load time, including ${VAR:-default} and references inside longer strings. The process config then holds the plain value, and a missing variable fails with EnvVarNotFoundError. Never commit literal values to a manifest.


[06]

Step 2: Read, expire, rotate and revoke

SecretScope works on its own, so you can see each operation without a process or a model:

Pythonscope_basics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""SecretScope on its own: resolve, read, expire, rotate, redact, revoke, audit. No model, no API key."""
import asyncio
import logging
import os

from promptise.runtime import InMemoryJournal, SecretScope, SecretScopeConfig

logging.disable(logging.WARNING)

# In production these come from your deployment (a secret manager, Kubernetes, Docker secrets).
os.environ["BILLING_API_KEY"] = "sk-billing-7f3a91"
os.environ["TICKETS_TOKEN"] = "tk-0c55e2"


async def main():
    journal = InMemoryJournal()
    scope = SecretScope(
        SecretScopeConfig(
            enabled=True,
            secrets={
                "billing_key": "${BILLING_API_KEY}",  # read from the environment
                "tickets_token": "${TICKETS_TOKEN}",
            },
            default_ttl=3600,             # one hour for every secret ...
            ttls={"tickets_token": 1.0},  # ... except this one: one second
        ),
        process_id="billing-agent",
        journal=journal,
    )
    await scope.resolve_initial()
    print("names:        ", scope.list_names())
    print("billing_key:  ", scope.get("billing_key"))

    await asyncio.sleep(1.2)
    print("tickets_token after its TTL:", scope.get("tickets_token"))
    print("names now:    ", scope.list_names())

    await scope.rotate("billing_key", "sk-billing-NEW-22b0")
    print("after rotate: ", scope.get("billing_key"))

    reply = "Charged with key sk-billing-NEW-22b0, old key sk-billing-7f3a91."
    print("sanitized:    ", scope.sanitize_text(reply))

    try:
        await SecretScope(SecretScopeConfig(enabled=True, secrets={"x": "${NOT_SET}"}), "p").resolve_initial()
    except KeyError as exc:
        print("missing env:  ", exc)

    short = SecretScope(SecretScopeConfig(enabled=True, secrets={
        "token": "${TICKETS_TOKEN}",
        "header": "Bearer ${TICKETS_TOKEN}",  # not a reference: kept as literal text
    }, default_ttl=1.0), "p")
    await short.resolve_initial()
    print("header:       ", short.get("header"))
    await asyncio.sleep(0.6)
    await short.rotate("token", "tk-rotated-91aa")  # the expiry is not reset
    await asyncio.sleep(0.6)
    print("rotated token after the first TTL:", short.get("token"))

    await scope.revoke_all()
    print("after revoke: ", scope.list_names(), scope.get("billing_key"))

    await asyncio.sleep(0)  # access-log writes are fire-and-forget tasks
    print("\naccess log (names only, never values):")
    for entry in await journal.read("billing-agent", entry_type="secret_access"):
        print(" ", entry.data)


asyncio.run(main())
Output
names:         ['billing_key', 'tickets_token']
billing_key:   sk-billing-7f3a91
tickets_token after its TTL: None
names now:     ['billing_key']
after rotate:  sk-billing-NEW-22b0
sanitized:     Charged with key [REDACTED], old key sk-billing-7f3a91.
missing env:   "Environment variable 'NOT_SET' not found (required by secret config)"
header:        Bearer ${TICKETS_TOKEN}
rotated token after the first TTL: None
after revoke:  [] None

access log (names only, never values):
  {'action': 'access', 'secret_name': 'billing_key'}
  {'action': 'expired', 'secret_name': 'tickets_token'}
  {'action': 'rotation', 'secret_name': 'billing_key'}
  {'action': 'access', 'secret_name': 'billing_key'}
  {'action': 'revoke_all', 'secret_name': '*'}

What each line shows:

  • Expiry is enforced on read. After its one-second TTL, tickets_token returns None and disappears from the name list, and the journal records expired. Code that calls get() must handle None.

  • Rotation is immediate. The next get() returns the new value, and the journal records rotation.

  • Rotation keeps the original expiry. The rotated token still expired at the end of its first TTL. TTLs count from process start, so a rotation can't extend a secret's life. A restart does: it resolves the environment again and starts fresh TTLs.

  • Redaction covers current values only. sanitize_text() replaced the new billing key but not the old one, which the scope no longer holds.

  • Literals stay literal. "Bearer ${TICKETS_TOKEN}" was not resolved.

  • Revocation empties the scope and is logged as revoke_all with the name *.


[07]

Step 3: Keep secrets away from the model

Most credentials should never enter the scope, because the model doesn't need them. The tool needs the key; the agent only needs the tool. Give the credential to the MCP server, in its own environment or in the spec the agent connects with:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os

from promptise.config import HTTPServerSpec, StdioServerSpec
from promptise.runtime import ProcessConfig

config = ProcessConfig(
    model="openai:gpt-5-mini",
    instructions="Answer billing tickets.",
    servers={
        # Sent as an Authorization header on every request; stored as a SecretStr.
        "billing": HTTPServerSpec(
            url="https://billing.internal.example/mcp",
            bearer_token=os.environ["BILLING_MCP_TOKEN"],
        ),
        # A local server reads its key from its own environment.
        "tickets": StdioServerSpec(
            command="python",
            args=["tickets_server.py"],
            env={"TICKETS_API_KEY": os.environ["TICKETS_API_KEY"]},
        ),
    },
)

The MCP client sends bearer_token as a header, and the model sees only the tools and their results. A stdio server starts with only the environment you pass in env. This path doesn't use SecretScope, so it has no TTL or access log; the server's own logs cover that. For servers that accept short-lived tokens tied to the agent's identity instead of static keys, see AI agent identity and authentication.

Use SecretScope for the rest, such as open-mode agents that build their own tools.


[08]

Step 4: Rotate over the API and watch an agent fetch a secret

This script runs a process in open mode with one scoped secret and a full journal. It lists and rotates the secret over the management API, lets the agent fetch it with get_secret in one real run, then reads the journal:

Pythonsecret_process.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""A process with scoped secrets: rotate over the API, let an open-mode agent fetch one, read the audit trail."""
import asyncio
import logging
import os
import secrets

import httpx
from promptise.runtime import (
    AgentRuntime,
    ExecutionMode,
    JournalConfig,
    OrchestrationAPI,
    OrchestrationClient,
    ProcessConfig,
    SecretScopeConfig,
    TriggerEvent,
)

logging.disable(logging.WARNING)
os.environ["TICKETS_TOKEN"] = "tk-0c55e2"  # set by your deployment in production


async def main():
    runtime = AgentRuntime()
    process = await runtime.add_process("support-agent", ProcessConfig(
        model="openai:gpt-5-mini",
        instructions=(
            "You are a support agent. When a run asks for it, fetch the tickets_token "
            "secret with get_secret and report only how many characters it has. "
            "Never write a secret value in your reply."
        ),
        execution_mode=ExecutionMode.OPEN,  # adds the get_secret meta-tool
        journal=JournalConfig(level="full", backend="memory"),
        secrets=SecretScopeConfig(
            enabled=True,
            secrets={"tickets_token": "${TICKETS_TOKEN}"},
            default_ttl=8 * 3600,  # one working day; restart or rotate before it ends
        ),
    ))
    token = secrets.token_urlsafe(24)
    api = OrchestrationAPI(runtime, port=9100, auth_token=token)
    await api.start()
    await process.start()

    async with OrchestrationClient("http://127.0.0.1:9100", token=token) as client:
        print("secrets:  ", await client.list_secrets("support-agent"))
        print("rotate:   ", await client.rotate_secret("support-agent", "tickets_token", "tk-rotated-4be1"))
        try:
            await client.rotate_secret("support-agent", "tickets-token", "typo")
        except httpx.HTTPStatusError as exc:
            print("bad name: ", exc.response.status_code, exc.response.json()["error"])

        await process.inject(TriggerEvent(
            trigger_id="ops", trigger_type="manual", payload={"task": "check the tickets token"}))
        while process.status()["invocation_count"] == 0:
            await asyncio.sleep(0.2)

        print("\njournal after one run:")
        for entry in await client.get_journal("support-agent", limit=20):
            if entry["entry_type"] in ("secret_access", "tool_call", "tool_result", "invocation_result"):
                data = entry["data"]
                shown = data.get("response") or data.get("result") or data
                print(f"  {entry['entry_type']:<17} {shown}")

        print("\naccess log:")
        for entry in await client.get_journal("support-agent", entry_type="secret_access"):
            print(f"  {entry['timestamp']}  {entry['data']['action']:<9} {entry['data']['secret_name']}")

        print("\nstatus:   ", {k: v for k, v in (await client.get_process("support-agent")).items()
                               if k in ("state", "active_secrets")})
        await process.stop()
        print("stopped:  ", {k: v for k, v in (await client.get_process("support-agent")).items()
                               if k in ("state", "active_secrets")})

    await api.stop()


asyncio.run(main())
Output
secrets:   [{'name': 'tickets_token', 'active': True}]
rotate:    {'name': 'support-agent', 'secret_rotated': 'tickets_token'}
bad name:  404 {'code': 'PROCESS_NOT_FOUND', 'message': "Process 'support-agent' not found"}

journal after one run:
  secret_access     {'action': 'rotation', 'secret_name': 'tickets_token'}
  tool_call         {'tool': 'get_secret', 'args': {'name': 'tickets_token'}}
  secret_access     {'action': 'access', 'secret_name': 'tickets_token'}
  tool_result       tk-rotated-4be1
  invocation_result 15 characters.

access log:
  2026-10-19 17:59:19.092567+00:00  rotation  tickets_token
  2026-10-19 17:59:21.865077+00:00  access    tickets_token

status:    {'state': 'running', 'active_secrets': 1}
stopped:   {'state': 'stopped', 'active_secrets': 0}

The API never returns values. GET /api/v1/processes/{name}/secrets lists names and whether each is active. PATCH /api/v1/processes/{name}/secrets/{secret} with {"value": "..."} rotates one at once, and DELETE /api/v1/processes/{name}/secrets revokes all of them while the process keeps running. Rotating a name that doesn't exist answers 404 with the misleading code PROCESS_NOT_FOUND, as the "bad name" line shows. The client's rotate_secret() accepts a ttl argument, but the server ignores it. The API itself is covered in the AI agent control API.

`get_secret` puts the value in the model's context. The agent called the tool, the scope logged an access, and the tool returned the rotated token as its result. The model followed its instructions and replied with only the length. But the value was in its context for that run, and with JournalConfig(level="full") the runtime journals every tool result, so tk-rotated-4be1 is now in the journal in plain text. With the default file backend, that is a file on disk.

Treat every get_secret call as exposure:

  • Prefer tool-side credentials from step 3, so no tool call needs the value.

  • Don't combine `get_secret` with `level="full"` journals, or write them to storage that you protect like the secrets themselves.

  • Keep TTLs short for any secret an open-mode agent can read, and rotate after incidents.

  • Remember the reply. If an injection makes the agent repeat the value, it lands in the conversation buffer, the invocation_result entry and checkpoints.

The access log is a journal filter. get_journal(name, entry_type="secret_access") returns one timestamped entry per rotation and read, by name only.

Stopping revokes. After stop(), active_secrets dropped to 0. The next start() resolves the environment again.


[09]

Step 5: Redact secret values from output

sanitize_text(text) replaces every value the scope currently holds with [REDACTED]. The runtime doesn't call it on replies, journal entries or events, so call it wherever text leaves your control, such as before you forward a reply or ship logs. It is an exact string match, so three cases slip through:

  • Old values. After a rotation, the previous value is no longer in the scope, as step 2 showed.

  • Changed values. A key split across lines, encoded in base64 or partly masked isn't matched.

  • Secrets the scope doesn't hold, such as a server spec's token or a key a tool returned.

As a second layer, the runtime's guardrails option can scan output for credential-shaped strings; see AI agent guardrails.


[10]

The complete example

The project has two files: scope_basics.py from step 2 and secret_process.py from step 4.

Text
secrets-demo/
├── scope_basics.py     # SecretScope alone: resolve, expire, rotate, redact, revoke, audit
└── secret_process.py   # process + API: list, rotate, get_secret in open mode, journal
>_Terminal
pip install "promptise>=1.4.3"
python scope_basics.py
export OPENAI_API_KEY=sk-...
python secret_process.py

[11]

Running it in production

Inject values from a secret manager

Promptise reads secrets from environment variables and has no built-in client for HashiCorp Vault or AWS Secrets Manager. Let the platform deliver them: a Kubernetes Secret or the External Secrets Operator mapped into the container environment, Vault Agent writing an env file, or Docker secrets loaded by your entrypoint. For dynamic credentials that change while the agent runs, have a small rotation job fetch the new value and call the rotate route of the API. For container setups, see deploy an AI agent with Docker.

Size TTLs to restarts

A TTL can't be extended at runtime: rotation keeps the original expiry, and an expired secret stays gone until a restart reads the environment again. A restart within the same OS process re-reads the same environment, so values your platform changed arrive only with a new container or pod. Pick a TTL longer than your redeploy cycle, or plan a restart before it ends.

Protect the journal and the API

The access log lives in the process journal, by default files under .promptise/journal, which also hold tool results at level="full". Restrict the directory like a credentials store. The management API can rotate and revoke secrets, so give it a strong bearer token and bind it to localhost or a private network.

One process, one set of keys

Split work across processes by trust level: the agent that reads web pages shouldn't hold the payments key. Running multiple AI agents shows how one runtime hosts several processes, each with its own config. Combine scoping with approvals for actions that spend money, as in human-in-the-loop approval.


[12]

Common problems and fixes

Symptom

Cause

Fix

Process fails at start with KeyError: Environment variable ... not found

A ${VAR} reference has no value in the environment

Inject the variable, or fix its name

A header contains the literal text ${TOKEN}

${VAR} must be the whole value in SecretScopeConfig

Store the bare token, add Bearer in code

get() returns None mid-run

The TTL passed; rotation doesn't reset it

Restart the process, or use a longer TTL

Rotating over the API answers 404 PROCESS_NOT_FOUND

The secret name is wrong or the secret expired

Check GET .../secrets; the process is fine

revoke_on_stop=False still wipes secrets

Every stop revokes in 1.4.3

Expect a fresh resolve on every start

A secret value appears in the journal

get_secret result journaled at level="full", or the agent repeated it

Use tool-side credentials, or a lower journal level

sanitize_text() misses a key

The key was rotated away, transformed, or never in the scope

Redact known patterns too, and keep keys away from the model

The ttl argument of rotate_secret() has no effect

The server ignores it

Set TTLs in the config


[13]

Limitations

  • Environment variables and literals only. No native secret-manager integration and no automatic refresh; rotation is a push through the API.

  • Static TTLs. They start when the process starts and can't be extended without a restart.

  • No automatic redaction. sanitize_text() exists, but you call it, and it only matches current values exactly.

  • `get_secret` exposes values to the model. There is no "use but don't reveal" mode. Strict-mode tools can't read the scope; tool credentials go in server specs.

  • Memory hygiene is best effort. Values are encrypted in memory and zero-filled on revoke, but decrypted copies may linger until garbage collection.

  • Manifest values are resolved at load. The process config then holds them in plain text for the process's life.


[14]

Frequently asked questions

How should AI agents store API keys?

Keep API keys in the tool, not the agent. An MCP server holds its own key or receives it through the connection spec, so the model only sees tool names and results. For credentials the agent's process must handle, use a per-agent scope that loads values from the environment at start, keeps them in memory, expires them and logs every access by name.

Can an LLM see the secrets I give my agent?

Only if they reach its context. Values in a Promptise SecretScope never go into the system prompt, state or logs on their own. An open-mode agent can read one with the get_secret tool, and then the value is in that run's context like any tool result. An MCP server's bearer token never reaches the model.

How do I rotate an AI agent's credentials without a restart?

Send the new value to PATCH /api/v1/processes/{name}/secrets/{secret} with a JSON body {"value": "..."}, or call rotate_secret() on the Python client. The next read returns the new value. Rotation keeps the secret's original expiry, so a secret close to its TTL still expires on time; restart the process to reload the environment and start fresh TTLs.

What happens when an agent secret expires?

The next read returns None, the secret disappears from the name list, and the journal records an expired entry. A tool or get_secret call that needs it fails with "not found or expired". The value doesn't come back by itself: restart the process so it resolves the environment again, or choose a TTL that outlasts your restart cycle.

How do I use HashiCorp Vault with an AI agent?

Let Vault deliver values as environment variables, for example through Vault Agent templates or the External Secrets Operator on Kubernetes, and reference them as ${VAR} in the secret config. For dynamic credentials, run a job that reads the new lease from Vault and pushes it to the agent with the rotate route of the management API. Promptise has no built-in Vault client.

How do I audit which secrets an agent used?

Every read, rotation, expiry and revocation is a secret_access entry in the process journal, with the action and the secret name but never the value. Read them with GET /api/v1/processes/{name}/journal?type=secret_access or the client's get_journal(name, entry_type="secret_access"). The journal is on by default, as files under .promptise/journal.


[15]

Next steps

  • Why secrets in prompts leak: secrets in prompts.

  • Replace static keys with workload identity: AI agent identity and authentication.

  • Rotate and revoke over HTTP: the AI agent control API.

  • Understand open mode before you enable get_secret: self-modifying AI agents.

  • Secure the MCP servers that hold the keys: MCP security best practices.

  • Reference: secret scoping and the orchestration API in the Promptise docs.

Contents

  1. 01How do you manage secrets for AI agents?
  2. 02How agent secrets management works
  3. 03When to use scoped secrets (and when not to)
  4. 04What you need
  5. 05Step 1: Declare scoped secrets for a process
  6. 06Step 2: Read, expire, rotate and revoke
  7. 07Step 3: Keep secrets away from the model
  8. 08Step 4: Rotate over the API and watch an agent fetch a secret
  9. 09Step 5: Redact secret values from output
  10. 10The complete example
  11. 11Running it in production
  12. 12Common problems and fixes
  13. 13Limitations
  14. 14Frequently asked questions
  15. 15Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Runtime, triggers and deployment

Related guides

All guides in this topic →
  • // 01Intermediate · 19 minMCP Long-Running Tasks: Background Jobs in PythonRun slow MCP tools as background jobs in Python: submit, poll progress, retry failures and cancel, with a real agent, real runs and honest limits.
  • // 02Beginner · 15 minHow to Run, Debug and Deploy an AI Agent in PythonRun, debug and deploy a Python AI agent project with one command: a package.json-style manifest, live traces, a terminal UI, JSON logs and an API.
  • // 03Intermediate · 15 minDeploy AI Agents in Production: Long-Running Python AgentsRun an AI agent as a long-lived Python process: cron and webhook triggers, state between runs, a journal, budgets that pause it, and honest limits.
  • // 04Intermediate · 18 minHow to Deploy an AI Agent with Docker (2026 Guide)Deploy an AI agent with Docker: a Dockerfile, env config and secrets, health checks, graceful shutdown and docker compose with an MCP server. Tested.
  • // 05Intermediate · 17 minAI Agent Production Checklist: What to Check Before LaunchAn AI agent production checklist: security, approvals, guardrails, limits, cost, observability, evals, deployment and incident response, with code.
  • // 06Intermediate · 18 minAI Agent Code Execution Sandbox: Run LLM Code SafelyLet an AI agent write and run Python in a Docker sandbox: analyse a CSV, try real escape attempts, set limits, and learn where the sandbox stops.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths