Publish curated ITWorx_MCP_Hub source from 9ec6419178e8ddc76a698e84bde72d5ff4472411
This commit is contained in:
1 parent
5ff5c807d3
commit
f26354e16c
20 files changed
+1627
-72
No files matched your search
@@ -1,3 +1,3 @@
|
||||
# Public source export
|
||||
|
||||
This source snapshot was generated from the private canonical repository at revision `e1fd00a3172eaaec29bf8f1e6e47f96d175c31ff`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
This source snapshot was generated from the private canonical repository at revision `9ec6419178e8ddc76a698e84bde72d5ff4472411`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
+51
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"sourceRevision": "e1fd00a3172eaaec29bf8f1e6e47f96d175c31ff",
|
||||
"sourceRevision": "9ec6419178e8ddc76a698e84bde72d5ff4472411",
|
||||
"files": [
|
||||
{
|
||||
"path": ".dockerignore",
|
||||
@@ -509,8 +509,8 @@
|
||||
},
|
||||
{
|
||||
"path": "catalog/policies/041-appops-governed-operations.yaml",
|
||||
"sha256": "1426694df3ec9f8d4691bce055abc21bd991d2a0041ba4b3b0e6619de7e747c1",
|
||||
"bytes": 1024
|
||||
"sha256": "915a080783aaf99145699b30d1556f16fd799d4c267f588d1d4dc06fac2ee047",
|
||||
"bytes": 1068
|
||||
},
|
||||
{
|
||||
"path": "catalog/policies/042-stitch-governed-design-writes.yaml",
|
||||
@@ -584,8 +584,8 @@
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-branch-protection-ensure.yaml",
|
||||
"sha256": "b0750a7de2b10c8c0403444e96bf9a9ef6c60c3c184f02bb8322de5d732f2bff",
|
||||
"bytes": 822
|
||||
"sha256": "bb072d1f18a17d7bbe9f7e628fd9c93ac7d8af7be2065b40d6195898e42502ff",
|
||||
"bytes": 968
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-branch-protection-get.yaml",
|
||||
@@ -594,8 +594,8 @@
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-branch-protection-preview.yaml",
|
||||
"sha256": "a31c94a963c8d384bfbd67706dec8a82ddf18a889f8749ac6f79d41b99e6670d",
|
||||
"bytes": 735
|
||||
"sha256": "e975e08cc47d619f61489cb1eb78ff6d8c0eca2fc281301f21bca83dc74e14ea",
|
||||
"bytes": 787
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-capabilities-get.yaml",
|
||||
@@ -709,18 +709,18 @@
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-portfolio-governance-apply.yaml",
|
||||
"sha256": "7b4f20d29390a005e13ed4d4a3726dadb0d19d15814ad61d4c265b717ce06153",
|
||||
"bytes": 960
|
||||
"sha256": "6c096efb328d513b7b7995c79e9cb03caf3ba85017d3af4941101baaae43f7b2",
|
||||
"bytes": 1183
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-portfolio-governance-preview.yaml",
|
||||
"sha256": "dcf866336af236d81dde3476988f066d1b86bfb2e8ae3432af30a068cab18a4b",
|
||||
"bytes": 810
|
||||
"sha256": "1ecb3aa9ae7b60871b48066221ae02b76c156422d96f4e86526196d8b9914d72",
|
||||
"bytes": 819
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-portfolio-governance-verify.yaml",
|
||||
"sha256": "59acb31c46829ac985ec9b1291b08c5eadd03d657be3d42b6c23fa9cfe12f86e",
|
||||
"bytes": 805
|
||||
"sha256": "425ce50c7d8fda4ea745fb631da869fe2023a8f1f846a4c7cece5d472f00416a",
|
||||
"bytes": 814
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/gitea-portfolio-health.yaml",
|
||||
@@ -1402,6 +1402,26 @@
|
||||
"sha256": "e90091e39c5e63285954f9f322f8afe2c2c35575152d13f7b31e29166c519648",
|
||||
"bytes": 659
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/server-appops-deployment-status.yaml",
|
||||
"sha256": "d766ff2982f8b3232d931160f3e1c369e94eb1ac01b4a44b6b15ff0d1d7ad658",
|
||||
"bytes": 661
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/server-appops-self-upgrade-apply.yaml",
|
||||
"sha256": "85f3d7f7cfb09312704881b48154ba0af71e82da22985ed7cf31ccb4b3fbe56c",
|
||||
"bytes": 1235
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/server-appops-self-upgrade-preflight.yaml",
|
||||
"sha256": "9eae687b97913f99642161c9c8bd5a1f1627671e1150fcf180366d1a77d18e6f",
|
||||
"bytes": 1011
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/server-appops-self-upgrade-status.yaml",
|
||||
"sha256": "500515ffee25623be12e847b4f81edb43edc13cee294c7174bf97be2a7cee8c5",
|
||||
"bytes": 766
|
||||
},
|
||||
{
|
||||
"path": "catalog/tools/server-audit-search.yaml",
|
||||
"sha256": "d43c06ce0b1d423a0ce13262b9189ffd645d3ea6cf2c3e9ea3fd4740c7fa293e",
|
||||
@@ -1634,8 +1654,8 @@
|
||||
},
|
||||
{
|
||||
"path": "connectors/gitea/server.py",
|
||||
"sha256": "7da7c3be6d259ef2c897f52c82771716a602559f7671c794ec6380634ffdc91e",
|
||||
"bytes": 209422
|
||||
"sha256": "d8c5be1efed22d99dc287a83fd78156e306afa731f6b233b4d076bb5736152b4",
|
||||
"bytes": 227648
|
||||
},
|
||||
{
|
||||
"path": "connectors/knowledge/__init__.py",
|
||||
@@ -2554,8 +2574,8 @@
|
||||
},
|
||||
{
|
||||
"path": "services/control_api/contextforge/fake.py",
|
||||
"sha256": "74cf3f097ea40c761d6bed05ee457d9fb0eea5bc603eba565da3e2da9b7d8023",
|
||||
"bytes": 18589
|
||||
"sha256": "7cd684a569d5df1624af6f449e4f13199b8b113c1244dd4afec0de2121421fc4",
|
||||
"bytes": 20697
|
||||
},
|
||||
{
|
||||
"path": "services/control_api/contextforge/models.py",
|
||||
@@ -2734,13 +2754,13 @@
|
||||
},
|
||||
{
|
||||
"path": "services/runtime/connector_appops.py",
|
||||
"sha256": "976f81079e1deb7b0eea2445e80da21bc5057ed788058b646f0afb8834316dfd",
|
||||
"bytes": 9190
|
||||
"sha256": "22360e7ac393d2a72cacd8309a6e68f2d232603271649992706a7f6c5ee3a83a",
|
||||
"bytes": 9474
|
||||
},
|
||||
{
|
||||
"path": "services/runtime/connector_gitea.py",
|
||||
"sha256": "144b4e7d8fc1a8fb37dc011dab2bdd169fc869382a361e35aee435ddc7f59a34",
|
||||
"bytes": 37459
|
||||
"sha256": "c89874089d1bdde3020864e9222052d2470970ad7c4aa5677201cbfbc4a88eea",
|
||||
"bytes": 37958
|
||||
},
|
||||
{
|
||||
"path": "services/runtime/connector_ingestion.py",
|
||||
@@ -2887,6 +2907,11 @@
|
||||
"sha256": "006648857d313b8f7fcaa62b86d5f453aa565a75a348aefe7b8557e3b4ba8620",
|
||||
"bytes": 63
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/projectbrain-managed-validation.yml",
|
||||
"sha256": "2f78d37ff51f38f0a3b0c9e222eb2dbf83d5108aee3615eb53f2c01283f37f34",
|
||||
"bytes": 9185
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/security/wp160-untrusted-inputs.json",
|
||||
"sha256": "779ab274237df2dfd4d23f4b0fe2c21621594ca5a3f64aeef586ba448f702a5c",
|
||||
@@ -2899,8 +2924,8 @@
|
||||
},
|
||||
{
|
||||
"path": "tests/test_appops_connector.py",
|
||||
"sha256": "5520aa212d9a175da22cb4bb584f8c36811e923003f3d13d468ebfd224f334f4",
|
||||
"bytes": 5257
|
||||
"sha256": "4e088d187b239cd1e8b81b7c5d72aed27eddae78491678564bfe2776d37913be",
|
||||
"bytes": 9448
|
||||
},
|
||||
{
|
||||
"path": "tests/test_auth.py",
|
||||
@@ -2914,7 +2939,7 @@
|
||||
},
|
||||
{
|
||||
"path": "tests/test_catalog.py",
|
||||
"sha256": "e591b502f2dd07ae165b5cafa8f0a0e42a84530b8a352c963116cb803e865cb8",
|
||||
"sha256": "adbe584172170fa17fe21c0e6a1953729e9b83190845d8e9d053b0d2cef4b1c6",
|
||||
"bytes": 16467
|
||||
},
|
||||
{
|
||||
@@ -3039,8 +3064,8 @@
|
||||
},
|
||||
{
|
||||
"path": "tests/test_gitea_connector.py",
|
||||
"sha256": "62c25f2953cd6d0d02c59d07925430886f87b5f04f6f995ef4c8168a77e59483",
|
||||
"bytes": 97203
|
||||
"sha256": "c6389e924471d392f1a19cae48e9c409b349223b76faa34c20b9ae4c7894ede2",
|
||||
"bytes": 128684
|
||||
},
|
||||
{
|
||||
"path": "tests/test_gitea_merge_eligibility.py",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"server.project.reconcile",
|
||||
"server.deployment.target.enroll",
|
||||
"server.deployment.apply",
|
||||
"server.deployment.rollback"
|
||||
"server.deployment.rollback",
|
||||
"server.appops.self_upgrade.apply"
|
||||
]
|
||||
},
|
||||
"conditions": {"requireEnabledClient": true, "requireBinding": true, "maxArgumentBytes": 65536},
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Branch Protection Ensure","slug":"gitea-branch-protection-ensure"},"spec":{"canonicalName":"gitea.branchprotection.ensure","version":"1.8.0","connectorType":"gitea","description":"Apply one unchanged governance preview and verify managed validation plus default-branch protection.","riskClass":"R2","readOnly":false,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"},"expectedPlanHash":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"required":["repositoryRef","expectedPlanHash"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":30000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"documents","requiredScopes":["project:{project}:write"]}}
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Branch Protection Ensure","slug":"gitea-branch-protection-ensure"},"spec":{"canonicalName":"gitea.branchprotection.ensure","version":"1.9.0","connectorType":"gitea","description":"Apply one unchanged governance preview with an explicitly hashed scope; ProjectBrain protection-only scope adds required status checks without rewriting its workflow.","riskClass":"R2","readOnly":false,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"},"expectedPlanHash":{"type":"string","pattern":"^[0-9a-f]{64}$"},"applyScope":{"type":"string","enum":["all","protection_only"],"default":"all"}},"required":["repositoryRef","expectedPlanHash"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":30000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"documents","requiredScopes":["project:{project}:write"]}}
|
||||
@@ -1 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Branch Protection Preview","slug":"gitea-branch-protection-preview"},"spec":{"canonicalName":"gitea.branchprotection.preview","version":"1.8.0","connectorType":"gitea","description":"Preview the exact managed-validation and default-branch protection diff and return a plan hash.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"}},"required":["repositoryRef"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":20000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Branch Protection Preview","slug":"gitea-branch-protection-preview"},"spec":{"canonicalName":"gitea.branchprotection.preview","version":"1.9.0","connectorType":"gitea","description":"Preview the exact managed-validation profile and default-branch protection diff, including a separately hashed ProjectBrain protection-only option.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"}},"required":["repositoryRef"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":20000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
@@ -1 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Apply","slug":"gitea-portfolio-governance-apply"},"spec":{"canonicalName":"gitea.portfolio.governance.apply","version":"1.8.0","connectorType":"gitea","description":"Apply and verify at most five exact repository governance plans in one bounded batch.","riskClass":"R2","readOnly":false,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"plans":{"type":"array","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"},"planHash":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"required":["repositoryRef","planHash"]}}},"required":["plans"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":120000,"maxOutputBytes":524288,"maxItems":5},"redactionProfile":"documents","requiredScopes":["project:{project}:write"]}}
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Apply","slug":"gitea-portfolio-governance-apply"},"spec":{"canonicalName":"gitea.portfolio.governance.apply","version":"1.9.0","connectorType":"gitea","description":"Apply and verify at most five exact repository governance plans, including the separately hashed ProjectBrain protection-only scope.","riskClass":"R2","readOnly":false,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"plans":{"type":"array","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"type":"object","additionalProperties":false,"properties":{"repositoryRef":{"type":"string"},"planHash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Use planHash for all scope or protectionOnlyPlanHash for protection_only scope."},"applyScope":{"type":"string","enum":["all","protection_only"],"default":"all"}},"required":["repositoryRef","planHash"]}}},"required":["plans"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":120000,"maxOutputBytes":524288,"maxItems":5},"redactionProfile":"documents","requiredScopes":["project:{project}:write"]}}
|
||||
@@ -1 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Preview","slug":"gitea-portfolio-governance-preview"},"spec":{"canonicalName":"gitea.portfolio.governance.preview","version":"1.8.0","connectorType":"gitea","description":"Preview managed validation and branch protection for at most 25 explicit repositories.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRefs":{"type":"array","minItems":1,"maxItems":25,"uniqueItems":true,"items":{"type":"string"}}},"required":["repositoryRefs"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":60000,"maxOutputBytes":524288,"maxItems":25},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Preview","slug":"gitea-portfolio-governance-preview"},"spec":{"canonicalName":"gitea.portfolio.governance.preview","version":"1.9.0","connectorType":"gitea","description":"Preview managed-validation profiles and branch protection for at most 25 explicit repositories.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRefs":{"type":"array","minItems":1,"maxItems":25,"uniqueItems":true,"items":{"type":"string"}}},"required":["repositoryRefs"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":60000,"maxOutputBytes":524288,"maxItems":25},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
@@ -1 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Verify","slug":"gitea-portfolio-governance-verify"},"spec":{"canonicalName":"gitea.portfolio.governance.verify","version":"1.8.0","connectorType":"gitea","description":"Verify managed validation and default-branch protection for at most 25 repositories.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRefs":{"type":"array","minItems":1,"maxItems":25,"uniqueItems":true,"items":{"type":"string"}}},"required":["repositoryRefs"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":60000,"maxOutputBytes":524288,"maxItems":25},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Gitea Portfolio Governance Verify","slug":"gitea-portfolio-governance-verify"},"spec":{"canonicalName":"gitea.portfolio.governance.verify","version":"1.9.0","connectorType":"gitea","description":"Verify managed-validation profiles and default-branch protection for at most 25 repositories.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repositoryRefs":{"type":"array","minItems":1,"maxItems":25,"uniqueItems":true,"items":{"type":"string"}}},"required":["repositoryRefs"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":60000,"maxOutputBytes":524288,"maxItems":25},"redactionProfile":"documents","requiredScopes":["project:{project}:read"]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Server AppOps Deployment Status","slug":"server-appops-deployment-status"},"spec":{"canonicalName":"server.appops.deployment.status","version":"1.4.0","connectorType":"appops","description":"Read the exact source repository, revision, source tree and health of the running AppOps gateway image.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false},"outputSchema":{"type":"object"},"limits":{"timeoutMs":30000,"maxOutputBytes":65536,"maxItems":1},"redactionProfile":"logs","requiredScopes":["project:{project}:read"]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Server AppOps Self-Upgrade Apply","slug":"server-appops-self-upgrade-apply"},"spec":{"canonicalName":"server.appops.self_upgrade.apply","version":"1.4.0","connectorType":"appops","description":"Request only the preflighted exact AppOps revision through its private single-instance controller, bound to the preflight fingerprint and idempotency key.","riskClass":"R3","readOnly":false,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repository":{"type":"string","const":"jens/unraid-appops-gateway"},"revision":{"type":"string","pattern":"^[0-9a-f]{40}$"},"reason":{"type":"string","minLength":8,"maxLength":500},"expected_preflight_fingerprint":{"type":"string","pattern":"^[0-9a-f]{64}$"},"idempotency_key":{"type":"string","pattern":"^[A-Za-z0-9._:-]{8,128}$"},"expected_current_revision":{"type":"string","pattern":"^[0-9a-f]{40}$"}},"required":["repository","revision","reason","expected_preflight_fingerprint","idempotency_key"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":60000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"logs","requiredScopes":["project:{project}:write"]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Server AppOps Self-Upgrade Preflight","slug":"server-appops-self-upgrade-preflight"},"spec":{"canonicalName":"server.appops.self_upgrade.preflight","version":"1.4.0","connectorType":"appops","description":"Check one exact protected-main AppOps revision and current-revision expectation through the private deployment controller.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"repository":{"type":"string","const":"jens/unraid-appops-gateway"},"revision":{"type":"string","pattern":"^[0-9a-f]{40}$"},"reason":{"type":"string","minLength":8,"maxLength":500},"expected_current_revision":{"type":"string","pattern":"^[0-9a-f]{40}$"}},"required":["repository","revision","reason"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":30000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"logs","requiredScopes":["project:{project}:read"]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"apiVersion":"hub.example.com/v1alpha1","kind":"ToolManifest","metadata":{"name":"Server AppOps Self-Upgrade Status","slug":"server-appops-self-upgrade-status"},"spec":{"canonicalName":"server.appops.self_upgrade.status","version":"1.4.0","connectorType":"appops","description":"Read durable status and exact deployment receipt for one AppOps self-upgrade operation identifier.","riskClass":"R1","readOnly":true,"idempotent":true,"inputSchema":{"type":"object","additionalProperties":false,"properties":{"operation_id":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"required":["operation_id"]},"outputSchema":{"type":"object"},"limits":{"timeoutMs":30000,"maxOutputBytes":262144,"maxItems":1},"redactionProfile":"logs","requiredScopes":["project:{project}:read"]}}
|
||||
+470
-30
@@ -13,9 +13,11 @@ import time
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
import yaml
|
||||
|
||||
from packages.connector_kit import (
|
||||
ConnectorContext,
|
||||
GiteaClient,
|
||||
@@ -72,6 +74,208 @@ _REPOSITORY_CONTEXT_DOCUMENT_LIMIT = 4
|
||||
_REPOSITORY_CONTEXT_DOCUMENT_BYTES = 4_096
|
||||
_REPOSITORY_CONTEXT_DOCUMENT_TOTAL_BYTES = 8_192
|
||||
|
||||
_PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY = "jens/projectbrain"
|
||||
_PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT = "Pull request / quality (pull_request)"
|
||||
_PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS = (
|
||||
MANAGED_VALIDATION_CONTEXT,
|
||||
_PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT,
|
||||
)
|
||||
_PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS = {
|
||||
"enable_push": False,
|
||||
"enable_push_whitelist": False,
|
||||
"enable_force_push": False,
|
||||
"enable_force_push_allowlist": False,
|
||||
"enable_bypass_allowlist": False,
|
||||
"enable_status_check": True,
|
||||
"block_admin_merge_override": True,
|
||||
}
|
||||
# Original source commit for the ProjectBrain-specific profile lineage.
|
||||
_PROJECTBRAIN_VALIDATION_PROFILE_SOURCE_SHA = "57157b1b24d3679018b54970c934fc35d1735df7"
|
||||
# SHA-256 of JSON-dumped jobs.full (sort_keys, compact separators), parsed with
|
||||
# BaseLoader. It pins the reviewed ProjectBrain workflow copy below.
|
||||
# Frozen workflow source and copied fixture raw SHA-256:
|
||||
# 2f78d37ff51f38f0a3b0c9e222eb2dbf83d5108aee3615eb53f2c01283f37f34.
|
||||
# Refresh only after review.
|
||||
_PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST = (
|
||||
"a3fefb8865f0bd0d4a8c10af79eeb5e22367c5e82e9333a2fe04bdaf97d0193b"
|
||||
)
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS = 200_000
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS = 10_000
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH = 64
|
||||
_PROJECTBRAIN_VALIDATION_RUN_NAME = "Managed validation / ${{ inputs.profile || 'full' }}"
|
||||
_PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS = [
|
||||
"test",
|
||||
"lint",
|
||||
"typecheck",
|
||||
"build",
|
||||
"security",
|
||||
"full",
|
||||
]
|
||||
_PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS = frozenset(
|
||||
{"name", "run-name", "on", "permissions", "concurrency", "jobs"}
|
||||
)
|
||||
_PROJECTBRAIN_VALIDATION_CONCURRENCY = {
|
||||
"group": "managed-validation-${{ gitea.repository }}-${{ gitea.ref }}",
|
||||
"cancel-in-progress": "true",
|
||||
}
|
||||
|
||||
|
||||
class _UniqueWorkflowLoader(yaml.BaseLoader):
|
||||
"""Parse workflow YAML without YAML 1.1's special treatment of the `on` key."""
|
||||
|
||||
|
||||
def _construct_unique_workflow_mapping(loader: Any, node: Any) -> dict[str, Any]:
|
||||
mapping: dict[str, Any] = {}
|
||||
for key_node, value_node in node.value:
|
||||
key = loader.construct_object(key_node)
|
||||
if not isinstance(key, str) or key in mapping:
|
||||
raise yaml.constructor.ConstructorError(
|
||||
"while constructing a workflow mapping",
|
||||
node.start_mark,
|
||||
"workflow mapping keys must be unique strings",
|
||||
key_node.start_mark,
|
||||
)
|
||||
mapping[key] = loader.construct_object(value_node)
|
||||
return mapping
|
||||
|
||||
|
||||
_UniqueWorkflowLoader.add_constructor(
|
||||
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
||||
_construct_unique_workflow_mapping,
|
||||
)
|
||||
|
||||
|
||||
def _workflow_mapping(value: Any) -> dict[str, Any] | None:
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
|
||||
def _workflow_yaml_is_bounded_and_alias_free(workflow_content: str) -> bool:
|
||||
if len(workflow_content) > _PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS:
|
||||
return False
|
||||
container_starts = (
|
||||
yaml.tokens.BlockMappingStartToken,
|
||||
yaml.tokens.BlockSequenceStartToken,
|
||||
yaml.tokens.FlowMappingStartToken,
|
||||
yaml.tokens.FlowSequenceStartToken,
|
||||
)
|
||||
container_ends = (
|
||||
yaml.tokens.BlockEndToken,
|
||||
yaml.tokens.FlowMappingEndToken,
|
||||
yaml.tokens.FlowSequenceEndToken,
|
||||
)
|
||||
depth = 0
|
||||
try:
|
||||
for token_count, token in enumerate(
|
||||
yaml.scan(workflow_content, Loader=_UniqueWorkflowLoader), start=1
|
||||
):
|
||||
if token_count > _PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS:
|
||||
return False
|
||||
if isinstance(token, yaml.tokens.AnchorToken | yaml.tokens.AliasToken):
|
||||
return False
|
||||
if isinstance(token, container_starts):
|
||||
depth += 1
|
||||
if depth > _PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH:
|
||||
return False
|
||||
elif isinstance(token, container_ends):
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
return False
|
||||
except (yaml.YAMLError, RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
return depth == 0
|
||||
|
||||
|
||||
def _projectbrain_managed_validation_is_strong(workflow_content: str) -> bool:
|
||||
if not _workflow_yaml_is_bounded_and_alias_free(workflow_content):
|
||||
return False
|
||||
try:
|
||||
# BaseLoader constructs only strings/lists/maps; it never creates Python objects.
|
||||
workflow = yaml.load(workflow_content, Loader=_UniqueWorkflowLoader) # noqa: S506
|
||||
except (yaml.YAMLError, RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
document = _workflow_mapping(workflow)
|
||||
if document is None or document.get("name") != "Managed validation":
|
||||
return False
|
||||
if (
|
||||
not set(document).issubset(_PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS)
|
||||
or document.get("run-name") != _PROJECTBRAIN_VALIDATION_RUN_NAME
|
||||
or document.get("permissions") != {"contents": "read"}
|
||||
or document.get("concurrency") != _PROJECTBRAIN_VALIDATION_CONCURRENCY
|
||||
):
|
||||
return False
|
||||
|
||||
triggers = _workflow_mapping(document.get("on"))
|
||||
if (
|
||||
triggers is None
|
||||
or set(triggers) != {"push", "pull_request", "workflow_dispatch"}
|
||||
):
|
||||
return False
|
||||
dispatch = _workflow_mapping(triggers.get("workflow_dispatch"))
|
||||
dispatch_inputs = _workflow_mapping(dispatch.get("inputs")) if dispatch is not None else None
|
||||
profile_input = (
|
||||
_workflow_mapping(dispatch_inputs.get("profile"))
|
||||
if dispatch_inputs is not None
|
||||
else None
|
||||
)
|
||||
if (
|
||||
dispatch_inputs is None
|
||||
or set(dispatch) != {"inputs"}
|
||||
or set(dispatch_inputs) != {"profile"}
|
||||
or profile_input is None
|
||||
or set(profile_input) != {"description", "required", "default", "type", "options"}
|
||||
or not isinstance(profile_input.get("description"), str)
|
||||
or profile_input.get("required") != "true"
|
||||
or profile_input.get("default") != "full"
|
||||
or profile_input.get("type") != "choice"
|
||||
or profile_input.get("options") != _PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS
|
||||
):
|
||||
return False
|
||||
push = _workflow_mapping(triggers.get("push"))
|
||||
if (
|
||||
push is None
|
||||
or set(push) != {"branches"}
|
||||
or push.get("branches") != ["main"]
|
||||
):
|
||||
return False
|
||||
pull_request = triggers.get("pull_request")
|
||||
if pull_request not in (None, ""):
|
||||
pull_request_mapping = _workflow_mapping(pull_request)
|
||||
if pull_request_mapping is None:
|
||||
return False
|
||||
if set(pull_request_mapping) - {"branches"}:
|
||||
return False
|
||||
branches = pull_request_mapping.get("branches")
|
||||
if branches is not None and branches != ["main"]:
|
||||
return False
|
||||
|
||||
jobs = _workflow_mapping(document.get("jobs"))
|
||||
if jobs is None or set(jobs) != {"full"}:
|
||||
return False
|
||||
full_job = _workflow_mapping(jobs.get("full"))
|
||||
if full_job is None:
|
||||
return False
|
||||
try:
|
||||
full_job_digest = hashlib.sha256(
|
||||
json.dumps(full_job, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
except (RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
return full_job_digest == _PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST
|
||||
|
||||
|
||||
def _managed_validation_profile(
|
||||
repository_qualified_name: str, workflow_content: str | None
|
||||
) -> str | None:
|
||||
"""Accept only the canonical workflow, except for ProjectBrain's stronger bounded profile."""
|
||||
if workflow_content is None:
|
||||
return None
|
||||
if repository_qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY:
|
||||
if _projectbrain_managed_validation_is_strong(workflow_content):
|
||||
return "projectbrain"
|
||||
return None
|
||||
digest = hashlib.sha256(workflow_content.encode("utf-8")).hexdigest()
|
||||
return "canonical" if digest == MANAGED_VALIDATION_DIGEST else None
|
||||
|
||||
|
||||
def _select_validation_workflow(workflows: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||
"""Prefer the bounded managed contract over legacy validation workflows."""
|
||||
@@ -177,6 +381,19 @@ def _branch_protection_projection(payload: dict[str, Any] | None) -> dict[str, A
|
||||
return {key: payload.get(key) for key in desired_keys}
|
||||
|
||||
|
||||
def _branch_protection_effective_projection(
|
||||
payload: dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
if payload is None:
|
||||
return None
|
||||
volatile_metadata = {"id", "created_at", "updated_at"}
|
||||
return {
|
||||
key: value
|
||||
for key, value in payload.items()
|
||||
if key not in volatile_metadata and key != "status_check_contexts"
|
||||
}
|
||||
|
||||
|
||||
def _validate_ref(value: str, *, change_branch: bool = False) -> str:
|
||||
pattern = _SAFE_CHANGE_BRANCH if change_branch else _SAFE_REF
|
||||
if (
|
||||
@@ -800,14 +1017,19 @@ class GiteaConnector:
|
||||
None,
|
||||
)
|
||||
desired = _desired_branch_protection(default_branch or "main")
|
||||
if current is not None:
|
||||
existing_contexts = current.get("status_check_contexts")
|
||||
if isinstance(existing_contexts, list):
|
||||
required_contexts = (
|
||||
_PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS
|
||||
if repository.qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY
|
||||
else (MANAGED_VALIDATION_CONTEXT,)
|
||||
)
|
||||
existing_contexts = current.get("status_check_contexts") if current is not None else None
|
||||
preserved_contexts = (
|
||||
{str(item) for item in existing_contexts if str(item).strip()}
|
||||
if isinstance(existing_contexts, list)
|
||||
else set()
|
||||
)
|
||||
desired["status_check_contexts"] = sorted(
|
||||
{
|
||||
MANAGED_VALIDATION_CONTEXT,
|
||||
*(str(item) for item in existing_contexts if str(item).strip()),
|
||||
}
|
||||
set(required_contexts).union(preserved_contexts)
|
||||
)
|
||||
current_projection = _branch_protection_projection(current)
|
||||
protection_diff = {
|
||||
@@ -839,13 +1061,51 @@ class GiteaConnector:
|
||||
if workflow_content is not None
|
||||
else None
|
||||
)
|
||||
workflow_profile = _managed_validation_profile(
|
||||
repository.qualified_name, workflow_content
|
||||
)
|
||||
is_projectbrain = (
|
||||
repository.qualified_name.casefold()
|
||||
== _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY
|
||||
)
|
||||
if is_projectbrain and workflow_profile is None:
|
||||
blockers.append("managed-validation-projectbrain-profile-invalid")
|
||||
# Fail closed: governance must never replace ProjectBrain's
|
||||
# repository-specific database/E2E gate with the generic baseline.
|
||||
workflow_action = "none"
|
||||
desired_workflow_digest = None
|
||||
else:
|
||||
workflow_action = (
|
||||
"create"
|
||||
if workflow_content is None
|
||||
else "none"
|
||||
if workflow_digest == MANAGED_VALIDATION_DIGEST
|
||||
if workflow_profile is not None
|
||||
else "update"
|
||||
)
|
||||
desired_workflow_digest = (
|
||||
workflow_digest
|
||||
if workflow_profile == "projectbrain"
|
||||
else MANAGED_VALIDATION_DIGEST
|
||||
)
|
||||
if is_projectbrain and current_projection is not None:
|
||||
if any(
|
||||
current_projection.get(key) != expected
|
||||
for key, expected in _PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS.items()
|
||||
):
|
||||
blockers.append("managed-validation-projectbrain-protection-safety-locks-invalid")
|
||||
if workflow_profile == "projectbrain":
|
||||
desired = dict(current_projection)
|
||||
desired["status_check_contexts"] = sorted(
|
||||
set(required_contexts).union(preserved_contexts)
|
||||
)
|
||||
protection_diff = {
|
||||
key: {
|
||||
"current": None if current_projection is None else current_projection.get(key),
|
||||
"desired": value,
|
||||
}
|
||||
for key, value in desired.items()
|
||||
if current_projection is None or current_projection.get(key) != value
|
||||
}
|
||||
protection_action = "create" if current is None else "update" if protection_diff else "none"
|
||||
plan_payload = {
|
||||
"repositoryRef": repository_ref,
|
||||
@@ -853,7 +1113,9 @@ class GiteaConnector:
|
||||
"workflow": {
|
||||
"path": MANAGED_VALIDATION_PATH,
|
||||
"currentDigest": workflow_digest,
|
||||
"desiredDigest": MANAGED_VALIDATION_DIGEST,
|
||||
"desiredDigest": desired_workflow_digest,
|
||||
"profile": workflow_profile
|
||||
or ("projectbrain-required" if is_projectbrain else "canonical"),
|
||||
"action": workflow_action,
|
||||
},
|
||||
"branchProtection": {
|
||||
@@ -864,17 +1126,50 @@ class GiteaConnector:
|
||||
},
|
||||
"blockers": blockers,
|
||||
}
|
||||
effective_protection = _branch_protection_effective_projection(current)
|
||||
plan_hash_source: dict[str, Any] = plan_payload
|
||||
if is_projectbrain:
|
||||
plan_hash_source = {
|
||||
"plan": plan_payload,
|
||||
"effectiveProtection": effective_protection,
|
||||
}
|
||||
plan_hash = hashlib.sha256(
|
||||
json.dumps(plan_payload, sort_keys=True, separators=(",", ":")).encode()
|
||||
json.dumps(plan_hash_source, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
protection_only_available = (
|
||||
is_projectbrain
|
||||
and current is not None
|
||||
and current.get("enable_status_check") is True
|
||||
and default_branch == "main"
|
||||
and isinstance(desired.get("status_check_contexts"), list)
|
||||
and set(blockers) == {"managed-validation-projectbrain-profile-invalid"}
|
||||
)
|
||||
protection_only_plan_hash = (
|
||||
hashlib.sha256(
|
||||
json.dumps(
|
||||
{
|
||||
"applyScope": "protection_only",
|
||||
"plan": plan_payload,
|
||||
"effectiveProtection": effective_protection,
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
).hexdigest()
|
||||
if protection_only_available
|
||||
else None
|
||||
)
|
||||
return {
|
||||
**plan_payload,
|
||||
"planHash": plan_hash,
|
||||
"protectionOnlyPlanHash": protection_only_plan_hash,
|
||||
"protectionOnlyAvailable": protection_only_available,
|
||||
"compliant": not blockers and workflow_action == "none" and protection_action == "none",
|
||||
"_repository": repository,
|
||||
"_workflowContent": workflow_content,
|
||||
"_workflowSha": workflow_sha,
|
||||
"_currentProtection": current,
|
||||
"_protectionOnlyAvailable": protection_only_available,
|
||||
}
|
||||
|
||||
@governed_tool("gitea.branchprotection.get")
|
||||
@@ -946,17 +1241,103 @@ class GiteaConnector:
|
||||
|
||||
@governed_tool("gitea.branchprotection.ensure")
|
||||
def branch_protection_ensure(
|
||||
self, context: ConnectorContext, repository_ref: str, expected_plan_hash: str
|
||||
self,
|
||||
context: ConnectorContext,
|
||||
repository_ref: str,
|
||||
expected_plan_hash: str,
|
||||
apply_scope: Literal["all", "protection_only"] = "all",
|
||||
) -> dict[str, Any]:
|
||||
if apply_scope not in {"all", "protection_only"}:
|
||||
raise GiteaError("GITEA_INPUT_INVALID", "governance apply scope is not allowlisted")
|
||||
plan = self._governance_plan_data(context, repository_ref)
|
||||
expected_hash = (
|
||||
plan["planHash"]
|
||||
if apply_scope == "all"
|
||||
else plan["protectionOnlyPlanHash"]
|
||||
)
|
||||
if (
|
||||
not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash)
|
||||
or expected_plan_hash != plan["planHash"]
|
||||
not isinstance(expected_hash, str)
|
||||
or not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash)
|
||||
or expected_plan_hash != expected_hash
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_PLAN_STALE",
|
||||
"governance state changed; preview the repository again before applying",
|
||||
)
|
||||
|
||||
if apply_scope == "protection_only":
|
||||
allowed_blockers = {"managed-validation-projectbrain-profile-invalid"}
|
||||
if (
|
||||
not plan["_protectionOnlyAvailable"]
|
||||
or any(blocker not in allowed_blockers for blocker in plan["blockers"])
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
"ProjectBrain protection-only apply requires an existing "
|
||||
"enabled status-check rule",
|
||||
)
|
||||
current_protection = plan["_currentProtection"]
|
||||
if current_protection is None:
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
"ProjectBrain protection-only apply requires an existing "
|
||||
"enabled status-check rule",
|
||||
)
|
||||
current_contexts = current_protection.get("status_check_contexts")
|
||||
expected_contexts = list(plan["branchProtection"]["desired"]["status_check_contexts"])
|
||||
protection_changed = current_contexts != expected_contexts
|
||||
if not protection_changed:
|
||||
return self._envelope(
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": False,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": False,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
self._writer().update_branch_protection(
|
||||
plan["_repository"],
|
||||
str(plan["defaultBranch"]),
|
||||
{"status_check_contexts": expected_contexts},
|
||||
)
|
||||
verified_plan = self._governance_plan_data(context, repository_ref)
|
||||
verified_protection = verified_plan["branchProtection"]["current"]
|
||||
expected_effective_protection = _branch_protection_effective_projection(
|
||||
current_protection
|
||||
)
|
||||
actual_effective_protection = _branch_protection_effective_projection(
|
||||
verified_plan["_currentProtection"]
|
||||
)
|
||||
actual_contexts = set(
|
||||
verified_protection.get("status_check_contexts") or []
|
||||
) if verified_protection is not None else set()
|
||||
if (
|
||||
verified_protection is None
|
||||
or verified_protection.get("enable_status_check") is not True
|
||||
or actual_contexts != set(expected_contexts)
|
||||
or actual_effective_protection != expected_effective_protection
|
||||
or verified_plan["workflow"]["currentDigest"]
|
||||
!= plan["workflow"]["currentDigest"]
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_VERIFY_FAILED",
|
||||
"Gitea did not retain the requested ProjectBrain protection-only contract",
|
||||
)
|
||||
return self._envelope(
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(verified_plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": True,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": False,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
|
||||
if plan["blockers"]:
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
@@ -965,7 +1346,12 @@ class GiteaConnector:
|
||||
if plan["compliant"]:
|
||||
return self._envelope(
|
||||
context,
|
||||
{**self._public_governance_plan(plan), "changed": False, "verified": True},
|
||||
{
|
||||
**self._public_governance_plan(plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": False,
|
||||
"verified": True,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
|
||||
@@ -992,6 +1378,11 @@ class GiteaConnector:
|
||||
if protection_action == "create":
|
||||
writer.create_branch_protection(repository, desired)
|
||||
elif protection_action == "update":
|
||||
if plan["workflow"]["profile"] == "projectbrain":
|
||||
edit_payload = {
|
||||
"status_check_contexts": desired["status_check_contexts"]
|
||||
}
|
||||
else:
|
||||
edit_payload = {
|
||||
key: value
|
||||
for key, value in desired.items()
|
||||
@@ -1027,7 +1418,36 @@ class GiteaConnector:
|
||||
raise
|
||||
|
||||
verified_plan = self._governance_plan_data(context, repository_ref)
|
||||
if not verified_plan["compliant"]:
|
||||
is_projectbrain_profile = plan["workflow"]["profile"] == "projectbrain"
|
||||
verified_contexts = set(
|
||||
(verified_plan["branchProtection"]["current"] or {}).get(
|
||||
"status_check_contexts"
|
||||
)
|
||||
or []
|
||||
)
|
||||
expected_contexts = set(
|
||||
plan["branchProtection"]["desired"].get("status_check_contexts") or []
|
||||
)
|
||||
projectbrain_protection_changed = is_projectbrain_profile and (
|
||||
verified_contexts != expected_contexts
|
||||
or (
|
||||
plan["_currentProtection"] is not None
|
||||
and _branch_protection_effective_projection(
|
||||
verified_plan["_currentProtection"]
|
||||
)
|
||||
!= _branch_protection_effective_projection(plan["_currentProtection"])
|
||||
)
|
||||
)
|
||||
projectbrain_workflow_changed = (
|
||||
is_projectbrain_profile
|
||||
and verified_plan["workflow"]["currentDigest"]
|
||||
!= plan["workflow"]["currentDigest"]
|
||||
)
|
||||
if (
|
||||
not verified_plan["compliant"]
|
||||
or projectbrain_protection_changed
|
||||
or projectbrain_workflow_changed
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_VERIFY_FAILED",
|
||||
"Gitea did not retain the requested governance contract",
|
||||
@@ -1036,6 +1456,7 @@ class GiteaConnector:
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(verified_plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": True,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": temporary_push_allowlist_used,
|
||||
@@ -1100,8 +1521,12 @@ class GiteaConnector:
|
||||
for item in plans:
|
||||
repository_ref = str(item.get("repositoryRef", ""))
|
||||
try:
|
||||
apply_scope = str(item.get("applyScope", "all"))
|
||||
result = self.branch_protection_ensure(
|
||||
context, repository_ref, str(item.get("planHash", ""))
|
||||
context,
|
||||
repository_ref,
|
||||
str(item.get("planHash", "")),
|
||||
apply_scope=apply_scope, # type: ignore[arg-type]
|
||||
)["data"]
|
||||
items.append(
|
||||
{
|
||||
@@ -1109,6 +1534,7 @@ class GiteaConnector:
|
||||
"applied": True,
|
||||
"changed": bool(result.get("changed")),
|
||||
"verified": bool(result.get("verified")),
|
||||
"applyScope": str(result.get("applyScope", "all")),
|
||||
}
|
||||
)
|
||||
except GiteaError as exc:
|
||||
@@ -4519,13 +4945,13 @@ GITEA_READ_TOOLS = tuple(
|
||||
"gitea.change.status",
|
||||
"gitea.change.review",
|
||||
"gitea.workspace.status",
|
||||
"gitea.branchprotection.preview",
|
||||
"gitea.portfolio.governance.preview",
|
||||
"gitea.portfolio.governance.verify",
|
||||
}
|
||||
else (
|
||||
"1.8.0"
|
||||
if name.startswith("gitea.branchprotection.")
|
||||
or name.startswith("gitea.portfolio.governance.")
|
||||
else "1.5.0"
|
||||
),
|
||||
else "1.8.0"
|
||||
if name == "gitea.branchprotection.get"
|
||||
else "1.5.0",
|
||||
connector_type="gitea",
|
||||
risk_class="R1",
|
||||
input_schema=_tool_input_schema(name),
|
||||
@@ -4620,6 +5046,11 @@ def _write_tool_input_schema(name: str) -> dict[str, Any]:
|
||||
}
|
||||
if name == "gitea.branchprotection.ensure":
|
||||
properties["expectedPlanHash"] = {"type": "string", "pattern": "^[0-9a-f]{64}$"}
|
||||
properties["applyScope"] = {
|
||||
"type": "string",
|
||||
"enum": ["all", "protection_only"],
|
||||
"default": "all",
|
||||
}
|
||||
required.append("expectedPlanHash")
|
||||
elif name == "gitea.portfolio.governance.apply":
|
||||
return {
|
||||
@@ -4639,7 +5070,19 @@ def _write_tool_input_schema(name: str) -> dict[str, Any]:
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]{0,127}$",
|
||||
},
|
||||
"planHash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"planHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$",
|
||||
"description": (
|
||||
"Use planHash for all scope or protectionOnlyPlanHash "
|
||||
"for protection_only scope."
|
||||
),
|
||||
},
|
||||
"applyScope": {
|
||||
"type": "string",
|
||||
"enum": ["all", "protection_only"],
|
||||
"default": "all",
|
||||
},
|
||||
},
|
||||
"required": ["repositoryRef", "planHash"],
|
||||
},
|
||||
@@ -4827,13 +5270,10 @@ GITEA_PR_WRITE_TOOLS = tuple(
|
||||
"gitea.pullrequest.update",
|
||||
"gitea.changeset.apply",
|
||||
}
|
||||
else "1.9.0"
|
||||
if name in {"gitea.branchprotection.ensure", "gitea.portfolio.governance.apply"}
|
||||
else "1.8.0"
|
||||
if name
|
||||
in {
|
||||
"gitea.repository.create",
|
||||
"gitea.branchprotection.ensure",
|
||||
"gitea.portfolio.governance.apply",
|
||||
}
|
||||
if name == "gitea.repository.create"
|
||||
else "1.5.0"
|
||||
),
|
||||
connector_type="gitea",
|
||||
|
||||
@@ -9,6 +9,49 @@ from uuid import UUID, uuid4
|
||||
|
||||
import httpx
|
||||
|
||||
APPOPS_INPUT_SCHEMAS: dict[str, dict[str, Any]] = {
|
||||
"server.appops.deployment.status": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"server.appops.self_upgrade.preflight": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"repository": {"type": "string", "const": "jens/unraid-appops-gateway"},
|
||||
"revision": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
"reason": {"type": "string", "minLength": 8, "maxLength": 500},
|
||||
"expected_current_revision": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
},
|
||||
"required": ["repository", "revision", "reason"],
|
||||
},
|
||||
"server.appops.self_upgrade.apply": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"repository": {"type": "string", "const": "jens/unraid-appops-gateway"},
|
||||
"revision": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
"reason": {"type": "string", "minLength": 8, "maxLength": 500},
|
||||
"expected_preflight_fingerprint": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"idempotency_key": {"type": "string", "pattern": "^[A-Za-z0-9._:-]{8,128}$"},
|
||||
"expected_current_revision": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
},
|
||||
"required": [
|
||||
"repository",
|
||||
"revision",
|
||||
"reason",
|
||||
"expected_preflight_fingerprint",
|
||||
"idempotency_key",
|
||||
],
|
||||
},
|
||||
"server.appops.self_upgrade.status": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {"operation_id": {"type": "string", "pattern": "^[0-9a-f]{64}$"}},
|
||||
"required": ["operation_id"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FakeContextForge:
|
||||
"""Fake REST gateway for offline adapter and reconciliation tests."""
|
||||
@@ -201,6 +244,10 @@ class FakeContextForge:
|
||||
"server.deployment.status",
|
||||
"server.deployment.verify",
|
||||
"server.deployment.rollback",
|
||||
"server.appops.deployment.status",
|
||||
"server.appops.self_upgrade.preflight",
|
||||
"server.appops.self_upgrade.apply",
|
||||
"server.appops.self_upgrade.status",
|
||||
),
|
||||
"itworx-opencode": (
|
||||
"opencode.capabilities.get",
|
||||
@@ -251,7 +298,9 @@ class FakeContextForge:
|
||||
@staticmethod
|
||||
def _tool_payload(tool_name: str) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"id": str(uuid4()), "name": tool_name}
|
||||
if tool_name == "gitea.repository.create":
|
||||
if tool_name in APPOPS_INPUT_SCHEMAS:
|
||||
payload["inputSchema"] = deepcopy(APPOPS_INPUT_SCHEMAS[tool_name])
|
||||
elif tool_name == "gitea.repository.create":
|
||||
payload["inputSchema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -45,6 +45,10 @@ UPSTREAM_TO_PUBLIC = {
|
||||
"deployment.status": "server.deployment.status",
|
||||
"deployment.verify": "server.deployment.verify",
|
||||
"deployment.rollback": "server.deployment.rollback",
|
||||
"appops.deployment.status": "server.appops.deployment.status",
|
||||
"appops.self_upgrade.preflight": "server.appops.self_upgrade.preflight",
|
||||
"appops.self_upgrade.apply": "server.appops.self_upgrade.apply",
|
||||
"appops.self_upgrade.status": "server.appops.self_upgrade.status",
|
||||
}
|
||||
PUBLIC_TO_UPSTREAM = {value: key for key, value in UPSTREAM_TO_PUBLIC.items()}
|
||||
MAX_BODY_BYTES = 1024 * 1024
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal, ParamSpec, TypeVar, cast
|
||||
from typing import Annotated, Any, Literal, ParamSpec, TypeVar, cast
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from mcp.server.mcpserver.context import Context
|
||||
@@ -87,6 +87,14 @@ class ResearchFetchOutput(BaseModel):
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class PortfolioGovernanceApplyPlan(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
repositoryRef: str
|
||||
planHash: str = Field(pattern=r"^[0-9a-f]{64}$")
|
||||
applyScope: Literal["all", "protection_only"] = "all"
|
||||
|
||||
|
||||
def _repository_ref(name: str) -> str:
|
||||
value = re.sub(r"[^a-z0-9]+", "-", name.casefold()).strip("-")
|
||||
if not value:
|
||||
@@ -655,9 +663,12 @@ def build_server() -> MCPServer[Any]:
|
||||
repositoryRef: str,
|
||||
expectedPlanHash: str,
|
||||
context: Context[Any, Any],
|
||||
applyScope: Literal["all", "protection_only"] = "all",
|
||||
) -> dict[str, Any]:
|
||||
"""Apply one unchanged governance preview and verify the stored result."""
|
||||
return connector.branch_protection_ensure(current(context), repositoryRef, expectedPlanHash)
|
||||
"""Apply one unchanged governance preview with its explicitly hashed scope."""
|
||||
return connector.branch_protection_ensure(
|
||||
current(context), repositoryRef, expectedPlanHash, apply_scope=applyScope
|
||||
)
|
||||
|
||||
@write_tool(
|
||||
name="gitea.portfolio.governance.apply",
|
||||
@@ -665,10 +676,15 @@ def build_server() -> MCPServer[Any]:
|
||||
structured_output=True,
|
||||
)
|
||||
async def portfolio_governance_apply(
|
||||
plans: list[dict[str, str]], context: Context[Any, Any]
|
||||
context: Context[Any, Any],
|
||||
plans: Annotated[
|
||||
list[PortfolioGovernanceApplyPlan], Field(min_length=1, max_length=5)
|
||||
],
|
||||
) -> dict[str, Any]:
|
||||
"""Apply at most five exact governance plans; every item is independently verified."""
|
||||
return connector.portfolio_governance_apply(current(context), plans)
|
||||
return connector.portfolio_governance_apply(
|
||||
current(context), [plan.model_dump() for plan in plans]
|
||||
)
|
||||
|
||||
@write_tool(name="gitea.branch.create", annotations=SAFE_WRITE_TOOL, structured_output=True)
|
||||
async def branch_create(
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
name: Managed validation
|
||||
run-name: "Managed validation / ${{ inputs.profile || 'full' }}"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
description: Allowlisted validation profile
|
||||
required: true
|
||||
default: full
|
||||
type: choice
|
||||
options: [test, lint, typecheck, build, security, full]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
full:
|
||||
name: full
|
||||
runs-on: linux-validation
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CI: "true"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: projectbrain_integration
|
||||
POSTGRES_USER: projectbrain
|
||||
POSTGRES_PASSWORD: projectbrain-test-password
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U projectbrain -d projectbrain_integration"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Validate repository with a bounded profile
|
||||
shell: bash
|
||||
env:
|
||||
REQUESTED_PROFILE: ${{ inputs.profile }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
profile="${REQUESTED_PROFILE:-full}"
|
||||
case "${profile}" in
|
||||
test|lint|typecheck|build|security|full) ;;
|
||||
*) echo "Profile is not allowlisted" >&2; exit 2 ;;
|
||||
esac
|
||||
if [[ "${profile}" == security && ( ! -f package.json || ! -f pnpm-lock.yaml ) ]]; then
|
||||
echo "Security profile requires the repository's pinned pnpm audit:security command" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
git diff --check
|
||||
if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then
|
||||
echo "Unresolved merge markers detected" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# MANAGED_FAST_PATH: only documentation may skip product tests.
|
||||
# Include deletions and renames; workflow and runner changes must
|
||||
# always execute validation.
|
||||
if [[ -n "${GITHUB_BASE_REF:-}" ]]; then
|
||||
git fetch --no-tags origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}"
|
||||
managed_base="origin/${GITHUB_BASE_REF}"
|
||||
git diff --check "${managed_base}..HEAD"
|
||||
mapfile -t managed_changed_files < <(
|
||||
git diff --name-only --no-renames "${managed_base}..HEAD"
|
||||
)
|
||||
managed_runtime_change=0
|
||||
for managed_path in "${managed_changed_files[@]}"; do
|
||||
case "${managed_path}" in
|
||||
*.md|*.mdx)
|
||||
;;
|
||||
*)
|
||||
managed_runtime_change=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ "${#managed_changed_files[@]}" -gt 0 && "${managed_runtime_change}" -eq 0 ]]; then
|
||||
printf 'Managed validation fast path: %s non-runtime file(s); full product suite skipped.\n' \
|
||||
"${#managed_changed_files[@]}"
|
||||
if [[ "${profile}" == full ]]; then
|
||||
node scripts/check-doc-links.mjs
|
||||
node scripts/check-unraid-template-readiness.mjs
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f pyproject.toml || -f requirements.txt ]]; then
|
||||
# Compile only tracked Python sources. Running compileall after a
|
||||
# Node install would otherwise traverse node_modules and turn a
|
||||
# lightweight baseline into a large runner workload.
|
||||
git ls-files -z '*.py' | xargs -0 -r python -m py_compile
|
||||
if [[ -f uv.lock ]]; then
|
||||
python -m venv "${RUNNER_TEMP}/managed-uv"
|
||||
uv_python="${RUNNER_TEMP}/managed-uv/bin/python"
|
||||
"${uv_python}" -m pip install --disable-pip-version-check uv==0.10.0
|
||||
managed_uv="${RUNNER_TEMP}/managed-uv/bin/uv"
|
||||
export UV_PROJECT_ENVIRONMENT="${RUNNER_TEMP}/managed-project-venv"
|
||||
"${managed_uv}" sync --locked
|
||||
export PATH="${UV_PROJECT_ENVIRONMENT}/bin:${PATH}"
|
||||
if [[ "${profile}" == test || "${profile}" == full ]]; then
|
||||
if "${managed_uv}" run python -c 'import pytest' 2>/dev/null; then
|
||||
"${managed_uv}" run python -m pytest
|
||||
fi
|
||||
fi
|
||||
if [[ "${profile}" == lint || "${profile}" == full ]]; then
|
||||
if "${managed_uv}" run python -c 'import ruff' 2>/dev/null; then
|
||||
"${managed_uv}" run python -m ruff check .
|
||||
fi
|
||||
fi
|
||||
elif [[ -f requirements.txt ]]; then
|
||||
python -m venv "${RUNNER_TEMP}/managed-python"
|
||||
managed_python="${RUNNER_TEMP}/managed-python/bin/python"
|
||||
"${managed_python}" -m pip install --disable-pip-version-check -r requirements.txt
|
||||
export PATH="${RUNNER_TEMP}/managed-python/bin:${PATH}"
|
||||
if [[ "${profile}" == test || "${profile}" == full ]]; then
|
||||
if "${managed_python}" -c 'import pytest' 2>/dev/null; then
|
||||
"${managed_python}" -m pytest
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keep ordinary unit/contract validation database-independent. The
|
||||
# real PostgreSQL suite is enabled only after migrations are applied
|
||||
# in the deployment-authoritative full profile below.
|
||||
if [[ -f package.json ]]; then
|
||||
corepack enable
|
||||
if [[ -f pnpm-lock.yaml ]]; then
|
||||
pnpm install --frozen-lockfile
|
||||
if [[ "${profile}" == test ]]; then
|
||||
pnpm --if-present test
|
||||
[[ -f backend/package.json ]] && pnpm --filter projectbrain-api run test
|
||||
elif [[ "${profile}" == full ]]; then
|
||||
pnpm run test:coverage
|
||||
[[ -f backend/package.json ]] && pnpm --filter projectbrain-api run test:coverage
|
||||
fi
|
||||
if [[ "${profile}" == security ]]; then
|
||||
pnpm run audit:security
|
||||
fi
|
||||
if [[ "${profile}" == lint || "${profile}" == full ]]; then
|
||||
pnpm --if-present lint
|
||||
[[ -f backend/package.json ]] && pnpm --filter projectbrain-api run lint
|
||||
fi
|
||||
if [[ "${profile}" == typecheck || "${profile}" == full ]]; then
|
||||
pnpm --if-present typecheck
|
||||
[[ -f backend/package.json ]] && pnpm --filter projectbrain-api run typecheck
|
||||
fi
|
||||
if [[ "${profile}" == build || "${profile}" == full ]]; then
|
||||
pnpm --if-present build
|
||||
[[ -f backend/package.json ]] && pnpm --filter projectbrain-api run build
|
||||
fi
|
||||
if [[ "${profile}" == full ]]; then
|
||||
pnpm run audit:openapi
|
||||
pnpm run audit:architecture
|
||||
pnpm run audit:docs
|
||||
pnpm run audit:v1-freeze
|
||||
pnpm run audit:unraid-template
|
||||
fi
|
||||
if [[ "${profile}" == full && -f backend/package.json ]]; then
|
||||
export DATABASE_URL="postgresql://projectbrain:projectbrain-test-password@postgres:5432/projectbrain_integration?schema=public"
|
||||
export TEST_DATABASE_URL="${DATABASE_URL}"
|
||||
pnpm --dir backend prisma:generate
|
||||
pnpm --dir backend db:deploy
|
||||
pnpm --dir backend db:seed
|
||||
pnpm --dir backend test:database
|
||||
pnpm --dir backend exec prisma migrate reset --schema prisma/schema.prisma --force --skip-seed
|
||||
pnpm --dir backend db:seed
|
||||
fi
|
||||
if [[ "${profile}" == full && -f playwright.config.ts ]]; then
|
||||
pnpm exec playwright install --with-deps --force chromium chromium-headless-shell
|
||||
pnpm test:e2e
|
||||
pnpm test:e2e:real
|
||||
fi
|
||||
elif [[ -f package-lock.json ]]; then
|
||||
npm ci
|
||||
[[ "${profile}" == test || "${profile}" == full ]] && npm run --if-present test
|
||||
[[ "${profile}" == lint || "${profile}" == full ]] && npm run --if-present lint
|
||||
if [[ "${profile}" == typecheck || "${profile}" == full ]]; then
|
||||
npm run --if-present typecheck
|
||||
fi
|
||||
[[ "${profile}" == build || "${profile}" == full ]] && npm run --if-present build
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f go.mod ]]; then
|
||||
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||
go test ./...
|
||||
fi
|
||||
fi
|
||||
if [[ -f Cargo.toml ]]; then
|
||||
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||
cargo test --locked
|
||||
fi
|
||||
fi
|
||||
if compgen -G '*.sln' >/dev/null; then
|
||||
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||
dotnet test --configuration Release
|
||||
fi
|
||||
fi
|
||||
@@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_appops_tool_catalog_is_bijective_and_server_namespaced() -> None:
|
||||
assert len(UPSTREAM_TO_PUBLIC) == len(PUBLIC_TO_UPSTREAM) == 25
|
||||
assert len(UPSTREAM_TO_PUBLIC) == len(PUBLIC_TO_UPSTREAM) == 29
|
||||
assert all(name.startswith("server.") for name in PUBLIC_TO_UPSTREAM)
|
||||
assert PUBLIC_TO_UPSTREAM["server.summary.get"] == "get_system_summary"
|
||||
assert PUBLIC_TO_UPSTREAM["server.operation.execute"] == "server.operation.execute"
|
||||
@@ -28,6 +28,19 @@ def test_appops_tool_catalog_is_bijective_and_server_namespaced() -> None:
|
||||
assert PUBLIC_TO_UPSTREAM["server.deployment.rollback"] == "deployment.rollback"
|
||||
assert PUBLIC_TO_UPSTREAM["server.deployment.target.enroll"] == "target.enroll"
|
||||
assert PUBLIC_TO_UPSTREAM["server.ci.runners.status"] == "gitea.actions.runners.status"
|
||||
assert PUBLIC_TO_UPSTREAM["server.appops.deployment.status"] == "appops.deployment.status"
|
||||
assert (
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.preflight"]
|
||||
== "appops.self_upgrade.preflight"
|
||||
)
|
||||
assert (
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.apply"]
|
||||
== "appops.self_upgrade.apply"
|
||||
)
|
||||
assert (
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.status"]
|
||||
== "appops.self_upgrade.status"
|
||||
)
|
||||
|
||||
|
||||
def test_every_public_appops_tool_has_governance_metadata() -> None:
|
||||
@@ -42,6 +55,50 @@ def test_every_public_appops_tool_has_governance_metadata() -> None:
|
||||
assert set(PUBLIC_TO_UPSTREAM) <= canonical_names
|
||||
|
||||
|
||||
def test_appops_self_upgrade_manifests_are_fixed_target_and_fingerprint_bound() -> None:
|
||||
specs = {}
|
||||
for path in (ROOT / "catalog" / "tools").glob("*.yaml"):
|
||||
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
spec = document.get("spec") if isinstance(document, dict) else None
|
||||
if isinstance(spec, dict) and spec.get("canonicalName", "").startswith(
|
||||
"server.appops."
|
||||
):
|
||||
specs[spec["canonicalName"]] = spec
|
||||
|
||||
assert set(specs) == {
|
||||
"server.appops.deployment.status",
|
||||
"server.appops.self_upgrade.preflight",
|
||||
"server.appops.self_upgrade.apply",
|
||||
"server.appops.self_upgrade.status",
|
||||
}
|
||||
for name in (
|
||||
"server.appops.self_upgrade.preflight",
|
||||
"server.appops.self_upgrade.apply",
|
||||
):
|
||||
schema = specs[name]["inputSchema"]
|
||||
assert schema["properties"]["repository"]["const"] == "jens/unraid-appops-gateway"
|
||||
assert schema["properties"]["revision"]["pattern"] == "^[0-9a-f]{40}$"
|
||||
assert schema["properties"]["reason"]["minLength"] == 8
|
||||
assert schema["properties"]["reason"]["maxLength"] == 500
|
||||
assert schema["properties"]["expected_current_revision"]["pattern"] == "^[0-9a-f]{40}$"
|
||||
apply = specs["server.appops.self_upgrade.apply"]
|
||||
assert apply["riskClass"] == "R3"
|
||||
assert apply["readOnly"] is False
|
||||
assert apply["inputSchema"]["properties"]["expected_preflight_fingerprint"][
|
||||
"pattern"
|
||||
] == "^[0-9a-f]{64}$"
|
||||
assert {
|
||||
"expected_preflight_fingerprint",
|
||||
"idempotency_key",
|
||||
} <= set(apply["inputSchema"]["required"])
|
||||
policy = yaml.safe_load(
|
||||
(ROOT / "catalog" / "policies" / "041-appops-governed-operations.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
assert "server.appops.self_upgrade.apply" in policy["spec"]["match"]["toolPatterns"]
|
||||
|
||||
|
||||
def test_appops_proxy_rewrites_only_registered_tool_calls() -> None:
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -73,6 +130,35 @@ def test_appops_proxy_rewrites_only_registered_tool_calls() -> None:
|
||||
with pytest.raises(ValueError, match="not registered"):
|
||||
rewrite_request({"method": "tools/call", "params": {"name": "shell.execute"}})
|
||||
|
||||
upgrade_request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 10,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "server.appops.self_upgrade.apply",
|
||||
"arguments": {
|
||||
"repository": "jens/unraid-appops-gateway",
|
||||
"revision": "a" * 40,
|
||||
"reason": "approved release",
|
||||
"expected_preflight_fingerprint": "b" * 64,
|
||||
"idempotency_key": "upgrade-2026-09-30",
|
||||
"expected_current_revision": "c" * 40,
|
||||
},
|
||||
},
|
||||
}
|
||||
upgrade_rewritten = rewrite_request(upgrade_request)
|
||||
assert upgrade_rewritten["params"]["name"] == "appops.self_upgrade.apply"
|
||||
assert upgrade_rewritten["params"]["arguments"] == upgrade_request["params"]["arguments"]
|
||||
|
||||
for upstream_name in (
|
||||
"appops.deployment.status",
|
||||
"appops.self_upgrade.preflight",
|
||||
"appops.self_upgrade.apply",
|
||||
"appops.self_upgrade.status",
|
||||
):
|
||||
with pytest.raises(ValueError, match="not registered"):
|
||||
rewrite_request({"method": "tools/call", "params": {"name": upstream_name}})
|
||||
|
||||
|
||||
def test_appops_proxy_rewrites_discovery_and_drops_unknown_tools() -> None:
|
||||
response = {
|
||||
@@ -84,6 +170,10 @@ def test_appops_proxy_rewrites_discovery_and_drops_unknown_tools() -> None:
|
||||
{"name": "gitea.projects.catalog", "description": "projects"},
|
||||
{"name": "deployment.preflight", "description": "deployment preflight"},
|
||||
{"name": "deployment.apply", "description": "typed deployment"},
|
||||
{"name": "appops.deployment.status", "description": "AppOps revision"},
|
||||
{"name": "appops.self_upgrade.preflight", "description": "upgrade preflight"},
|
||||
{"name": "appops.self_upgrade.apply", "description": "upgrade apply"},
|
||||
{"name": "appops.self_upgrade.status", "description": "upgrade status"},
|
||||
{"name": "future_unsafe_tool", "description": "unknown"},
|
||||
]
|
||||
},
|
||||
@@ -93,6 +183,10 @@ def test_appops_proxy_rewrites_discovery_and_drops_unknown_tools() -> None:
|
||||
{"name": "server.project.list", "description": "projects"},
|
||||
{"name": "server.deployment.preflight", "description": "deployment preflight"},
|
||||
{"name": "server.deployment.apply", "description": "typed deployment"},
|
||||
{"name": "server.appops.deployment.status", "description": "AppOps revision"},
|
||||
{"name": "server.appops.self_upgrade.preflight", "description": "upgrade preflight"},
|
||||
{"name": "server.appops.self_upgrade.apply", "description": "upgrade apply"},
|
||||
{"name": "server.appops.self_upgrade.status", "description": "upgrade status"},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class CatalogLoaderTests(unittest.TestCase):
|
||||
def test_committed_catalog_validates_without_network_or_secret_values(self) -> None:
|
||||
report = CatalogLoader(ROOT / "catalog").load()
|
||||
self.assertTrue(report.valid, report.issues)
|
||||
self.assertEqual(len(report.documents), 277)
|
||||
self.assertEqual(len(report.documents), 281)
|
||||
self.assertEqual(len(report.issues), 0)
|
||||
self.assertIsNotNone(report.desired_hash)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from connectors.gitea.changesets import (
|
||||
validate_operations,
|
||||
)
|
||||
from connectors.gitea.managed_validation import MANAGED_VALIDATION_WORKFLOW
|
||||
from connectors.gitea.server import _select_validation_workflow
|
||||
from connectors.gitea.server import _managed_validation_profile, _select_validation_workflow
|
||||
from connectors.knowledge import KnowledgeConnector
|
||||
from packages.connector_kit import (
|
||||
ConnectorContext,
|
||||
@@ -43,6 +43,49 @@ from services.control_api.gitea.registry import registration_from_project
|
||||
PROJECT_ID = UUID("11111111-1111-1111-1111-111111111111")
|
||||
OTHER_PROJECT_ID = UUID("22222222-2222-2222-2222-222222222222")
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW = (
|
||||
ROOT / "tests" / "fixtures" / "projectbrain-managed-validation.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def projectbrain_connector(
|
||||
server: FakeGiteaServer | None = None,
|
||||
) -> tuple[GiteaConnector, FakeGiteaServer]:
|
||||
fixture = server or FakeGiteaServer()
|
||||
fixture._repos["Jens/projectbrain"] = {
|
||||
"owner": {"login": "Jens"},
|
||||
"name": "projectbrain",
|
||||
"full_name": "Jens/projectbrain",
|
||||
"description": "ProjectBrain",
|
||||
"private": True,
|
||||
"default_branch": "main",
|
||||
}
|
||||
fixture._managed_workflows["Jens/projectbrain"] = PROJECTBRAIN_VALIDATION_WORKFLOW
|
||||
protection = copy.deepcopy(fixture._branch_protections["acme/hub"][0])
|
||||
protection["status_check_contexts"] = ["Pull request / quality (pull_request)"]
|
||||
protection["required_approvals"] = 2
|
||||
fixture._branch_protections["Jens/projectbrain"] = [protection]
|
||||
settings = GiteaSettings(
|
||||
base_url="http://gitea.test",
|
||||
token_reference="docker-secret://gitea_mcp_token", # noqa: S106
|
||||
allow_insecure_http_for_tests=True,
|
||||
)
|
||||
client = GiteaClient(
|
||||
settings, StaticTokenProvider("fixture-token-must-not-leak"), transport=fixture.transport()
|
||||
)
|
||||
registry = GiteaRepositoryRegistry(
|
||||
(
|
||||
RegisteredRepository(
|
||||
repository_ref="projectbrain-repository",
|
||||
project_id=PROJECT_ID,
|
||||
owner="Jens",
|
||||
name="projectbrain",
|
||||
source_uri="itworx-git://projectbrain/projectbrain-repository",
|
||||
private=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
return GiteaConnector(client, registry, write_client=client), fixture
|
||||
|
||||
|
||||
def context(project_id: UUID = PROJECT_ID, slug: str = "hub") -> ConnectorContext:
|
||||
@@ -114,6 +157,262 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
self.assertIs(_select_validation_workflow([legacy, managed]), managed)
|
||||
self.assertIs(_select_validation_workflow([managed, legacy]), managed)
|
||||
|
||||
def test_projectbrain_managed_validation_profile_is_pinned_and_repository_scoped(self) -> None:
|
||||
self.assertEqual(
|
||||
_managed_validation_profile("Jens/projectbrain", PROJECTBRAIN_VALIDATION_WORKFLOW),
|
||||
"projectbrain",
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace("pnpm test:e2e:real", ""),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"pnpm test:e2e:real", "# pnpm test:e2e:real"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"pnpm test:e2e:real", 'echo "pnpm test:e2e:real"'
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
'export TEST_DATABASE_URL="${DATABASE_URL}"',
|
||||
'# export TEST_DATABASE_URL="${DATABASE_URL}"',
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" image: postgres:16", " image: postgres:15"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(" pull_request:\n", ""),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", ' pull_request:\n paths: ["backend/**"]\n'
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", " pull_request:\n types: [edited]\n"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace("branches: [main]", "branches: [master]"),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"branches: [main]", "branches: [main, '!main']"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n",
|
||||
" pull_request:\n branches: [main, '!main']\n",
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" name: full\n", " name: full\n if: false\n"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" name: full\n", " name: full\n needs: [conditional-setup]\n"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" - name: Validate repository with a bounded profile\n",
|
||||
" - name: Validate repository with a bounded profile\n"
|
||||
" if: false\n",
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" name: full\n", " name: full\n continue-on-error: true\n"
|
||||
),
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" - name: Validate repository with a bounded profile\n",
|
||||
" - name: Validate repository with a bounded profile\n"
|
||||
" continue-on-error: true\n",
|
||||
),
|
||||
)
|
||||
)
|
||||
bypass_mutations = (
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm test:e2e:real\n",
|
||||
" cat <<'EOF'\n"
|
||||
" pnpm test:e2e:real\n"
|
||||
" EOF\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm --dir backend test:database\n",
|
||||
" exit 0\n"
|
||||
" pnpm --dir backend test:database\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm test:e2e:real\n",
|
||||
" if false; then\n"
|
||||
" pnpm test:e2e:real\n"
|
||||
" fi\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm test:e2e:real\n",
|
||||
" export PATH=/tmp/untrusted:$PATH\n"
|
||||
" pnpm test:e2e:real\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 "
|
||||
"# v4.2.2\n",
|
||||
" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 "
|
||||
"# v4.2.2\n"
|
||||
" - run: echo injected-action\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"run-name: \"Managed validation / ${{ inputs.profile || 'full' }}\"",
|
||||
'run-name: "Managed validation / full"',
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" default: full\n", " default: test\n"
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"permissions:\n", "env:\n PATH: /tmp/untrusted\n\npermissions:\n"
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"permissions:\n", "defaults:\n run:\n shell: sh\n\npermissions:\n"
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"jobs:\n", "jobs:\n extra:\n runs-on: linux-validation\n steps: []\n"
|
||||
),
|
||||
)
|
||||
for modified_workflow in bypass_mutations:
|
||||
with self.subTest(modified_workflow=modified_workflow[-160:]):
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile("Jens/projectbrain", modified_workflow)
|
||||
)
|
||||
|
||||
alias_chain = ["level0: &level0 [marker]"]
|
||||
for level in range(1, 23):
|
||||
alias_chain.append(
|
||||
f"level{level}: &level{level} [*level{level - 1}, *level{level - 1}]"
|
||||
)
|
||||
alias_bomb = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
"jobs:\n", "\n".join(alias_chain) + "\njobs:\n"
|
||||
)
|
||||
self.assertIsNone(_managed_validation_profile("Jens/projectbrain", alias_bomb))
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Jens/projectbrain", PROJECTBRAIN_VALIDATION_WORKFLOW + "\njobs: {}\n"
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile("Acme/hub", PROJECTBRAIN_VALIDATION_WORKFLOW)
|
||||
)
|
||||
self.assertEqual(
|
||||
_managed_validation_profile("Acme/hub", MANAGED_VALIDATION_WORKFLOW),
|
||||
"canonical",
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile(
|
||||
"Acme/hub", MANAGED_VALIDATION_WORKFLOW + "\n# custom project workflow\n"
|
||||
)
|
||||
)
|
||||
|
||||
def test_projectbrain_profile_pins_coverage_audits_security_and_doc_checks(self) -> None:
|
||||
required_commands = (
|
||||
("root coverage gate", "pnpm run test:coverage", "pnpm --if-present test"),
|
||||
(
|
||||
"backend coverage gate",
|
||||
"pnpm --filter projectbrain-api run test:coverage",
|
||||
"pnpm --filter projectbrain-api run test",
|
||||
),
|
||||
("security audit", "pnpm run audit:security", "pnpm --if-present audit:security"),
|
||||
("OpenAPI audit", "pnpm run audit:openapi", "echo skipped-openapi-audit"),
|
||||
(
|
||||
"architecture audit",
|
||||
"pnpm run audit:architecture",
|
||||
"echo skipped-architecture-audit",
|
||||
),
|
||||
("documentation audit", "pnpm run audit:docs", "echo skipped-docs-audit"),
|
||||
("v1 freeze audit", "pnpm run audit:v1-freeze", "echo skipped-v1-freeze-audit"),
|
||||
(
|
||||
"Unraid template audit",
|
||||
"pnpm run audit:unraid-template",
|
||||
"echo skipped-unraid-template-audit",
|
||||
),
|
||||
(
|
||||
"documentation fast-path check",
|
||||
"node scripts/check-doc-links.mjs",
|
||||
"echo skipped-doc-link-check",
|
||||
),
|
||||
(
|
||||
"Unraid documentation fast-path check",
|
||||
"node scripts/check-unraid-template-readiness.mjs",
|
||||
"echo skipped-unraid-readiness-check",
|
||||
),
|
||||
)
|
||||
for label, command, replacement in required_commands:
|
||||
with self.subTest(gate=label):
|
||||
self.assertIn(command, PROJECTBRAIN_VALIDATION_WORKFLOW)
|
||||
modified_workflow = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
command, replacement, 1
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile("Jens/projectbrain", modified_workflow)
|
||||
)
|
||||
|
||||
def test_native_skills_are_complete_digest_verified_and_bounded(self) -> None:
|
||||
from services.runtime.mcp_skills import SkillGetParams, SkillListParams, StaticSkills
|
||||
|
||||
@@ -653,6 +952,16 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
"main",
|
||||
)
|
||||
self.assertIn("expectedPlanHash", schemas["gitea.branchprotection.ensure"]["required"])
|
||||
self.assertEqual(
|
||||
schemas["gitea.branchprotection.ensure"]["properties"]["applyScope"]["enum"],
|
||||
["all", "protection_only"],
|
||||
)
|
||||
portfolio_plan_properties = schemas["gitea.portfolio.governance.apply"]["properties"][
|
||||
"plans"
|
||||
]["items"]["properties"]
|
||||
self.assertEqual(
|
||||
portfolio_plan_properties["applyScope"]["enum"], ["all", "protection_only"]
|
||||
)
|
||||
self.assertEqual(
|
||||
schemas["gitea.portfolio.governance.apply"]["properties"]["plans"]["maxItems"],
|
||||
5,
|
||||
@@ -755,6 +1064,24 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
self.assertEqual(server.version, "1.14.0")
|
||||
self.assertIn("plan-locked governance tools", server.instructions)
|
||||
names = {tool.name for tool in tools}
|
||||
ensure_tool = next(
|
||||
tool for tool in tools if tool.name == "gitea.branchprotection.ensure"
|
||||
)
|
||||
portfolio_apply_tool = next(
|
||||
tool for tool in tools if tool.name == "gitea.portfolio.governance.apply"
|
||||
)
|
||||
self.assertEqual(
|
||||
ensure_tool.input_schema["properties"]["applyScope"]["enum"],
|
||||
["all", "protection_only"],
|
||||
)
|
||||
portfolio_schema = portfolio_apply_tool.input_schema
|
||||
plan_ref = portfolio_schema["properties"]["plans"]["items"]["$ref"]
|
||||
plan_schema_name = plan_ref.rsplit("/", maxsplit=1)[-1]
|
||||
plan_schema = portfolio_schema["$defs"][plan_schema_name]
|
||||
self.assertEqual(
|
||||
plan_schema["properties"]["applyScope"]["enum"], ["all", "protection_only"]
|
||||
)
|
||||
self.assertEqual(portfolio_schema["properties"]["plans"]["maxItems"], 5)
|
||||
self.assertTrue(
|
||||
{
|
||||
"gitea.repository.create",
|
||||
@@ -1065,6 +1392,391 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_custom_workflow_is_preserved_while_full_and_quality_are_added(
|
||||
self,
|
||||
) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
original_workflow = fixture._managed_workflows["Jens/projectbrain"]
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)"
|
||||
]
|
||||
original_protection = copy.deepcopy(
|
||||
fixture._branch_protections["Jens/projectbrain"][0]
|
||||
)
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertEqual(preview["workflow"]["profile"], "projectbrain")
|
||||
self.assertEqual(preview["workflow"]["action"], "none")
|
||||
self.assertNotEqual(
|
||||
preview["workflow"]["currentDigest"],
|
||||
hashlib.sha256(MANAGED_VALIDATION_WORKFLOW.encode()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(
|
||||
preview["branchProtection"]["desired"]["status_check_contexts"],
|
||||
[
|
||||
"Managed validation / full (pull_request)",
|
||||
"Pull request / quality (pull_request)",
|
||||
],
|
||||
)
|
||||
self.assertEqual(preview["branchProtection"]["action"], "update")
|
||||
|
||||
applied = connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)["data"]
|
||||
|
||||
self.assertTrue(applied["changed"])
|
||||
self.assertTrue(applied["verified"])
|
||||
self.assertTrue(applied["compliant"])
|
||||
self.assertEqual(fixture._managed_workflows["Jens/projectbrain"], original_workflow)
|
||||
self.assertEqual(
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"],
|
||||
[
|
||||
"Managed validation / full (pull_request)",
|
||||
"Pull request / quality (pull_request)",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["required_approvals"],
|
||||
original_protection["required_approvals"],
|
||||
)
|
||||
workflow_writes = [
|
||||
(method, path)
|
||||
for method, path in zip(
|
||||
fixture.recorded_methods, fixture.recorded_paths, strict=True
|
||||
)
|
||||
if path.endswith("/.gitea/workflows/managed-validation.yml")
|
||||
and method in {"POST", "PUT", "DELETE"}
|
||||
]
|
||||
self.assertEqual(workflow_writes, [])
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_all_scope_fails_if_gitea_drops_an_extra_required_context(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)",
|
||||
"Additional required check",
|
||||
]
|
||||
original_read = connector_instance.client.branch_protections
|
||||
read_count = 0
|
||||
|
||||
def drop_extra_context(repository: RegisteredRepository) -> list[dict[str, object]]:
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 3:
|
||||
fixture._branch_protections["Jens/projectbrain"][0][
|
||||
"status_check_contexts"
|
||||
].remove("Additional required check")
|
||||
return original_read(repository)
|
||||
|
||||
try:
|
||||
current = context()
|
||||
with patch.object(
|
||||
connector_instance.client,
|
||||
"branch_protections",
|
||||
side_effect=drop_extra_context,
|
||||
):
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertIn(
|
||||
"Additional required check",
|
||||
preview["branchProtection"]["desired"]["status_check_contexts"],
|
||||
)
|
||||
with self.assertRaisesRegex(GiteaError, "did not retain"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)
|
||||
self.assertNotIn(
|
||||
"Additional required check",
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"],
|
||||
)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_all_scope_rejects_an_accepted_workflow_digest_change(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)"
|
||||
]
|
||||
modified_workflow = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", " pull_request:\n branches: [main]\n"
|
||||
)
|
||||
original_update = connector_instance.client.update_branch_protection
|
||||
|
||||
def update_protection_then_change_workflow(
|
||||
repository: RegisteredRepository,
|
||||
branch: str,
|
||||
payload: dict[str, object],
|
||||
) -> object:
|
||||
response = original_update(repository, branch, payload)
|
||||
fixture._managed_workflows["Jens/projectbrain"] = modified_workflow
|
||||
return response
|
||||
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertEqual(preview["workflow"]["profile"], "projectbrain")
|
||||
with patch.object(
|
||||
connector_instance.client,
|
||||
"update_branch_protection",
|
||||
side_effect=update_protection_then_change_workflow,
|
||||
):
|
||||
with self.assertRaisesRegex(GiteaError, "did not retain"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)
|
||||
self.assertEqual(
|
||||
_managed_validation_profile("Jens/projectbrain", modified_workflow),
|
||||
"projectbrain",
|
||||
)
|
||||
self.assertEqual(fixture._managed_workflows["Jens/projectbrain"], modified_workflow)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_custom_profile_blocks_unsafe_existing_protection(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
protection = fixture._branch_protections["Jens/projectbrain"][0]
|
||||
protection["enable_push"] = True
|
||||
protection["block_admin_merge_override"] = False
|
||||
original_protection = copy.deepcopy(protection)
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertEqual(preview["workflow"]["profile"], "projectbrain")
|
||||
self.assertIn(
|
||||
"managed-validation-projectbrain-protection-safety-locks-invalid",
|
||||
preview["blockers"],
|
||||
)
|
||||
with self.assertRaisesRegex(GiteaError, "unresolved blockers"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)
|
||||
self.assertEqual(
|
||||
fixture._branch_protections["Jens/projectbrain"][0], original_protection
|
||||
)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_all_scope_creates_safe_protection_without_rewriting_profile(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
original_workflow = fixture._managed_workflows["Jens/projectbrain"]
|
||||
fixture._branch_protections["Jens/projectbrain"] = []
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertEqual(preview["workflow"]["profile"], "projectbrain")
|
||||
self.assertEqual(preview["branchProtection"]["action"], "create")
|
||||
applied = connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)["data"]
|
||||
self.assertTrue(applied["compliant"])
|
||||
self.assertTrue(applied["verified"])
|
||||
self.assertEqual(fixture._managed_workflows["Jens/projectbrain"], original_workflow)
|
||||
created_protection = fixture._branch_protections["Jens/projectbrain"][0]
|
||||
self.assertEqual(
|
||||
created_protection["status_check_contexts"],
|
||||
[
|
||||
"Managed validation / full (pull_request)",
|
||||
"Pull request / quality (pull_request)",
|
||||
],
|
||||
)
|
||||
self.assertTrue(created_protection["block_admin_merge_override"])
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_protection_only_is_hash_bound_and_preserves_workflow(
|
||||
self,
|
||||
) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
invalid_workflow = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(" pull_request:\n", "")
|
||||
fixture._managed_workflows["Jens/projectbrain"] = invalid_workflow
|
||||
protection = fixture._branch_protections["Jens/projectbrain"][0]
|
||||
protection["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)",
|
||||
"Additional required check",
|
||||
]
|
||||
protection["required_approvals"] = 2
|
||||
protection["require_signed_commits"] = True
|
||||
original_protection = copy.deepcopy(protection)
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertEqual(preview["workflow"]["profile"], "projectbrain-required")
|
||||
self.assertEqual(preview["workflow"]["action"], "none")
|
||||
self.assertIn("managed-validation-projectbrain-profile-invalid", preview["blockers"])
|
||||
self.assertFalse(preview["compliant"])
|
||||
self.assertTrue(preview["protectionOnlyAvailable"])
|
||||
self.assertNotEqual(preview["planHash"], preview["protectionOnlyPlanHash"])
|
||||
with self.assertRaisesRegex(GiteaError, "unresolved blockers"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current, "projectbrain-repository", preview["planHash"]
|
||||
)
|
||||
with self.assertRaisesRegex(GiteaError, "preview the repository again"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current,
|
||||
"projectbrain-repository",
|
||||
preview["planHash"],
|
||||
apply_scope="protection_only",
|
||||
)
|
||||
self.assertEqual(fixture._managed_workflows["Jens/projectbrain"], invalid_workflow)
|
||||
self.assertEqual(
|
||||
fixture._branch_protections["Jens/projectbrain"][0], original_protection
|
||||
)
|
||||
|
||||
portfolio_result = connector_instance.portfolio_governance_apply(
|
||||
current,
|
||||
[
|
||||
{
|
||||
"repositoryRef": "projectbrain-repository",
|
||||
"planHash": preview["protectionOnlyPlanHash"],
|
||||
"applyScope": "protection_only",
|
||||
}
|
||||
],
|
||||
)["data"]
|
||||
self.assertEqual(portfolio_result["summary"], {"total": 1, "applied": 1, "failed": 0})
|
||||
self.assertEqual(portfolio_result["items"][0]["applyScope"], "protection_only")
|
||||
updated_protection = fixture._branch_protections["Jens/projectbrain"][0]
|
||||
self.assertEqual(
|
||||
updated_protection["status_check_contexts"],
|
||||
[
|
||||
"Additional required check",
|
||||
"Managed validation / full (pull_request)",
|
||||
"Pull request / quality (pull_request)",
|
||||
],
|
||||
)
|
||||
self.assertEqual(updated_protection["required_approvals"], 2)
|
||||
self.assertTrue(updated_protection["require_signed_commits"])
|
||||
self.assertEqual(fixture._managed_workflows["Jens/projectbrain"], invalid_workflow)
|
||||
self.assertEqual(
|
||||
fixture.recorded_bodies[-1],
|
||||
{
|
||||
"status_check_contexts": [
|
||||
"Additional required check",
|
||||
"Managed validation / full (pull_request)",
|
||||
"Pull request / quality (pull_request)",
|
||||
]
|
||||
},
|
||||
)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_protection_only_requires_an_existing_enabled_rule(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
fixture._managed_workflows["Jens/projectbrain"] = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", ""
|
||||
)
|
||||
fixture._branch_protections["Jens/projectbrain"] = []
|
||||
try:
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
context(), "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertFalse(preview["protectionOnlyAvailable"])
|
||||
self.assertIsNone(preview["protectionOnlyPlanHash"])
|
||||
self.assertEqual(preview["branchProtection"]["action"], "create")
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_protection_only_fails_if_gitea_drops_an_extra_context(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
fixture._managed_workflows["Jens/projectbrain"] = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", ""
|
||||
)
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)",
|
||||
"Additional required check",
|
||||
]
|
||||
original_read = connector_instance.client.branch_protections
|
||||
read_count = 0
|
||||
|
||||
def drop_extra_context(repository: RegisteredRepository) -> list[dict[str, object]]:
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 3:
|
||||
fixture._branch_protections["Jens/projectbrain"][0][
|
||||
"status_check_contexts"
|
||||
].remove("Additional required check")
|
||||
return original_read(repository)
|
||||
|
||||
try:
|
||||
current = context()
|
||||
with patch.object(
|
||||
connector_instance.client,
|
||||
"branch_protections",
|
||||
side_effect=drop_extra_context,
|
||||
):
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
self.assertTrue(preview["protectionOnlyAvailable"])
|
||||
with self.assertRaisesRegex(GiteaError, "did not retain"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current,
|
||||
"projectbrain-repository",
|
||||
preview["protectionOnlyPlanHash"],
|
||||
apply_scope="protection_only",
|
||||
)
|
||||
self.assertNotIn(
|
||||
"Additional required check",
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"],
|
||||
)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_projectbrain_protection_only_rejects_other_protection_changes(self) -> None:
|
||||
connector_instance, fixture = projectbrain_connector()
|
||||
fixture._managed_workflows["Jens/projectbrain"] = PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pull_request:\n", ""
|
||||
)
|
||||
original_read = connector_instance.client.branch_protections
|
||||
read_count = 0
|
||||
|
||||
def changed_protection_response(
|
||||
repository: RegisteredRepository,
|
||||
) -> list[dict[str, object]]:
|
||||
nonlocal read_count
|
||||
protections = original_read(repository)
|
||||
read_count += 1
|
||||
if read_count == 3:
|
||||
protections[0]["required_approvals"] = 0
|
||||
protections[0]["block_admin_merge_override"] = False
|
||||
return protections
|
||||
|
||||
try:
|
||||
current = context()
|
||||
with patch.object(
|
||||
connector_instance.client,
|
||||
"branch_protections",
|
||||
side_effect=changed_protection_response,
|
||||
):
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
current, "projectbrain-repository"
|
||||
)["data"]
|
||||
with self.assertRaisesRegex(GiteaError, "did not retain"):
|
||||
connector_instance.branch_protection_ensure(
|
||||
current,
|
||||
"projectbrain-repository",
|
||||
preview["protectionOnlyPlanHash"],
|
||||
apply_scope="protection_only",
|
||||
)
|
||||
self.assertEqual(
|
||||
fixture._managed_workflows["Jens/projectbrain"],
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(" pull_request:\n", ""),
|
||||
)
|
||||
finally:
|
||||
connector_instance.client.close()
|
||||
|
||||
def test_governance_update_never_relaxes_protected_main_push(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user