Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ddb10c0
fleet: route the protocol-family probe through the catalog-or-harvest…
Knucklessg1 Oct 10, 2026
8373649
Split GRAPHOS-HOST-R005.2.1 and .2.3 into per-operation children
Knucklessg1 Oct 10, 2026
3dc5a36
GRAPHOS-OPS-R032.2.1: identity composition-root constructor returning…
Knucklessg1 Oct 10, 2026
d45ad77
GRAPHOS-IDENTITY-R008.2: API-key scope intersection and revocation
Knucklessg1 Oct 10, 2026
eed4ec1
Merge PR #210 into bulk land
Knucklessg1 Oct 10, 2026
7f72286
Merge PR #211 into bulk land
Knucklessg1 Oct 10, 2026
68a60f1
Merge PR #212 into bulk land
Knucklessg1 Oct 10, 2026
7b36d6d
Merge PR #213 into bulk land
Knucklessg1 Oct 10, 2026
3dcb050
chore(specs): regenerate status for the third 2026-10-10 landing train
Knucklessg1 Oct 10, 2026
316ed1e
Split GRAPHOS-OPS-R032.2.3 into per-module children
Knucklessg1 Oct 10, 2026
567a2b4
Map legacy sync action to ingest.sources.sync
Knucklessg1 Oct 10, 2026
41451e8
GRAPHOS-MCP-RESOURCES-R003: split into guard (.1) and publish-step wi…
Knucklessg1 Oct 10, 2026
6376214
GRAPHOS-A2A-002-R005: bind no-advisory/cached/partial admission test
Knucklessg1 Oct 10, 2026
16c9b2b
GRAPHOS-OPS-R020.3.1.1: add ingest.packs.list op, split R020.3.1
Knucklessg1 Oct 10, 2026
47f0cf0
Merge PR #215 into bulk land
Knucklessg1 Oct 10, 2026
8b94b2e
Merge PR #216 into bulk land
Knucklessg1 Oct 10, 2026
d0a88f3
Merge PR #217 into bulk land
Knucklessg1 Oct 10, 2026
f626c1f
Merge PR #218 into bulk land
Knucklessg1 Oct 10, 2026
4bc51b8
Merge PR #219 into bulk land
Knucklessg1 Oct 10, 2026
bdf40f7
chore(specs): regenerate status for the fourth 2026-10-10 landing train
Knucklessg1 Oct 10, 2026
634a344
GRAPHOS-OPS-R020.3.1.2: add ingest.jobs.status read op
Knucklessg1 Oct 10, 2026
965d343
GRAPHOS-MCP-RESOURCES-R003.1: add require_reconciled_for_swap guard
Knucklessg1 Oct 10, 2026
2d90ac5
chore(clones): re-review the ops/ingest operations() register entry
Knucklessg1 Oct 10, 2026
aa856c2
Merge PR #221 into bulk land
Knucklessg1 Oct 10, 2026
65cfc64
Merge PR #222 into bulk land
Knucklessg1 Oct 10, 2026
1f5c37d
chore(specs): regenerate status after adding #221 and #222
Knucklessg1 Oct 10, 2026
364ef8b
GRAPHOS-OPS-R022.2.1: split evolution slice, deliver evolution.loops.…
Knucklessg1 Oct 10, 2026
1c50860
specs(hosted-api-operations): R022.2.1 is blocked on the engine regis…
Knucklessg1 Oct 10, 2026
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
4 changes: 2 additions & 2 deletions .config/dupehound-distinct.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ left_name = "operations"
left_digest = "sha256:1dd92a3cf607864448698507dd859bedcd0b2837cddb97a0aeef56d1adc0a706"
right_file = "graph_os/api/ops/ingest.py"
right_name = "operations"
right_digest = "sha256:ee097ee89d61f7ddcec595ea9f1a06767ad045baf374b12e808b69399a111d71"
right_digest = "sha256:b34d30a8a6914622efd841d8584e31cac462f51acf90ab8f9a1e51992dbe2193"
reason = "Both declare an unrelated domain's own OpSpec registry (operational-admin vs ingestion) using the one mandated declarative registration shape every ops module shares by design; the registry factory, not this duplication, is the single point of consolidation."
reviewed_on = "2026-10-09"
reviewed_on = "2026-10-10"

