mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/fix/web-agent-runtime-context' into worktree/pr830-retarget-review-20260801
# Conflicts: # apps/cli/src/web.ts
This commit is contained in:
@@ -155,6 +155,8 @@ export interface AppCLIEntryOptions {
|
||||
workspaceRoot?: string
|
||||
/** Extra authorities for the /api browser-trust fence (`host` or `host:port`), appended to the derived LAN IP literals. */
|
||||
trustedHosts?: string[]
|
||||
/** Surface setup registered after Loader installation and before any config-tree entry mounts. */
|
||||
prepare?: (ctx: Context) => Promise<void> | void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,8 +182,8 @@ export class AppCLIEntry {
|
||||
constructor(private readonly options: AppCLIEntryOptions) {}
|
||||
|
||||
/**
|
||||
* Run the boot chain: patch composition → Loader include boot (dev row
|
||||
* before await) → fail-loud triple.
|
||||
* Run the boot chain: patch composition → Loader installation → surface
|
||||
* preparation → config-tree boot (dev row before await) → fail-loud triple.
|
||||
* @returns the settled root context and the listening port.
|
||||
*/
|
||||
async run(): Promise<{ ctx: Context; port: number }> {
|
||||
@@ -246,7 +248,7 @@ export class AppCLIEntry {
|
||||
if (telemetryPatch !== undefined) this.patches.push(telemetryPatch)
|
||||
}
|
||||
|
||||
/** Shared Loader boot; the dev HMR row mounts before await so the activation audit covers it. */
|
||||
/** Shared Loader boot; surface preparation precedes the tree, and the dev HMR row precedes the activation audit. */
|
||||
private async bootTree(): Promise<void> {
|
||||
// One include of the shared base with every overlay as a sibling patch
|
||||
// list: patches never cross an include boundary, so nesting them would
|
||||
@@ -260,6 +262,7 @@ export class AppCLIEntry {
|
||||
...this.patches,
|
||||
]
|
||||
this.ctx = await boot('dsh', resolve(this.options.configPath), patches, async (ctx) => {
|
||||
await this.options.prepare?.(ctx)
|
||||
if (this.options.dev) await ctx.loader.create({ name: '@deepseek-ai/dsh-client-hmr' })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import { addHarnessSourceSection, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tool-bash'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
@@ -23,6 +24,10 @@ const DSH_WEB_MODE = 'DSH_WEB_MODE' as const
|
||||
|
||||
type WebMode = 'production' | 'development'
|
||||
|
||||
// Display-only mirror of the webserver schema's loopback host: the address the
|
||||
// local URL always prints. Not a source of truth — the schema is.
|
||||
const LOOPBACK_HOST = '127.0.0.1'
|
||||
|
||||
/** Model-visible orientation and acceptance boundary for sessions created through `dsh web`. */
|
||||
function webSurfacePrompt(webUrl: string, mode: WebMode): string {
|
||||
const updateContract = mode === 'development'
|
||||
@@ -40,36 +45,50 @@ function webSurfacePrompt(webUrl: string, mode: WebMode): string {
|
||||
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.'
|
||||
}
|
||||
|
||||
/** Resolve the canonical loopback URL from the active Web server. */
|
||||
function localWebUrl(ctx: Context): string {
|
||||
const port = ctx.get('httpServer')?.port
|
||||
if (port === undefined) throw new Error('dsh web: httpServer service missing while resolving Web runtime')
|
||||
return `http://${LOOPBACK_HOST}:${String(port)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Add launcher-owned source, Web-surface orientation, and the shell-visible
|
||||
* canonical URL after the shared config tree settles. The request header logs
|
||||
* the model-visible sections; each bash execution receives the same URL through
|
||||
* the managed environment.
|
||||
* @param ctx - settled Web application context.
|
||||
* Register the launcher-owned prompt and shell runtime context before the
|
||||
* shared config tree mounts. The earlier injections install the prompt
|
||||
* sections and managed Bash contributor when their owning services activate;
|
||||
* dynamic values read the bound server only when consumed.
|
||||
* @param ctx - Web root context with Loader installed but no config tree mounted.
|
||||
* @param sourceRoot - absolute checkout root resolved from the launcher module.
|
||||
* @param webUrl - canonical loopback URL printed by this Web process.
|
||||
* @param mode - whether this process mounted the client-plugin HMR receiver.
|
||||
*/
|
||||
export function installWebPromptContext(ctx: Context, sourceRoot: string, webUrl: string, mode: WebMode): void {
|
||||
const systemPrompt = ctx.get('systemPrompt')
|
||||
if (systemPrompt === undefined) throw new Error('dsh web: systemPrompt service missing after settled boot')
|
||||
const bashEnv = ctx.get('bashEnv')
|
||||
if (bashEnv === undefined) throw new Error('dsh web: bashEnv service missing after settled boot')
|
||||
addHarnessSourceSection(ctx, sourceRoot)
|
||||
systemPrompt.section({ name: 'app:web-surface', order: -98, text: webSurfacePrompt(webUrl, mode) })
|
||||
bashEnv.register({
|
||||
name: 'web-runtime',
|
||||
variables: {
|
||||
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
|
||||
[DSH_WEB_MODE]: { description: 'Web runtime mode: production, or development when the client-plugin HMR receiver is active.' },
|
||||
},
|
||||
resolve: () => ({ [DSH_WEB_URL]: webUrl, [DSH_WEB_MODE]: mode }),
|
||||
export function prepareWebRuntimeContext(ctx: Context, sourceRoot: string, mode: WebMode): void {
|
||||
ctx.inject(['systemPrompt'], (promptCtx) => {
|
||||
addHarnessSourceSection(promptCtx, sourceRoot)
|
||||
promptCtx.systemPrompt.section({
|
||||
name: 'app:web-surface',
|
||||
order: -98,
|
||||
text: () => webSurfacePrompt(localWebUrl(promptCtx), mode),
|
||||
})
|
||||
})
|
||||
ctx.inject(['bashEnv'], (runtimeCtx) => {
|
||||
runtimeCtx.bashEnv.register({
|
||||
name: 'web-runtime',
|
||||
variables: {
|
||||
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
|
||||
[DSH_WEB_MODE]: { description: 'Web runtime mode: production, or development when the client-plugin HMR receiver is active.' },
|
||||
},
|
||||
resolve: () => ({ [DSH_WEB_URL]: localWebUrl(runtimeCtx), [DSH_WEB_MODE]: mode }),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Display-only mirror of the webserver schema's loopback host: the address the
|
||||
// local URL always prints. Not a source of truth — the schema is.
|
||||
const LOOPBACK_HOST = '127.0.0.1'
|
||||
/**
|
||||
* Fail a settled Web boot whose composition omitted the managed Bash environment registry.
|
||||
* @param ctx - settled Web application context.
|
||||
*/
|
||||
export function assertWebRuntimeContext(ctx: Context): void {
|
||||
if (ctx.get('bashEnv') === undefined) throw new Error('dsh web: bashEnv service missing after settled boot')
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the browser UI from the shipped config tree. `host`/`port` are passed
|
||||
@@ -91,20 +110,21 @@ export async function runWeb(
|
||||
trustedHosts: string[] | undefined,
|
||||
config?: string,
|
||||
): Promise<void> {
|
||||
const mode: WebMode = dev ? 'development' : 'production'
|
||||
const entry = new AppCLIEntry({
|
||||
configPath: BASE_CONFIG,
|
||||
overlayPath: WEB_OVERLAY,
|
||||
...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) },
|
||||
dev,
|
||||
prepare: (ctx) => { prepareWebRuntimeContext(ctx, SOURCE_ROOT, mode) },
|
||||
...host !== undefined && { host },
|
||||
...port !== undefined && { port },
|
||||
...workspaceRoot !== undefined && { workspaceRoot },
|
||||
...trustedHosts !== undefined && { trustedHosts },
|
||||
})
|
||||
const { ctx, port: boundPort } = await entry.run()
|
||||
const localUrl = `http://${LOOPBACK_HOST}:${boundPort}`
|
||||
const mode: WebMode = dev ? 'development' : 'production'
|
||||
installWebPromptContext(ctx, SOURCE_ROOT, localUrl, mode)
|
||||
assertWebRuntimeContext(ctx)
|
||||
const resolvedLocalWebUrl = localWebUrl(ctx)
|
||||
|
||||
let exiting = false
|
||||
const shutdown = (code: number): void => {
|
||||
@@ -121,5 +141,5 @@ export async function runWeb(
|
||||
// The entry's boot-time snapshot, not a fresh sample: the printed LAN URL
|
||||
// must name an address the /api trust fence was configured with.
|
||||
const lanCandidate = entry.lanAddresses[0]
|
||||
console.log(`dsh web: ${localUrl}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${boundPort})`}`)
|
||||
console.log(`dsh web: ${resolvedLocalWebUrl}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${boundPort})`}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user