diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index f9c3b93d9850..033295f72b17 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -551,7 +551,7 @@ methods. Treat this as feature discovery, not a full enumeration of - `plugins.list` (`operator.read`) returns the installed plugin inventory plus locally curated official picks, diagnostics, and whether the current install mode allows mutations. - `plugins.search` (`operator.read`) searches installable ClawHub code-plugin and bundle-plugin families. Pass non-empty `query` and optional `limit` from 1 to 100. - - `plugins.install` (`operator.admin`) installs either an official catalog entry with `{ source: "official", pluginId, installPolicyWarningAcknowledgement? }` or a ClawHub package with `{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk?, installPolicyWarningAcknowledgement? }`. ClawHub installs preserve Gateway trust, integrity, and install-policy checks. When install policy warns and the Gateway can bind the request to an immutable resolved artifact, the request fails before commit with structured `error.details` containing `installPolicyCode: "install_policy_warning_acknowledgement_required"`, target metadata, `reason`, optional `findings`, and a server-issued `acknowledgementToken`. After showing those details, a client may retry with that token as `installPolicyWarningAcknowledgement`. The Gateway consumes the token once and only for the same install request and resolved artifact; policy then re-evaluates the staged source and continues when that fresh evaluation allows the install or repeats the unchanged warning the operator approved. Treat `acknowledgementToken` as a short-lived bearer secret. It is not bound to the Gateway connection, device, or operator that received it: any authenticated `operator.admin` client that has the token and submits the same install request may consume it once for the matching resolved artifact. Keep it only in memory for the immediate reviewed retry; do not log, persist, or forward it. Tokens expire after five minutes and are invalidated by a Gateway restart; after either event, retry without the token to receive the current warning and a fresh token. A changed or later warning fails that request with its own structured details and, when the artifact remains immutably resolved, a fresh token for another reviewed retry. A block or a warning without immutable resolution metadata is terminal and does not include an acknowledgement token. Successful installs require a Gateway restart. + - `plugins.install` (`operator.admin`) installs either an official catalog entry with `{ source: "official", pluginId, installPolicyWarningAcknowledgement? }` or a ClawHub package with `{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk?, installPolicyWarningAcknowledgement? }`. ClawHub installs preserve Gateway trust, integrity, and install-policy checks. When install policy warns and the Gateway can bind the request to an immutable resolved artifact, the request fails before commit with structured `error.details` containing `installPolicyCode: "install_policy_warning_acknowledgement_required"`, target metadata, `reason`, optional `findings`, and a server-issued `acknowledgementToken`. After showing those details, a client may retry with that token as `installPolicyWarningAcknowledgement`. Every presentation consumes the token, including an expired or wrong-request attempt; only its first presentation can authorize the same install request and resolved artifact. Policy then re-evaluates the staged source and continues when that fresh evaluation allows the install or repeats the unchanged warning the operator approved. Treat `acknowledgementToken` as a short-lived bearer secret. It is not bound to the Gateway connection, device, or operator that received it: any authenticated `operator.admin` client that has the token and submits the same install request may consume it once for the matching resolved artifact. Keep it only in memory for the immediate reviewed retry; do not log, persist, or forward it. Tokens expire after five minutes and are invalidated by a Gateway restart; after either event, retry without the token to receive the current warning and a fresh token. A changed or later warning fails that request with its own structured details and, when the artifact remains immutably resolved, a fresh token for another reviewed retry. A block or a warning without immutable resolution metadata is terminal and does not include an acknowledgement token. Successful installs require a Gateway restart. - `plugins.setEnabled` (`operator.admin`) changes one installed plugin's enabled policy with `{ pluginId, enabled }`. The response includes the updated catalog entry, restart metadata, and any slot-selection warnings. - `plugins.uninstall` (`operator.admin`) removes one externally installed plugin with `{ pluginId }`: config references, the install record, and managed files. Bundled plugins cannot be uninstalled, only disabled. The response lists the removal actions and always requires a Gateway restart. diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index e9297c9cf552..67f1b65aab7a 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -156,9 +156,10 @@ fails closed and requires interactive review. Gateway `plugins.install` clients receive structured warning details when the Gateway can bind the request to an immutable resolved artifact, and may make one explicit retry with the returned `acknowledgementToken` as -`installPolicyWarningAcknowledgement`. The Gateway consumes that server-issued -token once and only for the same install request and resolved artifact. OpenClaw -re-evaluates the staged source and continues when that fresh evaluation allows +`installPolicyWarningAcknowledgement`. Every presentation consumes the token, +including an expired or wrong-request attempt; only its first presentation can +authorize the same install request and resolved artifact. OpenClaw re-evaluates +the staged source and continues when that fresh evaluation allows the install or repeats the unchanged warning the operator approved. Treat `acknowledgementToken` as a short-lived bearer secret. It is not bound to the Gateway connection, device, or operator that received it: any authenticated diff --git a/docs/tools/skills-config.md b/docs/tools/skills-config.md index bcf25b40f62e..9c6f4963a209 100644 --- a/docs/tools/skills-config.md +++ b/docs/tools/skills-config.md @@ -204,9 +204,10 @@ fails closed and requires interactive review. Gateway `plugins.install` clients receive structured warning details when the Gateway can bind the request to an immutable resolved artifact, and may make one explicit retry with the returned `acknowledgementToken` as -`installPolicyWarningAcknowledgement`. The Gateway consumes that server-issued -token once and only for the same install request and resolved artifact. OpenClaw -re-evaluates the staged source and continues when that fresh evaluation allows +`installPolicyWarningAcknowledgement`. Every presentation consumes the token, +including an expired or wrong-request attempt; only its first presentation can +authorize the same install request and resolved artifact. OpenClaw re-evaluates +the staged source and continues when that fresh evaluation allows the install or repeats the unchanged warning the operator approved. Treat `acknowledgementToken` as a short-lived bearer secret. It is not bound to the Gateway connection, device, or operator that received it: any authenticated diff --git a/src/gateway/server-methods/plugins.test.ts b/src/gateway/server-methods/plugins.test.ts index 6e3f89fe94c2..18c228f9ce39 100644 --- a/src/gateway/server-methods/plugins.test.ts +++ b/src/gateway/server-methods/plugins.test.ts @@ -545,6 +545,17 @@ describe("plugin management Gateway handlers", () => { code: "INVALID_REQUEST", message: expect.stringContaining("does not match this plugin"), }); + + const retryAfterMismatch = await callHandler("plugins.install", { + source: "official", + pluginId: "diffs", + installPolicyWarningAcknowledgement: acknowledgementToken, + }); + + expect(retryAfterMismatch.error).toMatchObject({ + code: "INVALID_REQUEST", + message: expect.stringContaining("expired or does not match this plugin"), + }); expect(managementMocks.install).toHaveBeenCalledOnce(); });