Local agent action gateway health checks
Local agent action gateway health checks that verify app state, vault denial, HTTP and SSH canaries, and audit integrity without leaking credentials.

A local action gateway is healthy only when it can refuse the wrong action, perform the right controlled action, and leave evidence that survives review. A green process monitor proves almost none of that. It tells you that something has a PID. It does not tell you whether the vault is locked, whether an agent can reach the gateway, whether injected credentials still work, or whether the audit trail recorded the call.
That distinction matters with Sallyport because the app, the vault gate, the authorization decision, the external action, and the encrypted audit log are separate points of failure. Treating them as one generic "health" signal creates the worst kind of monitor: one that stays green while the control you rely on has stopped working.
Build this as a small acceptance suite, not as a pile of port checks. The suite should use harmless canary targets, return non-secret evidence, and classify a locked vault correctly. If a person must unlock the vault with Touch ID, your monitor should report that state plainly instead of trying to bypass it.
A running app is only the first condition
An availability probe should answer one narrow question: can the local Mac start and keep the gateway app running? It should not pretend that a running process proves external actions work.
For a menu-bar app, start with a local check that a supervisor can run without credentials. The exact process name and install location can vary by release, so keep those values in one local configuration file rather than scattering them through scripts. A basic shell probe can look like this:
#!/bin/sh
set -eu
APP_NAME="Sallyport"
if ! pgrep -x "$APP_NAME" >/dev/null 2>&1; then
open -a "$APP_NAME"
sleep 2
fi
if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
printf 'app=ready\n'
exit 0
fi
printf 'app=unavailable\n' >&2
exit 2
This has an intentionally modest claim. open -a asks macOS to launch an application, and pgrep observes a process after that request. It does not prove that the app finished initialization, that its vault can answer a request, or that its MCP shim can accept a client. Apple documents Launch Services as the system interface used to launch and activate applications, which is why launching through the operating system is better than hard-coding an application bundle path.
Keep the output structured and dull. app=ready is enough for a scheduler. Do not dump ps output into a central log where command arguments, usernames, or unrelated process details become permanent clutter.
A process can exist while it is wedged. It can also be absent because the Mac is asleep, logged out, or restarting after an update. Your alerting rule needs a grace period for those expected conditions. A health check that pages someone every time a laptop closes its lid will be disabled, and then it will not help during a real failure.
The useful availability signal is therefore local and bounded: the app started, remained present for a short settling interval, and an MCP client can begin a session. The last part belongs in a separate probe because it exercises a different boundary.
The vault state must be an explicit result
A locked vault is healthy when it denies actions. Calling that state an outage confuses security behavior with service failure.
Sallyport's vault gate is absolute: while locked, every action is denied. On supported macOS hardware, the vault gate uses Secure Enclave and Touch ID. A check must preserve that rule. Do not write a script that feeds passwords to a UI, stores a biometric bypass token, or treats an unlocked desktop session as proof that the vault should be unlocked.
Use four result states instead of a Boolean:
ready: the app is available, the vault is unlocked, and controlled checks may run.locked: the app is available, but the vault correctly denies actions.denied-unexpectedly: the vault is unlocked, yet the intended canary action was denied.unavailable: the app or its local MCP path cannot answer.
That vocabulary prevents a common operational mistake. Teams often schedule a credentialed probe at night, see failures after the screen locks, and weaken the system until it can unlock itself. They have not fixed monitoring. They have removed the human decision that the vault was built to require.
The practical pattern is a two-part run. An unattended job records app availability and locked-vault denial. A person, or a controlled workstation session that already has human approval, initiates the credentialed checks after unlocking. Record the reason in the run output:
{
"run_id": "hc-2026-07-22T141501Z-8f29",
"app": "ready",
"vault": "locked",
"http": "skipped",
"ssh": "skipped",
"audit": "verified",
"reason": "credentialed checks require an unlocked vault"
}
The run ID is not a secret. It gives operators something stable to compare with the activity record later. Do not use a username, machine serial number, endpoint URL, or credential label as the run ID.
Per-session authorization and per-call approval also need their own treatment. A new agent process may require session approval, and a credential may require approval for each use. That is expected behavior, not a flaky test. Your runner should state whether it is designed for an approved session or whether a human will approve each call. A silent timeout tells the next investigator nothing.
Use canary targets that prove credential injection
An HTTP check should call an endpoint built to validate a specific canary credential and return a fixed, non-secret result. Testing a public URL only proves that the network works. It says nothing about whether the gateway selected the intended credential, inserted it into the right header, or kept it away from the agent.
Set up a tiny service that accepts one path, one method, and one credential form. Store the expected canary secret on the service side. Store the same canary secret in the gateway vault. The client that triggers the action should never receive the secret, and the service should never echo it.
The response contract can be this small:
{
"check": "agent-gateway-http",
"result": "ok",
"request_id": "7d7a0f3c"
}
The service should return 401 when the credential is absent or wrong, 405 for the wrong method, and 200 only when the expected credential arrived. Generate request_id at the service and keep it opaque. Do not derive it from the authorization header or any part of the incoming request.
Use a dedicated route such as /agent-gateway-canary. Do not bolt the check onto an existing production endpoint. Production endpoints accumulate behavior over time: rate limits, redirects, content negotiation, caching rules, billing side effects, and permission changes. A canary route can stay intentionally boring.
RFC 9110 defines request method semantics and classifies GET, HEAD, OPTIONS, and TRACE as safe methods, but safe in HTTP means the requested action should not change the resource's intended state. It does not mean the call is harmless for your account, logs, quotas, or downstream behavior. An API can record a GET, charge a request, or trigger a poor implementation's side effect. Build a route whose server-side behavior you can inspect, rather than trusting a familiar verb.
A frequent bad recommendation is to use curl with a production API token as the health check. It is popular because it takes one line. It is wrong because shell history, process inspection, CI logs, and error output create too many places for a bearer token to appear. It also bypasses the behavior you need to test if the gateway normally injects credentials itself.
Your runner should invoke the gateway through the same MCP route that an agent uses. Do not invent a backdoor HTTP client for the check. Keep the transport-specific invocation behind a local adapter because tool names and request shapes can change. The adapter takes a logical action, asks the MCP-connected gateway to perform it, and emits only the normalized result.
{
"action": "http_canary",
"target": "canary-api",
"method": "POST",
"path": "/agent-gateway-canary",
"expected_status": 200,
"expected_check": "agent-gateway-http"
}
The adapter must redact on failure. It may report http_status=401, transport_error=timeout, or response_schema=invalid. It must not print outgoing headers, a request body, a full URL with query parameters, or the raw response unless you have reviewed that data as safe.
An HTTP success must prove the intended action
A 200 by itself is weak evidence. The check must validate the service response, the method, and the target identity so that a redirect, proxy page, or stale fixture does not turn into a false pass.
Make the canary response identify the test without identifying a credential. Compare a few exact fields:
status=200
check=agent-gateway-http
result=ok
request_id=7d7a0f3c
The checker should accept any syntactically valid request_id, then save it with the run ID. It should reject a missing field, a body that claims success but has a non-2xx status, and an unexpected content type. A captive portal, a corporate proxy error page, or a misrouted DNS record often returns a valid HTTP response. That is transport success, not action success.
The curl documentation makes a related point: without --fail or --fail-with-body, curl does not treat an HTTP status such as 404 or 401 as a command failure. That behavior is correct for a general transfer client, but it catches people who write monitors that look only at curl's exit status. If your adapter uses curl internally, capture both its process result and the HTTP status, then decide success from your explicit contract.
Do not let a check follow redirects automatically unless redirects are part of the intended endpoint design. A redirect can send your canary call to a login page that returns 200, or to another host that you did not mean to contact. Pin the HTTPS origin in configuration, validate the certificate through the normal client stack, and record the final peer identity only if that record cannot reveal sensitive network details.
Timeouts need separate labels. A DNS failure, TCP refusal, TLS validation failure, gateway denial, upstream 401, upstream 500, and response mismatch have different owners. If the runner calls all of them http=failed, you will waste the first ten minutes of every incident finding out where the request stopped.
A useful failure record looks like this:
{
"run_id": "hc-2026-07-22T141501Z-8f29",
"check": "http_canary",
"outcome": "failed",
"stage": "upstream_response",
"http_status": 401,
"request_id": null,
"secret_material": "redacted"
}
The secret_material line is a reminder for humans reading the record, not proof that redaction worked. Prove redaction with tests: deliberately make the canary reject the credential, capture the runner's stdout and stderr, and search those files for the test secret. The secret must not appear. Repeat with malformed JSON, a timeout, a TLS failure, and an agent-side tool error. Error paths are where secrets usually escape.
SSH needs a fenced target, not a login shell
An SSH health check should prove authentication and command execution against a dedicated account whose server refuses to run arbitrary commands. A successful connection to a general administrative host proves too much in the wrong direction: it gives the health credential a useful shell.
Create a separate account such as gateway-health on a controlled test host. Give it a forced command in authorized_keys or an equivalent server-side restriction. The forced command should ignore the original command, write a timestamp and opaque run ID to a local audit file, then return a constant response.
A conceptual authorized_keys entry looks like this:
command="/usr/local/libexec/gateway-health",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA... gateway-health
The public key belongs to the gateway's canary identity. The private key stays in the vault. The AAAA... text is intentionally incomplete here because you must generate your own key material, not copy an example into a production file.
The server command should avoid echoing $SSH_ORIGINAL_COMMAND, environment variables, or authentication details. It can return a fixed shape:
{"check":"agent-gateway-ssh","result":"ok","receipt":"c2b91a"}
If you need a run ID for correlation, pass an opaque token in the submitted command and have the forced command validate a tight format such as hexadecimal characters of a fixed length. Never accept an arbitrary string and write it to a shell command, a filename, or a log line. Better still, let the server generate the receipt and join records by time window during investigation.
The check should fail if the server permits an interactive shell, forwards a port, allocates a TTY, or executes a command other than the forced one. Those are configuration regressions. They deserve a different severity than a routine network timeout because they change the exposure of the canary credential.
Do not reuse the SSH identity used for deployments. Reuse makes the check easier to set up, then turns a monitoring failure into a broad access problem. A health credential should have one job, one target account, and no permission beyond producing the receipt.
Sallyport sends SSH through its bundled stateless sp-ssh helper. Your test should therefore go through the normal gateway action path rather than invoking a local private key with OpenSSH. Otherwise you have only checked the host and the account. You have skipped the credential boundary that needs proof.
Audit verification is a separate assertion
A successful canary action and a valid audit trail are different claims. Verify both.
The activity record tells you that the gateway recorded an individual action. The session record tells you about the agent run and gives you a way to revoke that run. Neither should be treated as a substitute for checking the remote effect, because a request can be logged before an upstream failure. Conversely, a remote system can receive a request while a local logging path fails at a bad moment. You need correlation, not wishful thinking.
For each successful HTTP or SSH action, collect three non-secret pieces of evidence:
- the health run ID generated by the runner;
- the opaque receipt or request ID generated by the canary service;
- the local action timestamp, recorded in UTC.
Then inspect the activity journal for the action metadata that is safe to retain, such as channel, action outcome, target alias, and time. Do not expect the journal to contain plaintext credentials. It should not. Do not require it to reproduce the remote response body either. The receipt belongs to the remote canary system, not to the gateway's secret store.
Sallyport projects its Sessions and Activity journals from a write-blind encrypted, hash-chained audit log. Run its offline integrity check as an independent part of the suite:
sp audit verify
A passing command should be recorded as audit=verified; a nonzero result is an integrity incident until proven otherwise. This verification does not need the vault key, which makes it suitable for an unattended local check even while the vault is locked.
Do not reduce audit verification to "a log file exists." File existence catches almost nothing. A truncated file, replaced records, a broken chain, or a writer that stopped after startup can all coexist with a path that looks normal in Finder.
There is another easy error: checking only that the audit chain validates. A chain can validate and still lack the event you expected if the gateway never attempted the action. Pair chain verification with an event-presence assertion after each canary action. Define a reasonable arrival window for local projection and report a delay separately from a missing event. A delayed journal update may need investigation, but it is not identical to a failed write.
Keep the runner outside the trust boundary it checks
The health runner should orchestrate actions and judge evidence. It should not hold credentials, parse vault files, or read secret-bearing application state.
A practical layout has four components:
- A local scheduler launches a small runner under a dedicated macOS account.
- The runner checks app presence and invokes a local MCP adapter.
- The adapter requests named canary actions through the gateway and returns redacted structured results.
- The runner verifies audit integrity and writes one short report to a protected local directory.
The dedicated account should not have access to the vault's data files. Apple places non-sandboxed macOS application support data under the current user's ~/Library/Application Support directory, but a monitor should not assume that it can or should inspect those files. The check needs behavior, not a copy of the vault's contents.
Keep configuration declarative and free of secrets. This example gives the runner enough information to validate outcomes without disclosing credentials:
checks:
http_canary:
target_alias: canary-api
expected_status: 200
expected_check: agent-gateway-http
timeout_seconds: 10
ssh_canary:
target_alias: canary-ssh
expected_check: agent-gateway-ssh
timeout_seconds: 10
audit:
command: sp audit verify
timeout_seconds: 15
Target aliases matter. An alias is less sensitive than a full URL or host name, and it forces you to make target selection explicit in the gateway configuration. If an operator changes an alias, the action check should show that configuration review is required before it silently begins testing a different place.
Do not give the runner permission to approve a new agent process. Per-session authorization is on by default for a reason. The first call from a new agent process identifies its code-signing authority in the approval card, and approval lasts only for that run. Arrange a long-lived, reviewed health-client process if you need scheduled checks after a person approves it. If the process exits, expect the next run to ask again.
That choice is a little less convenient than a hidden always-approved automation account. It also keeps a new executable from acquiring permission merely because it copied the health check's command line.
A failed check needs a useful diagnosis
Most gateway health failures are ordinary configuration drift. The dangerous part is how quickly people respond by weakening controls instead of locating the broken boundary.
Consider a real-looking sequence. The app is running. The vault is unlocked. The HTTP canary returns 401. The first instinct is to paste the canary token into a shell and call the endpoint directly. Do not do that. It proves that the token works, but it bypasses the vault and creates a new secret leak path.
Work through the evidence in order instead:
- Confirm that the local MCP adapter reached the gateway and received an action result rather than timing out before submission.
- Confirm that the target alias still selects the intended stored credential and endpoint configuration.
- Check the canary service logs for the opaque request ID, method, route, and rejection reason. Do not log its received authorization value.
- Inspect the activity journal for the matching action attempt and verify the audit chain with
sp audit verify. - Rotate the canary credential if configuration review shows that its expected value changed or may have been exposed.
This sequence separates four failures that otherwise look identical: an incorrect target mapping, a revoked or rotated secret, a gateway injection failure, and an upstream service configuration change. It also avoids the familiar self-inflicted incident where an operator "tests" a secret in a terminal, then finds it copied into shell history, a scrollback buffer, or a support bundle.
Classify severity by the control that failed. An unavailable app is an availability issue. A locked vault that denies an action is informational unless a scheduled approved check was expected. A canary action that succeeds while the vault reports locked is a security incident. A broken audit chain is also a security incident, even if both canary targets still return success.
Do not hide these categories behind one red or green badge. The operator who sees vault=locked, audit=verified, and http=skipped knows exactly what to do. The operator who sees health=warning starts guessing.
The smallest useful schedule has two lanes
Run non-secret checks frequently, then run credentialed canary actions sparingly and deliberately. The two lanes produce less noise and give you better evidence.
The unattended lane can run whenever the Mac is expected to be awake. It checks that the app is present, that a locked vault refuses action when locked, that the MCP path can return a classified response, and that sp audit verify passes. None of those checks should require extracting a credential or unlocking the vault.
The credentialed lane runs after a human unlocks the vault and approves the health-client session if required. It performs one HTTP canary action, one SSH canary action, confirms the remote receipts, checks for corresponding local activity records, and verifies the audit chain again. Use a modest cadence. Every credentialed action becomes an audit event and a remote service event, so a check every minute creates noise without giving you much more confidence.
Run the credentialed lane after changes to credential configuration, target aliases, agent tooling, macOS permissions, network controls, or the gateway app. That timing catches the changes most likely to break action execution. It also gives a reviewer a clear before-and-after record.
The standard is simple: a health suite must prove the controls you intend to rely on. If it can only prove that an app icon is still in the menu bar, call it an availability probe and stop there. Do not call it proof that autonomous agents can act safely.
FAQ
What should a local agent gateway health check test?
A process check only proves that a process exists. An action gateway check must also prove that the vault is enforcing its lock state, that a controlled external action can complete, and that the action leaves a verifiable audit record.
Can I use a real API credential in a health check?
Use a dedicated canary credential with access to a harmless endpoint or account. Do not use a production credential and do not make the check print request headers, command lines, environment variables, or response bodies that could contain a secret.
Should a locked vault make monitoring report an outage?
Treat a locked vault as a distinct, expected state. The check should prove that actions are denied while locked, then record that an authorized human unlocked the vault before it runs action tests.
What is a safe HTTP health check for an agent gateway?
A safe HTTP check calls a purpose-built endpoint that validates the injected credential and returns a fixed result such as a status code and request ID. Avoid GET requests against production APIs merely because they look harmless.
How do I test SSH execution without giving an agent shell access?
Use a restricted health account and a forced command on a dedicated SSH target. The forced command should ignore the submitted command, write a fixed marker with a server timestamp, and return a short success line.
Is an audit log enough to prove an agent action succeeded?
No. A successful request can still fail to be recorded, and an audit entry can exist for a request that never reached its intended service. Correlate an action result with an audit record and verify the audit chain separately.
How can a health check avoid leaking credentials into logs?
Never put a secret in an expected output file, a test name, a URL query string, or a shell argument. Health checks should compare stable non-secret evidence such as status codes, opaque request IDs, timestamps, and fixed response text.
Which agent gateway health check failures need an alert?
Alert immediately for unavailable app state, broken audit integrity, or a gateway that permits an action while the vault is locked. Alert less urgently for a failed canary target, because DNS, the local fixture, or the remote service may be the fault.
How often should credentialed action health checks run?
Run a cheap availability probe frequently, but run credentialed action checks less often and after configuration changes. Every action check writes an audit event, so an aggressive interval creates noise and makes investigation harder.
Can these checks run when the Mac is offline?
It can still prove app availability, local vault behavior, local audit integrity, and MCP connectivity. It cannot prove a remote HTTP or SSH action until the Mac has a reachable, controlled target.