diff --git a/BRANCH_TRIAGE.tsv b/BRANCH_TRIAGE.tsv new file mode 100644 index 0000000..d4683b5 --- /dev/null +++ b/BRANCH_TRIAGE.tsv @@ -0,0 +1,42 @@ +branch ahead behind disposition +feat/state-integrity-graph-reasoning-stubs 2 1 INTEGRATED (427745d: tui.py + agent_machine_adapter.py + cli wiring) into integ/agent-term-backlog-batch-1 (PR#90); 2nd commit captured by #89 (identical); backed up to backup/* +copilot/implement-git-workspace-state-classifier 0 207 DELETED captured (fully merged) +feat/network-byom-native-assistant-events 0 217 DELETED captured (fully merged) +feat/office-commands 0 221 DELETED captured (fully merged) +ops-history-dry-run-contracts 0 213 DELETED captured (fully merged) +ops-history-dry-run-demo 0 206 DELETED captured (fully merged) +seed/agentterm-chatops-core 0 193 DELETED captured (fully merged) +sourceos/m1-repo-manifest 0 208 DELETED captured (fully merged) +work/agent-registry-adapter 0 230 DELETED captured (fully merged) +work/agent-registry-service-backend 0 209 DELETED captured (fully merged) +work/ci-smoke-runner 0 209 DELETED captured (fully merged) +work/cloudshell-agentplane-adapters 0 225 DELETED captured (fully merged) +work/dispatch-cli 0 216 DELETED captured (fully merged) +work/generated-interaction-types 0 202 DELETED captured (fully merged) +work/knowledge-adapters 0 226 DELETED captured (fully merged) +work/knowledge-adapters-current 0 227 DELETED captured (fully merged) +work/matrix-adapter-scaffold 0 229 DELETED captured (fully merged) +work/matrix-adapter-scaffold-rebased 0 228 DELETED captured (fully merged) +work/matrix-cli 0 214 DELETED captured (fully merged) +work/matrix-incremental-sync 0 212 DELETED captured (fully merged) +work/matrix-service-backend 0 214 DELETED captured (fully merged) +work/matrix-sync-state 0 96 DELETED captured (fully merged) +work/noetica-interaction-artifact-import 0 199 DELETED captured (fully merged) +work/operator-dispatch-pipeline 0 221 DELETED captured (fully merged) +work/operator-quickstart-docs 0 206 DELETED captured (fully merged) +work/operator-smoke-runner 0 207 DELETED captured (fully merged) +work/pipeline-config-runtime 0 217 DELETED captured (fully merged) +work/policy-fabric-admission 0 229 DELETED captured (fully merged) +work/policy-fabric-service-backend 0 208 DELETED captured (fully merged) +work/predispatch-boundary-v0 0 205 DELETED captured (fully merged) +work/predispatch-boundary-v1 0 203 DELETED captured (fully merged) +work/registered-participants 0 225 DELETED captured (fully merged) +work/release-polish 0 206 DELETED captured (fully merged) +work/service-health-check 0 206 DELETED captured (fully merged) +work/sourceos-contract-sync 0 202 DELETED captured (fully merged) +work/sourceos-interaction-reference-pointer 0 202 DELETED captured (fully merged) +work/sourceos-interaction-render-43 0 202 DELETED captured (fully merged) +work/tui-scaffold 0 226 DELETED captured (fully merged) +work/tui-scaffold-current 0 221 DELETED captured (fully merged) +work/workspace-adapters 0 229 DELETED captured (fully merged) +work/workspace-adapters-current 0 226 DELETED captured (fully merged) diff --git a/src/agent_term/cli.py b/src/agent_term/cli.py index 16b2d76..ecc267e 100644 --- a/src/agent_term/cli.py +++ b/src/agent_term/cli.py @@ -109,6 +109,34 @@ def build_parser() -> argparse.ArgumentParser: sherlock_packet.add_argument("--topic") sherlock_packet.add_argument("--thread-id") + office = subparsers.add_parser( + "office", + help="Record governed Office (prophet-workspace) artifact request events.", + ) + office_sub = office.add_subparsers(dest="office_command", required=True) + + office_deck = office_sub.add_parser("create-deck", help="Request Office slide-deck generation.") + office_deck.add_argument("channel") + office_deck.add_argument("--workroom", default="default") + office_deck.add_argument("--title", required=True) + office_deck.add_argument("--sender", default="@operator") + office_deck.add_argument("--thread-id") + + office_inspect = office_sub.add_parser("inspect", help="Inspect an Office artifact (read-only).") + office_inspect.add_argument("channel") + office_inspect.add_argument("path") + office_inspect.add_argument("--workroom", default="default") + office_inspect.add_argument("--sender", default="@operator") + office_inspect.add_argument("--thread-id") + + office_convert = office_sub.add_parser("convert", help="Request Office artifact conversion.") + office_convert.add_argument("channel") + office_convert.add_argument("path") + office_convert.add_argument("--to", dest="to_format", required=True) + office_convert.add_argument("--workroom", default="default") + office_convert.add_argument("--sender", default="@operator") + office_convert.add_argument("--thread-id") + subparsers.add_parser("shell", help="Open the minimal AgentTerm interactive shell.") # --- state-integrity (#36) --- @@ -342,6 +370,49 @@ def cmd_sherlock_packet(store: EventStore, args: argparse.Namespace) -> int: return append_and_print(store, event) +def cmd_office(store: EventStore, args: argparse.Namespace) -> int: + if args.office_command == "create-deck": + event = make_plane_event( + plane="prophet-workspace", + kind="office_artifact_request", + channel=args.channel, + sender=args.sender, + body=f"Request Office slide-deck generation: {args.title}", + thread_id=args.thread_id, + metadata={"workroom": args.workroom, "artifact": "slide-deck", "title": args.title}, + approval_required=True, + ) + return append_and_print(store, event) + + if args.office_command == "inspect": + event = make_plane_event( + plane="prophet-workspace", + kind="office_artifact_inspection", + channel=args.channel, + sender=args.sender, + body=f"Office artifact inspection: {args.path}", + thread_id=args.thread_id, + metadata={"workroom": args.workroom, "path": args.path}, + approval_required=False, + ) + return append_and_print(store, event) + + if args.office_command == "convert": + event = make_plane_event( + plane="prophet-workspace", + kind="office_artifact_request", + channel=args.channel, + sender=args.sender, + body=f"Request Office conversion to {args.to_format}: {args.path}", + thread_id=args.thread_id, + metadata={"workroom": args.workroom, "path": args.path, "target_format": args.to_format}, + approval_required=True, + ) + return append_and_print(store, event) + + raise SystemExit(f"unknown office command: {args.office_command}") + + def cmd_shell(store: EventStore) -> int: print("AgentTerm shell. Type /help for commands, /quit to exit.") channel = DEFAULT_CHANNEL @@ -550,7 +621,6 @@ def cmd_reasoning_run(args: argparse.Namespace) -> int: def cmd_ops_history(args: argparse.Namespace) -> int: """OpsHistory control plane (issue #33).""" sub = args.oh_command - json_mode = getattr(args, "json_mode", False) if sub == "policy": result = policy_explain(args.profile) print(json.dumps(result, indent=2)) @@ -584,6 +654,8 @@ def main(argv: list[str] | None = None) -> int: return cmd_request_shell(store, args) if args.command == "sherlock-packet": return cmd_sherlock_packet(store, args) + if args.command == "office": + return cmd_office(store, args) if args.command == "shell": return cmd_shell(store) if args.command == "state-integrity": diff --git a/src/agent_term/config.py b/src/agent_term/config.py index b62366f..7917209 100644 --- a/src/agent_term/config.py +++ b/src/agent_term/config.py @@ -41,6 +41,19 @@ class AgentRegistrationConfig: fail_closed_when_registry_unavailable: bool = True repository: str = "SocioProphet/agent-registry" required_for: tuple[str, ...] = () + endpoint_url: str | None = None + fixture_path: str | None = None + token_env: str = "AGENT_TERM_AGENT_REGISTRY_TOKEN" + timeout_seconds: float = 5.0 + + +@dataclass(frozen=True) +class PolicyFabricConfig: + repository: str = "SocioProphet/policy-fabric" + endpoint_url: str | None = None + fixture_path: str | None = None + token_env: str = "AGENT_TERM_POLICY_FABRIC_TOKEN" + timeout_seconds: float = 5.0 @dataclass(frozen=True) @@ -83,6 +96,7 @@ class AgentTermConfig: event_store: EventStoreConfig = field(default_factory=EventStoreConfig) matrix: MatrixConfig = field(default_factory=MatrixConfig) agent_registration: AgentRegistrationConfig = field(default_factory=AgentRegistrationConfig) + policy_fabric: PolicyFabricConfig = field(default_factory=PolicyFabricConfig) planes: dict[str, PlaneConfig] = field(default_factory=dict) participants: dict[str, ParticipantConfig] = field(default_factory=dict) local_runtime: LocalRuntimeFixture = field(default_factory=LocalRuntimeFixture) @@ -120,6 +134,7 @@ def config_from_dict(raw: dict[str, Any]) -> AgentTermConfig: event_store_raw = _dict(raw.get("eventStore")) matrix_raw = _dict(raw.get("matrix")) registration_raw = _dict(raw.get("agentRegistration")) + policy_fabric_raw = _dict(raw.get("policyFabric")) participants_raw = _dict(raw.get("participants")) planes_raw = _dict(raw.get("planes")) local_runtime_raw = _dict(raw.get("localRuntime")) @@ -158,6 +173,21 @@ def config_from_dict(raw: dict[str, Any]) -> AgentTermConfig: ), repository=str(registration_raw.get("repository") or "SocioProphet/agent-registry"), required_for=tuple(str(item) for item in _list(registration_raw.get("requiredFor"))), + endpoint_url=_optional_str(registration_raw.get("endpointUrl")), + fixture_path=_optional_str(registration_raw.get("fixturePath")), + token_env=str( + registration_raw.get("tokenEnv") or "AGENT_TERM_AGENT_REGISTRY_TOKEN" + ), + timeout_seconds=float(registration_raw.get("timeoutSeconds") or 5.0), + ), + policy_fabric=PolicyFabricConfig( + repository=str(policy_fabric_raw.get("repository") or "SocioProphet/policy-fabric"), + endpoint_url=_optional_str(policy_fabric_raw.get("endpointUrl")), + fixture_path=_optional_str(policy_fabric_raw.get("fixturePath")), + token_env=str( + policy_fabric_raw.get("tokenEnv") or "AGENT_TERM_POLICY_FABRIC_TOKEN" + ), + timeout_seconds=float(policy_fabric_raw.get("timeoutSeconds") or 5.0), ), planes=planes, participants=participants, diff --git a/src/agent_term/dispatch_cli.py b/src/agent_term/dispatch_cli.py index 5afe562..3a82175 100644 --- a/src/agent_term/dispatch_cli.py +++ b/src/agent_term/dispatch_cli.py @@ -8,7 +8,8 @@ from pathlib import Path from agent_term.agent_registry import AgentRegistration, AgentRegistryAdapter -from agent_term.agent_registry import InMemoryAgentRegistryBackend, ToolGrant +from agent_term.agent_registry import AgentRegistryBackend, InMemoryAgentRegistryBackend, ToolGrant +from agent_term.agent_registry_service import build_agent_registry_backend_from_config from agent_term.agentplane import AgentPlaneAdapter, InMemoryAgentPlaneBackend from agent_term.cloudshell_fog import CloudShellFogAdapter, InMemoryCloudShellFogBackend from agent_term.config import AgentTermConfig, load_config @@ -38,8 +39,10 @@ InMemoryPolicyFabricBackend, PolicyDecision, PolicyFabricAdapter, + PolicyFabricBackend, ) from agent_term.policy_fabric import action_for_event +from agent_term.policy_fabric_service import build_policy_fabric_backend_from_config from agent_term.store import DEFAULT_DB_PATH, EventStore from agent_term.workspace import ( InMemoryProphetWorkspaceBackend, @@ -115,7 +118,7 @@ def build_event(args: argparse.Namespace, config: AgentTermConfig) -> AgentTermE ) -def build_registry_backend(args: argparse.Namespace, config: AgentTermConfig) -> InMemoryAgentRegistryBackend: +def build_registry_backend(args: argparse.Namespace, config: AgentTermConfig) -> AgentRegistryBackend: agent_ids = set(config.local_runtime.registered_agents) agent_ids.update(args.register_agent) agent_id = args.agent_id or config.participant_agent_id(args.source) @@ -132,7 +135,10 @@ def build_registry_backend(args: argparse.Namespace, config: AgentTermConfig) -> for agent_id in sorted(agent_ids) ] grants = [_parse_grant(raw) for raw in (*config.local_runtime.tool_grants, *args.grant)] - return InMemoryAgentRegistryBackend(agents=agents, grants=grants) + # A config-declared Agent Registry (file-backed or HTTP) is authoritative; + # the CLI-flag / local-runtime agents+grants serve as the fallback. + in_memory = InMemoryAgentRegistryBackend(agents=agents, grants=grants) + return build_agent_registry_backend_from_config(config, fallback=in_memory) def _parse_grant(raw: str) -> ToolGrant: @@ -148,7 +154,7 @@ def build_policy_backend( args: argparse.Namespace, event: AgentTermEvent, config: AgentTermConfig, -) -> InMemoryPolicyFabricBackend: +) -> PolicyFabricBackend: decisions: list[PolicyDecision] = [] for action in (*config.local_runtime.allow_policies, *args.allow_policy): decisions.append(_decision(action, ALLOW, args.policy_ref)) @@ -162,7 +168,10 @@ def build_policy_backend( elif args.sensitive_context and not decisions: decisions.append(_decision(action_for_event(event), ALLOW, args.policy_ref)) - return InMemoryPolicyFabricBackend(decisions) + # A config-declared Policy Fabric service (file-backed or HTTP) is authoritative; + # the CLI-flag / local-runtime decisions serve as the fallback when none is configured. + in_memory = InMemoryPolicyFabricBackend(decisions) + return build_policy_fabric_backend_from_config(config, fallback=in_memory) def _decision(action: str, status: str, policy_ref: str, reason: str | None = None) -> PolicyDecision: diff --git a/src/agent_term/graph_binding.py b/src/agent_term/graph_binding.py index 7350298..52c642b 100644 --- a/src/agent_term/graph_binding.py +++ b/src/agent_term/graph_binding.py @@ -85,12 +85,12 @@ def to_dict(self) -> dict[str, Any]: ], "active_leases": [ { - "lease_ref": l.lease_ref, - "agent_ref": l.agent_ref, - "capability": l.capability, - "decision": l.decision, + "lease_ref": lease.lease_ref, + "agent_ref": lease.agent_ref, + "capability": lease.capability, + "decision": lease.decision, } - for l in self.active_leases + for lease in self.active_leases ], "recent_decisions_count": len(self.recent_decisions), "memory_mesh_connected": self.memory_mesh_connected, @@ -151,12 +151,15 @@ def format_agent_list(agents: list[AgentRegistryEntry], json_mode: bool = False) def format_leases(leases: list[CapabilityLease], json_mode: bool = False) -> str: if json_mode: return json.dumps( - [{"lease_ref": l.lease_ref, "capability": l.capability, "decision": l.decision} for l in leases], + [ + {"lease_ref": lease.lease_ref, "capability": lease.capability, "decision": lease.decision} + for lease in leases + ], indent=2, ) if not leases: return f"No active capability leases. (note: {STUB_NOTE})" lines = ["Active capability leases:"] - for l in leases: - lines.append(f" {l.capability} {l.decision} {l.lease_ref}") + for lease in leases: + lines.append(f" {lease.capability} {lease.decision} {lease.lease_ref}") return "\n".join(lines) diff --git a/src/agent_term/matrix_cli.py b/src/agent_term/matrix_cli.py index 56a2852..d96160e 100644 --- a/src/agent_term/matrix_cli.py +++ b/src/agent_term/matrix_cli.py @@ -12,6 +12,12 @@ from agent_term.dispatch_cli import build_pipeline from agent_term.events import AgentTermEvent from agent_term.matrix_service import normalize_sync_payload +from agent_term.matrix_state import ( + MatrixStateStore, + MatrixSyncState, + resolve_matrix_room, + rooms_from_sync_payload, +) from agent_term.store import DEFAULT_DB_PATH, EventStore @@ -22,6 +28,7 @@ def build_parser() -> argparse.ArgumentParser: ) parser.add_argument("--config", help="Optional AgentTerm JSON config path.") parser.add_argument("--db", help="Path to local AgentTerm SQLite event log.") + parser.add_argument("--state", help="Path to a Matrix sync state JSON file (room map + next_batch).") subparsers = parser.add_subparsers(dest="command", required=True) send = subparsers.add_parser("send", help="Send a Matrix message through the AgentTerm dispatch pipeline.") @@ -41,6 +48,11 @@ def build_parser() -> argparse.ArgumentParser: sync = subparsers.add_parser("normalize-sync", help="Normalize a Matrix /sync JSON payload.") sync.add_argument("payload", help="Path to a Matrix sync payload JSON file, or '-' for stdin.") sync.add_argument("--persist", action="store_true", help="Persist normalized events into EventStore.") + sync.add_argument( + "--save-state", + action="store_true", + help="Record the room map + next_batch cursor into the --state file.", + ) sync.add_argument("--sender", default="@agent-term") sync.add_argument("--channel", default="!matrix-sync") @@ -50,8 +62,14 @@ def build_parser() -> argparse.ArgumentParser: def cmd_send(args: argparse.Namespace) -> int: config = load_config(args.config) db_path = Path(args.db or config.event_store.path or DEFAULT_DB_PATH) + state = ( + MatrixStateStore(Path(args.state)).load() + if getattr(args, "state", None) + else MatrixSyncState.from_dict({}) + ) + room_id = resolve_matrix_room(args.room, config, state) metadata: dict[str, object] = { - "matrix_room_id": args.room, + "matrix_room_id": room_id, "msgtype": args.msgtype, "policy_action": args.policy_action, } @@ -64,7 +82,7 @@ def cmd_send(args: argparse.Namespace) -> int: metadata["matrix_e2ee_verified"] = bool(args.matrix_verified) event = AgentTermEvent( - channel=args.room, + channel=room_id, sender=args.sender, kind="matrix_service_send", source="matrix-service", @@ -120,6 +138,19 @@ def cmd_normalize_sync(args: argparse.Namespace) -> int: store.close() print(f"persisted_events={len(events)}") + if args.save_state: + if not args.state: + raise SystemExit("--save-state requires --state ") + store_state = MatrixStateStore(Path(args.state)) + state = store_state.load() + rooms = rooms_from_sync_payload(payload) + if rooms: + state = state.with_rooms(rooms) + if batch.next_batch: + state = state.with_next_batch(batch.next_batch) + store_state.save(state) + print(f"matrix_state_saved={args.state}") + for event in events: print(f"{event.channel}\t{event.sender}\t{event.kind}\t{event.body}") return 0 diff --git a/src/agent_term/pipeline.py b/src/agent_term/pipeline.py index 62ab5f2..33cb182 100644 --- a/src/agent_term/pipeline.py +++ b/src/agent_term/pipeline.py @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from typing import Iterable from agent_term.adapters import AdapterResult, AgentTermAdapter @@ -85,7 +85,23 @@ def dispatch(self, event: AgentTermEvent) -> DispatchOutcome: if _is_blocked(policy_event): return self._outcome(False, event, persisted, _deny_reason(policy_event)) - adapter = self._select_adapter(event) + # Propagate the admitted policy decision onto the event so downstream + # domain adapters (which require a resolved decision) can proceed. + dispatch_event = event + if policy_event is not None: + decision_ref = policy_event.metadata.get( + "policy_decision_id" + ) or policy_event.metadata.get("policy_decision_ref") + already_present = event.metadata.get("policy_decision_ref") or event.metadata.get( + "policy_decision_id" + ) + if decision_ref and not already_present: + dispatch_event = replace( + event, + metadata={**event.metadata, "policy_decision_ref": decision_ref}, + ) + + adapter = self._select_adapter(dispatch_event) if adapter is None: no_adapter = _result_event( event, @@ -100,7 +116,7 @@ def dispatch(self, event: AgentTermEvent) -> DispatchOutcome: persisted.append(self.store.append(no_adapter)) return self._outcome(False, event, persisted, "no_adapter") - result_event = _result_event(event, adapter.handle(event)) + result_event = _result_event(dispatch_event, adapter.handle(dispatch_event)) persisted.append(self.store.append(result_event)) return self._outcome( not _is_blocked(result_event), diff --git a/src/agent_term/policy_fabric.py b/src/agent_term/policy_fabric.py index 729b3f7..3c476b0 100644 --- a/src/agent_term/policy_fabric.py +++ b/src/agent_term/policy_fabric.py @@ -40,6 +40,8 @@ "ci_retry", "tool_grant", "revocation", + "matrix_service_send", + "office_artifact_request", } )