Rename v2 auth service to account (#28260)
This commit is contained in:
121
specs/v2/instructions.md
Normal file
121
specs/v2/instructions.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# V2 Core Instructions
|
||||
|
||||
These notes describe how to work on `packages/core` during the v2 port.
|
||||
|
||||
## Direction
|
||||
|
||||
Move behavior out of large application services and into plugins. Core services should become small, typed containers that own state, expose simple operations, and trigger hooks where policy or integration-specific logic belongs.
|
||||
|
||||
The target shape is:
|
||||
|
||||
- `packages/core` contains domain schemas, typed errors, state containers, events, and plugin hook contracts.
|
||||
- Plugins implement provider-specific, config-specific, auth-specific, model-discovery, and generation behavior.
|
||||
- Services are hot-reloadable by design: updates are granular, observable, and do not require tearing down the whole process.
|
||||
- `packages/opencode` becomes thinner over time: UI, server routes, CLI, storage glue, and legacy compatibility should call the core services instead of owning domain logic directly.
|
||||
|
||||
## Service Shape
|
||||
|
||||
Core services should look like `Catalog`, `AccountV2`, and `AgentV2`:
|
||||
|
||||
- define schemas and branded ids at the top of the module
|
||||
- define typed `Schema.TaggedErrorClass` errors for expected failures
|
||||
- define an `Interface` with small operations
|
||||
- expose a `Context.Service`
|
||||
- implement `layer` with private in-memory state
|
||||
- expose `defaultLayer` with explicit dependencies
|
||||
- self-export with `export * as Name from "./file"`
|
||||
|
||||
Prefer a dumb container API:
|
||||
|
||||
- `get`, `all`, `available`, `default`, `update`, `remove`, `activate`, or other small domain verbs
|
||||
- `update(id, draft => ...)` for registration and mutation
|
||||
- hook calls before committing mutations when plugins need to enrich, cancel, or validate changes
|
||||
- events after committing mutations when other services or frontends need to react
|
||||
|
||||
Avoid putting application policy directly in core services unless it is a domain invariant. For example, resolving model endpoint inheritance is catalog-owned; deciding which providers to register is plugin-owned.
|
||||
|
||||
## Plugin Hooks
|
||||
|
||||
Plugins are the extension boundary for v2. Add hooks to `PluginV2.HookSpec` when logic should be provided by integrations instead of the container itself.
|
||||
|
||||
Hook conventions:
|
||||
|
||||
- hooks receive immutable input plus mutable output
|
||||
- mutable object outputs are exposed as Immer drafts
|
||||
- include `cancel: boolean` when plugins can prevent a mutation
|
||||
- trigger hooks sequentially so ordering remains deterministic
|
||||
- keep hook names domain-oriented, like `provider.update`, `model.update`, `account.activate`, `agent.generate`
|
||||
- keep hook payloads small and typed with core schemas
|
||||
|
||||
Use hooks for:
|
||||
|
||||
- registering providers and models
|
||||
- applying env/account/config-derived enablement
|
||||
- transforming SDK/provider options
|
||||
- implementing generated behavior such as agent generation
|
||||
- choosing defaults when the choice is policy rather than state
|
||||
|
||||
Do not use hooks as a dumping ground for transport concerns, UI behavior, or compatibility shims.
|
||||
|
||||
## Plugin Boot
|
||||
|
||||
Built-in core plugins are registered by `packages/core/src/plugin/boot.ts`.
|
||||
|
||||
When a new core service is intended to be available to plugins:
|
||||
|
||||
- add the service to the boot layer dependency type
|
||||
- yield the service inside the layer
|
||||
- provide it to each plugin effect in `add`
|
||||
- add its default layer to `PluginBoot.defaultLayer` only when that does not create a cycle
|
||||
|
||||
Keep boot as composition only. It should not contain provider, account, agent, or model policy itself.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Core should not import from `packages/opencode`. If a type or concept is needed by core, move or remodel the domain shape in core first.
|
||||
|
||||
Avoid moving legacy services over wholesale. Port the domain shape and the container API, then leave specific behavior behind hooks for plugins to implement.
|
||||
|
||||
When porting an opencode service:
|
||||
|
||||
- identify the state it owns
|
||||
- identify the operations callers actually need
|
||||
- identify which branches are policy or integration behavior
|
||||
- model state and operations in `packages/core`
|
||||
- add hooks for the policy/integration branches
|
||||
- keep old package code working until callers can migrate incrementally
|
||||
|
||||
## Schemas And Types
|
||||
|
||||
Use Effect schemas as the public contract:
|
||||
|
||||
- branded schemas for ids
|
||||
- `Schema.Class` or `Schema.Struct` for domain data
|
||||
- `Schema.TaggedErrorClass` for expected errors
|
||||
- existing core helpers like `DeepMutable`, `withStatics`, and integer schemas where appropriate
|
||||
|
||||
Prefer `Info` objects as the stored domain records. Add static `empty(...)` constructors when update APIs need to create records on first mutation.
|
||||
|
||||
Keep schemas stable and explicit. Do not rely on opencode config shapes as core domain shapes unless the config shape is actually the domain model.
|
||||
|
||||
## State And Events
|
||||
|
||||
Keep state private to the service layer. Use immutable replacement or Effect refs when persistence/concurrency requires it.
|
||||
|
||||
Publish events for committed domain changes, not for attempted mutations. Event names should describe domain facts, for example `catalog.model.updated`.
|
||||
|
||||
The v2 goal is granular reconfiguration. A model update should let dependents react to that model update; it should not require global reloads.
|
||||
|
||||
## Style
|
||||
|
||||
Follow the local core style:
|
||||
|
||||
- `Effect.gen(function* () { ... })` for composition
|
||||
- `Effect.fn("Domain.method")` for public service methods
|
||||
- `Effect.fnUntraced` for small internal mutation helpers
|
||||
- `yield* new ErrorClass(...)` for typed failures
|
||||
- minimal helpers unless they name a real concept
|
||||
- no `any` unless an existing plugin boundary requires it
|
||||
- no compatibility code without a concrete persisted or external-consumer need
|
||||
|
||||
Prefer the smallest correct port. The goal is to make services easier to replace and reason about, not to recreate the old architecture in a new package.
|
||||
@@ -232,7 +232,7 @@ export interface Interface {
|
||||
}
|
||||
```
|
||||
|
||||
`ProviderV2.Info.enabled` is stored provider state. Provider plugins set this field after checking env, auth, config, or provider-specific availability.
|
||||
`ProviderV2.Info.enabled` is stored provider state. Provider plugins set this field after checking env, account, config, or provider-specific availability.
|
||||
|
||||
`ProviderV2.Endpoint` includes `{ type: "unknown" }`. `CatalogV2.model.get()` and `CatalogV2.model.all()` resolve `unknown` endpoints from the provider before returning models.
|
||||
|
||||
@@ -256,6 +256,46 @@ const available = provider.enabled && model.status !== "deprecated"
|
||||
## Plugin Interface
|
||||
|
||||
```ts
|
||||
type HookSpec = {
|
||||
"account.update": {
|
||||
input: {
|
||||
id: AccountV2.ID
|
||||
serviceID: AccountV2.ServiceID
|
||||
}
|
||||
output: {
|
||||
description: string
|
||||
credential: AccountV2.Credential
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.remove": {
|
||||
input: {
|
||||
account: AccountV2.Info
|
||||
}
|
||||
output: {
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.activate": {
|
||||
input: {}
|
||||
output: {
|
||||
from?: AccountV2.ID
|
||||
to: AccountV2.ID
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.activated": {
|
||||
input: {
|
||||
from?: AccountV2.ID
|
||||
to: AccountV2.ID
|
||||
}
|
||||
output: {}
|
||||
}
|
||||
}
|
||||
|
||||
export type Definition<R = never> = Effect.Effect<
|
||||
{
|
||||
readonly order: number
|
||||
@@ -280,7 +320,7 @@ export interface Interface {
|
||||
export const Order = {
|
||||
modelsDev: 0,
|
||||
env: 10,
|
||||
auth: 20,
|
||||
account: 20,
|
||||
provider: 30,
|
||||
config: 40,
|
||||
discovery: 50,
|
||||
@@ -294,21 +334,21 @@ export const ModelsDevPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.S
|
||||
|
||||
export const EnvPlugin: PluginV2.Definition<ProviderV2.Service | Env.Service>
|
||||
|
||||
export const AuthPlugin: PluginV2.Definition<ProviderV2.Service | AuthV2.Service>
|
||||
export const AccountPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>
|
||||
|
||||
export const ConfigPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | Config.Service>
|
||||
|
||||
export const AnthropicPlugin: PluginV2.Definition<ProviderV2.Service | AuthV2.Service>
|
||||
export const AnthropicPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>
|
||||
|
||||
export const OpenRouterPlugin: PluginV2.Definition<ProviderV2.Service>
|
||||
|
||||
export const AmazonBedrockPlugin: PluginV2.Definition<ProviderV2.Service | AuthV2.Service | Env.Service>
|
||||
export const AmazonBedrockPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GoogleVertexPlugin: PluginV2.Definition<ProviderV2.Service | AuthV2.Service | Env.Service>
|
||||
export const GoogleVertexPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GitLabPlugin: PluginV2.Definition<ProviderV2.Service | AuthV2.Service | Env.Service>
|
||||
export const GitLabPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GitLabDiscoveryPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | AuthV2.Service>
|
||||
export const GitLabDiscoveryPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | AccountV2.Service>
|
||||
```
|
||||
|
||||
## Plugin Hooks
|
||||
|
||||
Reference in New Issue
Block a user