@jc_stack/ez-agents 0.1.0-beta.12

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 (147) hide show
  1. package/.dockerignore +24 -0
  2. package/.env.example +26 -0
  3. package/AGENTS.md +84 -0
  4. package/CHANGELOG.md +39 -0
  5. package/CONTRIBUTING.md +73 -0
  6. package/Dockerfile +16 -0
  7. package/LICENSE +21 -0
  8. package/README.md +134 -0
  9. package/SECURITY.md +26 -0
  10. package/THIRD_PARTY_NOTICES.md +14 -0
  11. package/bin/ezenciel-agents +2 -0
  12. package/bin/ezenciel-agents-ai +2 -0
  13. package/bin/ezenciel-agents-ai.mjs +5 -0
  14. package/bin/ezenciel-agents-approval +2 -0
  15. package/bin/ezenciel-agents-approval.mjs +16 -0
  16. package/bin/ezenciel-agents-create +12 -0
  17. package/bin/ezenciel-agents-docker +6 -0
  18. package/bin/ezenciel-agents-host +5 -0
  19. package/bin/ezenciel-agents-install +2 -0
  20. package/bin/ezenciel-agents-message +2 -0
  21. package/bin/ezenciel-agents-message.mjs +16 -0
  22. package/bin/ezenciel-agents-owner +2 -0
  23. package/bin/ezenciel-agents-owner.mjs +18 -0
  24. package/bin/ezenciel-agents-react +2 -0
  25. package/bin/ezenciel-agents-react.mjs +16 -0
  26. package/bin/ezenciel-agents-setup.mjs +18 -0
  27. package/bin/ezenciel-agents-source +2 -0
  28. package/bin/ezenciel-agents-source.mjs +16 -0
  29. package/bin/ezenciel-agents-tools.mjs +3 -0
  30. package/bin/ezenciel-agents.mjs +37 -0
  31. package/compose.whatsapp.yaml +12 -0
  32. package/compose.yaml +40 -0
  33. package/default-plugins.json +1 -0
  34. package/docker/entrypoint.sh +15 -0
  35. package/docker/healthcheck.mjs +8 -0
  36. package/docker/plugin-smoke.mjs +48 -0
  37. package/docker/pnpm-lock.yaml +415 -0
  38. package/docker/recovery.ts +11 -0
  39. package/docker/run.ts +52 -0
  40. package/docker/smoke.mjs +47 -0
  41. package/docker/status-smoke.mjs +30 -0
  42. package/docker/upgrade-smoke.mjs +58 -0
  43. package/docs/architecture/ai-selection.md +37 -0
  44. package/docs/architecture/authority-boundaries.md +14 -0
  45. package/docs/architecture/event-sources.md +34 -0
  46. package/docs/architecture/telegram-intake.md +29 -0
  47. package/docs/development-and-testing.md +18 -0
  48. package/docs/docker-runtime.md +118 -0
  49. package/docs/host-service.md +80 -0
  50. package/docs/plugin-contributions.md +34 -0
  51. package/docs/plugins.md +181 -0
  52. package/docs/releasing.md +71 -0
  53. package/docs/setup.md +234 -0
  54. package/docs/upgrades.md +193 -0
  55. package/package.json +106 -0
  56. package/scripts/assert-local-registry.mjs +22 -0
  57. package/scripts/release-check.mjs +14 -0
  58. package/scripts/smoke.ts +102 -0
  59. package/src/agent-install.ts +96 -0
  60. package/src/ai-cli.ts +22 -0
  61. package/src/ai.ts +88 -0
  62. package/src/approval-cli.ts +59 -0
  63. package/src/approval.ts +119 -0
  64. package/src/audio.ts +184 -0
  65. package/src/client-defaults.ts +101 -0
  66. package/src/config.ts +48 -0
  67. package/src/control-state.ts +350 -0
  68. package/src/desktop-bridge.ts +284 -0
  69. package/src/event-sources.ts +112 -0
  70. package/src/executor.ts +335 -0
  71. package/src/files.ts +75 -0
  72. package/src/format.ts +57 -0
  73. package/src/host-executor-client.ts +46 -0
  74. package/src/host-executor-protocol.ts +2 -0
  75. package/src/host-executor.ts +129 -0
  76. package/src/identity.ts +17 -0
  77. package/src/inbox.ts +171 -0
  78. package/src/index.ts +812 -0
  79. package/src/install-config.ts +56 -0
  80. package/src/install-tools.mjs +98 -0
  81. package/src/menu.ts +123 -0
  82. package/src/message-send.ts +57 -0
  83. package/src/message.ts +68 -0
  84. package/src/owner-args.ts +4 -0
  85. package/src/owner.ts +30 -0
  86. package/src/plugins/manager.mjs +272 -0
  87. package/src/react.ts +33 -0
  88. package/src/reaction.ts +32 -0
  89. package/src/read-request.ts +72 -0
  90. package/src/reply.ts +13 -0
  91. package/src/runs.ts +445 -0
  92. package/src/service.ts +28 -0
  93. package/src/setup.ts +180 -0
  94. package/src/software-status.ts +23 -0
  95. package/src/source-cli.ts +18 -0
  96. package/src/update-attention.ts +18 -0
  97. package/src/updates/artifact.mjs +83 -0
  98. package/src/updates/binding.mjs +27 -0
  99. package/src/updates/control.mjs +131 -0
  100. package/src/updates/launch.mjs +13 -0
  101. package/src/updates/runtime.mjs +140 -0
  102. package/src/updates/status.mjs +49 -0
  103. package/src/updates/supervisor.mjs +102 -0
  104. package/src/version.ts +4 -0
  105. package/src/workspace.ts +32 -0
  106. package/templates/agent/AGENTS.md +49 -0
  107. package/templates/agent/SOUL.md +11 -0
  108. package/templates/agent/TOOLS.md +46 -0
  109. package/templates/agent/USER.md +5 -0
  110. package/templates/updates.md +45 -0
  111. package/test/agent-install.test.ts +48 -0
  112. package/test/ai-cli.test.ts +28 -0
  113. package/test/ai.test.ts +105 -0
  114. package/test/approval.test.ts +40 -0
  115. package/test/audio.test.ts +77 -0
  116. package/test/client-defaults.test.ts +64 -0
  117. package/test/codex-context.test.ts +33 -0
  118. package/test/config.test.ts +32 -0
  119. package/test/control-state.test.ts +58 -0
  120. package/test/desktop-bridge.test.ts +159 -0
  121. package/test/docker-runtime.test.ts +23 -0
  122. package/test/event-sources.test.ts +113 -0
  123. package/test/executor.test.ts +135 -0
  124. package/test/files.test.ts +50 -0
  125. package/test/format.test.ts +41 -0
  126. package/test/host-executor.test.ts +149 -0
  127. package/test/inbox-burst.test.ts +66 -0
  128. package/test/inbox.test.ts +102 -0
  129. package/test/install-config.test.ts +75 -0
  130. package/test/install-tools.test.mjs +59 -0
  131. package/test/intake-relay.test.ts +337 -0
  132. package/test/owner-help.test.mjs +10 -0
  133. package/test/plugin-manager.test.mjs +157 -0
  134. package/test/publish-guard.test.ts +21 -0
  135. package/test/reaction.test.ts +122 -0
  136. package/test/read-request.test.ts +134 -0
  137. package/test/relay.test.ts +254 -0
  138. package/test/release-entrypoints.test.mjs +26 -0
  139. package/test/runs.test.ts +116 -0
  140. package/test/security.test.ts +85 -0
  141. package/test/setup.test.ts +68 -0
  142. package/test/software-status.test.ts +31 -0
  143. package/test/update-attention.test.ts +21 -0
  144. package/test/updates.test.mjs +282 -0
  145. package/test/upgrade-pause.test.ts +70 -0
  146. package/test/workspace.test.ts +80 -0
  147. package/tsconfig.json +19 -0
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url))
8
+ const require = createRequire(import.meta.url)
9
+ const tsx = require.resolve('tsx')
10
+ const entry = join(here, '..', 'src', 'source-cli.ts')
11
+ const forwarded = process.argv.slice(2).filter((arg) => arg !== '--')
12
+ const child = spawn(process.execPath, ['--import', tsx, entry, ...forwarded], { stdio: 'inherit' })
13
+ child.on('exit', (code, signal) => {
14
+ if (signal) process.kill(process.pid, signal)
15
+ process.exit(code ?? 1)
16
+ })
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/plugins/manager.mjs';
3
+ main(process.argv.slice(2)).catch(error => { console.error(JSON.stringify({ok:false,error:error.message})); process.exitCode=1; });
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import { dirname, join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const here = dirname(fileURLToPath(import.meta.url))
9
+ const require = createRequire(import.meta.url)
10
+ const entry = join(here, '..', 'src', 'index.ts')
11
+ const forwarded = process.argv.slice(2).filter((arg) => arg !== '--')
12
+ if (forwarded.length === 1 && forwarded[0] === '--version') {
13
+ console.log(require('../package.json').version)
14
+ process.exit(0)
15
+ }
16
+ if (forwarded.length === 1 && ['--help', '-h'].includes(forwarded[0])) {
17
+ console.log('Usage: ezenciel-agents [start]\nStarts the Docker relay bound by EZ_DEPLOYMENT_DIR/docker.env.\nInitialize the mind: ezenciel-agents-setup init\nSetup instructions: docs/setup.md in the installed package.')
18
+ process.exit(0)
19
+ }
20
+ if (forwarded.length && (forwarded.length !== 1 || forwarded[0] !== 'start')) {
21
+ console.error('Usage: ezenciel-agents [start] (or --help)')
22
+ process.exit(1)
23
+ }
24
+ if (!existsSync('/.dockerenv') && process.env.EZ_DEVELOPMENT !== '1') {
25
+ const docker = spawn(join(here, 'ezenciel-agents-docker'), ['up', '-d', '--wait', 'relay'], { stdio: 'inherit' })
26
+ docker.on('error', () => { console.error('Docker launcher could not start'); process.exitCode = 1 })
27
+ docker.on('exit', code => { process.exitCode = code ?? 1 })
28
+ } else {
29
+ const tsx = require.resolve('tsx')
30
+ const args = existsSync('.env') ? ['--env-file=.env', entry] : [entry]
31
+ const child = spawn(process.execPath, ['--import', tsx, ...args, ...forwarded], { stdio: 'inherit' })
32
+ child.on('exit', (code, signal) => {
33
+ if (signal) process.kill(process.pid, signal)
34
+ process.exit(code ?? 1)
35
+ })
36
+
37
+ }
@@ -0,0 +1,12 @@
1
+ services:
2
+ relay:
3
+ volumes:
4
+ - whatsapp-ipc:/plugins/whatsapp:ro
5
+ - whatsapp-client:/opt/plugins/whatsapp:ro
6
+ volumes:
7
+ whatsapp-ipc:
8
+ external: true
9
+ name: ${EZ_WHATSAPP_IPC_VOLUME:?Set the registered WhatsApp IPC volume}
10
+ whatsapp-client:
11
+ external: true
12
+ name: ${EZ_WHATSAPP_CLIENT_VOLUME:?Set the registered WhatsApp client volume}
package/compose.yaml ADDED
@@ -0,0 +1,40 @@
1
+ services:
2
+ relay:
3
+ image: ${EZ_RELAY_IMAGE:-ezenciel-agents:local}
4
+ build:
5
+ context: .
6
+ target: runtime
7
+ init: true
8
+ restart: unless-stopped
9
+ stop_grace_period: 30s
10
+ cap_drop: [ALL]
11
+ cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, SETPCAP]
12
+ environment:
13
+ EZ_AGENT_PURPOSE_FILE: /run/agent-purpose.md
14
+ EZ_EXECUTOR_TRANSPORT: host
15
+ EZ_AGENT_WORKSPACE: ${EZ_AGENT_WORKSPACE:?Set this agent workspace}
16
+ EZ_CONTROL_DIR: ${EZ_CONTROL_DIR:?Set this agent control directory}
17
+ EZ_EXECUTOR_CLI: ${EZ_EXECUTOR_CLI:?Set the host installation CLI}
18
+ EZ_EXECUTOR_TIMEOUT_SECONDS: ${EZ_EXECUTOR_TIMEOUT_SECONDS:-300}
19
+ secrets: [relay_env]
20
+ configs:
21
+ - source: agent_purpose
22
+ target: /run/agent-purpose.md
23
+ volumes:
24
+ - ${EZ_AGENT_WORKSPACE}:/workspace
25
+ - ${EZ_AGENT_WORKSPACE}:${EZ_AGENT_WORKSPACE}
26
+ - ${EZ_CONTROL_DIR}:/state/control
27
+ - ${EZ_CONTROL_DIR}:${EZ_CONTROL_DIR}
28
+ tmpfs: [/tmp]
29
+ healthcheck:
30
+ test: [CMD, node, /app/docker/healthcheck.mjs]
31
+ interval: 10s
32
+ timeout: 5s
33
+ start_period: 30s
34
+ retries: 3
35
+ configs:
36
+ agent_purpose:
37
+ file: ${EZ_AGENT_PURPOSE_FILE:-./templates/agent/SOUL.md}
38
+ secrets:
39
+ relay_env:
40
+ file: ${EZ_RELAY_ENV_FILE:?Set EZ_RELAY_ENV_FILE to the private relay env file}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,15 @@
1
+ #!/bin/sh -p
2
+ set -eu
3
+ umask 077
4
+ if [ "$(id -u)" = 0 ]; then
5
+ mkdir -p /state/control /state/home /workspace /run/secrets
6
+ chmod 700 /run/secrets
7
+ chown 1000:1000 /state/control /state/home /workspace
8
+ if [ -f /run/secrets/relay_env ]; then exec 3</run/secrets/relay_env; else exec 3</dev/null; fi
9
+ exec setpriv --ruid=1001 --euid=1000 --regid=1000 --clear-groups --bounding-set=-all --no-new-privs /app/docker/entrypoint.sh "$@"
10
+ fi
11
+ case "${1:-start}" in
12
+ start|smoke|exec|setup)
13
+ exec flock --no-fork -n -E 73 /state/control/relay.lock node --import /app/node_modules/tsx/dist/loader.mjs /app/docker/run.ts "$@" ;;
14
+ *) exec node --import /app/node_modules/tsx/dist/loader.mjs /app/docker/run.ts "$@" ;;
15
+ esac
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const value = JSON.parse(readFileSync('/state/control/heartbeat.json', 'utf8'));
3
+ if (!value.polling || Date.now() - value.at > 20000) process.exit(1);
4
+
5
+ if (process.env.EZ_EXECUTOR_TRANSPORT === 'host') {
6
+ const host = JSON.parse(readFileSync(process.env.EZ_CONTROL_DIR + '/host-executor/heartbeat.json', 'utf8'));
7
+ if (Date.now() - host.at > 15000) process.exit(1);
8
+ }
@@ -0,0 +1,48 @@
1
+ // Explicit opt-in: node docker/plugin-smoke.mjs. Only synthetic provider data.
2
+ import assert from 'node:assert/strict';
3
+ import * as fs from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import path from 'node:path';
6
+ import {execFile,spawn} from 'node:child_process';
7
+ import {promisify} from 'node:util';
8
+ import {snapshot,init} from '../src/plugins/manager.mjs';
9
+ const exec=promisify(execFile),bin=new URL('../bin/ezenciel-agents-tools.mjs',import.meta.url).pathname;
10
+ const root=await fs.mkdtemp(path.join(tmpdir(),'ez-plugin-docker-'));
11
+ const source=path.join(root,'source'),home=path.join(root,'tools'),mind=path.join(root,'mind');
12
+ const whatsappSource=path.resolve(process.env.EZ_WHATSAPP_SOURCE || new URL('../../ez_whatsapp',import.meta.url).pathname);
13
+ let record;
14
+ const call=async(...args)=>(await exec(process.execPath,[bin,'--home',home,...args],{maxBuffer:4*1024*1024})).stdout;
15
+ try {
16
+ await fs.mkdir(source);await fs.mkdir(mind);
17
+ const original=await snapshot(whatsappSource);
18
+ for(const [name,f]of original.files) {await fs.mkdir(path.dirname(path.join(source,name)),{recursive:true});await fs.writeFile(path.join(source,name),f.data,{mode:f.mode});}
19
+ await fs.copyFile(path.join(whatsappSource,'docker/fixture.mjs'),path.join(source,'src/transport.mjs'));
20
+ const m=JSON.parse(await fs.readFile(path.join(source,'ez-plugin.json')));m.commands.probe={executable:'bin/ez-whatsapp.mjs',args:[]};await fs.writeFile(path.join(source,'ez-plugin.json'),JSON.stringify(m));
21
+ const d=JSON.parse(await fs.readFile(path.join(source,'ez-deployment.json')));d.commands.probe={service:'whatsapp',argv:['node','-e',"console.log('ready');setInterval(()=>{},1000)"]};await fs.writeFile(path.join(source,'ez-deployment.json'),JSON.stringify(d));
22
+ const p=await snapshot(source);await init(home,mind);
23
+ console.log('Installing isolated WhatsApp snapshot with synthetic transport…');
24
+ const installed=JSON.parse(await call('plugins','install','whatsapp','--source',source,'--revision',p.revision));assert.equal(installed.started,false);
25
+ record=JSON.parse(await fs.readFile(path.join(home,'registry.json'))).plugins.whatsapp;
26
+ await call('plugins','start','whatsapp');
27
+ assert.equal(JSON.parse(await call('whatsapp','doctor','--json')).data.connected,true);
28
+ const first=JSON.parse(await call('whatsapp','inbox')).data;
29
+ assert.equal(first.messages.length,1);
30
+ const text=path.join(await fs.realpath(mind),'literal $(no execution) message.txt');await fs.writeFile(text,'Synthetic registry test');
31
+ const send=JSON.parse(await call('whatsapp','send','--to','+15551230000','--text-file',text,'--idempotency-key','registry:qa')).data;
32
+ assert.equal(send.state,'accepted');
33
+ const duplicate=JSON.parse(await call('whatsapp','send','--to','+15551230000','--text-file',text,'--idempotency-key','registry:qa')).data;assert.equal(duplicate.providerMessageId,send.providerMessageId);
34
+ await call('plugins','stop','whatsapp');await call('plugins','start','whatsapp');
35
+ assert.equal(JSON.parse(await call('whatsapp','operation','--idempotency-key','registry:qa')).data.providerMessageId,send.providerMessageId);
36
+ assert.equal(JSON.parse(await call('whatsapp','inbox')).data.nextCursor,first.nextCursor);
37
+ const probe=spawn(process.execPath,[bin,'--home',home,'probe'],{stdio:['ignore','pipe','pipe']});
38
+ await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('Probe never started')),20000);probe.stdout.once('data',()=>{clearTimeout(timeout);resolve();});});
39
+ const closed=new Promise(resolve=>probe.once('close',resolve));probe.kill('SIGTERM');assert.equal(await closed,130);
40
+ const clients=(await exec('docker',['ps','-a','--filter',`name=${record.project}-call-`,'--format','{{.Names}}'])).stdout.trim();assert.equal(clients,'');
41
+ assert.equal(JSON.parse(await call('whatsapp','doctor')).data.connected,true);
42
+ await call('plugins','uninstall','whatsapp');
43
+ const volumes=(await exec('docker',['volume','ls','--filter',`label=com.docker.compose.project=${record.project}`,'--format','{{.Name}}'])).stdout.trim();assert(volumes);
44
+ console.log('PASS: install without startup; registered CLI; synthetic account/read/send; literal file path; idempotency; stop/start persistence; data-preserving uninstall. No real provider calls.');
45
+ } catch(error) { if(record)console.error((await exec('docker',['compose','-p',record.project,'-f',record.compose,'logs','--tail','30']).catch(()=>({stdout:''}))).stdout);throw error; } finally {
46
+ if(record){await exec('docker',['compose','-p',record.project,'-f',record.compose,'down','--volumes']).catch(()=>{});await exec('docker',['image','rm',`${record.project}-whatsapp:${record.revision.slice(7,23)}`]).catch(()=>{});}
47
+ await fs.rm(root,{recursive:true,force:true});
48
+ }
@@ -0,0 +1,415 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ grammy:
12
+ specifier: ^1.39.3
13
+ version: 1.46.0
14
+ tsx:
15
+ specifier: ^4.21.0
16
+ version: 4.23.13
17
+ devDependencies:
18
+ '@types/node':
19
+ specifier: ^24.10.1
20
+ version: 24.13.3
21
+ typescript:
22
+ specifier: ^5.9.3
23
+ version: 5.9.3
24
+
25
+ packages:
26
+
27
+ '@esbuild/aix-ppc64@0.28.2':
28
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
29
+ engines: {node: '>=18'}
30
+ cpu: [ppc64]
31
+ os: [aix]
32
+
33
+ '@esbuild/android-arm64@0.28.2':
34
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
35
+ engines: {node: '>=18'}
36
+ cpu: [arm64]
37
+ os: [android]
38
+
39
+ '@esbuild/android-arm@0.28.2':
40
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
41
+ engines: {node: '>=18'}
42
+ cpu: [arm]
43
+ os: [android]
44
+
45
+ '@esbuild/android-x64@0.28.2':
46
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
47
+ engines: {node: '>=18'}
48
+ cpu: [x64]
49
+ os: [android]
50
+
51
+ '@esbuild/darwin-arm64@0.28.2':
52
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
53
+ engines: {node: '>=18'}
54
+ cpu: [arm64]
55
+ os: [darwin]
56
+
57
+ '@esbuild/darwin-x64@0.28.2':
58
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
59
+ engines: {node: '>=18'}
60
+ cpu: [x64]
61
+ os: [darwin]
62
+
63
+ '@esbuild/freebsd-arm64@0.28.2':
64
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
65
+ engines: {node: '>=18'}
66
+ cpu: [arm64]
67
+ os: [freebsd]
68
+
69
+ '@esbuild/freebsd-x64@0.28.2':
70
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
71
+ engines: {node: '>=18'}
72
+ cpu: [x64]
73
+ os: [freebsd]
74
+
75
+ '@esbuild/linux-arm64@0.28.2':
76
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
77
+ engines: {node: '>=18'}
78
+ cpu: [arm64]
79
+ os: [linux]
80
+
81
+ '@esbuild/linux-arm@0.28.2':
82
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
83
+ engines: {node: '>=18'}
84
+ cpu: [arm]
85
+ os: [linux]
86
+
87
+ '@esbuild/linux-ia32@0.28.2':
88
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
89
+ engines: {node: '>=18'}
90
+ cpu: [ia32]
91
+ os: [linux]
92
+
93
+ '@esbuild/linux-loong64@0.28.2':
94
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
95
+ engines: {node: '>=18'}
96
+ cpu: [loong64]
97
+ os: [linux]
98
+
99
+ '@esbuild/linux-mips64el@0.28.2':
100
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
101
+ engines: {node: '>=18'}
102
+ cpu: [mips64el]
103
+ os: [linux]
104
+
105
+ '@esbuild/linux-ppc64@0.28.2':
106
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
107
+ engines: {node: '>=18'}
108
+ cpu: [ppc64]
109
+ os: [linux]
110
+
111
+ '@esbuild/linux-riscv64@0.28.2':
112
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
113
+ engines: {node: '>=18'}
114
+ cpu: [riscv64]
115
+ os: [linux]
116
+
117
+ '@esbuild/linux-s390x@0.28.2':
118
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
119
+ engines: {node: '>=18'}
120
+ cpu: [s390x]
121
+ os: [linux]
122
+
123
+ '@esbuild/linux-x64@0.28.2':
124
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
125
+ engines: {node: '>=18'}
126
+ cpu: [x64]
127
+ os: [linux]
128
+
129
+ '@esbuild/netbsd-arm64@0.28.2':
130
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
131
+ engines: {node: '>=18'}
132
+ cpu: [arm64]
133
+ os: [netbsd]
134
+
135
+ '@esbuild/netbsd-x64@0.28.2':
136
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
137
+ engines: {node: '>=18'}
138
+ cpu: [x64]
139
+ os: [netbsd]
140
+
141
+ '@esbuild/openbsd-arm64@0.28.2':
142
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
143
+ engines: {node: '>=18'}
144
+ cpu: [arm64]
145
+ os: [openbsd]
146
+
147
+ '@esbuild/openbsd-x64@0.28.2':
148
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
149
+ engines: {node: '>=18'}
150
+ cpu: [x64]
151
+ os: [openbsd]
152
+
153
+ '@esbuild/openharmony-arm64@0.28.2':
154
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
155
+ engines: {node: '>=18'}
156
+ cpu: [arm64]
157
+ os: [openharmony]
158
+
159
+ '@esbuild/sunos-x64@0.28.2':
160
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
161
+ engines: {node: '>=18'}
162
+ cpu: [x64]
163
+ os: [sunos]
164
+
165
+ '@esbuild/win32-arm64@0.28.2':
166
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
167
+ engines: {node: '>=18'}
168
+ cpu: [arm64]
169
+ os: [win32]
170
+
171
+ '@esbuild/win32-ia32@0.28.2':
172
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
173
+ engines: {node: '>=18'}
174
+ cpu: [ia32]
175
+ os: [win32]
176
+
177
+ '@esbuild/win32-x64@0.28.2':
178
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
179
+ engines: {node: '>=18'}
180
+ cpu: [x64]
181
+ os: [win32]
182
+
183
+ '@grammyjs/types@5.0.0':
184
+ resolution: {integrity: sha512-iq1Qrq1iPKkB8yAa0qSuIURMZOCuqTY5pWy5gHpCeL1oQ+GPadGhw/cDTVE8waJwuCzacUzuIjRv1sESvk7u7A==}
185
+
186
+ '@types/node@24.13.3':
187
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
188
+
189
+ abort-controller@3.0.0:
190
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
191
+ engines: {node: '>=6.5'}
192
+
193
+ debug@4.4.3:
194
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
195
+ engines: {node: '>=6.0'}
196
+ peerDependencies:
197
+ supports-color: '*'
198
+ peerDependenciesMeta:
199
+ supports-color:
200
+ optional: true
201
+
202
+ esbuild@0.28.2:
203
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
204
+ engines: {node: '>=18'}
205
+ hasBin: true
206
+
207
+ event-target-shim@5.0.1:
208
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
209
+ engines: {node: '>=6'}
210
+
211
+ fsevents@2.3.3:
212
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
213
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
214
+ os: [darwin]
215
+
216
+ grammy@1.46.0:
217
+ resolution: {integrity: sha512-/8Qw+iisrUdOMk+p2mjEHouMm/BBdBEN1DHh16wiTpRUZkxDG3PxexdjCvR+wvK3LWPdrEvnQbdrwpU954sPhg==}
218
+ engines: {node: ^12.20.0 || >=14.13.1}
219
+
220
+ ms@2.1.3:
221
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
222
+
223
+ node-fetch@2.7.0:
224
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
225
+ engines: {node: 4.x || >=6.0.0}
226
+ peerDependencies:
227
+ encoding: ^0.1.0
228
+ peerDependenciesMeta:
229
+ encoding:
230
+ optional: true
231
+
232
+ tr46@0.0.3:
233
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
234
+
235
+ tsx@4.23.13:
236
+ resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==}
237
+ engines: {node: '>=18.0.0'}
238
+ hasBin: true
239
+
240
+ typescript@5.9.3:
241
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
242
+ engines: {node: '>=14.17'}
243
+ hasBin: true
244
+
245
+ undici-types@7.18.2:
246
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
247
+
248
+ webidl-conversions@3.0.1:
249
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
250
+
251
+ whatwg-url@5.0.0:
252
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
253
+
254
+ snapshots:
255
+
256
+ '@esbuild/aix-ppc64@0.28.2':
257
+ optional: true
258
+
259
+ '@esbuild/android-arm64@0.28.2':
260
+ optional: true
261
+
262
+ '@esbuild/android-arm@0.28.2':
263
+ optional: true
264
+
265
+ '@esbuild/android-x64@0.28.2':
266
+ optional: true
267
+
268
+ '@esbuild/darwin-arm64@0.28.2':
269
+ optional: true
270
+
271
+ '@esbuild/darwin-x64@0.28.2':
272
+ optional: true
273
+
274
+ '@esbuild/freebsd-arm64@0.28.2':
275
+ optional: true
276
+
277
+ '@esbuild/freebsd-x64@0.28.2':
278
+ optional: true
279
+
280
+ '@esbuild/linux-arm64@0.28.2':
281
+ optional: true
282
+
283
+ '@esbuild/linux-arm@0.28.2':
284
+ optional: true
285
+
286
+ '@esbuild/linux-ia32@0.28.2':
287
+ optional: true
288
+
289
+ '@esbuild/linux-loong64@0.28.2':
290
+ optional: true
291
+
292
+ '@esbuild/linux-mips64el@0.28.2':
293
+ optional: true
294
+
295
+ '@esbuild/linux-ppc64@0.28.2':
296
+ optional: true
297
+
298
+ '@esbuild/linux-riscv64@0.28.2':
299
+ optional: true
300
+
301
+ '@esbuild/linux-s390x@0.28.2':
302
+ optional: true
303
+
304
+ '@esbuild/linux-x64@0.28.2':
305
+ optional: true
306
+
307
+ '@esbuild/netbsd-arm64@0.28.2':
308
+ optional: true
309
+
310
+ '@esbuild/netbsd-x64@0.28.2':
311
+ optional: true
312
+
313
+ '@esbuild/openbsd-arm64@0.28.2':
314
+ optional: true
315
+
316
+ '@esbuild/openbsd-x64@0.28.2':
317
+ optional: true
318
+
319
+ '@esbuild/openharmony-arm64@0.28.2':
320
+ optional: true
321
+
322
+ '@esbuild/sunos-x64@0.28.2':
323
+ optional: true
324
+
325
+ '@esbuild/win32-arm64@0.28.2':
326
+ optional: true
327
+
328
+ '@esbuild/win32-ia32@0.28.2':
329
+ optional: true
330
+
331
+ '@esbuild/win32-x64@0.28.2':
332
+ optional: true
333
+
334
+ '@grammyjs/types@5.0.0': {}
335
+
336
+ '@types/node@24.13.3':
337
+ dependencies:
338
+ undici-types: 7.18.2
339
+
340
+ abort-controller@3.0.0:
341
+ dependencies:
342
+ event-target-shim: 5.0.1
343
+
344
+ debug@4.4.3:
345
+ dependencies:
346
+ ms: 2.1.3
347
+
348
+ esbuild@0.28.2:
349
+ optionalDependencies:
350
+ '@esbuild/aix-ppc64': 0.28.2
351
+ '@esbuild/android-arm': 0.28.2
352
+ '@esbuild/android-arm64': 0.28.2
353
+ '@esbuild/android-x64': 0.28.2
354
+ '@esbuild/darwin-arm64': 0.28.2
355
+ '@esbuild/darwin-x64': 0.28.2
356
+ '@esbuild/freebsd-arm64': 0.28.2
357
+ '@esbuild/freebsd-x64': 0.28.2
358
+ '@esbuild/linux-arm': 0.28.2
359
+ '@esbuild/linux-arm64': 0.28.2
360
+ '@esbuild/linux-ia32': 0.28.2
361
+ '@esbuild/linux-loong64': 0.28.2
362
+ '@esbuild/linux-mips64el': 0.28.2
363
+ '@esbuild/linux-ppc64': 0.28.2
364
+ '@esbuild/linux-riscv64': 0.28.2
365
+ '@esbuild/linux-s390x': 0.28.2
366
+ '@esbuild/linux-x64': 0.28.2
367
+ '@esbuild/netbsd-arm64': 0.28.2
368
+ '@esbuild/netbsd-x64': 0.28.2
369
+ '@esbuild/openbsd-arm64': 0.28.2
370
+ '@esbuild/openbsd-x64': 0.28.2
371
+ '@esbuild/openharmony-arm64': 0.28.2
372
+ '@esbuild/sunos-x64': 0.28.2
373
+ '@esbuild/win32-arm64': 0.28.2
374
+ '@esbuild/win32-ia32': 0.28.2
375
+ '@esbuild/win32-x64': 0.28.2
376
+
377
+ event-target-shim@5.0.1: {}
378
+
379
+ fsevents@2.3.3:
380
+ optional: true
381
+
382
+ grammy@1.46.0:
383
+ dependencies:
384
+ '@grammyjs/types': 5.0.0
385
+ abort-controller: 3.0.0
386
+ debug: 4.4.3
387
+ node-fetch: 2.7.0
388
+ transitivePeerDependencies:
389
+ - encoding
390
+ - supports-color
391
+
392
+ ms@2.1.3: {}
393
+
394
+ node-fetch@2.7.0:
395
+ dependencies:
396
+ whatwg-url: 5.0.0
397
+
398
+ tr46@0.0.3: {}
399
+
400
+ tsx@4.23.13:
401
+ dependencies:
402
+ esbuild: 0.28.2
403
+ optionalDependencies:
404
+ fsevents: 2.3.3
405
+
406
+ typescript@5.9.3: {}
407
+
408
+ undici-types@7.18.2: {}
409
+
410
+ webidl-conversions@3.0.1: {}
411
+
412
+ whatwg-url@5.0.0:
413
+ dependencies:
414
+ tr46: 0.0.3
415
+ webidl-conversions: 3.0.1
@@ -0,0 +1,11 @@
1
+ import { RunStore } from '../src/runs.js'
2
+
3
+ // Called only after the deployment's kernel writer lock is held. No worker
4
+ // from a previous container can still own this deployment; PIDs may be reused.
5
+ export const recoverInterruptedRuns = async (controlDir: string) => {
6
+ const store = new RunStore(controlDir)
7
+ for (const run of await store.list()) {
8
+ if (run.status === 'running')
9
+ await store.patch(run.id, { status: 'failed', endedAt: new Date().toISOString() })
10
+ }
11
+ }
package/docker/run.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { readFileSync, closeSync } from 'node:fs'
2
+ import { spawn } from 'node:child_process'
3
+ import { executorEnvironment, executorInvocation } from '../src/executor.js'
4
+ import { parseEnv } from 'node:util'
5
+ import { writeFile, rm } from 'node:fs/promises'
6
+ import { loadConfig } from '../src/config.js'
7
+ import { recoverInterruptedRuns } from './recovery.js'
8
+ import { createRelay } from '../src/index.js'
9
+ import { packageVersion } from '../src/version.js'
10
+
11
+ // Provider errors can contain credential-bearing URLs; never dump error causes.
12
+ process.on('uncaughtException', (error) => {
13
+ console.error(error.name + ': ' + error.message.replace(/bot\d+:[A-Za-z0-9_-]+/g, 'bot[redacted]'))
14
+ process.exit(1)
15
+ })
16
+
17
+ // Read and close the root-opened descriptor before any executor can start.
18
+ // Secrets never enter the initial process environment or a readable mount.
19
+ let privateEnv: NodeJS.ProcessEnv = {}
20
+ try { privateEnv = parseEnv(readFileSync(3, 'utf8')) } catch (error) {
21
+ if ((error as NodeJS.ErrnoException).code !== 'EINVAL' && (error as NodeJS.ErrnoException).code !== 'EBADF') throw error
22
+ } finally { try { closeSync(3) } catch {} }
23
+ for (const [key, value] of Object.entries(privateEnv)) {
24
+ if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY'].includes(key)) process.env[key] = value
25
+ }
26
+ privateEnv = {}
27
+ const [command = 'start', ...args] = process.argv.slice(2)
28
+ process.argv = [process.argv[0], '', ...args]
29
+ if (['start', 'smoke'].includes(command)) await recoverInterruptedRuns(loadConfig().controlDir)
30
+ if (command === 'start') {
31
+ const relay = createRelay(loadConfig())
32
+ const heartbeat = '/state/control/heartbeat.json'
33
+ await rm(heartbeat, { force: true })
34
+ const timer = setInterval(() => {
35
+ if (relay.bot.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: true, version: packageVersion }), { mode: 0o600 })
36
+ }, 5000)
37
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { clearInterval(timer); void relay.stop() })
38
+ try { await relay.start() } finally { clearInterval(timer); await rm(heartbeat, { force: true }) }
39
+ } else if (command === 'smoke') {
40
+ await import('../scripts/smoke.js')
41
+ } else if (command === 'exec') {
42
+ if (!args.length) throw new Error('exec requires a command')
43
+ const invocation = executorInvocation(args[0], args.slice(1))
44
+ const child = spawn(invocation.command, invocation.args, { stdio: 'inherit', env: executorEnvironment() })
45
+ child.on('error', () => { console.error('Command could not start'); process.exitCode = 1 })
46
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => child.kill(signal))
47
+ child.on('exit', (code) => { process.exitCode = code ?? 1 })
48
+ } else if (command === 'setup') {
49
+ await (await import('../src/setup.js')).runCli()
50
+ } else if (['owner', 'source', 'message', 'react'].includes(command)) {
51
+ await import(`../src/${command === 'source' ? 'source-cli' : command}.js`)
52
+ } else throw new Error('Use start, smoke, exec, setup, owner, source, message or react')