feat(T06): hostPath working-memory sync and repo-relative sink paths
Mount the-custodian memory/working from the railiance01 clone (hostPath) so sweep writeback commits daily-triage notes; worker runs as uid 1000; progress events store repo-relative working_memory_path; ops inventory defaults use custodian:// URIs.
This commit is contained in:
parent
70e3154b05
commit
ce03e78e26
10 changed files with 76 additions and 30 deletions
|
|
@ -338,7 +338,7 @@ data:
|
||||||
last_reviewed: "2026-06-05"
|
last_reviewed: "2026-06-05"
|
||||||
policy:
|
policy:
|
||||||
non_secret_inventory: true
|
non_secret_inventory: true
|
||||||
source_of_truth: "/home/worsch/the-custodian/ops/service-inventory.yml"
|
source_of_truth: "custodian://ops/service-inventory.yml"
|
||||||
projection: "Railiance activity-core ConfigMap snapshot for disabled probes"
|
projection: "Railiance activity-core ConfigMap snapshot for disabled probes"
|
||||||
environments:
|
environments:
|
||||||
- id: local
|
- id: local
|
||||||
|
|
@ -1012,6 +1012,10 @@ spec:
|
||||||
app.kubernetes.io/name: actcore-worker
|
app.kubernetes.io/name: actcore-worker
|
||||||
app.kubernetes.io/part-of: activity-core
|
app.kubernetes.io/part-of: activity-core
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
containers:
|
containers:
|
||||||
- name: worker
|
- name: worker
|
||||||
image: activity-core:railiance01-prod
|
image: activity-core:railiance01-prod
|
||||||
|
|
@ -1051,8 +1055,9 @@ spec:
|
||||||
configMap:
|
configMap:
|
||||||
name: actcore-ops-service-inventory
|
name: actcore-ops-service-inventory
|
||||||
- name: working-memory
|
- name: working-memory
|
||||||
persistentVolumeClaim:
|
hostPath:
|
||||||
claimName: actcore-working-memory
|
path: /home/tegwick/the-custodian/memory/working
|
||||||
|
type: DirectoryOrCreate
|
||||||
- name: custodian-runtime
|
- name: custodian-runtime
|
||||||
configMap:
|
configMap:
|
||||||
name: actcore-custodian-runtime
|
name: actcore-custodian-runtime
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ loaded into the railiance01 K3s containerd image store.
|
||||||
`20-runtime.yaml` also projects the disabled Custodian-owned
|
`20-runtime.yaml` also projects the disabled Custodian-owned
|
||||||
`ops-service-inventory-probes.md` ActivityDefinition and a non-secret
|
`ops-service-inventory-probes.md` ActivityDefinition and a non-secret
|
||||||
`actcore-ops-service-inventory` ConfigMap snapshot. The source of truth for the
|
`actcore-ops-service-inventory` ConfigMap snapshot. The source of truth for the
|
||||||
inventory remains `/home/worsch/the-custodian/ops/service-inventory.yml`; update
|
inventory source of truth remains `custodian://ops/service-inventory.yml`; update
|
||||||
the ConfigMap projection from that file before enabling the probe schedule.
|
the ConfigMap projection from that file before enabling the probe schedule.
|
||||||
`OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator
|
`OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator
|
||||||
provisions the Inter-Hub ops-hub key.
|
provisions the Inter-Hub ops-hub key.
|
||||||
|
|
@ -27,7 +27,8 @@ provisions the Inter-Hub ops-hub key.
|
||||||
The same runtime projection now includes the active
|
The same runtime projection now includes the active
|
||||||
`daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema
|
`daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema
|
||||||
and a persistent working-memory volume mounted at
|
and a persistent working-memory volume mounted at
|
||||||
`/home/worsch/the-custodian/memory/working`. Before trusting the daily 07:20
|
`/var/custodian/memory/working` (hostPath → `/home/tegwick/the-custodian/memory/working`).
|
||||||
|
Before trusting the daily 07:20
|
||||||
Europe/Berlin schedule, verify both runtime dependencies:
|
Europe/Berlin schedule, verify both runtime dependencies:
|
||||||
|
|
||||||
- `actcore-state-hub-bridge` can reach the State Hub API through the node-local
|
- `actcore-state-hub-bridge` can reach the State Hub API through the node-local
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ DEFAULT_PROGRESS_EVENT_TYPE = "daily_triage"
|
||||||
DEFAULT_TEMPORAL_HOST = "localhost:7233"
|
DEFAULT_TEMPORAL_HOST = "localhost:7233"
|
||||||
DEFAULT_TEMPORAL_NAMESPACE = "default"
|
DEFAULT_TEMPORAL_NAMESPACE = "default"
|
||||||
DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
||||||
DEFAULT_WORKING_MEMORY_DIR = "/home/worsch/the-custodian/memory/working"
|
from activity_core.runtime_paths import custodian_working_memory_dir
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
|
|
@ -50,10 +50,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
DEFAULT_STATE_HUB_URL,
|
DEFAULT_STATE_HUB_URL,
|
||||||
))
|
))
|
||||||
parser.add_argument("--progress-event-type", default=DEFAULT_PROGRESS_EVENT_TYPE)
|
parser.add_argument("--progress-event-type", default=DEFAULT_PROGRESS_EVENT_TYPE)
|
||||||
parser.add_argument("--working-memory-dir", default=os.environ.get(
|
parser.add_argument(
|
||||||
"DAILY_TRIAGE_WORKING_MEMORY_DIR",
|
"--working-memory-dir",
|
||||||
DEFAULT_WORKING_MEMORY_DIR,
|
default=os.environ.get(
|
||||||
))
|
"DAILY_TRIAGE_WORKING_MEMORY_DIR",
|
||||||
|
str(custodian_working_memory_dir()),
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--date",
|
"--date",
|
||||||
default=datetime.now(timezone.utc).date().isoformat(),
|
default=datetime.now(timezone.utc).date().isoformat(),
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from activity_core.sync_activity_definitions import ACTIVITY_DEFINITION_ID_NAMES
|
||||||
|
|
||||||
DEFAULT_TIMEZONE = "Europe/Berlin"
|
DEFAULT_TIMEZONE = "Europe/Berlin"
|
||||||
DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
||||||
DEFAULT_WORKING_MEMORY_DIR = "/home/worsch/the-custodian/memory/working"
|
from activity_core.runtime_paths import custodian_working_memory_dir
|
||||||
DEFAULT_TEMPORAL_NAMESPACE = "default"
|
DEFAULT_TEMPORAL_NAMESPACE = "default"
|
||||||
FAILURE_STATUSES = {"missed", "validation_failed", "sink_failed"}
|
FAILURE_STATUSES = {"missed", "validation_failed", "sink_failed"}
|
||||||
WEEKDAYS = {
|
WEEKDAYS = {
|
||||||
|
|
@ -57,7 +57,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser.add_argument("--activity-name", action="append", default=[])
|
parser.add_argument("--activity-name", action="append", default=[])
|
||||||
parser.add_argument("--db-url", default=os.environ.get("ACTCORE_DB_URL"))
|
parser.add_argument("--db-url", default=os.environ.get("ACTCORE_DB_URL"))
|
||||||
parser.add_argument("--state-hub-url", default=os.environ.get("STATE_HUB_URL", DEFAULT_STATE_HUB_URL))
|
parser.add_argument("--state-hub-url", default=os.environ.get("STATE_HUB_URL", DEFAULT_STATE_HUB_URL))
|
||||||
parser.add_argument("--working-memory-dir", default=os.environ.get("AUTOMATION_STATUS_WORKING_MEMORY_DIR", DEFAULT_WORKING_MEMORY_DIR))
|
parser.add_argument(
|
||||||
|
"--working-memory-dir",
|
||||||
|
default=os.environ.get(
|
||||||
|
"AUTOMATION_STATUS_WORKING_MEMORY_DIR",
|
||||||
|
str(custodian_working_memory_dir()),
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument("--temporal-host", default=os.environ.get("TEMPORAL_HOST"))
|
parser.add_argument("--temporal-host", default=os.environ.get("TEMPORAL_HOST"))
|
||||||
parser.add_argument("--temporal-namespace", default=os.environ.get("TEMPORAL_NAMESPACE", DEFAULT_TEMPORAL_NAMESPACE))
|
parser.add_argument("--temporal-namespace", default=os.environ.get("TEMPORAL_NAMESPACE", DEFAULT_TEMPORAL_NAMESPACE))
|
||||||
parser.add_argument("--timeout-seconds", type=float, default=float(os.environ.get("AUTOMATION_STATUS_TIMEOUT_SECONDS", "5")))
|
parser.add_argument("--timeout-seconds", type=float, default=float(os.environ.get("AUTOMATION_STATUS_TIMEOUT_SECONDS", "5")))
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ import httpx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, ContextResolver
|
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, ContextResolver
|
||||||
|
from activity_core.runtime_paths import default_ops_inventory_path
|
||||||
_DEFAULT_INVENTORY_PATH = "/home/worsch/the-custodian/ops/service-inventory.yml"
|
|
||||||
_DEFAULT_TIMEOUT_SECONDS = 10.0
|
_DEFAULT_TIMEOUT_SECONDS = 10.0
|
||||||
_SUPPORTED_ENDPOINT_TYPES = {"http", "https"}
|
_SUPPORTED_ENDPOINT_TYPES = {"http", "https"}
|
||||||
|
|
||||||
|
|
@ -39,12 +38,11 @@ CONTEXT_RESOLVER_REGISTRY["ops-inventory"] = OpsInventoryContextResolver
|
||||||
|
|
||||||
|
|
||||||
def _probe_services(params: dict[str, Any]) -> dict[str, Any]:
|
def _probe_services(params: dict[str, Any]) -> dict[str, Any]:
|
||||||
inventory_path = Path(
|
inventory_raw = params.get("inventory_path")
|
||||||
str(
|
inventory_path = (
|
||||||
params.get("inventory_path")
|
default_ops_inventory_path()
|
||||||
or os.environ.get("OPS_INVENTORY_PATH")
|
if not inventory_raw
|
||||||
or _DEFAULT_INVENTORY_PATH
|
else Path(str(inventory_raw)).expanduser()
|
||||||
)
|
|
||||||
)
|
)
|
||||||
timeout_seconds = float(params.get("timeout_seconds", _DEFAULT_TIMEOUT_SECONDS))
|
timeout_seconds = float(params.get("timeout_seconds", _DEFAULT_TIMEOUT_SECONDS))
|
||||||
allow_network = _bool_param(params.get("allow_network", True))
|
allow_network = _bool_param(params.get("allow_network", True))
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from activity_core.runtime_paths import custodian_repo_root, resolve_runtime_path
|
from activity_core.runtime_paths import (
|
||||||
|
custodian_repo_relative,
|
||||||
|
custodian_repo_root,
|
||||||
|
resolve_runtime_path,
|
||||||
|
)
|
||||||
from activity_core.state_hub_write import idempotency_headers
|
from activity_core.state_hub_write import idempotency_headers
|
||||||
|
|
||||||
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
||||||
|
|
@ -82,13 +86,14 @@ def _write_working_memory(
|
||||||
target = (directory / filename).resolve()
|
target = (directory / filename).resolve()
|
||||||
_assert_allowed_output_path(target)
|
_assert_allowed_output_path(target)
|
||||||
|
|
||||||
|
repo_relative = custodian_repo_relative(target)
|
||||||
if target.exists():
|
if target.exists():
|
||||||
text = target.read_text(encoding="utf-8")
|
text = target.read_text(encoding="utf-8")
|
||||||
if f"activity_core_run_id: {run_id}" in text:
|
if f"activity_core_run_id: {run_id}" in text:
|
||||||
return {
|
return {
|
||||||
"type": "working-memory",
|
"type": "working-memory",
|
||||||
"status": "exists",
|
"status": "exists",
|
||||||
"path": str(target),
|
"path": repo_relative,
|
||||||
}
|
}
|
||||||
raise FileExistsError(f"refusing to overwrite existing report note: {target}")
|
raise FileExistsError(f"refusing to overwrite existing report note: {target}")
|
||||||
|
|
||||||
|
|
@ -97,7 +102,7 @@ def _write_working_memory(
|
||||||
return {
|
return {
|
||||||
"type": "working-memory",
|
"type": "working-memory",
|
||||||
"status": "written",
|
"status": "written",
|
||||||
"path": str(target),
|
"path": repo_relative,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,23 @@ def resolve_runtime_path(raw_path: str) -> Path:
|
||||||
return (activity_core_root() / value.removeprefix(_ACTIVITY_CORE_SCHEME)).resolve()
|
return (activity_core_root() / value.removeprefix(_ACTIVITY_CORE_SCHEME)).resolve()
|
||||||
|
|
||||||
return Path(value).expanduser()
|
return Path(value).expanduser()
|
||||||
|
|
||||||
|
|
||||||
|
def custodian_working_memory_dir() -> Path:
|
||||||
|
return resolve_runtime_path("custodian://memory/working")
|
||||||
|
|
||||||
|
|
||||||
|
def custodian_repo_relative(path: Path) -> str:
|
||||||
|
"""Return a repo-relative POSIX path when under CUSTODIAN_REPO_ROOT."""
|
||||||
|
root = custodian_repo_root().resolve()
|
||||||
|
try:
|
||||||
|
return path.resolve().relative_to(root).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return path.as_posix()
|
||||||
|
|
||||||
|
|
||||||
|
def default_ops_inventory_path() -> Path:
|
||||||
|
raw = os.environ.get("OPS_INVENTORY_PATH", "").strip()
|
||||||
|
if raw:
|
||||||
|
return resolve_runtime_path(raw)
|
||||||
|
return resolve_runtime_path("custodian://ops/service-inventory.yml")
|
||||||
|
|
@ -152,12 +152,11 @@ def test_worker_mounts_daily_triage_schema_and_working_memory() -> None:
|
||||||
assert mounts["report-schemas"]["mountPath"] == "/etc/activity-core/schemas"
|
assert mounts["report-schemas"]["mountPath"] == "/etc/activity-core/schemas"
|
||||||
assert mounts["report-schemas"]["readOnly"] is True
|
assert mounts["report-schemas"]["readOnly"] is True
|
||||||
assert volumes["report-schemas"]["configMap"]["name"] == "actcore-report-schemas"
|
assert volumes["report-schemas"]["configMap"]["name"] == "actcore-report-schemas"
|
||||||
assert mounts["working-memory"]["mountPath"] == (
|
assert mounts["working-memory"]["mountPath"] == "/var/custodian/memory/working"
|
||||||
"/home/worsch/the-custodian/memory/working"
|
assert volumes["working-memory"]["hostPath"]["path"] == (
|
||||||
)
|
"/home/tegwick/the-custodian/memory/working"
|
||||||
assert volumes["working-memory"]["persistentVolumeClaim"]["claimName"] == (
|
|
||||||
"actcore-working-memory"
|
|
||||||
)
|
)
|
||||||
|
assert pod_spec["securityContext"]["runAsUser"] == 1000
|
||||||
|
|
||||||
|
|
||||||
def test_ops_hub_key_is_secret_only_placeholder() -> None:
|
def test_ops_hub_key_is_secret_only_placeholder() -> None:
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ def test_working_memory_sink_writes_idempotently(tmp_path) -> None:
|
||||||
assert first[0]["status"] == "written"
|
assert first[0]["status"] == "written"
|
||||||
assert second[0]["status"] == "exists"
|
assert second[0]["status"] == "exists"
|
||||||
note = tmp_path / "daily-triage-2026-05-19-12345678.md"
|
note = tmp_path / "daily-triage-2026-05-19-12345678.md"
|
||||||
|
assert first[0]["path"].endswith("daily-triage-2026-05-19-12345678.md")
|
||||||
text = note.read_text(encoding="utf-8")
|
text = note.read_text(encoding="utf-8")
|
||||||
assert "activity_core_run_id: 12345678-aaaa-bbbb-cccc-123456789abc" in text
|
assert "activity_core_run_id: 12345678-aaaa-bbbb-cccc-123456789abc" in text
|
||||||
assert "output_validated: true" in text
|
assert "output_validated: true" in text
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,11 @@ def test_resolve_activity_core_uri(monkeypatch) -> None:
|
||||||
assert resolve_runtime_path("activity-core://schemas/daily-triage-report.json") == (
|
assert resolve_runtime_path("activity-core://schemas/daily-triage-report.json") == (
|
||||||
__import__("pathlib").Path("/etc/activity-core/schemas/daily-triage-report.json")
|
__import__("pathlib").Path("/etc/activity-core/schemas/daily-triage-report.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_custodian_repo_relative(monkeypatch) -> None:
|
||||||
|
from activity_core.runtime_paths import custodian_repo_relative
|
||||||
|
|
||||||
|
monkeypatch.setenv("CUSTODIAN_REPO_ROOT", "/var/custodian")
|
||||||
|
path = __import__("pathlib").Path("/var/custodian/memory/working/note.md")
|
||||||
|
assert custodian_repo_relative(path) == "memory/working/note.md"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue