diff --git a/orchestrator/compose.yml b/orchestrator/compose.yml index fee538c..c7bf2b0 100644 --- a/orchestrator/compose.yml +++ b/orchestrator/compose.yml @@ -49,6 +49,8 @@ services: - /tree/branch-tree.json - --timers - /timers + - --kept + - /kept - --vendor - /app/vendor - --cleanup @@ -117,6 +119,12 @@ services: read_only: true bind: create_host_path: false + - type: bind + source: "${KVB_ORCHESTRATOR_KEPT:?written into .env by scripts/install.sh}" + target: /kept + read_only: true + bind: + create_host_path: false - type: bind source: "${KVB_ORCHESTRATOR_APPROVERS:?written into .env by scripts/install.sh}" target: /approvers diff --git a/orchestrator/kvb_orchestrator/hub.py b/orchestrator/kvb_orchestrator/hub.py index 10364cc..51d86ea 100644 --- a/orchestrator/kvb_orchestrator/hub.py +++ b/orchestrator/kvb_orchestrator/hub.py @@ -49,6 +49,7 @@ class Places: vendor: Path cleanup: Path offices: Path + kept: Path def environment(templates: Path) -> jinja2.Environment: @@ -89,6 +90,7 @@ class Hub(Mcp, Reviewing, Asking, Messaging, Choosing, Approving, Denying, Reque agent_sessions: Path model: Model timers: Path + kept: Path settings: Settings redirects: Redirects speech: Speech @@ -137,6 +139,7 @@ def open_hub(council: Path, pages: Path, public_port: int, state: Path, host_sta agent_sessions=state / SNAPSHOT, model=model, timers=places.timers, + kept=places.kept, council=council, pages=pages, public_port=public_port, diff --git a/orchestrator/server.py b/orchestrator/server.py index 7030b87..ea3e696 100755 --- a/orchestrator/server.py +++ b/orchestrator/server.py @@ -25,6 +25,8 @@ --tree-file: the JSON file `pkgmgr branch tree --output` writes, mounted read-only; read again whenever it changes. --timers: directory holding the systemd user units (*.timer). + --kept: ~/.local/state/agents-council, mounted read-only: gates.db and + the resources/ files of the host sampler. --vendor: directory holding the vendored Bootstrap Icons sprite, 3d-force-graph and three with their licence texts. --cleanup: host-mounted directory the hub writes signed branch deletions @@ -78,7 +80,7 @@ def main() -> int: parser.add_argument("--bind", required=True) for name in ("--listen-port", "--public-port"): parser.add_argument(name, type=int, required=True) - paths = ("--council", "--pages", "--state", "--host-state", "--skills", "--transcripts", "--templates", "--tree-file", "--timers", "--vendor", "--cleanup", "--offices", "--llm-key-file", "--approvers") + paths = ("--council", "--pages", "--state", "--host-state", "--skills", "--transcripts", "--templates", "--tree-file", "--timers", "--kept", "--vendor", "--cleanup", "--offices", "--llm-key-file", "--approvers") for name in (*paths, "--piper", "--voices", "--whisper", "--whisper-model", "--scratch"): parser.add_argument(name, type=Path, required=True) for name in ("--llm-url", "--llm-model", "--operator-voice"): @@ -94,7 +96,7 @@ def main() -> int: transcripts=args.transcripts, scripts=Scripts(skills=args.skills, council=args.council, home=args.state), templates=args.templates, - places=Places(tree_file=args.tree_file, timers=args.timers, vendor=args.vendor, cleanup=args.cleanup, offices=args.offices), + places=Places(tree_file=args.tree_file, timers=args.timers, vendor=args.vendor, cleanup=args.cleanup, offices=args.offices, kept=args.kept), model=Model(url=args.llm_url.strip(), model=args.llm_model.strip(), key=read_key(args.llm_key_file)), engines=Engines(piper=args.piper, voices=args.voices, operator_voice=args.operator_voice, whisper=args.whisper, whisper_model=args.whisper_model, scratch=args.scratch), approvers=args.approvers, diff --git a/scripts/orchestrator.sh b/scripts/orchestrator.sh index cd08030..67f4552 100644 --- a/scripts/orchestrator.sh +++ b/scripts/orchestrator.sh @@ -150,7 +150,7 @@ install_hub() { port="$(port_of "${REPO_ROOT}/orchestrator/.env")" log "skills: starting the Agent Orchestrator on http://localhost:${port}/" rm -rf "${HUB}" - mkdir -p "${HUB}" "${STATE}" "${APPROVALS}" "${COUNCIL}" "${HOME}/.claude/projects" "${TREE_DIR}" "${UNITS}" "${APPROVERS}" + mkdir -p "${HUB}" "${STATE}" "${APPROVALS}" "${COUNCIL}" "${HOME}/.claude/projects" "${TREE_DIR}" "${UNITS}" "${APPROVERS}" "${HOME}/.local/state/agents-council/resources" chmod 700 "${APPROVERS}" cp -a "${REPO_ROOT}/orchestrator/." "${HUB}/" # Python loads a cached .pyc whose header matches the source mtime and size, and cp -a keeps the mtime: one planted in the sandbox-writable tree would run on the host @@ -158,8 +158,8 @@ install_hub() { chmod +x "${HUB}/client/permission_hook.py" "${HUB}/client/runner.py" "${HUB}/client/approval.py" "${HUB}/client/cleanup.py" "${HUB}/client/offices.py" "${HUB}/client/sessions.py" log "skills: the PermissionRequest hook client is ${HUB}/client/permission_hook.py" log "skills: approvals need a pinned passkey: settings, 'Passkey registrieren', then 'make orchestrator-pin CREDENTIAL='; until then every executor refuses" - printf 'KVB_ORCHESTRATOR_USER=%s:%s\nKVB_ORCHESTRATOR_STATE=%s\nKVB_ORCHESTRATOR_COUNCIL=%s\nKVB_ORCHESTRATOR_SKILLS=%s\nKVB_ORCHESTRATOR_PROJECTS=%s\nKVB_ORCHESTRATOR_TREE=%s\nKVB_ORCHESTRATOR_TIMERS=%s\nKVB_ORCHESTRATOR_APPROVERS=%s\n' \ - "$(id -u)" "$(id -g)" "${STATE}" "${COUNCIL}" "${HOME}/.claude/skills" "${HOME}/.claude/projects" "${TREE_DIR}" "${UNITS}" "${APPROVERS}" >>"${HUB}/.env" + printf 'KVB_ORCHESTRATOR_USER=%s:%s\nKVB_ORCHESTRATOR_STATE=%s\nKVB_ORCHESTRATOR_COUNCIL=%s\nKVB_ORCHESTRATOR_SKILLS=%s\nKVB_ORCHESTRATOR_PROJECTS=%s\nKVB_ORCHESTRATOR_TREE=%s\nKVB_ORCHESTRATOR_TIMERS=%s\nKVB_ORCHESTRATOR_APPROVERS=%s\nKVB_ORCHESTRATOR_KEPT=%s\n' \ + "$(id -u)" "$(id -g)" "${STATE}" "${COUNCIL}" "${HOME}/.claude/skills" "${HOME}/.claude/projects" "${TREE_DIR}" "${UNITS}" "${APPROVERS}" "${HOME}/.local/state/agents-council" >>"${HUB}/.env" if ! docker compose --file "${HUB}/compose.yml" up -d --build; then warn "skills: docker compose failed; the Agent Orchestrator stays down until 'make orchestrator-install' succeeds." fi diff --git a/tests/hub_case.py b/tests/hub_case.py index 11dfe4a..674c68d 100644 --- a/tests/hub_case.py +++ b/tests/hub_case.py @@ -47,11 +47,12 @@ class HubCase(unittest.TestCase): self.projects = base / "projects" self.tree_file = base / "tree" / "branch-tree.json" self.timers = base / "timers" + self.kept = base / "kept" self.cleanup = base / "state" / "cleanup" self.offices = base / "state" / "offices" self.approvers = base / "config" / "approvers.json" self.replay = base / "replay" - for path in (self.pages, self.state, self.outside, self.projects, self.tree_file.parent, self.timers): + for path in (self.pages, self.state, self.outside, self.projects, self.tree_file.parent, self.timers, self.kept): path.mkdir() silence = mock.patch.object(server.Handler, "log_message") silence.start() @@ -70,7 +71,7 @@ class HubCase(unittest.TestCase): transcripts=self.projects, scripts=self.scripts, templates=HUB / "templates", - places=server.Places(tree_file=self.tree_file, timers=self.timers, vendor=HUB / "vendor", cleanup=self.cleanup, offices=self.offices), + places=server.Places(tree_file=self.tree_file, timers=self.timers, vendor=HUB / "vendor", cleanup=self.cleanup, offices=self.offices, kept=self.kept), model=server.Model(url="", model="", key=""), engines=self.engines, approvers=self.approvers, diff --git a/tests/test_hub_speech.py b/tests/test_hub_speech.py index 6d11db6..26adb58 100644 --- a/tests/test_hub_speech.py +++ b/tests/test_hub_speech.py @@ -175,7 +175,7 @@ class TestHubSpeech(HubCase): args = { "--bind": "127.0.0.1", "--listen-port": "0", "--public-port": "1", "--council": str(self.council), "--pages": str(self.pages), "--state": str(self.state), "--host-state": str(HOST_STATE), "--skills": str(self.outside), "--transcripts": str(self.projects), "--templates": str(HUB / "templates"), - "--tree-file": str(self.tree_file), "--timers": str(self.timers), "--vendor": str(HUB / "vendor"), "--cleanup": str(self.cleanup), "--offices": str(self.offices), + "--tree-file": str(self.tree_file), "--timers": str(self.timers), "--kept": str(self.kept), "--vendor": str(HUB / "vendor"), "--cleanup": str(self.cleanup), "--offices": str(self.offices), "--llm-key-file": os.devnull, "--llm-url": "", "--llm-model": "", "--piper": str(self.engines.piper), "--voices": str(self.engines.voices), "--operator-voice": "en_US-test-high:9", "--whisper": str(self.engines.whisper), "--whisper-model": str(self.engines.whisper_model), "--scratch": str(self.engines.scratch), "--approvers": str(self.approvers),