feat(ai-workspace): support SEO codex provider bridge
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { readJson } from './catalog.mjs'
|
||||
import { loadCatalog, readJson } from './catalog.mjs'
|
||||
|
||||
const LAUNCHER_ROOT_GROUPS = new Set(['nodedc:superadmin', 'nodedc:launcher:admin'])
|
||||
const LAUNCHER_ROOT_ROLES = new Set(['root_admin'])
|
||||
const LAUNCHER_CLIENT_ADMIN_ROLES = new Set(['client_owner', 'client_admin'])
|
||||
|
||||
export async function loadAssistantAccessPolicy() {
|
||||
return readJson('catalog/assistant-access-policy.json')
|
||||
const catalog = await loadCatalog()
|
||||
return catalog.assistantAccessPolicy
|
||||
}
|
||||
|
||||
function normalizeString(value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
export const serviceRoot = path.resolve(__dirname, '..')
|
||||
export const catalogRoot = path.join(serviceRoot, 'catalog')
|
||||
export const domainPackagesRoot = path.join(catalogRoot, 'domain-packages')
|
||||
|
||||
export async function readJson(relativePath) {
|
||||
const fullPath = path.join(serviceRoot, relativePath)
|
||||
@@ -13,6 +14,229 @@ export async function readJson(relativePath) {
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
async function readJsonAt(fullPath) {
|
||||
const raw = await fs.readFile(fullPath, 'utf8')
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
async function readOptionalJsonAt(fullPath, fallback) {
|
||||
try {
|
||||
return await readJsonAt(fullPath)
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') return fallback
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function listDomainPackageDirs() {
|
||||
try {
|
||||
const entries = await fs.readdir(domainPackagesRoot, { withFileTypes: true })
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(domainPackagesRoot, entry.name))
|
||||
.sort()
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') return []
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueByValue(values) {
|
||||
return [...new Set(values)]
|
||||
}
|
||||
|
||||
function mergeRoleMatrix(baseMatrix = [], patchMatrix = []) {
|
||||
const byRole = new Map(baseMatrix.map((entry) => [entry.roleId, { ...entry }]))
|
||||
|
||||
for (const patch of patchMatrix || []) {
|
||||
const current = byRole.get(patch.roleId) || { roleId: patch.roleId }
|
||||
byRole.set(patch.roleId, {
|
||||
...current,
|
||||
...patch,
|
||||
allowedCapabilities: uniqueByValue([
|
||||
...(current.allowedCapabilities || []),
|
||||
...(patch.allowedCapabilities || []),
|
||||
]),
|
||||
deniedCapabilities: uniqueByValue([
|
||||
...(current.deniedCapabilities || []),
|
||||
...(patch.deniedCapabilities || []),
|
||||
]),
|
||||
deniedWhenNoAdminScope: uniqueByValue([
|
||||
...(current.deniedWhenNoAdminScope || []),
|
||||
...(patch.deniedWhenNoAdminScope || []),
|
||||
]),
|
||||
conditionalCapabilities: [
|
||||
...(current.conditionalCapabilities || []),
|
||||
...(patch.conditionalCapabilities || []),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return [...byRole.values()]
|
||||
}
|
||||
|
||||
function mergeAssistantAccessPolicy(base, patch = {}) {
|
||||
return {
|
||||
...base,
|
||||
roleVocabulary: [...(base.roleVocabulary || []), ...(patch.roleVocabulary || [])],
|
||||
roleAliases: [...(base.roleAliases || []), ...(patch.roleAliases || [])],
|
||||
sourceAuthorities: [...(base.sourceAuthorities || []), ...(patch.sourceAuthorities || [])],
|
||||
capabilities: [...(base.capabilities || []), ...(patch.capabilities || [])],
|
||||
roleMatrix: mergeRoleMatrix(base.roleMatrix, patch.roleMatrix),
|
||||
operations: [...(base.operations || []), ...(patch.operations || [])],
|
||||
hardDenies: [...(base.hardDenies || []), ...(patch.hardDenies || [])],
|
||||
}
|
||||
}
|
||||
|
||||
function mergeCatalog(base, extension) {
|
||||
const next = {
|
||||
...base,
|
||||
entities: {
|
||||
...base.entities,
|
||||
statusVocabulary: uniqueByValue([
|
||||
...(base.entities.statusVocabulary || []),
|
||||
...(extension.entities.statusVocabulary || []),
|
||||
]),
|
||||
entities: [...(base.entities.entities || []), ...(extension.entities.entities || [])],
|
||||
},
|
||||
relations: {
|
||||
...base.relations,
|
||||
relations: [...(base.relations.relations || []), ...(extension.relations.relations || [])],
|
||||
},
|
||||
aliases: {
|
||||
...base.aliases,
|
||||
aliases: [...(base.aliases.aliases || []), ...(extension.aliases.aliases || [])],
|
||||
},
|
||||
guardrails: {
|
||||
...base.guardrails,
|
||||
rules: [...(base.guardrails.rules || []), ...(extension.guardrails.rules || [])],
|
||||
blockedConflations: [
|
||||
...(base.guardrails.blockedConflations || []),
|
||||
...(extension.guardrails.blockedConflations || []),
|
||||
],
|
||||
},
|
||||
evidence: {
|
||||
...base.evidence,
|
||||
sourceRoots: [...(base.evidence.sourceRoots || []), ...(extension.evidence.sourceRoots || [])],
|
||||
ledgers: [...(base.evidence.ledgers || []), ...(extension.evidence.ledgers || [])],
|
||||
baselineDocs: [...(base.evidence.baselineDocs || []), ...(extension.evidence.baselineDocs || [])],
|
||||
restrictions: [...(base.evidence.restrictions || []), ...(extension.evidence.restrictions || [])],
|
||||
},
|
||||
resolverRules: {
|
||||
...base.resolverRules,
|
||||
rules: [...(base.resolverRules.rules || []), ...(extension.resolverRules.rules || [])],
|
||||
},
|
||||
assistantAccessPolicy: mergeAssistantAccessPolicy(
|
||||
base.assistantAccessPolicy,
|
||||
extension.assistantAccessPolicy,
|
||||
),
|
||||
assistantActions: {
|
||||
...base.assistantActions,
|
||||
apps: [...(base.assistantActions.apps || []), ...(extension.assistantActions.apps || [])],
|
||||
adapterStatuses: uniqueByValue([
|
||||
...(base.assistantActions.adapterStatuses || []),
|
||||
...(extension.assistantActions.adapterStatuses || []),
|
||||
]),
|
||||
actions: [...(base.assistantActions.actions || []), ...(extension.assistantActions.actions || [])],
|
||||
},
|
||||
assistantRiskPolicy: {
|
||||
...base.assistantRiskPolicy,
|
||||
riskLevels: [...(base.assistantRiskPolicy.riskLevels || []), ...(extension.assistantRiskPolicy.riskLevels || [])],
|
||||
confirmationModes: [
|
||||
...(base.assistantRiskPolicy.confirmationModes || []),
|
||||
...(extension.assistantRiskPolicy.confirmationModes || []),
|
||||
],
|
||||
selfActionPolicies: [
|
||||
...(base.assistantRiskPolicy.selfActionPolicies || []),
|
||||
...(extension.assistantRiskPolicy.selfActionPolicies || []),
|
||||
],
|
||||
hardDenies: [...(base.assistantRiskPolicy.hardDenies || []), ...(extension.assistantRiskPolicy.hardDenies || [])],
|
||||
globalRules: [
|
||||
...(base.assistantRiskPolicy.globalRules || []),
|
||||
...(extension.assistantRiskPolicy.globalRules || []),
|
||||
],
|
||||
},
|
||||
contextBindings: {
|
||||
...base.contextBindings,
|
||||
contextStatuses: uniqueByValue([
|
||||
...(base.contextBindings.contextStatuses || []),
|
||||
...(extension.contextBindings.contextStatuses || []),
|
||||
]),
|
||||
bindingStatuses: uniqueByValue([
|
||||
...(base.contextBindings.bindingStatuses || []),
|
||||
...(extension.contextBindings.bindingStatuses || []),
|
||||
]),
|
||||
contexts: [...(base.contextBindings.contexts || []), ...(extension.contextBindings.contexts || [])],
|
||||
bindingTypes: [
|
||||
...(base.contextBindings.bindingTypes || []),
|
||||
...(extension.contextBindings.bindingTypes || []),
|
||||
],
|
||||
bindings: [...(base.contextBindings.bindings || []), ...(extension.contextBindings.bindings || [])],
|
||||
},
|
||||
domainPackages: [...(base.domainPackages || []), ...(extension.domainPackages || [])],
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
async function loadDomainPackage(packageDir) {
|
||||
const relativeDir = path.relative(serviceRoot, packageDir)
|
||||
const metadata = await readOptionalJsonAt(path.join(packageDir, 'package.json'), {})
|
||||
|
||||
return {
|
||||
entities: await readOptionalJsonAt(path.join(packageDir, 'entities.json'), { statusVocabulary: [], entities: [] }),
|
||||
relations: await readOptionalJsonAt(path.join(packageDir, 'relations.json'), { relations: [] }),
|
||||
aliases: await readOptionalJsonAt(path.join(packageDir, 'aliases.json'), { aliases: [] }),
|
||||
guardrails: await readOptionalJsonAt(path.join(packageDir, 'guardrails.json'), { rules: [], blockedConflations: [] }),
|
||||
evidence: await readOptionalJsonAt(path.join(packageDir, 'evidence.json'), {
|
||||
sourceRoots: [],
|
||||
ledgers: [],
|
||||
baselineDocs: [],
|
||||
restrictions: [],
|
||||
}),
|
||||
resolverRules: await readOptionalJsonAt(path.join(packageDir, 'resolver-rules.json'), { rules: [] }),
|
||||
assistantAccessPolicy: await readOptionalJsonAt(path.join(packageDir, 'assistant-access-policy.json'), {
|
||||
roleVocabulary: [],
|
||||
roleAliases: [],
|
||||
sourceAuthorities: [],
|
||||
capabilities: [],
|
||||
roleMatrix: [],
|
||||
operations: [],
|
||||
hardDenies: [],
|
||||
}),
|
||||
assistantActions: await readOptionalJsonAt(path.join(packageDir, 'assistant-actions.json'), {
|
||||
apps: [],
|
||||
adapterStatuses: [],
|
||||
actions: [],
|
||||
}),
|
||||
assistantRiskPolicy: await readOptionalJsonAt(path.join(packageDir, 'assistant-risk-policy.json'), {
|
||||
riskLevels: [],
|
||||
confirmationModes: [],
|
||||
selfActionPolicies: [],
|
||||
hardDenies: [],
|
||||
globalRules: [],
|
||||
}),
|
||||
contextBindings: await readOptionalJsonAt(path.join(packageDir, 'context-bindings.json'), {
|
||||
contextStatuses: [],
|
||||
bindingStatuses: [],
|
||||
contexts: [],
|
||||
bindingTypes: [],
|
||||
bindings: [],
|
||||
}),
|
||||
domainPackages: [{
|
||||
id: metadata.id || path.basename(packageDir),
|
||||
status: metadata.status || 'unknown',
|
||||
relativePath: relativeDir,
|
||||
summary: metadata.summary || '',
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDomainPackages() {
|
||||
const packageDirs = await listDomainPackageDirs()
|
||||
return Promise.all(packageDirs.map((packageDir) => loadDomainPackage(packageDir)))
|
||||
}
|
||||
|
||||
export async function loadCatalog() {
|
||||
const [
|
||||
entities,
|
||||
@@ -37,15 +261,8 @@ export async function loadCatalog() {
|
||||
])
|
||||
const contextBindings = await readJson('catalog/context-bindings.json')
|
||||
|
||||
const entityById = new Map(entities.entities.map((entity) => [entity.id, entity]))
|
||||
const relationById = new Map(relations.relations.map((relation) => [relation.id, relation]))
|
||||
const contextById = new Map(contextBindings.contexts.map((context) => [context.id, context]))
|
||||
const bindingTypeById = new Map(contextBindings.bindingTypes.map((bindingType) => [bindingType.id, bindingType]))
|
||||
const aliasByKey = new Map(
|
||||
aliases.aliases.map((alias) => [normalizeAlias(alias.alias), alias.canonicalId]),
|
||||
)
|
||||
|
||||
return {
|
||||
const domainPackages = await loadDomainPackages()
|
||||
const mergedCatalog = domainPackages.reduce((catalog, extension) => mergeCatalog(catalog, extension), {
|
||||
entities,
|
||||
relations,
|
||||
aliases,
|
||||
@@ -56,6 +273,19 @@ export async function loadCatalog() {
|
||||
assistantActions,
|
||||
assistantRiskPolicy,
|
||||
contextBindings,
|
||||
domainPackages: [],
|
||||
})
|
||||
|
||||
const entityById = new Map(mergedCatalog.entities.entities.map((entity) => [entity.id, entity]))
|
||||
const relationById = new Map(mergedCatalog.relations.relations.map((relation) => [relation.id, relation]))
|
||||
const contextById = new Map(mergedCatalog.contextBindings.contexts.map((context) => [context.id, context]))
|
||||
const bindingTypeById = new Map(mergedCatalog.contextBindings.bindingTypes.map((bindingType) => [bindingType.id, bindingType]))
|
||||
const aliasByKey = new Map(
|
||||
mergedCatalog.aliases.aliases.map((alias) => [normalizeAlias(alias.alias), alias.canonicalId]),
|
||||
)
|
||||
|
||||
return {
|
||||
...mergedCatalog,
|
||||
entityById,
|
||||
relationById,
|
||||
contextById,
|
||||
|
||||
@@ -349,6 +349,7 @@ export async function validateCatalog() {
|
||||
contexts: contextBindings.contexts.length,
|
||||
bindingTypes: contextBindings.bindingTypes.length,
|
||||
bindings: contextBindings.bindings.length,
|
||||
domainPackages: catalog.domainPackages.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user