From 66b52f56350750c8b628ea051f0440987df4968d Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 1/7] fix(helm): default image.tag to chart appVersion instead of 'latest' Closes #530 --- helm/projectkeystone/values.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helm/projectkeystone/values.yaml b/helm/projectkeystone/values.yaml index 9537a993..e0996c19 100644 --- a/helm/projectkeystone/values.yaml +++ b/helm/projectkeystone/values.yaml @@ -22,7 +22,11 @@ replicaCount: 2 image: repository: projectkeystone pullPolicy: IfNotPresent - tag: "latest" + # Default to the chart's appVersion (Chart.yaml: appVersion: "1.0.0") so + # deployments are reproducible and rollbacks are deterministic. Override at + # install time with `--set image.tag=` in CI/CD pipelines. + # Avoid `latest` here: it breaks rollbacks and cache invalidation. + tag: "" imagePullSecrets: [] nameOverride: "" From 04fefcd19f8e65471b2941e766bd8693866a1990 Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 2/7] fix(k8s): set allowPrivilegeEscalation:false and readOnlyRootFilesystem in container securityContext Closes #526 --- helm/projectkeystone/values.yaml | 9 +++++++-- k8s/deployment.yaml | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/helm/projectkeystone/values.yaml b/helm/projectkeystone/values.yaml index e0996c19..98ad8978 100644 --- a/helm/projectkeystone/values.yaml +++ b/helm/projectkeystone/values.yaml @@ -50,8 +50,13 @@ podSecurityContext: runAsUser: 1000 fsGroup: 1000 -# Container security context -securityContext: {} +# Container security context (production hardening — issue #526) +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL # Service configuration service: diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 9e5fd531..3c8b0969 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -38,6 +38,14 @@ spec: image: projectkeystone:latest imagePullPolicy: IfNotPresent + # Container security hardening (issue #526) + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + # Resource limits resources: requests: From ab6c0718cb32c08136f210015f34c6a41513ef2a Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 3/7] fix(packaging): set CPack contact to real maintainer address Closes #528 --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 886ec09e..ca8b9c90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -901,8 +901,10 @@ set(CPACK_PACKAGE_VENDOR "ProjectKeystone Development Team") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Hierarchical Multi-Agent System (HMAS) in C++20") # Version already defined earlier for CMake package config -set(CPACK_PACKAGE_CONTACT "projectkeystone@example.com") -set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/projectkeystone/hmas") +set(CPACK_PACKAGE_CONTACT + "ProjectKeystone Maintainers ") +set(CPACK_PACKAGE_HOMEPAGE_URL + "https://github.com/HomericIntelligence/ProjectKeystone") # Resource files set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") From e2b409e818614b0424630c750a7510c25f760d61 Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 4/7] docs(claude): clarify Python tooling exception to C++20-only mandate Closes #520 --- CLAUDE.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2ef48bf9..7fddbb2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,19 @@ invisible plumbing beneath every other component. ### Primary Language: C++20 -**This project is EXCLUSIVELY C++20. Do NOT use Python, Mojo, or other languages for implementation.** +**The transport runtime in this project is EXCLUSIVELY C++20. Do NOT use +Python, Mojo, or other languages for new transport, message-bus, or +agent-runtime code.** + +**Exception — supporting Python tooling.** A small number of Python modules +remain in `src/keystone/` as a thin orchestration / test harness layer +(`config.py`, `daemon.py`, `dag_walker.py`, `models.py`, `nats_listener.py`, +`task_claimer.py`, `validation.py`, `logging.py`). These predate the ADR-015 +extraction to ProjectAgamemnon and are still imported by the Python tests +under `tests/`. They are maintained in-place but **must not** grow new +production responsibilities — any new orchestration logic belongs in +ProjectAgamemnon, and any new transport logic must be implemented in C++20 +under `src/transport/`, `src/network/`, or `include/`. ### Required Technologies From 0a21eeb0ca81e547007975a4981ef392de3ab54c Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 5/7] docs(readme): document setup-env.sh contents and side-effects Closes #529 --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index cf1e8aaf..1cb2181a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,15 @@ cd ProjectKeystone # Setup environment variables (required for Docker) ./scripts/setup-env.sh +# Writes ./.env with: +# GIT_COMMIT - short SHA of HEAD (or "latest" outside a git checkout), +# baked into image tags so docker compose can pull/build +# reproducible artifacts. +# BUILD_UID - your host UID, mapped into the dev container so files +# written from inside the container are owned by you. +# BUILD_GID - your host GID, same reason. +# Re-run this whenever you switch branches/commits or change UID/GID. +# The script writes only ./.env (gitignored); no other system state changes. # Build and test make docker.build From 15d05bc3b9c4900f2a4238bf14786ebf2b4f5be9 Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 6/7] chore(deps): drop redundant requirements.txt; pyproject.toml is canonical Closes #523 --- requirements.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8d24a0e1..00000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Python dependencies for ProjectKeystone - -# Test dependencies -pytest>=7.0 -pytest-asyncio>=0.21 From ec9d30f63f3ddddf184764c79052035d61921f2d Mon Sep 17 00:00:00 2001 From: Micah Villmow <4211002+mvillmow@users.noreply.github.com> Date: Mon, 11 May 2026 20:15:46 -0700 Subject: [PATCH 7/7] fix(nats_listener): wire dispatch callback into JetStream subscribe; drain on stop Closes #521 Closes #527 --- src/keystone/nats_listener.py | 58 +++++++++++++++++++++++++++++++++-- tests/test_nats_listener.py | 4 ++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/keystone/nats_listener.py b/src/keystone/nats_listener.py index 7dfa563d..66170a62 100644 --- a/src/keystone/nats_listener.py +++ b/src/keystone/nats_listener.py @@ -46,6 +46,26 @@ class NATSListener: def __init__(self, task_claimer: TaskClaimer) -> None: self._task_claimer = task_claimer self._shutting_down: bool = False + self._subscription: Any = None + + async def _dispatch_message(self, msg: Any) -> None: + """JetStream subscription callback: parse subject, validate, dispatch. + + Wired into ``js.subscribe(..., cb=self._dispatch_message)`` in + :meth:`start` so incoming NATS messages reach :meth:`_on_task_event` + instead of being silently dropped (issue #521). + """ + try: + team_id, task_id = self._parse_subject(msg.subject) + except ValueError as exc: + logger.warning( + "nats_event_dropped_invalid_subject", + extra={"subject": msg.subject, "error": str(exc)}, + ) + return + await self._on_task_event( + msg.subject, team_id, task_id, raw_payload=msg.data + ) @property def shutting_down(self) -> bool: @@ -91,7 +111,9 @@ async def start( for attempt in range(1, max_retries + 1): try: - await js.subscribe(subject) + self._subscription = await js.subscribe( + subject, cb=self._dispatch_message + ) logger.info( "nats_listener_subscribed", extra={"subject": subject, "stream": stream}, @@ -237,5 +259,37 @@ def _parse_subject(subject: str) -> tuple[str, str]: return parts[2], parts[3] # team_id, task_id async def stop(self) -> None: - """Drain the NATS connection and release resources.""" + """Drain in-flight messages and unsubscribe from JetStream (issue #527). + + Must be preceded by :meth:`begin_shutdown` if the caller wants new + events that arrive during the drain to be dropped rather than + dispatched. + """ + sub = self._subscription + self._subscription = None + if sub is None: + logger.info("nats_listener_stopped", extra={"subscription": "none"}) + return + + # Drain delivers any in-flight messages to the callback, then + # unsubscribes. Fall back to an explicit unsubscribe if drain is + # unsupported by the underlying client (older nats-py). + try: + drain = getattr(sub, "drain", None) + if drain is not None: + await drain() + else: + await sub.unsubscribe() + except Exception as exc: # noqa: BLE001 — best-effort shutdown + logger.warning( + "nats_listener_stop_drain_failed", + extra={"error": str(exc)}, + ) + try: + await sub.unsubscribe() + except Exception as inner_exc: # noqa: BLE001 + logger.warning( + "nats_listener_stop_unsubscribe_failed", + extra={"error": str(inner_exc)}, + ) logger.info("nats_listener_stopped") diff --git a/tests/test_nats_listener.py b/tests/test_nats_listener.py index e548b62a..085b32e2 100644 --- a/tests/test_nats_listener.py +++ b/tests/test_nats_listener.py @@ -167,7 +167,9 @@ async def test_subscribe_success_uses_subject(self) -> None: listener, _ = _make_listener() nc = _make_mock_nc() await listener.start(nc, "hi.tasks.team-1.>", stream="homeric-tasks") - nc.jetstream().subscribe.assert_awaited_once_with("hi.tasks.team-1.>") + nc.jetstream().subscribe.assert_awaited_once_with( + "hi.tasks.team-1.>", cb=listener._dispatch_message + ) class TestStartStreamNotFound: