diff --git a/.difyignore b/.difyignore index 359985c..5cd3508 100644 --- a/.difyignore +++ b/.difyignore @@ -5,6 +5,14 @@ __pycache__/ .venv env/ venv/ +.git .git/ .gitignore .DS_Store +.venv-test/ +tests/ +.github/ +.pytest_cache/ +.ruff_cache/ +.claude/ +requirements-dev.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2b8a3..da4f5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,17 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follo ## [Unreleased] +## [0.0.3] - 2026-05-30 + ### Added +- `sign_action` now returns a structured `authorized` decision. On a permitted + action it returns `authorized=true` with the signature receipt, now including + `signature_b64`. On a policy block, emergency halt, delegation limit, or + agent quarantine it returns `authorized=false` with a machine `reason` + (`policy_blocked`, `emergency_halt`, `delegation_denied`, `quarantine`, or + `denied`) and a human-readable `detail`, so a workflow can branch on the + decision instead of failing. A policy block also passes through the signed + denial receipt (`signed_deny`), which is itself verifiable. - pytest smoke suite covering the provider entrypoint and all three tools, with network calls mocked so the suite runs offline. - GitHub Actions CI (`.github/workflows/ci.yml`) running pytest on Python 3.12 @@ -14,6 +24,26 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follo push to `main`. - `requirements-dev.txt` listing the test dependencies, plus a Development section in the README explaining how to run the suite locally. +- `output_schema` declarations on all three tools (`sign_action`, + `verify_signature`, `request_action`) so downstream workflow nodes can + address each returned field by name. + +### Changed +- Plugin user-facing text is now English-only (`en_US`). Removed the + untranslated `zh_Hans` and `pt_BR` locale keys that duplicated the English + strings across the manifest, provider, and tool definitions. +- README now documents the full output of each tool, including the structured + `authorized` decision and `signature_b64` from `sign_action`. + +### Fixed +- Each tool now emits its result as individual workflow variables in addition + to the JSON message, so a Dify workflow node can address each field by name + (`signature_id`, `authorized`, `verified`, and so on) at runtime. A JSON + message alone left named selectors unresolved once wired in a workflow. +- The `sign_action` `timestamp` output is a Unix epoch number, not an ISO 8601 + string; corrected its type and description in the tool schema. +- Capitalised the display label and credential prose to "Asqav" across the + manifest, provider, and tool definitions. ## [0.0.2] - 2026-05-11 diff --git a/README.md b/README.md index d6d16c4..76f319e 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,18 @@ The plugin inherits the SDK's `mode` behavior whenever it is invoked through the ## Tools ### Sign Action -Signs an agent action with ML-DSA-65. Provide an action type (e.g. "read:data", "tool:execute") and optional context. Returns a signature ID and public verification URL. +Signs an agent action with ML-DSA-65. Provide an action type (e.g. "read:data", "tool:execute") and optional context JSON. + +Returns a JSON object with an `authorized` boolean so a workflow can branch on the decision instead of failing: + +- When the action is permitted, `authorized` is `true` and the result includes `signature_id`, `action_id`, `timestamp`, `verification_url`, and the base64 ML-DSA-65 signature in `signature_b64`. +- When the action is blocked, `authorized` is `false` and the result includes a machine `reason` (`policy_blocked`, `emergency_halt`, `delegation_denied`, `quarantine`, or `denied`) plus a human-readable `detail`. A policy block also returns `attestation_hash` and a `signed_deny` envelope, which is itself a verifiable signed denial receipt. ### Verify Signature -Verifies a signature by its ID. This is a public endpoint - no authentication needed. Returns the verification status, signing agent, action details, and timestamp. +Verifies a signature by its ID. This is a public endpoint - no authentication needed. Returns `verified`, the signing `agent_id` and `agent_name`, the `action_type`, the `algorithm` (e.g. ML-DSA-65), the `signed_at` timestamp, and the `verification_url`. ### Request Action -Creates a multi-party signing session for high-risk actions. The action must be approved by enough signing entities before it is authorized. Use this as a pre-execution gate in workflows. +Creates a multi-party signing session for high-risk actions. The action must be approved by enough signing entities before it is authorized. Use this as a pre-execution gate in workflows. Returns the `session_id`, `status`, `approvals_required`, `signatures_collected`, `action_type`, `created_at`, and `expires_at`. ## Setup diff --git a/manifest.yaml b/manifest.yaml index 5ad0540..a3bf475 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -1,15 +1,11 @@ -version: 0.0.2 +version: 0.0.3 type: plugin author: asqav name: asqav label: - en_US: asqav - zh_Hans: asqav - pt_BR: asqav + en_US: Asqav description: en_US: AI agent governance via asqav.com. Audit trails, policy enforcement, and compliance. Sign agent actions, verify signatures, and request multi-party authorized actions. - zh_Hans: AI agent governance via asqav.com. Audit trails, policy enforcement, and compliance. Sign agent actions, verify signatures, and request multi-party authorized actions. - pt_BR: AI agent governance via asqav.com. Audit trails, policy enforcement, and compliance. Sign agent actions, verify signatures, and request multi-party authorized actions. icon: icon.svg resource: memory: 268435456 @@ -23,7 +19,7 @@ plugins: tools: - provider/asqav.yaml meta: - version: 0.0.2 + version: 0.0.3 arch: - amd64 - arm64 diff --git a/provider/asqav.yaml b/provider/asqav.yaml index 0ac24da..e918fc5 100644 --- a/provider/asqav.yaml +++ b/provider/asqav.yaml @@ -2,13 +2,9 @@ identity: author: asqav name: asqav label: - en_US: asqav - zh_Hans: asqav - pt_BR: asqav + en_US: Asqav description: en_US: AI agent governance, audit trails, and compliance. Sign agent actions with ML-DSA-65 via asqav.com. - zh_Hans: AI agent governance, audit trails, and compliance. Sign agent actions with ML-DSA-65 via asqav.com. - pt_BR: AI agent governance, audit trails, and compliance. Sign agent actions with ML-DSA-65 via asqav.com. icon: icon.svg tags: - utilities @@ -17,33 +13,21 @@ credentials_for_provider: type: secret-input required: true label: - en_US: asqav API Key - zh_Hans: asqav API Key - pt_BR: asqav API Key + en_US: Asqav API Key placeholder: - en_US: Enter your asqav API key (sk_...) - zh_Hans: Enter your asqav API key (sk_...) - pt_BR: Enter your asqav API key (sk_...) + en_US: Enter your Asqav API key (sk_...) help: en_US: Get your API key at asqav.com - zh_Hans: Get your API key at asqav.com - pt_BR: Get your API key at asqav.com url: https://asqav.com asqav_agent_id: type: text-input required: true label: en_US: Agent ID - zh_Hans: Agent ID - pt_BR: Agent ID placeholder: - en_US: Your asqav agent ID (agent_...) - zh_Hans: Your asqav agent ID (agent_...) - pt_BR: Your asqav agent ID (agent_...) + en_US: Your Asqav agent ID (agent_...) help: en_US: The agent ID to use for signing actions - zh_Hans: The agent ID to use for signing actions - pt_BR: The agent ID to use for signing actions tools: - tools/sign_action.yaml - tools/verify_signature.yaml diff --git a/tests/test_tools_smoke.py b/tests/test_tools_smoke.py index 0a02780..5e6c806 100644 --- a/tests/test_tools_smoke.py +++ b/tests/test_tools_smoke.py @@ -14,7 +14,7 @@ from dify_plugin.entities.tool import ToolInvokeMessage, ToolRuntime from tools.request_action import RequestActionTool -from tools.sign_action import SignActionTool +from tools.sign_action import SignActionTool, _denied_result from tools.verify_signature import VerifySignatureTool @@ -31,6 +31,17 @@ def _make_tool(tool_cls, credentials: dict[str, Any]): return tool +def _json_and_var_names(messages): + """Split tool output into its single JSON message and the variable names emitted.""" + json_msgs = [m for m in messages if m.type == ToolInvokeMessage.MessageType.JSON] + var_names = { + m.message.variable_name + for m in messages + if m.type == ToolInvokeMessage.MessageType.VARIABLE + } + return json_msgs, var_names + + @pytest.fixture def credentials() -> dict[str, Any]: return { @@ -77,7 +88,9 @@ def test_sign_action_posts_expected_body( assert kwargs["json"]["action_type"] == "tool:execute" assert kwargs["json"]["context"] == {"foo": "bar"} - assert len(messages) == 1 + json_msgs, var_names = _json_and_var_names(messages) + assert len(json_msgs) == 1 + assert {"authorized", "signature_id", "verification_url"} <= var_names def test_sign_action_treats_invalid_json_context_as_raw( @@ -132,7 +145,9 @@ def test_verify_signature_calls_public_endpoint( fake_get.assert_called_once() args, _ = fake_get.call_args assert args[0].endswith("/verify/sig_123") - assert len(messages) == 1 + json_msgs, var_names = _json_and_var_names(messages) + assert len(json_msgs) == 1 + assert {"verified", "signature_id", "algorithm"} <= var_names def test_request_action_builds_signing_session_body( @@ -167,4 +182,65 @@ def test_request_action_builds_signing_session_body( assert kwargs["json"]["agent_id"] == credentials["asqav_agent_id"] assert kwargs["json"]["action_type"] == "transfer:funds" assert kwargs["json"]["params"] == {"amount": 100} - assert len(messages) == 1 + json_msgs, var_names = _json_and_var_names(messages) + assert len(json_msgs) == 1 + assert {"session_id", "status", "approvals_required"} <= var_names + + +def test_sign_action_surfaces_403_without_raising( + mocker, credentials: dict[str, Any], fake_response_factory +) -> None: + """A 403 policy decision is yielded as one structured message, never raised.""" + mocker.patch( + "tools.sign_action.httpx.post", + return_value=fake_response_factory( + status_code=403, + json_data={ + "detail": "Organization is under emergency halt; signing is disabled" + }, + ), + ) + + tool = _make_tool(SignActionTool, credentials) + messages = list(tool._invoke({"action_type": "refund:approve", "context": ""})) + + json_msgs, var_names = _json_and_var_names(messages) + assert len(json_msgs) == 1 + assert {"authorized", "reason"} <= var_names + + +@pytest.mark.parametrize( + ("detail", "expected_reason"), + [ + ("Organization is under emergency halt; signing is disabled", "emergency_halt"), + ("Action type is outside the agent's delegated scope", "delegation_denied"), + ("Agent's delegation has expired", "delegation_denied"), + ("Agent is in quarantine mode (read/verify only, signing disabled)", "quarantine"), + ("Some unmapped refusal text", "denied"), + ], +) +def test_denied_result_maps_string_details( + detail: str, expected_reason: str, fake_response_factory +) -> None: + """String-form 403 details map to a stable machine reason.""" + result = _denied_result(fake_response_factory(status_code=403, json_data={"detail": detail})) + + assert result["authorized"] is False + assert result["reason"] == expected_reason + assert result["detail"] == detail + assert result["signed_deny"] is None + + +def test_denied_result_passes_through_signed_deny_envelope(fake_response_factory) -> None: + """A policy/content block returns policy_blocked with its signed denial receipt.""" + detail = { + "error": "action_denied", + "attestation_hash": "abc123", + "signed_deny": {"body": {}, "signature_b64": "ZmFrZQ==", "algorithm": "ML-DSA-65"}, + } + result = _denied_result(fake_response_factory(status_code=403, json_data={"detail": detail})) + + assert result["authorized"] is False + assert result["reason"] == "policy_blocked" + assert result["attestation_hash"] == "abc123" + assert result["signed_deny"]["signature_b64"] == "ZmFrZQ==" diff --git a/tools/_outputs.py b/tools/_outputs.py new file mode 100644 index 0000000..f7b8803 --- /dev/null +++ b/tools/_outputs.py @@ -0,0 +1,19 @@ +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + + +def emit(tool: Tool, payload: dict[str, Any]) -> Generator[ToolInvokeMessage]: + """Emit a result as one JSON message plus one variable message per field. + + A Dify workflow node can address a tool's individual outputs by name + (``{{#node.field#}}``) only when the tool emits variable messages; a JSON + message alone surfaces under the single ``json`` output and leaves named + selectors unresolved at runtime. Emitting both keeps the ``json`` blob and + makes every field resolvable as ``[node, key]``. + """ + yield tool.create_json_message(payload) + for key, value in payload.items(): + yield tool.create_variable_message(key, value) diff --git a/tools/request_action.py b/tools/request_action.py index c00fbf1..3b15bbb 100644 --- a/tools/request_action.py +++ b/tools/request_action.py @@ -7,6 +7,8 @@ from dify_plugin import Tool from dify_plugin.entities.tool import ToolInvokeMessage +from tools._outputs import emit + API_BASE = "https://api.asqav.com/api/v1" @@ -36,7 +38,7 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag response.raise_for_status() data = response.json() - yield self.create_json_message({ + yield from emit(self, { "session_id": data["session_id"], "status": data["status"], "approvals_required": data["approvals_required"], diff --git a/tools/request_action.yaml b/tools/request_action.yaml index 65671e7..c080911 100644 --- a/tools/request_action.yaml +++ b/tools/request_action.yaml @@ -3,13 +3,9 @@ identity: author: asqav label: en_US: Request Action - zh_Hans: Request Action - pt_BR: Request Action description: human: en_US: Request a multi-party authorized action. Creates a signing session that requires entity approvals before the action is authorized. Use this as a pre-execution gate for high-risk actions. - zh_Hans: Request a multi-party authorized action. Creates a signing session that requires entity approvals before the action is authorized. Use this as a pre-execution gate for high-risk actions. - pt_BR: Request a multi-party authorized action. Creates a signing session that requires entity approvals before the action is authorized. Use this as a pre-execution gate for high-risk actions. llm: Request a multi-party authorized action for an agent. Creates a signing session that requires multiple entity approvals. Returns the session ID, status, required approvals count, and current signatures collected. Use this as a gate before executing high-risk actions. parameters: - name: action_type @@ -17,12 +13,8 @@ parameters: required: true label: en_US: Action Type - zh_Hans: Action Type - pt_BR: Action Type human_description: en_US: Type of action requiring authorization, e.g. "deploy:production" - zh_Hans: Type of action requiring authorization, e.g. "deploy:production" - pt_BR: Type of action requiring authorization, e.g. "deploy:production" llm_description: The action type that needs multi-party authorization. Examples - "deploy:production", "delete:resource", "transfer:funds". form: llm - name: params @@ -30,14 +22,34 @@ parameters: required: false label: en_US: Parameters (JSON) - zh_Hans: Parameters (JSON) - pt_BR: Parameters (JSON) human_description: en_US: Optional JSON string with parameters for the action - zh_Hans: Optional JSON string with parameters for the action - pt_BR: Optional JSON string with parameters for the action llm_description: Optional JSON string with parameters for the action. Example - {"target":"us-east-1","version":"2.0"} form: llm extra: python: source: tools/request_action.py +output_schema: + type: object + properties: + session_id: + type: string + description: ID of the multi-party signing session. + status: + type: string + description: Current session status, e.g. pending, approved, or expired. + approvals_required: + type: number + description: Number of entity approvals required before authorization. + signatures_collected: + type: number + description: Number of approvals collected so far. + action_type: + type: string + description: The action type requiring authorization. + created_at: + type: string + description: ISO 8601 timestamp when the session was created. + expires_at: + type: string + description: ISO 8601 timestamp when the session expires. diff --git a/tools/sign_action.py b/tools/sign_action.py index cadc473..2cbf8d5 100644 --- a/tools/sign_action.py +++ b/tools/sign_action.py @@ -7,8 +7,18 @@ from dify_plugin import Tool from dify_plugin.entities.tool import ToolInvokeMessage +from tools._outputs import emit + API_BASE = "https://api.asqav.com/api/v1" +#: Substring of a string-form 403 detail -> stable machine reason (halt/delegation/quarantine). +_STRING_REASONS: tuple[tuple[str, str], ...] = ( + ("emergency halt", "emergency_halt"), + ("delegated scope", "delegation_denied"), + ("delegation has expired", "delegation_denied"), + ("quarantine", "quarantine"), +) + class SignActionTool(Tool): def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: @@ -33,12 +43,61 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag }, timeout=30.0, ) + + # A 403 is a policy decision the workflow can branch on; other non-2xx stays loud below. + if response.status_code == 403: + yield from emit(self, _denied_result(response)) + return + response.raise_for_status() data = response.json() - yield self.create_json_message({ + yield from emit(self, { + "authorized": True, "signature_id": data["signature_id"], "action_id": data["action_id"], "timestamp": data["timestamp"], "verification_url": data["verification_url"], + "signature_b64": data.get("signature_b64"), }) + + +def _denied_result(response: httpx.Response) -> dict[str, Any]: + """Normalize a 403 refusal into a branchable ``authorized=false`` result. + + The Asqav cloud returns two ``detail`` shapes for a refused sign: a dict + ``{"error", "attestation_hash", "signed_deny"}`` for a policy or content + block, or a plain string for emergency halt, delegation, and quarantine + refusals. A policy block carries a signed denial receipt (``signed_deny``) + that is itself verifiable. Both shapes map to ``authorized=false``; the + caller never sees this when a signature was actually minted. + """ + try: + detail: Any = response.json().get("detail") + except (ValueError, KeyError, AttributeError): + detail = None + + if isinstance(detail, dict): + is_policy = detail.get("error") == "action_denied" + return { + "authorized": False, + "reason": "policy_blocked" if is_policy else "denied", + "detail": detail.get("error", "action_denied"), + "attestation_hash": detail.get("attestation_hash"), + "signed_deny": detail.get("signed_deny"), + } + + text = detail if isinstance(detail, str) and detail else "Action denied by policy" + reason = "denied" + lowered = text.lower() + for needle, mapped in _STRING_REASONS: + if needle in lowered: + reason = mapped + break + return { + "authorized": False, + "reason": reason, + "detail": text, + "attestation_hash": None, + "signed_deny": None, + } diff --git a/tools/sign_action.yaml b/tools/sign_action.yaml index 25e978c..6a480b5 100644 --- a/tools/sign_action.yaml +++ b/tools/sign_action.yaml @@ -3,26 +3,18 @@ identity: author: asqav label: en_US: Sign Action - zh_Hans: Sign Action - pt_BR: Sign Action description: human: - en_US: Sign an agent action with ML-DSA-65. Creates a tamper-evident audit record via asqav.com for AI agent governance and compliance. Returns the signature ID and verification URL. - zh_Hans: Sign an agent action with ML-DSA-65. Creates a tamper-evident audit record via asqav.com for AI agent governance and compliance. Returns the signature ID and verification URL. - pt_BR: Sign an agent action with ML-DSA-65. Creates a tamper-evident audit record via asqav.com for AI agent governance and compliance. Returns the signature ID and verification URL. - llm: Sign an agent action with ML-DSA-65 for AI agent governance and compliance. Provide an action_type string (e.g. "read:data", "api:call", "tool:execute") and optional context as a JSON string. Returns the signature ID, action ID, timestamp, and a public verification URL. + en_US: Sign an agent action with ML-DSA-65. Creates a tamper-evident audit record via asqav.com for AI agent governance and compliance. Returns authorized=true with the signature ID and verification URL when the action is permitted, or authorized=false with a reason when a policy, an emergency halt, or a delegation limit blocks it. + llm: Sign an agent action with ML-DSA-65 for AI agent governance and compliance. Provide an action_type string (e.g. "read:data", "api:call", "tool:execute") and optional context as a JSON string. Returns a JSON object with an "authorized" boolean. When true, it includes signature_id, action_id, timestamp, verification_url, and signature_b64. When false, it includes a "reason" (policy_blocked, emergency_halt, delegation_denied, quarantine, or denied) and a human-readable "detail" so the workflow can branch instead of failing. parameters: - name: action_type type: string required: true label: en_US: Action Type - zh_Hans: Action Type - pt_BR: Action Type human_description: en_US: Type of action being signed, e.g. "read:data", "api:call", "tool:execute" - zh_Hans: Type of action being signed, e.g. "read:data", "api:call", "tool:execute" - pt_BR: Type of action being signed, e.g. "read:data", "api:call", "tool:execute" llm_description: The action type string that identifies the kind of action being signed. Examples - "read:data", "api:call", "tool:execute", "deploy:production". form: llm - name: context @@ -30,14 +22,43 @@ parameters: required: false label: en_US: Context (JSON) - zh_Hans: Context (JSON) - pt_BR: Context (JSON) human_description: en_US: Optional JSON string with additional context for the action - zh_Hans: Optional JSON string with additional context for the action - pt_BR: Optional JSON string with additional context for the action llm_description: Optional JSON string with metadata about the action. Example - {"file":"config.json","user":"admin"} form: llm extra: python: source: tools/sign_action.py +output_schema: + type: object + properties: + authorized: + type: boolean + description: True if the action was signed and permitted, false if blocked by policy, halt, delegation, or quarantine. + reason: + type: string + description: Machine reason when authorized is false - policy_blocked, emergency_halt, delegation_denied, quarantine, or denied. Absent when authorized. + detail: + type: string + description: Human-readable explanation of a denial. + attestation_hash: + type: string + description: Hash of the attestation for a content or policy block. Null otherwise. + signed_deny: + type: object + description: Verifiable signed denial for a policy block. Null otherwise. + signature_id: + type: string + description: The signature ID when authorized. Starts with sig_. + action_id: + type: string + description: The signed action ID when authorized. + timestamp: + type: number + description: Unix epoch timestamp (seconds) of the signature. + verification_url: + type: string + description: Public URL to verify the signature. + signature_b64: + type: string + description: Base64-encoded ML-DSA-65 signature. diff --git a/tools/verify_signature.py b/tools/verify_signature.py index 3932d4b..8d02c2f 100644 --- a/tools/verify_signature.py +++ b/tools/verify_signature.py @@ -6,6 +6,8 @@ from dify_plugin import Tool from dify_plugin.entities.tool import ToolInvokeMessage +from tools._outputs import emit + API_BASE = "https://api.asqav.com/api/v1" @@ -20,7 +22,7 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag response.raise_for_status() data = response.json() - yield self.create_json_message({ + yield from emit(self, { "verified": data["verified"], "signature_id": data["signature_id"], "agent_id": data["agent_id"], diff --git a/tools/verify_signature.yaml b/tools/verify_signature.yaml index 1dbe99d..ed634bc 100644 --- a/tools/verify_signature.yaml +++ b/tools/verify_signature.yaml @@ -3,28 +3,47 @@ identity: author: asqav label: en_US: Verify Signature - zh_Hans: Verify Signature - pt_BR: Verify Signature description: human: - en_US: Verify an asqav signature by its ID. This is a public endpoint that requires no authentication. Returns whether the signature is valid, the agent that created it, and the action details. - zh_Hans: Verify an asqav signature by its ID. This is a public endpoint that requires no authentication. Returns whether the signature is valid, the agent that created it, and the action details. - pt_BR: Verify an asqav signature by its ID. This is a public endpoint that requires no authentication. Returns whether the signature is valid, the agent that created it, and the action details. - llm: Verify an asqav signature by its signature ID. Returns verification status, the signing agent name/ID, the action type, and the timestamp. No authentication needed. + en_US: Verify an Asqav signature by its ID. This is a public endpoint that requires no authentication. Returns whether the signature is valid, the agent that created it, and the action details. + llm: Verify an Asqav signature by its signature ID. Returns verification status, the signing agent name/ID, the action type, and the timestamp. No authentication needed. parameters: - name: signature_id type: string required: true label: en_US: Signature ID - zh_Hans: Signature ID - pt_BR: Signature ID human_description: en_US: The signature ID to verify (e.g. sig_abc123) - zh_Hans: The signature ID to verify (e.g. sig_abc123) - pt_BR: The signature ID to verify (e.g. sig_abc123) - llm_description: The asqav signature ID to verify. Usually starts with "sig_". + llm_description: The Asqav signature ID to verify. Usually starts with "sig_". form: llm extra: python: source: tools/verify_signature.py +output_schema: + type: object + properties: + verified: + type: boolean + description: True if the signature is valid. + signature_id: + type: string + description: The verified signature ID. + agent_id: + type: string + description: ID of the agent that produced the signature. + agent_name: + type: string + description: Display name of the signing agent. + action_type: + type: string + description: The action type that was signed. + algorithm: + type: string + description: Signature algorithm, e.g. ML-DSA-65. + signed_at: + type: string + description: ISO 8601 timestamp of when the action was signed. + verification_url: + type: string + description: Public URL to verify the signature.