diff --git a/deployment/requests/HLCC-NUMERIC-SHARED-MODE-PERSIST-20260806.json b/deployment/requests/HLCC-NUMERIC-SHARED-MODE-PERSIST-20260806.json new file mode 100644 index 0000000..18d2d59 --- /dev/null +++ b/deployment/requests/HLCC-NUMERIC-SHARED-MODE-PERSIST-20260806.json @@ -0,0 +1,62 @@ +{ + "schema": "guanghu.architecture-provision-request/v1", + "request_id": "HLCC-NUMERIC-SHARED-MODE-PERSIST-20260806", + "target_node": "JD-FD-PRIMARY", + "architecture_id": "GLS-HLCC-009", + "module": { + "code": "HLCC-JD-CANDIDATE-01", + "name": "光湖代码频道数值共享模式持久化", + "bind": "loopback:3340,3341", + "owner": "systemd", + "unit": "hlcc-jd-candidate.service", + "run_user": "guanghu", + "writable_paths": [ + "/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1" + ], + "read_only_paths": [] + }, + "source_ref": "REPO-012:refs/heads/main", + "deployed_commit_policy": "use the exact human-merged commit and record it in the server-owned deployment receipt", + "source_paths": [ + "server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py", + "server-tools/hololake-code-channel/jd-candidate/app.ini", + "server-tools/hololake-code-channel/jd-candidate/hlcc-jd-candidate.service" + ], + "initial_provision": { + "kind": "new-architecture-unit", + "not_an_existing_action_bridge_extension": true, + "requires": [ + "persist core.sharedRepository=0660 for compatibility with RestrictSUIDSGID", + "preserve UMask=0077 for all non-repository code-channel state", + "keep the obsolete recursive chmod repository hook absent", + "quote the generated Forgejo custom-hook basename test to prevent argument expansion warnings", + "restart only hlcc-jd-candidate.service", + "do not alter repository content, branches, owner identity or authentication" + ] + }, + "verification": [ + "the automatic deployment receipt records DEPLOYED_AND_VERIFIED", + "the candidate subsequently reaches ready=true and mode=isolated-candidate", + "the exact repository reports core.sharedRepository=0660", + "the obsolete repository chmod hook remains absent", + "flat branch create and delete complete without chmod or shell argument warnings", + "both code-channel and direct-receiver repository checks pass" + ], + "runtime_check": { + "url": "http://127.0.0.1:3341/health", + "expected": { + "ok": true, + "mode": "bootstrap", + "version": "16.0.1", + "code": "HLCC-JD-CANDIDATE-01", + "package_profile": "full-offline-v16.0.1" + } + }, + "rollback": [ + "restore the backed-up hlcc-jd-candidate.service", + "restart the previous immutable release", + "retain repository content and authentication state", + "retain the server-owned hot-repair receipts for manual recovery" + ], + "status": "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING" +} diff --git a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py index 3a35229..6e3bb5d 100644 --- a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py +++ b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py @@ -482,8 +482,9 @@ def seed_fifth_domain_channel(binary: pathlib.Path) -> str: def configure_shared_channel_repository( repository: pathlib.Path = CHANNEL_REPOSITORY_PATH, + generated_hook: pathlib.Path | None = None, ) -> str: - """Use Git's native shared-repository mode instead of a post-receive chmod.""" + """Use sandbox-compatible native sharing instead of post-receive chmod.""" if not repository.is_dir(): raise RuntimeError("channel repository path unavailable") @@ -494,7 +495,7 @@ def configure_shared_channel_repository( str(repository), "config", "core.sharedRepository", - "group", + "0660", ], check=True, capture_output=True, @@ -513,7 +514,7 @@ def configure_shared_channel_repository( capture_output=True, text=True, ).stdout.strip() - if configured not in {"1", "group"}: + if configured != "0660": raise RuntimeError("shared repository configuration verification failed") obsolete_hook = ( @@ -530,6 +531,23 @@ def configure_shared_channel_repository( raise RuntimeError("obsolete sharing hook identity mismatch") obsolete_hook.unlink() + generated_hook = generated_hook or ( + STATE_ROOT / "data" / "data" / "home" / "hooks" / "post-receive" + ) + if not generated_hook.is_file() or generated_hook.is_symlink(): + raise RuntimeError("generated post-receive hook path unavailable") + generated_text = generated_hook.read_text(encoding="utf-8") + unsafe_test = 'if [ $(basename "${hook}") != "gitea" ]; then' + safe_test = 'if [ "$(basename "${hook}")" != "gitea" ]; then' + if unsafe_test in generated_text: + generated_hook.write_text( + generated_text.replace(unsafe_test, safe_test, 1), + encoding="utf-8", + ) + generated_hook.chmod(0o700) + elif safe_test not in generated_text: + raise RuntimeError("generated post-receive hook identity mismatch") + return "configured" diff --git a/server-tools/hololake-code-channel/jd-candidate/native.test.js b/server-tools/hololake-code-channel/jd-candidate/native.test.js index bd6438f..d1cbff1 100644 --- a/server-tools/hololake-code-channel/jd-candidate/native.test.js +++ b/server-tools/hololake-code-channel/jd-candidate/native.test.js @@ -48,8 +48,10 @@ assert.match(bootstrap, /access_tokens_migrated": False/); assert.match(bootstrap, /repositories_migrated": False/); assert.match(bootstrap, /delete from access_token/); assert.match(bootstrap, /core\.sharedRepository/); -assert.match(bootstrap, /"group"/); +assert.match(bootstrap, /"0660"/); assert.match(bootstrap, /obsolete sharing hook identity mismatch/); +assert.match(bootstrap, /generated post-receive hook identity mismatch/); +assert.match(bootstrap, /basename "\$\{hook\}"/); assert.match(bootstrap, /configure_shared_channel_repository\(\)/); assert.doesNotMatch( bootstrap, diff --git a/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py b/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py index 75cdf80..2b6cf0a 100644 --- a/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py +++ b/server-tools/hololake-code-channel/jd-candidate/test_owner_migration.py @@ -150,9 +150,14 @@ class SharedRepositoryTests(unittest.TestCase): "#!/bin/sh\nprintf '%s\\n' post_receive_permissions_reconciled\n", encoding="utf-8", ) + generated_hook = pathlib.Path(temporary) / "generated-post-receive" + generated_hook.write_text( + '#!/usr/bin/env bash\nif [ $(basename "${hook}") != "gitea" ]; then\n :\nfi\n', + encoding="utf-8", + ) self.assertEqual( - MODULE.configure_shared_channel_repository(repository), + MODULE.configure_shared_channel_repository(repository, generated_hook), "configured", ) configured = subprocess.run( @@ -168,8 +173,12 @@ class SharedRepositoryTests(unittest.TestCase): capture_output=True, text=True, ).stdout.strip() - self.assertIn(configured, {"1", "group"}) + self.assertEqual(configured, "0660") self.assertFalse(hook.exists()) + self.assertIn( + 'if [ "$(basename "${hook}")" != "gitea" ]; then', + generated_hook.read_text(encoding="utf-8"), + ) def test_refuses_unknown_hook_content(self) -> None: with tempfile.TemporaryDirectory() as temporary: @@ -178,9 +187,14 @@ class SharedRepositoryTests(unittest.TestCase): hook = repository / "hooks" / "post-receive.d" / "guanghu-ice-heart-share" hook.parent.mkdir(parents=True) hook.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + generated_hook = pathlib.Path(temporary) / "generated-post-receive" + generated_hook.write_text( + '#!/usr/bin/env bash\nif [ $(basename "${hook}") != "gitea" ]; then\n :\nfi\n', + encoding="utf-8", + ) with self.assertRaisesRegex(RuntimeError, "identity mismatch"): - MODULE.configure_shared_channel_repository(repository) + MODULE.configure_shared_channel_repository(repository, generated_hook) self.assertTrue(hook.exists()) diff --git a/server-tools/lake-lamp-authz/README.md b/server-tools/lake-lamp-authz/README.md index 49f3f1d..a6d8d00 100644 --- a/server-tools/lake-lamp-authz/README.md +++ b/server-tools/lake-lamp-authz/README.md @@ -125,9 +125,10 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \ `safe.directory`,不得使用通配符。 光湖代码频道本身保留 `UMask=0077`,以免放宽数据库和其他状态目录。对需要由直达 -接收器共同读写的精确裸仓库设置 Git 原生 `core.sharedRepository=group`,让新建的 -`objects` 与 `refs` 从写入时就继承专用共享组权限。不要安装推送后递归 `chmod` 的 -钩子,也不要为了共享一个裸仓库而修改整个代码频道服务的 UMask。 +接收器共同读写的精确裸仓库设置 Git 原生 `core.sharedRepository=0660`。数值模式让 +新建文件对专用共享组可读写,同时不会要求安全沙箱禁止的 setgid 操作。部署分支使用 +不含斜杠的扁平名称,避免由代码频道服务临时创建新的私有引用目录。不要安装推送后递归 +`chmod` 的钩子,也不要为了共享一个裸仓库而修改整个代码频道服务的 UMask。 部署后用当前主分支生成无变化验收 bundle,经 `receiveBundle` 完整执行一次;验收前后 主分支 SHA 必须一致,并且代码频道账户与授权服务账户执行 `git fsck` 均通过。