kiokuko-dsh 0.2.38 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/dsh/core/host.js +4 -4
  2. package/dist/dsh/core/host.js.map +1 -1
  3. package/dist/dsh/decisions/diagnostics.d.ts +4 -3
  4. package/dist/dsh/decisions/diagnostics.d.ts.map +1 -1
  5. package/dist/dsh/decisions/diagnostics.js +2 -2
  6. package/dist/dsh/decisions/diagnostics.js.map +1 -1
  7. package/dist/dsh/decisions/intent-assessment.d.ts +10 -4
  8. package/dist/dsh/decisions/intent-assessment.d.ts.map +1 -1
  9. package/dist/dsh/decisions/intent-assessment.js +26 -10
  10. package/dist/dsh/decisions/intent-assessment.js.map +1 -1
  11. package/dist/dsh/decisions/openai.d.ts.map +1 -1
  12. package/dist/dsh/decisions/openai.js +3 -0
  13. package/dist/dsh/decisions/openai.js.map +1 -1
  14. package/dist/dsh/host-adapter.js +4 -4
  15. package/dist/dsh/host-adapter.js.map +1 -1
  16. package/dist/dsh/lisp/sandbox.d.ts +1 -0
  17. package/dist/dsh/lisp/sandbox.d.ts.map +1 -1
  18. package/dist/dsh/lisp/sandbox.js +2 -2
  19. package/dist/dsh/lisp/sandbox.js.map +1 -1
  20. package/dist/dsh/on-demand-intake.d.ts +19 -4
  21. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  22. package/dist/dsh/on-demand-intake.js +154 -61
  23. package/dist/dsh/on-demand-intake.js.map +1 -1
  24. package/dist/dsh/task-startup.d.ts +4 -0
  25. package/dist/dsh/task-startup.d.ts.map +1 -1
  26. package/dist/dsh/task-startup.js +8 -2
  27. package/dist/dsh/task-startup.js.map +1 -1
  28. package/dist/locale/en.json +1 -1
  29. package/dist/locale/ja.json +1 -1
  30. package/dist/locale/ko.json +1 -1
  31. package/dist/locale/zh-CN.json +1 -1
  32. package/docs/micro-judge.md +1 -1
  33. package/docs/on-demand-intake.md +2 -0
  34. package/lisp/supervisor.mjs +90 -9
  35. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  // Trusted host helper, never evaluated inside Lisp. stdin is a parent-liveness
2
2
  // pipe. It stays outside the worker sandbox so arbitrary Lisp cannot disable it.
3
- import { spawn } from 'node:child_process'
4
- import { openSync, closeSync, writeSync } from 'node:fs'
3
+ import { spawn, execFileSync } from 'node:child_process'
4
+ import { openSync, closeSync, writeSync, readFileSync } from 'node:fs'
5
5
  import { claimRepository } from './repository-owner.mjs'
6
+ import { processIdentity, sameProcess } from './process-identity.mjs'
6
7
  const launch = JSON.parse(process.argv[2])
7
8
  let repositoryLease
8
9
  if (launch.repositoryRoot) {
@@ -13,8 +14,60 @@ if (launch.repositoryRoot) {
13
14
  }
14
15
  }
15
16
  let child, closing = false, terminationSent = false
