security: verify authenticated Forgejo routes live (FORGE6-ROUTES-001 r3)

Revision 2 live run passed 24/25; the clean merge hit Forgejo's 405 during
the mergeability recheck. Revision 3 waits for mergeable, retries only 405
(max 3) and records status codes. 7 boundary tests, isolated rehearsal
(27 checks) and the attended live run (26/26, verified, cleanup confirmed)
pass. Revision 2 proposal and receipts retained.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-10-18 02:33:46 +02:00
parent aae10f5bbb
commit 81f2f59649
10 changed files with 261 additions and 16 deletions

View file

@ -65,6 +65,28 @@ class Suite:
raise ValueError('invalid-object-id')
return oid
def wait_mergeable(self, suffix):
for _ in range(40):
code, body = self.api('GET', suffix)
if code == 200 and json.loads(body).get('mergeable') is True:
return
time.sleep(.5)
raise ValueError('pull-not-mergeable')
def clean_merge(self, suffix):
# A refused merge makes Forgejo recheck mergeability; until that finishes
# it answers 405. Wait again and retry only that status, a bounded few times.
statuses = self.evidence['clean_merge_status_codes'] = []
for _ in range(3):
self.wait_mergeable(suffix)
code = self.api('POST', suffix + '/merge', {'Do': 'merge',
'MergeTitleField': 'Clean merge', 'MergeMessageField': 'Clean proof.'})[0]
statuses.append(code)
if code != 405:
break
time.sleep(1)
return statuses[-1]
def content(self, name, content, branch='main'):
return self.api('POST', '/contents/' + name,
{'content': base64.b64encode(content).decode(),
@ -137,13 +159,7 @@ class Suite:
if type(number) is not int:
raise ValueError('invalid-pull-number')
suffix = '/pulls/' + str(number)
for _ in range(40):
code, body = self.api('GET', suffix)
if code == 200 and json.loads(body).get('mergeable') is True:
break
time.sleep(.5)
else:
raise ValueError('pull-not-mergeable')
self.wait_mergeable(suffix)
before = self.head()
code, body = self.api('POST', suffix + '/merge', {'Do': 'merge',
'MergeTitleField': 'Disposable merge', 'MergeMessageField': fixture.decode(),
@ -152,8 +168,7 @@ class Suite:
detected(json.loads(body).get('message', '').encode()))
code, body = self.api('GET', suffix)
self.check('rejected_merge_not_applied', code == 200 and json.loads(body).get('merged') is False and self.head() == before)
self.check('clean_merge_accepted', self.api('POST', suffix + '/merge', {'Do': 'merge',
'MergeTitleField': 'Clean merge', 'MergeMessageField': 'Clean proof.'})[0] in (200, 201))
self.check('clean_merge_accepted', self.clean_merge(suffix) in (200, 201))
code, body = self.api('GET', suffix)
self.check('clean_merge_applied', code == 200 and json.loads(body).get('merged') is True and self.head() != before)
self.evidence['acceptance_passed'] = True

View file

@ -0,0 +1,30 @@
{
"proposal_id": "FORGE6-ROUTES-001",
"revision": 2,
"state": "prepared-isolated-verified-awaiting-attended-login",
"origin": "https://forgejo.coulomb.social",
"user": "tegwick",
"repository_name": "custodian-routes-f140c6b840da4bbb9de64a74",
"source_sha256": {
"accept_forge_routes.py": "96608481ab62445d2688e20f48c76a35c3d7be26c37452be3c46202eec4e103c",
"run_forge_routes.py": "6258140d408e5fbed7373611cdcd65e64aee743a0b36ce4fe55b5dc94663120f",
"receive-hook-proposal.json": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d",
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
"activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3",
"verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd"
},
"target": "One newly created private disposable repository; pinned deployed Forgejo and admitted hook. Existing repository content, hooks, accounts and policies remain untouched.",
"expected_result": "Authenticated native HTTPS clean acceptance and synthetic-secret refusal with detector evidence; API private creation inherits exact hook; clean/content-secret and clean/merge-message refusal with unchanged refs; quarantine commit absent; anonymous private access refused; owned test repository deleted and absent.",
"verification": "Exact shared Suite rehearsed against pinned isolated Forgejo; five boundary regression tests. Live receipt required; rehearsal is not deployed acceptance.",
"rollback": "Automatic cleanup checks numeric repository ID, owner, private flag, name and ownership description before deleting only the newly created test repository. On interrupted known creation, rerun run_forge_routes.py with the same proposal and receipt plus --cleanup-only. Ambiguous POST before numeric ID receipt requires reconciliation, never replay or delete by name.",
"abort_owner": "railiance-forge; custodian-codex prepares and verifies receipts",
"cost_estimate_eur": 0,
"cost_basis": "Existing service only; serialized small private probes, no added resources or spend commitments.",
"risk_estimate": "Bounded synthetic test writes to a unique private disposable repository. No real secret fixtures, account creation, access-policy change or external publication. Unexpected acceptance may retain an unbound synthetic private-key fixture until cleanup. API/network failure stops the trial; ambiguous creation and cleanup remain explicitly unverified.",
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"lane": "orange: existing caller OIDC/MFA required; no access or secret-policy scope change",
"live_receipt": "docs/evidence/2026-10-13-netkingdom-security/forge-routes-live.json",
"remaining_scope": "Browser/SSO journey, external gitlink repository, CI, import/mirror, historical/LFS/log/artifact detection and finding response remain open.",
"original_revision_sha256": "a253fce3a565a2af94133da019bdc434dc57b21a8c61b06b4c27e036cf5a01d3",
"refinement": "Pre-execution rehearsal corrected inherited Git hook mode to accept exact hash/owner with 0555 or 0755; initial failed rehearsal and source-binding limitation are retained."
}

View file

@ -1,12 +1,12 @@
{
"proposal_id": "FORGE6-ROUTES-001",
"revision": 2,
"state": "prepared-isolated-verified-awaiting-attended-login",
"revision": 3,
"state": "prepared-awaiting-isolated-rehearsal",
"origin": "https://forgejo.coulomb.social",
"user": "tegwick",
"repository_name": "custodian-routes-f140c6b840da4bbb9de64a74",
"repository_name": "custodian-routes-b15df5a32e9697ab47215efc",
"source_sha256": {
"accept_forge_routes.py": "96608481ab62445d2688e20f48c76a35c3d7be26c37452be3c46202eec4e103c",
"accept_forge_routes.py": "73191bb85bfb41de7084a539eb30ff3f4cb5650fd79e68ff8acc496089bb77f0",
"run_forge_routes.py": "6258140d408e5fbed7373611cdcd65e64aee743a0b36ce4fe55b5dc94663120f",
"receive-hook-proposal.json": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d",
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
@ -15,7 +15,7 @@
},
"target": "One newly created private disposable repository; pinned deployed Forgejo and admitted hook. Existing repository content, hooks, accounts and policies remain untouched.",
"expected_result": "Authenticated native HTTPS clean acceptance and synthetic-secret refusal with detector evidence; API private creation inherits exact hook; clean/content-secret and clean/merge-message refusal with unchanged refs; quarantine commit absent; anonymous private access refused; owned test repository deleted and absent.",
"verification": "Exact shared Suite rehearsed against pinned isolated Forgejo; five boundary regression tests. Live receipt required; rehearsal is not deployed acceptance.",
"verification": "Seven boundary tests pass. Exact shared Suite must be re-rehearsed against pinned isolated Forgejo with the admitted binaries before live use; rehearsal is not deployed acceptance.",
"rollback": "Automatic cleanup checks numeric repository ID, owner, private flag, name and ownership description before deleting only the newly created test repository. On interrupted known creation, rerun run_forge_routes.py with the same proposal and receipt plus --cleanup-only. Ambiguous POST before numeric ID receipt requires reconciliation, never replay or delete by name.",
"abort_owner": "railiance-forge; custodian-codex prepares and verifies receipts",
"cost_estimate_eur": 0,
@ -23,8 +23,11 @@
"risk_estimate": "Bounded synthetic test writes to a unique private disposable repository. No real secret fixtures, account creation, access-policy change or external publication. Unexpected acceptance may retain an unbound synthetic private-key fixture until cleanup. API/network failure stops the trial; ambiguous creation and cleanup remain explicitly unverified.",
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"lane": "orange: existing caller OIDC/MFA required; no access or secret-policy scope change",
"live_receipt": "docs/evidence/2026-10-13-netkingdom-security/forge-routes-live.json",
"live_receipt": "docs/evidence/2026-10-18-netkingdom-security/forge-routes-live-r3.json",
"remaining_scope": "Browser/SSO journey, external gitlink repository, CI, import/mirror, historical/LFS/log/artifact detection and finding response remain open.",
"original_revision_sha256": "a253fce3a565a2af94133da019bdc434dc57b21a8c61b06b4c27e036cf5a01d3",
"refinement": "Pre-execution rehearsal corrected inherited Git hook mode to accept exact hash/owner with 0555 or 0755; initial failed rehearsal and source-binding limitation are retained."
"refinement": "Pre-execution rehearsal corrected inherited Git hook mode to accept exact hash/owner with 0555 or 0755; initial failed rehearsal and source-binding limitation are retained.",
"previous_revision_sha256": "d12c12857e9ed5f4afa3c1c231fd142adad580a458cc175e74f0ea0e3118d792",
"previous_live_receipt": "docs/evidence/2026-10-13-netkingdom-security/forge-routes-live.json",
"refinement_r3": "Revision 2 live run (2026-10-18) passed 24/25 checks; the clean merge sent ~1s after the refused merge got 405 in 17ms (Forgejo log), i.e. mergeability recheck still running. Revision 3 waits for mergeable again before the clean merge, retries only 405 at most three times, and records the merge status codes (no response bodies). Two new boundary tests cover retry and its bounds."
}

View file

@ -54,6 +54,37 @@ class CleanupTests(unittest.TestCase):
self.assertFalse(m.detected(b'{"status":"unverified"}'))
class MergeRetryTests(unittest.TestCase):
def suite(self, directory, merges):
calls = []
class Backend:
user = 'tegwick'
scope = 'test'
def api(self, method, path, data=None, authenticated=True):
calls.append(method)
if method == 'GET':
return 200, json.dumps({'mergeable': True}).encode()
return merges.pop(0), b'{}'
suite = m.Suite(Backend(), Path(directory) / 'receipt.json', 'custodian-routes-' + 'a' * 24)
return suite, calls
def test_recheck_405_is_waited_out_and_retried(self):
with tempfile.TemporaryDirectory() as directory, mock.patch.object(m.time, 'sleep'):
suite, calls = self.suite(directory, [405, 200])
self.assertEqual(suite.clean_merge('/pulls/1'), 200)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [405, 200])
self.assertEqual(calls, ['GET', 'POST', 'GET', 'POST'])
def test_other_refusals_are_not_retried_and_retries_are_bounded(self):
with tempfile.TemporaryDirectory() as directory, mock.patch.object(m.time, 'sleep'):
suite, _ = self.suite(directory, [409])
self.assertEqual(suite.clean_merge('/pulls/1'), 409)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [409])
suite, _ = self.suite(directory, [405, 405, 405, 200])
self.assertEqual(suite.clean_merge('/pulls/1'), 405)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [405, 405, 405])
class TransportTests(unittest.TestCase):
def test_http_redirect_does_not_forward_injected_credential(self):
hits = []

