7 min read

Should locked vault pending requests wait at all?

Locked vault pending requests need an explicit failure path, fresh user context after unlock, narrow retry rules, and audit records that explain every denial.

Should locked vault pending requests wait at all?

An agent request that reaches a locked vault should fail then and there. It should not become a dormant command that springs back to life when someone unlocks the vault an hour later.

That sounds strict until you look at what an unlock actually proves. It proves that a person authorized access to a secret store at this moment. It does not prove that an old agent process still has the same task, that a deployment is still desirable, that a pull request has not changed, or that the old HTTP body and SSH command still make sense.

This is one of those decisions that gets disguised as a convenience feature. Someone sees a blocked run and proposes a queue: hold the request, show a notification, release the work after Touch ID. The queue feels helpful because the request is already formed. That is also why it is dangerous. A formed request has crossed the line from a plan into an action waiting for authority.

The safer rule is simple: while the vault is locked, deny actions and discard their dispatchable form. After unlock, the agent may inspect its current state and submit a new request. That second request can receive session authorization or per-call approval under current context.

A locked vault must reject the action now

A vault lock is an access boundary, not a temporary network outage. Treating it like an outage encourages automatic retry behavior, and automatic retry is precisely what you do not want around old agent intent.

When an agent asks to call an API or open an SSH connection, the gateway has enough information to decide whether the request may proceed. If the vault is locked, it does not need to inspect a body, resolve a host, start a connection, or wait for a user. It should return a denial before it touches the outside world.

The result needs to tell the agent what happened without inviting it to replay blindly. A useful shape looks like this:

{
  "ok": false,
  "error": {
    "code": "VAULT_LOCKED",
    "message": "The credential vault is locked. This action was not queued or sent.",
    "request_id": "req_7d4c1f",
    "retryable": false
  }
}

retryable: false may look counterintuitive. The agent can make a later request, but the failed request itself is not safe to retry. That distinction keeps client authors from building a generic backoff loop that turns an unlock into an unreviewed burst of old work.

Do not replace this with 503 Service Unavailable, a timeout, or a generic transport error. Those responses tell a well-meaning client to repeat exactly the same action. The gateway needs a semantic result that says, "A human-controlled security condition blocked this call, and this exact call is finished."

That rule applies equally to reads and writes. Teams often reserve caution for writes because a stale write can delete or deploy something. A stale read can still expose customer information, reveal a production configuration, or cause an agent to make a later decision on data the user no longer meant to retrieve.

Unlocking does not approve an earlier intent

An unlock event and an action approval answer different questions. An unlock asks whether the vault may use its secrets at all. An action approval asks whether this process may make this specific type of external call under the current task.

Combining those decisions creates a subtle authorization bug. Imagine an agent prepares an SSH command to restart a service. The developer closes their laptop, the vault locks, and the agent sends the call anyway. Forty minutes later, the developer returns, unlocks the Mac to inspect a separate issue, and the restart fires because the gateway kept the old command around. The developer did not approve a restart at that moment. They approved vault access for themselves.

The same problem appears in quieter forms:

  • An agent prepared a pull request comment, but the reviewer has already addressed the issue.
  • An agent prepared a cloud API call using a branch name that no longer points at the same commit.
  • An agent prepared a support-system lookup, but the customer request that justified it has ended.
  • An agent prepared a package publication, but a test failure arrived while access was locked.

The popular counterargument is that the request was already authorized before lock. Sometimes that is true, but it still does not justify deferred dispatch. Authorization can survive for a session while the process exists. Intent does not survive merely because a byte sequence sits in a queue.

A gateway should therefore make a clean separation:

  1. The vault lock decides whether any secret-backed action can begin.
  2. Session authorization decides whether this agent process is recognized for its current run.
  3. Per-call approval decides whether a credential marked for individual confirmation may be used now.

If the first decision says no, stop. Do not evaluate the later decisions and do not preserve a ready-to-send request for when the answer changes.

A notification is not a request queue

You can notify a person that work is blocked without retaining work that can execute. Those are different designs, and teams blur them because both get called "pending requests."

A notification is a non-actionable fact. It may say that a process attempted to use a named credential reference against a destination class, at a given time. It helps the person decide whether to unlock and return to the task. It cannot reconstruct headers, a request body, an SSH command, or an approval token.