17
+ let namespace, terminal, finishTimer, ownershipError = false
18
+ // Pending promises and SQLite leases do not keep Node alive after stdin EOF
19
+ // and monitor exit. Keep unconfirmed ownership visible to the host watchdog.
20
+ const namespaceLifetime = launch.namespaceInfo ? setInterval(() => {}, 60000) : undefined
21
+ const unconfirmed = message => {
22
+ if (!ownershipError) console.error(`LISP_STOP_UNCONFIRMED: ${message}`)
23
+ ownershipError = true
24
+ }
25
+ const reportRuntime = pid => {
26
+ if (!repositoryLease) return
27
+ try {
28
+ const identity = repositoryLease.recordRuntime(pid)
29
+ writeSync(4, JSON.stringify({ type: 'runtime', pid, identity })+'\n')
30
+ } catch {
31
+ try { writeSync(4, JSON.stringify({ type: 'error', code: 'REPOSITORY_LOCK_FAILED' })+'\n') } finally { stop() }
32
+ }
33
+ }
34
+ const parentPid = pid => process.platform === 'linux'
35
+ ? Number(readFileSync(`/proc/${pid}/stat`, 'utf8').replace(/^.*\) /, '').split(' ')[1])
36
+ : Number(execFileSync('/bin/ps', ['-p', String(pid), '-o', 'ppid='], { encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] }).trim())
37
+ const finish = () => {
38
+ if (!terminal) return
39
+ if (launch.namespaceInfo) {
40
+ if (!namespace) return // Missing ownership evidence never confirms Stop.
41
+ try {
42
+ if (sameProcess(namespace.pid, namespace.identity)) {
43
+ finishTimer ??= setTimeout(() => { finishTimer = undefined; finish() }, 10)
44
+ return
45
+ }
46
+ } catch { unconfirmed('cannot confirm sandbox child termination'); return }
47
+ }
48
+ clearTimeout(finishTimer)
49
+ clearInterval(namespaceLifetime)
50
+ repositoryLease?.close()
51
+ process.exit(terminal.signal ? 128 : terminal.code ?? 70)
52
+ }
16
53
  const kill = () => {
17
54
  if (!child?.pid || terminationSent) return
55
+ if (launch.namespaceInfo) {
56
+ // bwrap's monitor can fork before its namespace init installs PDEATHSIG.
57
+ // Keep the monitor alive until its private info pipe identifies that child.
58
+ if (!namespace) return
59
+ try {
60
+ if (sameProcess(namespace.pid, namespace.identity)) {
61
+ if (!namespace.identity || processIdentity(namespace.pid) !== namespace.identity) throw new Error('identity unavailable')
62
+ process.kill(namespace.pid, 'SIGKILL')
63
+ }
64
+ terminationSent = true
65
+ } catch (error) {
66
+ if (error.code === 'ESRCH') terminationSent = true
67
+ else unconfirmed('sandbox child termination was not authorized or its identity changed')
68
+ }
69
+ return
70
+ }
18
71
  try {
19
72
  if (launch.group && child?.pid) process.kill(-child.pid, 'SIGKILL')
20
73
  else child.kill('SIGKILL')
@@ -33,22 +86,50 @@ process.stdin.resume()
33
86
  const filter = launch.seccompPath ? openSync(launch.seccompPath, 'r') : undefined
34
87
  const stdio = ['ignore', 'inherit', 'inherit', launch.protocol ? 3 : 'ignore']
35
88
  if (filter !== undefined) stdio.push(filter)
89
+ if (launch.namespaceInfo) {
90
+ while (stdio.length < 5) stdio.push('ignore')
91
+ stdio.push('pipe')
92
+ }
36
93
  child = spawn(launch.command, launch.args, { cwd: launch.cwd, env: launch.env, stdio, detached: !!launch.group })
37
94
  if (filter !== undefined) closeSync(filter)
95
+ if (launch.namespaceInfo) {
96
+ let info = '', excessive = false
97
+ child.stdio[5].on('data', chunk => {
98
+ if (excessive) return
99
+ info += chunk.toString()
100
+ if (Buffer.byteLength(info) > 16384) { excessive = true; unconfirmed('invalid sandbox ownership report') }
101
+ })
102
+ child.stdio[5].once('error', () => unconfirmed('sandbox ownership channel failed'))
103
+ child.stdio[5].once('end', () => {
104
+ try {
105
+ if (excessive) return
106
+ const pid = JSON.parse(info)['child-pid']
107
+ if (!Number.isSafeInteger(pid) || pid < 1 || pid === child.pid || pid === process.pid) throw new Error('invalid pid')
108
+ const identity = processIdentity(pid)
109
+ // The channel is private to bwrap and closed before guest execution.
110
+ // Check kernel parentage while its monitor is alive, then retain the
111
+ // birth fingerprint so a later orphan or PID reuse cannot change scope.
112
+ if (sameProcess(pid, identity) && (!identity || parentPid(pid) !== child.pid)) throw new Error('not an owned child')
113
+ namespace = { pid, identity }
114
+ // A dead bwrap monitor cannot donate a still-live namespace runtime.
115
+ reportRuntime(pid)
116
+ if (closing) kill()
117
+ finish()
118
+ } catch { unconfirmed('invalid or unowned sandbox child report') }
119
+ })
120
+ }
38
121
  // Stream one JSON object: older readers still consume the complete terminal
39
122
  // object, while current readers observe the prefix only after actual spawn.
40
123
  child.once('spawn',()=>{
41
- if(launch.repositoryRoot){
42
- try{const identity=repositoryLease.recordRuntime(child.pid);writeSync(4,JSON.stringify({type:'runtime',pid:child.pid,identity})+'\n')}
43
- catch{try{writeSync(4,JSON.stringify({type:'error',code:'REPOSITORY_LOCK_FAILED'})+'\n')}finally{stop()}}
44
- }
124
+ if (!launch.namespaceInfo) reportRuntime(child.pid)
45
125
  if(launch.report){try{writeSync(4,'{"processStarted":true,')}catch{/* host already stopped */}}
46
126
  })
47
127
  child.once('error', error => { process.stderr.write(`LISP_LAUNCH_ERROR: ${error.message}\n`); process.exit(70) })
48
128
  child.once('exit', (code, signal) => {
49
129
  if(launch.report){try{writeSync(4,JSON.stringify({exitCode:code,signal}).slice(1)+'\n')}catch{/* host already stopped */}}
50
- if (launch.group) kill()
51
- repositoryLease?.close()
52
- process.exit(signal ? 128 : code ?? 70)
130
+ terminal = { code, signal }
131
+ if (launch.namespaceInfo) stop()
132
+ else if (launch.group) kill()
133
+ finish()
53
134
  })
54
135
  if (closing) kill()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiokuko-dsh",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "description": "Kiokuko plugin for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "MIT",