View file

@ -0,0 +1,39 @@
{
"result": "unverified",
"repository_name": "custodian-routes-f140c6b840da4bbb9de64a74",
"observed_at": "2026-10-18T00:18:27.583559+00:00",
"checks": {
"expected_authenticated_caller": true,
"target_does_not_preexist": true,
"owned_private_repository_created": true,
"anonymous_private_read_denied": true,
"new_repository_inherits_exact_hook": true,
"http_clean_push_accepted": true,
"http_clean_ref_matches": true,
"http_secret_push_detected_and_refused": true,
"http_refused_ref_absent": true,
"http_rejection_preserves_main": true,
"http_quarantine_object_absent": true,
"api_clean_content_accepted": true,
"api_secret_content_refused": true,
"api_refusal_has_detector_evidence": true,
"api_refusal_preserves_main": true,
"api_refused_content_absent": true,
"feature_branch_created": true,
"feature_content_accepted": true,
"pull_request_created": true,
"merge_secret_message_detected_and_refused": true,
"rejected_merge_not_applied": true,
"clean_merge_accepted": false,
"cleanup_identity_matches": true,
"owned_disposable_repository_deleted": true,
"deleted_repository_absent": true
},
"scope": "Deployed HTTPS Git, repository creation, content API and merge API on one owned private disposable repository. No browser/SSO, CI, import/mirror or finding-response conformance inferred.",
"proposal_sha256": "d12c12857e9ed5f4afa3c1c231fd142adad580a458cc175e74f0ea0e3118d792",
"phase": "cleanup",
"executor_sha256": "96608481ab62445d2688e20f48c76a35c3d7be26c37452be3c46202eec4e103c",
"creation_attempted": true,
"created_repository_id": 173,
"failure_phase": "merge-api"
}