[[pair]]
left_file = "graph_os/api/ops/memory.py"
Expand Down
42 changes: 42 additions & 0 deletions graph_os/api/ops/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
Verb,
)
from graph_os.ingest.service import (
get_job_status,
get_source_status,
index_repository,
list_packs,
list_sources,
sync_source,
)
Expand All @@ -38,8 +40,10 @@
#: (``graph_os.ingest.service``).
__all__ = [
"operations",
"get_job_status",
"get_source_status",
"index_repository",
"list_packs",
"list_sources",
"sync_source",
]
Expand Down Expand Up @@ -82,6 +86,22 @@ class IngestSourceStatusResult(_Params):
value: dict[str, str]


class IngestPacksListParams(_Params):
pass


class IngestPackListResult(_Params):
value: dict[str, object]


class IngestJobStatusParams(_Params):
job_id: str = Field(min_length=1, max_length=256)


class IngestJobStatusResult(_Params):
value: dict[str, str]


def operations() -> tuple[OpSpec, ...]:
return (
OpSpec(
Expand Down Expand Up @@ -132,4 +152,26 @@ def operations() -> tuple[OpSpec, ...]:
scopes=frozenset({"ingest:read"}),
effect=Effect.READ,
),
OpSpec(
id="ingest.packs.list",
verb=Verb.FIND,
summary="List this tenant's ingestion packs",
examples=("list my ingestion packs",),
params=IngestPacksListParams,
result=IngestPackListResult,
binding=Composite(handler="graph_os.api.ops.ingest.list_packs"),
scopes=frozenset({"ingest:read"}),
effect=Effect.READ,
),
OpSpec(
id="ingest.jobs.status",
verb=Verb.ASK,
summary="Show one durable ingestion job's current status",
examples=("show the status of this ingestion job",),
params=IngestJobStatusParams,
result=IngestJobStatusResult,
binding=Composite(handler="graph_os.api.ops.ingest.get_job_status"),
scopes=frozenset({"ingest:read"}),
effect=Effect.READ,
),
)
39 changes: 39 additions & 0 deletions graph_os/fleet/mcp_resource_reconciliation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,42 @@ def gate_mcp_resource_publication(
return _unreconciled("stale", receipt)

return _reconciled(receipt)


class PublicationRefusedError(Exception):
"""Raised when a candidate generation may not be swapped in as published.

Carries the typed ``PublicationGateResult`` so the caller can surface the
stable ``reason`` instead of swallowing the refusal.
"""

def __init__(self, result: PublicationGateResult) -> None:
self.result = result
super().__init__(f"{result.code}: publication refused (reason={result.reason})")


def require_reconciled_for_swap(
receipt: ReconciliationReceipt | None,
*,
tenant_id: str,
expected_generation: int,
expected_digest: str,
now_ms: int,
max_age_ms: int,
) -> ReconciliationReceipt:
"""Return the matching receipt, or raise ``PublicationRefusedError``.

Wraps ``gate_mcp_resource_publication`` so the publish step cannot bypass
the gate by ignoring a returned status.
"""
result = gate_mcp_resource_publication(
receipt,
tenant_id=tenant_id,
expected_generation=expected_generation,
expected_digest=expected_digest,
now_ms=now_ms,
max_age_ms=max_age_ms,
)
if result.status != "reconciled" or result.receipt is None:
raise PublicationRefusedError(result)
return result.receipt
4 changes: 2 additions & 2 deletions graph_os/fleet/multiplexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4917,15 +4917,15 @@ async def _probe_protocol_families(
errors["prompts"] = type(exc).__name__
native_prompts = []

await self._harvest_resource_bodies(
await self._resolve_via_local_catalog_or_harvest(
server_name,
session,
skills,
_SKILL_HARVEST_SPEC,
self._read_skill_body,
probe_deadline=probe_deadline,
)
await self._harvest_resource_bodies(
await self._resolve_via_local_catalog_or_harvest(
server_name,
session,
prompt_resources,
Expand Down
21 changes: 21 additions & 0 deletions graph_os/identity/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

from __future__ import annotations

from collections.abc import Collection
from dataclasses import dataclass
from datetime import datetime

from .engine import IdentityUnavailable

Expand All @@ -20,6 +22,7 @@ class ApiKeyGrant:
key_id: str
owner_principal_id: str
scopes: frozenset[str]
expires_at: datetime | None = None

def __post_init__(self) -> None:
if not isinstance(self.key_id, str) or not self.key_id:
Expand All @@ -31,3 +34,21 @@ def __post_init__(self) -> None:
raise IdentityUnavailable(
f"API keys may not carry approver-class scope(s): {sorted(rejected)}"
)


def effective_scopes(
grant: ApiKeyGrant,
owner_scopes: frozenset[str],
*,
now: datetime,
revoked_key_ids: Collection[str] = (),
) -> frozenset[str]:
"""Scopes usable right now: key scopes intersected with the owner's current scopes.

Fails closed: a revoked or expired key is refused immediately.
"""
if grant.key_id in revoked_key_ids:
raise IdentityUnavailable("API key has been revoked")
if grant.expires_at is not None and now >= grant.expires_at:
raise IdentityUnavailable("API key has expired")
return (grant.scopes & owner_scopes) - _APPROVER_SCOPES
58 changes: 58 additions & 0 deletions graph_os/identity/composition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Identity composition root: build the concrete runtime once, expose only ports.

Dependents receive ``CredentialAuthority`` and ``SessionAuthority`` objects and
never import a concrete implementation. Absent authoritative facts or a
malformed authority answer fail closed with ``IdentityUnavailable``.
"""

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from .engine import IdentityUnavailable
from .ports import (
CredentialAuthority,
CredentialState,
SessionAuthority,
SessionState,
)


@dataclass(frozen=True, slots=True)
class IdentityPorts:
"""The only identity surface handed to dependents."""

credentials: CredentialAuthority
sessions: SessionAuthority


class _CheckedRuntime:
"""Port-typed view over the single concrete runtime; validates answers."""

def __init__(self, runtime: Any) -> None:
self._runtime = runtime

async def resolve_credential(self, credential: str) -> CredentialState:
state = await self._runtime.resolve_credential(credential)
if not isinstance(state, CredentialState):
raise IdentityUnavailable("credential authority returned no state")
return state

async def resolve_session(self, credential: str) -> SessionState:
state = await self._runtime.resolve_session(credential)
if not isinstance(state, SessionState):
raise IdentityUnavailable("session authority returned no state")
return state


def build_identity_ports(
credentials: Any,
sessions: Any,
*,
runtime_factory: Callable[[Any, Any], Any],
) -> IdentityPorts:
"""Construct the concrete runtime exactly once and return port-typed views."""
if credentials is None or sessions is None:
raise IdentityUnavailable("authoritative identity sources required")
runtime = _CheckedRuntime(runtime_factory(credentials, sessions))
return IdentityPorts(credentials=runtime, sessions=runtime)
48 changes: 48 additions & 0 deletions graph_os/ingest/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ class IngestSourceInventory(BaseModel):
sources: tuple[IngestSourceRecord, ...]


class IngestPackRecord(BaseModel):
"""One ingestion pack's identity and current state."""

model_config = ConfigDict(frozen=True, extra="forbid")

pack_id: str
tenant: str
state: IngestSourceState


class IngestPackInventory(BaseModel):
"""A tenant's full ingestion-pack inventory."""

model_config = ConfigDict(frozen=True, extra="forbid")

packs: tuple[IngestPackRecord, ...]


class IngestJobRecord(BaseModel):
"""One durable ingestion job's identity and current state."""

model_config = ConfigDict(frozen=True, extra="forbid")

job_id: str
tenant: str
status: IngestJobStatus


@runtime_checkable
class IngestRunner(Protocol):
"""The one typed port GraphOS calls the ingestion SDK through.
Expand All @@ -90,6 +118,10 @@ async def get_source_status(
self, *, tenant: str, source_id: str
) -> IngestSourceRecord: ...

async def list_packs(self, *, tenant: str) -> IngestPackInventory: ...

async def get_job_status(self, *, tenant: str, job_id: str) -> IngestJobRecord: ...


def _bound_runner(context: Any) -> IngestRunner:
runner = context.services.get("ingest_runner")
Expand Down Expand Up @@ -142,3 +174,19 @@ async def get_source_status(context: Any, params: Mapping[str, Any], op: Any) ->
tenant=context.caller.tenant, source_id=params["source_id"]
)
return {"value": record.model_dump(mode="json")}


async def list_packs(context: Any, params: Mapping[str, Any], op: Any) -> Any:
"""List this tenant's ingestion packs through the composed runner."""
runner = _bound_runner(context)
inventory = await runner.list_packs(tenant=context.caller.tenant)
return {"value": inventory.model_dump(mode="json")}


async def get_job_status(context: Any, params: Mapping[str, Any], op: Any) -> Any:
"""Report one durable ingestion job's state through the composed runner."""
runner = _bound_runner(context)
record = await runner.get_job_status(
tenant=context.caller.tenant, job_id=params["job_id"]
)
return {"value": record.model_dump(mode="json")}
1 change: 1 addition & 0 deletions graph_os/mcp_server/legacy_action_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ def validate_served_actions(
ActionOperationMapping("vault_sync", "secret_vault.vault_sync"),
ActionOperationMapping("install_hooks", "graph_loops.install_hooks"),
ActionOperationMapping("uninstall_hooks", "graph_loops.uninstall_hooks"),
ActionOperationMapping("sync", "ingest.sources.sync"),
)
)
15 changes: 10 additions & 5 deletions specs/fleet-catalog-and-tools/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,8 @@
"a94c87aa090e"
],
"verified_by": [
"tests/fleet/test_harvest_inventory.py:106",
"tests/fleet/test_harvest_inventory.py:117"
"tests/fleet/test_harvest_inventory.py:105",
"tests/fleet/test_harvest_inventory.py:116"
]
},
{
Expand Down Expand Up @@ -360,9 +360,14 @@
{
"id": "GRAPHOS-FLEET-R006.3.1",
"title": "`_probe_protocol_families` harvest calls routed through the cutover helper",
"delivery_state": "SPECIFIED",
"landed_in": [],
"verified_by": []
"delivery_state": "VERIFIED",
"landed_in": [
"ddb10c07995e"
],
"verified_by": [
"tests/fleet/test_harvest_inventory.py:128",
"tests/fleet/test_harvest_inventory.py:135"
]
},
{
"id": "GRAPHOS-FLEET-R006.3.2",
Expand Down
10 changes: 7 additions & 3 deletions specs/graphos-a2a-002/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@
{
"id": "GRAPHOS-A2A-002-R005",
"title": "No advisory, cached, or partial substitute",
"delivery_state": "SPECIFIED",
"landed_in": [],
"verified_by": []
"delivery_state": "VERIFIED",
"landed_in": [
"63762147f0bc"
],
"verified_by": [
"tests/a2a/test_admission.py:52"
]
},
{
"id": "GRAPHOS-A2A-002-R006",
Expand Down
Loading
Loading