1
#!/usr/bin/env node2
/**3
* Scripted stand-in for the DeepSeek Harness SDK runtime, driven entirely by4
* env vars — no model, no network, no harness imports. Speaks the runtime's5
* newline-delimited JSON-RPC protocol on stdio: answers `initialize`,6
* `session/prompt` (streaming scripted `session.event` notifications, then7
* `session.finished`, then the response), and `shutdown`.8
*9
* Script vocabulary (all optional):10
* - `FAKE_TEXT`: assistant text for each turn (default `hello from fake runtime`).11
* - `FAKE_STATUS`: the `session.finished` status (default `ok`).12
* - `FAKE_REASON_KIND`: the `session.finished` reason kind (default `completed`; `none` omits the reason).13
* - `FAKE_ABORT_REASON_KIND`: nested cause for an `aborted` turn (default `user`).14
* - `FAKE_SUBAGENT`: also emit a child session (subagent.started + child event + subagent.finished).15
* - `FAKE_ECHO_CWD`: prefix the assistant text with the process cwd.16
* - `FAKE_ECHO_ENV`: comma-separated env names to echo as `name=value` lines in the assistant text.17
* - `FAKE_MALFORMED`: `initialize` returns `{}` (no serverInfo); `prompt` returns `{}` (no accepted).18
* - `FAKE_MALFORMED_PROMPT`: `initialize` is normal; only `prompt` returns `{}` (no accepted).19
* - `FAKE_INIT_ERROR`: `initialize` answers a JSON-RPC error response with code 7.20
* - `FAKE_INIT_ERROR_ONCE_FILE`: fail `initialize` (code 7) only when this21
* marker file does NOT exist yet, creating it — so the first runtime22
* process fails the handshake and a respawned one succeeds (retry probe).23
* - `FAKE_ECHO_CWD_IN_INIT`: reply `serverInfo.version` = this process's cwd24
* (wire-visible spawn-cwd probe).25
* - `FAKE_MALFORMED_EVENT`: the turn's `session.event` carries a number as26
* the event; `FAKE_MALFORMED_MESSAGE`: assistant/message content is not an27
* array; `FAKE_MESSAGE_WITHOUT_DATA`: assistant/message with no data28
* member; `FAKE_MALFORMED_REASON`: the `turn/end` carries a bare reason29
* (`1`), an aborted reason without its cause (`aborted`), an unknown abort30
* cause (`abort-unknown`), a hook cause without its reason (`hook`), or no31
* data member (`no-data`) for wire-validation probes.32
* - `FAKE_EMPTY_MESSAGE`: record an empty assistant/message whose embedded33
* stream contains only usage and max-tokens settlement.34
* - `FAKE_HANG_INIT`: never answer `initialize` (mid-handshake cancel probe).35
* - `FAKE_INIT_READY` + `FAKE_INIT_GO`: touch the READY file when `initialize`36
* arrives, then poll for the GO file before answering (deterministic37
* cancel-during-handshake window).38
* - `FAKE_HANG_PROMPT`: never answer `session/prompt` (for timeout/dispose tests).39
* - `FAKE_EXIT_DURING_PROMPT`: commit one interrupted assistant message, then40
* exit 17 while the owned session run is waiting for its terminal state.41
* - `FAKE_STREAM_THEN_MALFORMED`: commit a partial assistant attempt for the42
* prompt, then answer `{}` (no accepted) — same-pipe ordering makes the43
* attempt arrive before the protocol failure (partial-output retention probe).44
* - `FAKE_IGNORE_EOF` + `FAKE_SIGTERM_FILE`: keep running after stdin EOF; touch the file on SIGTERM (ladder probe).45
* - `FAKE_TRAP_SIGTERM`: with `FAKE_IGNORE_EOF`, survive SIGTERM too (SIGKILL-rung probe).46
* - `FAKE_EXIT_BEFORE_INIT`: exit 3 immediately (spawn-then-die probe).47
* - `FAKE_STDERR`: write this line to stderr at boot (diagnostics-tail probe).48
* - `FAKE_STDERR_NO_NEWLINE`: write this to stderr WITHOUT a newline (buffer-flush probe).49
* - `FAKE_RECORD_INIT`: append each `initialize` params JSON to this file (handshake probe).50
*/52
import { appendFileSync, existsSync, writeFileSync } from 'node:fs'53
import process from 'node:process'54
import { createInterface } from 'node:readline'56
const env = process.env58
if (env.FAKE_STDERR !== undefined) process.stderr.write(`${env.FAKE_STDERR}\n`)59
if (env.FAKE_STDERR_NO_NEWLINE !== undefined) process.stderr.write(env.FAKE_STDERR_NO_NEWLINE)60
if (env.FAKE_EXIT_BEFORE_INIT !== undefined) process.exit(3)62
if (env.FAKE_IGNORE_EOF !== undefined) {63
// Simulate a runtime that never quiesces from EOF so the dispose ladder64
// must escalate; record which rung fired.65
process.stdin.resume()66
process.stdin.on('end', () => { setInterval(() => {}, 1_000) })67
process.on('SIGTERM', () => {68
if (env.FAKE_SIGTERM_FILE !== undefined) writeFileSync(env.FAKE_SIGTERM_FILE, 'sigterm\n')69
if (env.FAKE_TRAP_SIGTERM === undefined) process.exit(0)70
})71
}73
function write(message: object): void {74
process.stdout.write(`${JSON.stringify(message)}\n`)75
}77
function notify(method: string, params: object): void {78
write({ jsonrpc: '2.0', method, params })79
}81
let seq = 082
function event(sessionId: string, type: string, data: object): void {83
notify('session.event', { sessionId, event: { type, seq: seq++, time: 0, data } })84
}86
function assistantText(): string {87
const parts: string[] = []88
if (env.FAKE_ECHO_CWD !== undefined) parts.push(`cwd=${process.cwd()}`)89
for (const name of (env.FAKE_ECHO_ENV ?? '').split(',').filter(entry => entry.length > 0)) {90
parts.push(`${name}=${env[name] ?? ''}`)91
}92
parts.push(env.FAKE_TEXT ?? 'hello from fake runtime')93
return parts.join('\n')94
}96
function textStream(text: string): object[] {97
return [98
{ type: 'chunk', time: 0, chunk: { type: 'block-start', index: 0, blockType: 'text' } },99
{ type: 'text-chunks', time0: 0, index: 0, dt: [], texts: [text] },100
{ type: 'chunk', time: 0, chunk: { type: 'block-end', index: 0, block: { type: 'text', text } } },101
{ type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'stop' } } },102
]103
}105
function usageOnlyStream(): object[] {106
return [107
{108
type: 'chunk',109
time: 0,110
chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 0, totalTokens: 1 } },111
},112
{ type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'max-tokens' } } },113
]114
}116
function runTurn(sessionId: string): void {117
const text = assistantText()118
if (env.FAKE_MALFORMED_EVENT !== undefined) {119
notify('session.event', { sessionId, event: 42 })120
return121
}122
event(sessionId, 'turn/start', { turn: 0 })123
if (env.FAKE_MALFORMED_MESSAGE !== undefined) {124
event(sessionId, 'assistant/attempt', {125
turn: 0,126
step: 0,127
stream: textStream(text),128
})129
event(sessionId, 'assistant/message', {130
turn: 0,131
step: 0,132
message: {133
id: 'fake-malformed-message',134
role: 'assistant',135
content: 'not-an-array',136
source: { kind: 'model', provider: 'fake', model: 'fake' },137
},138
stream: textStream(text),139
})140
return141
}142
if (env.FAKE_MESSAGE_WITHOUT_DATA !== undefined) {143
notify('session.event', { sessionId, event: { type: 'assistant/message', seq: seq++, time: 0 } })144
return145
}146
if (env.FAKE_EMPTY_MESSAGE !== undefined) {147
event(sessionId, 'assistant/attempt', {148
turn: 0,149
step: 0,150
stream: textStream(text),151
})152
}153
event(sessionId, 'assistant/message', {154
turn: 0,155
step: 0,156
message: {157
id: `fake-assistant-${seq}`,158
role: 'assistant',159
// Model the usage-only message recorded after a max-tokens step that160
// assembled no output blocks.161
content: env.FAKE_EMPTY_MESSAGE !== undefined ? [] : [{ type: 'text', text }],162
source: { kind: 'model', provider: 'fake', model: 'fake' },163
},164
stream: env.FAKE_EMPTY_MESSAGE !== undefined ? usageOnlyStream() : textStream(text),165
})166
const reasonKind = env.FAKE_REASON_KIND ?? 'completed'167
if (reasonKind !== 'none') {168
if (env.FAKE_MALFORMED_REASON === 'no-data') {169
notify('session.event', { sessionId, event: { type: 'turn/end', seq: seq++, time: 0 } })170
return171
}172
const reason = env.FAKE_MALFORMED_REASON === 'aborted'173
? { kind: 'aborted' }174
: env.FAKE_MALFORMED_REASON === 'abort-unknown'175
? { kind: 'aborted', reason: { kind: 'future' } }176
: env.FAKE_MALFORMED_REASON === 'hook'177
? { kind: 'aborted', reason: { kind: 'hook' } }178
: env.FAKE_MALFORMED_REASON !== undefined179
? 'not-a-reason-envelope'180
: reasonKind === 'aborted'181
? {182
kind: 'aborted',183
reason: env.FAKE_ABORT_REASON_KIND === 'hook'184
? { kind: 'hook', reason: 'scripted hook abort' }185
: { kind: env.FAKE_ABORT_REASON_KIND ?? 'user' },186
}187
: reasonKind === 'error'188
? { kind: 'error', error: { message: 'scripted child error', code: 'UNKNOWN' } }189
: { kind: reasonKind }190
event(sessionId, 'turn/end', { turn: 0, reason })191
}192
if (env.FAKE_SUBAGENT !== undefined) {193
const childId = `${sessionId}-child`194
notify('subagent.started', { parentSessionId: sessionId, childSessionId: childId })195
event(childId, 'assistant/message', {196
turn: 0,197
step: 0,198
message: {199
id: `fake-child-${seq}`,200
role: 'assistant',201
content: [{ type: 'text', text: 'child says hi' }],202
source: { kind: 'model', provider: 'fake', model: 'fake' },203
},204
stream: textStream('child says hi'),205
})206
notify('subagent.finished', {207
provider: 'spawn',208
agentId: childId,209
parentSessionId: sessionId,210
childSessionId: childId,211
status: 'ok',212
stopReason: 'completed',213
lastAssistantMessage: [{ type: 'text', text: 'child says hi' }],214
})215
}216
}218
function sessionIdOf(params: Record<string, unknown> | undefined): string {219
const value = params?.sessionId220
return typeof value === 'string' ? value : ''221
}223
const reader = createInterface({ input: process.stdin })224
reader.on('line', (line) => {225
if (line.trim().length === 0) return226
const frame = JSON.parse(line) as { id?: string | number; method?: string; params?: Record<string, unknown> }227
if (frame.method === undefined || frame.id === undefined) return228
const respond = (result: object): void => { write({ jsonrpc: '2.0', id: frame.id, result }) }229
switch (frame.method) {230
case 'initialize':231
if (env.FAKE_RECORD_INIT !== undefined) appendFileSync(env.FAKE_RECORD_INIT, `${JSON.stringify(frame.params)}\n`)232
if (env.FAKE_HANG_INIT !== undefined) return233
if (env.FAKE_INIT_READY !== undefined && env.FAKE_INIT_GO !== undefined) {234
writeFileSync(env.FAKE_INIT_READY, 'ready\n')235
const go = env.FAKE_INIT_GO236
const id = frame.id237
const poll = setInterval(() => {238
if (!existsSync(go)) return239
clearInterval(poll)240
write({ jsonrpc: '2.0', id, result: { serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } } })241
}, 5)242
return243
}244
if (env.FAKE_INIT_ERROR !== undefined) {245
write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted init failure', data: { hint: 'fake' } } })246
return247
}248
if (env.FAKE_INIT_ERROR_ONCE_FILE !== undefined && !existsSync(env.FAKE_INIT_ERROR_ONCE_FILE)) {249
writeFileSync(env.FAKE_INIT_ERROR_ONCE_FILE, 'failed-once\n')250
write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted first-boot failure' } })251
return252
}253
if (env.FAKE_MALFORMED !== undefined) {254
respond({})255
return256
}257
if (env.FAKE_ECHO_CWD_IN_INIT !== undefined) {258
respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: process.cwd() } })259
return260
}261
respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } })262
return263
case 'session/prompt': {264
const sessionId = sessionIdOf(frame.params)265
const messageId = `fake-user-${seq}`266
event(sessionId, 'agent/inbox/spliced', {267
target: 'next-turn',268
start: 0,269
inserted: [{270
id: messageId,271
role: 'user',272
content: [],273
source: { kind: 'user' },274
}],275
})276
notify('session.status', { sessionId, status: 'running' })277
if (env.FAKE_STREAM_THEN_MALFORMED !== undefined) {278
event(sessionId, 'assistant/attempt', {279
turn: 0,280
step: 0,281
stream: textStream('streamed then cut short'),282
})283
respond({})284
return285
}286
if (env.FAKE_EXIT_DURING_PROMPT !== undefined) {287
const partial = env.FAKE_TEXT ?? 'partial before exit'288
respond({ messageId })289
event(sessionId, 'assistant/message', {290
turn: 0,291
step: 0,292
message: {293
id: `fake-partial-${seq}`,294
role: 'assistant',295
content: [{ type: 'text', text: partial }],296
source: { kind: 'model', provider: 'fake', model: 'fake' },297
},298
stream: textStream(partial),299
interrupted: true,300
})301
setImmediate(() => { process.exit(17) })302
return303
}304
if (env.FAKE_HANG_PROMPT !== undefined) return305
if (env.FAKE_MALFORMED !== undefined || env.FAKE_MALFORMED_PROMPT !== undefined) {306
respond({})307
return308
}309
runTurn(sessionId)310
notify('session.status', { sessionId, status: 'idle' })311
respond({ messageId })312
return313
}314
case 'shutdown':315
respond({})316
// An EOF-ignoring fake also refuses the protocol exit, so the client's317
// dispose ladder (not this cooperative path) must reap it.318
if (env.FAKE_IGNORE_EOF === undefined) setImmediate(() => process.exit(0))319
return320
default:321
write({ jsonrpc: '2.0', id: frame.id, error: { code: -32603, message: `unknown method: ${frame.method}` } })322
}323
})