Dual-version compatibility: the plugin now works identically on
0.1.0-rc.8 ~ 0.1.1-rc.2 and 0.1.2-alpha.1 (the latter verified against a
source build of the dsh-v0.1.2-alpha.1 tag: mount smoke 14/14 + aggregate
double-mount; 0.1.1-rc.2 re-verified 14/14).
- MarkdownText: alpha.1 renamed the flat optional `codeLabels` prop to a
required nested `labels: { code: {copyLabel, copiedLabel}, footnotes }`;
the old prop alone crashes fence rendering with "reading 'code'". All
four render sites (mermaid / MarkdownHtml / TextEditor / SideChatView)
now go through src/client/markdown-labels.tsx (dual-shape object passed
under both prop names).
- e2e mount lanes speak both host dialects (tests/e2e/host-protocol.ts +
host.ts, unit-locked by tests/e2e-host-protocol.spec.ts):
- one-time-token browser auth: the launch URL grep extends past the
`/?token=` query (truncating at `/` loses the token), goto navigates
the token URL (browser cookie exchange), and stamped navigations
pre-seed the context cookie via addCookies — the exchange 303 drops
sibling query params;
- Remote gateway: dot endpoints first, slash endpoints on 404 fallback,
with args keyed by the controller parameter name (workspace/create ->
{request}, session/list -> {_request}; verified on a live host).
- package.json: drop the @deepseek-ai/dsh-client-runtime peer (package
removed upstream in alpha.1; zero references in the source — the chunk
externals entry stays and is resolution-tolerant).
- scripts/e2e-mount.sh: fix a latent set -u crash in the npx fallback
branch ($DSH_CMD followed by a full-width comma parsed as part of the
variable name).
- CI pin stays at @deepseek-ai/dsh@0.1.1-rc.2 until alpha.1 publishes to
npm (comment updated); README/AGENTS document the new contracts.
- Version lockstep bumped to 0.17.1.
105 lines
5.0 KiB
TypeScript
105 lines
5.0 KiB
TypeScript
/**
|
|
* Pure host-transport contracts for the e2e lanes, spanning the DSH
|
|
* 0.1.1-rc.x → 0.1.2-alpha.1 breaking split (Remote gateway + one-time-token
|
|
* browser auth). No Playwright imports here — these shapes are unit-testable
|
|
* (tests/e2e-host-protocol.spec.ts); ./host.ts wires them to a request
|
|
* context.
|
|
*
|
|
* What changed in 0.1.2-alpha.1 (deepseek-harness tag dsh-v0.1.2-alpha.1):
|
|
*
|
|
* - `dsh web` prints an AUTHENTICATED launch URL — `dsh web:
|
|
* http://127.0.0.1:<port>/?token=<43 base64url chars>`. Navigating it
|
|
* exchanges the token for a signed cookie (HttpOnly, SameSite=Strict) that
|
|
* every `/api` request must then carry; the clean URL answers 401. Older
|
|
* hosts print a bare origin and have no browser auth.
|
|
* - The legacy ApiProxy dot-method endpoints (`POST /api/workspace.create`
|
|
* with `payload: <args>`) were replaced by the Remote gateway's slash
|
|
* endpoints — `POST /api/workspace/create` with `payload: { args }` whose
|
|
* args object is keyed by the controller's TypeScript PARAMETER name
|
|
* (`workspace/create` → `{ request: {...} }`; `session/list`'s unused
|
|
* parameter is literally `_request` and is not omissible); a dot path is
|
|
* no longer claimed (404). The `{type:'client-request', rpcId, method,
|
|
* payload}` / `{type:'server-response', rpcId, result}` envelopes are
|
|
* unchanged, and the plugin's own `/sidebar/*` routes stay public (the
|
|
* webserver carrier owns no authentication — only `/api`, the index HTML,
|
|
* and the remote mux upgrade sit behind the browser auth).
|
|
*/
|
|
|
|
/** The pieces of a `dsh web` launch URL the lanes need. */
|
|
export interface LaunchUrl {
|
|
/** Scheme + host + port — the base for URL construction (a token URL would
|
|
* corrupt path concatenation). */
|
|
origin: string
|
|
/** The URL as printed (token query included on 0.1.2-alpha.1+); the correct
|
|
* page.goto target — navigating it performs the token→cookie exchange. */
|
|
pageUrl: string
|
|
/** The one-time launch token, when the host printed one. */
|
|
token: string | undefined
|
|
}
|
|
|
|
/** Split a `dsh web` launch URL (bare origin on 0.1.1-rc.x, `/?token=` on
|
|
* 0.1.2-alpha.1+) into the pieces the lanes address. */
|
|
export function parseLaunchUrl(raw: string): LaunchUrl {
|
|
const url = new URL(raw)
|
|
return {
|
|
origin: url.origin,
|
|
pageUrl: raw,
|
|
token: url.searchParams.get('token') ?? undefined,
|
|
}
|
|
}
|
|
|
|
/** Page navigation URL with extra query stamps (e.g. desktop-shell URL
|
|
* parameters) merged in — never naively append `?...` to a token URL (a
|
|
* double `?` breaks both params). The token survives the merge. */
|
|
export function pageUrlWith(raw: string, extra: Record<string, string>): string {
|
|
const url = new URL(raw)
|
|
for (const [key, value] of Object.entries(extra)) url.searchParams.set(key, value)
|
|
return url.toString()
|
|
}
|
|
|
|
/** One host-RPC protocol attempt. */
|
|
export interface RpcAttempt {
|
|
/** Which host dialect this attempt speaks. */
|
|
protocol: 'dot' | 'slash'
|
|
/** Request path, relative to the origin, starting with `/api/`. */
|
|
path: string
|
|
/** Envelope `method` string — must equal the endpoint the path selects. */
|
|
method: string
|
|
/** Envelope `payload` — the bare args object on dot hosts, `{ args }` on
|
|
* slash hosts (the gateway rejects any other payload shape). */
|
|
payload: Record<string, unknown>
|
|
}
|
|
|
|
/** The slash-dialect args key per host method: the 0.1.2-alpha.1 Remote
|
|
* controllers key the args object by the TypeScript PARAMETER name, not by
|
|
* the endpoint's own naming — workspace/create and session/create declare a
|
|
* single `request` parameter, while session/list's unused parameter is
|
|
* literally named `_request` (and is NOT omissible: the typert gateway
|
|
* rejects `{}` and `{request:{}}` alike). Verified against a live
|
|
* dsh-v0.1.2-alpha.1 host: every other shape fails with `args fields do not
|
|
* match the descriptor`. */
|
|
const SLASH_ARGS_KEY: Record<string, string> = {
|
|
'workspace.create': 'request',
|
|
'session.create': 'request',
|
|
'session.list': '_request',
|
|
}
|
|
|
|
/** Build the two dialects of one host RPC (method in dot form, e.g.
|
|
* `'workspace.create'`): the 0.1.1-rc.x dot endpoint first — the dialect
|
|
* every currently-published host speaks, so the deployed lanes never pay a
|
|
* probe round-trip — then the 0.1.2-alpha.1+ slash endpoint as the 404
|
|
* fallback. Throws for methods without a known slash args key: the wrapper
|
|
* is per-parameter, so an unverified shape must fail loudly at build time
|
|
* instead of as a confusing gateway error mid-lane. */
|
|
export function rpcAttempts(method: string, args: Record<string, unknown>): RpcAttempt[] {
|
|
const slashKey = SLASH_ARGS_KEY[method]
|
|
if (slashKey === undefined) {
|
|
throw new Error(`host-protocol: no slash-dialect args key for ${JSON.stringify(method)} — verify the parameter name on the host and extend SLASH_ARGS_KEY`)
|
|
}
|
|
const slash = method.split('.').join('/')
|
|
return [
|
|
{ protocol: 'dot', path: `/api/${method}`, method, payload: args },
|
|
{ protocol: 'slash', path: `/api/${slash}`, method: slash, payload: { args: { [slashKey]: args } } },
|
|
]
|
|
}
|