Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ace/application/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@
IntelligenceBuilderPresentationService,
IntelligenceBuilderResourceProjectionReader,
IntelligenceOnboardingProfileAdmission,
IntelligenceOnboardingProfileV1Alpha1,
)
from ace.application.intelligence_ledger import (
PreparedIntelligenceAdmission,
Expand Down Expand Up @@ -482,6 +483,7 @@
"INTELLIGENCE_ONBOARDING_PROFILE_RECORD_KIND",
"IntelligenceBuilderPresentationService",
"IntelligenceBuilderResourceProjectionReader",
"IntelligenceOnboardingProfileV1Alpha1",
"IntelligenceOnboardingProfileAdmission",
"ActionResourceProjectionReader",
"RESOURCE_QUERY_AUTHORITY",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,5 +398,6 @@ async def read(
"INTELLIGENCE_ONBOARDING_PROFILE_RECORD_KIND",
"IntelligenceBuilderPresentationService",
"IntelligenceBuilderResourceProjectionReader",
"IntelligenceOnboardingProfileV1Alpha1",
"IntelligenceOnboardingProfileAdmission",
]
55 changes: 55 additions & 0 deletions core/engine/api/intelligence_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Read-only catalog of validated inert Intelligence starting profiles."""

from __future__ import annotations

from typing import Literal

from fastapi import APIRouter, Depends
from pydantic import BaseModel, ConfigDict

from core.engine.core.auth import get_current_user
from core.engine.core.installed_intelligence_catalog import (
IntelligenceOnboardingProfileV1Alpha1,
discover_installed_onboarding_profiles,
)

router = APIRouter(prefix="/v1/intelligence/catalog", tags=["intelligence-catalog"])


class InstalledIntelligenceProfileV1(BaseModel):
model_config = ConfigDict(extra="forbid")

distribution: str
distribution_version: str
resource_path: str
profile: IntelligenceOnboardingProfileV1Alpha1


class InstalledIntelligenceCatalogV1(BaseModel):
model_config = ConfigDict(extra="forbid")

contract: Literal["ace.http.installed-intelligence-catalog/v1alpha1"] = (
"ace.http.installed-intelligence-catalog/v1alpha1"
)
profiles: tuple[InstalledIntelligenceProfileV1, ...]


@router.get("/profiles", response_model=InstalledIntelligenceCatalogV1)
async def installed_profiles(user: dict = Depends(get_current_user)) -> InstalledIntelligenceCatalogV1:
"""List validated installed profiles; authentication never grants their proposed effects."""

del user
return InstalledIntelligenceCatalogV1(
profiles=tuple(
InstalledIntelligenceProfileV1(
distribution=item.distribution,
distribution_version=item.distribution_version,
resource_path=item.resource_path,
profile=item.profile,
)
for item in discover_installed_onboarding_profiles()
)
)


__all__ = ["InstalledIntelligenceCatalogV1", "InstalledIntelligenceProfileV1", "installed_profiles", "router"]
2 changes: 2 additions & 0 deletions core/engine/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ async def api_version():
from core.engine.api.foresight import router as foresight_router
from core.engine.api.intel import router as intel_router
from core.engine.api.intelligence_builds import router as intelligence_builds_router
from core.engine.api.intelligence_catalog import router as intelligence_catalog_router
from core.engine.api.intelligence_resources import router as intelligence_resources_router
from core.engine.api.landscape import router as landscape_router
from core.engine.api.product_state import router as product_state_router
Expand All @@ -676,6 +677,7 @@ async def api_version():
app.include_router(capture_router)
app.include_router(intel_router)
app.include_router(intelligence_builds_router)
app.include_router(intelligence_catalog_router)
app.include_router(intelligence_resources_router)
app.include_router(landscape_router)
app.include_router(product_state_router)
Expand Down
126 changes: 126 additions & 0 deletions core/engine/core/installed_intelligence_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Discover inert Intelligence onboarding profiles from installed distributions.

The host recognizes a validated resource shape, never a domain or package name.
Domain distributions remain JSON-only and do not gain executable entry points.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from importlib import metadata
from pathlib import Path, PurePosixPath
from typing import Iterable, Protocol

from pydantic import ValidationError

from ace.application import IntelligenceOnboardingProfileV1Alpha1

MAX_ONBOARDING_PROFILE_BYTES = 1_000_000
ONBOARDING_PROFILE_FILENAME = "onboarding_profile.json"


class InstalledIntelligenceCatalogError(RuntimeError):
"""Installed catalog material was unreadable, invalid, or conflicting."""


class InstalledDistribution(Protocol):
@property
def files(self): ...

@property
def metadata(self): ...

@property
def version(self) -> str: ...

def locate_file(self, path) -> Path: ...


@dataclass(frozen=True, slots=True)
class InstalledOnboardingProfile:
distribution: str
distribution_version: str
resource_path: str
profile: IntelligenceOnboardingProfileV1Alpha1


def _distribution_name(distribution: InstalledDistribution) -> str:
value = distribution.metadata.get("Name")
if not isinstance(value, str) or not value.strip():
raise InstalledIntelligenceCatalogError("installed distribution omitted its canonical name")
return value.strip()


def _profile_paths(distribution: InstalledDistribution) -> tuple:
paths = []
for candidate in distribution.files or ():
normalized = PurePosixPath(str(candidate).replace("\\", "/"))
if (
normalized.name == ONBOARDING_PROFILE_FILENAME
and "domain_packs" in normalized.parts
and ".dist-info" not in normalized.as_posix()
):
paths.append(candidate)
return tuple(sorted(paths, key=str))


def _load_profile(*, distribution: InstalledDistribution, candidate, name: str) -> InstalledOnboardingProfile:
resource_path = str(candidate).replace("\\", "/")
try:
path = Path(distribution.locate_file(candidate))
payload = path.read_bytes()
except OSError as exc:
raise InstalledIntelligenceCatalogError(
f"installed onboarding profile is unreadable: {name}:{resource_path}"
) from exc
if not payload or len(payload) > MAX_ONBOARDING_PROFILE_BYTES:
raise InstalledIntelligenceCatalogError(
f"installed onboarding profile exceeded its bounded size: {name}:{resource_path}"
)
try:
json.loads(payload)
profile = IntelligenceOnboardingProfileV1Alpha1.model_validate_json(payload)
except (UnicodeDecodeError, json.JSONDecodeError, ValidationError, TypeError, ValueError) as exc:
raise InstalledIntelligenceCatalogError(
f"installed onboarding profile failed exact validation: {name}:{resource_path}"
) from exc
return InstalledOnboardingProfile(
distribution=name,
distribution_version=str(distribution.version),
resource_path=resource_path,
profile=profile,
)


def discover_installed_onboarding_profiles(
distributions: Iterable[InstalledDistribution] | None = None,
) -> tuple[InstalledOnboardingProfile, ...]:
"""Return validated inert profiles with deterministic provenance and ordering."""

installed = metadata.distributions() if distributions is None else distributions
discovered: list[InstalledOnboardingProfile] = []
for distribution in sorted(installed, key=lambda item: _distribution_name(item).lower()):
name = _distribution_name(distribution)
for candidate in _profile_paths(distribution):
discovered.append(_load_profile(distribution=distribution, candidate=candidate, name=name))

by_id: dict[str, InstalledOnboardingProfile] = {}
for item in discovered:
incumbent = by_id.get(item.profile.profile_id)
if incumbent is None:
by_id[item.profile.profile_id] = item
continue
if incumbent.profile.profile_digest != item.profile.profile_digest:
raise InstalledIntelligenceCatalogError(
f"installed onboarding profile identity conflicts across distributions: {item.profile.profile_id}"
)
return tuple(sorted(by_id.values(), key=lambda item: (item.profile.domain_label or "", item.profile.profile_id)))


__all__ = [
"InstalledIntelligenceCatalogError",
"InstalledOnboardingProfile",
"IntelligenceOnboardingProfileV1Alpha1",
"discover_installed_onboarding_profiles",
]
27 changes: 27 additions & 0 deletions core/ui/canvas/src/api/intelligenceCatalogApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'

import { getToken } from './auth'
import { queryInstalledIntelligenceCatalog } from './intelligenceCatalogApi'

vi.mock('./auth', () => ({ clearToken: vi.fn(), getToken: vi.fn() }))

describe('queryInstalledIntelligenceCatalog', () => {
beforeEach(() => {
vi.mocked(getToken).mockReset()
vi.mocked(getToken).mockResolvedValue('personal-token')
})

test('reads installed profiles through the authenticated domain-neutral catalog', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
contract: 'ace.http.installed-intelligence-catalog/v1alpha1',
profiles: [{ distribution: 'pack', distribution_version: '1.0.0', resource_path: 'domain_packs/x/onboarding_profile.json', profile: {} }],
}), { status: 200, headers: { 'Content-Type': 'application/json' } })))

const result = await queryInstalledIntelligenceCatalog()

expect(result.profiles).toHaveLength(1)
expect(fetch).toHaveBeenCalledWith('/v1/intelligence/catalog/profiles', {
headers: { Authorization: 'Bearer personal-token' },
})
})
})
33 changes: 33 additions & 0 deletions core/ui/canvas/src/api/intelligenceCatalogApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { clearToken, getToken } from './auth'

const BASE = import.meta.env.VITE_API_BASE_URL ?? ''

export interface InstalledIntelligenceProfile {
readonly distribution: string
readonly distribution_version: string
readonly resource_path: string
readonly profile: unknown
}

export interface InstalledIntelligenceCatalog {
readonly contract: 'ace.http.installed-intelligence-catalog/v1alpha1'
readonly profiles: readonly InstalledIntelligenceProfile[]
}

async function getCatalog(token: string): Promise<Response> {
return fetch(`${BASE}/v1/intelligence/catalog/profiles`, {
headers: { Authorization: `Bearer ${token}` },
})
}

export async function queryInstalledIntelligenceCatalog(): Promise<InstalledIntelligenceCatalog> {
let token = await getToken()
let response = await getCatalog(token)
if (response.status === 401) {
clearToken()
token = await getToken()
response = await getCatalog(token)
}
if (!response.ok) throw new Error(`Installed Intelligence catalog is unavailable (${response.status}).`)
return (await response.json()) as InstalledIntelligenceCatalog
}
10 changes: 9 additions & 1 deletion core/ui/canvas/src/app/atrium/IntelligenceOS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from './intelligenceModel'
import { ResourceCard } from './ResourceCard'
import { useIntelligenceResources } from './useIntelligenceResources'
import { useInstalledIntelligenceCatalog } from './useInstalledIntelligenceCatalog'

type Surface = 'intelligence' | 'opportunities' | 'agents' | 'connections' | 'strategy'

Expand Down Expand Up @@ -422,11 +423,18 @@ export function IntelligenceOS() {
const surface = activeSurface(pathname)
const copy = SURFACE_COPY[surface]
const { page, loading, error, refresh, adoptPage } = useIntelligenceResources()
const installedCatalog = useInstalledIntelligenceCatalog()
const groups = useMemo(() => groupResources(page?.items ?? []), [page?.items])
const productName = productDisplayName(page?.product_id)
const freshness = pageFreshness(page)
const [onboardingOpen, setOnboardingOpen] = useState(false)
const onboardingProfiles = useMemo(() => onboardingProfilesFromResources(page?.items ?? []), [page?.items])
const onboardingProfiles = useMemo(
() => onboardingProfilesFromResources(
page?.items ?? [],
installedCatalog.map((item) => item.profile),
),
[installedCatalog, page?.items],
)
const onboardingSession = useMemo(() => onboardingSessionFromResources(page?.items ?? []), [page?.items])

function openFirstBrief() {
Expand Down
13 changes: 13 additions & 0 deletions core/ui/canvas/src/app/atrium/onboardingModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ describe('Atrium onboarding resources', () => {
expect(onboardingProfilesFromResources([]).map((item) => item.domain_label)).toEqual(['Custom Intelligence'])
})

it('adds validated installed profiles without replacing admitted product profiles', () => {
const installedWorld = { ...profile, profile_id: 'onboarding_profile:world', domain_label: 'World Intelligence' }
const installedDuplicate = { ...profile, domain_label: 'Installed duplicate' }
expect(onboardingProfilesFromResources(
[resource('builder_profile', profile)],
[installedWorld, installedDuplicate],
).map((item) => item.domain_label)).toEqual([
'Test domain',
'World Intelligence',
'Custom Intelligence',
])
})

it('uses the latest exact builder session revision', () => {
const older = resource('builder_session', { ...session, sequence: 6, stage: 'intelligence_model_approved' }, 6)
const latest = resource('builder_session', session, 7)
Expand Down
9 changes: 8 additions & 1 deletion core/ui/canvas/src/app/atrium/onboardingModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,10 @@ export function parseOnboardingProfile(value: unknown): IntelligenceOnboardingPr
* generic discovery agents rather than naming a domain. Exact duplicates are
* collapsed by profile identity; no domain name is hard-coded here.
*/
export function onboardingProfilesFromResources(items: readonly IntelligenceResourceRecord[]): readonly IntelligenceOnboardingProfile[] {
export function onboardingProfilesFromResources(
items: readonly IntelligenceResourceRecord[],
installedProfiles: readonly unknown[] = [],
): readonly IntelligenceOnboardingProfile[] {
const profiles = new Map<string, IntelligenceOnboardingProfile>()
for (const item of items) {
const payload = canonicalPayload(item.payload)
Expand All @@ -286,6 +289,10 @@ export function onboardingProfilesFromResources(items: readonly IntelligenceReso
if (profile !== null && !profiles.has(profile.profile_id)) profiles.set(profile.profile_id, profile)
}
}
for (const value of installedProfiles) {
const profile = parseOnboardingProfile(value)
if (profile !== null && !profiles.has(profile.profile_id)) profiles.set(profile.profile_id, profile)
}
profiles.set(FALLBACK_PROFILE.profile_id, FALLBACK_PROFILE)
return [...profiles.values()]
}
Expand Down
28 changes: 28 additions & 0 deletions core/ui/canvas/src/app/atrium/useInstalledIntelligenceCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react'

import {
queryInstalledIntelligenceCatalog,
type InstalledIntelligenceProfile,
} from '@/api/intelligenceCatalogApi'

export function useInstalledIntelligenceCatalog(): readonly InstalledIntelligenceProfile[] {
const [profiles, setProfiles] = useState<readonly InstalledIntelligenceProfile[]>([])

useEffect(() => {
let cancelled = false
queryInstalledIntelligenceCatalog()
.then((catalog) => {
if (!cancelled) setProfiles(catalog.profiles)
})
.catch(() => {
// Installed profiles enhance first use. Current admitted product
// profiles and Core's Custom starting point remain usable if the
// local catalog cannot be read.
})
return () => {
cancelled = true
}
}, [])

return profiles
}
Loading