feat(domain): project verified runtime policy to desktop

This commit is contained in:
冰朔 2026-08-10 10:25:25 +08:00
commit 683e135f5e
12 changed files with 224 additions and 25 deletions

View file

@ -294,6 +294,11 @@ test('current registries, signed node claim and verified handoff project runtime
assert.equal(status.stage, 'runtime-ready');
assert.equal(status.runtimeReady, true);
assert.deepEqual(status.blockers, []);
assert.equal(status.runtimePolicy?.themeOwner, 'fifth-domain');
assert.equal(status.runtimePolicy?.themePackageRef, 'theme://origin-domain/lake-reflects-stars');
assert.equal(status.runtimePolicy?.routeRef, 'domain-route://origin-domain/runtime');
assert.deepEqual(status.runtimePolicy?.allowedSessionScopes, ['domain:enter', 'knowledge:read']);
assert.doesNotMatch(JSON.stringify(status.runtimePolicy), /signature|signer|repository|publicKey/i);
});
test('source failures are reduced to safe status without raw details', async () => {

View file

@ -4,6 +4,7 @@ import {
type DomainAccessEvidence,
type DomainNodeType,
type DomainAccessStatus,
type DomainRuntimePolicyProjection,
} from './domain-access.js';
import {
TrustedSignerSnapshotLoader,
@ -57,6 +58,7 @@ export interface DomainAccessProjection {
nodeId: string;
nodeType: DomainNodeType;
nodeRegistrationSource: Readonly<NodeRegistrationSnapshotReceipt>;
runtimePolicy?: DomainRuntimePolicyProjection;
runtimeReady: boolean;
stage: DomainAccessStatus['stage'];
trustSource: Readonly<TrustedSignerSnapshotReceipt>;

View file

@ -208,6 +208,27 @@ test('runtime access requires matching manifest, scoped capability and online re
assert.equal(status.runtimeReady, true);
assert.equal(status.stage, 'runtime-ready');
assert.deepEqual(status.blockers, []);
assert.deepEqual(status.runtimePolicy, {
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
forbiddenDataScopes: ['private:relationship-core'],
manifestDigest: DIGEST,
permissionPolicyRef: 'policy://origin-domain/default',
routeRef: 'domain-route://origin-domain/runtime',
themeOwner: 'fifth-domain',
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
});
assert.equal(Object.isFrozen(status.runtimePolicy), true);
assert.equal(Object.isFrozen(status.runtimePolicy?.allowedSessionScopes), true);
assert.deepEqual(Object.keys(status.runtimePolicy ?? {}).sort(), [
'allowedSessionScopes',
'forbiddenDataScopes',
'manifestDigest',
'permissionPolicyRef',
'routeRef',
'themeOwner',
'themePackageRef',
]);
assert.doesNotMatch(JSON.stringify(status.runtimePolicy), /signature|signer|repository|sourceCommit|publicKey/i);
});
test('the runtime handoff entry resolves its signer only from a loaded snapshot', async () => {
@ -302,6 +323,7 @@ test('mismatched or expired evidence fails closed', () => {
sessionCapability: { capabilityId: 'CAP-001', domainId: 'DOM-FIFTH-0001', expiresAt: NOW, nodeId: 'LOCAL-001', nodeType: NODE_TYPE, scopes: ['domain:enter'] },
}, NOW);
assert.equal(status.runtimeReady, false);
assert.equal(status.runtimePolicy, undefined);
assert.ok(status.blockers.includes('scoped_session_capability_missing'));
assert.ok(status.blockers.includes('matching_connection_receipt_missing'));
});
@ -446,5 +468,6 @@ test('copying verified-looking fields cannot bypass the in-process verifier boun
sessionCapability: verifiedHandoff.sessionCapability,
}, NOW);
assert.equal(status.runtimeReady, false);
assert.equal(status.runtimePolicy, undefined);
assert.ok(status.blockers.includes('verified_domain_manifest_missing'));
});

View file

@ -99,12 +99,23 @@ export interface DomainAccessEvidence {
sessionCapability?: DomainSessionCapability;
}
export interface DomainRuntimePolicyProjection {
allowedSessionScopes: readonly string[];
forbiddenDataScopes: readonly string[];
manifestDigest: string;
permissionPolicyRef: string;
routeRef: string;
themeOwner: DomainThemeOwner;
themePackageRef: string;
}
export interface DomainAccessStatus {
blockers: string[];
domainId: string;
localWorkspaceAllowed: true;
nodeId: string;
nodeType: DomainNodeType;
runtimePolicy?: DomainRuntimePolicyProjection;
runtimeReady: boolean;
stage: 'login-required' | 'identity-verified' | 'runtime-ready';
}
@ -347,12 +358,24 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
blockers.push('matching_connection_receipt_missing');
}
const runtimeReady = blockers.length === 0;
const runtimePolicy = runtimeReady && manifest
? Object.freeze({
allowedSessionScopes: Object.freeze([...manifest.runtimePolicy.allowedSessionScopes]),
forbiddenDataScopes: Object.freeze([...manifest.runtimePolicy.forbiddenDataScopes]),
manifestDigest: manifest.digest,
permissionPolicyRef: manifest.runtimePolicy.permissionPolicyRef,
routeRef: manifest.runtimePolicy.routeRef,
themeOwner: manifest.runtimePolicy.themeOwner,
themePackageRef: manifest.runtimePolicy.themePackageRef,
})
: undefined;
return {
blockers,
domainId: evidence.domainId,
localWorkspaceAllowed: true,
nodeId: evidence.nodeId,
nodeType: evidence.nodeType,
...(runtimePolicy ? { runtimePolicy } : {}),
runtimeReady,
stage: runtimeReady
? 'runtime-ready'