feat(ai-workspace): support SEO codex provider bridge

This commit is contained in:
Codex
2026-07-03 08:43:37 +03:00
parent 1aceabee44
commit a7c6c34c82
26 changed files with 1649 additions and 36 deletions
@@ -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) {
+239 -9
View File
@@ -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,
+1
View File
@@ -349,6 +349,7 @@ export async function validateCatalog() {
contexts: contextBindings.contexts.length,
bindingTypes: contextBindings.bindingTypes.length,
bindings: contextBindings.bindings.length,
domainPackages: catalog.domainPackages.length,
},
}
}