feat(api): serve settings through Remote controllers

This commit is contained in:
imccyu
2026-08-27 03:00:49 +08:00
parent 0a9a9ee686
commit dd70c0c88d
23 changed files with 1109 additions and 29 deletions
@@ -0,0 +1,70 @@
{
"name": "@deepseek-ai/dsh-api-settings-controller",
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
"version": "0.1.1-rc.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/api/settings-controller"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./types": {
"types": "./lib/types/types.d.ts",
"default": "./lib/types/types.js"
},
"./typert": {
"types": "./lib/typert.host.d.ts",
"default": "./lib/typert.host.js"
},
"./remote": {
"types": "./lib/typert.remote-client.d.ts",
"default": "./lib/typert.remote-client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts",
"lib/typert.host.js",
"lib/typert.host.d.ts",
"lib/typert.remote-client.js",
"lib/typert.remote-client.d.ts"
],
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
}
}
@@ -0,0 +1,157 @@
/**
* Host owner of the `credentials` Remote namespace: the reference half of
* `ctx.credentials` as a browser configuration page reads and writes it.
*
* @module @deepseek-ai/dsh-api-settings-controller/src/credentials.ts
*/
import { Context } from '@deepseek-ai/cordis'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { z } from 'zod'
/**
* Fan-out bound on one remote `describe` batch. A settings page asks about the
* references its own rows name, so this is far above any real page and still
* keeps one authenticated request from starting unbounded provider work.
*/
const MAX_DESCRIBE_REFS = 64
const credentialRefSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
const describeRequestSchema = z.object({
refs: z.array(credentialRefSchema).max(MAX_DESCRIBE_REFS),
})
const setRequestSchema = z.object({ ref: credentialRefSchema, value: z.string().min(1) })
const unsetRequestSchema = z.object({ ref: credentialRefSchema })
/** Parse the domain constraints that are more specific than generated TypeScript codecs. */
function parseRequest<T>(method: string, schema: z.ZodType<T>, value: unknown): T {
const parsed = schema.safeParse(value)
if (!parsed.success) {
throw new TypertRemoteFailure({
code: 'bad-request',
message: `invalid payload for ${method}`,
details: { issues: parsed.error.issues },
})
}
return parsed.data
}
/**
* Copy exactly the fields {@link CredentialInfo} declares. The Gateway returns
* a business result without decoding it, so a provider whose `describe` carried
* extra enumerable properties would otherwise serialize them to the caller.
* @param info - the provider's answer for one reference.
* @returns the same facts with nothing else attached.
*/
function projectCredentialInfo(info: CredentialInfo): CredentialInfo {
return {
configured: info.configured,
...info.source === undefined ? {} : { source: info.source },
writable: info.writable,
}
}
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host owner of the `credentials` Remote namespace. */
credentialsController: CredentialsController
}
}
/**
* Host service backing the generated `ctx.remote.credentials` namespace. It
* carries every wire obligation the credential seam itself does not: the batch
* fan-out bound, the field-by-field view projection, the reference-grammar
* guard, and the refusal mapping. Secret values cross in one direction only —
* no method here returns one.
*/
export class CredentialsController extends TypertRemoteService {
/** @param ctx - Host context where a credential provider may be mounted. */
constructor(ctx: Context) {
super(ctx, 'credentialsController', { namespace: 'credentials' })
}
/**
* Describe several references for one configuration surface. Batched because
* a settings page describes every reference its rows name at once, and one
* round trip keeps those rows from settling separately.
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
* @returns one view per requested name, keyed by that name.
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
*/
@Remote
async describe(refs: string[]): Promise<Record<string, CredentialInfo>> {
const request = parseRequest('credentials.describe', describeRequestSchema, { refs })
const branded = request.refs.map(ref => [ref, credentialRef(ref)] as const)
const credentials = this.provider()
const entries = await Promise.all(branded.map(async ([ref, key]) =>
[ref, projectCredentialInfo(await credentials.describe(key))] as const))
return Object.fromEntries(entries)
}
/**
* Store one value from a configuration surface. The value crosses the wire in
* this direction only: no read path returns it.
* @param ref - reference name to store under.
* @param value - the non-empty secret value.
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
*/
@Remote
async set(ref: string, value: string): Promise<void> {
const request = parseRequest('credentials.set', setRequestSchema, { ref, value })
const branded = credentialRef(request.ref)
const credentials = this.provider()
await this.write(request.ref, () => credentials.set(branded, request.value))
}
/**
* Remove one reference from a configuration surface.
* @param ref - reference name to remove.
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
*/
@Remote
async unset(ref: string): Promise<void> {
const request = parseRequest('credentials.unset', unsetRequestSchema, { ref })
const branded = credentialRef(request.ref)
const credentials = this.provider()
await this.write(request.ref, () => credentials.unset(branded))
}
/** Resolve the optional provider or report how to supply it. */
private provider(): CredentialProvider {
const credentials = this.ctx.get('credentials')
if (credentials === undefined) {
throw new TypertRemoteFailure({
code: 'internal',
message: 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
details: {},
})
}
return credentials
}
/**
* Run one remote write and report every refusal as `credential-rejected`
* carrying the seam's own message: a read-only source shadowing the reference
* is what a configuration surface must show verbatim. Callers brand the
* reference before entering, so a name outside the grammar never reaches this
* path and fails the same way it does on the read side. The details name only
* the reference, so no failure path can carry the value back out.
*/
private async write(ref: string, write: () => Promise<void>): Promise<void> {
try {
await write()
} catch (error: unknown) {
throw new TypertRemoteFailure({
code: 'credential-rejected',
message: error instanceof Error ? error.message : String(error),
details: { ref },
})
}
}
}
export default CredentialsController
@@ -0,0 +1,222 @@
/**
* Host Remote owner for the configuration surfaces over the settings-domain
* seams. Two namespaces: `settings`, the redacted reads and writes of
* `ctx.settings`, owned by the class below; and `credentials`, mounted from
* here as its own plugin.
*
* @module @deepseek-ai/dsh-api-settings-controller
*/
import { Context } from '@deepseek-ai/cordis'
import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings'
import type { SettingsDescriptor, SettingsPathOp, SettingsProvider } from '@deepseek-ai/dsh-settings'
import type {
SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView,
} from '@deepseek-ai/dsh-settings/types'
import type { JsonValue } from '@deepseek-ai/dsh-session/types'
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { z } from 'zod'
import { CredentialsController } from './credentials.ts'
export { CredentialsController } from './credentials.ts'
export type * from './types.ts'
const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) })
/**
* Project one redacted descriptor onto its wire view, field by field. The
* Gateway returns a business result without decoding it, so a provider whose
* descriptor carried extra enumerable properties would otherwise serialize them
* to the caller.
* @param descriptor - one descriptor read under `redactSecrets`.
* @returns the same facts with nothing else attached.
*/
function namespaceView(descriptor: SettingsDescriptor): SettingsNamespaceView {
return {
ns: String(descriptor.ns),
schema: descriptor.schema as JsonValue,
value: descriptor.value as JsonValue,
...descriptor.base === undefined ? {} : { base: descriptor.base as JsonValue },
...descriptor.user === undefined ? {} : { user: descriptor.user as JsonValue },
applies: descriptor.applies,
secrets: (descriptor.secrets ?? []).map(secret => ({ path: [...secret.path], set: secret.set })),
revision: descriptor.revision,
}
}
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host owner of the `settings` Remote namespace. */
settingsController: SettingsController
}
}
/**
* Host service backing the generated `ctx.remote.settings` namespace. Every
* remote read uses `redactSecrets: true`, so a `role('secret')` field cannot
* ride a response. Writes expose the settings service's merge, replacement,
* and path-addressed operations, and classify every provider refusal as
* `settings-conflict` or `settings-rejected` with the service's message.
*/
export class SettingsController extends TypertRemoteService {
/**
* Register the settings namespace and mount the credentials namespace beside
* it. Both namespaces stay registered when a provider is absent so calls can
* return the configuration API's actionable missing-provider diagnostic.
* @param ctx - Host context where settings and credential providers may be mounted.
*/
constructor(ctx: Context) {
super(ctx, 'settingsController', { namespace: 'settings' })
ctx.plugin(CredentialsController)
}
/**
* Describe every registered namespace for a configuration page: redacted
* layered values plus the serialized schema the page renders its form from.
* @returns provider writability, local-document presence, and one view per namespace.
* @throws TypertRemoteFailure when no settings provider is mounted.
*/
@Remote
describe(): SettingsDescribeValue {
const settings = this.provider()
return {
writable: settings.writable,
hasDocument: settings.documentPath !== undefined,
namespaces: settings.describe({ redactSecrets: true }).map(namespaceView),
}
}
/**
* Merge a patch into one namespace's stored user section.
* @param ns - namespace key to write.
* @param patch - fields to merge into the user section.
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
* @returns the namespace's redacted view after the write.
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
*/
@Remote
update(
ns: string,
patch: Record<string, JsonValue>,
expectedRevision: number | undefined,
): Promise<SettingsNamespaceView> {
return this.write(ns, 'update', patch, expectedRevision)
}
/**
* Replace one namespace's stored user section wholesale.
* @param ns - namespace key to write.
* @param section - complete replacement user section.
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
* @returns the namespace's redacted view after the write.
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
*/
@Remote
replace(
ns: string,
section: Record<string, JsonValue>,
expectedRevision: number | undefined,
): Promise<SettingsNamespaceView> {
return this.write(ns, 'replace', section, expectedRevision)
}
/**
* Apply path-addressed edits to one namespace's user section, resolved against
* the section as stored rather than against whatever the caller last read,
* then answer with that namespace's new redacted view.
* @param ns - namespace key to write.
* @param ops - the edits to apply, in order.
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
* @returns the namespace's redacted view after the write.
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
*/
@Remote
async mutate(
ns: string,
ops: SettingsPathOpView[],
expectedRevision: number | undefined,
): Promise<SettingsNamespaceView> {
return this.write(ns, 'mutate', ops, expectedRevision)
}
private async write(
ns: string,
mode: 'update' | 'replace' | 'mutate',
input: Record<string, JsonValue> | SettingsPathOpView[],
expectedRevision: number | undefined,
): Promise<SettingsNamespaceView> {
const parsed = settingsNamespaceRequestSchema.safeParse({ ns })
if (!parsed.success) {
throw new TypertRemoteFailure({
code: 'bad-request',
message: `invalid payload for settings.${mode}`,
details: { issues: parsed.error.issues },
})
}
const settings = this.provider()
let branded
try {
// A malformed name can address no registration, so it fails exactly as an
// unregistered one does.
branded = settingsNamespace(parsed.data.ns)
} catch (error: unknown) {
throw rejected(ns, error)
}
try {
if (mode === 'update') await settings.update(branded, input, expectedRevision)
else if (mode === 'replace') await settings.replace(branded, input, expectedRevision)
else await settings.mutate(branded, input as SettingsPathOp[], expectedRevision)
} catch (error: unknown) {
throw rejected(ns, error)
}
const descriptor = settings.describe({ redactSecrets: true }).find(candidate => candidate.ns === branded)
if (descriptor === undefined) {
// The write committed but the namespace vanished before this read: only a
// concurrent registrant disposal can produce it.
throw new TypertRemoteFailure({
code: 'internal',
message: `settings namespace "${ns}" was disposed after the ${mode}`,
details: {},
})
}
return namespaceView(descriptor)
}
/** Resolve the optional provider or report how to supply it. */
private provider(): SettingsProvider {
const settings = this.ctx.get('settings')
if (settings === undefined) {
throw new TypertRemoteFailure({
code: 'internal',
message: 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition',
details: {},
})
}
return settings
}
}
/**
* Classify one seam refusal. A stale writer is its own outcome, not a malformed
* request: the client must re-read and re-apply rather than treat the write as
* invalid.
* @param ns - the namespace the write addressed.
* @param error - whatever the seam threw.
* @returns the failure to raise for that refusal.
*/
function rejected(ns: string, error: unknown): TypertRemoteFailure {
if (error instanceof SettingsConflictError) {
return new TypertRemoteFailure({
code: 'settings-conflict',
message: error.message,
details: { ns, expected: error.expected, actual: error.actual },
})
}
return new TypertRemoteFailure({
code: 'settings-rejected',
message: error instanceof Error ? error.message : String(error),
details: { ns },
})
}
export default SettingsController
@@ -0,0 +1,23 @@
/** Package-owned invariant companion. @module @deepseek-ai/dsh-api-settings-controller/invariant */
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-api-settings-controller'
/** Cordis companion plugin name. */
export const name = 'api-settings-controller-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the settings and credential seams own storage and
* update events, while this package only projects their methods onto the wire.
*/
const install: InvariantInstaller = () => {}
/** Register this package's invariant companion. */
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,49 @@
/**
* Browser-safe failure vocabulary of the configuration surfaces this package
* serves. The redacted views themselves live with their seam in
* `@deepseek-ai/dsh-settings/types`, whose Cordis event declarations already
* register that file for the Client compilation face.
*
* @module @deepseek-ai/dsh-api-settings-controller/types
*/
/** Stable settings failure details returned by the `settings` namespace. */
export interface SettingsErrorDetailsMap {
/**
* Every seam refusal that is not a stale write: an unregistered or malformed
* namespace, a read-only provider, schema validation, storage.
*/
'settings-rejected': { readonly ns: string }
/**
* The stored revision moved after the caller read it. Its own outcome rather
* than an invalid request: the caller must re-read and re-apply.
*/
'settings-conflict': { readonly ns: string; readonly expected: number; readonly actual: number }
}
/** Settings business failure carried by a rejected Remote call. */
export type SettingsError = {
[Code in keyof SettingsErrorDetailsMap]: {
readonly code: Code
readonly message: string
readonly details: SettingsErrorDetailsMap[Code]
}
}[keyof SettingsErrorDetailsMap]
/** Stable credential failure details returned by the `credentials` namespace. */
export interface CredentialErrorDetailsMap {
/**
* The provider refused a valid write, for example because a read-only source
* shadows the reference. The details name only the reference, never the value.
*/
'credential-rejected': { readonly ref: string }
}
/** Credential business failure carried by a rejected Remote call. */
export type CredentialError = {
[Code in keyof CredentialErrorDetailsMap]: {
readonly code: Code
readonly message: string
readonly details: CredentialErrorDetailsMap[Code]
}
}[keyof CredentialErrorDetailsMap]
@@ -0,0 +1,141 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
import CredentialsController from '../src/credentials.ts'
import { MemoryCredentials } from '../../../credentials/credentials/tests/memory.ts'
/** A store whose `describe` carries more than the view declares, as a foreign provider might. */
class LeakyCredentials extends MemoryCredentials {
override describe(): Promise<CredentialInfo> {
return Promise.resolve(
{ configured: true, source: 'memory', writable: true, value: 'sk-leaked' } as CredentialInfo,
)
}
}
/** A store whose write rejects with a bare string, the way some client libraries do. */
class LiteralRejectingCredentials extends MemoryCredentials {
override async set(): Promise<void> {
throw 'the store refused'
}
}
/** A store whose provider-owned policy rejects an otherwise valid write. */
class RejectingCredentials extends MemoryCredentials {
override set(): Promise<void> {
return Promise.reject(new Error('a read-only source shadows this reference'))
}
}
async function boot(
seed: Record<string, string> = {},
provider: typeof MemoryCredentials = MemoryCredentials,
): Promise<CredentialsController> {
const ctx = new Context()
await ctx.plugin(provider, seed)
await ctx.plugin(CredentialsController)
return ctx.credentialsController
}
describe('the credentials Remote namespace a configuration surface calls', () => {
it('publishes the credentials namespace from its own service key', async () => {
const controller = await boot()
const binding = controller.typertRemote
expect(binding.serviceKey).toBe('credentialsController')
expect(binding.namespace).toBe('credentials')
expect(remoteMethods(controller)).toEqual([
{ method: 'describe', invocation: { kind: 'direct' } },
{ method: 'set', invocation: { kind: 'direct' } },
{ method: 'unset', invocation: { kind: 'direct' } },
])
})
it('reports the actionable configuration error while no credential provider is mounted', async () => {
const ctx = new Context()
await ctx.plugin(CredentialsController)
for (const call of [
() => ctx.credentialsController.describe(['DEEPSEEK_API_KEY']),
() => ctx.credentialsController.set('DEEPSEEK_API_KEY', 'sk-live'),
() => ctx.credentialsController.unset('DEEPSEEK_API_KEY'),
]) {
const failure = await call().catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
expect((failure as TypertRemoteFailure).failure).toEqual({
code: 'internal',
message: 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
details: {},
})
}
})
it('describes a batch of references as one map, values excluded', async () => {
const controller = await boot({ DEEPSEEK_API_KEY: 'sk-seeded' })
const described = await controller.describe(['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'])
expect(described).toEqual({
DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true },
OPENAI_API_KEY: { configured: false, writable: true },
})
expect(JSON.stringify(described)).not.toContain('sk-seeded')
})
it('reports an invalid reference as bad-request', async () => {
const controller = await boot()
for (const call of [
() => controller.describe(['DEEPSEEK_API_KEY', 'not a var']),
() => controller.set('not a var', 'sk-live'),
() => controller.unset('not a var'),
]) {
const failure = await call().catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
}
})
it('answers the largest batch it accepts and reports one reference more as bad-request', async () => {
const controller = await boot()
const accepted = Array.from({ length: 64 }, (_unused, index) => `REF_${String(index)}`)
expect(Object.keys(await controller.describe(accepted))).toHaveLength(64)
const failure = await controller.describe([...accepted, 'REF_64']).catch((error: unknown) => error)
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
})
it('answers only the fields the view declares, whatever a provider returns', async () => {
const controller = await boot({}, LeakyCredentials)
const described = await controller.describe(['DEEPSEEK_API_KEY'])
expect(described.DEEPSEEK_API_KEY).toEqual({ configured: true, source: 'memory', writable: true })
expect(JSON.stringify(described)).not.toContain('sk-leaked')
})
it('stores and removes through the same references the batch describes', async () => {
const controller = await boot()
await controller.set('DEEPSEEK_API_KEY', 'sk-live')
expect(await controller.describe(['DEEPSEEK_API_KEY']))
.toEqual({ DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true } })
await controller.unset('DEEPSEEK_API_KEY')
expect(await controller.describe(['DEEPSEEK_API_KEY']))
.toEqual({ DEEPSEEK_API_KEY: { configured: false, writable: true } })
})
it('reports a refused write as credential-rejected naming only the reference', async () => {
const controller = await boot({}, RejectingCredentials)
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
const { code, message, details } = (failure as TypertRemoteFailure).failure
expect(code).toBe('credential-rejected')
expect(message).toContain('read-only source')
expect(details).toEqual({ ref: 'DEEPSEEK_API_KEY' })
})
it('reports an empty value as bad-request', async () => {
const controller = await boot()
const failure = await controller.set('DEEPSEEK_API_KEY', '').catch((error: unknown) => error)
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
})
it('stringifies a refusal that is not an Error', async () => {
const controller = await boot({}, LiteralRejectingCredentials)
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
expect((failure as TypertRemoteFailure).failure.message).toBe('the store refused')
})
})
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SettingsControllerInvariant from '../src/invariant.ts'
describe('api-settings-controller invariant companion', () => {
it('reserves the package name against duplicate registration', async () => {
const ctx = new Context()
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SettingsControllerInvariant)
expect(() => {
ctx.invariants.register('@deepseek-ai/dsh-api-settings-controller', () => {})
}).toThrow(/already registered/)
})
})
@@ -0,0 +1,241 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import type { SettingsDescriptor, SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
import SettingsController from '../src/index.ts'
import { MemorySettings } from '../../../settings/settings/tests/memory.ts'
const NS = settingsNamespace('ui-test')
const Profile = z.object({
preference: z.union(['light', 'dark']).default('light'),
apiKey: z.string().role('secret'),
})
/** A provider that reports a local document, for the `hasDocument` fact. */
class DocumentSettings extends MemorySettings {
override get documentPath(): string | undefined {
return '/deployment/settings.yaml'
}
}
/** A provider whose read forgets the namespace its write just committed. */
class VanishingSettings extends MemorySettings {
override describe(): SettingsDescriptor[] {
return []
}
}
/**
* A provider whose descriptor omits the secret-slot list. `secrets` is optional
* on the descriptor, so a foreign provider may leave it out even under
* `redactSecrets`, and the view still has to declare an empty list.
*/
class SlotlessSettings extends MemorySettings {
override describe(): SettingsDescriptor[] {
return [{
ns: NS,
schema: Profile.toJSON(),
value: { preference: 'light' },
applies: 'live',
revision: 0,
} as unknown as SettingsDescriptor]
}
}
/** A provider that refuses every write the way a read-only backing store would. */
class RefusingSettings extends MemorySettings {
override mutate(ns: SettingsNamespace): Promise<void> {
return Promise.reject(new Error(`settings "${ns}" is read-only in this deployment`))
}
}
/** A provider that refuses with a bare string, the way some storage clients do. */
class LiteralRefusingSettings extends MemorySettings {
override async mutate(): Promise<void> {
throw 'the document is locked'
}
}
async function boot(
provider: typeof MemorySettings = MemorySettings,
options: { doc?: Record<string, unknown>; base?: { preference: 'light' | 'dark' } } = {},
): Promise<{ controller: SettingsController; ctx: Context }> {
const ctx = new Context()
await ctx.plugin(provider, options.doc === undefined ? {} : { doc: options.doc })
ctx.settings.register(NS, Profile, options.base === undefined ? {} : { base: options.base })
await ctx.plugin(SettingsController)
return { controller: ctx.settingsController, ctx }
}
describe('the settings Remote namespace a configuration page calls', () => {
it('publishes the settings namespace from its own service key', async () => {
const { controller } = await boot()
expect(controller.typertRemote.serviceKey).toBe('settingsController')
expect(controller.typertRemote.namespace).toBe('settings')
expect(remoteMethods(controller)).toEqual([
{ method: 'describe', invocation: { kind: 'direct' } },
{ method: 'update', invocation: { kind: 'direct' } },
{ method: 'replace', invocation: { kind: 'direct' } },
{ method: 'mutate', invocation: { kind: 'direct' } },
])
})
it('reports the actionable configuration error while no settings provider is mounted', async () => {
const ctx = new Context()
await ctx.plugin(SettingsController)
for (const call of [
() => ctx.settingsController.describe(),
() => ctx.settingsController.update('ui-test', {}, undefined),
() => ctx.settingsController.replace('ui-test', {}, undefined),
() => ctx.settingsController.mutate('ui-test', [], undefined),
]) {
const failure = await Promise.resolve().then(call).catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
expect((failure as TypertRemoteFailure).failure).toEqual({
code: 'internal',
message: 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition',
details: {},
})
}
})
it('mounts the credentials namespace beside its own', async () => {
const ctx = new Context()
await ctx.plugin(MemorySettings)
ctx.settings.register(NS, Profile)
const fiber = ctx.plugin(SettingsController)
await fiber.await()
expect(ctx.get('credentialsController')).toBeDefined()
await fiber.dispose()
expect(ctx.get('settingsController')).toBeUndefined()
expect(ctx.get('credentialsController')).toBeUndefined()
})
it('describes every namespace redacted, with the deployment facts around them', async () => {
const { controller } = await boot(DocumentSettings, { doc: { 'ui-test': { apiKey: 'sk-stored' } } })
const value = controller.describe()
expect(value).toMatchObject({ writable: true, hasDocument: true })
const [view] = value.namespaces
expect(view?.ns).toBe('ui-test')
// The secret never rides; its slot reports only that one is stored.
expect(JSON.stringify(value)).not.toContain('sk-stored')
expect(view?.secrets).toEqual([{ path: ['apiKey'], set: true }])
// Redaction removes the field rather than replacing it, so the layer that
// stored a secret comes back empty instead of carrying a placeholder.
expect(view?.user).toEqual({})
})
it('reports a read-only provider and omits the layers it has none of', async () => {
const { controller } = await boot(class extends MemorySettings {
override get writable(): boolean {
return false
}
})
const value = controller.describe()
expect(value).toMatchObject({ writable: false, hasDocument: false })
const [view] = value.namespaces
// No composition base was declared and no user section is stored, so
// neither optional layer appears at all.
expect(view && 'base' in view).toBe(false)
expect(view && 'user' in view).toBe(false)
})
it('declares an empty slot list when the provider names no secrets', async () => {
const { controller } = await boot(SlotlessSettings)
const [view] = controller.describe().namespaces
expect(view?.secrets).toEqual([])
})
it('carries the composition base layer when the registrant declared one', async () => {
const { controller } = await boot(MemorySettings, { base: { preference: 'dark' } })
const [view] = controller.describe().namespaces
expect(view?.base).toEqual({ preference: 'dark' })
})
it('applies path-addressed edits and answers with the namespace it just wrote', async () => {
const { controller } = await boot()
const view = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
expect(view).toMatchObject({ ns: 'ui-test', user: { preference: 'dark' } })
expect(view.revision).toBeGreaterThan(0)
})
it('supports merge updates and wholesale replacement on the Remote namespace', async () => {
const { controller } = await boot(MemorySettings, {
doc: { 'ui-test': { preference: 'dark', apiKey: 'sk-stored' } },
})
const updated = await controller.update('ui-test', { preference: 'light' }, undefined)
expect(updated.user).toEqual({ preference: 'light' })
expect(updated.secrets).toEqual([{ path: ['apiKey'], set: true }])
const replaced = await controller.replace('ui-test', {}, updated.revision)
expect(replaced.value).toEqual({ preference: 'light' })
expect(replaced.user).toEqual({})
expect(replaced.secrets).toEqual([{ path: ['apiKey'], set: false }])
})
it('refuses a stale write as settings-conflict carrying both revisions', async () => {
const { controller } = await boot()
const held = controller.describe().namespaces[0]!.revision
await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], held)
const failure = await controller
.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'light' }], held)
.catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
const { code, details } = (failure as TypertRemoteFailure).failure
expect(code).toBe('settings-conflict')
expect(details).toMatchObject({ ns: 'ui-test', expected: held })
})
it('answers a malformed namespace exactly as an unregistered one', async () => {
const { controller } = await boot()
for (const ns of ['Not A Namespace', 'unregistered']) {
const failure = await controller.mutate(ns, [{ op: 'unset', path: ['preference'] }], undefined)
.catch((error: unknown) => error)
expect((failure as TypertRemoteFailure).failure).toMatchObject({
code: 'settings-rejected',
details: { ns },
})
}
})
it('reports an empty namespace as bad-request', async () => {
const { controller } = await boot()
for (const call of [
() => controller.update('', {}, undefined),
() => controller.replace('', {}, undefined),
() => controller.mutate('', [], undefined),
]) {
const failure = await call().catch((error: unknown) => error)
expect(failure).toBeInstanceOf(TypertRemoteFailure)
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
}
})
it('reports a refused write as settings-rejected carrying the seam message', async () => {
const { controller } = await boot(RefusingSettings)
const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
.catch((error: unknown) => error)
const { code, message } = (failure as TypertRemoteFailure).failure
expect(code).toBe('settings-rejected')
expect(message).toContain('read-only in this deployment')
})
it('stringifies a refusal that is not an Error', async () => {
const { controller } = await boot(LiteralRefusingSettings)
const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
.catch((error: unknown) => error)
expect((failure as TypertRemoteFailure).failure.message).toBe('the document is locked')
})
it('reports a namespace disposed between the write and its read-back', async () => {
const { controller } = await boot(VanishingSettings)
const failure = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
.catch((error: unknown) => error)
const { code, message } = (failure as TypertRemoteFailure).failure
expect(code).toBe('internal')
expect(message).toContain('was disposed after the mutate')
})
})
@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../credentials/credentials"
},
{
"path": "../../core/session"
},
{
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../settings/settings"
},
{
"path": "../../typert/protocol"
}
]
}
+5
View File
@@ -86,6 +86,11 @@
- id: session-controller
name: '@deepseek-ai/dsh-api-session-controller'
# Configuration-surface reads and writes over Typert Remote. Each method
# reports an actionable error when its settings-domain provider is absent.
- id: settings-controller
name: '@deepseek-ai/dsh-api-settings-controller'
# Workspace commands and reconnect-safe projection over Typert Remote.
- id: workspace-controller
name: '@deepseek-ai/dsh-api-workspace-controller'
+1
View File
@@ -108,6 +108,7 @@
"@deepseek-ai/dsh-session-log-export": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-api-settings-controller": "workspace:^",
"@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-workspace": "workspace:^",
+4 -12
View File
@@ -9,9 +9,11 @@
*/
import { Context, Service } from '@deepseek-ai/cordis'
import type { CredentialKey, CredentialRecord, CredentialRef } from './types.ts'
import type { CredentialInfo, CredentialKey, CredentialRecord, CredentialRef } from './types.ts'
export type { ApiKeyRecord, CredentialKey, CredentialRecord, CredentialRef, GrantRecord } from './types.ts'
export type {
ApiKeyRecord, CredentialInfo, CredentialKey, CredentialRecord, CredentialRef, GrantRecord,
} from './types.ts'
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
@@ -119,16 +121,6 @@ export interface ResolvedCredential {
source: string
}
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
export interface CredentialInfo {
/** Whether {@link CredentialProvider.resolve} would currently return a value. */
configured: boolean
/** Source layer currently supplying the value; absent while unconfigured. */
source?: string
/** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
writable: boolean
}
/** Presence and writability facts for one record, safe for configuration UIs — never the value. */
export interface CredentialRecordInfo {
/**
+18 -3
View File
@@ -1,8 +1,9 @@
/**
* Client-safe type surface of the credential seam: the two key brands, the
* stored-record union, and the seam's Cordis event declarations. Types only —
* no runtime code, and nothing here reaches a Host-only symbol, so a Client
* compilation face reads exactly the signature the Host emits.
* stored-record union, the reference view crossing the Remote wire, and the
* seam's Cordis event declarations. Types only — no runtime code, and nothing
* here reaches a Host-only symbol, so a Client compilation face reads exactly
* the signature the Host emits.
*
* @module @deepseek-ai/dsh-credentials/types
*/
@@ -58,6 +59,20 @@ export interface GrantRecord {
/** One durable credential record, tagged by what the seam may do with it. */
export type CredentialRecord = ApiKeyRecord | GrantRecord
/**
* Source and writability facts for one reference, safe for configuration UIs —
* never the value. The view has no slot a value could ride in, which is what
* lets the whole read half cross the Remote wire.
*/
export interface CredentialInfo {
/** Whether resolving the reference would currently return a value. */
configured: boolean
/** Source layer currently supplying the value; absent while unconfigured. */
source?: string
/** Whether the active provider can write this reference. */
writable: boolean
}
declare module '@deepseek-ai/cordis' {
interface Events {
/**
+4 -2
View File
@@ -37,15 +37,17 @@
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^"
}
}
+59 -2
View File
@@ -1,13 +1,15 @@
/**
* Client-safe type surface of the user-settings seam: the namespace brand, the
* commit-origin union, and the seam's Cordis event declarations. Types only —
* no runtime code, and nothing here reaches a Host-only symbol, so a Client
* commit-origin union, the redacted views a configuration surface reads over
* the Remote wire, and the seam's Cordis event declarations. Types only — no
* runtime code, and nothing here reaches a Host-only symbol, so a Client
* compilation face reads exactly the signatures the Host emits.
*
* @module @deepseek-ai/dsh-settings/types
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { JsonValue } from '@deepseek-ai/dsh-session/types'
/** Nominal id of one registered settings namespace. */
export type SettingsNamespace = Branded<'SettingsNamespace'>
@@ -15,6 +17,61 @@ export type SettingsNamespace = Branded<'SettingsNamespace'>
/** Origin of one committed settings change. */
export type SettingsUpdateSource = 'update' | 'provider'
/** One schema-declared secret slot inside a redacted namespace value. */
export interface SettingsSecretView {
/** Path from the section root to the removed field. */
path: string[]
/** Whether the slot currently holds a value; the value itself never rides. */
set: boolean
}
/**
* Wire view of one registered namespace, always read under `redactSecrets`. The
* JSON-valued fields are `JsonValue` rather than the descriptor's `unknown`
* because the Remote boundary admits no unconstrained data.
*/
export interface SettingsNamespaceView {
/** Namespace key (`llm-deepseek`, `llm-pi-ai`, …). */
ns: string
/** Serialized schemastery schema envelope (`schema.toJSON()`); rehydrate with `new Schema(json)`. */
schema: JsonValue
/** Redacted resolved value (schema defaults → composition base → user layer). */
value: JsonValue
/** Redacted composition base layer, when the registrant declared one. */
base?: JsonValue
/** Redacted raw user section, when one exists; a field's presence here marks it user-overridden. */
user?: JsonValue
/** When the owner applies changes. */
applies: 'live' | 'restart'
/** Every schema-declared secret slot with its configured state. */
secrets: SettingsSecretView[]
/**
* Monotonic revision of the raw user section this view was read at. Send it
* back as `expectedRevision` on a write so a stale editor is refused rather
* than silently overwriting a concurrent change.
*/
revision: number
}
/**
* One path-addressed edit carried by a remote settings write. `set` writes the
* value at the path, creating intermediate objects; `unset` removes it. The
* empty path addresses the section root.
*/
export type SettingsPathOpView =
| { op: 'set'; path: string[]; value: JsonValue }
| { op: 'unset'; path: string[] }
/** Every registered namespace with the deployment facts a configuration page renders around them. */
export interface SettingsDescribeValue {
/** Whether the provider accepts writes; `false` disables every write control. */
writable: boolean
/** Whether a file-backed provider owns a local document, without exposing its Host path. */
hasDocument: boolean
/** One view per registered namespace. */
namespaces: SettingsNamespaceView[]
}
declare module '@deepseek-ai/cordis' {
interface Events {
/**
+3
View File
@@ -20,6 +20,9 @@
{
"path": "../../util/brand"
},
{
"path": "../../core/session"
},
{
"path": "../../runtime-diagnostics/invariants"
}
+40 -9
View File
@@ -683,6 +683,9 @@ importers:
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../session-controller
'@deepseek-ai/dsh-api-settings-controller':
specifier: workspace:^
version: link:../settings-controller
'@deepseek-ai/dsh-api-workspace-controller':
specifier: workspace:^
version: link:../workspace-controller
@@ -820,6 +823,31 @@ importers:
specifier: workspace:^
version: link:../../workspace/workspace
packages/api/settings-controller:
dependencies:
zod:
specifier: ^4.4.3
version: 4.4.3
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-credentials':
specifier: workspace:^
version: link:../../credentials/credentials
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-settings':
specifier: workspace:^
version: link:../../settings/settings
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:^
version: link:../../typert/protocol
packages/api/workspace-controller:
dependencies:
zod:
@@ -1376,6 +1404,9 @@ importers:
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../../api/session-controller
'@deepseek-ai/dsh-api-settings-controller':
specifier: workspace:^
version: link:../../api/settings-controller
'@deepseek-ai/dsh-api-workspace-controller':
specifier: workspace:^
version: link:../../api/workspace-controller
@@ -1630,6 +1661,9 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-settings':
specifier: workspace:^
version: link:../../settings/settings
'@deepseek-ai/dsh-tool-todo':
specifier: workspace:^
version: link:../../todo/tool-todo
@@ -2644,9 +2678,6 @@ importers:
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../../api/session-controller
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../connection
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../locale
@@ -3096,9 +3127,6 @@ importers:
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:^
version: link:../../api/remotes
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../connection
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../locale
@@ -5703,9 +5731,6 @@ importers:
'@deepseek-ai/dsh-commands':
specifier: workspace:^
version: link:../../interaction/commands
'@deepseek-ai/dsh-credentials':
specifier: workspace:^
version: link:../../credentials/credentials
'@deepseek-ai/dsh-host-directory-picker':
specifier: workspace:^
version: link:../directory-picker
@@ -5752,6 +5777,9 @@ importers:
'@deepseek-ai/dsh-agent-presets':
specifier: workspace:^
version: link:../../preset/agent-presets
'@deepseek-ai/dsh-credentials':
specifier: workspace:^
version: link:../../credentials/credentials
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
@@ -7531,6 +7559,9 @@ importers:
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
packages/settings/settings-file:
dependencies:
+6
View File
@@ -70,6 +70,8 @@ export const SERVICE_PAGE: Record<string, string> = {
cordisInspect: 'extensions.md',
authorization: 'credentials.md',
credentials: 'credentials.md',
credentialsController: 'credentials.md',
settingsController: 'settings.md',
directoryPicker: 'workspace.md',
deepseekLlmApiExtensions: 'llm-streaming.md',
dynamicCordisRunner: 'extensions.md',
@@ -526,6 +528,10 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SettingsRegisterOptions: 'settings.md',
SettingsScope: 'settings.md',
SettingsDescriptor: 'settings.md',
SettingsDescribeValue: 'settings.md',
SettingsNamespaceView: 'settings.md',
SettingsPathOpView: 'settings.md',
SettingsSecretView: 'settings.md',
SettingsPathOp: 'settings.md',
SettingsDescribeOptions: 'settings.md',
SettingsUpdateSource: 'settings.md',
+14
View File
@@ -157,6 +157,20 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['host-apiproxy'],
note: 'Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains.',
},
{
key: 'credentialsController',
pkg: 'api-settings-controller',
title: 'Host credential-surface Remote controller',
mode: 'core',
note: 'Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition.',
},
{
key: 'settingsController',
pkg: 'api-settings-controller',
title: 'Host settings-surface Remote controller',
mode: 'core',
note: 'Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition.',
},
{
key: 'workspaceController',
pkg: 'api-workspace-controller',
+1 -1
View File
@@ -1664,7 +1664,7 @@
{
"doc": "docs/subsystems/credentials.md",
"symbol": "CredentialInfo",
"source": "packages/credentials/credentials/src/index.ts"
"source": "packages/credentials/credentials/src/types.ts"
},
{
"doc": "docs/subsystems/settings.md",
@@ -162,6 +162,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/test-support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
'packages/api/session-controller': { kind: 'none', reason: 'Session API and transport owner; invoked Agent commands own any model-visible effect.' },
'packages/api/settings-controller': { kind: 'none', reason: 'Configuration-surface API owner; it registers no prompt, tool, or session event.' },
'packages/api/workspace-controller': { kind: 'none', reason: 'Workspace API and state projection owner; it registers no prompt, tool, or session event.' },
'packages/typert/protocol': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
+3
View File
@@ -52,6 +52,9 @@
"@deepseek-ai/dsh-api-session-controller/invariant": ["./packages/api/session-controller/src/invariant.ts"],
"@deepseek-ai/dsh-api-session-controller/types": ["./packages/api/session-controller/src/types.ts"],
"@deepseek-ai/dsh-api-session-controller/remote-events": ["./packages/api/session-controller/src/remote-events.ts"],
"@deepseek-ai/dsh-api-settings-controller": ["./packages/api/settings-controller/src/index.ts"],
"@deepseek-ai/dsh-api-settings-controller/invariant": ["./packages/api/settings-controller/src/invariant.ts"],
"@deepseek-ai/dsh-api-settings-controller/types": ["./packages/api/settings-controller/src/types.ts"],
"@deepseek-ai/dsh-api-workspace-controller": ["./packages/api/workspace-controller/src/index.ts"],
"@deepseek-ai/dsh-api-workspace-controller/client": ["./packages/api/workspace-controller/src/client/index.ts"],
"@deepseek-ai/dsh-api-workspace-controller/invariant": ["./packages/api/workspace-controller/src/invariant.ts"],
+1
View File
@@ -146,6 +146,7 @@
{ "path": "./packages/api/gateway/tsconfig.host.json" },
{ "path": "./packages/api/remotes/tsconfig.host.json" },
{ "path": "./packages/api/session-controller/tsconfig.host.json" },
{ "path": "./packages/api/settings-controller" },
{ "path": "./packages/api/workspace-controller/tsconfig.host.json" },
{ "path": "./packages/typert/loader" },
{ "path": "./packages/session/session-persistence" },