View file

@ -0,0 +1,8 @@
{
"route_exit_code": 1,
"receipt_present": true,
"cleanup_only": false,
"proposal_sha256": "d12c12857e9ed5f4afa3c1c231fd142adad580a458cc175e74f0ea0e3118d792",
"raw_diagnostics_retained": false,
"credential_returned_to_agent": false
}

View file

@ -0,0 +1,24 @@
{
"observed_at": "2026-10-18T00:27:53.039275+00:00",
"image": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba",
"result": "fixture-ready",
"checks": {
"synthetic_identity_authenticated": true,
"native_repository_created": true
},
"scope": "Pinned-image fixture setup only; shared suite has its own acceptance receipt.",
"identity_http_status": 200,
"repository_create_http_status": 201,
"repository_create_error_kinds": [
"email"
],
"inherited_hook_mode_owner": "755 1000 1000",
"failure_diagnostics": [
"PushRejected",
"app.ini",
"exit status 1"
],
"isolated_container_and_volumes_removed": true,
"last_phase": "baseline",
"executor_sha256": "88bbf4235086b7ffc40ca4d7df7fafa1d67e2e1b41d2f0055ec82421ed4840cb"
}

View file

@ -0,0 +1,44 @@
{
"result": "verified",
"repository_name": "custodian-routes-8da2932b43a6476ab883aca2",
"observed_at": "2026-10-18T00:28:10.732135+00:00",
"checks": {
"expected_authenticated_caller": true,
"target_does_not_preexist": true,
"owned_private_repository_created": true,
"anonymous_private_read_denied": true,
"new_repository_inherits_exact_hook": true,
"http_clean_push_accepted": true,
"http_clean_ref_matches": true,
"http_secret_push_detected_and_refused": true,
"http_refused_ref_absent": true,
"http_rejection_preserves_main": true,
"http_quarantine_object_absent": true,
"api_clean_content_accepted": true,
"api_secret_content_refused": true,
"api_refusal_has_detector_evidence": true,
"api_refusal_preserves_main": true,
"api_refused_content_absent": true,
"feature_branch_created": true,
"feature_content_accepted": true,
"pull_request_created": true,
"merge_secret_message_detected_and_refused": true,
"rejected_merge_not_applied": true,
"clean_merge_accepted": true,
"clean_merge_applied": true,
"executor_unchanged_during_rehearsal": true,
"cleanup_identity_matches": true,
"owned_disposable_repository_deleted": true,
"deleted_repository_absent": true
},
"scope": "Exact shared HTTP/API suite on pinned isolated Forgejo, SQLite, no external network; no live credential or deployed acceptance.",
"loaded_executor_sha256": "73191bb85bfb41de7084a539eb30ff3f4cb5650fd79e68ff8acc496089bb77f0",
"phase": "cleanup",
"executor_sha256": "73191bb85bfb41de7084a539eb30ff3f4cb5650fd79e68ff8acc496089bb77f0",
"creation_attempted": true,
"created_repository_id": 2,
"clean_merge_status_codes": [
200
],
"acceptance_passed": true
}

