hub-core/docs/owner-access-integration.md
tegwick f0eff0ac92
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / pytest-smoke (push) Failing after 4s
feat: atomically journal and deliver authorized native operation outcomes
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
2026-09-28 14:00:26 +02:00

13 KiB

HUB-WP-0012 owner integration review — 2026-09-28

The Hub foundation is committed as 3e38614. This follow-up delivers an Audit Core sender and runtime composition, and identifies the remaining source contracts. These are owner review inputs, not evidence of deployed grants.

Account, root entitlement and tenant facts

Owner surface reviewed Observed behavior Required integration
User Engine web.py, service.py::me /api/v1/me resolves (iss, sub) but creates User/Account/ExternalIdentity records if absent A side-effect-free authority lookup; Hub must not bootstrap identities to check access
User Engine service.py PLATFORM_TENANT = "platform:root"; platform-operator is an actor role Explicit mapping to IAM tenant:platform and the one immutable root principal; a role or string substitution is insufficient
User Engine tenant administration APIs Human/edge-oriented routes; no reviewed Hub workload lookup for current root entitlement Independently authenticated Hub workload, exact lookup scope and current entitlement provenance
Tenant Engine /tenants/{tenant_id} Current lifecycle and record version, authorized tenant.read Resolve immutable tenant ID versus canonical identifier; preserve owner version and lifecycle
Tenant Engine /tenants/{tenant_id}/roles/live Live role state with tenant.role.read.live, caller supplies an actor query Admit/authenticate the actual Hub workload and its actor assertion; do not mistake query text or a network path for caller authentication

The accepted integration must return identity references, account status, explicit current root entitlement, actor/target tenant status, observation times and owner evidence/version references. Unknown identity is denied without creating anything. Revocation must be visible on the next privileged read as well as write; no cached token role supplies current entitlement.

LiveFacts is the Hub-side normalized result, not a wire endpoint invented for an owner. Each source lookup must complete inside its timeout. The observation age is measured from the actual source observation and cannot be reset after slow downstream calls. All joined facts must still be at most five seconds old when policy/audit finish. Missing, ambiguous, inactive or stale facts deny. Producer aliases must come from an explicit owner binding to that identity.

T01/T02 require owner review of the lookup and root/tenant mapping before a production FactSource is configured. The runtime will not load fixture facts, query owner databases directly, forward Hub bearer tokens to other audiences, or use /me as an account-provisioning side effect.

Audit Core adapter and sender admission

AuditCoreSink implements the actual docs/event-envelope.md contract: POST /v1/events with eight fields, a distinct rotating sender bearer, and an Idempotency-Key matching the event ID. Source is exactly hub-core, tenant exactly tenant:platform. Only a matching 202 accepted or 200 duplicate with a nonempty archive reference counts as custody. Before each append, /readyz must report status=ok, durable=true, and custody class operational or its rollout alias archive. The entire append is bounded at three seconds, uses TLS, and never follows redirects.

Allow is blocked until the archive accepts the authorization record. A lost receipt blocks the business operation even if the attempt reached storage. This is a pre-execution authorization journal, not proof that an operation committed. Authorization cannot use a local success buffer or silent redaction. Native transaction outcomes now use a separate durable outbox; its production delivery and broader mutation coverage remain T03/T04 gates.

The exact verified signed decision is retained under data.signed_decision as serialized JSON so another serialization of the archive cannot reorder its Go struct fields. The verifier refuses secret-shaped field names before retention. No end-user bearer, message body or command body is emitted. Independent tests reverify the signed artifact after retrieval from the owner's receiver.

Proposed receiver registration (no credential values):

  • Name/source: hub-core; allowed tenants: only tenant:platform.
  • Write enabled, read disabled; distinct sender tokens with rotation overlap.
  • secret_policy=reject; authorization record classes hub.access.authorized, hub.access.denied, hub.access.refused.
  • Load-bearing authorization evidence: execution requires receipt. Owner review must settle exact emission-cadence/failure detection; no claim that this journal provides complete domain mutation evidence or archive tamper evidence.

Credential routing: warden route show audit-core-senders --json identifies ops-mason, subsystem OpenBao + audit-core, warden_executes=false, and currently resolvable=false. This route points to registry custody; it does not prove an admitted Hub sender or mint a credential. No secret was requested or retrieved.

Runtime assembly

SecuritySettings uses these HUB_CORE_SECURITY_ environment suffixes:

Suffix Value
ISSUER, AUDIENCE, ROOT_SUBJECT Admitted HTTPS issuer, Hub audience, immutable root subject
POLICY_URL, POLICY_CALLER Dedicated HTTPS PDP and exact admitted ServiceAccount principal
POLICY_TOKEN_FILE, POLICY_KEYS_FILE Absolute projected caller-token and trusted public-key paths
AUDIT_URL, AUDIT_TOKEN_FILE HTTPS receiver and separate absolute sender-token path

A host calls create_app(access_facts=reviewed_owner_adapter) in enforcement mode. It can alternatively supply an explicit SecuritySettings instance. The runtime owns and closes the shared HTTP client; proxy environment variables are not inherited. Partial/invalid composition is rejected. Without an explicit facts adapter, the standalone app stays closed even if security variables exist. No file-backed allowlist or dynamic arbitrary-module loader supplies authority.

Evidence boundary

