From fc093acbbecc917d8e408a1a881da953a6588820 Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Sun, 25 Oct 2026 16:59:04 -0500 Subject: [PATCH 1/3] feat(connector): declare GitLab read access contracts Add source-local live read contracts for every GitLab class answered by the MCP tools, with direct field bindings and parser-backed validation. Keep Incident and SoftwareProject crosswalks outside this unapproved source contract. Spec: none (connector-side artifact is governed by SDK-CONNECTOR-CONTROL-R021; no local requirement row) Co-Authored-By: Codex --- gitlab_api/ontology/access_contracts.ttl | 287 +++++++++++++++++++++++ tests/test_access_contracts.py | 75 ++++++ 2 files changed, 362 insertions(+) create mode 100644 gitlab_api/ontology/access_contracts.ttl create mode 100644 tests/test_access_contracts.py diff --git a/gitlab_api/ontology/access_contracts.ttl b/gitlab_api/ontology/access_contracts.ttl new file mode 100644 index 0000000..8c3fa50 --- /dev/null +++ b/gitlab_api/ontology/access_contracts.ttl @@ -0,0 +1,287 @@ +@prefix ac: . +@prefix gl: . + +# Read contracts for the classes declared by gitlab.ttl. The MCP tools expose +# both read and write actions, but these mappings are read-only query routes: +# callers select the listed get action through the operation's action envelope. +# The source classes stay in GitLab's ontology namespace. No heuristic +# crosswalk to a hub class is asserted here. + +gl:gitlabGroupAccess a ac:AccessContract ; + ac:classIri gl:GitLabGroup ; + ac:sourceId "gitlab-api" ; + ac:entity "group" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_groups" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "medium" . + +gl:gitlabGroupWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:gitlabGroupAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:projectAccess a ac:AccessContract ; + ac:classIri gl:Project ; + ac:sourceId "gitlab-api" ; + ac:entity "project" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_projects" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "low" . + +gl:projectWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:projectAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:mergeRequestAccess a ac:AccessContract ; + ac:classIri gl:MergeRequest ; + ac:sourceId "gitlab-api" ; + ac:entity "merge_request" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_merge_requests" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:mergeRequestIidBinding a ac:PropertyBinding ; + ac:contract gl:mergeRequestAccess ; ac:property gl:iid ; ac:field "iid" . + +gl:mergeRequestStateBinding a ac:PropertyBinding ; + ac:contract gl:mergeRequestAccess ; ac:property gl:state ; ac:field "state" . + +gl:mergeRequestWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:mergeRequestAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:pipelineAccess a ac:AccessContract ; + ac:classIri gl:Pipeline ; + ac:sourceId "gitlab-api" ; + ac:entity "pipeline" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_pipelines" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:pipelineStatusBinding a ac:PropertyBinding ; + ac:contract gl:pipelineAccess ; ac:property gl:pipelineStatus ; ac:field "status" . + +gl:pipelineRefBinding a ac:PropertyBinding ; + ac:contract gl:pipelineAccess ; ac:property gl:ref ; ac:field "ref" . + +gl:pipelineShaBinding a ac:PropertyBinding ; + ac:contract gl:pipelineAccess ; ac:property gl:sha ; ac:field "sha" . + +gl:pipelineWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:pipelineAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:pipelineRunAccess a ac:AccessContract ; + ac:classIri gl:PipelineRun ; + ac:sourceId "gitlab-api" ; + ac:entity "pipeline" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_pipelines" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:jobAccess a ac:AccessContract ; + ac:classIri gl:Job ; + ac:sourceId "gitlab-api" ; + ac:entity "job" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_jobs" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:jobStatusBinding a ac:PropertyBinding ; + ac:contract gl:jobAccess ; ac:property gl:pipelineStatus ; ac:field "status" . + +gl:jobRefBinding a ac:PropertyBinding ; + ac:contract gl:jobAccess ; ac:property gl:ref ; ac:field "ref" . + +gl:jobWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:jobAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:checkRunAccess a ac:AccessContract ; + ac:classIri gl:CheckRun ; + ac:sourceId "gitlab-api" ; + ac:entity "job" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_jobs" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:issueAccess a ac:AccessContract ; + ac:classIri gl:Issue ; + ac:sourceId "gitlab-api" ; + ac:entity "issue" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_issues" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:issueIidBinding a ac:PropertyBinding ; + ac:contract gl:issueAccess ; ac:property gl:iid ; ac:field "iid" . + +gl:issueStateBinding a ac:PropertyBinding ; + ac:contract gl:issueAccess ; ac:property gl:state ; ac:field "state" . + +gl:issueWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:issueAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:epicAccess a ac:AccessContract ; + ac:classIri gl:Epic ; + ac:sourceId "gitlab-api" ; + ac:entity "epic" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_epics" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "medium" . + +gl:epicIidBinding a ac:PropertyBinding ; + ac:contract gl:epicAccess ; ac:property gl:iid ; ac:field "iid" . + +gl:epicWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:epicAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:milestoneAccess a ac:AccessContract ; + ac:classIri gl:Milestone ; + ac:sourceId "gitlab-api" ; + ac:entity "milestone" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_milestones" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "medium" . + +gl:milestoneStateBinding a ac:PropertyBinding ; + ac:contract gl:milestoneAccess ; ac:property gl:state ; ac:field "state" . + +gl:milestoneWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:milestoneAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:commitAccess a ac:AccessContract ; + ac:classIri gl:Commit ; + ac:sourceId "gitlab-api" ; + ac:entity "commit" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_commits" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 60 ; + ac:costHint "medium" . + +gl:commitShaBinding a ac:PropertyBinding ; + ac:contract gl:commitAccess ; ac:property gl:sha ; ac:field "sha" . + +gl:commitWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:commitAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:branchAccess a ac:AccessContract ; + ac:classIri gl:Branch ; + ac:sourceId "gitlab-api" ; + ac:entity "branch" ; + ac:keyField "name" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_branches" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "low" . + +gl:branchWebUrlBinding a ac:PropertyBinding ; + ac:contract gl:branchAccess ; ac:property gl:webUrl ; ac:field "web_url" . + +gl:releaseAccess a ac:AccessContract ; + ac:classIri gl:Release ; + ac:sourceId "gitlab-api" ; + ac:entity "release" ; + ac:keyField "tag_name" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_releases" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "medium" . + +gl:runnerAccess a ac:AccessContract ; + ac:classIri gl:Runner ; + ac:sourceId "gitlab-api" ; + ac:entity "runner" ; + ac:keyField "id" ; + ac:accessKind "mcp_tool" ; + ac:operation "gitlab_runners" ; + ac:parameter "action", "params_json" ; + ac:pagination "page" ; + ac:pushdown "filter", "limit", "sort" ; + ac:authRef "env://GITLAB_TOKEN" ; + ac:rateLimitPerMinute 60 ; + ac:freshnessSeconds 300 ; + ac:costHint "medium" . + +gl:runnerStateBinding a ac:PropertyBinding ; + ac:contract gl:runnerAccess ; ac:property gl:state ; ac:field "status" . diff --git a/tests/test_access_contracts.py b/tests/test_access_contracts.py new file mode 100644 index 0000000..6448789 --- /dev/null +++ b/tests/test_access_contracts.py @@ -0,0 +1,75 @@ +"""GitLab's read-only source access contracts stay source-local.""" + +from __future__ import annotations + +from pathlib import Path + +from agent_connector_sdk.access_contract import parse_access_contracts + + +ROOT = Path(__file__).resolve().parents[1] +CONTRACTS = ROOT / "gitlab_api" / "ontology" / "access_contracts.ttl" +KG = "http://knuckles.team/kg#" + +EXPECTED = { + "GitLabGroup": ("gitlab_groups", "group", "id"), + "Project": ("gitlab_projects", "project", "id"), + "MergeRequest": ("gitlab_merge_requests", "merge_request", "id"), + "Pipeline": ("gitlab_pipelines", "pipeline", "id"), + "PipelineRun": ("gitlab_pipelines", "pipeline", "id"), + "Job": ("gitlab_jobs", "job", "id"), + "CheckRun": ("gitlab_jobs", "job", "id"), + "Issue": ("gitlab_issues", "issue", "id"), + "Epic": ("gitlab_epics", "epic", "id"), + "Milestone": ("gitlab_milestones", "milestone", "id"), + "Commit": ("gitlab_commits", "commit", "id"), + "Branch": ("gitlab_branches", "branch", "name"), + "Release": ("gitlab_releases", "release", "tag_name"), + "Runner": ("gitlab_runners", "runner", "id"), +} + + +def _contracts(): + return parse_access_contracts(CONTRACTS.read_text(encoding="utf-8")) + + +def test_every_declared_gitlab_class_has_a_typed_read_contract() -> None: + contracts = _contracts() + by_class = {contract.class_iri.rsplit("#", 1)[-1]: contract for contract in contracts} + + assert set(by_class) == set(EXPECTED) + for class_name, (operation, entity, key_field) in EXPECTED.items(): + contract = by_class[class_name] + assert contract.class_iri == KG + class_name + assert contract.source_id == "gitlab-api" + assert contract.access_kind == "mcp_tool" + assert contract.operation == operation + assert contract.entity == entity + assert contract.key_field == key_field + assert contract.parameters == ("action", "params_json") + assert contract.auth_ref == "env://GITLAB_TOKEN" + assert contract.pagination == "page" + assert contract.pushdown == frozenset({"filter", "limit", "sort"}) + + +def test_gitlab_contracts_do_not_promote_draft_crosswalks() -> None: + contracts = _contracts() + + assert all(contract.class_iri.startswith(KG) for contract in contracts) + assert all( + contract.class_iri not in {KG + "Incident", KG + "SoftwareProject"} + for contract in contracts + ) + assert all(contract.source_id == "gitlab-api" for contract in contracts) + + +def test_gitlab_contract_artifact_is_packaged_under_ontology() -> None: + assert CONTRACTS.is_file() + + +def test_gitlab_contract_bindings_use_declared_fields() -> None: + contracts = {contract.class_iri.rsplit("#", 1)[-1]: contract for contract in _contracts()} + + assert (KG + "webUrl", "web_url") in contracts["Project"].predicates + assert (KG + "pipelineStatus", "status") in contracts["Pipeline"].predicates + assert (KG + "sha", "sha") in contracts["Commit"].predicates From 35330319be7d73a1e2daf9e23294be849ec8225f Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Sun, 25 Oct 2026 17:03:06 -0500 Subject: [PATCH 2/3] chore(release): bump gitlab-api to 27.3.14 Use the repository's bump2version tool for the next publishable artifact version and refresh the lockfile root metadata. Spec: none (connector-side artifact is governed by SDK-CONNECTOR-CONTROL-R021; no local requirement row) Co-Authored-By: Codex --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 08c1fb1..9ab8f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gitlab-api" -version = "27.3.13" +version = "27.3.14" description = "GitLab API + MCP Server" readme = "README.md" classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Environment :: Console", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3",] diff --git a/uv.lock b/uv.lock index bafd457..c74db4c 100644 --- a/uv.lock +++ b/uv.lock @@ -611,7 +611,7 @@ server = [ [[package]] name = "gitlab-api" -version = "27.3.13" +version = "27.3.14" source = { editable = "." } dependencies = [ { name = "agent-connector-sdk", extra = ["graphql", "mcp"] }, From 72c4df1b11400ff4c5f5e86aca51005ba35ea3fc Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Sun, 25 Oct 2026 17:04:06 -0500 Subject: [PATCH 3/3] chore(spec): refresh generated status output Regenerate status.json files with the current shared spec-status generator so the pre-push specification gate has canonical output. Spec: none (generated status refresh required by the repository pre-push gate) Co-Authored-By: Codex --- specs/_template/status.json | 4 +--- specs/connector-content/status.json | 14 ++------------ specs/gitlab-api/status.json | 4 +--- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/specs/_template/status.json b/specs/_template/status.json index 9392d49..f641a3c 100644 --- a/specs/_template/status.json +++ b/specs/_template/status.json @@ -4,7 +4,5 @@ "owner_repo": "gitlab-api", "delivery_state": "RETIRED", "requirement_ids": [], - "requirements": [], - "frontier": [], - "unserved": [] + "requirements": [] } diff --git a/specs/connector-content/status.json b/specs/connector-content/status.json index 72938b2..bf2e24e 100644 --- a/specs/connector-content/status.json +++ b/specs/connector-content/status.json @@ -26,8 +26,7 @@ "landed_in": [], "verified_by": [ "tests/test_ontology_parent_link.py:10" - ], - "priority": 0 + ] }, { "id": "CONN-GITLAB-API-CONNECTOR-CONTENT-R004", @@ -36,8 +35,7 @@ "landed_in": [], "verified_by": [ "tests/test_cq_paths.py:13" - ], - "priority": 0 + ] }, { "id": "CONN-GITLAB-API-CONNECTOR-CONTENT-R005", @@ -51,13 +49,5 @@ "tests/test_catalog_edges.py:43" ] } - ], - "frontier": [ - "CONN-GITLAB-API-CONNECTOR-CONTENT-R002", - "CONN-GITLAB-API-CONNECTOR-CONTENT-R004" - ], - "unserved": [ - "CONN-GITLAB-API-CONNECTOR-CONTENT-R002", - "CONN-GITLAB-API-CONNECTOR-CONTENT-R004" ] } diff --git a/specs/gitlab-api/status.json b/specs/gitlab-api/status.json index 297cead..8c29707 100644 --- a/specs/gitlab-api/status.json +++ b/specs/gitlab-api/status.json @@ -47,7 +47,5 @@ "tests/test_gitlab_gql.py:237" ] } - ], - "frontier": [], - "unserved": [] + ] }