mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
7a06f5e8bc
* refactor(session): make ModelLane the provider boundary (#979) ## Summary This closes the model-lane ownership gap left by #832: `ChatSession` no longer stores raw provider/client handles. `ResolvedModelBinding` now carries the provider, client, model, capabilities, registry generation, and backend-auth configuration as one coherent snapshot. - Atomically rebind existing sessions after model-registry changes while pinning each in-flight send, fallback, judge, output guard, task agent, title, compaction, perception, and voice operation to its initiating principal and binding. - Fence UI publication, canonical trajectory folds, durable writes, streams, retries, child scopes, and judge work by generation. Stop can hand off to a successor without accepting late state; cancelled tools retain typed effect receipts, and concurrent approval batches resolve by exact cycle or call. - Make create, fork, open, close, and delete race-safe with hidden `creating` reservations, incarnation-aware state tails, and an ACL-rechecked transaction that clones checkpoint-bounded history, configuration, project/persona state, and attachment references. - Extend REST/OpenAPI and Python/TypeScript SDK contracts for create/fork inputs, routed-create metadata, live-workstream probes, targeted approvals, and structured cancellation results. - Update architecture, storage, authentication, judge, channel, console, API, and SDK documentation, including regenerated architecture diagrams and OpenAPI artifacts. ## Validation - SQLite suite: 11,188 passed, 9 skipped, 10 deselected - PostgreSQL suite: 11,195 passed, 2 skipped, 10 deselected - Live backend: 3 passed - SSE recovery: 6 passed; browser recovery harness passed all scenarios - Ruff: clean; 595 files correctly formatted - mypy: 243 source files clean - TypeScript: typecheck/build and 35 tests passed - OpenAPI artifacts fresh; all 14 changed diagrams reproduce byte-for-byte - `git diff --check` and Git LFS integrity clean Closes #979. * fix(deps): update nanoid for GHSA-2v37-7h3g-55p8 Refresh the transitive lock entry admitted by PostCSS so the TypeScript security gate no longer resolves the vulnerable custom-generator implementation. Validation: - npm ci - npm audit --audit-level=moderate: 0 vulnerabilities - TypeScript typecheck and build - TypeScript tests: 35 passed * fix(test): assert canonical model registry URLs Replace prefix checks with exact canonical base URL assertions so the tests do not model incomplete URL validation. Validation: tests/test_model_registry.py (185 passed); Ruff check/format; mypy.
154 lines
4.6 KiB
Plaintext
154 lines
4.6 KiB
Plaintext
@startuml
|
|
!theme plain
|
|
title Turnstone — User Authentication and Model-Backend Credentials
|
|
|
|
skinparam class {
|
|
BackgroundColor<<core>> #E8EAF6
|
|
BackgroundColor<<token>> #C8E6C9
|
|
BackgroundColor<<storage>> #B3E5FC
|
|
BackgroundColor<<runtime>> #FFE0B2
|
|
BackgroundColor<<model>> #F3E5F5
|
|
}
|
|
|
|
package "Request identity" {
|
|
class "AuthMiddleware / check_request()" as RequestAuth <<core>> {
|
|
Extract bearer or HttpOnly cookie
|
|
Validate audience + expiry
|
|
Check scope / permission
|
|
Publish AuthResult in request state
|
|
}
|
|
|
|
class "AuthResult" as AuthResult <<core>> {
|
|
+ user_id: str
|
|
+ scopes: frozenset[str]
|
|
+ permissions: frozenset[str]
|
|
+ token_source: str
|
|
}
|
|
|
|
class "JWT" as JWT <<token>> {
|
|
HS256, sub, aud, iat, exp
|
|
console proxy mints short-lived
|
|
server-audience identity
|
|
}
|
|
|
|
class "API / config token" as ApiToken <<token>> {
|
|
ts_* token: SHA-256 DB lookup
|
|
config token: constant-time compare
|
|
}
|
|
|
|
class "users / roles / api_tokens" as UserTables <<storage>> {
|
|
password hash + token hash
|
|
role-derived permissions
|
|
}
|
|
}
|
|
|
|
package "Immutable model binding" {
|
|
class "ModelRegistry" as Registry <<model>> {
|
|
+ resolve_binding(alias)
|
|
+ generation: int
|
|
--
|
|
Atomically resolves client, provider,
|
|
model, ModelConfig, generation.
|
|
}
|
|
|
|
class "ModelConfig snapshot" as ModelConfig <<model>> {
|
|
+ alias / provider / endpoint / static key
|
|
+ auth_mode
|
|
+ obo_audience
|
|
+ obo_scopes
|
|
--
|
|
static | entra_obo | entra_app | rfc8693_obo
|
|
}
|
|
|
|
class "ModelLane" as Lane <<model>> {
|
|
+ client / provider / model / capabilities
|
|
+ backend_auth_config: ModelConfig
|
|
+ backend_auth_resolver: Callable
|
|
}
|
|
|
|
class "Model definitions" as ModelTable <<storage>> {
|
|
DB + config-file definitions
|
|
encrypted protected fields
|
|
}
|
|
}
|
|
|
|
package "Per-call credential resolution" {
|
|
class "resolve_model_backend_auth_token()" as Resolver <<runtime>> {
|
|
+ alias + pinned ModelConfig
|
|
+ initiating principal_id
|
|
+ ConfigStore + mint client
|
|
→ dynamic token | None | fail closed
|
|
}
|
|
|
|
class "Model mint client" as Mint <<runtime>> {
|
|
+ mint_model_obo_token_sync(...)
|
|
+ mint_app_token_sync(...)
|
|
--
|
|
Cached by alias / principal / grant leg;
|
|
retains refusal cause for diagnostics.
|
|
}
|
|
|
|
class "OIDC / OBO protected state" as OBOState <<storage>> {
|
|
encrypted user refresh credential
|
|
deployment Fernet key
|
|
configured grant profile
|
|
}
|
|
|
|
class "lane_call_client()" as CallClient <<runtime>> {
|
|
cancel check before mint
|
|
resolve once per plant call
|
|
cancel check after mint
|
|
client.with_options(api_key=token)
|
|
}
|
|
|
|
class "Provider SDK request" as ProviderCall <<runtime>> {
|
|
Anthropic: x-api-key
|
|
OpenAI-style: Authorization Bearer
|
|
}
|
|
}
|
|
|
|
RequestAuth --> JWT : validates
|
|
RequestAuth --> ApiToken : validates
|
|
RequestAuth --> UserTables : lookup + permissions
|
|
RequestAuth --> AuthResult : returns
|
|
|
|
ModelTable --> Registry : load / hot reload
|
|
Registry --> ModelConfig : immutable snapshot
|
|
Registry --> Lane : coherent binding
|
|
|
|
AuthResult --> Resolver : initiating principal
|
|
Lane --> Resolver : callable + pinned config
|
|
Resolver --> Mint : dynamic modes only
|
|
Mint --> OBOState : decrypt / grant policy
|
|
CallClient --> Lane
|
|
CallClient --> Resolver
|
|
CallClient --> ProviderCall : cloned SDK client
|
|
|
|
note right of Resolver
|
|
**Mode policy**
|
|
• static: return None; registry client's explicit key remains.
|
|
• entra_obo / rfc8693_obo: require an effective principal. HTTP
|
|
turns pin the authenticated initiator; single-user internal lanes
|
|
may use their session owner. Never borrow another generation's identity.
|
|
• entra_app: use deployment app identity, no user required.
|
|
• rfc8693_obo alone sends obo_scopes; each dynamic mode is paired
|
|
with its required Entra or RFC 8693 grant profile.
|
|
end note
|
|
|
|
note bottom of CallClient
|
|
Dynamic credentials are minted at dispatch, not cached in the registry
|
|
snapshot. Endpoint, audience, scopes, auth mode, and static-key presence stay
|
|
pinned to the same ModelConfig generation as the SDK client. The global
|
|
model.auth_fail_closed policy is read live on every mint. A Stop that wins
|
|
before or during mint prevents model bytes from being sent afterward.
|
|
end note
|
|
|
|
note bottom of ProviderCall
|
|
If minting fails, a configured fail-closed deployment or a keyless alias
|
|
raises BackendAuthUnavailableError. A dynamic alias with an explicit static
|
|
key may fall back only when policy allows. Authentication refusal is not a
|
|
backend-health failure and does not walk to a static fallback model.
|
|
end note
|
|
|
|
@enduml
|