From 66229270e85232807a4448b62b92ce2fd92192db Mon Sep 17 00:00:00 2001 From: Edwin Amirian Date: Thu, 13 Aug 2026 15:00:55 -0700 Subject: [PATCH 1/2] Connect Atrium to governed intelligence builds --- core/engine/api/intelligence_builds.py | 43 +++ core/engine/api/main.py | 2 + core/engine/core/intelligence_build.py | 257 ++++++++++++++++++ .../src/api/intelligenceBuildsApi.test.ts | 72 +++++ .../canvas/src/api/intelligenceBuildsApi.ts | 77 ++++++ .../canvas/src/app/atrium/IntelligenceOS.tsx | 9 +- .../src/app/atrium/OnboardingPreview.tsx | 35 ++- .../app/atrium/useIntelligenceResources.ts | 7 +- tests/test_api_intelligence_builds.py | 182 +++++++++++++ 9 files changed, 680 insertions(+), 4 deletions(-) create mode 100644 core/engine/api/intelligence_builds.py create mode 100644 core/engine/core/intelligence_build.py create mode 100644 core/ui/canvas/src/api/intelligenceBuildsApi.test.ts create mode 100644 core/ui/canvas/src/api/intelligenceBuildsApi.ts create mode 100644 tests/test_api_intelligence_builds.py diff --git a/core/engine/api/intelligence_builds.py b/core/engine/api/intelligence_builds.py new file mode 100644 index 0000000..ca5f9d6 --- /dev/null +++ b/core/engine/api/intelligence_builds.py @@ -0,0 +1,43 @@ +"""HTTP transport for starting one governed personal Intelligence build.""" + +from fastapi import APIRouter, Depends, HTTPException, status + +from core.engine.core.auth import get_current_user +from core.engine.core.intelligence_build import ( + IntelligenceBuildContractConflict, + IntelligenceBuildDenied, + IntelligenceBuildHttpRuntime, + IntelligenceBuildResultV1, + IntelligenceBuildStartV1, + IntelligenceBuildUnauthenticated, + IntelligenceBuildUnavailable, + intelligence_build_runtime, + start_intelligence_build, +) + +router = APIRouter(prefix="/v1/intelligence/builds", tags=["intelligence-builds"]) + + +@router.post("/start", response_model=IntelligenceBuildResultV1) +async def start_build( + request: IntelligenceBuildStartV1, + user: dict = Depends(get_current_user), + runtime: IntelligenceBuildHttpRuntime = Depends(intelligence_build_runtime), +) -> IntelligenceBuildResultV1: + try: + return await start_intelligence_build(request=request, user=user, runtime=runtime) + except IntelligenceBuildUnauthenticated as exc: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Verified token lacks product scope" + ) from exc + except IntelligenceBuildDenied as exc: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Intelligence build denied") from exc + except IntelligenceBuildContractConflict as exc: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Intelligence build contract conflict" + ) from exc + except IntelligenceBuildUnavailable as exc: + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc + + +__all__ = ["router", "start_build"] diff --git a/core/engine/api/main.py b/core/engine/api/main.py index 62e16f2..12a57be 100644 --- a/core/engine/api/main.py +++ b/core/engine/api/main.py @@ -664,6 +664,7 @@ async def api_version(): from core.engine.api.extension_invocations import router as extension_invocations_router 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_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 @@ -674,6 +675,7 @@ async def api_version(): app.include_router(extension_invocations_router) app.include_router(capture_router) app.include_router(intel_router) +app.include_router(intelligence_builds_router) app.include_router(intelligence_resources_router) app.include_router(landscape_router) app.include_router(product_state_router) diff --git a/core/engine/core/intelligence_build.py b/core/engine/core/intelligence_build.py new file mode 100644 index 0000000..c17a877 --- /dev/null +++ b/core/engine/core/intelligence_build.py @@ -0,0 +1,257 @@ +"""Governed host boundary for starting one personal Intelligence build. + +Core owns authentication, authority use, and the stable HTTP material. A host +supplies the domain-neutral executor; domain repositories remain responsible +for source options, ontology vocabulary, watch policy, and briefing strategy. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from typing import Literal, Protocol + +from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator + +from ace.core import ImmutableRecordStore +from ace.core.contracts import canonical_hash +from ace.core.runtime_use import AuthorityUseReceiptV1Alpha1 +from ace.intelligence.contracts.resource_plane import IntelligenceResourcePageV1Alpha1 +from core.engine.core.agent_composition_runtime import ( + GovernedCompositionAuthorityError, + GovernedStateRuntimeUseResolver, + persist_task_authentication_receipt, +) +from core.engine.core.db import pool +from core.engine.core.governed_state import SurrealGovernedStateStore +from core.engine.core.immutable_records import SurrealImmutableRecordStore + +INTELLIGENCE_BUILD_AUTHORITY = "intelligence_build" +INTELLIGENCE_BUILD_OPERATION = "start_intelligence_build" +INTELLIGENCE_BUILD_RESULT_VERSION = "ace.http.intelligence-build-result/v1alpha1" + + +class IntelligenceBuildStartV1(BaseModel): + """One reviewed Atrium plan submitted for governed execution.""" + + model_config = ConfigDict(extra="forbid") + + authority_grant_ref: str = Field(min_length=1, max_length=240) + client_request_id: str = Field(min_length=1, max_length=240) + profile_id: str = Field(min_length=1, max_length=240) + subject: str = Field(min_length=8, max_length=2_000) + outcome_id: str = Field(min_length=1, max_length=240) + source_group_ids: tuple[str, ...] = Field(default_factory=tuple, max_length=64) + cadence_id: str = Field(min_length=1, max_length=240) + requested_at: datetime + + @field_validator("source_group_ids") + @classmethod + def _unique_source_groups(cls, value: tuple[str, ...]) -> tuple[str, ...]: + if len(value) != len(set(value)): + raise ValueError("source_group_ids must be unique") + return value + + +class IntelligenceBuildResultV1(BaseModel): + """Authorized result returned after a host completes or safely blocks a build.""" + + model_config = ConfigDict(extra="forbid") + + contract: Literal["ace.http.intelligence-build-result/v1alpha1"] = INTELLIGENCE_BUILD_RESULT_VERSION + build_id: str + request_digest: str + product_id: str + actor_ref: str + accepted_at: datetime + authority_use: AuthorityUseReceiptV1Alpha1 + resource_page: IntelligenceResourcePageV1Alpha1 + + +@dataclass(frozen=True, slots=True) +class AuthorizedIntelligenceBuild: + build_id: str + request_digest: str + product_id: str + actor_ref: str + request: IntelligenceBuildStartV1 + authority_use: AuthorityUseReceiptV1Alpha1 + + +class IntelligenceBuildExecutor(Protocol): + async def start(self, build: AuthorizedIntelligenceBuild) -> IntelligenceResourcePageV1Alpha1: ... + + +class IntelligenceBuildAuthorizationPort(Protocol): + async def resolve_authority_use( + self, + *, + context, + use_subject_ref: str, + use_subject_digest: str, + operation: str, + authority: str, + grant_ref: str, + evaluated_at: datetime, + ) -> AuthorityUseReceiptV1Alpha1: ... + + +@dataclass(frozen=True, slots=True) +class IntelligenceBuildHttpRuntime: + records: ImmutableRecordStore + authority: IntelligenceBuildAuthorizationPort + executor: IntelligenceBuildExecutor + + +class IntelligenceBuildError(RuntimeError): + """Base failure for the governed Intelligence build boundary.""" + + +class IntelligenceBuildDenied(IntelligenceBuildError): + """Current verified identity or Core authority denied the build.""" + + +class IntelligenceBuildUnauthenticated(IntelligenceBuildError): + """Verified token lacks a usable product-scoped identity.""" + + +class IntelligenceBuildUnavailable(IntelligenceBuildError): + """The host cannot currently execute or receipt a build.""" + + +class IntelligenceBuildContractConflict(IntelligenceBuildError): + """A host result did not preserve the authorized request.""" + + +class _UnavailableIntelligenceBuildExecutor: + async def start(self, build: AuthorizedIntelligenceBuild) -> IntelligenceResourcePageV1Alpha1: + del build + raise IntelligenceBuildUnavailable("no Intelligence build executor is registered") + + +def intelligence_build_runtime() -> IntelligenceBuildHttpRuntime: + records = SurrealImmutableRecordStore(pool) + governed_state = SurrealGovernedStateStore(pool) + return IntelligenceBuildHttpRuntime( + records=records, + authority=GovernedStateRuntimeUseResolver(governed_state=governed_state), + executor=_UnavailableIntelligenceBuildExecutor(), + ) + + +def _verified_claims(user: dict) -> tuple[str, str]: + actor_ref = user.get("sub") + product_id = user.get("product") + authorities = user.get("authorities") + if not isinstance(actor_ref, str) or not actor_ref or not isinstance(product_id, str) or not product_id: + raise IntelligenceBuildUnauthenticated("verified token lacks product scope") + if not isinstance(authorities, list) or INTELLIGENCE_BUILD_AUTHORITY not in authorities: + raise IntelligenceBuildDenied("Intelligence build authority is required") + return actor_ref, product_id + + +def _request_identity(*, request: IntelligenceBuildStartV1, product_id: str, actor_ref: str) -> tuple[str, str]: + material = request.model_dump(mode="json", exclude={"authority_grant_ref"}) + raw_digest = canonical_hash([product_id, actor_ref, material]) + return f"intelligence_build:{raw_digest[:32]}", f"sha256:{raw_digest}" + + +async def start_intelligence_build( + *, + request: IntelligenceBuildStartV1, + user: dict, + runtime: IntelligenceBuildHttpRuntime, +) -> IntelligenceBuildResultV1: + """Authorize one reviewed plan and delegate only its domain execution.""" + + actor_ref, product_id = _verified_claims(user) + evaluated_at = datetime.now(UTC) + if request.requested_at.tzinfo is None or request.requested_at.utcoffset() is None: + raise IntelligenceBuildContractConflict("requested_at must include a timezone") + if request.requested_at.astimezone(UTC) > evaluated_at + timedelta(minutes=5): + raise IntelligenceBuildContractConflict("requested_at cannot be materially in the future") + build_id, request_digest = _request_identity( + request=request, + product_id=product_id, + actor_ref=actor_ref, + ) + + try: + authentication = await persist_task_authentication_receipt( + claims={**user, "sub": actor_ref, "product": product_id}, + verified_at=evaluated_at, + store=runtime.records, + verification_policy_ref="jwt_verification_policy:v1", + ) + authority_use = await runtime.authority.resolve_authority_use( + context=authentication.runtime_context(), + use_subject_ref=build_id, + use_subject_digest=request_digest, + operation=INTELLIGENCE_BUILD_OPERATION, + authority=INTELLIGENCE_BUILD_AUTHORITY, + grant_ref=request.authority_grant_ref, + evaluated_at=evaluated_at, + ) + exact_authority = AuthorityUseReceiptV1Alpha1.model_validate(authority_use.model_dump(mode="python")) + if ( + exact_authority.product_id != product_id + or exact_authority.actor_ref != actor_ref + or exact_authority.use_subject_ref != build_id + or exact_authority.use_subject_digest != request_digest + or exact_authority.operation != INTELLIGENCE_BUILD_OPERATION + or exact_authority.authority != INTELLIGENCE_BUILD_AUTHORITY + or exact_authority.grant_ref != request.authority_grant_ref + ): + raise IntelligenceBuildContractConflict("authority resolver changed the Intelligence build request") + + page = IntelligenceResourcePageV1Alpha1.model_validate( + ( + await runtime.executor.start( + AuthorizedIntelligenceBuild( + build_id=build_id, + request_digest=request_digest, + product_id=product_id, + actor_ref=actor_ref, + request=request, + authority_use=exact_authority, + ) + ) + ).model_dump(mode="python") + ) + if page.product_id != product_id or page.actor_ref != actor_ref: + raise IntelligenceBuildContractConflict("build executor crossed the authenticated product scope") + return IntelligenceBuildResultV1( + build_id=build_id, + request_digest=request_digest, + product_id=product_id, + actor_ref=actor_ref, + accepted_at=evaluated_at, + authority_use=exact_authority, + resource_page=page, + ) + except GovernedCompositionAuthorityError as exc: + raise IntelligenceBuildDenied("current Core grant denied the build") from exc + except (ValidationError, TypeError, ValueError) as exc: + raise IntelligenceBuildContractConflict("Intelligence build result failed exact validation") from exc + except IntelligenceBuildError: + raise + except Exception as exc: + raise IntelligenceBuildUnavailable("Intelligence build execution is unavailable") from exc + + +__all__ = [ + "INTELLIGENCE_BUILD_AUTHORITY", + "INTELLIGENCE_BUILD_OPERATION", + "AuthorizedIntelligenceBuild", + "IntelligenceBuildContractConflict", + "IntelligenceBuildAuthorizationPort", + "IntelligenceBuildDenied", + "IntelligenceBuildExecutor", + "IntelligenceBuildHttpRuntime", + "IntelligenceBuildResultV1", + "IntelligenceBuildStartV1", + "IntelligenceBuildUnauthenticated", + "IntelligenceBuildUnavailable", + "intelligence_build_runtime", + "start_intelligence_build", +] diff --git a/core/ui/canvas/src/api/intelligenceBuildsApi.test.ts b/core/ui/canvas/src/api/intelligenceBuildsApi.test.ts new file mode 100644 index 0000000..cb5ebae --- /dev/null +++ b/core/ui/canvas/src/api/intelligenceBuildsApi.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { getToken } from './auth' +import { startIntelligenceBuild } from './intelligenceBuildsApi' + +vi.mock('./auth', () => ({ clearToken: vi.fn(), getToken: vi.fn() })) + +const result = { + contract: 'ace.http.intelligence-build-result/v1alpha1', + build_id: 'intelligence_build:test', + request_digest: `sha256:${'a'.repeat(64)}`, + product_id: 'product:test', + actor_ref: 'principal:test', + accepted_at: '2026-08-13T00:00:00Z', + authority_use: {}, + resource_page: { items: [], state: 'complete' }, +} + +describe('startIntelligenceBuild', () => { + beforeEach(() => { + vi.mocked(getToken).mockReset() + vi.mocked(getToken).mockResolvedValue('personal-token') + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(result), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }))) + }) + + test('submits the reviewed Atrium plan to the governed build boundary', async () => { + const response = await startIntelligenceBuild({ + profile_id: 'profile:world-ai', + subject: 'Keep me ahead of meaningful AI changes.', + outcome_id: 'outcome:decision-readiness', + source_group_ids: ['sources:official', 'sources:independent'], + cadence_id: 'cadence:daily', + }) + + expect(response.build_id).toBe('intelligence_build:test') + const [path, options] = vi.mocked(fetch).mock.calls[0] ?? [] + expect(path).toBe('/v1/intelligence/builds/start') + expect(options?.method).toBe('POST') + expect(options?.headers).toEqual(expect.objectContaining({ Authorization: 'Bearer personal-token' })) + const body = JSON.parse(String(options?.body)) as Record + expect(body).toEqual(expect.objectContaining({ + profile_id: 'profile:world-ai', + subject: 'Keep me ahead of meaningful AI changes.', + outcome_id: 'outcome:decision-readiness', + source_group_ids: ['sources:official', 'sources:independent'], + cadence_id: 'cadence:daily', + })) + expect(body.client_request_id).toMatch(/^atrium-request:/) + expect(body.requested_at).toEqual(expect.any(String)) + }) + + test('fails visibly when the host has no Intelligence build executor', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response( + JSON.stringify({ detail: 'no Intelligence build executor is registered' }), + { status: 503, headers: { 'Content-Type': 'application/json' } }, + ))) + + await expect(startIntelligenceBuild({ + profile_id: 'profile:custom', + subject: 'Track the decisions and changes that matter to me.', + outcome_id: 'outcome:custom', + source_group_ids: [], + cadence_id: 'cadence:weekly', + })).rejects.toMatchObject({ + status: 503, + message: 'no Intelligence build executor is registered', + }) + }) +}) diff --git a/core/ui/canvas/src/api/intelligenceBuildsApi.ts b/core/ui/canvas/src/api/intelligenceBuildsApi.ts new file mode 100644 index 0000000..507b9dd --- /dev/null +++ b/core/ui/canvas/src/api/intelligenceBuildsApi.ts @@ -0,0 +1,77 @@ +import { clearToken, getToken } from './auth' +import type { IntelligenceResourcePage } from './intelligenceResourcesApi' + +const BASE = import.meta.env.VITE_API_BASE_URL ?? '' +const BUILD_AUTHORITY_GRANT_REF = + import.meta.env.VITE_INTELLIGENCE_BUILD_AUTHORITY_GRANT_REF ?? + 'authority_grant:atrium-intelligence-build' + +export interface IntelligenceBuildStartInput { + readonly profile_id: string + readonly subject: string + readonly outcome_id: string + readonly source_group_ids: readonly string[] + readonly cadence_id: string +} + +export interface IntelligenceBuildResult { + readonly contract: 'ace.http.intelligence-build-result/v1alpha1' + readonly build_id: string + readonly request_digest: string + readonly product_id: string + readonly actor_ref: string + readonly accepted_at: string + readonly resource_page: IntelligenceResourcePage +} + +export class IntelligenceBuildApiError extends Error { + readonly status: number + + constructor(status: number, message: string) { + super(message) + this.name = 'IntelligenceBuildApiError' + this.status = status + } +} + +function requestId(): string { + if (typeof crypto.randomUUID === 'function') return `atrium-request:${crypto.randomUUID()}` + return `atrium-request:${Date.now().toString(36)}` +} + +async function postBuild(token: string, input: IntelligenceBuildStartInput): Promise { + return fetch(`${BASE}/v1/intelligence/builds/start`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + authority_grant_ref: BUILD_AUTHORITY_GRANT_REF, + client_request_id: requestId(), + ...input, + requested_at: new Date().toISOString(), + }), + }) +} + +export async function startIntelligenceBuild(input: IntelligenceBuildStartInput): Promise { + let token = await getToken() + let response = await postBuild(token, input) + if (response.status === 401) { + clearToken() + token = await getToken() + response = await postBuild(token, input) + } + if (!response.ok) { + let detail = `ACE could not start this Intelligence build (${response.status}).` + try { + const body = (await response.json()) as { detail?: string } + if (typeof body.detail === 'string') detail = body.detail + } catch { + // Keep the bounded status-only message for non-JSON failures. + } + throw new IntelligenceBuildApiError(response.status, detail) + } + return (await response.json()) as IntelligenceBuildResult +} diff --git a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx index 91874e2..02da756 100644 --- a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx +++ b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx @@ -20,6 +20,7 @@ import { } from 'lucide-react' import type { IntelligenceResourceRecord } from '@/api/intelligenceResourcesApi' +import { startIntelligenceBuild, type IntelligenceBuildStartInput } from '@/api/intelligenceBuildsApi' import { Alert, AlertDescription, AlertTitle } from '@/design/shadcn/ui/alert' import { Badge } from '@/design/shadcn/ui/badge' import { Button } from '@/design/shadcn/ui/button' @@ -420,7 +421,7 @@ export function IntelligenceOS() { const { pathname } = useLocation() const surface = activeSurface(pathname) const copy = SURFACE_COPY[surface] - const { page, loading, error, refresh } = useIntelligenceResources() + const { page, loading, error, refresh, adoptPage } = useIntelligenceResources() const groups = useMemo(() => groupResources(page?.items ?? []), [page?.items]) const productName = productDisplayName(page?.product_id) const freshness = pageFreshness(page) @@ -432,6 +433,11 @@ export function IntelligenceOS() { requestAnimationFrame(() => document.getElementById('latest-brief')?.scrollIntoView({ behavior: 'smooth' })) } + async function buildIntelligence(request: IntelligenceBuildStartInput) { + const result = await startIntelligenceBuild(request) + adoptPage(result.resource_page) + } + return (
@@ -510,6 +516,7 @@ export function IntelligenceOS() { onOpenChange={setOnboardingOpen} profiles={onboardingProfiles} session={onboardingSession} + onStartBuild={buildIntelligence} onOpenBrief={openFirstBrief} /> diff --git a/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx index 1faa797..451c1c3 100644 --- a/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx +++ b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx @@ -30,6 +30,7 @@ import { Button } from '@/design/shadcn/ui/button' import { Card, CardContent } from '@/design/shadcn/ui/card' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/design/shadcn/ui/dialog' import { Textarea } from '@/design/shadcn/ui/textarea' +import type { IntelligenceBuildStartInput } from '@/api/intelligenceBuildsApi' import type { IntelligenceBuilderSession, IntelligenceBuilderStage, @@ -149,12 +150,14 @@ export function OnboardingPreview({ onOpenChange, profiles, session, + onStartBuild, onOpenBrief, }: { readonly open: boolean readonly onOpenChange: (open: boolean) => void readonly profiles: readonly IntelligenceOnboardingProfile[] readonly session: IntelligenceBuilderSession | null + readonly onStartBuild: (request: IntelligenceBuildStartInput) => Promise readonly onOpenBrief: () => void }) { const [profileId, setProfileId] = useState(profiles[0].profile_id) @@ -169,6 +172,8 @@ export function OnboardingPreview({ const [sourceGroupIds, setSourceGroupIds] = useState(() => profile.source_groups.filter((group) => group.default_selected).map((group) => group.source_group_id), ) + const [buildPending, setBuildPending] = useState(false) + const [buildError, setBuildError] = useState(null) const outcome = useMemo(() => profile.outcomes.find((item) => item.outcome_id === outcomeId) ?? profile.outcomes[0], [outcomeId, profile.outcomes]) const selectedSourceGroups = useMemo( () => profile.source_groups.filter((group) => sourceGroupIds.includes(group.source_group_id)), @@ -209,6 +214,26 @@ export function OnboardingPreview({ function close(next: boolean) { onOpenChange(next) if (!next) setStep(0) + if (!next) setBuildError(null) + } + + async function build() { + setBuildPending(true) + setBuildError(null) + try { + await onStartBuild({ + profile_id: profile.profile_id, + subject: subject.trim(), + outcome_id: outcome.outcome_id, + source_group_ids: sourceGroupIds, + cadence_id: cadenceId, + }) + setStep(4) + } catch (reason: unknown) { + setBuildError(reason instanceof Error ? reason.message : 'ACE could not start this Intelligence build.') + } finally { + setBuildPending(false) + } } function finish() { @@ -225,7 +250,7 @@ export function OnboardingPreview({ Build your intelligence
- {activeSession === null ? 'Proposal only' : `Live · step ${activeSession.sequence}`} + {buildPending ? 'Building' : step < 4 ? 'Plan review' : activeSession === null ? 'Plan ready' : `Live · step ${activeSession.sequence}`}
@@ -422,12 +447,18 @@ export function OnboardingPreview({
)} + {buildError !== null && ( +
+ +
ACE paused before changing your intelligence.
{buildError}
+
+ )}
{step < 4 - ? + ? : }
diff --git a/core/ui/canvas/src/app/atrium/useIntelligenceResources.ts b/core/ui/canvas/src/app/atrium/useIntelligenceResources.ts index a308f6c..b45fc5c 100644 --- a/core/ui/canvas/src/app/atrium/useIntelligenceResources.ts +++ b/core/ui/canvas/src/app/atrium/useIntelligenceResources.ts @@ -10,6 +10,7 @@ interface IntelligenceResourcesState { readonly loading: boolean readonly error: Error | null readonly refresh: () => void + readonly adoptPage: (page: IntelligenceResourcePage) => void } export function useIntelligenceResources(): IntelligenceResourcesState { @@ -19,6 +20,10 @@ export function useIntelligenceResources(): IntelligenceResourcesState { const [revision, setRevision] = useState(0) const refresh = useCallback(() => setRevision((value) => value + 1), []) + const adoptPage = useCallback((nextPage: IntelligenceResourcePage) => { + setPage(nextPage) + setError(null) + }, []) useEffect(() => { let cancelled = false @@ -41,5 +46,5 @@ export function useIntelligenceResources(): IntelligenceResourcesState { } }, [revision]) - return { page, loading, error, refresh } + return { page, loading, error, refresh, adoptPage } } diff --git a/tests/test_api_intelligence_builds.py b/tests/test_api_intelligence_builds.py new file mode 100644 index 0000000..8656fb8 --- /dev/null +++ b/tests/test_api_intelligence_builds.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +import pytest +from fastapi import FastAPI +from httpx import ASGITransport, AsyncClient + +from ace.core import GovernedStateHeadPreconditionV1Alpha1 +from ace.core.runtime_use import AuthorityUseReceiptV1Alpha1 +from ace.intelligence.contracts.resource_plane import ( + IntelligenceResourcePageState, + IntelligenceResourcePageV1Alpha1, +) +from ace.testing import InMemoryImmutableRecordStore +from core.engine.api.intelligence_builds import router +from core.engine.core.auth import get_current_user +from core.engine.core.intelligence_build import ( + AuthorizedIntelligenceBuild, + IntelligenceBuildHttpRuntime, + intelligence_build_runtime, +) + +pytestmark = pytest.mark.unit + +NOW = datetime.now(UTC) +PRODUCT = "product:personal-intelligence" +ACTOR = "principal:personal-analyst" +GRANT = "authority_grant:personal-intelligence-build" + + +def _receipt(*, build: AuthorizedIntelligenceBuild | None, kwargs: dict) -> AuthorityUseReceiptV1Alpha1: + if build is None: + context = kwargs["context"] + subject_ref = kwargs["use_subject_ref"] + subject_digest = kwargs["use_subject_digest"] + operation = kwargs["operation"] + authority = kwargs["authority"] + grant_ref = kwargs["grant_ref"] + evaluated_at = kwargs["evaluated_at"] + else: + context = build.authority_use.authenticated_context + subject_ref = "intelligence_query:personal-first-picture" + subject_digest = "sha256:" + "e" * 64 + operation = "query_intelligence_resources" + authority = "observe_read" + grant_ref = "authority_grant:personal-intelligence-read" + evaluated_at = build.authority_use.evaluated_at + return AuthorityUseReceiptV1Alpha1( + product_id=PRODUCT, + actor_ref=ACTOR, + authenticated_context=context, + use_subject_ref=subject_ref, + use_subject_digest=subject_digest, + operation=operation, + authority=authority, + grant_ref=grant_ref, + grant_hash="b" * 64, + evaluated_at=evaluated_at, + expires_at=NOW + timedelta(hours=1), + state_head_precondition=GovernedStateHeadPreconditionV1Alpha1( + state_kind="authority_grant", + product_id=PRODUCT, + state_id=grant_ref, + sequence=1, + revision_id="authority_revision:personal-intelligence", + commit_receipt_id="authority_receipt:personal-intelligence", + ), + ) + + +class _Authority: + def __init__(self, *, deny: bool = False) -> None: + self.deny = deny + self.calls: list[dict] = [] + + async def resolve_authority_use(self, **kwargs) -> AuthorityUseReceiptV1Alpha1: + self.calls.append(kwargs) + if self.deny: + from core.engine.core.agent_composition_runtime import GovernedCompositionAuthorityError + + raise GovernedCompositionAuthorityError("inactive grant") + return _receipt(build=None, kwargs=kwargs) + + +class _Executor: + def __init__(self) -> None: + self.builds: list[AuthorizedIntelligenceBuild] = [] + + async def start(self, build: AuthorizedIntelligenceBuild) -> IntelligenceResourcePageV1Alpha1: + self.builds.append(build) + read_authority = _receipt(build=build, kwargs={}) + evaluated_at = read_authority.evaluated_at + return IntelligenceResourcePageV1Alpha1( + query_id=read_authority.use_subject_ref, + query_digest=read_authority.use_subject_digest, + product_id=PRODUCT, + actor_ref=ACTOR, + as_of=evaluated_at, + available_at=evaluated_at, + evaluated_at=evaluated_at, + state=IntelligenceResourcePageState.COMPLETE, + items=(), + authority_use=read_authority, + ) + + +def _claims(*, authorities: list[str] | None = None) -> dict: + return { + "sub": ACTOR, + "product": PRODUCT, + "authorities": ["intelligence_build"] if authorities is None else authorities, + "exp": (NOW + timedelta(hours=1)).timestamp(), + } + + +def _body() -> dict: + return { + "authority_grant_ref": GRANT, + "client_request_id": "atrium-request:first-picture", + "profile_id": "profile:world-ai", + "subject": "Keep me ahead of meaningful changes in artificial intelligence.", + "outcome_id": "outcome:decision-readiness", + "source_group_ids": ["sources:official", "sources:independent"], + "cadence_id": "cadence:daily", + "requested_at": NOW.isoformat(), + } + + +async def _request(*, claims: dict, authority: _Authority, executor: _Executor): + app = FastAPI() + app.include_router(router) + records = InMemoryImmutableRecordStore() + app.dependency_overrides[get_current_user] = lambda: claims + app.dependency_overrides[intelligence_build_runtime] = lambda: IntelligenceBuildHttpRuntime( + records=records, + authority=authority, + executor=executor, + ) + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post("/v1/intelligence/builds/start", json=_body()) + return response, records + + +@pytest.mark.asyncio +async def test_start_build_authorizes_exact_reviewed_plan_and_returns_resource_page() -> None: + authority = _Authority() + executor = _Executor() + response, records = await _request(claims=_claims(), authority=authority, executor=executor) + + assert response.status_code == 200 + body = response.json() + assert body["contract"] == "ace.http.intelligence-build-result/v1alpha1" + assert body["product_id"] == PRODUCT + assert body["actor_ref"] == ACTOR + assert body["resource_page"]["state"] == "complete" + assert executor.builds[0].request.subject == _body()["subject"] + assert executor.builds[0].request.source_group_ids == ("sources:official", "sources:independent") + assert authority.calls[0]["authority"] == "intelligence_build" + assert authority.calls[0]["operation"] == "start_intelligence_build" + assert any(record.record_kind == "task_authentication" for record in records.records.values()) + + +@pytest.mark.asyncio +async def test_start_build_requires_build_authority_and_current_core_grant() -> None: + denied_token, records = await _request(claims=_claims(authorities=[]), authority=_Authority(), executor=_Executor()) + assert denied_token.status_code == 403 + assert records.records == {} + + denied_grant, _ = await _request(claims=_claims(), authority=_Authority(deny=True), executor=_Executor()) + assert denied_grant.status_code == 403 + assert denied_grant.json()["detail"] == "Intelligence build denied" + + +def test_start_build_openapi_exposes_stable_request_and_result_contracts() -> None: + app = FastAPI() + app.include_router(router) + operation = app.openapi()["paths"]["/v1/intelligence/builds/start"]["post"] + request_schema = operation["requestBody"]["content"]["application/json"]["schema"] + response_schema = operation["responses"]["200"]["content"]["application/json"]["schema"] + assert request_schema["$ref"].endswith("IntelligenceBuildStartV1") + assert response_schema["$ref"].endswith("IntelligenceBuildResultV1") From ee74a2eda151b3eb72253fef6793a9f46b81be8d Mon Sep 17 00:00:00 2001 From: Edwin Amirian Date: Thu, 13 Aug 2026 15:28:14 -0700 Subject: [PATCH 2/2] Preserve the public Builder boundary --- core/engine/core/intelligence_build.py | 2 +- tests/test_public_core_boundaries.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/engine/core/intelligence_build.py b/core/engine/core/intelligence_build.py index c17a877..4c86ca9 100644 --- a/core/engine/core/intelligence_build.py +++ b/core/engine/core/intelligence_build.py @@ -13,10 +13,10 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator +from ace.application import IntelligenceResourcePageV1Alpha1 from ace.core import ImmutableRecordStore from ace.core.contracts import canonical_hash from ace.core.runtime_use import AuthorityUseReceiptV1Alpha1 -from ace.intelligence.contracts.resource_plane import IntelligenceResourcePageV1Alpha1 from core.engine.core.agent_composition_runtime import ( GovernedCompositionAuthorityError, GovernedStateRuntimeUseResolver, diff --git a/tests/test_public_core_boundaries.py b/tests/test_public_core_boundaries.py index a6507ff..a926a8c 100644 --- a/tests/test_public_core_boundaries.py +++ b/tests/test_public_core_boundaries.py @@ -107,6 +107,7 @@ def test_host_adapters_are_the_only_core_engine_edge_into_public_ace() -> None: "core/engine/core/governed_state.py", "core/engine/core/immutable_records.py", "core/engine/core/intelligence_resource_plane.py", + "core/engine/core/intelligence_build.py", "core/engine/core/live_cognition.py", "core/engine/core/action_execution.py", "core/engine/core/agent_composition_runtime.py",