Skip to content

Cloud

phpclaw/phpclaw-cloud is optional. With a cloud key set, it sends lifecycle events to a cloud endpoint and can register a guard that asks that endpoint to scan each message.

Terminal window
composer require phpclaw/phpclaw-cloud

It requires PHP 8.1, ext-curl, ext-json and phpclaw/phpclaw. Every adapter already depends on it.


use PhpClaw\Claw;
$agent = Claw::builder()
->cloudKey('...')
->build();
Builder methodEffect
cloudKey(string)enables cloud; an empty key leaves it off
cloudDisable(array)features to turn off (observability, scan) and hide_ names that keep content on your server
cloudSigningSecret(string)verify signed scan responses; empty leaves verification off

Cloud starts on the first send(), stream() or conversation() call, not at build(). If the package is not installed, a cloud key does nothing: core checks for CloudManager with class_exists() before booting it.

Requests go to https://phpclaw.ai/platform/api, fixed in code as CloudHttp::DEFAULT_BASE_URL. No environment variable redirects it, so the trace stream cannot be pointed somewhere else.

Requests are made only over https://, or plain http:// to localhost or 127.0.0.1. Any other URL is dropped before a connection is opened, and nothing is thrown. Redirects are not followed. Every request carries Authorization: Bearer <cloud key> and X-PhpClaw-Version: 1.0.

The loopback list in the code also holds ::1, but a URL for it must be written http://[::1]:8080/, whose parsed host is [::1], so an IPv6 receiver is dropped like any other host.

On start, cloud fetches /v1/manifest, which lists the events to forward and the guard features to enable. It is cached for one hour in the system temp directory, signed with an HMAC keyed by the cloud key and written with mode 0600. While that cache is still fresh, start-up makes no request at all. A cache that was edited, or signed with another key, is treated as missing rather than trusted. When the fetch fails, the last cached manifest is used at any age; with no cache at all, nothing is forwarded and no guard is added.

cloudDisable() takes feature names from the manifest. Disabling a feature drops every event it groups and its guard. The manifest names two:

NameTurns off
observabilityevery lifecycle event; nothing is posted to /v1/runs
scanthe scan guard; no message is posted to /v1/scan

To send nothing at all, disable both, or leave the cloud key unset. cloudDisable() also takes the three hide_ names described in Keeping content on your server, which keep tracing on but leave the content out.


Enabled events are posted to /v1/runs, except provider.token, which is never sent.

Posting does not wait for a reply. Each request has a 500 millisecond limit, is pumped for at most 50 milliseconds when the event fires, and any still pending are finished when PHP shuts down.

RuleResult
a field named password, passwd, secret, secret_key, private_key, api_key, apikey, api_token, access_token, auth_token, token or authorizationvalue replaced with [redacted]
a string over 8192 bytescut, ending …[truncated]
an array over 100 itemsreplaced whole by the string [array-too-large:N]
the whole payload over 64 KBreplaced by event, ts, run_id, oversized: true and original_bytes, with no content

Redaction matches the whole field name, ignoring case. api_key and API_KEY are redacted; my_api_key is not, and neither is a secret that appears inside a string, such as a password in an SQL query or a message.

Three more names in cloudDisable() still send every event, but without its content. The field keeps its key and arrives as [hidden]; a field that was never set stays empty.

$agent = Claw::builder()
->cloudKey('...')
->cloudDisable(['hide_inputs', 'hide_outputs'])
->build();
NameFields sent as [hidden]
hide_inputsmessage, prompt, message_excerpt, tool_input, command
hide_outputstext, response, tool_result
hide_metadatametadata

Fields are matched at any depth and ignoring case, so the nested payload of a custom event is covered too. Everything else still arrives: event names, run ids, timings, token counts, status and tool names, so runs are still traced and counted.

hide_inputs also leaves out the scan guard, because the scan posts the message. error fields are never hidden. A misspelled name such as hide_input is ignored like any other unknown name, so nothing is hidden.

When the manifest enables it, CloudScanGuard is registered on the guard chain at the priority the manifest gives, or 50. For each message it posts the text to /v1/scan and blocks when the reply says so. It is not registered when hide_inputs is set.

It scans the augmented message, so memory and skill text added to the prompt is sent too.

SituationResult
the endpoint cannot be reachedthe message is allowed
a signing secret is set, and the reply’s signature is missing or wrongthe message is blocked
the reply is allowed: falsethe message is blocked with the reply’s reason

The signature is an HMAC-SHA256 over allowed, reason and request_id, keyed by the signing secret.

CloudManager::boot() also takes $failClosed, which blocks messages when the endpoint is unreachable. The builder does not pass it, so through Claw::builder() the guard always allows on an outage.


Every adapter starts cloud only when its Store Messages setting is on, so turning that off keeps every event and every scan local.

That check lives in the adapters. In core, a cloud key alone is enough: there is no storage setting in between.


ClassRole
CloudManagerboot($key, $disable, $signingSecret, $failClosed): fetches the manifest and registers the hook and guard, once per key
CloudHttpthe HTTP client: get, post and non-blocking fire; failures return nothing and are not thrown
CloudManifestthe enabled events and guard features
CloudPayloadBuilderbuilds and reduces each event payload
CloudWebhookHookthe hook that sends events
CloudScanGuardthe guard that calls the scan endpoint