From ab30100cfd64030279e0bc544b436e1ffb1759c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Sun, 26 Jul 2026 17:58:51 +0800 Subject: [PATCH] fix: guard personal source packaging by deployment identity --- docs/ARCHITECTURE.md | 2 + docs/TEAM-FOUNDATION-HANDOFF.md | 5 + ...source-ownership-and-deployment-routing.md | 31 ++++++ package.json | 10 +- ...CHANNEL-0001-ICE-GL-INFINITY-ZERO-CORE.yml | 9 +- research/source-route-policy.json | 45 ++++++++ scripts/deployment-source-guard.mjs | 55 +++++++++ scripts/deployment-source-guard.test.mjs | 104 ++++++++++++++++++ 8 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 docs/adr/0155-source-ownership-and-deployment-routing.md create mode 100644 research/source-route-policy.json create mode 100644 scripts/deployment-source-guard.mjs create mode 100644 scripts/deployment-source-guard.test.mjs diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ff82f5d..9014638 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -285,6 +285,8 @@ On Linux, `run()` applies WebKitGTK startup safeguards before Tauri creates the Internal HoloLake review artifacts use the repository-owned `scripts/build-internal-release.sh` dispatcher and take their version from `src-tauri/tauri.conf.json`. The macOS lane builds a Tauri app bundle, runs the public-architecture/private-route content gate, applies an ad-hoc deep signature, verifies the bundle, and creates a verified DMG plus SHA-256 file. The JD Linux lane cannot ask Tauri's Linux CLI for an NSIS target, so it uses cargo-xwin to build the PE32+ Windows x64 application, stages the executable with MCP, Agent, and public GLS resources, then invokes `scripts/windows-installer.nsi` directly. The same content gate runs against both the frontend output and Windows stage before the installer is accepted. `docs/skills/internal-release-packaging/SKILL.md` is the recovery and handoff contract for this pipeline; production notarization and publisher signing remain owned by the formal release workflows described by ADR-0131. +Deployment source ownership is separate from repository readability. `research/source-route-policy.json` binds each deployable distribution to a physical repository ID, source channel, source owner, human authorizer, executing persona, execution runtime, and target node. Personal source in this repository is owned and authorized by `ICE-GL∞`, executed by the Fifth Domain Zhuyuan controller `ICE-GL-ZY001` through `SYS-GLW-ZY-EXEC-0001`, and may target only `JD-FD-PRIMARY`. Team deployment must originate from the enterprise repository on `AW-GZ-001`, and reusable public modules must come from a separately registered module repository. Packaging performs an early fail-closed check, while the protected Lake Lamp deployment receiver repeats the check as the authoritative enforcement boundary. See ADR-0155. + ## Multi-Window (Note Windows) Notes can be opened in separate Tauri windows for focused editing. Secondary windows boot the same `App` shell and load the same active workspace graph as the main window, but they start in the editor-only view mode with side panels collapsed. diff --git a/docs/TEAM-FOUNDATION-HANDOFF.md b/docs/TEAM-FOUNDATION-HANDOFF.md index 22d9d1d..7812d6d 100644 --- a/docs/TEAM-FOUNDATION-HANDOFF.md +++ b/docs/TEAM-FOUNDATION-HANDOFF.md @@ -33,6 +33,11 @@ GLS 团队基座 ## 3. 真实源码入口 +> 2026-07-26 安全收束:本仓当前物理源登记为冰朔个人 `HLP-CHANNEL-0001`, +> 不再作为团队部署源。团队页面可继续本地开发和测试,但团队安装包必须等 +> `AW-GZ-001` 企业研发仓取得正式仓库编号与频道编号后,从企业仓生成。 +> 公共复用模块另入公共模块仓;读取本仓不构成部署授权。 + - 团队 GLS 页面:`src/TeamFoundationApp.tsx` - 团队独立渲染入口:`src/team-foundation-main.tsx` - 团队独立 HTML:`team-foundation.html` diff --git a/docs/adr/0155-source-ownership-and-deployment-routing.md b/docs/adr/0155-source-ownership-and-deployment-routing.md new file mode 100644 index 0000000..5273691 --- /dev/null +++ b/docs/adr/0155-source-ownership-and-deployment-routing.md @@ -0,0 +1,31 @@ +# ADR 0155: Source ownership and deployment routing + +## Status + +Accepted as an immediate fail-closed boundary on 2026-07-26. + +## Context + +The HoloLake research repository is readable by collaborators, but its source was also treated as a generic deployment source. The repository only registered `HLP-CHANNEL-0001`, Ice Shuo's Zero Core personal channel, while build commands exposed both personal and team packages. A collaborator could therefore select the personal build path and deploy source they did not own. + +Read access is intentional. Deployment authority is not. + +## Decision + +HoloLake uses three separately routed source domains: + +1. Personal product source lives in its human owner's repository on the owner's registered server. +2. Team product source lives in the enterprise research repository on `AW-GZ-001`. +3. Reusable modules live in a public module repository and are imported as versioned dependencies, never deployed as an application source. + +`research/source-route-policy.json` is the repository-side route declaration. Packaging now fails closed unless distribution, repository, source channel, persona, and target match an active route. The Lake Lamp deployment receiver independently enforces the same binding from its protected server registry. + +Until exact enterprise and public-module repository identifiers are registered, their routes remain disabled. Team UI development can continue with `pnpm build:team-foundation`, but this personal repository cannot produce a team deployment artifact. + +## Consequences + +- Cloning or reading source remains allowed. +- A team persona cannot package or deploy Ice Shuo's personal distribution. +- Personal packaging requires source owner and human authorizer `ICE-GL∞`, executing persona `ICE-GL-ZY001`, public execution runtime `SYS-GLW-ZY-EXEC-0001`, and target `JD-FD-PRIMARY`. +- Team deployment remains blocked until the enterprise repository is created and mapped. +- Public modules need an explicit repository and module manifest before cross-channel reuse. diff --git a/package.json b/package.json index 8b54637..7dce4f9 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,12 @@ "test:coverage": "node scripts/run-vitest-coverage.mjs", "verify:internal-package": "node scripts/verify-internal-package-content.mjs", "test:internal-release": "node --test scripts/internal-release-version.test.mjs", - "package:internal:macos": "./scripts/build-internal-release.sh macos", - "package:internal:windows": "./scripts/build-internal-release.sh windows", - "package:team:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Lighthouse Team Beta 0.2.0' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Lighthouse-{version}-Team-Beta-Mac-aarch64' ./scripts/build-internal-release.sh macos", - "package:team:windows": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Windows-x64-setup' ./scripts/build-internal-release.sh windows", + "guard:deployment-source": "node scripts/deployment-source-guard.mjs", + "test:deployment-source": "node --test scripts/deployment-source-guard.test.mjs", + "package:internal:macos": "HOLOLAKE_DISTRIBUTION=personal HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && ./scripts/build-internal-release.sh macos", + "package:internal:windows": "HOLOLAKE_DISTRIBUTION=personal HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && ./scripts/build-internal-release.sh windows", + "package:team:macos": "HOLOLAKE_DISTRIBUTION=team HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Lighthouse Team Beta 0.2.0' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Lighthouse-{version}-Team-Beta-Mac-aarch64' ./scripts/build-internal-release.sh macos", + "package:team:windows": "HOLOLAKE_DISTRIBUTION=team HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Windows-x64-setup' ./scripts/build-internal-release.sh windows", "prepare": "husky" }, "dependencies": { diff --git a/research/channels/HLP-CHANNEL-0001-ICE-GL-INFINITY-ZERO-CORE.yml b/research/channels/HLP-CHANNEL-0001-ICE-GL-INFINITY-ZERO-CORE.yml index 02caa26..6c89489 100644 --- a/research/channels/HLP-CHANNEL-0001-ICE-GL-INFINITY-ZERO-CORE.yml +++ b/research/channels/HLP-CHANNEL-0001-ICE-GL-INFINITY-ZERO-CORE.yml @@ -26,10 +26,17 @@ active_workorders: [] collaborators: - ICE-GL-ZY001 source_home: apps/tolaria/modules/ +source_repository: REPO-008 +source_node: JD-FD-PRIMARY +allowed_distribution: personal +deployment_policy: research/source-route-policy.json module_registry: research/module-registry.yml broadcast: research/broadcast/HLP-BRD-0003-ICE-GL-INFINITY-ZERO-CORE-ACTIVATION.md memory: research/memory/ICE-GL-INFINITY-TOLARIA-OS-MEMORY-001.md latest_progress: research/progress/HLP-DEV-0003-ICE-GL-INFINITY-TOLARIA-LIGHTWEIGHT-UI-AND-DRAG-FIX.md visibility: private execution_boundary: >- - 本频道登记研发方向与模块认领;不授予服务器、账户、部署或密钥权限。 + 本频道是冰朔个人版源码归属;公开读取不授予部署权。只有冰朔 ICE-GL∞ 完成人类授权, + 再由第五域铸渊主控 ICE-GL-ZY001 进入公共执行运行时 SYS-GLW-ZY-EXEC-0001, + 才能把 personal 发行部署到 JD-FD-PRIMARY。团队发行必须转入 AW-GZ-001 上另行登记的 + 企业研发仓;公共模块必须转入另行登记的公共模块仓。 diff --git a/research/source-route-policy.json b/research/source-route-policy.json new file mode 100644 index 0000000..84619b4 --- /dev/null +++ b/research/source-route-policy.json @@ -0,0 +1,45 @@ +{ + "schema": "hololake.source-route-policy/v1", + "repository_role": "personal_source_with_cross-domain_navigation", + "routes": [ + { + "distribution": "personal", + "repository_id": "REPO-008", + "channel_id": "HLP-CHANNEL-0001", + "source_node": "JD-FD-PRIMARY", + "source_owner_id": "ICE-GL∞", + "allowed_authorizers": ["ICE-GL∞"], + "allowed_personas": ["ICE-GL-ZY001"], + "allowed_execution_runtimes": ["SYS-GLW-ZY-EXEC-0001"], + "allowed_targets": ["JD-FD-PRIMARY"], + "deployment_enabled": true, + "state": "active_personal_source" + }, + { + "distribution": "team", + "repository_id": null, + "channel_id": null, + "source_node": "AW-GZ-001", + "allowed_personas": [], + "allowed_targets": ["AW-GZ-001"], + "deployment_enabled": false, + "state": "enterprise_repository_registration_required" + }, + { + "distribution": "public-module", + "repository_id": null, + "channel_id": null, + "source_node": null, + "allowed_personas": [], + "allowed_targets": [], + "deployment_enabled": false, + "state": "public_module_repository_registration_required" + } + ], + "rules": [ + "Repository read access never grants deployment authority.", + "Personal source deploys only from its owner channel to its registered personal node.", + "Team source must live in the enterprise repository on AW-GZ-001 before team deployment is enabled.", + "Public modules are reusable source packages and are never an application deployment target." + ] +} diff --git a/scripts/deployment-source-guard.mjs b/scripts/deployment-source-guard.mjs new file mode 100644 index 0000000..000ff6e --- /dev/null +++ b/scripts/deployment-source-guard.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises' + +export function validateDeploymentSource(policy, context) { + const route = policy.routes?.find(candidate => candidate.distribution === context.distribution) + if (!route || route.deployment_enabled !== true) { + return { ok: false, reason: 'deployment_source_route_inactive' } + } + if (route.repository_id !== context.repositoryId || route.channel_id !== context.channelId) { + return { ok: false, reason: 'deployment_source_binding_mismatch' } + } + if (route.source_owner_id !== context.sourceOwnerId) { + return { ok: false, reason: 'deployment_source_owner_mismatch' } + } + if (!route.allowed_authorizers?.includes(context.authorizerId)) { + return { ok: false, reason: 'deployment_authorizer_not_allowed' } + } + if (!route.allowed_personas?.includes(context.personaId)) { + return { ok: false, reason: 'deployment_persona_not_allowed' } + } + if (!route.allowed_execution_runtimes?.includes(context.executionRuntimeId)) { + return { ok: false, reason: 'deployment_execution_runtime_not_allowed' } + } + if (!route.allowed_targets?.includes(context.target)) { + return { ok: false, reason: 'deployment_target_not_allowed' } + } + return { ok: true } +} + +async function run() { + const policyFile = process.env.HOLOLAKE_SOURCE_POLICY || 'research/source-route-policy.json' + const policy = JSON.parse(await readFile(policyFile, 'utf8')) + const context = { + distribution: process.env.HOLOLAKE_DISTRIBUTION || '', + repositoryId: process.env.HOLOLAKE_SOURCE_REPOSITORY_ID || '', + channelId: process.env.HOLOLAKE_SOURCE_CHANNEL_ID || '', + sourceOwnerId: process.env.HOLOLAKE_SOURCE_OWNER_ID || '', + authorizerId: process.env.HOLOLAKE_HUMAN_AUTHORIZER_ID || '', + personaId: process.env.HOLOLAKE_PERSONA_ID || '', + executionRuntimeId: process.env.HOLOLAKE_EXECUTION_RUNTIME_ID || '', + target: process.env.HOLOLAKE_DEPLOY_TARGET || '', + } + const result = validateDeploymentSource(policy, context) + if (!result.ok) { + console.error(`HoloLake deployment source rejected: ${result.reason}`) + process.exitCode = 77 + return + } + console.log(`HoloLake deployment source verified: ${context.distribution} · ${context.channelId} · ${context.target}`) +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run() +} diff --git a/scripts/deployment-source-guard.test.mjs b/scripts/deployment-source-guard.test.mjs new file mode 100644 index 0000000..945a066 --- /dev/null +++ b/scripts/deployment-source-guard.test.mjs @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { validateDeploymentSource } from './deployment-source-guard.mjs' + +const policy = { + schema: 'hololake.source-route-policy/v1', + routes: [ + { + distribution: 'personal', + repository_id: 'REPO-008', + channel_id: 'HLP-CHANNEL-0001', + source_node: 'JD-FD-PRIMARY', + source_owner_id: 'ICE-GL∞', + allowed_authorizers: ['ICE-GL∞'], + allowed_personas: ['ICE-GL-ZY001'], + allowed_execution_runtimes: ['SYS-GLW-ZY-EXEC-0001'], + allowed_targets: ['JD-FD-PRIMARY'], + deployment_enabled: true, + }, + { + distribution: 'team', + repository_id: null, + channel_id: null, + source_node: 'AW-GZ-001', + allowed_personas: [], + allowed_targets: ['AW-GZ-001'], + deployment_enabled: false, + }, + { + distribution: 'public-module', + repository_id: null, + channel_id: null, + source_node: null, + allowed_personas: [], + allowed_targets: [], + deployment_enabled: false, + }, + ], +} + +test('allows Ice Shuo authorization plus Zhuyuan main control to package the personal distribution', () => { + assert.deepEqual(validateDeploymentSource(policy, { + distribution: 'personal', + repositoryId: 'REPO-008', + channelId: 'HLP-CHANNEL-0001', + sourceOwnerId: 'ICE-GL∞', + authorizerId: 'ICE-GL∞', + personaId: 'ICE-GL-ZY001', + executionRuntimeId: 'SYS-GLW-ZY-EXEC-0001', + target: 'JD-FD-PRIMARY', + }), { ok: true }) +}) + +test('rejects a team persona using Ice Shuo personal source', () => { + assert.deepEqual(validateDeploymentSource(policy, { + distribution: 'personal', + repositoryId: 'REPO-008', + channelId: 'HLP-CHANNEL-0001', + sourceOwnerId: 'ICE-GL∞', + authorizerId: 'ICE-GL∞', + personaId: 'AGE-TEAM-001', + executionRuntimeId: 'SYS-GLW-ZY-EXEC-0001', + target: 'JD-FD-PRIMARY', + }), { ok: false, reason: 'deployment_persona_not_allowed' }) +}) + +test('blocks team deployment until its enterprise repository route is registered', () => { + assert.deepEqual(validateDeploymentSource(policy, { + distribution: 'team', + repositoryId: 'REPO-008', + channelId: 'HLP-CHANNEL-0001', + sourceOwnerId: 'ICE-GL∞', + authorizerId: 'ICE-GL∞', + personaId: 'AGE-TEAM-001', + executionRuntimeId: 'SYS-GLW-ZY-EXEC-0001', + target: 'AW-GZ-001', + }), { ok: false, reason: 'deployment_source_route_inactive' }) +}) + +test('public modules are reusable source and never an application deployment target', () => { + assert.deepEqual(validateDeploymentSource(policy, { + distribution: 'public-module', + repositoryId: 'REPO-008', + channelId: 'HLP-CHANNEL-0001', + sourceOwnerId: 'ICE-GL∞', + authorizerId: 'ICE-GL∞', + personaId: 'ICE-GL-ZY001', + executionRuntimeId: 'SYS-GLW-ZY-EXEC-0001', + target: 'JD-FD-PRIMARY', + }), { ok: false, reason: 'deployment_source_route_inactive' }) +}) + +test('rejects a human identity presented as the executing persona', () => { + assert.deepEqual(validateDeploymentSource(policy, { + distribution: 'personal', + repositoryId: 'REPO-008', + channelId: 'HLP-CHANNEL-0001', + sourceOwnerId: 'ICE-GL∞', + authorizerId: 'ICE-GL∞', + personaId: 'ICE-GL∞', + executionRuntimeId: 'SYS-GLW-ZY-EXEC-0001', + target: 'JD-FD-PRIMARY', + }), { ok: false, reason: 'deployment_persona_not_allowed' }) +})