From c5bb0bdef2247fefab4409398f8f18a69566ce2d Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Tue, 6 Oct 2026 09:50:57 -0500 Subject: [PATCH 1/3] feat(deploy): provision-semantic-content production operation Add `graph-os-production-ops provision-semantic-content`. Under GraphOS's own verified process session and the served engine bootstrap it idempotently ensures the tenant graph and the served `__commons__` registrations for each semantic content provider, obtains EG's catalog binding through ConnectorPack.attest_self_served_catalog (pinned to the exact server entry of the pack it imports), imports through the SDK sink under AU's policy-gated pack_import_authority with the EG owner principal, reprojects and attaches each pack to the session graph, and exits non-zero unless verify_semantic_content passes. Co-Authored-By: Claude Opus 5.5 (1M context) --- graph_os/deployment/production_ops.py | 50 ++- graph_os/deployment/semantic_provisioning.py | 354 ++++++++++++++++++ .../test_provision_semantic_content.py | 282 ++++++++++++++ 3 files changed, 680 insertions(+), 6 deletions(-) create mode 100644 graph_os/deployment/semantic_provisioning.py create mode 100644 tests/deployment/test_provision_semantic_content.py diff --git a/graph_os/deployment/production_ops.py b/graph_os/deployment/production_ops.py index 009a354..8b9cf4b 100755 --- a/graph_os/deployment/production_ops.py +++ b/graph_os/deployment/production_ops.py @@ -380,6 +380,31 @@ async def _restore_validate(archive_root: Path, scratch_root: Path) -> dict[str, shutil.rmtree(destination, ignore_errors=True) +async def _provision_semantic_content(served_url: str) -> dict[str, Any]: + """Provision GraphOS's semantic packs under its own verified process session. + + The session and engine come from the same bootstrap the served process + uses, so every EG call carries exactly the authority GraphOS serves with. + """ + + from agent_utilities.api.session import use_session + from agent_utilities.security.brain_context import use_actor + + from graph_os.deployment.semantic_provisioning import provision_semantic_content + + # ``runtime`` binds bootstrap's host slots on import; import it first. + from graph_os.mcp_server import bootstrap, runtime + + session = runtime._mint_process_session("http") + with use_actor(session.actor), use_session(session): + engine = runtime._get_engine() + bootstrap._wait_for_engine_materialization(engine) + report = await provision_semantic_content( + engine=engine, session=session, served_url=served_url + ) + return {"operation": "provision-semantic-content", "ok": True, **report} + + def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="graph-os-production-ops") subparsers = parser.add_subparsers(dest="operation", required=True) @@ -388,18 +413,31 @@ def _parser() -> argparse.ArgumentParser: restore = subparsers.add_parser("restore-validate") restore.add_argument("--archive-root", type=Path, required=True) restore.add_argument("--scratch-root", type=Path, required=True) + provision = subparsers.add_parser("provision-semantic-content") + provision.add_argument( + "--served-url", + default=os.environ.get("GRAPH_OS_SERVED_MCP_URL", ""), + help="MCP URL GraphOS serves its content at (registered when absent).", + ) return parser +def _run(args: argparse.Namespace) -> dict[str, Any]: + if args.operation == "backup": + return asyncio.run(_backup(args.archive_root)) + if args.operation == "restore-validate": + return asyncio.run(_restore_validate(args.archive_root, args.scratch_root)) + if not args.served_url: + raise ProductionOperationError( + "--served-url (or GRAPH_OS_SERVED_MCP_URL) is required" + ) + return asyncio.run(_provision_semantic_content(args.served_url)) + + def main(argv: list[str] | None = None) -> int: args = _parser().parse_args(argv) try: - if args.operation == "backup": - report = asyncio.run(_backup(args.archive_root)) - else: - report = asyncio.run( - _restore_validate(args.archive_root, args.scratch_root) - ) + report = _run(args) except Exception as exc: # noqa: BLE001 - CLI returns one privacy-safe failure report = { "operation": args.operation, diff --git a/graph_os/deployment/semantic_provisioning.py b/graph_os/deployment/semantic_provisioning.py new file mode 100644 index 0000000..b871395 --- /dev/null +++ b/graph_os/deployment/semantic_provisioning.py @@ -0,0 +1,354 @@ +"""Deploy-time provisioning of GraphOS's own semantic content packs. + +GraphOS serves the ``graph-os`` and ``agent-utilities`` ConnectorContent packs +in process, so it is a *self-served* catalog producer: epistemic-graph issues +the catalog binding for each pack through ``attest_self_served_catalog``, +pinned to the exact server entry the import then carries. Every step runs under +GraphOS's own verified process session and is idempotent, so this command is +safe to re-run on every deploy: + +1. ensure the session's tenant graph exists; +2. ensure each connector's served registration exists in ``__commons__``; +3. per provider: build the pack, obtain EG's binding, import it through the + SDK sink under AU's policy-gated ``pack_import_authority``; +4. reproject the pack head and attach its schema to the tenant graph; +5. prove the result with :func:`graph_os.semantic_content.verify_semantic_content`. +""" + +from __future__ import annotations + +import hashlib +import json +import secrets +import time +from collections.abc import Callable, Sequence +from contextlib import AbstractContextManager +from typing import Any + +from graph_os.semantic_content import ( + ContentProvider, + default_content_providers, + verify_semantic_content, +) + +COMMONS_GRAPH = "__commons__" +TENANT_GRAPH_TYPE = "Team" +#: The longest lease ``RegisterServer`` accepts; provisioning only needs the +#: registration live while it attests, and the serving process heartbeats it. +REGISTRATION_TTL_SECS = 86_400 +_PLACEHOLDER_DIGEST = "sha256:" + "0" * 64 + +GraphBinder = Callable[[str], AbstractContextManager[object]] + + +class SemanticProvisioningError(RuntimeError): + """A provisioning step was refused or answered inconsistently.""" + + +def _bind_session_graph(graph: str) -> AbstractContextManager[object]: + """Run one engine call under the verified session narrowed to ``graph``.""" + + from agent_utilities.api.session import resolve_session, use_session + + return use_session(resolve_session().with_graph(graph)) + + +def _sha256_json(value: Any) -> str: + encoded = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def registration_config_digest(url: str, resources: Any) -> str: + """EG's identity of a registration's served configuration (URL + resources).""" + + return _sha256_json({"resources": resources, "url": url}) + + +def catalog_content_digest(pack: Any) -> str: + """Digest of every served entry (tools, prompts, resources, templates).""" + + entries = sorted( + (entry.model_dump(mode="json") for entry in pack.archive.entries), + key=lambda entry: entry["uri"], + ) + return _sha256_json(entries) + + +def _shell_context(tenant_id: str, key: str) -> Any: + """A request-body context shell; EG re-binds every field from the session.""" + + from epistemic_graph.generated.connector_pack import AgentLibraryMutationContext + + return AgentLibraryMutationContext( + request_id=secrets.randbits(63), + principal="bound-by-engine", + caller_principal="bound-by-engine", + attempt_nonce=secrets.token_hex(32), + tenant_id=tenant_id, + actor_scope="bound-by-engine", + purpose_id="mcp-catalog:reconcile", + policy_revision="bound-by-engine", + policy_digest=_PLACEHOLDER_DIGEST, + policy_decision_id="bound-by-engine", + idempotency_key=key, + expected_revision=None, + trace_id=None, + created_at_ms=int(time.time() * 1000), + ) + + +async def ensure_tenant_graph(client: Any, graph: str) -> bool: + """Create the tenant graph when absent; return whether it was created.""" + + from epistemic_graph.generated.cluster import ( + decode_list_graphs, + send_create_graph, + send_list_graphs, + ) + + listed = decode_list_graphs(await send_list_graphs(client, {}, graph)) + if any(listing.name == graph for listing in listed): + return False + await send_create_graph( + client, + {"graph_name": graph, "graph_type": TENANT_GRAPH_TYPE}, + graph, + idempotency_key=f"graph-os:create-graph:{graph}", + ) + return True + + +async def _registry_page(commons: Any) -> Any: + from epistemic_graph.generated.cluster import send_list_registered_servers + + return await send_list_registered_servers(commons, {"request": {}}, COMMONS_GRAPH) + + +async def ensure_registrations( + commons: Any, connectors: Sequence[str], served_url: str +) -> tuple[str, ...]: + """Register every connector GraphOS serves that has no live registration. + + A live registration is left untouched: its URL and desired state belong to + the operator, and re-registering would move the registry for no reason. + """ + + from epistemic_graph.generated.cluster import send_register_server + + live = {entry.name for entry in (await _registry_page(commons)).entries} + registered: list[str] = [] + for connector in connectors: + if connector in live: + continue + await send_register_server( + commons, + { + "name": connector, + "url": served_url, + "resources_json": "{}", + "ttl_secs": REGISTRATION_TTL_SECS, + "transport": "streamable_http", + "desired": "enabled", + }, + COMMONS_GRAPH, + idempotency_key=f"graph-os:register-server:{connector}:{served_url}", + ) + registered.append(connector) + return tuple(registered) + + +async def attest_self_served_catalog( + client: Any, + commons: Any, + *, + tenant_id: str, + graph: str, + pack: Any, + bind_graph: GraphBinder, +) -> Any: + """Ask EG for the binding of ``pack``'s catalog, fenced by the last generation.""" + + from epistemic_graph.generated.connector_pack import ( + McpCatalogAuthorityStatusRequest, + McpSelfServedCatalogAttestRequest, + ) + from epistemic_graph.generated.storage import ( + send_connector_pack_attest_self_served_catalog, + send_connector_pack_catalog_authority_status, + ) + + connector = pack.connector + with bind_graph(COMMONS_GRAPH): + page = await _registry_page(commons) + view = next((entry for entry in page.entries if entry.name == connector), None) + if view is None: + raise SemanticProvisioningError( + f"served registration for {connector!r} is not live" + ) + with bind_graph(graph): + current = await send_connector_pack_catalog_authority_status( + client, + McpCatalogAuthorityStatusRequest( + tenant_id=tenant_id, server_name=connector + ), + graph, + ) + observed = { + "connector": connector, + "server_name": connector, + "server_entry_digest": pack.archive.server.body.sha256, + "registry_revision": page.registry_revision, + "registry_digest": page.registry_digest, + "registration_config_digest": registration_config_digest( + view.url, view.resources + ), + "four_family_digest": catalog_content_digest(pack), + "expected_catalog_generation": ( + None if current is None else current.catalog_generation + ), + } + key = f"mcp-catalog:self-served:{connector}:{_sha256_json(observed)[:32]}" + request = McpSelfServedCatalogAttestRequest( + context=_shell_context(tenant_id, key), **observed + ) + with bind_graph(graph): + return await send_connector_pack_attest_self_served_catalog( + client, request, graph, idempotency_key=key + ) + + +async def _owner_principal( + client: Any, *, tenant_id: str, graph: str, connector: str +) -> str: + from epistemic_graph.generated.connector_pack import ( + ConnectorPackOpCatalogRequestOwnerPrincipal, + McpCatalogAuthorityStatusRequest, + ) + + op = ConnectorPackOpCatalogRequestOwnerPrincipal( + op="catalog_request_owner_principal", + request=McpCatalogAuthorityStatusRequest( + tenant_id=tenant_id, server_name=connector + ), + ) + payload = await client._send( + "ConnectorPack", {"op": op.model_dump(mode="json")}, graph + ) + if not isinstance(payload, str) or not payload.strip(): + raise SemanticProvisioningError("EG owner principal is unavailable") + return payload + + +async def _reproject_and_attach( + client: Any, *, tenant_id: str, graph: str, connector: str +) -> tuple[Any, Any]: + from epistemic_graph.generated.reasoning import send_graph_schema + + context = _shell_context(tenant_id, f"connector-pack:{connector}:reproject") + reprojected = await client._send( + "ConnectorPack", + { + "op": { + "op": "reproject", + "request": { + "context": context.model_dump(mode="json", exclude_none=True), + "connector": connector, + }, + } + }, + graph, + idempotency_key=f"graph-os:reproject:{connector}:{secrets.token_hex(8)}", + ) + attached = await send_graph_schema( + client, + {"op": {"op": "attach_pack", "connector": connector}}, + graph, + idempotency_key=f"graph-os:attach-pack:{graph}:{connector}:{secrets.token_hex(8)}", + ) + return reprojected, attached + + +async def provision_semantic_content( + *, + engine: Any, + session: Any, + served_url: str, + providers: Sequence[ContentProvider] | None = None, + bind_graph: GraphBinder = _bind_session_graph, +) -> dict[str, Any]: + """Run every provisioning step; raise unless the semantic content verifies.""" + + from agent_connector_sdk.artifacts.pack import build_connector_content_pack + from agent_connector_sdk.sinks.epistemic_graph import EpistemicGraphSink + from agent_utilities.api import pack_import_authority + + tenant_id = str(session.engine_verified_context()["tenant"]) + graph = str(session.graph) + compute = engine.graph_compute + client = compute.for_graph(graph).async_client + commons = compute.for_graph(COMMONS_GRAPH).async_client + contents = tuple( + provider() for provider in (providers or default_content_providers()) + ) + connectors = tuple(content.connector for content in contents) + with bind_graph(graph): + created = await ensure_tenant_graph(client, graph) + with bind_graph(COMMONS_GRAPH): + registered = await ensure_registrations(commons, connectors, served_url) + imports: dict[str, str] = {} + for content in contents: + pack = await build_connector_content_pack(content) + binding = await attest_self_served_catalog( + client, + commons, + tenant_id=tenant_id, + graph=graph, + pack=pack, + bind_graph=bind_graph, + ) + with bind_graph(graph): + owner = await _owner_principal( + client, tenant_id=tenant_id, graph=graph, connector=pack.connector + ) + sink = EpistemicGraphSink( + client, + pack_import_authority( + engine, + session.with_graph(graph), + catalog_binding=lambda binding=binding: binding, + serving_principal=lambda owner=owner: owner, + ), + ) + result = await sink.import_pack(pack) + if getattr(result, "result", None) == "rejected": + raise SemanticProvisioningError( + f"ConnectorPack import of {pack.connector!r} was rejected" + ) + await _reproject_and_attach( + client, tenant_id=tenant_id, graph=graph, connector=pack.connector + ) + imports[pack.connector] = str(getattr(result, "result", type(result).__name__)) + with bind_graph(graph): + await verify_semantic_content( + client=client, tenant_id=tenant_id, graph=graph, connectors=connectors + ) + return { + "graph": graph, + "graph_created": created, + "registered": list(registered), + "imports": imports, + "verified": True, + } + + +__all__ = [ + "SemanticProvisioningError", + "attest_self_served_catalog", + "catalog_content_digest", + "ensure_registrations", + "ensure_tenant_graph", + "provision_semantic_content", + "registration_config_digest", +] diff --git a/tests/deployment/test_provision_semantic_content.py b/tests/deployment/test_provision_semantic_content.py new file mode 100644 index 0000000..33b65bf --- /dev/null +++ b/tests/deployment/test_provision_semantic_content.py @@ -0,0 +1,282 @@ +"""``graph-os-production-ops provision-semantic-content`` through its CLI entrypoint. + +The EG transport is faked at ``client._send`` so every generated sender, model +and decode runs for real; the process session, AU's policy-gated import +authority and the pack builder are the real ones. +""" + +from __future__ import annotations + +import json +from typing import Any + +import pytest +from agent_utilities.knowledge_graph.core.session import GraphSession +from agent_utilities.orchestration.action_policy import ( + ActionDecision, + ActionRequest, + PolicyDisposition, + PolicyReceipt, +) +from agent_utilities.security.actor_identity import ActorType +from agent_utilities.security.brain_context import ActorContext + +from graph_os.deployment import production_ops, semantic_provisioning + +TENANT = "tenant-a" +GRAPH = "tenant__tenant-a__default" +URL = "https://graph-os.example/mcp" +OWNER = "principal:sha256:" + "e" * 64 + + +def _binding(generation: int) -> dict[str, Any]: + return { + "configuration_revision": 1, + "catalog_generation": generation, + "snapshot_digest": "5" * 64, + "child_connection_generation": 1, + "authorization_scope_digest": "4" * 64, + } + + +class _Engine: + """One fake EG transport shared by every graph view.""" + + def __init__(self, *, registered: set[str], graphs: set[str]) -> None: + self.registered = registered + self.graphs = graphs + self.calls: list[tuple[str, str, Any]] = [] + self.attests: list[dict[str, Any]] = [] + self.graph_compute = self + + def for_graph(self, _graph: str) -> Any: + return type("View", (), {"async_client": self})() + + async def _send( + self, method: str, params: Any, graph: Any, *, idempotency_key: Any = None + ) -> Any: + op = (params or {}).get("op") if method == "ConnectorPack" else None + name = op["op"] if isinstance(op, dict) else method + self.calls.append((name, graph, params)) + handler = getattr(self, f"_on_{name}".replace("-", "_"), None) + if handler is None: + raise AssertionError(f"unexpected EG call {name}") + return handler(params, idempotency_key) + + def _on_ListGraphs(self, _params: Any, _key: Any) -> Any: + return [ + {"name": name, "type": "Team", "valid": True, "index_manifests": []} + for name in sorted(self.graphs) + ] + + def _on_CreateGraph(self, params: Any, _key: Any) -> Any: + self.graphs.add(params["graph_name"]) + return {"created": params["graph_name"]} + + def _on_ListRegisteredServers(self, _params: Any, _key: Any) -> Any: + entries = [ + { + "name": name, + "url": URL, + "transport": "streamable_http", + "desired": "enabled", + "resources": {}, + "ttl_secs": 86_400, + "registered_at_ms": 1, + "last_heartbeat_ms": 1, + "lease_expires_at_ms": 2**40, + } + for name in sorted(self.registered) + ] + return { + "schema_version": 1, + "entries": entries, + "next_cursor": None, + "observed_at_ms": 1, + "total_live": len(entries), + "registry_revision": 7, + "registry_digest": "7" * 64, + } + + def _on_RegisterServer(self, params: Any, _key: Any) -> Any: + assert params["url"] == URL + self.registered.add(params["name"]) + return "registered" + + def _on_catalog_authority_status(self, _params: Any, _key: Any) -> Any: + return None + + def _on_attest_self_served_catalog(self, params: Any, key: Any) -> Any: + request = params["op"]["request"] + assert key == request["context"]["idempotency_key"] + self.attests.append(request) + return _binding(1) + + def _on_catalog_request_owner_principal(self, _params: Any, _key: Any) -> Any: + return OWNER + + def _on_reproject(self, _params: Any, _key: Any) -> Any: + return {"reprojected": True} + + def _on_GraphSchema(self, params: Any, _key: Any) -> Any: + assert params["op"]["op"] == "attach_pack" + return { + "changed": True, + "composed_digest": "c" * 64, + "graph": GRAPH, + "graph_version": 1, + "schema_version": 1, + } + + +class _Sink: + """Stands in for the SDK sink; resolves the real AU authority per import.""" + + imported: list[tuple[str, Any, Any]] = [] + + def __init__(self, client: Any, pack_import_authority: Any) -> None: + self._resolve = pack_import_authority + + async def import_pack(self, pack: Any) -> Any: + binding, context = await self._resolve(pack.connector) + _Sink.imported.append((pack.connector, binding, context)) + return type("Unchanged", (), {"result": "unchanged"})() + + +class _AllowPolicy: + def decide(self, request: ActionRequest) -> ActionDecision: + return ActionDecision( + decision="allow", + tier="auto_notify", + request=request, + receipt=PolicyReceipt( + receipt_id="action_decision:test", + request_digest=request.digest(), + disposition=PolicyDisposition.APPROVE, + policy_origin="test", + ), + ) + + +def _session() -> GraphSession: + return GraphSession( + actor=ActorContext( + actor_id="service:graph-os", + actor_type=ActorType.AUTOMATED_SERVICE, + tenant_id=TENANT, + authenticated=True, + ), + tenant=TENANT, + scopes=frozenset({"agent:pack-control", "connector:catalog-attest"}), + graph=GRAPH, + audience="graph-os", + policy_version="policy-a", + ) + + +@pytest.fixture +def wired(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + from graph_os.mcp_server import bootstrap, runtime + + state: dict[str, Any] = {"verified": []} + engine = _Engine(registered=set(), graphs=set()) + session = _session() + monkeypatch.setattr(runtime, "_mint_process_session", lambda transport: session) + monkeypatch.setattr(runtime, "_get_engine", lambda: engine) + monkeypatch.setattr(bootstrap, "_wait_for_engine_materialization", lambda e: None) + monkeypatch.setattr( + "agent_utilities.api.provisioning.get_action_policy", + lambda _engine: _AllowPolicy(), + ) + monkeypatch.setattr( + "agent_connector_sdk.sinks.epistemic_graph.EpistemicGraphSink", _Sink + ) + + async def verify(**kwargs: Any) -> None: + state["verified"].append(kwargs) + + monkeypatch.setattr(semantic_provisioning, "verify_semantic_content", verify) + _Sink.imported = [] + state["engine"] = engine + return state + + +def test_cli_provisions_both_packs_under_eg_issued_bindings( + wired: dict[str, Any], capsys: pytest.CaptureFixture[str] +) -> None: + engine: _Engine = wired["engine"] + + code = production_ops.main(["provision-semantic-content", "--served-url", URL]) + + report = json.loads(capsys.readouterr().out) + assert code == 0, report + assert report["ok"] is True + assert report["graph"] == GRAPH and report["graph_created"] is True + assert sorted(report["registered"]) == ["agent-utilities", "graph-os"] + assert sorted(attest["connector"] for attest in engine.attests) == [ + "agent-utilities", + "graph-os", + ] + for attest in engine.attests: + # The first binding of a fresh store: no generation to fence on. + assert attest.get("expected_catalog_generation") is None + assert attest["server_name"] == attest["connector"] + for connector, binding, context in _Sink.imported: + assert binding.catalog_generation == 1 + assert context.principal == OWNER and context.tenant_id == TENANT + attaches = [call for call in engine.calls if call[0] == "GraphSchema"] + assert {call[1] for call in attaches} == {GRAPH} + assert len(wired["verified"]) == 1 + assert wired["verified"][0]["graph"] == GRAPH + + +def test_rerun_registers_and_creates_nothing( + wired: dict[str, Any], capsys: pytest.CaptureFixture[str] +) -> None: + engine: _Engine = wired["engine"] + engine.registered.update({"graph-os", "agent-utilities"}) + engine.graphs.add(GRAPH) + + assert production_ops.main(["provision-semantic-content", "--served-url", URL]) == 0 + + report = json.loads(capsys.readouterr().out) + assert report["graph_created"] is False and report["registered"] == [] + assert not [ + call for call in engine.calls if call[0] in {"CreateGraph", "RegisterServer"} + ] + + +def test_failed_verification_exits_non_zero( + wired: dict[str, Any], + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + async def refuse(**_kwargs: Any) -> None: + raise RuntimeError("semantic pack is not attached") + + monkeypatch.setattr(semantic_provisioning, "verify_semantic_content", refuse) + + assert production_ops.main(["provision-semantic-content", "--served-url", URL]) == 1 + assert json.loads(capsys.readouterr().out)["ok"] is False + + +def test_served_url_is_required( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.delenv("GRAPH_OS_SERVED_MCP_URL", raising=False) + + assert production_ops.main(["provision-semantic-content"]) == 1 + assert ( + json.loads(capsys.readouterr().out)["error_type"] == "ProductionOperationError" + ) + + +def test_registration_digest_matches_engine_canonical_json() -> None: + # EG digests compact, key-sorted JSON of {"resources", "url"}. + digest = semantic_provisioning.registration_config_digest(URL, {"b": 1, "a": [2]}) + import hashlib + + expected = hashlib.sha256( + b'{"resources":{"a":[2],"b":1},"url":"https://graph-os.example/mcp"}' + ).hexdigest() + assert digest == expected From 4d576d06e63537d325482335f34c1bee394604d8 Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Tue, 6 Oct 2026 13:12:50 -0500 Subject: [PATCH 2/3] ci: pin AU and EG sources that serve self-served catalog attestation Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b0ce87..75886b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: Knuckles-Team/agent-utilities - ref: fbc939fedeadeb20ddf4b468245165414a18c34e + ref: 2d40ed53f6440fdda0b5cfb7868d912fa8f162e1 path: .uv-workspace-siblings/agent-utilities persist-credentials: false @@ -115,7 +115,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: Knuckles-Team/epistemic-graph - ref: 49d63da5396fef7482fc3617df3f90a836661722 + ref: 70037e8a400c7b32b1222623bb05bbfaf55deab8 path: .uv-workspace-siblings/agent-utilities/.uv-workspace-siblings/epistemic-graph persist-credentials: false From d0fe7521886b3a0df0273eca2486e3274a145880 Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Tue, 6 Oct 2026 13:43:31 -0500 Subject: [PATCH 3/3] fix(deploy): read the served URL through declared configuration Co-Authored-By: Claude Opus 5.5 (1M context) --- graph_os/deployment/production_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph_os/deployment/production_ops.py b/graph_os/deployment/production_ops.py index 8b9cf4b..6b59f9e 100755 --- a/graph_os/deployment/production_ops.py +++ b/graph_os/deployment/production_ops.py @@ -416,7 +416,7 @@ def _parser() -> argparse.ArgumentParser: provision = subparsers.add_parser("provision-semantic-content") provision.add_argument( "--served-url", - default=os.environ.get("GRAPH_OS_SERVED_MCP_URL", ""), + default=str(setting("GRAPH_OS_SERVED_MCP_URL", "") or ""), help="MCP URL GraphOS serves its content at (registered when absent).", ) return parser