A request queue retains enough material to dispatch later. It may include an HTTP method, URL, body, command arguments, credential selection, an authorization decision, or a signed replay token. Once you keep that material, you have built delayed execution.

The distinction matters in implementation. This is acceptable as a blocked-work notice:

{
  "event": "action_denied",
  "reason": "vault_locked",
  "session_id": "ses_31b8",
  "channel": "ssh",
  "credential_label": "production-deploy",
  "destination": "deploy host",
  "occurred_at": "2026-07-22T21:14:05Z"
}

This is not acceptable if the system can later recover the full command, destination address, private request body, or a credential-use authorization from the event. The record should support investigation, not replay.

Be careful with hashes too. A digest is often safe for correlation, but only if the gateway cannot use it to retrieve a retained payload. A digest plus a hidden blob is still a queue. A digest that exists only in an append-only audit record is different.

There is a product temptation here: a list of pending requests makes a dashboard look responsive. Resist it unless each entry requires the agent to re-submit a fresh call after the user acts. A screen that offers "run all after unlock" has converted a security prompt into a delayed job scheduler.

Give the agent a state machine it cannot misunderstand

Agents behave better when the gateway exposes a small, explicit state model. Ambiguous errors make agents invent recovery plans, and their invented plan may be to wait, retry, or find another credential path.

Use a state machine where a request has one terminal result once the vault gate denies it:

received
  |
  +-- vault locked --> denied_locked (terminal)
  |
  +-- vault unlocked --> session check
                           |
                           +-- not approved --> denied_session (terminal)
                           |
                           +-- approved --> per-call check
                                             |
                                             +-- approval declined --> denied_call (terminal)
                                             |
                                             +-- approved --> dispatched --> completed

The important part is not the diagram. The important part is that denied_locked has no arrow back into dispatched. A later request may enter at received, but the old one cannot re-enter anywhere.

This design also makes idempotency easier to reason about. If a call fails because the vault was locked, do not reserve an idempotency token as though the server accepted the action. The agent must build another request later, with a new request ID. If the external API supports idempotency keys, the newly submitted request can use an application-level key that reflects the intended business operation, but the gateway's denial should not create a half-finished action record.

For write operations, have the agent include its own current precondition whenever the destination supports one. That might be a revision ID, an entity version, an expected branch head, or an ETag. When the vault opens, the agent should fetch current context again and build a call that reflects it. A stale request cannot pass a good precondition, and a fresh request has evidence that the agent looked again.

Do not try to infer freshness from elapsed time alone. Five seconds can be too long for a rapidly changing deployment, while an hour can be harmless for a static lookup. Freshness comes from rereading the task state and rebuilding the action, not from a timer.

Approval must bind the actual call

Keep HTTP secrets outside
Send HTTP calls through credential injection, without handing bearer or custom-header secrets to the agent.

A fresh post-unlock request still needs a narrow approval model. Otherwise you have removed deferred replay only to replace it with a vague permission that lets the agent change its mind after the user clicks.

An approval card should bind to material that changes the security meaning of the action. For HTTP, that usually includes the method, normalized destination, credential reference, and a digest of the request body. For SSH, it includes the host identity, account, command or command digest, and credential reference. It should also bind to the agent process and expire quickly.

Avoid a card that says only, "Allow agent access to production." That sentence makes a person approve a category while the agent controls the details. A person may be willing to let a process read one endpoint but not invoke an administrative endpoint under the same hostname.

A practical approval record can look like this:

{
  "approval_id": "apr_8c62",
  "session_id": "ses_31b8",
  "process_identity": "signed-authority-and-process-instance",
  "channel": "http",
  "credential_label": "billing-api",
  "method": "POST",
  "destination": "api.example.internal/v1/invoices",
  "payload_digest": "sha256:...",
  "expires_at": "2026-07-22T21:16:00Z",
  "used": false
}

The gateway does not need to show every byte of a large payload to be honest about what it approves. It does need to bind the approval to the bytes it will send. A concise human summary can sit beside a digest, but the digest protects the exact request from substitution.

Use one-time approval records for per-call credentials. Mark the approval used before dispatch begins, not after a response returns. If a connection drops after dispatch, the agent may need to inspect the destination to determine whether the action took effect. Reusing the approval would make duplicate writes easier.

