diff --git a/configs/agent-term.local.example.json b/configs/agent-term.local.example.json new file mode 100644 index 0000000..feec1d0 --- /dev/null +++ b/configs/agent-term.local.example.json @@ -0,0 +1,87 @@ +{ + "workspace": "sourceos-local", + "defaultChannel": "!sourceos-ops", + "eventStore": { + "driver": "sqlite", + "path": ".agent-term/events.sqlite3" + }, + "matrix": { + "enabled": false, + "homeserverUrl": "https://matrix.example.org", + "userId": "@agent-term:example.org", + "deviceName": "agent-term-local", + "rooms": { + "sourceosOps": "!sourceos-ops:example.org", + "agentRegistry": "!agent-registry:example.org", + "policyFabric": "!policyfabric:example.org", + "memoryMesh": "!memory-mesh:example.org", + "github": "!github:example.org" + }, + "requireEncryptedRoomPostureForSensitiveContext": true, + "preserveBridgeMetadata": true, + "preserveRedactions": true, + "preserveMembershipEvents": true + }, + "agentRegistration": { + "requireRegisteredParticipants": true, + "failClosedWhenRegistryUnavailable": true, + "repository": "SocioProphet/agent-registry", + "fixturePath": "configs/fixtures/agent-registry.local.example.json", + "tokenEnv": "AGENT_TERM_AGENT_REGISTRY_TOKEN", + "timeoutSeconds": 5, + "requiredFor": [ + "hermes", + "codex", + "claudeCode", + "openclaw", + "matrixBots", + "githubBots", + "ciBots", + "mcpTools", + "localProcessAgents" + ] + }, + "policyFabric": { + "repository": "SocioProphet/policy-fabric", + "fixturePath": "configs/fixtures/policy-fabric.local.example.json", + "tokenEnv": "AGENT_TERM_POLICY_FABRIC_TOKEN", + "timeoutSeconds": 5 + }, + "participants": { + "github": { + "enabled": true, + "mode": "repo-branch-pr", + "requireAgentRegistryResolution": true, + "agentRegistryId": "agent.github", + "requirePolicyApprovalForMutation": true + }, + "codex": { + "enabled": true, + "mode": "repo-branch-pr", + "requireAgentRegistryResolution": true, + "agentRegistryId": "agent.codex", + "requirePolicyApprovalForMutation": true + }, + "claude-code": { + "enabled": true, + "mode": "repo-branch-pr", + "requireAgentRegistryResolution": true, + "agentRegistryId": "agent.claude-code", + "requirePolicyApprovalForMutation": true + }, + "mcp": { + "enabled": true, + "mode": "tool-plane", + "requireAgentRegistryResolution": true, + "agentRegistryId": "agent.mcp", + "requirePolicyApprovalForSideEffects": true + } + }, + "localRuntime": { + "registeredAgents": [], + "toolGrants": [], + "allowPolicies": [], + "denyPolicies": [], + "pendingPolicies": [] + } +} diff --git a/configs/fixtures/agent-registry.local.example.json b/configs/fixtures/agent-registry.local.example.json new file mode 100644 index 0000000..37574cd --- /dev/null +++ b/configs/fixtures/agent-registry.local.example.json @@ -0,0 +1,81 @@ +{ + "agents": [ + { + "agent_id": "agent.github", + "registry_ref": "fixture://agent-registry/agent.github", + "spec_version": "v1.local", + "runtime_authority": "agent-registry", + "status": "registered", + "session_id": "session-agent-github-local", + "tool_grants": [ + "grant.repo-write" + ] + }, + { + "agent_id": "agent.codex", + "registry_ref": "fixture://agent-registry/agent.codex", + "spec_version": "v1.local", + "runtime_authority": "agent-registry", + "status": "registered", + "session_id": "session-agent-codex-local", + "tool_grants": [ + "grant.repo-read", + "grant.repo-write" + ] + }, + { + "agent_id": "agent.claude-code", + "registry_ref": "fixture://agent-registry/agent.claude-code", + "spec_version": "v1.local", + "runtime_authority": "agent-registry", + "status": "registered", + "session_id": "session-agent-claude-code-local", + "tool_grants": [ + "grant.repo-read" + ] + }, + { + "agent_id": "agent.mcp", + "registry_ref": "fixture://agent-registry/agent.mcp", + "spec_version": "v1.local", + "runtime_authority": "agent-registry", + "status": "registered", + "session_id": "session-agent-mcp-local", + "tool_grants": [ + "grant.context-read" + ] + } + ], + "tool_grants": [ + { + "grant_id": "grant.repo-read", + "agent_id": "agent.codex", + "tool": "repo-read", + "status": "active" + }, + { + "grant_id": "grant.repo-write", + "agent_id": "agent.codex", + "tool": "repo-write", + "status": "active" + }, + { + "grant_id": "grant.repo-read", + "agent_id": "agent.claude-code", + "tool": "repo-read", + "status": "active" + }, + { + "grant_id": "grant.repo-write", + "agent_id": "agent.github", + "tool": "repo-write", + "status": "active" + }, + { + "grant_id": "grant.context-read", + "agent_id": "agent.mcp", + "tool": "context-read", + "status": "active" + } + ] +} diff --git a/configs/fixtures/matrix-sync.local.example.json b/configs/fixtures/matrix-sync.local.example.json new file mode 100644 index 0000000..26a87ec --- /dev/null +++ b/configs/fixtures/matrix-sync.local.example.json @@ -0,0 +1,40 @@ +{ + "next_batch": "local-batch-2", + "rooms": { + "join": { + "!sourceos-ops:example.org": { + "timeline": { + "events": [ + { + "event_id": "$local-message-1", + "sender": "@operator:example.org", + "type": "m.room.message", + "content": { + "body": "AgentTerm local operator flow is online.", + "msgtype": "m.text", + "m.relates_to": { + "rel_type": "m.thread", + "event_id": "$local-thread-root" + } + }, + "unsigned": { + "bridge": { + "network": "local-fixture", + "channel": "sourceos-ops" + } + } + }, + { + "event_id": "$local-member-1", + "sender": "@agent-term:example.org", + "type": "m.room.member", + "content": { + "membership": "join" + } + } + ] + } + } + } + } +} diff --git a/configs/fixtures/policy-fabric.local.example.json b/configs/fixtures/policy-fabric.local.example.json new file mode 100644 index 0000000..487df3d --- /dev/null +++ b/configs/fixtures/policy-fabric.local.example.json @@ -0,0 +1,42 @@ +{ + "decisions": [ + { + "decision_id": "decision.allow.matrix-service.matrix_service_send.local", + "action": "matrix-service.matrix_service_send", + "status": "allow", + "policy_ref": "fixture://policy-fabric/matrix-service-send", + "obligations": [ + "record-audit", + "preserve-matrix-event-id" + ] + }, + { + "decision_id": "decision.allow.memory-mesh.memory_recall.local", + "action": "memory-mesh.memory_recall", + "status": "allow", + "policy_ref": "fixture://policy-fabric/memory-recall", + "obligations": [ + "record-context-pack", + "preserve-workroom-scope" + ] + }, + { + "decision_id": "decision.allow.github.pr.create.local", + "action": "github.pr.create", + "status": "allow", + "policy_ref": "fixture://policy-fabric/github-pr-create", + "obligations": [ + "record-pr-url", + "preserve-agent-id", + "preserve-tool-grant" + ] + }, + { + "decision_id": "decision.deny.github.repo.delete.local", + "action": "github.repo.delete", + "status": "deny", + "policy_ref": "fixture://policy-fabric/github-repo-delete", + "reason": "Repository deletion is not allowed from AgentTerm local operator flow." + } + ] +} diff --git a/docs/operator-quickstart.md b/docs/operator-quickstart.md new file mode 100644 index 0000000..9b594d7 --- /dev/null +++ b/docs/operator-quickstart.md @@ -0,0 +1,131 @@ +# AgentTerm operator quickstart + +This quickstart exercises the current local AgentTerm control loop without requiring live Matrix, Agent Registry, or Policy Fabric services. + +It demonstrates the intended operator path: + +```text +agent-term-check + -> agent-term-matrix normalize-sync + -> agent-term-dispatch + -> agent-term-snapshot +``` + +The fixtures in `configs/fixtures/` are local examples only. They are not authority. In production, Agent Registry and Policy Fabric remain the authority for agent identity, grants, sessions, revocation, and policy admission. + +## 1. Install for local development + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install -e '.[dev]' +``` + +## 2. Check configured service seams + +```bash +agent-term-check \ + --config configs/agent-term.local.example.json \ + --agent-id agent.github \ + --tool repo-write \ + --policy-action github.pr.create +``` + +Expected posture: + +- Matrix reports `warn` because the local example uses the offline/in-memory backend. +- Agent Registry reports `ok` because `agent.github` and the `repo-write` grant resolve from the local fixture. +- Policy Fabric reports `ok` because `github.pr.create` resolves from the local fixture. + +Use strict mode when warnings should fail an operator preflight: + +```bash +agent-term-check \ + --config configs/agent-term.local.example.json \ + --strict +``` + +## 3. Normalize a Matrix sync payload + +```bash +agent-term-matrix \ + --config configs/agent-term.local.example.json \ + normalize-sync configs/fixtures/matrix-sync.local.example.json \ + --persist \ + --save-state +``` + +This persists normalized Matrix events into `.agent-term/events.sqlite3` and saves `next_batch` plus joined room IDs into `.agent-term/matrix-state.json`. + +Show current Matrix state: + +```bash +agent-term-matrix \ + --config configs/agent-term.local.example.json \ + state +``` + +## 4. Dispatch a policy-gated Matrix send + +```bash +agent-term-matrix \ + --config configs/agent-term.local.example.json \ + send sourceosOps "AgentTerm dispatch pipeline is online." +``` + +The send goes through the AgentTerm dispatch pipeline. It is admitted by the local Policy Fabric fixture and sent through the offline Matrix backend unless live Matrix config is enabled. + +## 5. Dispatch a registered GitHub participant action + +```bash +agent-term-dispatch \ + --config configs/agent-term.local.example.json \ + --tool repo-write \ + --policy-action github.pr.create \ + github github_mutation '!github' 'Create PR for AgentTerm operator quickstart' +``` + +Expected event flow: + +1. Original `github.github_mutation` event is recorded. +2. Agent Registry resolves `agent.github`. +3. Agent Registry resolves `repo-write` grant. +4. Policy Fabric admits `github.pr.create`. +5. Registered participant adapter records the invocation. + +## 6. Dispatch a governed Memory Mesh recall + +```bash +agent-term-dispatch \ + --config configs/agent-term.local.example.json \ + --metadata-json '{"query":"operator quickstart context","policy_action":"memory-mesh.memory_recall","workroom":"operator-quickstart","topic_scope":"sourceos-agentterm"}' \ + memory-mesh memory_recall '!memory-mesh' 'Recall operator quickstart context' +``` + +Expected event flow: + +1. Original `memory-mesh.memory_recall` event is recorded. +2. Policy Fabric admits `memory-mesh.memory_recall`. +3. Memory Mesh adapter records a context-pack reference. + +## 7. Render the operator snapshot + +```bash +agent-term-snapshot \ + --db .agent-term/events.sqlite3 \ + --limit 100 +``` + +The snapshot groups events into operator panes such as Matrix rooms, agents, approvals, context, runs, shells, and evidence. + +## 8. Clean local state + +```bash +rm -rf .agent-term +``` + +## Notes on live services + +Live Matrix requires `matrix.enabled=true` in config and `AGENT_TERM_MATRIX_ACCESS_TOKEN` in the environment. Do not place access tokens in JSON config. + +Live Agent Registry and Policy Fabric endpoints can be configured with `agentRegistration.endpointUrl` and `policyFabric.endpointUrl`. The local examples use fixtures so the flow remains runnable in CI and on developer machines without service credentials. diff --git a/tests/test_operator_examples.py b/tests/test_operator_examples.py new file mode 100644 index 0000000..71665e1 --- /dev/null +++ b/tests/test_operator_examples.py @@ -0,0 +1,80 @@ +from pathlib import Path + +from agent_term.agent_registry_service import JsonFileAgentRegistryBackend +from agent_term.config import load_config +from agent_term.matrix_service import normalize_sync_payload +from agent_term.policy_fabric_service import JsonFilePolicyFabricBackend +from agent_term.events import AgentTermEvent + + +REPO_ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = REPO_ROOT / "configs" / "agent-term.local.example.json" +AGENT_FIXTURE_PATH = REPO_ROOT / "configs" / "fixtures" / "agent-registry.local.example.json" +POLICY_FIXTURE_PATH = REPO_ROOT / "configs" / "fixtures" / "policy-fabric.local.example.json" +MATRIX_SYNC_FIXTURE_PATH = REPO_ROOT / "configs" / "fixtures" / "matrix-sync.local.example.json" + + +def test_local_example_config_loads_and_points_to_fixtures(): + config = load_config(CONFIG_PATH) + + assert config.workspace == "sourceos-local" + assert config.agent_registration.fixture_path == "configs/fixtures/agent-registry.local.example.json" + assert config.policy_fabric.fixture_path == "configs/fixtures/policy-fabric.local.example.json" + assert config.participant_agent_id("github") == "agent.github" + assert config.matrix.rooms["sourceosOps"] == "!sourceos-ops:example.org" + + +def test_agent_registry_fixture_resolves_quickstart_agent_and_grant(): + backend = JsonFileAgentRegistryBackend(AGENT_FIXTURE_PATH) + + agent = backend.resolve_agent("agent.github") + grant = backend.resolve_tool_grant("agent.github", "repo-write") + + assert agent is not None + assert agent.session_id == "session-agent-github-local" + assert grant is not None + assert grant.grant_id == "grant.repo-write" + + +def test_policy_fabric_fixture_resolves_quickstart_decisions(): + backend = JsonFilePolicyFabricBackend(POLICY_FIXTURE_PATH) + + allow = backend.evaluate( + AgentTermEvent( + channel="!github", + sender="@operator", + kind="github_mutation", + source="github", + body="Create PR", + metadata={"policy_action": "github.pr.create"}, + ) + ) + deny = backend.evaluate( + AgentTermEvent( + channel="!github", + sender="@operator", + kind="github_mutation", + source="github", + body="Delete repo", + metadata={"policy_action": "github.repo.delete"}, + ) + ) + + assert allow is not None + assert allow.is_allowed is True + assert deny is not None + assert deny.is_allowed is False + assert deny.reason == "Repository deletion is not allowed from AgentTerm local operator flow." + + +def test_matrix_sync_fixture_normalizes_to_events(): + import json + + payload = json.loads(MATRIX_SYNC_FIXTURE_PATH.read_text(encoding="utf-8")) + batch = normalize_sync_payload(payload) + + assert batch.next_batch == "local-batch-2" + assert len(batch.events) == 2 + assert batch.events[0].event_id == "$local-message-1" + assert batch.events[0].thread_root_event_id == "$local-thread-root" + assert batch.events[1].membership == "join"