The opt-in tests/test_audit_core_owner_contract.py exercises the actual Audit Core WSGI receiver and durable SQLite storage, including reopen, wrong-credential and lost-receipt cases. A test-only readiness fixture reports operational custody to exercise the production client's receipt checks; this is not native operational custody. Unmodified development custody is independently refused. A composed fixture journey verifies a real RSA JWT, fresh facts, an Ed25519 policy decision, receiver custody and native Hub write, followed by entitlement withdrawal denial. Real root enrollment/login, service deployment, live key and sender custody, and operational acceptance remain open.

Browser composition (source candidate)

Pass browser_settings=BrowserSettings(origin="https://hub.example", client_id="hub-browser", client_secret_file=Path("/run/secrets/hub-oidc-client")) to create_app, alongside the enforced controller or authoritative-facts security composition described above. Import BrowserSettings from hub_core.security.browser. Browser configuration cannot activate a development runtime or a missing controller. Its confidential-client credential is separate from the policy/audit workload credentials and is reread at each code exchange.

Register the exact HTTPS origin plus /auth/callback with the admitted issuer. The source candidate requires authorization code, S256 PKCE, RS256 ID tokens and client_secret_basic discovery support. It requests fresh authentication and AAL2; validated access-token assurance and current owner facts decide access. Issuer registration, actual MFA behavior, audience mapping and live owner acceptance remain deployment gates. The implementation follows the OIDC ID token validation contract and OAuth security best practices.

  • GET /auth/login initiates login; arbitrary query parameters are refused.
  • GET /auth/callback consumes browser-bound state once, exchanges the code with PKCE, checks signature/issuer/audience/nonce/authentication time/access binding, and requires root authorization and audit custody before issuing a session.
  • GET /auth/session rechecks authority and returns expiry and a CSRF token.
  • POST /auth/logout requires the session, exact Origin and X-Hub-CSRF, then destroys the local session even during identity/policy/audit outages.

Sessions contain access tokens only in server memory. Cookies carry random opaque IDs with Secure, HttpOnly, SameSite=Lax, host-only scope and / path. Sessions expire within five minutes or either token's expiry, whichever comes first; there is no refresh-token renewal. Process restart/shutdown invalidates sessions. Multiple workers need sticky routing or a separately reviewed shared session store; this candidate does not provide distributed sessions. Pending logins and sessions have bounded capacity. Production ingress must also rate-limit login.

All protected API requests recheck identity, current facts, signed policy and durable audit. Cookie-authenticated writes additionally require exact Origin and X-Hub-CSRF; browser clients obtain the latter from /auth/session. The bundled Swagger UI does not automatically inject that header. Mixed cookies and bearer credentials are rejected. Session expiry/logout is checked again after authority awaits and before dispatch. Logout cannot cancel already executing requests or log out the upstream identity provider; subsequent login requests fresh authentication. Logout is local invalidation, not a durable audited business mutation. Refused browser flows are recorded without codes/tokens/query parameters; audit failure returns 503. Login initiation does not require authority.

The app must observe the configured HTTPS origin. Do not trust arbitrary forwarded headers; configure an authenticated trusted proxy separately. Callback query strings are cleared before response-time application access logging, but reverse proxies and tracing collectors must independently suppress callback queries, cookies and authorization headers. No public listener is enabled by these source changes.

MCP caller composition and backend mapping

The standalone CLI now registers only four tools with equivalent runtime APIs:

MCP tool Runtime GET route
query_repository_navigation /ports/projections/repository-navigation/repositories
get_repository_navigation_facet /ports/projections/repository-navigation/facets/{facet_kind}/{facet_value}
query_workloads /ports/projections/workloads
resolve_workload_reference /ports/projections/workloads/resolve

These use exact paths without redirect-based slash normalization. The other 27 generic tools require embedded host APIs: state/domain orientation, message read/write/reply, capabilities/requests, repo/DOI operations, service/TPSC and legacy progress operations. They remain available in the SDK's default backend_profile="embedded"; they are not advertised by the standalone runtime profile. Native message/event commands are not equivalent replacements for legacy thread, recipient, author or event semantics. The inventory records each tool's backend profiles. Host-specific routing and caller admission remain T04/T05 work.

For a private, single-principal stdio process, an operator can project an already admitted Hub-audience credential and run:

HUB_CORE_ENV=production hub-core mcp --transport stdio \
  --api-base https://hub.example --token-file /run/secrets/hub-mcp-caller

The file is reread for every request, with bounded size and sanitized errors. Missing, invalid or expired credentials cannot fall back to anonymous/shared-root access. Expiry, issuer, audience and current grants are checked by the Hub API. The CLI neither obtains nor refreshes credentials; the projecting owner controls rotation. This example is a composition interface, not an issued grant or live acceptance. Do not share this process between principals or use an operator/root token as an automated workload identity.

Enforced CLI network transports refuse startup: a shared MCP endpoint needs a host that authenticates callers. --token-file is rejected for every network transport, including development. The host SDK composition supplies token_provider=current_invocation_hub_credential, require_credentials=True, and an explicit backend profile. The provider must resolve the authenticated invocation's Hub-audience credential; forwarding an MCP-audience token or putting a static root token in the callback is not an admitted composition. The SDK does not implement token exchange, delegation or host authentication.

Authenticated outbound requests require non-local HTTPS, never follow redirects, and ignore proxy environment variables. Tools never accept credentials as model arguments. Facet path segments reject traversal/separator/query injection. Tests exercise actual FastMCP tool invocation with distinct concurrent caller contexts, file rotation/removal, route-catalog admission for all four runtime tools, enforced backend denial and CLI transport restrictions. They do not claim that an external MCP consumer or workload has been admitted.