The failure case to test is unlock at the wrong moment

The most revealing test is not "does the call fail while locked?" It is "what happens when the world changes before the user unlocks?"

Set up a harmless test service with one endpoint that records a deployment target and another that changes the currently permitted target. Then run this sequence:

  1. Start an agent task that plans to send POST /deploy with {"revision":"a1b2c3"}.
  2. Lock the vault before the agent sends the call.
  3. Confirm that the gateway returns VAULT_LOCKED and the test service receives nothing.
  4. Change the permitted revision to d4e5f6 while the vault remains locked.
  5. Unlock the vault for an unrelated reason.
  6. Wait without touching the agent.

The correct result is boring: the test service still receives nothing. If it receives a deployment for a1b2c3, your gateway has a deferred execution path.

Then tell the agent that the call was denied, let it reread the permitted revision, and have it submit a new request. The expected request is now d4e5f6, and the gateway can ask for whatever session or per-call authorization applies. This proves that the recovery path preserves current context instead of treating time spent locked as an invisible pause button.

Run the same test for SSH. Use a command that writes a harmless marker containing the intended revision. Do not test only connection establishment. The unsafe implementation often holds a command after it has selected a key, then dispatches it when the vault becomes available. You want proof that the command text itself dies at the lock boundary.

Do not let clients hide the denial

Authorize the current run
Approve a new agent process once, then let that session end when the process exits.

A gateway can make the right decision and still produce bad behavior if its clients flatten every error into "try again later." The protocol needs enough structure for agent frameworks and wrapper scripts to handle a locked vault deliberately.

Agents should receive three instructions in the response contract. First, the action did not leave the gateway. Second, the gateway discarded the request. Third, the agent should not retry the same request automatically.

The agent's own recovery loop should look more like this:

if result.error.code == "VAULT_LOCKED":
    record_blocked_task()
    ask the user to unlock when appropriate
    stop this action

if user later resumes the task:
    reread relevant state
    decide whether the action is still needed
    create a new request

The line decide whether the action is still needed matters. It should not be replaced with retry request. An agent that received new user instructions, edited files, switched branches, or learned that a test failed may now need a different action or no action at all.

For unattended runs, return the denial to the orchestrator and let the run end in a blocked state. Do not have the gateway wait for someone to unlock. A waiting agent process holds memory, retains context that may become sensitive, and creates pressure to treat an eventual unlock as permission to continue. A clean stop gives the human a chance to review the task before resuming it.

If your interface shows a notification, make the wording precise: "An agent action was denied because the vault was locked." Avoid buttons labeled "continue" or "approve pending." A button can open the vault or reveal session details, but it should not cause an old action to run.

Logs should prove that nothing was sent

A denial deserves an audit record because it answers a question operators will eventually ask: did the agent merely attempt the action, or did it actually contact the outside system?

Record the attempted channel, the identity of the agent run, the credential reference, a normalized destination, the outcome, and timestamps. Mark the dispatch state plainly. Operators should be able to distinguish denied_before_dispatch from dispatch_started, remote_rejected, and completed without reading tea leaves from exception strings.

Do not log secrets, raw authorization headers, private key material, or full bodies by default. For sensitive request bodies, record a digest and a small approved summary if the system can produce one without leaking content. The goal is to establish what happened, not to build a second copy of the data the vault was meant to protect.

An audit query should be able to answer this kind of incident report:

21:14:05  session ses_31b8 attempted SSH action using production-deploy
21:14:05  vault gate denied action before dispatch
21:15:41  vault unlocked by local user action
21:16:09  no action dispatched from ses_31b8

That final line may be inferred from the absence of dispatch records, but explicit terminal states make investigations faster and reduce ambiguity. If you use a hash-chained journal, verify the chain during incident review as well as during routine checks. Tamper evidence has little value if nobody uses it when the record matters.

Sallyport's split between a Sessions journal for runs and an Activity journal for individual calls fits this problem well, because a locked-vault denial belongs to both the run's history and the action-level trail. Its offline sp audit verify check also lets a team test the integrity of that history without opening the vault.

Convenience queues create a second authorization system

Keep old requests credential-free
Keep API and SSH credentials encrypted in Sallyport, never in an agent's context.

