Tool Authoring Guide: ARVIS
Position in ARVIS Architecture
Tools are NOT executed directly.
They are executed via the Syscall System:
Pipeline → Intent → Kernel → Syscall (tool.execute) → Tool
Overview
A Tool is a capability exposed to the Cognitive OS.
It must: - be deterministic when possible - handle errors safely - expose a clear contract
Base Class
from arvis.tools.base import BaseTool
class MyTool(BaseTool):
name = "my_tool"
description = "Does something useful"
def execute(self, input_data):
...
Input Structure
Prefer the structured invocation API:
def execute_invocation(self, invocation: ToolInvocation):
payload = invocation.payload
identity = invocation.effect_context
idempotency_key = invocation.idempotency_key
The legacy execute(input_data) adapter is restricted to:
input_data = {
"tool_payload": dict,
"effect_context": AuthorizedEffectContext,
"idempotency_key": str | None,
"invocation": ToolInvocation,
}
There is no pipeline context, raw authentication object or syscall service
container in either API.
Execution Constraints
Tools MUST:
- be callable via syscall only
- not be invoked directly by the pipeline
- not modify cognitive reasoning
Example
class EchoTool(BaseTool):
name = "echo"
def execute(self, input_data):
payload = input_data["tool_payload"]
return {"echo": payload}
Validation (optional but recommended)
def validate(self, input_data):
if "tool_payload" not in input_data:
raise ValueError("Missing payload")
Best Practices
1. Pure vs Side Effects
Type Example Pure compute, transform Side-effect API call, DB write
2. Idempotency
Declaring an operation idempotent is only the first step:
ToolSpec(
name="payments.capture",
description="Capture one payment",
idempotent=True,
)
Every effectful adapter MUST forward the exact key provided by ARVIS rather than generating a new one:
def execute_invocation(self, invocation: ToolInvocation):
return provider.capture(
payload=invocation.payload,
idempotency_key=invocation.idempotency_key,
)
Legacy adapters receive the same value under
input_data["idempotency_key"]. The key is stable for one logical action,
persisted in the pre-effect outbox and bound by the audit receipt through the
intent commitment. A retry or crash-recovery worker MUST reuse the persisted
key exactly. idempotent=True without forwarding the key is only a declaration
and does not make an external effect safely replayable.
3. Error Handling
Never swallow errors:
raise Exception("clear message")
Executor will capture it.
4. Latency Awareness
Execution time is automatically tracked.
Anti-patterns forbidden
- reading identity, tenant or target from a pipeline context
- carrying credentials, clients or database sessions in effect context
- hidden global state
- non-deterministic behavior without reason
- blocking calls without timeout
ToolSpec and the Capability Manifest
A ToolSpec describes a tool's contract, its execution semantics, and its
governance. Beyond the basic identity and schemas, the spec carries a
capability manifest: declarative metadata a host reads to govern
sovereignty, egress and consent uniformly across local tools and external
(e.g. MCP) tools.
from arvis.tools.spec import ToolSpec
ToolSpec(
name="my_tool",
description="...",
input_schema={...},
output_schema={...},
# execution semantics
idempotent=False,
retryable=True,
side_effectful=True,
timeout_seconds=None,
# governance (enforced by ToolPolicyEvaluator, except where noted)
max_risk=1.0, # deny if the request risk exceeds this ceiling
requires_confirmation=False, # deny with a confirmation-required decision
audit_required=False, # declarative: tool.execute always journals
reversible=True, # False marks an effect that cannot be undone
# capability manifest (opaque to ARVIS; the host interprets the labels)
provider=None, # third-party service ("google", "notion"), or None
data_egress=False, # True if it sends the caller's data outbound
data_class="unspecified", # host-defined sensitivity ("public", "personal", ...)
required_consent=None, # opaque consent key the host must have granted
)
Sovereignty taxonomy
The manifest yields three tool classes, derived from provider and
data_egress (a host reads spec.crosses_trust_boundary, i.e.
provider is not None):
| Class | provider | data_egress | Meaning |
|---|---|---|---|
| sovereign | None |
False |
No third party; stays in the local boundary. |
| connected (read) | set | False |
Talks to a provider, inbound fetch only. |
| connected (egress) | set | True |
Sends the caller's data outbound; maximal scrutiny. |
ARVIS does not interpret provider, data_class or required_consent -- they
are opaque labels. The host maps them onto its own consent system, data taxonomy
and egress policy (for example, denying egress of a confidential data class,
or gating a tool on a granted consent). Declaring a manifest is how a new tool,
local or external, becomes governable without bespoke policy code.
Examples
# Sovereign: reads a local encrypted store, no third party.
ToolSpec(name="calendar.read", description="Read the calendar.",
side_effectful=False, required_consent="calendar_read")
# Connected (egress): publishes the caller's content to a third party.
ToolSpec(name="notion.create_page", description="Create a page in Notion.",
side_effectful=True, reversible=False,
provider="notion", data_egress=True, data_class="personal",
required_consent="notion_access")
Testing
Minimal test:
def test_tool():
tool = MyTool()
result = tool.execute({"tool_payload": {...}})
assert result is not None
Summary
A good tool is:
- predictable
- observable
- bounded
- safe
Calling tools from a host
A host must never call a tool body, ToolManager.run(), or executor capability
methods to perform a business effect. Register the tool, then let CognitiveOS
and its tool.execute syscall perform authorization, outbox persistence,
capability activation, execution, and journaling.
os.register_tool(MyTool())
result = os.run_as(principal=principal, cognitive_input=request)
ToolManager and ToolExecutor remain importable for advanced runtime
composition and Veramem compatibility, but their public effect methods are
fail-closed. Focused test composition helpers live under tests/support, which
is not included in the distributed package.
Veramem integration pattern
Veramem owns business dependencies and injects them when constructing a tool:
from typing import Protocol
from arvis.adapters.tools.invocation import ToolInvocation
from arvis.tools.base import BaseTool
class DocumentService(Protocol):
def delete(
self,
*,
document_id: str,
principal: str,
tenant: str | None,
idempotency_key: str | None,
) -> dict: ...
class VeramemDocumentDeleteTool(BaseTool):
name = "veramem.document.delete"
def __init__(self, document_service: DocumentService) -> None:
self._document_service = document_service
def execute_invocation(self, invocation: ToolInvocation) -> dict:
return self._document_service.delete(
document_id=str(invocation.payload["document_id"]),
principal=invocation.effect_context.principal,
tenant=invocation.effect_context.tenant,
idempotency_key=invocation.idempotency_key,
)
def execute(self, input_data: dict) -> dict: # compatibility contract
raise RuntimeError("structured invocation required")
The service may own a repository, transaction factory or API client. The
invocation never carries those objects. Veramem must also stamp a real
AuthenticatedPrincipal, persist the pre-effect intent through a qualified
database sink, and preserve confirmation/idempotency state across its workers.