diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed1fb89 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# agentplane + +Agentplane is the tenant-side control and execution plane for local-first and hybrid agents. + +This repository is not the local supervisor and it is not the canonical wire-spec repository. Instead, it is the remote control-plane and worker-plane complement to the device-local runtime. + +## What already exists here + +The current repository already contains useful runtime artifact scaffolds and local-state conventions: + +- `schemas/session-artifact.schema.v0.1.json` +- `schemas/promotion-artifact.schema.v0.1.json` +- `schemas/reversal-artifact.schema.v0.1.json` +- `schemas/bundle.schema.patch.json` +- `state/pointers/.keep` +- `.gitignore` rules for local `artifacts/` and machine-local pointer state + +Those files tell us two important things: + +1. Agentplane already assumes evidence-bearing runtime artifacts. +2. Agentplane already assumes machine-local pointer state should not be committed. + +## Repository role + +Agentplane owns the **tenant-side** parts of the first local-hybrid slice: + +- gateway and ingress policy handoff for remote-eligible tasks +- capability resolution from logical capability ID to worker binding +- worker runtime envelopes for remote execution +- promotion and reversal semantics for future side-effecting flows +- tenant-side evidence handoff hooks + +Agentplane does **not** own: + +- the local supervisor runtime (`sociosphere`) +- the canonical deterministic transport and fixtures (`TriTRPC`) +- the shared cross-repo contract canon (`socioprophet-standards-storage`) + +## Planned layout + +- `docs/` — architecture notes, slice definitions, repo map +- `gateway/` — tenant ingress and policy-gated dispatch adapters +- `capability-registry/` — logical capability descriptors and bindings +- `worker-runtime/` — tenant execution wrappers and runtime contracts +- `schemas/` — artifact schemas and patch fragments used by runtime flows + +## Current implementation stance + +The first slice is deliberately narrow: + +- local-first planning and retrieval +- optional tenant execution only after policy approval +- typed capability resolution +- evidence append and replay/cairn materialization +- no public-provider egress by default +- no generic multi-agent prompt soup + +See `docs/local_hybrid_slice_v0.md` for the execution slice and `docs/repository_map.md` for cross-repo boundaries. diff --git a/capability-registry/README.md b/capability-registry/README.md new file mode 100644 index 0000000..77673f6 --- /dev/null +++ b/capability-registry/README.md @@ -0,0 +1,12 @@ +# capability-registry + +Logical capability descriptors and runtime bindings live here. + +Initial responsibilities: + +- map capability IDs to execution bindings +- record execution-lane constraints +- record timeout and context limits +- record side-effect posture and credential scope requirements + +The first concrete example to support is a narrow capability such as `summarize.abstractive.v1`. diff --git a/capability-registry/examples/summarize.abstractive.v1.json b/capability-registry/examples/summarize.abstractive.v1.json new file mode 100644 index 0000000..cc60494 --- /dev/null +++ b/capability-registry/examples/summarize.abstractive.v1.json @@ -0,0 +1,33 @@ +{ + "capabilityId": "summarize.abstractive.v1", + "version": "1.0.0", + "kind": "analysis", + "description": "Deterministic stub binding for abstractive summarization with risk extraction.", + "inputSchemaRef": "org.socioprophet.capabilities.v1.SummarizeInput", + "outputSchemaRef": "org.socioprophet.capabilities.v1.SummarizeOutput", + "execution": { + "supportedLanes": ["local", "tenant"], + "defaultLane": "local", + "requiresGpu": false, + "maxContextBytes": 16384, + "timeoutSeconds": 60 + }, + "trust": { + "egressDefault": "deny", + "sideEffectsDefault": "deny", + "dataLabelsAllowed": ["public", "internal", "tenant_confidential"], + "dataLabelsDenied": ["regulated_export_controlled"] + }, + "policyHooks": { + "preExec": ["policy.v1.Decision/Evaluate"], + "postExec": ["evidence.v1.Event/Append"] + }, + "binding": { + "capabilityInstanceId": "capinst.summarize.abstractive.v1.stub", + "executionLane": "tenant", + "workerEndpoint": "tritrpc://tenant/summarize-01", + "workerContract": "worker.v1.Capability/Execute", + "credentialScope": "task-scoped", + "bindingTtlSeconds": 120 + } +} diff --git a/capability-registry/resolve_binding_stub.py b/capability-registry/resolve_binding_stub.py new file mode 100644 index 0000000..5fe8b54 --- /dev/null +++ b/capability-registry/resolve_binding_stub.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Minimal capability resolution stub for the first local-hybrid slice. + +This script resolves a logical capability descriptor into a runtime binding. +It intentionally uses only the Python standard library so it can run in a bare +repository checkout. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def load_descriptor(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def resolve_binding(descriptor: dict[str, Any], requested_lane: str | None = None) -> dict[str, Any]: + execution = descriptor.get("execution", {}) + binding = descriptor.get("binding", {}) + supported_lanes = execution.get("supportedLanes", []) + lane = requested_lane or binding.get("executionLane") or execution.get("defaultLane") + if lane not in supported_lanes: + raise ValueError(f"unsupported lane: {lane!r}; supported={supported_lanes!r}") + return { + "resolved": True, + "binding": { + "capabilityInstanceId": binding["capabilityInstanceId"], + "executionLane": lane, + "workerEndpoint": binding["workerEndpoint"], + "workerContract": binding["workerContract"], + "credentialScope": binding["credentialScope"], + "bindingTtlSeconds": binding["bindingTtlSeconds"], + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("descriptor", type=Path) + parser.add_argument("--lane", default=None) + args = parser.parse_args() + descriptor = load_descriptor(args.descriptor) + result = resolve_binding(descriptor, requested_lane=args.lane) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/local_hybrid_slice_v0.md b/docs/local_hybrid_slice_v0.md new file mode 100644 index 0000000..3b93865 --- /dev/null +++ b/docs/local_hybrid_slice_v0.md @@ -0,0 +1,98 @@ +# Local-Hybrid Slice v0 + +## Purpose + +This document freezes the first end-to-end execution slice for Agentplane. + +The slice is intentionally narrow. It exists to prove the architecture, not to implement every capability class at once. + +## Scope + +The first slice is: + +- a local-first request enters the device-local supervisor +- local retrieval and local task planning run first +- policy decides whether any remote execution is permitted +- Agentplane resolves a remote capability when policy allows it +- a tenant worker executes the bound capability +- evidence is appended +- a replay/cairn handle is materialized + +## Seven-method lifecycle + +1. `supervisor.v1.Session/Open` +2. `supervisor.v1.Task/Plan` +3. `policy.v1.Decision/Evaluate` +4. `control.v1.Capability/Resolve` +5. `worker.v1.Capability/Execute` +6. `evidence.v1.Event/Append` +7. `replay.v1.Cairn/Materialize` + +Agentplane owns the tenant-side responsibilities for steps 4 and 5 directly, and may mirror or participate in 3 and 6 where tenant policy and evidence relays are required. + +## What Agentplane already has + +The repo already contains artifact schema scaffolds for: + +- session artifacts +- promotion artifacts +- reversal artifacts +- bundle spec patch fields for runtime behavior + +These are useful because they establish the repo as a runtime artifact plane rather than only a conceptual architecture bucket. + +## What Agentplane must add next + +### Gateway + +The gateway is the tenant ingress for remote-eligible work. It should: + +- accept already-classified and policy-scoped work from the local supervisor +- validate capability binding requests +- reject out-of-policy egress or side-effect requests +- emit tenant-side evidence handoff events + +### Capability registry + +The capability registry maps a logical capability ID to an execution binding. A binding should minimally describe: + +- capability instance ID +- worker endpoint +- supported execution lanes +- timeout and context limits +- side-effect posture +- required credentials or scopes + +### Worker runtime + +The worker runtime wraps the remote execution contract. It should: + +- execute only typed capability payloads +- run with scoped credentials +- record input and output digests +- emit provenance metadata suitable for evidence append + +## Relation to existing schemas + +The existing artifact schemas are not wasted work. They align with the future execution lifecycle as follows: + +- `session-artifact.schema.v0.1.json` supports session-level receipts and replay references +- `promotion-artifact.schema.v0.1.json` supports later promotion/review flows for side-effecting actions +- `reversal-artifact.schema.v0.1.json` supports rollback/reversal for promoted changes +- `bundle.schema.patch.json` already introduces runtime-oriented fields such as `sessionPolicyRef`, `skillRefs`, `memoryNamespace`, `worktreeStrategy`, `rolloutFlags`, `telemetrySink`, and `receiptSchemaVersion` + +## Non-goals for v0 + +- generalized autonomous multi-agent swarms +- unconstrained public-provider model egress +- long-lived secret material inside workers +- untyped prompt-only worker contracts +- cloud-first session authority + +## Immediate follow-on work + +1. Add gateway scaffolding. +2. Add capability-registry scaffolding. +3. Add worker-runtime scaffolding. +4. Add examples that bind a single capability such as `summarize.abstractive.v1`. +5. Align shared schemas and fixtures with `TriTRPC` and `socioprophet-standards-storage`. diff --git a/docs/repository_map.md b/docs/repository_map.md new file mode 100644 index 0000000..ca7c88c --- /dev/null +++ b/docs/repository_map.md @@ -0,0 +1,66 @@ +# Agentplane Repository Map + +## Cross-repo ownership + +### Agentplane + +Tenant-side control and execution responsibilities: + +- gateway and remote ingress +- capability resolution and binding +- tenant worker runtime wrappers +- promotion and reversal runtime artifacts +- tenant-side evidence relay hooks + +### Sociosphere + +Device-local orchestration responsibilities: + +- local supervisor +- local planning, retrieval, and execution precedence +- deterministic multi-repo orchestration + +### TriTRPC + +Deterministic transport and fixture responsibilities: + +- method and envelope canon +- fixture vectors +- verification and repack invariants +- cross-language interoperability surface + +### socioprophet-standards-storage + +Shared contracts and measurement responsibilities: + +- shared schemas +- benchmark definitions +- storage and interface standards +- governance and portability measurements + +## Internal layout for Agentplane + +### `schemas/` +Runtime artifact schemas and patch fragments. + +### `docs/` +Architecture notes and slice definitions. + +### `gateway/` +Tenant ingress for remote-eligible work. + +### `capability-registry/` +Logical capability descriptors and runtime bindings. + +### `worker-runtime/` +Tenant worker execution wrappers and contract adapters. + +## First-slice sequence boundary + +- `supervisor.v1.Session/Open` — local +- `supervisor.v1.Task/Plan` — local +- `policy.v1.Decision/Evaluate` — local first, tenant mirror optional +- `control.v1.Capability/Resolve` — tenant +- `worker.v1.Capability/Execute` — tenant +- `evidence.v1.Event/Append` — shared with local precedence +- `replay.v1.Cairn/Materialize` — local first diff --git a/evidence/append_event_stub.py b/evidence/append_event_stub.py new file mode 100644 index 0000000..7d3d567 --- /dev/null +++ b/evidence/append_event_stub.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Deterministic evidence append stub for the first local-hybrid slice.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def canonical_bytes(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def append_event(payload: dict[str, Any]) -> dict[str, Any]: + event = payload.get("event", payload) + digest = hashlib.sha256(canonical_bytes(event)).hexdigest() + journal_offset = int(digest[:12], 16) + return { + "appended": True, + "journalOffset": journal_offset, + "evidenceDigest": f"sha256:{digest}", + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("payload", type=Path) + args = parser.parse_args() + result = append_event(load_json(args.payload)) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 0000000..3e72d78 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,12 @@ +# gateway + +Tenant ingress for policy-scoped remote work. + +Initial responsibilities: + +- accept remote-eligible work only after policy approval upstream +- validate capability binding requests +- reject side-effecting or out-of-policy execution attempts +- emit tenant-side evidence relay events + +This directory is a scaffold for the first local-hybrid slice and should stay narrow until the typed execution path is implemented. diff --git a/gateway/dispatch_stub.py b/gateway/dispatch_stub.py new file mode 100644 index 0000000..a73c2da --- /dev/null +++ b/gateway/dispatch_stub.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Minimal tenant gateway dispatch stub for the first local-hybrid slice.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def build_dispatch(task: dict[str, Any], decision: dict[str, Any], binding: dict[str, Any]) -> dict[str, Any]: + if not decision.get("allow", False): + raise ValueError("policy decision denies remote dispatch") + transforms = decision.get("requiredTransformations", []) + payload = { + "taskId": task["taskId"], + "capabilityInstanceId": binding["binding"]["capabilityInstanceId"], + "workerEndpoint": binding["binding"]["workerEndpoint"], + "executionLane": binding["binding"]["executionLane"], + "requiredTransformations": transforms, + "input": task["input"], + } + payload_bytes = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + payload["dispatchDigest"] = "sha256:" + hashlib.sha256(payload_bytes).hexdigest() + return payload + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("task", type=Path) + parser.add_argument("decision", type=Path) + parser.add_argument("binding", type=Path) + args = parser.parse_args() + dispatch = build_dispatch(load_json(args.task), load_json(args.decision), load_json(args.binding)) + print(json.dumps(dispatch, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/replay/materialize_cairn_stub.py b/replay/materialize_cairn_stub.py new file mode 100644 index 0000000..e0928b1 --- /dev/null +++ b/replay/materialize_cairn_stub.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Deterministic replay/cairn materialization stub for the first local-hybrid slice.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def canonical_bytes(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def materialize(payload: dict[str, Any]) -> dict[str, Any]: + digest = hashlib.sha256(canonical_bytes(payload)).hexdigest() + task_id = payload["taskId"] + journal_offset = payload["journalOffset"] + artifact_id = f"artifact:{digest[:16]}" + return { + "cairnId": f"sha256:{digest}", + "replayHandle": f"cairn://{task_id}/{journal_offset}", + "artifacts": [ + { + "artifactId": artifact_id, + "digest": f"sha256:{hashlib.sha256((task_id + str(journal_offset)).encode('utf-8')).hexdigest()}" + } + ] + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("payload", type=Path) + args = parser.parse_args() + result = materialize(load_json(args.payload)) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/worker-runtime/README.md b/worker-runtime/README.md new file mode 100644 index 0000000..25ee1ab --- /dev/null +++ b/worker-runtime/README.md @@ -0,0 +1,12 @@ +# worker-runtime + +Tenant worker execution wrappers and contract adapters live here. + +Initial responsibilities: + +- accept only typed capability payloads +- run with scoped credentials +- record input and output digests +- emit provenance metadata for evidence append + +This directory should remain tightly bounded to the first local-hybrid slice until the typed execution path is verified end to end. diff --git a/worker-runtime/execute_stub.py b/worker-runtime/execute_stub.py new file mode 100644 index 0000000..fb6f902 --- /dev/null +++ b/worker-runtime/execute_stub.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Deterministic worker execution stub for the first local-hybrid slice.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def summarize_chunks(chunks: list[dict[str, Any]]) -> str: + texts = [str(chunk.get("text", "")).strip() for chunk in chunks] + joined = " ".join(part for part in texts if part) + compact = " ".join(joined.split()) + return compact[:280] + + +def execute(request: dict[str, Any]) -> dict[str, Any]: + chunks = request.get("input", {}).get("context", {}).get("chunks", []) + summary = summarize_chunks(chunks) + input_digest = "sha256:" + hashlib.sha256( + json.dumps(request, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + output = { + "summary": summary, + "risks": [ + {"id": "r1", "text": "Replay semantics incomplete."}, + {"id": "r2", "text": "Tenant failover remains unspecified in the stub path."} + ] + } + output_digest = "sha256:" + hashlib.sha256( + json.dumps(output, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + return { + "taskId": request["taskId"], + "status": "completed", + "output": output, + "provenance": { + "workerId": "worker:summarize-01", + "modelId": "model:deterministic-stub-01", + "toolchain": ["agentplane.worker-runtime.execute_stub"], + "inputDigest": input_digest, + "outputDigest": output_digest + } + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("request", type=Path) + args = parser.parse_args() + result = execute(load_json(args.request)) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())