Once a gateway stores requests for later release, it starts accumulating rules: how long requests live, who can release them, whether the original process must still exist, whether content can change, what happens after a reboot, and whether one unlock releases one request or all of them.

Those rules are a policy engine in disguise. They are difficult to explain to users because each exception changes what unlock means. A short queue timeout does not fix the meaning problem. Requiring the original process to remain alive does not fix it either, because the process may be compromised or simply operating on stale context.

Keep the design smaller. The vault gate denies all actions while locked. A new process session may require approval. A credential marked for per-call approval requires an explicit confirmation each time. Every other convenience should live on the agent side as task recovery, where the agent must rebuild its plan and where the human can see what changed.

This also gives users a reliable habit: unlocking restores the ability to consider new actions. It never releases actions they forgot were waiting. People can make sound decisions with that mental model. They struggle when a lock screen doubles as a hidden work queue.

Make the safe path less annoying than the unsafe one

Teams build queues because a hard failure can feel disruptive during normal development. Fix the friction without retaining executable requests.

Keep session authorization scoped to the lifetime of the agent process so a developer does not approve every ordinary call. Reserve per-call confirmation for credentials that deserve deliberate friction, such as production administration or external publication. Return a clear denial that lets the agent report blocked work in plain language. Give the user a way to unlock, inspect the agent session, and resume the task consciously.

Then make agent instructions explicit. Tell agents that credentials stay outside their context, that a locked-vault result ends the attempted action, and that a later action must be reconstructed after checking current state. An agent prompt cannot enforce the rule, but it reduces pointless retries and makes the protocol behavior easier to work with.

The test for this design is simple. If a person unlocks the vault while distracted, tired, or handling an unrelated task, no earlier agent action should occur. If that claim is not true, remove the queue before it becomes an incident report.

FAQ

Should an AI agent request wait until the vault is unlocked?

Treat a locked vault as an immediate denial boundary. The gateway should return a machine-readable locked result and discard the actionable request, rather than storing it for later execution. The agent can retry only after it has fresh context and chooses to make the call again.

Why should unlocking a vault not automatically run queued agent actions?

No. Unlocking proves that a human can access secrets again; it does not prove that an earlier request is still wanted. Time, repository state, user intent, and the agent's plan may all have changed while the vault was closed.

What error should a gateway return when the vault is locked?

Use a distinct error such as VAULT_LOCKED with retryable: false for the original request. Include a short explanation for the agent and a request ID for troubleshooting, but do not preserve the credential-bearing action for replay.

Can an agent safely retry a request after the vault opens?

Usually no. Retrying a read-only call can still reveal data after the user has moved on, and retrying a write can create duplicate or stale changes. Let the agent decide whether to issue a newly constructed request after it receives fresh context.

Is a short expiration time enough to make queued requests safe?

Expiry limits accidental buildup, but it does not repair stale intent. A request made before the vault opened still lacks proof that the agent's current task and the human's current intent match the original action. Expiry is useful for approval cards, not for a hidden execution queue.

What should an approval card bind to?

Yes. The approval should bind to the exact method, destination, credential reference, request digest, agent process, and a narrow time window. A broad approval that says "allow the agent" gives an attacker and a confused agent too much room to change the action afterward.

How should locked-vault denials appear in audit logs?

The session record should show that the process attempted an action while access was locked and that the gateway denied it before dispatch. The activity record should identify the attempted channel and destination without storing secret material. A denial is a security event, not empty noise.

Can a gateway show pending requests without queuing them?

It can work if the waiting room never contains a dispatchable request. Store only a non-actionable notice such as "agent X needs access to service Y" and force the agent to submit a new, complete request after unlock. Do not retain headers, bodies, commands, or authorization state.

What happens when an autonomous agent runs while nobody is present?

A gateway should preserve the human boundary even when an agent runs unattended. If no person can unlock the vault and approve the action, the run should stop, report the blocked work, and wait for a human to resume or restart it later.

How do I avoid giving agents credentials when the vault is often locked?

Never hand a credential to the agent as a workaround. Either wait for a human-controlled session, use a deliberately scoped nonhuman credential in a separate system, or redesign the task so it can produce a reviewable plan without making the external call.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov