Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <noreply@homericintelligence.dev>")
set(CPACK_PACKAGE_HOMEPAGE_URL
"https://github.com/HomericIntelligence/ProjectKeystone")

# Resource files
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions helm/projectkeystone/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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=<sha-or-semver>` in CI/CD pipelines.
# Avoid `latest` here: it breaks rollbacks and cache invalidation.
tag: ""

imagePullSecrets: []
nameOverride: ""
Expand All @@ -46,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:
Expand Down
8 changes: 8 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 0 additions & 5 deletions requirements.txt

This file was deleted.

58 changes: 56 additions & 2 deletions src/keystone/nats_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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")
4 changes: 3 additions & 1 deletion tests/test_nats_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading