@coboot/headless 0.2.2-rc.0 → 0.2.2-rc.1

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/README.md CHANGED
@@ -3,48 +3,65 @@
3
3
  Node.js 20+ 的无浏览器后台运行包。首次启动只需一次性设备 token:
4
4
 
5
5
  ```bash
6
- npx @coboot/headless <device-token>
6
+ npx -y @coboot/headless@0.2.2-rc.1 '<device-token>'
7
7
  ```
8
8
 
9
- 命令会把运行时安装到 `/data/coboot/.coboot`,启动 detached daemon 后退出。
10
- 不需要设置 `BACKEND_URL`、`CLIENT_UID`,也不需要安装 Chromium。
9
+ 命令会把运行时安装到 `/data/coboot/.siboot`,在客户端鉴权和运行时初始化完成后启动
10
+ detached daemon 并退出。不需要设置 `BACKEND_URL`、`CLIENT_UID`,也不需要安装 Chromium。
11
11
 
12
12
  ```bash
13
- npx @coboot/headless status
14
- npx @coboot/headless logs
15
- npx @coboot/headless restart
16
- npx @coboot/headless stop
13
+ npx -y @coboot/headless@0.2.2-rc.1 status
14
+ npx -y @coboot/headless@0.2.2-rc.1 logs
15
+ npx -y @coboot/headless@0.2.2-rc.1 restart
16
+ npx -y @coboot/headless@0.2.2-rc.1 stop
17
17
  ```
18
18
 
19
- 默认数据目录可通过 `HEADLESS_DATA_DIR` 修改。daemon 默认禁用 updater 和浏览器预热。
19
+ 默认数据根目录可通过 `SIBOOT_DATA_ROOT` 修改。daemon 默认禁用 updater 和浏览器预热。
20
20
 
21
21
  已有设备凭证无效或需要切换设备身份时,显式重新注册:
22
22
 
23
23
  ```bash
24
- npx @coboot/headless --force-enroll <device-token>
24
+ npx -y @coboot/headless@0.2.2-rc.1 --force-enroll '<device-token>'
25
25
  ```
26
26
 
27
27
  命令会先停止 daemon 并临时备份旧凭证;新凭证和客户端均就绪后才完成替换,失败时恢复旧凭证。
28
28
 
29
+ ## Linux 沙箱
30
+
31
+ 当 `package.json` 的 `headlessLinuxSandbox` 不为 `null` 时,包内已携带 Linux x64/arm64 的
32
+ bubblewrap、musl loader 和 libcap,不要求目标机器额外安装 `bwrap`。首次命令执行前会优先探测
33
+ 包内运行时,失败后再探测系统 `$PATH` 中的 `bwrap`;两者都不可用或内核禁止 user/mount
34
+ namespace 时,会记录原因并按兼容策略使用普通 Node 进程。
35
+
36
+ 日志中的 `source=bundled` 表示已经选择包内运行时。Linux Headless 的 Bash 使用当前会话
37
+ `COBOOT_WORKSPACE/.home` 作为 `HOME`,并清理 `BASH_ENV` / `ENV`,不会读取宿主账号的 shell
38
+ 启动脚本。
39
+
40
+ Coboot Linux Headless 会把整个 `SIBOOT_DATA_ROOT`(默认 `/data/coboot`)作为可写根,其中
41
+ `.siboot` 运行状态目录仍由 hard deny 遮蔽。这同时允许访问同机其他 team/agent/session 的数据,
42
+ 只适用于部署侧接受该权限边界的独占目标机;结构化文件工具仍继续执行自身的 ACL。
43
+
29
44
  ## 运行环境
30
45
 
31
46
  - Edition:`coboot`,在构建期写入,启动时不能切换。
32
47
  - 默认 HTTP 服务:`https://www.coboot.com`。
33
48
  - 默认 WebSocket 服务:`wss://www.coboot.com/wsschat`。
34
- - 默认数据目录:`/data/coboot`。
49
+ - 默认数据根目录:`/data/coboot`。
35
50
  - 包不携带浏览器、Chromium、`playwright-core` 或 updater,不能用于浏览器工具。
36
51
 
37
- 使用其他服务环境时,必须同时设置与设备 token 匹配的地址:
52
+ npm CLI 默认清理父进程遗留的 endpoint 环境变量。仅本地联调或自建环境可显式开启覆盖,且
53
+ HTTP 与 WebSocket 必须同域并与设备 token 匹配:
38
54
 
39
55
  ```bash
56
+ HEADLESS_ALLOW_ENDPOINT_OVERRIDE=1 \
40
57
  BACKEND_URL=https://example.com \
41
58
  WS_URL=wss://example.com/wsschat \
42
- npx @coboot/headless <device-token>
59
+ npx -y @coboot/headless@0.2.2-rc.1 '<device-token>'
43
60
  ```
44
61
 
45
- 代码读取的 WebSocket 覆盖变量名是 `WS_URL`,不是 `WSS_URL`。
62
+ WebSocket 覆盖变量名是 `WS_URL`,不是 `WSS_URL`。
46
63
 
47
- ## 维护者构建
64
+ ## 维护者构建与发布
48
65
 
49
66
  在 `frontend/packages/app-desktop` 下执行:
50
67
 
@@ -52,16 +69,26 @@ npx @coboot/headless <device-token>
52
69
  CU_EDITION=coboot \
53
70
  PKG_NAME=@coboot/headless \
54
71
  PKG_BIN_NAME=coboot-headless \
55
- PKG_VERSION=0.2.2 \
72
+ PKG_VERSION=0.2.2-rc.1 \
56
73
  pnpm build:headless:npm
57
74
  ```
58
75
 
59
- 发布前核对包内清单、edition 和哈希:
76
+ 发布前核对包内清单、元数据和哈希:
60
77
 
61
78
  ```bash
62
79
  npm pack --dry-run --json ./dist-headless
63
- tar -xOf dist-headless/coboot-headless-0.2.2-rc.0.tgz package/package.json
64
- sha256sum dist-headless/coboot-headless-0.2.2-rc.0.tgz
80
+ tar -xOf dist-headless/coboot-headless-0.2.2-rc.1.tgz package/package.json
81
+ sha256sum dist-headless/coboot-headless-0.2.2-rc.1.tgz
82
+ ```
83
+
84
+ 确认后把 RC 发布到 `next` tag:
85
+
86
+ ```bash
87
+ npm publish ./dist-headless/coboot-headless-0.2.2-rc.1.tgz \
88
+ --registry=https://registry.npmjs.org/ \
89
+ --access public \
90
+ --tag next
65
91
  ```
66
92
 
67
- `.tgz` 属于本地构建产物,不提交 Git。只发布已经完成上述核对的 tarball。
93
+ `.tgz` 属于本地构建产物,不提交 Git;只发布已经完成上述核对的 tarball。`device-token` 是
94
+ Coboot 生成的一次性设备注册 token,不是 npm token,也不要发送给其他人。
@@ -0,0 +1,16 @@
1
+ # Third-party notices for the bundled Linux sandbox
2
+
3
+ The Coboot Headless npm package redistributes the following unmodified Alpine Linux binaries:
4
+
5
+ | Component | Version | License | Source |
6
+ | ----------- | -------- | ---------------------------- | ------------------------------------------------------------- |
7
+ | bubblewrap | 0.10.0 | LGPL-2.0-or-later | https://github.com/containers/bubblewrap/releases/tag/v0.10.0 |
8
+ | musl loader | 1.2.5-r3 | MIT | https://musl.libc.org/ |
9
+ | libcap | 2.78-r0 | BSD-3-Clause OR GPL-2.0-only | https://git.kernel.org/pub/scm/libs/libcap/libcap.git/ |
10
+
11
+ The exact Alpine package versions, download hashes, extracted-file hashes and target architectures
12
+ are recorded by `build-headless-linux-sandbox.sh` and the generated `native/manifest.json`.
13
+
14
+ Full license texts are included under `native/LICENSES/`. Corresponding source is available from the
15
+ source links above and the Alpine v3.20 source packages. Recipients may replace or relink these native
16
+ files; the npm installer copies them as separate files and does not combine them into `entry.mjs`.
package/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import{execFileSync as C,spawn as A}from"child_process";import i from"fs";import o from"path";import{fileURLToPath as P}from"url";var N=new Set(["help","start","stop","restart","status","logs"]);function j(e){if(e==="coboot")return{edition:e,packageName:"@coboot/headless",productName:"Coboot",dataRoot:"/data/coboot",stateDirectory:".coboot"};if(e!=="siboot")throw new Error(`unsupported headless edition: ${e}`);return{edition:"siboot",packageName:"siboot-headless",productName:"Siboot",dataRoot:"/data/siboot",stateDirectory:".siboot"}}var v="coboot",a=j(v);function w(){console.log(`Usage:
3
- ${a.packageName} <device-token>
4
- ${a.packageName} --force-enroll <device-token>
5
- ${a.packageName} start|stop|restart|status|logs
2
+ import{execFileSync as P,spawn as j}from"child_process";import{createHash as B}from"crypto";import s from"fs";import i from"path";import{fileURLToPath as U}from"url";var k=new Set(["help","start","stop","restart","status","logs"]);function A(e="siboot-headless",n="/data/siboot"){console.log(`Usage:
3
+ ${e} <device-token>
4
+ ${e} --force-enroll <device-token>
5
+ ${e} start|stop|restart|status|logs
6
6
 
7
7
  Environment:
8
- HEADLESS_DATA_DIR Persistent data root (default: ${a.dataRoot})`)}function p(e){let n=e[0]?.trim()||"";if(!n||n==="--help"||n==="-h"||n==="help")return{command:"help"};if(n==="--force-enroll"){let r=e[1]?.trim()||"";if(!r||e.length!==2)throw new Error("--force-enroll requires one device token");return{command:"install",token:r,forceEnroll:!0}}return N.has(n)?{command:n}:{command:"install",token:n}}import l from"fs";var x=e=>new Promise(n=>setTimeout(n,e));function g(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch{return!1}}function _(e){try{return JSON.parse(l.readFileSync(e,"utf8"))}catch{return null}}function $(e){let n=_(e);return!!(n?.deviceId?.trim()&&n?.credentialId?.trim()&&n?.ownerUid?.trim()&&n?.deviceName?.trim()&&n?.deviceType>0&&n?.publicKey?.trim()&&n?.privateKeyPem?.trim())}function b(e){return[e.credential,e.authSession]}function O(e){for(let n of e.entries){try{l.unlinkSync(n.file)}catch{}n.existed&&l.renameSync(n.backup,n.file)}}function R(e){let n=`${process.pid}-${Date.now()}`,r=b(e).map(s=>({file:s,backup:`${s}.force-enroll-${n}.bak`,existed:l.existsSync(s)})),t=[];try{for(let s of r)s.existed&&(l.renameSync(s.file,s.backup),t.push(s))}catch(s){for(let c of t.reverse())l.renameSync(c.backup,c.file);throw s}return{entries:r}}function L(e){for(let n of e.entries)n.existed&&l.unlinkSync(n.backup)}function H(e){O(e)}async function F(e,n){let r=Date.now()+n,t=0;for(;Date.now()<r;){let s=_(`${e.run}/status.json`);if($(e.credential)&&g(s?.client_pid)){if(t||=Date.now(),Date.now()-t>=1e3)return}else t=0;let c=Number.parseInt(l.readFileSync(`${e.run}/daemon.pid`,"utf8"),10);if(s?.client_exit_code!=null&&!s?.client_pid)throw new Error(`client exited during force enrollment with code ${s.client_exit_code}`);if(c&&!g(c))throw new Error("daemon exited during force enrollment");await x(100)}throw new Error(`force enrollment did not become ready within ${n}ms`)}async function D(e,n,r,t={}){let s=R(e);try{let c=await n();return await F(e,t.timeoutMs||15e3),L(s),c}catch(c){await r().catch(()=>{});try{H(s)}catch(d){throw new AggregateError([c,d],"force enrollment and rollback failed")}throw c}}var k=P(import.meta.url),E=a.dataRoot;function U(e){let n=o.resolve(e||E);if(!o.isAbsolute(e||E)||n===o.parse(n).root)throw new Error(`HEADLESS_DATA_DIR must be a non-root absolute path: ${e}`);return n}function B(e=process.env.SIBOOT_DATA_ROOT||process.env.HEADLESS_DATA_DIR){let n=U(e||E),r=o.join(n,a.stateDirectory);return{root:n,home:r,releases:o.join(r,"releases"),current:o.join(r,"current"),scripts:o.join(r,"scripts"),run:o.join(r,"run"),logs:o.join(r,"logs"),workspace:o.join(n,"workspace"),credential:o.join(r,"headless-device-credential.json"),authSession:o.join(r,"headless-auth-session.json")}}function I(e){for(let n of[e.home,e.releases,e.scripts,e.run,e.workspace,o.join(e.logs,"daemon"),o.join(e.logs,"client"),o.join(e.logs,"update")])i.mkdirSync(n,{recursive:!0,mode:448})}function J(e){let n=JSON.parse(i.readFileSync(o.join(e,"package.json"),"utf8")),r=String(n.version||"").trim();if(!r||!/^[0-9A-Za-z._-]+$/.test(r))throw new Error("package version is missing or invalid");return r}function V(e,n){let r=o.relative(e.home,n),t=`${e.current}.next-${process.pid}`;try{i.unlinkSync(t)}catch{}i.symlinkSync(r,t),i.renameSync(t,e.current)}function y(e,n,r){let t=`${n}.next-${process.pid}`;i.copyFileSync(e,t),i.chmodSync(t,r),i.renameSync(t,n)}function K(e,n,r){let t=`${e}.next-${process.pid}`;i.writeFileSync(t,n,{mode:r}),i.renameSync(t,e)}function M(e,n){I(n);let r=J(e),t=o.join(n.releases,r);i.mkdirSync(t,{recursive:!0,mode:448});let s=o.join(e,"entry.mjs"),c=o.join(e,"daemon.mjs");if(!i.existsSync(s)||!i.existsSync(c))throw new Error("package is missing entry.mjs or daemon.mjs");return y(s,o.join(t,"entry.mjs"),320),y(c,o.join(n.scripts,"daemon.mjs"),320),K(o.join(t,"manifest.json"),`${JSON.stringify({type:"headless-npm-daemon",version:r,browserRuntime:"disabled"},null,2)}
9
- `,256),V(n,t),{version:r,releaseDir:t}}function h(e){try{let n=Number.parseInt(i.readFileSync(e,"utf8").trim(),10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function m(e){if(!e)return!1;try{return process.kill(e,0),!0}catch{return!1}}function W(e,n){let r="";try{r=i.readFileSync(`/proc/${e}/cmdline`,"utf8").replaceAll("\0"," ").trim()}catch{}if(!r)try{r=C("ps",["-ww","-p",String(e),"-o","command="],{encoding:"utf8"}).trim()}catch{return!1}return[n,S(n)].some(t=>r.includes(t))}var T=e=>new Promise(n=>setTimeout(n,e));async function f(e){let n=o.join(e.run,"daemon.pid"),r=h(n);if(!m(r)){try{i.unlinkSync(n)}catch{}return!1}let t=o.join(e.scripts,"daemon.mjs");if(!W(r,t))throw new Error(`refusing to stop unrelated process from stale daemon.pid: ${r}`);process.kill(r,"SIGTERM");for(let s=0;s<50&&m(r);s+=1)await T(100);if(m(r))throw new Error(`daemon did not stop within 5 seconds: ${r}`);return!0}function Q(e,n,r=process.env){let t={...r,NO_ELECTRON:"1",CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:e.home,SIBOOT_DATA_ROOT:e.root,HEADLESS_DATA_DIR:e.root,SIBOOT_DATA_DIR:e.root,HEADLESS_DEVICE_CREDENTIAL_PATH:e.credential,HEADLESS_AUTH_SESSION_PATH:e.authSession,HEADLESS_DEVICE_REQUIRE_AUTH:"1",HEADLESS_PREWARM_DEFAULT_SESSION:"0",WORKSPACE:e.workspace,ANYDEV_UPDATE_ENABLED:"0"};return n?t.HEADLESS_DEVICE_TOKEN=n:delete t.HEADLESS_DEVICE_TOKEN,delete t.HEADLESS_DEVICE_FORCE_ENROLL,t}async function Y(e,n=""){if(I(e),!i.existsSync(e.credential)&&!n)throw new Error(`device credential is missing; run ${a.packageName} <device-token> first`);let r=o.join(e.scripts,"daemon.mjs");if(!i.existsSync(o.join(e.current,"entry.mjs"))||!i.existsSync(r))throw new Error(`runtime is not installed; run ${a.packageName} <device-token> first`);let t=i.existsSync(e.credential)?"":n,s=o.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`),c=i.openSync(s,"a",384);A(process.execPath,[r],{detached:!0,env:Q(e,t),stdio:["ignore",c,c]}).unref(),i.closeSync(c),await T(500);let u=h(o.join(e.run,"daemon.pid"));if(!m(u))throw new Error(`daemon failed to start; check ${s}`);return{pid:u,logFile:s}}function q(e){let n=h(o.join(e.run,"daemon.pid")),r=null;try{r=JSON.parse(i.readFileSync(o.join(e.run,"status.json"),"utf8"))}catch{}console.log(JSON.stringify({running:m(n),pid:n,current:i.existsSync(e.current),status:r},null,2))}async function z(e){let n=o.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`);if(!i.existsSync(n))throw new Error(`log file not found: ${n}`);let r=A("tail",["-n","100","-f",n],{stdio:"inherit"});await new Promise((t,s)=>{r.once("error",s),r.once("exit",c=>c===0?t():s(new Error(`tail exited ${c}`)))})}async function G(){let e=p(process.argv.slice(2));if(e.command==="help")return w();let n=B();if(e.command==="status")return q(n);if(e.command==="logs")return z(n);if(e.command==="stop"){let d=await f(n);console.log(d?`${a.productName} Headless stopped`:`${a.productName} Headless is not running`);return}let r=o.dirname(k);await f(n);let t=async()=>{let d=M(r,n),u=await Y(n,e.token||"");return{installed:d,started:u}},{installed:s,started:c}=e.forceEnroll?await D(n,t,()=>f(n)):await t();console.log(`${a.productName} Headless ${s.version} started in background (pid=${c.pid})`),e.forceEnroll&&console.log("Device credential replaced by force enrollment"),console.log(`Logs: ${c.logFile}`)}function S(e){try{return i.realpathSync(e)}catch{return o.resolve(e)}}process.argv[1]&&S(process.argv[1])===S(k)&&G().catch(e=>{console.error(`${a.packageName}: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});export{Q as createDaemonEnv,M as installHeadlessRuntime,p as parseHeadlessCliArgs,B as resolveHeadlessLayout,Y as startHeadlessRuntime,f as stopHeadlessRuntime};
8
+ SIBOOT_DATA_ROOT Persistent data root (default: ${n})`)}function w(e){let n=e[0]?.trim()||"";if(!n||n==="--help"||n==="-h"||n==="help")return{command:"help"};if(n==="--force-enroll"){let t=e[1]?.trim()||"";if(!t||e.length!==2)throw new Error("--force-enroll requires one device token");return{command:"install",token:t,forceEnroll:!0}}return k.has(n)?{command:n}:{command:"install",token:n}}import u from"fs";var T=e=>new Promise(n=>setTimeout(n,e));function x(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch{return!1}}function D(e){try{return JSON.parse(u.readFileSync(e,"utf8"))}catch{return null}}function v(e){let n=D(e);return!!(n?.deviceId?.trim()&&n?.credentialId?.trim()&&n?.ownerUid?.trim()&&n?.deviceName?.trim()&&n?.deviceType>0&&n?.publicKey?.trim()&&n?.privateKeyPem?.trim())}function I(e){return[e.credential,e.authSession]}function $(e){for(let n of e.entries){try{u.unlinkSync(n.file)}catch{}n.existed&&u.renameSync(n.backup,n.file)}}function N(e){let n=`${process.pid}-${Date.now()}`,t=I(e).map(r=>({file:r,backup:`${r}.force-enroll-${n}.bak`,existed:u.existsSync(r)})),o=[];try{for(let r of t)r.existed&&(u.renameSync(r.file,r.backup),o.push(r))}catch(r){for(let c of o.reverse())u.renameSync(c.backup,c.file);throw r}return{entries:t}}function H(e){for(let n of e.entries)n.existed&&u.unlinkSync(n.backup)}function F(e){$(e)}async function C(e,n){let t=Date.now()+n,o=0;for(;Date.now()<t;){let r=D(`${e.run}/status.json`);if(v(e.credential)&&r?.client_ready===!0&&x(r.client_pid)){if(o||=Date.now(),Date.now()-o>=1e3)return}else o=0;let c=Number.parseInt(u.readFileSync(`${e.run}/daemon.pid`,"utf8"),10);if(r?.client_exit_code!=null&&!r?.client_pid)throw new Error(`client exited during force enrollment with code ${r.client_exit_code}`);if(c&&!x(c))throw new Error("daemon exited during force enrollment");await T(100)}throw new Error(`force enrollment did not become ready within ${n}ms`)}async function y(e,n,t,o={}){let r=N(e);try{let c=await n();return await C(e,o.timeoutMs||15e3),H(r),c}catch(c){await t().catch(()=>{});try{F(r)}catch(a){throw new AggregateError([c,a],"force enrollment and rollback failed")}throw c}}var O=U(import.meta.url),b={siboot:"/data/siboot",coboot:"/data/coboot"},V=15e3,M=["BACKEND_URL","WS_URL","SIBOOT_URL","MCP_CREDENTIAL_API_BASE_URL","SKILL_CREDENTIAL_API_BASE_URL"],W=["HEADLESS_BWRAP_EXECUTABLE","HEADLESS_BWRAP_LOADER","HEADLESS_BWRAP_LIBRARY_DIR"];function J(e,n){let t=e||b[n],o=i.resolve(t);if(!i.isAbsolute(t)||o===i.parse(o).root)throw new Error(`SIBOOT_DATA_ROOT must be a non-root absolute path: ${e}`);return o}function K(e=process.env.SIBOOT_DATA_ROOT,n="siboot"){if(!(n in b))throw new Error(`unsupported headless edition: ${n}`);let t=J(e,n),o=i.join(t,".siboot");return{edition:n,root:t,home:o,releases:i.join(o,"releases"),current:i.join(o,"current"),scripts:i.join(o,"scripts"),run:i.join(o,"run"),logs:i.join(o,"logs"),workspace:i.join(t,"workspace"),credential:i.join(o,"headless-device-credential.json"),authSession:i.join(o,"headless-auth-session.json")}}function L(e){for(let n of[e.home,e.releases,e.scripts,e.run,e.workspace,i.join(e.logs,"daemon"),i.join(e.logs,"client"),i.join(e.logs,"update")])s.mkdirSync(n,{recursive:!0,mode:448})}function R(e){let n=JSON.parse(s.readFileSync(i.join(e,"package.json"),"utf8")),t=String(n.version||"").trim();if(!t||!/^[0-9A-Za-z._-]+$/.test(t))throw new Error("package version is missing or invalid");let o=String(n.headlessEdition||"").trim();if(!(o in b))throw new Error("package headlessEdition must be siboot or coboot");let r=n.headlessLinuxSandbox??null;if(o==="siboot"&&r)throw new Error("Siboot package must not include Linux sandbox metadata");if(o==="coboot"&&(r?.backend!=="bubblewrap"||!Array.isArray(r.architectures)||r.architectures.length===0))throw new Error("Coboot package is missing bundled Linux sandbox metadata");return{version:t,edition:o,linuxSandbox:r}}function Y(e,n){let t=i.relative(e.home,n),o=`${e.current}.next-${process.pid}`;try{s.unlinkSync(o)}catch{}s.symlinkSync(t,o),s.renameSync(o,e.current)}function p(e,n,t){let o=`${n}.next-${process.pid}`;s.copyFileSync(e,o),s.chmodSync(o,t),s.renameSync(o,n)}function X(e,n,t){let o=`${e}.next-${process.pid}`;s.writeFileSync(o,n,{mode:t}),s.renameSync(o,e)}function Q(e){return B("sha256").update(s.readFileSync(e)).digest("hex")}function q(e,n){if(!s.existsSync(e)||Q(e)!==n)throw new Error(`bundled Linux sandbox file failed integrity check: ${i.basename(e)}`)}function z(e,n,t,o){if(!t)return null;if(o.platform!=="linux")throw new Error(`Coboot Headless bundled sandbox supports Linux only: ${o.platform}`);if(!t.architectures.includes(o.arch))throw new Error(`Coboot Headless does not include Linux sandbox for arch: ${o.arch}`);let r=i.join(e,"native",`linux-${o.arch}`),c=i.join(r,"manifest.json");if(!s.existsSync(c))throw new Error(`package is missing Linux sandbox bundle for arch: ${o.arch}`);let a=JSON.parse(s.readFileSync(c,"utf8"));if(a.backend!==t.backend||a.backendVersion!==t.backendVersion||a.arch!==o.arch)throw new Error("bundled Linux sandbox manifest does not match package metadata");let l={bwrap:i.join(r,"bwrap"),loader:i.join(r,"ld-musl"),libcap:i.join(r,"lib","libcap.so.2")};for(let[m,E]of Object.entries(l))q(E,a.files?.[m]);let d=i.join(n,"native");return s.mkdirSync(i.join(d,"lib"),{recursive:!0,mode:448}),p(l.bwrap,i.join(d,"bwrap"),320),p(l.loader,i.join(d,"ld-musl"),320),p(l.libcap,i.join(d,"lib","libcap.so.2"),256),p(c,i.join(d,"manifest.json"),256),{backend:a.backend,version:a.backendVersion,arch:a.arch,bundled:!0}}function G(e,n,t={platform:process.platform,arch:process.arch}){L(n);let{version:o,edition:r,linuxSandbox:c}=R(e);if(r!==n.edition)throw new Error(`package edition ${r} does not match runtime layout ${n.edition}`);let a=i.join(n.releases,o);s.mkdirSync(a,{recursive:!0,mode:448});let l=i.join(e,"entry.mjs"),d=i.join(e,"daemon.mjs");if(!s.existsSync(l)||!s.existsSync(d))throw new Error("package is missing entry.mjs or daemon.mjs");p(l,i.join(a,"entry.mjs"),320),p(d,i.join(n.scripts,"daemon.mjs"),320);let m=z(e,a,c,t);return X(i.join(a,"manifest.json"),`${JSON.stringify({type:"headless-npm-daemon",version:o,edition:r,browserRuntime:"disabled",linuxSandbox:m},null,2)}
9
+ `,256),Y(n,a),{version:o,releaseDir:a}}function h(e){try{let n=Number.parseInt(s.readFileSync(e,"utf8").trim(),10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function f(e){if(!e)return!1;try{return process.kill(e,0),!0}catch{return!1}}function Z(e,n){let t="";try{t=s.readFileSync(`/proc/${e}/cmdline`,"utf8").replaceAll("\0"," ").trim()}catch{}if(!t)try{t=P("ps",["-ww","-p",String(e),"-o","command="],{encoding:"utf8"}).trim()}catch{return!1}return[n,_(n)].some(o=>t.includes(o))}var g=e=>new Promise(n=>setTimeout(n,e));async function S(e){let n=i.join(e.run,"daemon.pid"),t=h(n);if(!f(t)){try{s.unlinkSync(n)}catch{}return!1}let o=i.join(e.scripts,"daemon.mjs");if(!Z(t,o))throw new Error(`refusing to stop unrelated process from stale daemon.pid: ${t}`);process.kill(t,"SIGTERM");for(let r=0;r<50&&f(t);r+=1)await g(100);if(f(t))throw new Error(`daemon did not stop within 5 seconds: ${t}`);return!0}function ee(e,n,t=process.env){let o={...t,CU_EDITION:e.edition,NO_ELECTRON:"1",CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:e.home,SIBOOT_DATA_ROOT:e.root,HEADLESS_DATA_DIR:e.root,SIBOOT_DATA_DIR:e.root,HEADLESS_DEVICE_CREDENTIAL_PATH:e.credential,HEADLESS_AUTH_SESSION_PATH:e.authSession,HEADLESS_DEVICE_REQUIRE_AUTH:"1",HEADLESS_PREWARM_DEFAULT_SESSION:"0",WORKSPACE:e.workspace,ANYDEV_UPDATE_ENABLED:"0"};n?o.HEADLESS_DEVICE_TOKEN=n:delete o.HEADLESS_DEVICE_TOKEN,delete o.CLIENT_UID,delete o.HEADLESS_DEVICE_FORCE_ENROLL;for(let r of W)delete o[r];if(e.edition==="coboot"){let r=i.join(e.current,"native");o.HEADLESS_BWRAP_EXECUTABLE=i.join(r,"bwrap"),o.HEADLESS_BWRAP_LOADER=i.join(r,"ld-musl"),o.HEADLESS_BWRAP_LIBRARY_DIR=i.join(r,"lib")}if(o.HEADLESS_ALLOW_ENDPOINT_OVERRIDE!=="1")for(let r of M)delete o[r];return o}async function ne(e,n=V){let t=Date.now()+n;for(;Date.now()<t;){let o=h(i.join(e.run,"daemon.pid"));if(!f(o))throw new Error("daemon exited during client startup");let r=null;try{r=JSON.parse(s.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}if(r?.client_ready===!0&&f(r.client_pid))return;if(r?.client_exit_code!=null&&!r?.client_pid)throw new Error(`client exited during startup with code ${r.client_exit_code}`);await g(100)}throw new Error(`client did not become ready within ${n}ms`)}async function oe(e,n=""){L(e);let t=e.edition==="coboot"?"coboot-headless":"siboot-headless";if(!s.existsSync(e.credential)&&!n)throw new Error(`device credential is missing; run ${t} <device-token> first`);let o=i.join(e.scripts,"daemon.mjs");if(!s.existsSync(i.join(e.current,"entry.mjs"))||!s.existsSync(o))throw new Error(`runtime is not installed; run ${t} <device-token> first`);let r=s.existsSync(e.credential)?"":n,c=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`),a=s.openSync(c,"a",384);j(process.execPath,[o],{detached:!0,env:ee(e,r),stdio:["ignore",a,a]}).unref(),s.closeSync(a),await g(500);let d=h(i.join(e.run,"daemon.pid"));if(!f(d))throw new Error(`daemon failed to start; check ${c}`);return await ne(e),{pid:d,logFile:c}}function re(e){let n=h(i.join(e.run,"daemon.pid")),t=null;try{t=JSON.parse(s.readFileSync(i.join(e.run,"status.json"),"utf8"))}catch{}console.log(JSON.stringify({running:f(n),pid:n,current:s.existsSync(e.current),status:t},null,2))}async function te(e){let n=i.join(e.logs,"daemon",`${new Date().toISOString().slice(0,10)}.log`);if(!s.existsSync(n))throw new Error(`log file not found: ${n}`);let t=j("tail",["-n","100","-f",n],{stdio:"inherit"});await new Promise((o,r)=>{t.once("error",r),t.once("exit",c=>c===0?o():r(new Error(`tail exited ${c}`)))})}async function ie(){let e=w(process.argv.slice(2)),n=i.basename(process.argv[1]||"siboot-headless"),t=i.dirname(O),o=R(t),r=K(process.env.SIBOOT_DATA_ROOT,o.edition);if(e.command==="help")return A(n,r.root);let c=o.edition==="coboot"?"Coboot":"Siboot";if(e.command==="status")return re(r);if(e.command==="logs")return te(r);if(e.command==="stop"){let m=await S(r);console.log(m?`${c} Headless stopped`:`${c} Headless is not running`);return}await S(r);let a=async()=>{let m=G(t,r);try{let E=await oe(r,e.token||"");return{installed:m,started:E}}catch(E){throw await S(r).catch(()=>{}),E}},{installed:l,started:d}=e.forceEnroll?await y(r,a,()=>S(r)):await a();console.log(`${c} Headless ${l.version} started in background (pid=${d.pid})`),e.forceEnroll&&console.log("Device credential replaced by force enrollment"),console.log(`Logs: ${d.logFile}`)}function _(e){try{return s.realpathSync(e)}catch{return i.resolve(e)}}process.argv[1]&&_(process.argv[1])===_(O)&&ie().catch(e=>{let n=i.basename(process.argv[1]||"headless-client");console.error(`${n}: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});export{ee as createDaemonEnv,G as installHeadlessRuntime,w as parseHeadlessCliArgs,R as readPackageMetadata,K as resolveHeadlessLayout,oe as startHeadlessRuntime,S as stopHeadlessRuntime};
package/daemon.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import r from"fs";import B from"os";import n from"path";import{spawn as $}from"child_process";var d=process.env.SIBOOT_BASE_DIR,I=n.join(d,"current"),O=process.env.WORKSPACE||"/data/siboot/workspace",E=n.join(d,"run"),_=n.join(d,"logs"),f=n.join(I,"entry.mjs"),T=n.join(d,"scripts","updater.mjs"),j=n.join(E,"daemon.pid"),A=n.join(E,"node.pid"),N=n.join(E,"updater.pid"),Y=n.join(E,"status.json"),L=Number.parseInt(process.env.ANYDEV_CLIENT_RESTART_DELAY_MS||"1000",10),F=Number.parseInt(process.env.ANYDEV_UPDATER_RESTART_DELAY_MS||"10000",10),R=Number.parseInt(process.env.ANYDEV_LOG_RETENTION_DAYS||"4",10),s=null,c=null,l=!1,y=0,v=0,U=new Date().toISOString();function G(){for(let e of[E,O,n.join(_,"daemon"),n.join(_,"client"),n.join(_,"update")])r.mkdirSync(e,{recursive:!0})}function M(){return new Date().toISOString().slice(0,10)}function h(e){return n.join(_,e,`${M()}.log`)}function o(e,t){let i=`${new Date().toISOString()} [${e}] ${t}
3
- `;r.appendFile(h(e),i,()=>{}),e==="daemon"&&process.stdout.write(i)}function V(e){try{return JSON.parse(r.readFileSync(e,"utf8"))}catch{return null}}function b(e,t){r.mkdirSync(n.dirname(e),{recursive:!0}),r.writeFileSync(e,JSON.stringify(t,null,2),"utf8")}function S(e){try{r.unlinkSync(e)}catch{}}function m(){let e=V(n.join(I,"manifest.json"));if(typeof e?.version=="string"&&e.version.trim())return e.version.trim();try{return n.basename(r.realpathSync(I))}catch{return process.env.APP_VERSION||"unknown"}}function k(){let e=process.env.HEADLESS_DEVICE_CREDENTIAL_PATH||process.env.SIBOOT_DEVICE_CREDENTIAL_PATH;if(e&&e.trim())return n.resolve(e.trim());let t=process.env.HEADLESS_DATA_DIR||process.env.SIBOOT_DATA_DIR||n.join(B.homedir(),".siboot");return n.join(t,"headless-device-credential.json")}function H(){try{return r.existsSync(k())}catch{return!1}}function u(e={}){b(Y,{pid:process.pid,client_pid:s?.pid||null,updater_pid:c?.pid||null,client_start_count:y,updater_start_count:v,version:m(),current_dir:I,state:l?"stopping":"running",started_at:U,updated_at:new Date().toISOString(),...e})}function J(e,t){let i=p=>{let a=Buffer.from(p);r.appendFile(h(t),a,()=>{})};e.stdout?.on("data",i),e.stderr?.on("data",i)}function P(){if(!Number.isFinite(R)||R<=0)return;let e=Date.now();for(let t of["daemon","client","update"]){let i=n.join(_,t);if(r.existsSync(i))for(let p of r.readdirSync(i)){let a=n.join(i,p),D=null;try{D=r.statSync(a)}catch{continue}if(!D.isFile())continue;e-D.mtimeMs>R*24*60*60*1e3&&S(a)}}}function g(){if(l||s)return;if(!r.existsSync(f)){o("daemon",`client entry missing entry=${f}`),u({state:"client_entry_missing"}),setTimeout(g,L).unref?.();return}y+=1;let e={...process.env,APP_VERSION:m(),CLIENT_TYPE:process.env.CLIENT_TYPE||"anydev",SIBOOT_BASE_DIR:d,SIBOOT_CURRENT_DIR:I,SIBOOT_RUN_DIR:E,SIBOOT_LOG_DIR:_};H()&&(delete e.HEADLESS_DEVICE_TOKEN,delete process.env.HEADLESS_DEVICE_TOKEN),o("daemon",`starting client entry=${f} cwd=${O} version=${m()} start_count=${y}`),s=$(process.execPath,[f],{cwd:O,env:e,stdio:["ignore","pipe","pipe"]}),r.writeFileSync(A,String(s.pid)),J(s,"client"),u(),s.on("error",t=>{o("daemon",`client spawn error error=${t.message}`)}),s.on("exit",(t,i)=>{o("daemon",`client exited pid=${s?.pid||""} code=${t} signal=${i}`),s=null,S(A),u({client_exit_code:t,client_exit_signal:i}),l||setTimeout(g,L).unref?.()})}function x(){if(!(l||c||process.env.ANYDEV_UPDATE_ENABLED==="0")){if(!r.existsSync(T)){o("daemon",`updater entry missing entry=${T}`);return}v+=1,o("daemon",`starting updater entry=${T} start_count=${v}`),c=$(process.execPath,[T],{cwd:d,env:{...process.env,CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:d,SIBOOT_CURRENT_DIR:I,SIBOOT_RUN_DIR:E,SIBOOT_LOG_DIR:_},stdio:["ignore","inherit","inherit"]}),r.writeFileSync(N,String(c.pid)),u(),c.on("error",e=>{o("daemon",`updater spawn error error=${e.message}`)}),c.on("exit",(e,t)=>{o("daemon",`updater exited pid=${c?.pid||""} code=${e} signal=${t}`),c=null,S(N),u({updater_exit_code:e,updater_exit_signal:t}),l||setTimeout(x,F).unref?.()})}}function w(e,t){return e?.pid?(o("daemon",`stopping ${t} pid=${e.pid}`),new Promise(i=>{let p=!1,a=()=>{p||(p=!0,i())};e.once("exit",a);try{e.kill("SIGTERM")}catch{a();return}setTimeout(()=>{if(!p){try{e.kill("SIGKILL")}catch{}a()}},5e3).unref?.()})):Promise.resolve()}async function C(e){l||(l=!0,o("daemon",`shutdown signal=${e}`),u({state:"stopping",signal:e}),await Promise.all([w(c,"updater"),w(s,"client")]),S(N),S(A),S(j),u({state:"stopped",signal:e}),process.exit(0))}function K(){G(),r.writeFileSync(j,String(process.pid)),process.on("SIGTERM",()=>{C("SIGTERM")}),process.on("SIGINT",()=>{C("SIGINT")}),o("daemon",`started base=${d} version=${m()}`),u({state:"starting"}),P(),g(),x(),setInterval(P,3600*1e3).unref?.()}K();
2
+ import r from"fs";import F from"os";import t from"path";import{spawn as h}from"child_process";var d=process.env.SIBOOT_BASE_DIR,f=t.join(d,"current"),A=process.env.WORKSPACE||"/data/siboot/workspace",S=t.join(d,"run"),l=t.join(d,"logs"),I=t.join(f,"entry.mjs"),T=t.join(d,"scripts","updater.mjs"),x=t.join(S,"daemon.pid"),N=t.join(S,"node.pid"),g=t.join(S,"updater.pid"),U=t.join(S,"status.json"),P=Number.parseInt(process.env.ANYDEV_CLIENT_RESTART_DELAY_MS||"1000",10),G=Number.parseInt(process.env.ANYDEV_UPDATER_RESTART_DELAY_MS||"10000",10),y=Number.parseInt(process.env.ANYDEV_LOG_RETENTION_DAYS||"4",10),o=null,c=null,_=!1,v=0,L=0,m=!1,D=null,M=new Date().toISOString();function V(){for(let e of[S,A,t.join(l,"daemon"),t.join(l,"client"),t.join(l,"update")])r.mkdirSync(e,{recursive:!0})}function b(){return new Date().toISOString().slice(0,10)}function B(e){return t.join(l,e,`${b()}.log`)}function s(e,n){let i=`${new Date().toISOString()} [${e}] ${n}
3
+ `;r.appendFile(B(e),i,()=>{}),e==="daemon"&&process.stdout.write(i)}function k(e){try{return JSON.parse(r.readFileSync(e,"utf8"))}catch{return null}}function H(e,n){r.mkdirSync(t.dirname(e),{recursive:!0}),r.writeFileSync(e,JSON.stringify(n,null,2),"utf8")}function E(e){try{r.unlinkSync(e)}catch{}}function R(){let e=k(t.join(f,"manifest.json"));if(typeof e?.version=="string"&&e.version.trim())return e.version.trim();try{return t.basename(r.realpathSync(f))}catch{return process.env.APP_VERSION||"unknown"}}function J(){let e=process.env.HEADLESS_DEVICE_CREDENTIAL_PATH||process.env.SIBOOT_DEVICE_CREDENTIAL_PATH;if(e&&e.trim())return t.resolve(e.trim());let n=process.env.HEADLESS_DATA_DIR||process.env.SIBOOT_DATA_DIR||t.join(F.homedir(),".siboot");return t.join(n,"headless-device-credential.json")}function K(){try{return r.existsSync(J())}catch{return!1}}function a(e={}){H(U,{pid:process.pid,client_pid:o?.pid||null,updater_pid:c?.pid||null,client_ready:m,client_ready_at:D,client_start_count:v,updater_start_count:L,version:R(),current_dir:f,state:_?"stopping":"running",started_at:M,updated_at:new Date().toISOString(),...e})}function W(e,n){let i=p=>{let u=Buffer.from(p);r.appendFile(B(n),u,()=>{})};e.stdout?.on("data",i),e.stderr?.on("data",i)}function $(){if(!Number.isFinite(y)||y<=0)return;let e=Date.now();for(let n of["daemon","client","update"]){let i=t.join(l,n);if(r.existsSync(i))for(let p of r.readdirSync(i)){let u=t.join(i,p),O=null;try{O=r.statSync(u)}catch{continue}if(!O.isFile())continue;e-O.mtimeMs>y*24*60*60*1e3&&E(u)}}}function w(){if(_||o)return;if(!r.existsSync(I)){s("daemon",`client entry missing entry=${I}`),a({state:"client_entry_missing"}),setTimeout(w,P).unref?.();return}v+=1,m=!1,D=null;let e={...process.env,APP_VERSION:R(),CLIENT_TYPE:process.env.CLIENT_TYPE||"anydev",SIBOOT_BASE_DIR:d,SIBOOT_CURRENT_DIR:f,SIBOOT_RUN_DIR:S,SIBOOT_LOG_DIR:l};K()&&(delete e.HEADLESS_DEVICE_TOKEN,delete process.env.HEADLESS_DEVICE_TOKEN),s("daemon",`starting client entry=${I} cwd=${A} version=${R()} start_count=${v}`),o=h(process.execPath,[I],{cwd:A,env:e,stdio:["ignore","pipe","pipe","ipc"]}),r.writeFileSync(N,String(o.pid)),W(o,"client"),a(),o.on("message",n=>{n?.type==="headless-client-ready"&&(m=!0,D=new Date().toISOString(),a(),s("daemon",`client ready pid=${o?.pid||""}`))}),o.on("error",n=>{s("daemon",`client spawn error error=${n.message}`)}),o.on("exit",(n,i)=>{s("daemon",`client exited pid=${o?.pid||""} code=${n} signal=${i}`),o=null,m=!1,D=null,E(N),a({client_exit_code:n,client_exit_signal:i}),_||setTimeout(w,P).unref?.()})}function Y(){if(!(_||c||process.env.ANYDEV_UPDATE_ENABLED==="0")){if(!r.existsSync(T)){s("daemon",`updater entry missing entry=${T}`);return}L+=1,s("daemon",`starting updater entry=${T} start_count=${L}`),c=h(process.execPath,[T],{cwd:d,env:{...process.env,CLIENT_TYPE:"anydev",SIBOOT_BASE_DIR:d,SIBOOT_CURRENT_DIR:f,SIBOOT_RUN_DIR:S,SIBOOT_LOG_DIR:l},stdio:["ignore","inherit","inherit"]}),r.writeFileSync(g,String(c.pid)),a(),c.on("error",e=>{s("daemon",`updater spawn error error=${e.message}`)}),c.on("exit",(e,n)=>{s("daemon",`updater exited pid=${c?.pid||""} code=${e} signal=${n}`),c=null,E(g),a({updater_exit_code:e,updater_exit_signal:n}),_||setTimeout(Y,G).unref?.()})}}function C(e,n){return e?.pid?(s("daemon",`stopping ${n} pid=${e.pid}`),new Promise(i=>{let p=!1,u=()=>{p||(p=!0,i())};e.once("exit",u);try{e.kill("SIGTERM")}catch{u();return}setTimeout(()=>{if(!p){try{e.kill("SIGKILL")}catch{}u()}},5e3).unref?.()})):Promise.resolve()}async function j(e){_||(_=!0,s("daemon",`shutdown signal=${e}`),a({state:"stopping",signal:e}),await Promise.all([C(c,"updater"),C(o,"client")]),E(g),E(N),E(x),a({state:"stopped",signal:e}),process.exit(0))}function Q(){V(),r.writeFileSync(x,String(process.pid)),process.on("SIGTERM",()=>{j("SIGTERM")}),process.on("SIGINT",()=>{j("SIGINT")}),s("daemon",`started base=${d} version=${R()}`),a({state:"starting"}),$(),w(),Y(),setInterval($,3600*1e3)}Q();