Merge origin/master: web permission sandbox, default pi-ai providers
This commit is contained in:
@@ -39,15 +39,15 @@ Swappable LLM, bash, filesystem, and other capability providers remain in the le
|
||||
| `toolTasks` | owner defaults | Background-task control-tool config, or `false` |
|
||||
| `goals` | owner defaults | Persisted goal-domain and model-tool config; `false` removes the goal stack and `/goal` producer |
|
||||
| `workspaceContext` | required | Workspace-instruction config, or `false` |
|
||||
| `persistenceRoot` | `./.sessions` | JSONL persistence root and parent of the derived `session-query.db` index |
|
||||
| `persistenceRoot` | `./.sessions` (launcher boot slot overrides) | JSONL persistence root and parent of the derived `session-query.db` index |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
|
||||
| `sessionReferences` | service defaults | Cross-session candidate and snapshot limits routed to `dsh-session-reference` |
|
||||
| `welcome` | `ready.` | TUI subtitle |
|
||||
| `resumeCommand` | — | Exit and no-host fallback command template; the selector itself uses session query and host handoff |
|
||||
| `ui` | owner defaults | TUI presentation settings such as reasoning, color, and card height |
|
||||
| `resumeSessionId` | — | Exact persisted session to resume |
|
||||
|
||||
Fresh runs mint a `main-session-<uuid>` session id and pass it to both the TUI and configured agent. Resumed runs bind both components to `resumeSessionId`. The TUI mounts before the spine so it can render a matching config-start failure instead of leaving a blank terminal. The app composes persistence and session query for `/resume`; an embedding host may additionally provide `tuiResumeHost` for in-place process handoff.
|
||||
Session identity is launcher-owned rather than configurable: a launcher provides `MAIN_SESSION_ID_KEY` on the boot context, and this app binds both the TUI and the configured agent to that id, loading persisted history only when the launcher also set `resume`. With no such slot the app mints a `main-session-<uuid>` and creates it fresh. The TUI mounts before the spine so it can render a matching config-start failure instead of leaving a blank terminal. The app composes persistence and session query for `/resume`; a launcher may additionally provide `tuiResumeHost` for in-place process handoff and `TUI_GOODBYE_MESSAGE_KEY` for the line printed on exit.
|
||||
|
||||
`persistenceRoot` defaults to project-local `./.sessions`: an app bundle must not assume the user's shared session store. A launcher that wants one store across every cwd states that policy through the `SESSIONS_ROOT_KEY` boot slot (`ctx.provide` before any Loader entry mounts) — the dsh CLI provides its Harness-home root there, so its `/resume` lists sessions from every workspace. Precedence is explicit config, then the launcher slot, then the project-local default.
|
||||
|
||||
## Front door
|
||||
|
||||
|
||||
@@ -30,20 +30,20 @@
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||
"@deepseek-ai/dsh-command-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
|
||||
"@deepseek-ai/dsh-command-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-query": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-reference": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tui": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tui": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
@@ -53,21 +53,21 @@
|
||||
"@cordisjs/plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tui": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-tui": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
|
||||
@@ -30,6 +30,11 @@ import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as uiTui from '@deepseek-ai/dsh-tui'
|
||||
|
||||
export const name = 'tui-demo'
|
||||
|
||||
// The bundle's own fallback stays project-local: a plugin must never assume
|
||||
// the user's shared session store. The dsh launcher's SESSIONS_ROOT_KEY slot
|
||||
// (opaque here — the CLI resolves it to DSH_HOME/sessions) carries any
|
||||
// shared-store policy, and explicit config wins over both.
|
||||
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
|
||||
|
||||
// Each front door keeps a complete Loader contract so its deployment config is
|
||||
@@ -53,7 +58,12 @@ export interface Config {
|
||||
dshHome?: string
|
||||
/** Fallback session-title limits forwarded through agent-spine-demo. */
|
||||
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
|
||||
/** Directory for JSONL sessions and the derived query index. Defaults to `./.sessions`. */
|
||||
/**
|
||||
* Directory for JSONL sessions and the derived query index. Precedence:
|
||||
* this explicit config, then the launcher's opaque `SESSIONS_ROOT_KEY` boot
|
||||
* slot (the dsh CLI resolves it to `DSH_HOME/sessions`), then a project-local
|
||||
* `./.sessions` fallback — the bundle itself never assumes a global store.
|
||||
*/
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
@@ -61,13 +71,6 @@ export interface Config {
|
||||
sessionReferences?: SessionReferenceConfig
|
||||
/** TUI transcript's optional first line; absent renders nothing on start. */
|
||||
welcome?: string
|
||||
/**
|
||||
* Shell command template the TUI prints on exit and lists under `/resume`,
|
||||
* with `{session}` replaced by the live session id (forwarded to the front
|
||||
* door). Set it to a command that resumes the session, e.g.
|
||||
* `dsh --resume {session}`.
|
||||
*/
|
||||
resumeCommand?: string
|
||||
/** Full-screen TUI presentation settings. */
|
||||
ui?: uiTui.TuiConfig
|
||||
/** Skill registry, local-provider, and model-facing consumer config. */
|
||||
@@ -78,8 +81,6 @@ export interface Config {
|
||||
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
|
||||
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and command. */
|
||||
goals?: agentCore.GoalConfig | false
|
||||
/** Persisted session id to resume instead of creating a fresh session. */
|
||||
resumeSessionId?: string
|
||||
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
}
|
||||
@@ -94,33 +95,38 @@ export const Config: z<Config> = z.object({
|
||||
tools: ToolRegistry.Config,
|
||||
dshHome: z.string(),
|
||||
sessionTitle: agentCore.SessionTitleConfigSchema,
|
||||
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
|
||||
// No schema default: schemastery would materialize it before composeTuiApp
|
||||
// runs, shadowing the launcher's SESSIONS_ROOT_KEY slot for a Loader mount.
|
||||
persistenceRoot: z.string(),
|
||||
persistenceCompression: JsonlCompressionSchema,
|
||||
sessionReferences: SessionReferenceService.Config,
|
||||
welcome: z.string(),
|
||||
resumeCommand: z.string(),
|
||||
ui: uiTui.TuiConfigSchema,
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
toolBash: agentCore.ToolBashConfigSchema,
|
||||
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
|
||||
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
|
||||
resumeSessionId: z.string(),
|
||||
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
|
||||
})
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/**
|
||||
* Compose the spine, TUI, JSONL persistence, and user-question tool around one
|
||||
* exact fresh or resumed session identity. The TUI subscribes to startup
|
||||
* failures before the spine creates the agent.
|
||||
* exact fresh or resumed session identity, taken from the launcher's
|
||||
* {@link uiTui.MAIN_SESSION_ID_KEY} slot. The TUI subscribes to startup failures
|
||||
* before the spine creates the agent.
|
||||
* @param ctx - context receiving the app's child plugins.
|
||||
* @param config - validated app configuration.
|
||||
*/
|
||||
export function composeTuiApp(ctx: Context, config: Config): void {
|
||||
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
|
||||
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
|
||||
// The launcher, not the deployment config, owns `main`'s session identity: it
|
||||
// reaches a Loader-mounted bundle only through this context slot. A launcher
|
||||
// that supplies an id knows whether that session already exists, so it also
|
||||
// states whether to load persisted history. No launcher means mint one here.
|
||||
const identity = ctx.get(uiTui.MAIN_SESSION_ID_KEY)
|
||||
const sessionId = SessionId(identity?.id ?? `main-session-${randomUUID()}`)
|
||||
const goals = config.goals ?? {}
|
||||
const persistenceRoot = config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT
|
||||
const persistenceRoot = config.persistenceRoot ?? ctx.get(uiTui.SESSIONS_ROOT_KEY) ?? DEFAULT_PERSISTENCE_ROOT
|
||||
ctx.plugin(CommandService)
|
||||
if (goals !== false) ctx.plugin(commandGoal)
|
||||
ctx.plugin(SessionPersistenceJsonl, {
|
||||
@@ -135,7 +141,6 @@ export function composeTuiApp(ctx: Context, config: Config): void {
|
||||
ctx.plugin(uiTui, {
|
||||
...config.ui,
|
||||
...config.welcome === undefined ? {} : { welcome: config.welcome },
|
||||
...config.resumeCommand === undefined ? {} : { resumeCommand: config.resumeCommand },
|
||||
sessionId,
|
||||
})
|
||||
ctx.plugin(agentCore, {
|
||||
@@ -146,7 +151,9 @@ export function composeTuiApp(ctx: Context, config: Config): void {
|
||||
provider: config.provider,
|
||||
model: config.model,
|
||||
cwd: process.cwd(),
|
||||
...resumeSessionId === undefined ? { sessionId } : { resumeSessionId: sessionId },
|
||||
// `resumeSessionId` requires existing persisted history and rejects a
|
||||
// missing log, so only a launcher that asked to resume takes that path.
|
||||
...identity?.resume === true ? { resumeSessionId: sessionId } : { sessionId },
|
||||
}],
|
||||
})
|
||||
ctx.plugin(toolAskUser)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { join } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { MAIN_SESSION_ID_KEY, SESSIONS_ROOT_KEY, type MainSessionIdentity } from '@deepseek-ai/dsh-tui'
|
||||
import * as tuiAgent from '../src/index.ts'
|
||||
|
||||
interface PluginCall {
|
||||
@@ -10,12 +12,21 @@ interface PluginCall {
|
||||
readonly config: unknown
|
||||
}
|
||||
|
||||
function recordingContext(): { readonly ctx: Context; readonly calls: PluginCall[] } {
|
||||
/**
|
||||
* Record the composed plugin tree. `identity` stands in for the launcher-owned
|
||||
* {@link MAIN_SESSION_ID_KEY} slot; omitting it means no launcher chose a session.
|
||||
*/
|
||||
function recordingContext(
|
||||
identity?: MainSessionIdentity,
|
||||
sessionsRoot?: string,
|
||||
): { readonly ctx: Context; readonly calls: PluginCall[] } {
|
||||
const calls: PluginCall[] = []
|
||||
const ctx = {
|
||||
plugin(plugin: { name?: string }, config?: unknown) {
|
||||
calls.push({ name: plugin.name ?? '', config })
|
||||
},
|
||||
get: (key: string) => key === MAIN_SESSION_ID_KEY ? identity
|
||||
: key === SESSIONS_ROOT_KEY ? sessionsRoot : undefined,
|
||||
} as unknown as Context
|
||||
return { ctx, calls }
|
||||
}
|
||||
@@ -39,7 +50,6 @@ describe('dsh-tui-demo app', () => {
|
||||
maxReferenceBytes: 1234,
|
||||
},
|
||||
welcome: 'TUI ready',
|
||||
resumeCommand: 'dsh --resume {session}',
|
||||
ui: { theme: { color: false }, maxToolOutputLines: 3 },
|
||||
skills: { tool: { catalogDescriptionMaxLength: 8 } },
|
||||
toolBash: { enableRunInBackground: false },
|
||||
@@ -71,7 +81,6 @@ describe('dsh-tui-demo app', () => {
|
||||
const tuiConfig = calls[8]?.config as { sessionId: string }
|
||||
expect(tuiConfig).toMatchObject({
|
||||
welcome: 'TUI ready',
|
||||
resumeCommand: 'dsh --resume {session}',
|
||||
theme: { color: false },
|
||||
maxToolOutputLines: 3,
|
||||
})
|
||||
@@ -100,16 +109,46 @@ describe('dsh-tui-demo app', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('resumes the configured session and applies runtime defaults', () => {
|
||||
const { ctx, calls } = recordingContext()
|
||||
it('uses the launcher sessions-root slot through schema-normalized config', () => {
|
||||
// The Loader normalizes config through the schemastery Config BEFORE apply
|
||||
// runs. A schema .default() on persistenceRoot would materialize here and
|
||||
// permanently shadow the launcher slot — the regression this test pins.
|
||||
const normalized = tuiAgent.Config({
|
||||
provider: 'mock',
|
||||
model: 'mock-model',
|
||||
workspaceContext: false,
|
||||
} as never)
|
||||
expect(normalized.persistenceRoot).toBeUndefined()
|
||||
|
||||
const { ctx, calls } = recordingContext(undefined, '/launcher/sessions')
|
||||
tuiAgent.composeTuiApp(ctx, normalized)
|
||||
expect(calls[2]?.config).toMatchObject({ root: '/launcher/sessions' })
|
||||
expect(calls[4]?.config).toEqual({ path: join('/launcher/sessions', 'session-query.db') })
|
||||
})
|
||||
|
||||
it('lets an explicit persistenceRoot win over the launcher slot', () => {
|
||||
const { ctx, calls } = recordingContext(undefined, '/launcher/sessions')
|
||||
tuiAgent.composeTuiApp(ctx, {
|
||||
provider: 'mock',
|
||||
model: 'mock-model',
|
||||
persistenceRoot: '/explicit/root',
|
||||
workspaceContext: false,
|
||||
})
|
||||
expect(calls[2]?.config).toEqual({ root: '/explicit/root' })
|
||||
})
|
||||
|
||||
it('loads persisted history for a launcher-selected resume identity', () => {
|
||||
// The bundle default stays project-local: shared-store policy is the
|
||||
// launcher's, which patches `persistenceRoot` itself (the dsh CLI does).
|
||||
const { ctx, calls } = recordingContext({ id: SessionId('persisted-session'), resume: true })
|
||||
tuiAgent.composeTuiApp(ctx, {
|
||||
provider: 'mock',
|
||||
model: 'mock-model',
|
||||
resumeSessionId: 'persisted-session',
|
||||
workspaceContext: false,
|
||||
})
|
||||
|
||||
expect(calls[2]?.config).toEqual({ root: './.sessions' })
|
||||
expect(calls[4]?.config).toEqual({ path: join('./.sessions', 'session-query.db') })
|
||||
expect(calls[5]?.config).toEqual({})
|
||||
// No configured welcome forwards none: the TUI banner sweeps in without a subtitle.
|
||||
expect(calls[8]?.config).toEqual({ sessionId: 'persisted-session' })
|
||||
@@ -119,12 +158,24 @@ describe('dsh-tui-demo app', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes an empty resume id and routes apply through the same composition', () => {
|
||||
it('creates a launcher-minted identity fresh rather than loading history', () => {
|
||||
const { ctx, calls } = recordingContext({ id: SessionId('minted-session'), resume: false })
|
||||
tuiAgent.composeTuiApp(ctx, {
|
||||
provider: 'mock',
|
||||
model: 'mock-model',
|
||||
workspaceContext: false,
|
||||
})
|
||||
|
||||
expect(calls[8]?.config).toEqual({ sessionId: 'minted-session' })
|
||||
expect((calls[9]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
|
||||
.toMatchObject({ id: 'main', sessionId: 'minted-session' })
|
||||
})
|
||||
|
||||
it('mints a fresh session with no launcher slot and routes apply through the same composition', () => {
|
||||
const { ctx, calls } = recordingContext()
|
||||
tuiAgent.apply(ctx, {
|
||||
provider: 'mock',
|
||||
model: 'mock-model',
|
||||
resumeSessionId: '',
|
||||
goals: false,
|
||||
workspaceContext: false,
|
||||
})
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../util/paths"
|
||||
},
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user