View file

@ -0,0 +1,43 @@
{
"result": "verified",
"repository_name": "custodian-routes-b15df5a32e9697ab47215efc",
"observed_at": "2026-10-18T00:32:19.351604+00:00",
"checks": {
"expected_authenticated_caller": true,
"target_does_not_preexist": true,
"owned_private_repository_created": true,
"anonymous_private_read_denied": true,
"new_repository_inherits_exact_hook": true,
"http_clean_push_accepted": true,
"http_clean_ref_matches": true,
"http_secret_push_detected_and_refused": true,
"http_refused_ref_absent": true,
"http_rejection_preserves_main": true,
"http_quarantine_object_absent": true,
"api_clean_content_accepted": true,
"api_secret_content_refused": true,
"api_refusal_has_detector_evidence": true,
"api_refusal_preserves_main": true,
"api_refused_content_absent": true,
"feature_branch_created": true,
"feature_content_accepted": true,
"pull_request_created": true,
"merge_secret_message_detected_and_refused": true,
"rejected_merge_not_applied": true,
"clean_merge_accepted": true,
"clean_merge_applied": true,
"cleanup_identity_matches": true,
"owned_disposable_repository_deleted": true,
"deleted_repository_absent": true
},
"scope": "Deployed HTTPS Git, repository creation, content API and merge API on one owned private disposable repository. No browser/SSO, CI, import/mirror or finding-response conformance inferred.",
"proposal_sha256": "322434dcb5a363f962a62d4e03613b68e829248ff75c849997577dac45185d8d",
"phase": "cleanup",
"executor_sha256": "73191bb85bfb41de7084a539eb30ff3f4cb5650fd79e68ff8acc496089bb77f0",
"creation_attempted": true,
"created_repository_id": 174,
"clean_merge_status_codes": [
200
],
"acceptance_passed": true
}

View file

@ -0,0 +1,8 @@
{
"route_exit_code": 0,
"receipt_present": true,
"cleanup_only": false,
"proposal_sha256": "322434dcb5a363f962a62d4e03613b68e829248ff75c849997577dac45185d8d",
"raw_diagnostics_retained": false,
"credential_returned_to_agent": false
}