feat(domain): bind runtime policy into signed manifests
This commit is contained in:
parent
6f0902846d
commit
fbff657d88
4 changed files with 189 additions and 5 deletions
|
|
@ -139,6 +139,14 @@ function validHandoff() {
|
||||||
const payload = {
|
const payload = {
|
||||||
domainId: DOMAIN_ID,
|
domainId: DOMAIN_ID,
|
||||||
repositoryId: 'REPO-014',
|
repositoryId: 'REPO-014',
|
||||||
|
runtimePolicy: {
|
||||||
|
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
|
||||||
|
forbiddenDataScopes: ['private:relationship-core'],
|
||||||
|
permissionPolicyRef: 'policy://origin-domain/default',
|
||||||
|
routeRef: 'domain-route://origin-domain/runtime',
|
||||||
|
themeOwner: 'fifth-domain',
|
||||||
|
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
|
||||||
|
},
|
||||||
schema: 'gh-aios.domain-manifest/v1',
|
schema: 'gh-aios.domain-manifest/v1',
|
||||||
signerId: 'GH-LIGHTHOUSE-001',
|
signerId: 'GH-LIGHTHOUSE-001',
|
||||||
sourceCommit: 'b'.repeat(40),
|
sourceCommit: 'b'.repeat(40),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ const COMMIT = 'b'.repeat(40);
|
||||||
const NODE_TYPE = 'local-terminal' as const;
|
const NODE_TYPE = 'local-terminal' as const;
|
||||||
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
||||||
function trustedSignerFromRegistry(overrides: Record<string, unknown> = {}): TrustedManifestSigner {
|
function trustedSignerFromRegistry(overrides: Record<string, unknown> = {}): TrustedManifestSigner {
|
||||||
|
const domainId = Array.isArray(overrides.domainIds) && typeof overrides.domainIds[0] === 'string'
|
||||||
|
? overrides.domainIds[0]
|
||||||
|
: 'DOM-FIFTH-0001';
|
||||||
const parsed = parseTrustedManifestSignerRegistry({
|
const parsed = parseTrustedManifestSignerRegistry({
|
||||||
registryId: 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001',
|
registryId: 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001',
|
||||||
schema: 'gh-aios.trusted-domain-manifest-signers/v1',
|
schema: 'gh-aios.trusted-domain-manifest-signers/v1',
|
||||||
|
|
@ -39,7 +42,7 @@ function trustedSignerFromRegistry(overrides: Record<string, unknown> = {}): Tru
|
||||||
sourceUrl: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart',
|
sourceUrl: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart',
|
||||||
});
|
});
|
||||||
const signer = resolveTrustedManifestSigner(parsed, {
|
const signer = resolveTrustedManifestSigner(parsed, {
|
||||||
domainId: 'DOM-FIFTH-0001',
|
domainId,
|
||||||
repositoryId: String(overrides.repositoryId ?? 'REPO-014'),
|
repositoryId: String(overrides.repositoryId ?? 'REPO-014'),
|
||||||
signerId: 'GH-LIGHTHOUSE-001',
|
signerId: 'GH-LIGHTHOUSE-001',
|
||||||
});
|
});
|
||||||
|
|
@ -50,6 +53,14 @@ const trustedSigner = trustedSignerFromRegistry();
|
||||||
const manifestPayload = {
|
const manifestPayload = {
|
||||||
domainId: 'DOM-FIFTH-0001',
|
domainId: 'DOM-FIFTH-0001',
|
||||||
repositoryId: 'REPO-014',
|
repositoryId: 'REPO-014',
|
||||||
|
runtimePolicy: {
|
||||||
|
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
|
||||||
|
forbiddenDataScopes: ['private:relationship-core'],
|
||||||
|
permissionPolicyRef: 'policy://origin-domain/default',
|
||||||
|
routeRef: 'domain-route://origin-domain/runtime',
|
||||||
|
themeOwner: 'fifth-domain',
|
||||||
|
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
|
||||||
|
},
|
||||||
schema: 'gh-aios.domain-manifest/v1',
|
schema: 'gh-aios.domain-manifest/v1',
|
||||||
signerId: 'GH-LIGHTHOUSE-001',
|
signerId: 'GH-LIGHTHOUSE-001',
|
||||||
sourceCommit: COMMIT,
|
sourceCommit: COMMIT,
|
||||||
|
|
@ -338,6 +349,76 @@ test('manifest tampering and signatures from another key fail closed', () => {
|
||||||
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the signed runtime policy rejects theme ownership, unknown fields and scope escalation', () => {
|
||||||
|
assert.equal(Object.isFrozen(verifiedHandoff.manifest.runtimePolicy), true);
|
||||||
|
assert.equal(Object.isFrozen(verifiedHandoff.manifest.runtimePolicy.allowedSessionScopes), true);
|
||||||
|
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
...validHandoff,
|
||||||
|
manifest: {
|
||||||
|
...validHandoff.manifest,
|
||||||
|
runtimePolicy: { ...validHandoff.manifest.runtimePolicy, themeOwner: 'domain-team' },
|
||||||
|
},
|
||||||
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
...validHandoff,
|
||||||
|
manifest: {
|
||||||
|
...validHandoff.manifest,
|
||||||
|
runtimePolicy: { ...validHandoff.manifest.runtimePolicy, decorativeOverride: true },
|
||||||
|
},
|
||||||
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
...validHandoff,
|
||||||
|
sessionCapability: { ...validHandoff.sessionCapability, scopes: ['domain:enter', 'relationship:private'] },
|
||||||
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime policy lists must be explicit, unique and bound by the signature', () => {
|
||||||
|
for (const runtimePolicy of [
|
||||||
|
{ ...validHandoff.manifest.runtimePolicy, allowedSessionScopes: [] },
|
||||||
|
{ ...validHandoff.manifest.runtimePolicy, allowedSessionScopes: ['domain:enter', 'domain:enter'] },
|
||||||
|
{ ...validHandoff.manifest.runtimePolicy, forbiddenDataScopes: [] },
|
||||||
|
]) {
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
...validHandoff,
|
||||||
|
manifest: { ...validHandoff.manifest, runtimePolicy },
|
||||||
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
...validHandoff,
|
||||||
|
manifest: {
|
||||||
|
...validHandoff.manifest,
|
||||||
|
runtimePolicy: { ...validHandoff.manifest.runtimePolicy, routeRef: 'domain-route://origin-domain/other' },
|
||||||
|
},
|
||||||
|
}, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an enterprise domain cannot reuse the Fifth Domain theme namespace', () => {
|
||||||
|
const enterpriseSigner = trustedSignerFromRegistry({ domainIds: ['DOM-PRIMARY-0001'] });
|
||||||
|
const enterprisePayload = {
|
||||||
|
...manifestPayload,
|
||||||
|
domainId: 'DOM-PRIMARY-0001',
|
||||||
|
runtimePolicy: {
|
||||||
|
...manifestPayload.runtimePolicy,
|
||||||
|
themeOwner: 'domain-team' as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const enterpriseBytes = domainManifestSigningBytes(enterprisePayload);
|
||||||
|
const enterpriseDigest = createHash('sha256').update(enterpriseBytes).digest('hex');
|
||||||
|
assert.throws(() => parseDomainAccessHandoff({
|
||||||
|
connectionReceipt: { ...validHandoff.connectionReceipt, domainId: enterprisePayload.domainId, manifestDigest: enterpriseDigest },
|
||||||
|
manifest: {
|
||||||
|
digest: enterpriseDigest,
|
||||||
|
...enterprisePayload,
|
||||||
|
signature: sign(null, enterpriseBytes, privateKey).toString('base64'),
|
||||||
|
},
|
||||||
|
sessionCapability: { ...validHandoff.sessionCapability, domainId: enterprisePayload.domainId },
|
||||||
|
}, enterprisePayload.domainId, 'LOCAL-001', NODE_TYPE, enterpriseSigner, NOW), /domain_access_handoff_invalid/);
|
||||||
|
});
|
||||||
|
|
||||||
test('trusted signer identity and repository are external inputs, not payload authority', () => {
|
test('trusted signer identity and repository are external inputs, not payload authority', () => {
|
||||||
assert.throws(() => parseDomainAccessHandoff(
|
assert.throws(() => parseDomainAccessHandoff(
|
||||||
validHandoff,
|
validHandoff,
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,27 @@ import {
|
||||||
const MANIFEST_SCHEMA = 'gh-aios.domain-manifest/v1' as const;
|
const MANIFEST_SCHEMA = 'gh-aios.domain-manifest/v1' as const;
|
||||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||||
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
||||||
|
const POLICY_SCOPE_PATTERN = /^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)?$/;
|
||||||
|
const POLICY_REFERENCE_PATTERN = /^[a-z][a-z0-9+.-]*:\/\/[A-Za-z0-9._~:/-]{1,240}$/;
|
||||||
const cryptographicallyVerifiedManifests = new WeakSet<object>();
|
const cryptographicallyVerifiedManifests = new WeakSet<object>();
|
||||||
|
|
||||||
export type DomainNodeType = 'local-terminal' | 'cloud-resident';
|
export type DomainNodeType = 'local-terminal' | 'cloud-resident';
|
||||||
|
export type DomainThemeOwner = 'domain-team' | 'fifth-domain';
|
||||||
|
|
||||||
|
export interface DomainRuntimeManifestPolicy {
|
||||||
|
allowedSessionScopes: readonly string[];
|
||||||
|
forbiddenDataScopes: readonly string[];
|
||||||
|
permissionPolicyRef: string;
|
||||||
|
routeRef: string;
|
||||||
|
themeOwner: DomainThemeOwner;
|
||||||
|
themePackageRef: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainManifestEvidence {
|
export interface DomainManifestEvidence {
|
||||||
digest: string;
|
digest: string;
|
||||||
domainId: string;
|
domainId: string;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
|
runtimePolicy: DomainRuntimeManifestPolicy;
|
||||||
schema: typeof MANIFEST_SCHEMA;
|
schema: typeof MANIFEST_SCHEMA;
|
||||||
signature: string;
|
signature: string;
|
||||||
signerId: string;
|
signerId: string;
|
||||||
|
|
@ -38,6 +51,7 @@ export interface DomainManifestEvidence {
|
||||||
export interface DomainManifestSignedPayload {
|
export interface DomainManifestSignedPayload {
|
||||||
domainId: string;
|
domainId: string;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
|
runtimePolicy: DomainRuntimeManifestPolicy;
|
||||||
schema: typeof MANIFEST_SCHEMA;
|
schema: typeof MANIFEST_SCHEMA;
|
||||||
signerId: string;
|
signerId: string;
|
||||||
sourceCommit: string;
|
sourceCommit: string;
|
||||||
|
|
@ -108,6 +122,18 @@ function isIdentifier(value: unknown): value is string {
|
||||||
return typeof value === 'string' && value.length > 0 && value.length <= 160;
|
return typeof value === 'string' && value.length > 0 && value.length <= 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPolicyReference(value: unknown): value is string {
|
||||||
|
return typeof value === 'string' && POLICY_REFERENCE_PATTERN.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPolicyScopeList(value: unknown): value is string[] {
|
||||||
|
return Array.isArray(value)
|
||||||
|
&& value.length > 0
|
||||||
|
&& value.length <= 64
|
||||||
|
&& value.every((scope) => typeof scope === 'string' && POLICY_SCOPE_PATTERN.test(scope))
|
||||||
|
&& new Set(value).size === value.length;
|
||||||
|
}
|
||||||
|
|
||||||
function invalidHandoff(): never {
|
function invalidHandoff(): never {
|
||||||
throw new Error('domain_access_handoff_invalid');
|
throw new Error('domain_access_handoff_invalid');
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +142,14 @@ export function domainManifestSigningBytes(payload: DomainManifestSignedPayload)
|
||||||
return Buffer.from(JSON.stringify({
|
return Buffer.from(JSON.stringify({
|
||||||
domainId: payload.domainId,
|
domainId: payload.domainId,
|
||||||
repositoryId: payload.repositoryId,
|
repositoryId: payload.repositoryId,
|
||||||
|
runtimePolicy: {
|
||||||
|
allowedSessionScopes: payload.runtimePolicy.allowedSessionScopes,
|
||||||
|
forbiddenDataScopes: payload.runtimePolicy.forbiddenDataScopes,
|
||||||
|
permissionPolicyRef: payload.runtimePolicy.permissionPolicyRef,
|
||||||
|
routeRef: payload.runtimePolicy.routeRef,
|
||||||
|
themeOwner: payload.runtimePolicy.themeOwner,
|
||||||
|
themePackageRef: payload.runtimePolicy.themePackageRef,
|
||||||
|
},
|
||||||
schema: payload.schema,
|
schema: payload.schema,
|
||||||
signerId: payload.signerId,
|
signerId: payload.signerId,
|
||||||
sourceCommit: payload.sourceCommit,
|
sourceCommit: payload.sourceCommit,
|
||||||
|
|
@ -140,9 +174,13 @@ export function parseDomainAccessHandoff(
|
||||||
const manifest = input.manifest;
|
const manifest = input.manifest;
|
||||||
const capability = input.sessionCapability;
|
const capability = input.sessionCapability;
|
||||||
const connection = input.connectionReceipt;
|
const connection = input.connectionReceipt;
|
||||||
if (!isRecord(manifest) || !hasExactKeys(manifest, ['digest', 'domainId', 'repositoryId', 'schema', 'signature', 'signerId', 'sourceCommit'])) invalidHandoff();
|
if (!isRecord(manifest) || !hasExactKeys(manifest, ['digest', 'domainId', 'repositoryId', 'runtimePolicy', 'schema', 'signature', 'signerId', 'sourceCommit'])) invalidHandoff();
|
||||||
if (!isRecord(capability) || !hasExactKeys(capability, ['capabilityId', 'domainId', 'expiresAt', 'nodeId', 'nodeType', 'scopes'])) invalidHandoff();
|
if (!isRecord(capability) || !hasExactKeys(capability, ['capabilityId', 'domainId', 'expiresAt', 'nodeId', 'nodeType', 'scopes'])) invalidHandoff();
|
||||||
if (!isRecord(connection) || !hasExactKeys(connection, ['connectionId', 'domainId', 'manifestDigest', 'nodeId', 'nodeType', 'receiptId', 'state'])) invalidHandoff();
|
if (!isRecord(connection) || !hasExactKeys(connection, ['connectionId', 'domainId', 'manifestDigest', 'nodeId', 'nodeType', 'receiptId', 'state'])) invalidHandoff();
|
||||||
|
const runtimePolicy = manifest.runtimePolicy;
|
||||||
|
if (!isRecord(runtimePolicy) || !hasExactKeys(runtimePolicy, ['allowedSessionScopes', 'forbiddenDataScopes', 'permissionPolicyRef', 'routeRef', 'themeOwner', 'themePackageRef'])) invalidHandoff();
|
||||||
|
const allowedSessionScopes = runtimePolicy.allowedSessionScopes;
|
||||||
|
const forbiddenDataScopes = runtimePolicy.forbiddenDataScopes;
|
||||||
|
|
||||||
const digest = manifest.digest;
|
const digest = manifest.digest;
|
||||||
if (manifest.schema !== MANIFEST_SCHEMA
|
if (manifest.schema !== MANIFEST_SCHEMA
|
||||||
|
|
@ -156,9 +194,21 @@ export function parseDomainAccessHandoff(
|
||||||
|| typeof manifest.sourceCommit !== 'string' || !COMMIT_PATTERN.test(manifest.sourceCommit)
|
|| typeof manifest.sourceCommit !== 'string' || !COMMIT_PATTERN.test(manifest.sourceCommit)
|
||||||
|| typeof manifest.signature !== 'string') invalidHandoff();
|
|| typeof manifest.signature !== 'string') invalidHandoff();
|
||||||
|
|
||||||
|
if (!isPolicyScopeList(allowedSessionScopes)
|
||||||
|
|| !allowedSessionScopes.includes('domain:enter')
|
||||||
|
|| !isPolicyScopeList(forbiddenDataScopes)
|
||||||
|
|| !isPolicyReference(runtimePolicy.permissionPolicyRef)
|
||||||
|
|| !isPolicyReference(runtimePolicy.routeRef)
|
||||||
|
|| !isPolicyReference(runtimePolicy.themePackageRef)
|
||||||
|
|| (runtimePolicy.themeOwner !== 'domain-team' && runtimePolicy.themeOwner !== 'fifth-domain')
|
||||||
|
|| (expectedDomainId === 'DOM-FIFTH-0001'
|
||||||
|
? runtimePolicy.themeOwner !== 'fifth-domain' || !runtimePolicy.themePackageRef.startsWith('theme://origin-domain/')
|
||||||
|
: runtimePolicy.themeOwner !== 'domain-team' || runtimePolicy.themePackageRef.startsWith('theme://origin-domain/'))) invalidHandoff();
|
||||||
|
|
||||||
const signedPayload = domainManifestSigningBytes({
|
const signedPayload = domainManifestSigningBytes({
|
||||||
domainId: manifest.domainId,
|
domainId: manifest.domainId,
|
||||||
repositoryId: manifest.repositoryId,
|
repositoryId: manifest.repositoryId,
|
||||||
|
runtimePolicy: runtimePolicy as unknown as DomainRuntimeManifestPolicy,
|
||||||
schema: manifest.schema,
|
schema: manifest.schema,
|
||||||
signerId: manifest.signerId,
|
signerId: manifest.signerId,
|
||||||
sourceCommit: manifest.sourceCommit,
|
sourceCommit: manifest.sourceCommit,
|
||||||
|
|
@ -180,7 +230,9 @@ export function parseDomainAccessHandoff(
|
||||||
|| typeof capability.expiresAt !== 'number' || !Number.isSafeInteger(capability.expiresAt) || capability.expiresAt <= now
|
|| typeof capability.expiresAt !== 'number' || !Number.isSafeInteger(capability.expiresAt) || capability.expiresAt <= now
|
||||||
|| !Array.isArray(capability.scopes)
|
|| !Array.isArray(capability.scopes)
|
||||||
|| capability.scopes.some((scope) => !isIdentifier(scope))
|
|| capability.scopes.some((scope) => !isIdentifier(scope))
|
||||||
|| !capability.scopes.includes('domain:enter')) invalidHandoff();
|
|| new Set(capability.scopes).size !== capability.scopes.length
|
||||||
|
|| !capability.scopes.includes('domain:enter')
|
||||||
|
|| capability.scopes.some((scope) => !allowedSessionScopes.includes(scope))) invalidHandoff();
|
||||||
|
|
||||||
if (!isIdentifier(connection.connectionId)
|
if (!isIdentifier(connection.connectionId)
|
||||||
|| !isIdentifier(connection.receiptId)
|
|| !isIdentifier(connection.receiptId)
|
||||||
|
|
@ -193,7 +245,21 @@ export function parseDomainAccessHandoff(
|
||||||
const handoff: DomainAccessHandoff = {
|
const handoff: DomainAccessHandoff = {
|
||||||
connectionReceipt: connection as unknown as DomainConnectionReceipt,
|
connectionReceipt: connection as unknown as DomainConnectionReceipt,
|
||||||
manifest: {
|
manifest: {
|
||||||
...(manifest as unknown as Omit<DomainManifestEvidence, 'verifierReceipt'>),
|
digest,
|
||||||
|
domainId: manifest.domainId as string,
|
||||||
|
repositoryId: manifest.repositoryId as string,
|
||||||
|
runtimePolicy: Object.freeze({
|
||||||
|
allowedSessionScopes: Object.freeze([...allowedSessionScopes]),
|
||||||
|
forbiddenDataScopes: Object.freeze([...forbiddenDataScopes]),
|
||||||
|
permissionPolicyRef: runtimePolicy.permissionPolicyRef as string,
|
||||||
|
routeRef: runtimePolicy.routeRef as string,
|
||||||
|
themeOwner: runtimePolicy.themeOwner as DomainThemeOwner,
|
||||||
|
themePackageRef: runtimePolicy.themePackageRef as string,
|
||||||
|
}),
|
||||||
|
schema: manifest.schema,
|
||||||
|
signature: manifest.signature,
|
||||||
|
signerId: manifest.signerId as string,
|
||||||
|
sourceCommit: manifest.sourceCommit,
|
||||||
verifierReceipt: {
|
verifierReceipt: {
|
||||||
manifestDigest: digest,
|
manifestDigest: digest,
|
||||||
signerId: manifest.signerId,
|
signerId: manifest.signerId,
|
||||||
|
|
@ -265,7 +331,8 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
|
||||||
|| capability.nodeId !== evidence.nodeId
|
|| capability.nodeId !== evidence.nodeId
|
||||||
|| capability.nodeType !== evidence.nodeType
|
|| capability.nodeType !== evidence.nodeType
|
||||||
|| capability.expiresAt <= now
|
|| capability.expiresAt <= now
|
||||||
|| !capability.scopes.includes('domain:enter')) {
|
|| !capability.scopes.includes('domain:enter')
|
||||||
|
|| capability.scopes.some((scope) => !manifest?.runtimePolicy.allowedSessionScopes.includes(scope))) {
|
||||||
blockers.push('scoped_session_capability_missing');
|
blockers.push('scoped_session_capability_missing');
|
||||||
}
|
}
|
||||||
const receipt = evidence.connectionReceipt;
|
const receipt = evidence.connectionReceipt;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# HoloLake 域 Manifest 运行策略契约
|
||||||
|
|
||||||
|
- 契约编号:`HLP-DOMAIN-MANIFEST-RUNTIME-POLICY-001`
|
||||||
|
- 阶段:`DEV-20260809-007 / Stage 007`
|
||||||
|
- 状态:实现验收基线
|
||||||
|
- 日期:2026-08-10
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
将“统一 GH-AIOS 公共灯塔 + 五个公开门厅 + 登录后进入五套独立域运行体”的域边界落到可验签的运行契约。路由、主题所有权、主题包和会话权限不能由页面临时状态决定,必须由受信签名者发布的域 Manifest 一并签名。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 域 Manifest 的签名内容必须绑定运行路由、主题所有权、主题包、权限策略引用、允许的会话 scope 和禁止的数据 scope;修改任一字段均使摘要或签名校验失败。
|
||||||
|
2. 第五域 · 光湖本源域只能声明 `fifth-domain` 主题所有权,并使用 `theme://origin-domain/` 命名空间;前四域只能声明 `domain-team`,且不能复用第五域主题命名空间。
|
||||||
|
3. 每个 Manifest 的允许 scope 必须包含 `domain:enter`,会话能力中的全部 scope 必须是该签名允许列表的子集。
|
||||||
|
4. 禁止的数据 scope 必须显式、非空、无重复;私人关系核心必须能作为禁止项由域策略隔离,不能因登录而默认开放。
|
||||||
|
5. 未知字段、空值、重复 scope、越权 scope、所有权错配、非法引用或任何签名内容篡改,均必须在加载域运行体之前 fail closed。
|
||||||
|
6. 验证后返回规范化且冻结的运行策略,不继续信任调用方传入的可变对象。
|
||||||
|
7. 本阶段代码完成、测试通过、桌面部署健康,只证明客户端具备契约守卫;在生产 Manifest、登记端点、交接端点和域会话不存在时,不得声称真实域运行体已经上线。
|
||||||
|
|
||||||
|
## 固定边界
|
||||||
|
|
||||||
|
- GH-AIOS 公共灯塔与通用外壳保持统一设计语法。
|
||||||
|
- 前四域的主题由各自主控团队发布并签名;HoloLake 不代替其定义视觉内容。
|
||||||
|
- 第五域主题由第五域持有,采用已选定的“湖面映星”方向。
|
||||||
|
- 公共门厅信息不等于登录后的域资源,Manifest 验签也不等于节点身份、私钥持有证明或在线连接回执。
|
||||||
|
|
||||||
Loading…
Reference in a new issue