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.
- package/dist/dsh/core/host.js +4 -4
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/decisions/diagnostics.d.ts +4 -3
- package/dist/dsh/decisions/diagnostics.d.ts.map +1 -1
- package/dist/dsh/decisions/diagnostics.js +2 -2
- package/dist/dsh/decisions/diagnostics.js.map +1 -1
- package/dist/dsh/decisions/intent-assessment.d.ts +10 -4
- package/dist/dsh/decisions/intent-assessment.d.ts.map +1 -1
- package/dist/dsh/decisions/intent-assessment.js +26 -10
- package/dist/dsh/decisions/intent-assessment.js.map +1 -1
- package/dist/dsh/decisions/openai.d.ts.map +1 -1
- package/dist/dsh/decisions/openai.js +3 -0
- package/dist/dsh/decisions/openai.js.map +1 -1
- package/dist/dsh/host-adapter.js +4 -4
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/lisp/sandbox.d.ts +1 -0
- package/dist/dsh/lisp/sandbox.d.ts.map +1 -1
- package/dist/dsh/lisp/sandbox.js +2 -2
- package/dist/dsh/lisp/sandbox.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +19 -4
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +154 -61
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/task-startup.d.ts +4 -0
- package/dist/dsh/task-startup.d.ts.map +1 -1
- package/dist/dsh/task-startup.js +8 -2
- package/dist/dsh/task-startup.js.map +1 -1
- package/dist/locale/en.json +1 -1
- package/dist/locale/ja.json +1 -1
- package/dist/locale/ko.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/docs/micro-judge.md +1 -1
- package/docs/on-demand-intake.md +2 -0
- package/lisp/supervisor.mjs +90 -9
- package/package.json +1 -1
package/lisp/supervisor.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
130
|
+
terminal = { code, signal }
|
|
131
|
+
if (launch.namespaceInfo) stop()
|
|
132
|
+
else if (launch.group) kill()
|
|
133
|
+
finish()
|
|
53
134
|
})
|
|
54
135
|
if (closing) kill()
|